| 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", |
| "old_type_inference", |
| "plugins", |
| "disable_bytearray_promotion", |
| "disable_memoryview_promotion", |
| } |
| ) - {"debug_cache"} |
| |
| # Features that are currently (or were recently) incomplete/experimental |
| TYPE_VAR_TUPLE: Final = "TypeVarTuple" |
| UNPACK: Final = "Unpack" |
| PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes" |
| INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES,)) |
| COMPLETE_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-<pattern>]' 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_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 |
| # Disable new experimental type inference algorithm. |
| self.old_type_inference = False |
| # Deprecated reverse version of the above, do not use. |
| self.new_type_inference = 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 |
| |
| def use_star_unpack(self) -> bool: |
| return self.python_version >= (3, 11) |
| |
| # 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 |