from __future__ import annotations import pprint import re import sys import sysconfig from collections.abc import Callable from re import Pattern from typing import Any, Final from librt.internal import WriteBuffer, write_bool, write_str 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_old", "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", "follow_untyped_imports", "ignore_errors", "ignore_missing_imports", "implicit_optional", "implicit_reexport", "local_partial_types", "mypyc", "strict_concatenate", "strict_equality", "strict_equality_for_none", "strict_optional", "warn_no_return", "warn_return_any", "warn_unreachable", "warn_unused_ignores", } OPTIONS_AFFECTING_CACHE: Final = ( PER_MODULE_OPTIONS | { "platform", "bazel", "native_parser", "old_type_inference", "plugins", "disable_bytearray_promotion", "disable_memoryview_promotion", "strict_bytes", "fixed_format_cache", "untyped_calls_exclude", "enable_incomplete_feature", "install_types", } ) - {"debug_cache"} # OPTIONS_AFFECTING_CACHE without "platform", as a sorted tuple for fast iteration. # "platform" is handled separately in options_snapshot(). OPTIONS_AFFECTING_CACHE_NO_PLATFORM: Final = tuple(sorted(OPTIONS_AFFECTING_CACHE - {"platform"})) # Features that are currently (or were recently) incomplete/experimental TYPE_VAR_TUPLE: Final = "TypeVarTuple" UNPACK: Final = "Unpack" PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes" NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax" INLINE_TYPEDDICT: Final = "InlineTypedDict" TYPE_FORM: Final = "TypeForm" INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM)) COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX)) 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 # Typecheck modules without stubs or py.typed marker self.follow_untyped_imports = 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] = [] self.exclude_gitignore: bool = False # 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 # Report importing or using deprecated features as errors instead of notes. self.report_deprecated_as_note = False # Allow deprecated calls from function coming from modules/packages # in this list (each item effectively acts as a prefix match) self.deprecated_calls_exclude: list[str] = [] # 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_old = False # Allow flexible variable redefinition with an arbitrary type, in different # blocks and at different nesting levels 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 # Extend the logic of `strict_equality` to comparisons with `None`. self.strict_equality_for_none = False # If False, switch to pre-mypy-2.0 legacy behavior where bytearray and memoryview are # treated as subtypes of bytes self.strict_bytes = True # 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 self.junit_format: str = "global" # global|per_file # Caching and incremental checking options self.incremental = True self.cache_dir = defaults.CACHE_DIR self.sqlite_cache = True self.sqlite_num_shards = defaults.SQLITE_NUM_SHARDS self.fixed_format_cache = True 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 # This should only be set when running certain mypy tests. # Use this sparingly to avoid tests diverging from non-test behavior. self.test_env = False # -- experimental options -- self.num_workers: int = 0 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 # This is an internal-only flag to simplify migrating test output. self.reveal_verbose_types = 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 = True # If True, use the native parser (experimental) self.native_parser = 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 type inference algorithm. self.old_type_inference = False # Disable expression cache (for debugging). self.disable_expression_cache = False # Export line-level, limited, fine-grained dependency information in cache data # (undocumented feature). self.export_ref_info = False # Treat special methods as being implicitly positional-only. # Set to False when running stubtest. self.pos_only_special_methods = True self.disable_bytearray_promotion = True self.disable_memoryview_promotion = True # Sets custom output format self.output: str | None = None # Output html file for mypyc -a self.mypyc_annotation_file: str | None = None # Skip writing C output files, but perform all other steps of a build (allows # preserving manual tweaks to generated C file) self.mypyc_skip_c_generation = False def use_star_unpack(self) -> bool: return self.python_version >= (3, 11) or not self.reveal_verbose_types 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): 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 process_error_codes(self, *, error_callback: Callable[[str], Any]) -> None: """Process `--enable-error-code` and `--disable-error-code` flags.""" disabled_code_names = set(self.disable_error_code) enabled_code_names = set(self.enable_error_code) valid_error_code_names = set(error_codes.keys()) invalid_code_names_here = ( enabled_code_names | disabled_code_names ) - valid_error_code_names if invalid_code_names_here: error_callback(f"Invalid error code(s): {', '.join(sorted(invalid_code_names_here))}") self.disabled_error_codes |= {error_codes[code] for code in disabled_code_names} self.enabled_error_codes |= {error_codes[code] for code in enabled_code_names} # Enabling an error code always overrides disabling self.disabled_error_codes -= self.enabled_error_codes def process_incomplete_features( self, *, error_callback: Callable[[str], Any], warning_callback: Callable[[str], Any] ) -> None: # Validate incomplete features. for feature in self.enable_incomplete_feature: if feature not in INCOMPLETE_FEATURES | COMPLETE_FEATURES: error_callback(f"Unknown incomplete feature: {feature}") if feature in COMPLETE_FEATURES: warning_callback(f"Warning: {feature} is already enabled by default") def process_strict_bytes(self) -> None: # Sync `--strict-bytes` and `--disable-{bytearray,memoryview}-promotion` if self.strict_bytes: # backwards compatibility self.disable_bytearray_promotion = True self.disable_memoryview_promotion = True else: self.disable_bytearray_promotion = False self.disable_memoryview_promotion = False 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) -> tuple[str, list[object]]: """Return (platform, [values...]) for options that affect the cache. The list contains values for OPTIONS_AFFECTING_CACHE_NO_PLATFORM in sorted attribute name order. Keys are omitted since the cache is invalidated when the mypy version changes, and keys are constant on any specific mypy version. """ result: list[object] = [] for opt in OPTIONS_AFFECTING_CACHE_NO_PLATFORM: val = getattr(self, opt) if opt in ("disabled_error_codes", "enabled_error_codes"): val = sorted([code.code for code in val]) result.append(val) return self.platform, result def dep_import_options(self) -> bytes: """Return opaque bytes with options that can affect dependent modules as well. The value can be compared for equality to detect changed options. """ buf = WriteBuffer() write_bool(buf, self.ignore_missing_imports) write_str(buf, self.follow_imports) write_bool(buf, self.follow_imports_for_stubs) return buf.getvalue()