diff --git a/mypy/api.py b/mypy/api.py new file mode 100644 index 000000000000..dee0a9ad1c88 --- /dev/null +++ b/mypy/api.py @@ -0,0 +1,242 @@ +# This module contains an API for using mypy as a module inside another (host) application, +# rather than from the command line or as a separate process. Examples of such host applications +# are IDE's and command line build- or (pre)compilation-tools. Once this API is stable and +# flexible enough, it deserves consideration to make the command line version of mypy just another +# host application. +# +# Being an interface, this module attempts to be thin, stable and self-explanatory. Since the guts +# of mypy are bound to change, it doesn't depend too much upon them. Rather it presents an +# external view of mypy, using a very limited number of domain bound, hence presumably relativey +# stable concepts. +# +# More specific, it exports: +# +# - A singleton object named type_validator, representing mypy to the host application. This +# object features two methods: +# +# - Method set_options, which allows setting those options of mypy which are meant for +# production use. Its argument list makes clear which they are and which defaults they +# have. +# +# - Method validate, which receives a list of strings, denoting source file paths of top +# level modules. These top level modules and the modules they import are checked +# recursively. Method validate returns a polymorphic list containing objects whose class +# derives from ValidationMessage. +# +# - Class ValidationMessage. This class facilitates the use of its subclasses in a +# polymorphic, but still typed, list. In most situations there's no need to use this +# baseclass directly. Objects of its subclasses represent messages that the validator wants +# to deliver to the user via the host. Such objects are in binary form, granting the host +# the freedom to convert them to any suitable format. +# +# - Class ValidationRemark is a subclass of ValidationMessage. It is the baseclass of all +# ValidationMessage's that do not represent an error. +# +# - Class ValidationError is also a subclass of ValidationMessage. It is the baseclass +# of all errors encountered during a valiation run. In most situations there's no need +# to use this baseclass directly. There is no separate warning class, rather objects of +# some subclasses of ValidationError can have an severity attribute. +# +# - Class StaticTypingError is a subclass of ValidationError. Its instances represent +# static typing inconsistencies found by mypy. Finding objects of this class is what +# mypy is about. +# +# - Class CompilationError is a subclass of ValidationError. Its instances represent +# any other problem encountered by mypy. Currently this category isn't subdivided +# any further. Its derived classes, which are currently unused, suggest that in the +# future such a subdivision may be useful. +# +# - Class SyntaxError is a subclass of CompilationError. Its instances represent +# syntax errors encountered by mypy in the code under scrutiny. It is there for +# future use. While in the end the Python interpreter will catch any syntax +# errors, if mypy already knows about them, a second parse is redundant and can +# be avoided. +# +# - Class InternalError is a subclass of CompilationError. Its instances represent +# errors due to malfunction of mypy itself. It is there for future use. + +import sys +from typing import List +from mypy import build, defaults, errors, options + + +# Any message produced by the validator. These messages are structured objects rather than +# strings. In this way each tool that hosts mypy can represent them in its own suitable text +# format. +class ValidationMessage: + # Default values of inherited attributes are set here. However overriding them happens + # explicitly in derived classes, rather than via this constructor. + def __init__(self): + self.identifier = None + self.description = None + + +# Any ValidationMessage that isn't a ValidationError +class ValidationRemark(ValidationMessage): + pass + + +# Any error produced by the validator. Having a common (abstract) base class allows the use of +# polymorphic, yet typed, error lists. +class ValidationError(ValidationMessage): + pass + + +# Any typing inconsistency in the code that is being validated +class StaticTypingError(ValidationError): + def __init__(self, error_info: errors.ErrorInfo) -> None: + ValidationError.__init__(self) # Make sure attributes exist, init explicitly below + self._error_info = error_info # Private + + self.description = self._error_info.message + self.import_context = self._error_info.import_ctx + self.file_name = self._error_info.file.replace('\\', '/') + self.class_name = self._error_info.type + self.function_name = self._error_info.function_or_member + self.line_nr = self._error_info.line + self.severity = self._error_info.severity + + +# Any other error occuring during validation that isn't a StaticTypingError +class CompilationError(ValidationError): + def __init__(self, compile_error: errors.CompileError) -> None: + ValidationError.__init__(self) # Make sure attributes exist, init explicitly below + + self._compile_error = compile_error # Private + self._static_typing_errors = [] # type: List[StaticTypingError] # Private + + # BEGIN tempory hack. + + # Since a CompileError doesn't contain raw error info, we'll just reconstruct it from text + # for now. The alternative, adding an attribute containing raw error info to CompileError, + # is avoided for the moment, since such a temporary solution might easily lead even more + # code becoming dependent on this vulnerable part of the design. + # + # The long term solution is probably a thorough revision of the raise_error / CompileError + # mechanism, but currently the focus is on getting the external view of this API right. + # Behind such a stable facade all kinds of future reconstruction activities may be + # endeavoured, limiting their impact on hosts. + + if self._compile_error.messages[0] .startswith('mypy:'): + self.description = self._compile_error.messages[0] + else: + self.description = 'Unspecified compilation error' + + for formatted_message in self._compile_error.messages: + if ': error:' in formatted_message: + file_name, line_nr, severity, description = formatted_message.split(':', 4) + self._static_typing_errors.append(StaticTypingError(errors.ErrorInfo( + import_ctx = None, + file = file_name, + typ = None, + function_or_member = None, + line = int(line_nr), + severity = severity, + message = description, + blocker = None, + only_once = None + ))) + + # END temporary hack. + + +# For future use. +class SyntaxError(CompilationError): + pass + + +# For future use. +class InternalError(CompilationError): + pass + + +# Private class, only a singleton instance is exported. +class _TypeValidator: + def __init__(self) -> None: + self.set_options() + + def set_options( + self, + + # Target Python version. + python_version = defaults.PYTHON3_VERSION, + + # Target platform. + platform = sys.platform, + + # Only import types from .pyi files, not from .py files . + silent_imports = False, + + # Disallow calling untyped functions from typed ones. + disallow_untyped_calls = False, + + # Disallow defining untyped (or incompletely typed) functions. + disallow_untyped_defs = False, + + # Type check unannotated functions. + check_untyped_defs = False, + + # Also check typeshed for missing annotations. + warn_incomplete_stub = False, + + # Warn about casting an expression to its inferred type. + warn_redundant_casts = False, + + # Warn about unused '# type: ignore' comments. + warn_unused_ignores = False + ) -> None: + params = locals() .items() + self._options = options.Options() + for param in params: + setattr(self._options, *param) + + # A call to validate denotes one validation run on a list of top level modules and the + # hierarchy of modules they import recursively. This method returns one polymorphic list + # of ValidationMessage's, enabling easy future expansion and refinement of the message + # hierarchy. + def validate(self, source_paths: str) -> List[ValidationMessage]: + compilation_error = None + + try: + build_result = build.build( + [build.BuildSource(source_path, None, None) for source_path in source_paths], + self._options + ) + static_typing_errors = [ + StaticTypingError(error_info) + for error_info in build_result.manager.errors.error_info + ] + except errors.CompileError as compile_error: + compilation_error = CompilationError(compile_error) + static_typing_errors = compilation_error._static_typing_errors + + validation_messages = [] # type: List[ValidationMessage] + + # Sort StaticTypingError's on file_name, line_nr, error_message respectively, then remove + # duplicates. + old_error = None # type: StaticTypingError + for index, error in enumerate(sorted( + static_typing_errors, + key = lambda error: (error.file_name, error.line_nr, error.description) + )): + if(index and(not( + error._error_info.only_once and + error.file_name == old_error.file_name and + error.line_nr == old_error.line_nr and + error.description == old_error.description + ))): + validation_messages.append(error) + old_error = error + + # Append instance of CompilationError if it's there. + if compilation_error: + validation_messages.append(compilation_error) + + return validation_messages + + +# Singleton instance, exported to represent the mypy static type validator in any 3rd party tools +# that it's part of. +type_validator = _TypeValidator() + +# (Module revision timestamp: y16m09d09 h9m55s00 GMT)