123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- """Finders try to find right section for passed module name"""
- import importlib.machinery
- import inspect
- import os
- import os.path
- import re
- import sys
- import sysconfig
- from abc import ABCMeta, abstractmethod
- from contextlib import contextmanager
- from fnmatch import fnmatch
- from functools import lru_cache
- from glob import glob
- from pathlib import Path
- from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type
- from isort import sections
- from isort.settings import KNOWN_SECTION_MAPPING, Config
- from isort.utils import exists_case_sensitive
- try:
- from pipreqs import pipreqs # type: ignore
- except ImportError:
- pipreqs = None
- try:
- from pip_api import parse_requirements # type: ignore
- except ImportError:
- parse_requirements = None
- try:
- from requirementslib import Pipfile # type: ignore
- except ImportError:
- Pipfile = None
- @contextmanager
- def chdir(path: str) -> Iterator[None]:
- """Context manager for changing dir and restoring previous workdir after exit."""
- curdir = os.getcwd()
- os.chdir(path)
- try:
- yield
- finally:
- os.chdir(curdir)
- class BaseFinder(metaclass=ABCMeta):
- def __init__(self, config: Config) -> None:
- self.config = config
- @abstractmethod
- def find(self, module_name: str) -> Optional[str]:
- raise NotImplementedError
- class ForcedSeparateFinder(BaseFinder):
- def find(self, module_name: str) -> Optional[str]:
- for forced_separate in self.config.forced_separate:
- # Ensure all forced_separate patterns will match to end of string
- path_glob = forced_separate
- if not forced_separate.endswith("*"):
- path_glob = "%s*" % forced_separate
- if fnmatch(module_name, path_glob) or fnmatch(module_name, "." + path_glob):
- return forced_separate
- return None
- class LocalFinder(BaseFinder):
- def find(self, module_name: str) -> Optional[str]:
- if module_name.startswith("."):
- return "LOCALFOLDER"
- return None
- class KnownPatternFinder(BaseFinder):
- def __init__(self, config: Config) -> None:
- super().__init__(config)
- self.known_patterns: List[Tuple[Pattern[str], str]] = []
- for placement in reversed(config.sections):
- known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower()
- config_key = f"known_{known_placement}"
- known_patterns = list(
- getattr(self.config, config_key, self.config.known_other.get(known_placement, []))
- )
- known_patterns = [
- pattern
- for known_pattern in known_patterns
- for pattern in self._parse_known_pattern(known_pattern)
- ]
- for known_pattern in known_patterns:
- regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$"
- self.known_patterns.append((re.compile(regexp), placement))
- def _parse_known_pattern(self, pattern: str) -> List[str]:
- """Expand pattern if identified as a directory and return found sub packages"""
- if pattern.endswith(os.path.sep):
- patterns = [
- filename
- for filename in os.listdir(os.path.join(self.config.directory, pattern))
- if os.path.isdir(os.path.join(self.config.directory, pattern, filename))
- ]
- else:
- patterns = [pattern]
- return patterns
- def find(self, module_name: str) -> Optional[str]:
- # Try to find most specific placement instruction match (if any)
- parts = module_name.split(".")
- module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1))
- for module_name_to_check in module_names_to_check:
- for pattern, placement in self.known_patterns:
- if pattern.match(module_name_to_check):
- return placement
- return None
- class PathFinder(BaseFinder):
- def __init__(self, config: Config, path: str = ".") -> None:
- super().__init__(config)
- # restore the original import path (i.e. not the path to bin/isort)
- root_dir = os.path.abspath(path)
- src_dir = f"{root_dir}/src"
- self.paths = [root_dir, src_dir]
- # virtual env
- self.virtual_env = self.config.virtual_env or os.environ.get("VIRTUAL_ENV")
- if self.virtual_env:
- self.virtual_env = os.path.realpath(self.virtual_env)
- self.virtual_env_src = ""
- if self.virtual_env:
- self.virtual_env_src = f"{self.virtual_env}/src/"
- for venv_path in glob(f"{self.virtual_env}/lib/python*/site-packages"):
- if venv_path not in self.paths:
- self.paths.append(venv_path)
- for nested_venv_path in glob(f"{self.virtual_env}/lib/python*/*/site-packages"):
- if nested_venv_path not in self.paths:
- self.paths.append(nested_venv_path)
- for venv_src_path in glob(f"{self.virtual_env}/src/*"):
- if os.path.isdir(venv_src_path):
- self.paths.append(venv_src_path)
- # conda
- self.conda_env = self.config.conda_env or os.environ.get("CONDA_PREFIX") or ""
- if self.conda_env:
- self.conda_env = os.path.realpath(self.conda_env)
- for conda_path in glob(f"{self.conda_env}/lib/python*/site-packages"):
- if conda_path not in self.paths:
- self.paths.append(conda_path)
- for nested_conda_path in glob(f"{self.conda_env}/lib/python*/*/site-packages"):
- if nested_conda_path not in self.paths:
- self.paths.append(nested_conda_path)
- # handle case-insensitive paths on windows
- self.stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()["stdlib"])
- if self.stdlib_lib_prefix not in self.paths:
- self.paths.append(self.stdlib_lib_prefix)
- # add system paths
- for system_path in sys.path[1:]:
- if system_path not in self.paths:
- self.paths.append(system_path)
- def find(self, module_name: str) -> Optional[str]:
- for prefix in self.paths:
- package_path = "/".join((prefix, module_name.split(".")[0]))
- path_obj = Path(package_path).resolve()
- is_module = (
- exists_case_sensitive(package_path + ".py")
- or any(
- exists_case_sensitive(package_path + ext_suffix)
- for ext_suffix in importlib.machinery.EXTENSION_SUFFIXES
- )
- or exists_case_sensitive(package_path + "/__init__.py")
- )
- is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path)
- if is_module or is_package:
- if (
- "site-packages" in prefix
- or "dist-packages" in prefix
- or (self.virtual_env and self.virtual_env_src in prefix)
- ):
- return sections.THIRDPARTY
- if os.path.normcase(prefix) == self.stdlib_lib_prefix:
- return sections.STDLIB
- if self.conda_env and self.conda_env in prefix:
- return sections.THIRDPARTY
- for src_path in self.config.src_paths:
- if src_path in path_obj.parents and not self.config.is_skipped(path_obj):
- return sections.FIRSTPARTY
- if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix):
- return sections.STDLIB # pragma: no cover - edge case for one OS. Hard to test.
- return self.config.default_section
- return None
- class ReqsBaseFinder(BaseFinder):
- enabled = False
- def __init__(self, config: Config, path: str = ".") -> None:
- super().__init__(config)
- self.path = path
- if self.enabled:
- self.mapping = self._load_mapping()
- self.names = self._load_names()
- @abstractmethod
- def _get_names(self, path: str) -> Iterator[str]:
- raise NotImplementedError
- @abstractmethod
- def _get_files_from_dir(self, path: str) -> Iterator[str]:
- raise NotImplementedError
- @staticmethod
- def _load_mapping() -> Optional[Dict[str, str]]:
- """Return list of mappings `package_name -> module_name`
- Example:
- django-haystack -> haystack
- """
- if not pipreqs:
- return None
- path = os.path.dirname(inspect.getfile(pipreqs))
- path = os.path.join(path, "mapping")
- with open(path) as f:
- mappings: Dict[str, str] = {} # pypi_name: import_name
- for line in f:
- import_name, _, pypi_name = line.strip().partition(":")
- mappings[pypi_name] = import_name
- return mappings
- # return dict(tuple(line.strip().split(":")[::-1]) for line in f)
- def _load_names(self) -> List[str]:
- """Return list of thirdparty modules from requirements"""
- names = []
- for path in self._get_files():
- for name in self._get_names(path):
- names.append(self._normalize_name(name))
- return names
- @staticmethod
- def _get_parents(path: str) -> Iterator[str]:
- prev = ""
- while path != prev:
- prev = path
- yield path
- path = os.path.dirname(path)
- def _get_files(self) -> Iterator[str]:
- """Return paths to all requirements files"""
- path = os.path.abspath(self.path)
- if os.path.isfile(path):
- path = os.path.dirname(path)
- for path in self._get_parents(path):
- yield from self._get_files_from_dir(path)
- def _normalize_name(self, name: str) -> str:
- """Convert package name to module name
- Examples:
- Django -> django
- django-haystack -> django_haystack
- Flask-RESTFul -> flask_restful
- """
- if self.mapping:
- name = self.mapping.get(name.replace("-", "_"), name)
- return name.lower().replace("-", "_")
- def find(self, module_name: str) -> Optional[str]:
- # required lib not installed yet
- if not self.enabled:
- return None
- module_name, _sep, _submodules = module_name.partition(".")
- module_name = module_name.lower()
- if not module_name:
- return None
- for name in self.names:
- if module_name == name:
- return sections.THIRDPARTY
- return None
- class RequirementsFinder(ReqsBaseFinder):
- exts = (".txt", ".in")
- enabled = bool(parse_requirements)
- def _get_files_from_dir(self, path: str) -> Iterator[str]:
- """Return paths to requirements files from passed dir."""
- yield from self._get_files_from_dir_cached(path)
- @classmethod
- @lru_cache(maxsize=16)
- def _get_files_from_dir_cached(cls, path: str) -> List[str]:
- results = []
- for fname in os.listdir(path):
- if "requirements" not in fname:
- continue
- full_path = os.path.join(path, fname)
- # *requirements*/*.{txt,in}
- if os.path.isdir(full_path):
- for subfile_name in os.listdir(full_path):
- for ext in cls.exts:
- if subfile_name.endswith(ext):
- results.append(os.path.join(full_path, subfile_name))
- continue
- # *requirements*.{txt,in}
- if os.path.isfile(full_path):
- for ext in cls.exts:
- if fname.endswith(ext):
- results.append(full_path)
- break
- return results
- def _get_names(self, path: str) -> Iterator[str]:
- """Load required packages from path to requirements file"""
- yield from self._get_names_cached(path)
- @classmethod
- @lru_cache(maxsize=16)
- def _get_names_cached(cls, path: str) -> List[str]:
- result = []
- with chdir(os.path.dirname(path)):
- requirements = parse_requirements(path)
- for req in requirements.values():
- if req.name:
- result.append(req.name)
- return result
- class PipfileFinder(ReqsBaseFinder):
- enabled = bool(Pipfile)
- def _get_names(self, path: str) -> Iterator[str]:
- with chdir(path):
- project = Pipfile.load(path)
- for req in project.packages:
- yield req.name
- def _get_files_from_dir(self, path: str) -> Iterator[str]:
- if "Pipfile" in os.listdir(path):
- yield path
- class DefaultFinder(BaseFinder):
- def find(self, module_name: str) -> Optional[str]:
- return self.config.default_section
- class FindersManager:
- _default_finders_classes: Sequence[Type[BaseFinder]] = (
- ForcedSeparateFinder,
- LocalFinder,
- KnownPatternFinder,
- PathFinder,
- PipfileFinder,
- RequirementsFinder,
- DefaultFinder,
- )
- def __init__(
- self, config: Config, finder_classes: Optional[Iterable[Type[BaseFinder]]] = None
- ) -> None:
- self.verbose: bool = config.verbose
- if finder_classes is None:
- finder_classes = self._default_finders_classes
- finders: List[BaseFinder] = []
- for finder_cls in finder_classes:
- try:
- finders.append(finder_cls(config))
- except Exception as exception:
- # if one finder fails to instantiate isort can continue using the rest
- if self.verbose:
- print(
- (
- f"{finder_cls.__name__} encountered an error ({exception}) during "
- "instantiation and cannot be used"
- )
- )
- self.finders: Tuple[BaseFinder, ...] = tuple(finders)
- def find(self, module_name: str) -> Optional[str]:
- for finder in self.finders:
- try:
- section = finder.find(module_name)
- if section is not None:
- return section
- except Exception as exception:
- # isort has to be able to keep trying to identify the correct
- # import section even if one approach fails
- if self.verbose:
- print(
- f"{finder.__class__.__name__} encountered an error ({exception}) while "
- f"trying to identify the {module_name} module"
- )
- return None
|