"""Low-level infrastructure to find modules. This builds on fscache.py; find_sources.py builds on top of this. """ from __future__ import annotations import ast import collections import functools import os import re import subprocess import sys from enum import Enum, unique from mypy.errors import CompileError if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib from typing import Dict, Final, List, NamedTuple, Optional, Tuple, Union from typing_extensions import TypeAlias as _TypeAlias from mypy import pyinfo from mypy.fscache import FileSystemCache from mypy.nodes import MypyFile from mypy.options import Options from mypy.stubinfo import approved_stub_package_exists # Paths to be searched in find_module(). class SearchPaths(NamedTuple): python_path: tuple[str, ...] # where user code is found mypy_path: tuple[str, ...] # from $MYPYPATH or config variable package_path: tuple[str, ...] # from get_site_packages_dirs() typeshed_path: tuple[str, ...] # paths in typeshed # Package dirs are a two-tuple of path to search and whether to verify the module OnePackageDir = Tuple[str, bool] PackageDirs = List[OnePackageDir] # Minimum and maximum Python versions for modules in stdlib as (major, minor) StdlibVersions: _TypeAlias = Dict[str, Tuple[Tuple[int, int], Optional[Tuple[int, int]]]] PYTHON_EXTENSIONS: Final = [".pyi", ".py"] # TODO: Consider adding more reasons here? # E.g. if we deduce a module would likely be found if the user were # to set the --namespace-packages flag. @unique class ModuleNotFoundReason(Enum): # The module was not found: we found neither stubs nor a plausible code # implementation (with or without a py.typed file). NOT_FOUND = 0 # The implementation for this module plausibly exists (e.g. we # found a matching folder or *.py file), but either the parent package # did not contain a py.typed file or we were unable to find a # corresponding *-stubs package. FOUND_WITHOUT_TYPE_HINTS = 1 # The module was not found in the current working directory, but # was able to be found in the parent directory. WRONG_WORKING_DIRECTORY = 2 # Stub PyPI package (typically types-pkgname) known to exist but not installed. APPROVED_STUBS_NOT_INSTALLED = 3 def error_message_templates(self, daemon: bool) -> tuple[str, list[str]]: doc_link = "See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports" if self is ModuleNotFoundReason.NOT_FOUND: msg = 'Cannot find implementation or library stub for module named "{module}"' notes = [doc_link] elif self is ModuleNotFoundReason.WRONG_WORKING_DIRECTORY: msg = 'Cannot find implementation or library stub for module named "{module}"' notes = [ "You may be running mypy in a subpackage, " "mypy should be run on the package root" ] elif self is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: msg = ( 'Skipping analyzing "{module}": module is installed, but missing library stubs ' "or py.typed marker" ) notes = [doc_link] elif self is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED: msg = 'Library stubs not installed for "{module}"' notes = ['Hint: "python3 -m pip install {stub_dist}"'] if not daemon: notes.append( '(or run "mypy --install-types" to install all missing stub packages)' ) notes.append(doc_link) else: assert False return msg, notes # If we found the module, returns the path to the module as a str. # Otherwise, returns the reason why the module wasn't found. ModuleSearchResult = Union[str, ModuleNotFoundReason] class BuildSource: """A single source file.""" def __init__( self, path: str | None, module: str | None, text: str | None = None, base_dir: str | None = None, followed: bool = False, ) -> None: self.path = path # File where it's found (e.g. 'xxx/yyy/foo/bar.py') self.module = module or "__main__" # Module name (e.g. 'foo.bar') self.text = text # Source code, if initially supplied, else None self.base_dir = base_dir # Directory where the package is rooted (e.g. 'xxx/yyy') self.followed = followed # Was this found by following imports? def __repr__(self) -> str: return ( "BuildSource(path={!r}, module={!r}, has_text={}, base_dir={!r}, followed={})".format( self.path, self.module, self.text is not None, self.base_dir, self.followed ) ) class BuildSourceSet: """Helper to efficiently test a file's membership in a set of build sources.""" def __init__(self, sources: list[BuildSource]) -> None: self.source_text_present = False self.source_modules: dict[str, str] = {} self.source_paths: set[str] = set() for source in sources: if source.text is not None: self.source_text_present = True if source.path: self.source_paths.add(source.path) if source.module: self.source_modules[source.module] = source.path or "" def is_source(self, file: MypyFile) -> bool: return ( (file.path and file.path in self.source_paths) or file._fullname in self.source_modules or self.source_text_present ) class FindModuleCache: """Module finder with integrated cache. Module locations and some intermediate results are cached internally and can be cleared with the clear() method. All file system accesses are performed through a FileSystemCache, which is not ever cleared by this class. If necessary it must be cleared by client code. """ def __init__( self, search_paths: SearchPaths, fscache: FileSystemCache | None, options: Options | None, stdlib_py_versions: StdlibVersions | None = None, source_set: BuildSourceSet | None = None, ) -> None: self.search_paths = search_paths self.source_set = source_set self.fscache = fscache or FileSystemCache() # Cache for get_toplevel_possibilities: # search_paths -> (toplevel_id -> list(package_dirs)) self.initial_components: dict[tuple[str, ...], dict[str, list[str]]] = {} # Cache find_module: id -> result self.results: dict[str, ModuleSearchResult] = {} self.ns_ancestors: dict[str, str] = {} self.options = options custom_typeshed_dir = None if options: custom_typeshed_dir = options.custom_typeshed_dir self.stdlib_py_versions = stdlib_py_versions or load_stdlib_py_versions( custom_typeshed_dir ) def clear(self) -> None: self.results.clear() self.initial_components.clear() self.ns_ancestors.clear() def find_module_via_source_set(self, id: str) -> ModuleSearchResult | None: """Fast path to find modules by looking through the input sources This is only used when --fast-module-lookup is passed on the command line.""" if not self.source_set: return None p = self.source_set.source_modules.get(id, None) if p and self.fscache.isfile(p): # We need to make sure we still have __init__.py all the way up # otherwise we might have false positives compared to slow path # in case of deletion of init files, which is covered by some tests. # TODO: are there some combination of flags in which this check should be skipped? d = os.path.dirname(p) for _ in range(id.count(".")): if not any( self.fscache.isfile(os.path.join(d, "__init__" + x)) for x in PYTHON_EXTENSIONS ): return None d = os.path.dirname(d) return p idx = id.rfind(".") if idx != -1: # When we're looking for foo.bar.baz and can't find a matching module # in the source set, look up for a foo.bar module. parent = self.find_module_via_source_set(id[:idx]) if parent is None or not isinstance(parent, str): return None basename, ext = os.path.splitext(parent) if not any(parent.endswith("__init__" + x) for x in PYTHON_EXTENSIONS) and ( ext in PYTHON_EXTENSIONS and not self.fscache.isdir(basename) ): # If we do find such a *module* (and crucially, we don't want a package, # hence the filtering out of __init__ files, and checking for the presence # of a folder with a matching name), then we can be pretty confident that # 'baz' will either be a top-level variable in foo.bar, or will not exist. # # Either way, spelunking in other search paths for another 'foo.bar.baz' # module should be avoided because: # 1. in the unlikely event that one were found, it's highly likely that # it would be unrelated to the source being typechecked and therefore # more likely to lead to erroneous results # 2. as described in _find_module, in some cases the search itself could # potentially waste significant amounts of time return ModuleNotFoundReason.NOT_FOUND return None def find_lib_path_dirs(self, id: str, lib_path: tuple[str, ...]) -> PackageDirs: """Find which elements of a lib_path have the directory a module needs to exist. This is run for the python_path, mypy_path, and typeshed_path search paths. """ components = id.split(".") dir_chain = os.sep.join(components[:-1]) # e.g., 'foo/bar' dirs = [] for pathitem in self.get_toplevel_possibilities(lib_path, components[0]): # e.g., '/usr/lib/python3.4/foo/bar' dir = os.path.normpath(os.path.join(pathitem, dir_chain)) if self.fscache.isdir(dir): dirs.append((dir, True)) return dirs def get_toplevel_possibilities(self, lib_path: tuple[str, ...], id: str) -> list[str]: """Find which elements of lib_path could contain a particular top-level module. In practice, almost all modules can be routed to the correct entry in lib_path by looking at just the first component of the module name. We take advantage of this by enumerating the contents of all of the directories on the lib_path and building a map of which entries in the lib_path could contain each potential top-level module that appears. """ if lib_path in self.initial_components: return self.initial_components[lib_path].get(id, []) # Enumerate all the files in the directories on lib_path and produce the map components: dict[str, list[str]] = {} for dir in lib_path: try: contents = self.fscache.listdir(dir) except OSError: contents = [] # False positives are fine for correctness here, since we will check # precisely later, so we only look at the root of every filename without # any concern for the exact details. for name in contents: name = os.path.splitext(name)[0] components.setdefault(name, []).append(dir) self.initial_components[lib_path] = components return components.get(id, []) def find_module(self, id: str, *, fast_path: bool = False) -> ModuleSearchResult: """Return the path of the module source file or why it wasn't found. If fast_path is True, prioritize performance over generating detailed error descriptions. """ if id not in self.results: top_level = id.partition(".")[0] use_typeshed = True if id in self.stdlib_py_versions: use_typeshed = self._typeshed_has_version(id) elif top_level in self.stdlib_py_versions: use_typeshed = self._typeshed_has_version(top_level) self.results[id] = self._find_module(id, use_typeshed) if ( not (fast_path or (self.options is not None and self.options.fast_module_lookup)) and self.results[id] is ModuleNotFoundReason.NOT_FOUND and self._can_find_module_in_parent_dir(id) ): self.results[id] = ModuleNotFoundReason.WRONG_WORKING_DIRECTORY return self.results[id] def _typeshed_has_version(self, module: str) -> bool: if not self.options: return True version = typeshed_py_version(self.options) min_version, max_version = self.stdlib_py_versions[module] return version >= min_version and (max_version is None or version <= max_version) def _find_module_non_stub_helper( self, components: list[str], pkg_dir: str ) -> OnePackageDir | ModuleNotFoundReason: plausible_match = False dir_path = pkg_dir for index, component in enumerate(components): dir_path = os.path.join(dir_path, component) if self.fscache.isfile(os.path.join(dir_path, "py.typed")): return os.path.join(pkg_dir, *components[:-1]), index == 0 elif not plausible_match and ( self.fscache.isdir(dir_path) or self.fscache.isfile(dir_path + ".py") ): plausible_match = True # If this is not a directory then we can't traverse further into it if not self.fscache.isdir(dir_path): break for i in range(len(components), 0, -1): if approved_stub_package_exists(".".join(components[:i])): return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED if plausible_match: return ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS else: return ModuleNotFoundReason.NOT_FOUND def _update_ns_ancestors(self, components: list[str], match: tuple[str, bool]) -> None: path, verify = match for i in range(1, len(components)): pkg_id = ".".join(components[:-i]) if pkg_id not in self.ns_ancestors and self.fscache.isdir(path): self.ns_ancestors[pkg_id] = path path = os.path.dirname(path) def _can_find_module_in_parent_dir(self, id: str) -> bool: """Test if a module can be found by checking the parent directories of the current working directory. """ working_dir = os.getcwd() parent_search = FindModuleCache( SearchPaths((), (), (), ()), self.fscache, self.options, stdlib_py_versions=self.stdlib_py_versions, ) while any(is_init_file(file) for file in os.listdir(working_dir)): working_dir = os.path.dirname(working_dir) parent_search.search_paths = SearchPaths((working_dir,), (), (), ()) if not isinstance(parent_search._find_module(id, False), ModuleNotFoundReason): return True return False def _find_module(self, id: str, use_typeshed: bool) -> ModuleSearchResult: fscache = self.fscache # Fast path for any modules in the current source set. # This is particularly important when there are a large number of search # paths which share the first (few) component(s) due to the use of namespace # packages, for instance: # foo/ # company/ # __init__.py # foo/ # bar/ # company/ # __init__.py # bar/ # baz/ # company/ # __init__.py # baz/ # # mypy gets [foo/company/foo, bar/company/bar, baz/company/baz, ...] as input # and computes [foo, bar, baz, ...] as the module search path. # # This would result in O(n) search for every import of company.*, leading to # O(n**2) behavior in load_graph as such imports are unsurprisingly present # at least once, and usually many more times than that, in each and every file # being parsed. # # Thankfully, such cases are efficiently handled by looking up the module path # via BuildSourceSet. p = ( self.find_module_via_source_set(id) if (self.options is not None and self.options.fast_module_lookup) else None ) if p: return p # If we're looking for a module like 'foo.bar.baz', it's likely that most of the # many elements of lib_path don't even have a subdirectory 'foo/bar'. Discover # that only once and cache it for when we look for modules like 'foo.bar.blah' # that will require the same subdirectory. components = id.split(".") dir_chain = os.sep.join(components[:-1]) # e.g., 'foo/bar' # We have two sets of folders so that we collect *all* stubs folders and # put them in the front of the search path third_party_inline_dirs: PackageDirs = [] third_party_stubs_dirs: PackageDirs = [] found_possible_third_party_missing_type_hints = False need_installed_stubs = False # Third-party stub/typed packages for pkg_dir in self.search_paths.package_path: stub_name = components[0] + "-stubs" stub_dir = os.path.join(pkg_dir, stub_name) if fscache.isdir(stub_dir) and self._is_compatible_stub_package(stub_dir): stub_typed_file = os.path.join(stub_dir, "py.typed") stub_components = [stub_name] + components[1:] path = os.path.join(pkg_dir, *stub_components[:-1]) if fscache.isdir(path): if fscache.isfile(stub_typed_file): # Stub packages can have a py.typed file, which must include # 'partial\n' to make the package partial # Partial here means that mypy should look at the runtime # package if installed. if fscache.read(stub_typed_file).decode().strip() == "partial": runtime_path = os.path.join(pkg_dir, dir_chain) third_party_inline_dirs.append((runtime_path, True)) # if the package is partial, we don't verify the module, as # the partial stub package may not have a __init__.pyi third_party_stubs_dirs.append((path, False)) else: # handle the edge case where people put a py.typed file # in a stub package, but it isn't partial third_party_stubs_dirs.append((path, True)) else: third_party_stubs_dirs.append((path, True)) non_stub_match = self._find_module_non_stub_helper(components, pkg_dir) if isinstance(non_stub_match, ModuleNotFoundReason): if non_stub_match is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: found_possible_third_party_missing_type_hints = True elif non_stub_match is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED: need_installed_stubs = True else: third_party_inline_dirs.append(non_stub_match) self._update_ns_ancestors(components, non_stub_match) if self.options and self.options.use_builtins_fixtures: # Everything should be in fixtures. third_party_inline_dirs.clear() third_party_stubs_dirs.clear() found_possible_third_party_missing_type_hints = False python_mypy_path = self.search_paths.mypy_path + self.search_paths.python_path candidate_base_dirs = self.find_lib_path_dirs(id, python_mypy_path) if use_typeshed: # Search for stdlib stubs in typeshed before installed # stubs to avoid picking up backports (dataclasses, for # example) when the library is included in stdlib. candidate_base_dirs += self.find_lib_path_dirs(id, self.search_paths.typeshed_path) candidate_base_dirs += third_party_stubs_dirs + third_party_inline_dirs # If we're looking for a module like 'foo.bar.baz', then candidate_base_dirs now # contains just the subdirectories 'foo/bar' that actually exist under the # elements of lib_path. This is probably much shorter than lib_path itself. # Now just look for 'baz.pyi', 'baz/__init__.py', etc., inside those directories. seplast = os.sep + components[-1] # so e.g. '/baz' sepinit = os.sep + "__init__" near_misses = [] # Collect near misses for namespace mode (see below). for base_dir, verify in candidate_base_dirs: base_path = base_dir + seplast # so e.g. '/usr/lib/python3.4/foo/bar/baz' has_init = False dir_prefix = base_dir for _ in range(len(components) - 1): dir_prefix = os.path.dirname(dir_prefix) # Prefer package over module, i.e. baz/__init__.py* over baz.py*. for extension in PYTHON_EXTENSIONS: path = base_path + sepinit + extension path_stubs = base_path + "-stubs" + sepinit + extension if fscache.isfile_case(path, dir_prefix): has_init = True if verify and not verify_module(fscache, id, path, dir_prefix): near_misses.append((path, dir_prefix)) continue return path elif fscache.isfile_case(path_stubs, dir_prefix): if verify and not verify_module(fscache, id, path_stubs, dir_prefix): near_misses.append((path_stubs, dir_prefix)) continue return path_stubs # In namespace mode, register a potential namespace package if self.options and self.options.namespace_packages: if ( not has_init and fscache.exists_case(base_path, dir_prefix) and not fscache.isfile_case(base_path, dir_prefix) ): near_misses.append((base_path, dir_prefix)) # No package, look for module. for extension in PYTHON_EXTENSIONS: path = base_path + extension if fscache.isfile_case(path, dir_prefix): if verify and not verify_module(fscache, id, path, dir_prefix): near_misses.append((path, dir_prefix)) continue return path # In namespace mode, re-check those entries that had 'verify'. # Assume search path entries xxx, yyy and zzz, and we're # looking for foo.bar.baz. Suppose near_misses has: # # - xxx/foo/bar/baz.py # - yyy/foo/bar/baz/__init__.py # - zzz/foo/bar/baz.pyi # # If any of the foo directories has __init__.py[i], it wins. # Else, we look for foo/bar/__init__.py[i], etc. If there are # none, the first hit wins. Note that this does not take into # account whether the lowest-level module is a file (baz.py), # a package (baz/__init__.py), or a stub file (baz.pyi) -- for # these the first one encountered along the search path wins. # # The helper function highest_init_level() returns an int that # indicates the highest level at which a __init__.py[i] file # is found; if no __init__ was found it returns 0, if we find # only foo/bar/__init__.py it returns 1, and if we have # foo/__init__.py it returns 2 (regardless of what's in # foo/bar). It doesn't look higher than that. if self.options and self.options.namespace_packages and near_misses: levels = [ highest_init_level(fscache, id, path, dir_prefix) for path, dir_prefix in near_misses ] index = levels.index(max(levels)) return near_misses[index][0] # Finally, we may be asked to produce an ancestor for an # installed package with a py.typed marker that is a # subpackage of a namespace package. We only fess up to these # if we would otherwise return "not found". ancestor = self.ns_ancestors.get(id) if ancestor is not None: return ancestor if need_installed_stubs: return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED elif found_possible_third_party_missing_type_hints: return ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS else: return ModuleNotFoundReason.NOT_FOUND def _is_compatible_stub_package(self, stub_dir: str) -> bool: """Does a stub package support the target Python version? Stub packages may contain a metadata file which specifies whether the stubs are compatible with Python 2 and 3. """ metadata_fnam = os.path.join(stub_dir, "METADATA.toml") if not os.path.isfile(metadata_fnam): return True with open(metadata_fnam, "rb") as f: metadata = tomllib.load(f) return bool(metadata.get("python3", True)) def find_modules_recursive(self, module: str) -> list[BuildSource]: module_path = self.find_module(module) if isinstance(module_path, ModuleNotFoundReason): return [] sources = [BuildSource(module_path, module, None)] package_path = None if is_init_file(module_path): package_path = os.path.dirname(module_path) elif self.fscache.isdir(module_path): package_path = module_path if package_path is None: return sources # This logic closely mirrors that in find_sources. One small but important difference is # that we do not sort names with keyfunc. The recursive call to find_modules_recursive # calls find_module, which will handle the preference between packages, pyi and py. # Another difference is it doesn't handle nested search paths / package roots. seen: set[str] = set() names = sorted(self.fscache.listdir(package_path)) for name in names: # Skip certain names altogether if name in ("__pycache__", "site-packages", "node_modules") or name.startswith("."): continue subpath = os.path.join(package_path, name) if self.options and matches_exclude( subpath, self.options.exclude, self.fscache, self.options.verbosity >= 2 ): continue if self.fscache.isdir(subpath): # Only recurse into packages if (self.options and self.options.namespace_packages) or ( self.fscache.isfile(os.path.join(subpath, "__init__.py")) or self.fscache.isfile(os.path.join(subpath, "__init__.pyi")) ): seen.add(name) sources.extend(self.find_modules_recursive(module + "." + name)) else: stem, suffix = os.path.splitext(name) if stem == "__init__": continue if stem not in seen and "." not in stem and suffix in PYTHON_EXTENSIONS: # (If we sorted names by keyfunc) we could probably just make the BuildSource # ourselves, but this ensures compatibility with find_module / the cache seen.add(stem) sources.extend(self.find_modules_recursive(module + "." + stem)) return sources def matches_exclude( subpath: str, excludes: list[str], fscache: FileSystemCache, verbose: bool ) -> bool: if not excludes: return False subpath_str = os.path.relpath(subpath).replace(os.sep, "/") if fscache.isdir(subpath): subpath_str += "/" for exclude in excludes: if re.search(exclude, subpath_str): if verbose: print( f"TRACE: Excluding {subpath_str} (matches pattern {exclude})", file=sys.stderr ) return True return False def is_init_file(path: str) -> bool: return os.path.basename(path) in ("__init__.py", "__init__.pyi") def verify_module(fscache: FileSystemCache, id: str, path: str, prefix: str) -> bool: """Check that all packages containing id have a __init__ file.""" if is_init_file(path): path = os.path.dirname(path) for i in range(id.count(".")): path = os.path.dirname(path) if not any( fscache.isfile_case(os.path.join(path, f"__init__{extension}"), prefix) for extension in PYTHON_EXTENSIONS ): return False return True def highest_init_level(fscache: FileSystemCache, id: str, path: str, prefix: str) -> int: """Compute the highest level where an __init__ file is found.""" if is_init_file(path): path = os.path.dirname(path) level = 0 for i in range(id.count(".")): path = os.path.dirname(path) if any( fscache.isfile_case(os.path.join(path, f"__init__{extension}"), prefix) for extension in PYTHON_EXTENSIONS ): level = i + 1 return level def mypy_path() -> list[str]: path_env = os.getenv("MYPYPATH") if not path_env: return [] return path_env.split(os.pathsep) def default_lib_path( data_dir: str, pyversion: tuple[int, int], custom_typeshed_dir: str | None ) -> list[str]: """Return default standard library search paths.""" path: list[str] = [] if custom_typeshed_dir: typeshed_dir = os.path.join(custom_typeshed_dir, "stdlib") mypy_extensions_dir = os.path.join(custom_typeshed_dir, "stubs", "mypy-extensions") versions_file = os.path.join(typeshed_dir, "VERSIONS") if not os.path.isdir(typeshed_dir) or not os.path.isfile(versions_file): print( "error: --custom-typeshed-dir does not point to a valid typeshed ({})".format( custom_typeshed_dir ) ) sys.exit(2) else: auto = os.path.join(data_dir, "stubs-auto") if os.path.isdir(auto): data_dir = auto typeshed_dir = os.path.join(data_dir, "typeshed", "stdlib") mypy_extensions_dir = os.path.join(data_dir, "typeshed", "stubs", "mypy-extensions") path.append(typeshed_dir) # Get mypy-extensions stubs from typeshed, since we treat it as an # "internal" library, similar to typing and typing-extensions. path.append(mypy_extensions_dir) # Add fallback path that can be used if we have a broken installation. if sys.platform != "win32": path.append("/usr/local/lib/mypy") if not path: print( "Could not resolve typeshed subdirectories. Your mypy install is broken.\n" "Python executable is located at {}.\nMypy located at {}".format( sys.executable, data_dir ), file=sys.stderr, ) sys.exit(1) return path @functools.lru_cache(maxsize=None) def get_search_dirs(python_executable: str | None) -> tuple[list[str], list[str]]: """Find package directories for given python. This runs a subprocess call, which generates a list of the directories in sys.path. To avoid repeatedly calling a subprocess (which can be slow!) we lru_cache the results. """ if python_executable is None: return ([], []) elif python_executable == sys.executable: # Use running Python's package dirs sys_path, site_packages = pyinfo.getsearchdirs() else: # Use subprocess to get the package directory of given Python # executable env = {**dict(os.environ), "PYTHONSAFEPATH": "1"} try: sys_path, site_packages = ast.literal_eval( subprocess.check_output( [python_executable, pyinfo.__file__, "getsearchdirs"], env=env, stderr=subprocess.PIPE, ).decode() ) except subprocess.CalledProcessError as err: print(err.stderr) print(err.stdout) raise except OSError as err: reason = os.strerror(err.errno) raise CompileError( [f"mypy: Invalid python executable '{python_executable}': {reason}"] ) from err return sys_path, site_packages def compute_search_paths( sources: list[BuildSource], options: Options, data_dir: str, alt_lib_path: str | None = None ) -> SearchPaths: """Compute the search paths as specified in PEP 561. There are the following 4 members created: - User code (from `sources`) - MYPYPATH (set either via config or environment variable) - installed package directories (which will later be split into stub-only and inline) - typeshed """ # Determine the default module search path. lib_path = collections.deque( default_lib_path( data_dir, options.python_version, custom_typeshed_dir=options.custom_typeshed_dir ) ) if options.use_builtins_fixtures: # Use stub builtins (to speed up test cases and to make them easier to # debug). This is a test-only feature, so assume our files are laid out # as in the source tree. # We also need to allow overriding where to look for it. Argh. root_dir = os.getenv("MYPY_TEST_PREFIX", None) if not root_dir: root_dir = os.path.dirname(os.path.dirname(__file__)) lib_path.appendleft(os.path.join(root_dir, "test-data", "unit", "lib-stub")) # alt_lib_path is used by some tests to bypass the normal lib_path mechanics. # If we don't have one, grab directories of source files. python_path: list[str] = [] if not alt_lib_path: for source in sources: # Include directory of the program file in the module search path. if source.base_dir: dir = source.base_dir if dir not in python_path: python_path.append(dir) # Do this even if running as a file, for sanity (mainly because with # multiple builds, there could be a mix of files/modules, so its easier # to just define the semantics that we always add the current director # to the lib_path # TODO: Don't do this in some cases; for motivation see see # https://github.com/python/mypy/issues/4195#issuecomment-341915031 if options.bazel: dir = "." else: dir = os.getcwd() if dir not in lib_path: python_path.insert(0, dir) # Start with a MYPYPATH environment variable at the front of the mypy_path, if defined. mypypath = mypy_path() # Add a config-defined mypy path. mypypath.extend(options.mypy_path) # If provided, insert the caller-supplied extra module path to the # beginning (highest priority) of the search path. if alt_lib_path: mypypath.insert(0, alt_lib_path) sys_path, site_packages = get_search_dirs(options.python_executable) # We only use site packages for this check for site in site_packages: assert site not in lib_path if ( site in mypypath or any(p.startswith(site + os.path.sep) for p in mypypath) or (os.path.altsep and any(p.startswith(site + os.path.altsep) for p in mypypath)) ): print(f"{site} is in the MYPYPATH. Please remove it.", file=sys.stderr) print( "See https://mypy.readthedocs.io/en/stable/running_mypy.html" "#how-mypy-handles-imports for more info", file=sys.stderr, ) sys.exit(1) return SearchPaths( python_path=tuple(reversed(python_path)), mypy_path=tuple(mypypath), package_path=tuple(sys_path + site_packages), typeshed_path=tuple(lib_path), ) def load_stdlib_py_versions(custom_typeshed_dir: str | None) -> StdlibVersions: """Return dict with minimum and maximum Python versions of stdlib modules. The contents look like {..., 'secrets': ((3, 6), None), 'symbol': ((2, 7), (3, 9)), ...} None means there is no maximum version. """ typeshed_dir = custom_typeshed_dir or os.path.join(os.path.dirname(__file__), "typeshed") stdlib_dir = os.path.join(typeshed_dir, "stdlib") result = {} versions_path = os.path.join(stdlib_dir, "VERSIONS") assert os.path.isfile(versions_path), (custom_typeshed_dir, versions_path, __file__) with open(versions_path) as f: for line in f: line = line.split("#")[0].strip() if line == "": continue module, version_range = line.split(":") versions = version_range.split("-") min_version = parse_version(versions[0]) max_version = ( parse_version(versions[1]) if len(versions) >= 2 and versions[1].strip() else None ) result[module] = min_version, max_version return result def parse_version(version: str) -> tuple[int, int]: major, minor = version.strip().split(".") return int(major), int(minor) def typeshed_py_version(options: Options) -> tuple[int, int]: """Return Python version used for checking whether module supports typeshed.""" # Typeshed no longer covers Python 3.x versions before 3.7, so 3.7 is # the earliest we can support. return max(options.python_version, (3, 7))