from __future__ import annotations import pprint import re import sys import sysconfig from typing import Any, Callable, Final, Mapping, Pattern from mypy import defaults from mypy.errorcodes import ErrorCode, error_codes from mypy.util import get_class_descriptors, replace_object_state class BuildType: STANDARD: Final = 0 MODULE: Final = 1 PROGRAM_TEXT: Final = 2 PER_MODULE_OPTIONS: Final = { # Please keep this list sorted "allow_redefinition", "allow_untyped_globals", "always_false", "always_true", "check_untyped_defs", "debug_cache", "disable_error_code", "disabled_error_codes", "disallow_any_decorated", "disallow_any_explicit", "disallow_any_expr", "disallow_any_generics", "disallow_any_unimported", "disallow_incomplete_defs", "disallow_subclassing_any", "disallow_untyped_calls", "disallow_untyped_decorators", "disallow_untyped_defs", "enable_error_code", "enabled_error_codes", "extra_checks", "follow_imports_for_stubs", "follow_imports", "ignore_errors", "ignore_missing_imports", "implicit_optional", "implicit_reexport", "local_partial_types", "mypyc", "strict_concatenate", "strict_equality", "strict_optional", "warn_no_return", "warn_return_any", "warn_unreachable", "warn_unused_ignores", } OPTIONS_AFFECTING_CACHE: Final = ( PER_MODULE_OPTIONS | { "platform", "bazel", "new_type_inference", "plugins", "disable_bytearray_promotion", "disable_memoryview_promotion", } ) - {"debug_cache"} # Features that are currently incomplete/experimental TYPE_VAR_TUPLE: Final = "TypeVarTuple" UNPACK: Final = "Unpack" INCOMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK)) class Options: """Options collected from flags.""" def __init__(self) -> None: # Cache for clone_for_module() self._per_module_cache: dict[str, Options] | None = None # -- build options -- self.build_type = BuildType.STANDARD self.python_version: tuple[int, int] = sys.version_info[:2] # The executable used to search for PEP 561 packages. If this is None, # then mypy does not search for PEP 561 packages. self.python_executable: str | None = sys.executable # When cross compiling to emscripten, we need to rely on MACHDEP because # sys.platform is the host build platform, not emscripten. MACHDEP = sysconfig.get_config_var("MACHDEP") if MACHDEP == "emscripten": self.platform = MACHDEP else: self.platform = sys.platform self.custom_typing_module: str | None = None self.custom_typeshed_dir: str | None = None # The abspath() version of the above, we compute it once as an optimization. self.abs_custom_typeshed_dir: str | None = None self.mypy_path: list[str] = [] self.report_dirs: dict[str, str] = {} # Show errors in PEP 561 packages/site-packages modules self.no_silence_site_packages = False self.no_site_packages = False self.ignore_missing_imports = False # Is ignore_missing_imports set in a per-module section self.ignore_missing_imports_per_module = False self.follow_imports = "normal" # normal|silent|skip|error # Whether to respect the follow_imports setting even for stub files. # Intended to be used for disabling specific stubs. self.follow_imports_for_stubs = False # PEP 420 namespace packages # This allows definitions of packages without __init__.py and allows packages to span # multiple directories. This flag affects both import discovery and the association of # input files/modules/packages to the relevant file and fully qualified module name. self.namespace_packages = True # Use current directory and MYPYPATH to determine fully qualified module names of files # passed by automatically considering their subdirectories as packages. This is only # relevant if namespace packages are enabled, since otherwise examining __init__.py's is # sufficient to determine module names for files. As a possible alternative, add a single # top-level __init__.py to your packages. self.explicit_package_bases = False # File names, directory names or subpaths to avoid checking self.exclude: list[str] = [] # disallow_any options self.disallow_any_generics = False self.disallow_any_unimported = False self.disallow_any_expr = False self.disallow_any_decorated = False self.disallow_any_explicit = False # Disallow calling untyped functions from typed ones self.disallow_untyped_calls = False # Always allow untyped calls for function coming from modules/packages # in this list (each item effectively acts as a prefix match) self.untyped_calls_exclude: list[str] = [] # Disallow defining untyped (or incompletely typed) functions self.disallow_untyped_defs = False # Disallow defining incompletely typed functions self.disallow_incomplete_defs = False # Type check unannotated functions self.check_untyped_defs = False # Disallow decorating typed functions with untyped decorators self.disallow_untyped_decorators = False # Disallow subclassing values of type 'Any' self.disallow_subclassing_any = False # Also check typeshed for missing annotations self.warn_incomplete_stub = False # Warn about casting an expression to its inferred type self.warn_redundant_casts = False # Warn about falling off the end of a function returning non-None self.warn_no_return = True # Warn about returning objects of type Any when the function is # declared with a precise type self.warn_return_any = False # Warn about unused '# type: ignore' comments self.warn_unused_ignores = False # Warn about unused '[mypy-]' or '[[tool.mypy.overrides]]' config sections self.warn_unused_configs = False # Files in which to ignore all non-fatal errors self.ignore_errors = False # Apply strict None checking self.strict_optional = True # Show "note: In function "foo":" messages. self.show_error_context = False # Use nicer output (when possible). self.color_output = True self.error_summary = True # Assume arguments with default values of None are Optional self.implicit_optional = False # Don't re-export names unless they are imported with `from ... as ...` self.implicit_reexport = True # Suppress toplevel errors caused by missing annotations self.allow_untyped_globals = False # Allow variable to be redefined with an arbitrary type in the same block # and the same nesting level as the initialization self.allow_redefinition = False # Prohibit equality, identity, and container checks for non-overlapping types. # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. self.strict_equality = False # Deprecated, use extra_checks instead. self.strict_concatenate = False # Enable additional checks that are technically correct but impractical. self.extra_checks = False # Report an error for any branches inferred to be unreachable as a result of # type analysis. self.warn_unreachable = False # Variable names considered True self.always_true: list[str] = [] # Variable names considered False self.always_false: list[str] = [] # Error codes to disable self.disable_error_code: list[str] = [] self.disabled_error_codes: set[ErrorCode] = set() # Error codes to enable self.enable_error_code: list[str] = [] self.enabled_error_codes: set[ErrorCode] = set() # Use script name instead of __main__ self.scripts_are_modules = False # Config file name self.config_file: str | None = None # A filename containing a JSON mapping from filenames to # mtime/size/hash arrays, used to avoid having to recalculate # source hashes as often. self.quickstart_file: str | None = None # A comma-separated list of files/directories for mypy to type check; # supports globbing self.files: list[str] | None = None # A list of packages for mypy to type check self.packages: list[str] | None = None # A list of modules for mypy to type check self.modules: list[str] | None = None # Write junit.xml to given file self.junit_xml: str | None = None # Caching and incremental checking options self.incremental = True self.cache_dir = defaults.CACHE_DIR self.sqlite_cache = False self.debug_cache = False self.skip_version_check = False self.skip_cache_mtime_checks = False self.fine_grained_incremental = False # Include fine-grained dependencies in written cache files self.cache_fine_grained = False # Read cache files in fine-grained incremental mode (cache must include dependencies) self.use_fine_grained_cache = False # Run tree.serialize() even if cache generation is disabled self.debug_serialize = False # Tune certain behaviors when being used as a front-end to mypyc. Set per-module # in modules being compiled. Not in the config file or command line. self.mypyc = False # An internal flag to modify some type-checking logic while # running inspections (e.g. don't expand function definitions). # Not in the config file or command line. self.inspections = False # Disable the memory optimization of freeing ASTs when # possible. This isn't exposed as a command line option # because it is intended for software integrating with # mypy. (Like mypyc.) self.preserve_asts = False # If True, function and class docstrings will be extracted and retained. # This isn't exposed as a command line option # because it is intended for software integrating with # mypy. (Like stubgen.) self.include_docstrings = False # Paths of user plugins self.plugins: list[str] = [] # Per-module options (raw) self.per_module_options: dict[str, dict[str, object]] = {} self._glob_options: list[tuple[str, Pattern[str]]] = [] self.unused_configs: set[str] = set() # -- development options -- self.verbosity = 0 # More verbose messages (for troubleshooting) self.pdb = False self.show_traceback = False self.raise_exceptions = False self.dump_type_stats = False self.dump_inference_stats = False self.dump_build_stats = False self.enable_incomplete_features = False # deprecated self.enable_incomplete_feature: list[str] = [] self.timing_stats: str | None = None self.line_checking_stats: str | None = None # -- test options -- # Stop after the semantic analysis phase self.semantic_analysis_only = False # Use stub builtins fixtures to speed up tests self.use_builtins_fixtures = False # -- experimental options -- self.shadow_file: list[list[str]] | None = None self.show_column_numbers: bool = False self.show_error_end: bool = False self.hide_error_codes = False self.show_error_code_links = False # Use soft word wrap and show trimmed source snippets with error location markers. self.pretty = False self.dump_graph = False self.dump_deps = False self.logical_deps = False # If True, partial types can't span a module top level and a function self.local_partial_types = False # Some behaviors are changed when using Bazel (https://bazel.build). self.bazel = False # If True, export inferred types for all expressions as BuildResult.types self.export_types = False # List of package roots -- directories under these are packages even # if they don't have __init__.py. self.package_root: list[str] = [] self.cache_map: dict[str, tuple[str, str]] = {} # Don't properly free objects on exit, just kill the current process. self.fast_exit = True # fast path for finding modules from source set self.fast_module_lookup = False # Allow empty function bodies even if it is not safe, used for testing only. self.allow_empty_bodies = False # Used to transform source code before parsing if not None # TODO: Make the type precise (AnyStr -> AnyStr) self.transform_source: Callable[[Any], Any] | None = None # Print full path to each file in the report. self.show_absolute_path: bool = False # Install missing stub packages if True self.install_types = False # Install missing stub packages in non-interactive mode (don't prompt for # confirmation, and don't show any errors) self.non_interactive = False # When we encounter errors that may cause many additional errors, # skip most errors after this many messages have been reported. # -1 means unlimited. self.many_errors_threshold = defaults.MANY_ERRORS_THRESHOLD # Enable new experimental type inference algorithm. self.new_type_inference = False # Disable recursive type aliases (currently experimental) self.disable_recursive_aliases = False # Deprecated reverse version of the above, do not use. self.enable_recursive_aliases = False # Export line-level, limited, fine-grained dependency information in cache data # (undocumented feature). self.export_ref_info = False self.disable_bytearray_promotion = False self.disable_memoryview_promotion = False self.force_uppercase_builtins = False self.force_union_syntax = False def use_lowercase_names(self) -> bool: if self.python_version >= (3, 9): return not self.force_uppercase_builtins return False def use_or_syntax(self) -> bool: if self.python_version >= (3, 10): return not self.force_union_syntax return False # To avoid breaking plugin compatibility, keep providing new_semantic_analyzer @property def new_semantic_analyzer(self) -> bool: return True def snapshot(self) -> dict[str, object]: """Produce a comparable snapshot of this Option""" # Under mypyc, we don't have a __dict__, so we need to do worse things. d = dict(getattr(self, "__dict__", ())) for k in get_class_descriptors(Options): if hasattr(self, k) and k != "new_semantic_analyzer": d[k] = getattr(self, k) # Remove private attributes from snapshot d = {k: v for k, v in d.items() if not k.startswith("_")} return d def __repr__(self) -> str: return f"Options({pprint.pformat(self.snapshot())})" def apply_changes(self, changes: dict[str, object]) -> Options: # Note: effects of this method *must* be idempotent. new_options = Options() # Under mypyc, we don't have a __dict__, so we need to do worse things. replace_object_state(new_options, self, copy_dict=True) for key, value in changes.items(): setattr(new_options, key, value) if changes.get("ignore_missing_imports"): # This is the only option for which a per-module and a global # option sometimes beheave differently. new_options.ignore_missing_imports_per_module = True # These two act as overrides, so apply them when cloning. # Similar to global codes enabling overrides disabling, so we start from latter. new_options.disabled_error_codes = self.disabled_error_codes.copy() new_options.enabled_error_codes = self.enabled_error_codes.copy() for code_str in new_options.disable_error_code: code = error_codes[code_str] new_options.disabled_error_codes.add(code) new_options.enabled_error_codes.discard(code) for code_str in new_options.enable_error_code: code = error_codes[code_str] new_options.enabled_error_codes.add(code) new_options.disabled_error_codes.discard(code) return new_options def compare_stable(self, other_snapshot: dict[str, object]) -> bool: """Compare options in a way that is stable for snapshot() -> apply_changes() roundtrip. This is needed because apply_changes() has non-trivial effects for some flags, so Options().apply_changes(options.snapshot()) may result in a (slightly) different object. """ return ( Options().apply_changes(self.snapshot()).snapshot() == Options().apply_changes(other_snapshot).snapshot() ) def build_per_module_cache(self) -> None: self._per_module_cache = {} # Config precedence is as follows: # 1. Concrete section names: foo.bar.baz # 2. "Unstructured" glob patterns: foo.*.baz, in the order # they appear in the file (last wins) # 3. "Well-structured" wildcard patterns: foo.bar.*, in specificity order. # Since structured configs inherit from structured configs above them in the hierarchy, # we need to process per-module configs in a careful order. # We have to process foo.* before foo.bar.* before foo.bar, # and we need to apply *.bar to foo.bar but not to foo.bar.*. # To do this, process all well-structured glob configs before non-glob configs and # exploit the fact that foo.* sorts earlier ASCIIbetically (unicodebetically?) # than foo.bar.*. # (A section being "processed last" results in its config "winning".) # Unstructured glob configs are stored and are all checked for each module. unstructured_glob_keys = [k for k in self.per_module_options.keys() if "*" in k[:-1]] structured_keys = [k for k in self.per_module_options.keys() if "*" not in k[:-1]] wildcards = sorted(k for k in structured_keys if k.endswith(".*")) concrete = [k for k in structured_keys if not k.endswith(".*")] for glob in unstructured_glob_keys: self._glob_options.append((glob, self.compile_glob(glob))) # We (for ease of implementation) treat unstructured glob # sections as used if any real modules use them or if any # concrete config sections use them. This means we need to # track which get used while constructing. self.unused_configs = set(unstructured_glob_keys) for key in wildcards + concrete: # Find what the options for this key would be, just based # on inheriting from parent configs. options = self.clone_for_module(key) # And then update it with its per-module options. self._per_module_cache[key] = options.apply_changes(self.per_module_options[key]) # Add the more structured sections into unused configs, since # they only count as used if actually used by a real module. self.unused_configs.update(structured_keys) def clone_for_module(self, module: str) -> Options: """Create an Options object that incorporates per-module options. NOTE: Once this method is called all Options objects should be considered read-only, else the caching might be incorrect. """ if self._per_module_cache is None: self.build_per_module_cache() assert self._per_module_cache is not None # If the module just directly has a config entry, use it. if module in self._per_module_cache: self.unused_configs.discard(module) return self._per_module_cache[module] # If not, search for glob paths at all the parents. So if we are looking for # options for foo.bar.baz, we search foo.bar.baz.*, foo.bar.*, foo.*, # in that order, looking for an entry. # This is technically quadratic in the length of the path, but module paths # don't actually get all that long. options = self path = module.split(".") for i in range(len(path), 0, -1): key = ".".join(path[:i] + ["*"]) if key in self._per_module_cache: self.unused_configs.discard(key) options = self._per_module_cache[key] break # OK and *now* we need to look for unstructured glob matches. # We only do this for concrete modules, not structured wildcards. if not module.endswith(".*"): for key, pattern in self._glob_options: if pattern.match(module): self.unused_configs.discard(key) options = options.apply_changes(self.per_module_options[key]) # We could update the cache to directly point to modules once # they have been looked up, but in testing this made things # slower and not faster, so we don't bother. return options def compile_glob(self, s: str) -> Pattern[str]: # Compile one of the glob patterns to a regex so that '.*' can # match *zero or more* module sections. This means we compile # '.*' into '(\..*)?'. parts = s.split(".") expr = re.escape(parts[0]) if parts[0] != "*" else ".*" for part in parts[1:]: expr += re.escape("." + part) if part != "*" else r"(\..*)?" return re.compile(expr + "\\Z") def select_options_affecting_cache(self) -> Mapping[str, object]: result: dict[str, object] = {} for opt in OPTIONS_AFFECTING_CACHE: val = getattr(self, opt) if opt in ("disabled_error_codes", "enabled_error_codes"): val = sorted([code.code for code in val]) result[opt] = val return result