client/find_directories.py (135 lines of code) (raw):

# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. import itertools import logging import sys from pathlib import Path from typing import Callable, List, NamedTuple, Optional CONFIGURATION_FILE: str = ".pyre_configuration" LOCAL_CONFIGURATION_FILE: str = ".pyre_configuration.local" BINARY_NAME: str = "pyre.bin" CLIENT_NAME: str = "pyre-client" LOG_DIRECTORY: str = ".pyre" LOG: logging.Logger = logging.getLogger(__name__) def _find_parent_directory_containing( base: Path, target: str, predicate: Callable[[Path], bool], stop_search_after: Optional[int], ) -> Optional[Path]: resolved_base = base.resolve(strict=True) # Using `itertools.chain` to avoid expanding `resolve_base.parents` eagerly for i, candidate_directory in enumerate( itertools.chain([resolved_base], resolved_base.parents) ): candidate_path = candidate_directory / target try: if predicate(candidate_path): return candidate_directory except PermissionError: # We might not have sufficient permission to read the file/directory. # In that case, pretend the file doesn't exist. pass if stop_search_after is not None: if i >= stop_search_after: return None return None def find_parent_directory_containing_file( base: Path, target: str, stop_search_after: Optional[int] = None, ) -> Optional[Path]: """ Walk directories upwards from `base`, until the root directory is reached. At each step, check if the `target` file exist, and return the closest such directory if found. Return None if the search is unsuccessful. We stop searching after checking `stop_search_after` parent directories of `base` if provided; this is mainly for testing. """ def is_file(path: Path) -> bool: return path.is_file() return _find_parent_directory_containing( base, target, predicate=is_file, stop_search_after=stop_search_after, ) def find_outermost_directory_containing_file( base: Path, target: str, stop_search_after: Optional[int], ) -> Optional[Path]: """ Walk directories upwards from `base`, until the root directory is reached. At each step, check if the `target` file exist, and return the farthest such directory if found. Return None if the search is unsuccessful. We stop searching after checking `stop_search_after` parent directories of `base` if provided; this is mainly for testing. """ result: Optional[Path] = None resolved_base = base.resolve(strict=True) # Using `itertools.chain` to avoid expanding `resolve_base.parents` eagerly for i, candidate_directory in enumerate( itertools.chain([resolved_base], resolved_base.parents) ): candidate_path = candidate_directory / target try: if candidate_path.is_file(): result = candidate_directory except PermissionError: # We might not have sufficient permission to read the file/directory. # In that case, pretend the file doesn't exist. pass if stop_search_after is not None: if i >= stop_search_after: break return result def find_global_root(base: Path) -> Optional[Path]: """Pyre always runs from the directory containing the nearest .pyre_configuration, if one exists.""" return find_parent_directory_containing_file(base, CONFIGURATION_FILE) def get_relative_local_root( global_root: Path, local_root: Optional[Path] ) -> Optional[str]: if local_root is None: return None else: try: return str(local_root.relative_to(global_root)) except ValueError: # This happens when `local_root` is not prefixed by `global_root` return None class FoundRoot(NamedTuple): global_root: Path local_root: Optional[Path] = None def find_global_and_local_root(base: Path) -> Optional[FoundRoot]: """ Walk directories upwards from `base` and try to find both the global and local pyre configurations. Return `None` if no global configuration is found. If a global configuration exists but no local configuration is found below it, return the path to the global configuration. If both global and local exist, return them as a pair. """ found_global_root = find_parent_directory_containing_file(base, CONFIGURATION_FILE) if found_global_root is None: return None found_local_root = find_parent_directory_containing_file( base, LOCAL_CONFIGURATION_FILE ) if found_local_root is None: return FoundRoot(found_global_root) # If the global configuration root is deeper than local configuration, ignore local. if found_local_root in found_global_root.parents: return FoundRoot(found_global_root) else: return FoundRoot(found_global_root, found_local_root) def find_parent_directory_containing_directory( base: Path, target: str, stop_search_after: Optional[int] = None, ) -> Optional[Path]: """ Walk directories upwards from base, until the root directory is reached. At each step, check if the target directory exist, and return it if found. Return None if the search is unsuccessful. We stop searching after checking `stop_search_after` parent directories of `base` if provided; this is mainly for testing. """ def is_directory(path: Path) -> bool: return path.is_dir() return _find_parent_directory_containing( base, target, predicate=is_directory, stop_search_after=stop_search_after, ) def find_typeshed() -> Optional[Path]: # Prefer the typeshed we bundled ourselves (if any) to the one # from the environment. install_root = Path(sys.prefix) bundled_typeshed = install_root / "lib/pyre_check/typeshed/" if bundled_typeshed.is_dir(): return bundled_typeshed LOG.debug("Could not find bundled typeshed. Try importing typeshed directly...") try: import typeshed # pyre-fixme: Can't find module import typeshed return Path(typeshed.typeshed) except ImportError: LOG.debug("`import typeshed` failed.") return None def find_typeshed_search_paths(typeshed_root: Path) -> List[Path]: """ Given the root of typeshed, find all subdirectories in it that can be used as search paths for Pyre. """ search_path = [] third_party_root = typeshed_root / "stubs" third_party_subdirectories = ( sorted(third_party_root.iterdir()) if third_party_root.is_dir() else [] ) for typeshed_subdirectory in itertools.chain( [typeshed_root / "stdlib"], third_party_subdirectories ): if typeshed_subdirectory.is_dir(): search_path.append(typeshed_subdirectory) return search_path def find_taint_models_directory() -> Optional[Path]: install_root = Path(sys.prefix) bundled_taint_models = install_root / "lib/pyre_check/taint/" if bundled_taint_models.is_dir(): return bundled_taint_models return None