| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900 |
- """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
- if approved_stub_package_exists(components[0]):
- if len(components) == 1 or (
- self.find_module(components[0])
- is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
- ):
- return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
- if approved_stub_package_exists(".".join(components[:2])):
- 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))
|