From 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Wed, 3 Apr 2024 03:10:44 -0400 Subject: venv --- .../site-packages/setuptools/config/expand.py | 462 +++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 venv/lib/python3.11/site-packages/setuptools/config/expand.py (limited to 'venv/lib/python3.11/site-packages/setuptools/config/expand.py') diff --git a/venv/lib/python3.11/site-packages/setuptools/config/expand.py b/venv/lib/python3.11/site-packages/setuptools/config/expand.py new file mode 100644 index 0000000..c8db2c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/config/expand.py @@ -0,0 +1,462 @@ +"""Utility functions to expand configuration directives or special values +(such glob patterns). + +We can split the process of interpreting configuration files into 2 steps: + +1. The parsing the file contents from strings to value objects + that can be understand by Python (for example a string with a comma + separated list of keywords into an actual Python list of strings). + +2. The expansion (or post-processing) of these values according to the + semantics ``setuptools`` assign to them (for example a configuration field + with the ``file:`` directive should be expanded from a list of file paths to + a single string with the contents of those files concatenated) + +This module focus on the second step, and therefore allow sharing the expansion +functions among several configuration file formats. + +**PRIVATE MODULE**: API reserved for setuptools internal usage only. +""" +import ast +import importlib +import io +import os +import pathlib +import sys +import warnings +from glob import iglob +from configparser import ConfigParser +from importlib.machinery import ModuleSpec +from itertools import chain +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Tuple, + TypeVar, + Union, + cast +) +from pathlib import Path +from types import ModuleType + +from distutils.errors import DistutilsOptionError + +from .._path import same_path as _same_path + +if TYPE_CHECKING: + from setuptools.dist import Distribution # noqa + from setuptools.discovery import ConfigDiscovery # noqa + from distutils.dist import DistributionMetadata # noqa + +chain_iter = chain.from_iterable +_Path = Union[str, os.PathLike] +_K = TypeVar("_K") +_V = TypeVar("_V", covariant=True) + + +class StaticModule: + """Proxy to a module object that avoids executing arbitrary code.""" + + def __init__(self, name: str, spec: ModuleSpec): + module = ast.parse(pathlib.Path(spec.origin).read_bytes()) + vars(self).update(locals()) + del self.self + + def _find_assignments(self) -> Iterator[Tuple[ast.AST, ast.AST]]: + for statement in self.module.body: + if isinstance(statement, ast.Assign): + yield from ((target, statement.value) for target in statement.targets) + elif isinstance(statement, ast.AnnAssign) and statement.value: + yield (statement.target, statement.value) + + def __getattr__(self, attr): + """Attempt to load an attribute "statically", via :func:`ast.literal_eval`.""" + try: + return next( + ast.literal_eval(value) + for target, value in self._find_assignments() + if isinstance(target, ast.Name) and target.id == attr + ) + except Exception as e: + raise AttributeError(f"{self.name} has no attribute {attr}") from e + + +def glob_relative( + patterns: Iterable[str], root_dir: Optional[_Path] = None +) -> List[str]: + """Expand the list of glob patterns, but preserving relative paths. + + :param list[str] patterns: List of glob patterns + :param str root_dir: Path to which globs should be relative + (current directory by default) + :rtype: list + """ + glob_characters = {'*', '?', '[', ']', '{', '}'} + expanded_values = [] + root_dir = root_dir or os.getcwd() + for value in patterns: + + # Has globby characters? + if any(char in value for char in glob_characters): + # then expand the glob pattern while keeping paths *relative*: + glob_path = os.path.abspath(os.path.join(root_dir, value)) + expanded_values.extend(sorted( + os.path.relpath(path, root_dir).replace(os.sep, "/") + for path in iglob(glob_path, recursive=True))) + + else: + # take the value as-is + path = os.path.relpath(value, root_dir).replace(os.sep, "/") + expanded_values.append(path) + + return expanded_values + + +def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) -> str: + """Return the content of the files concatenated using ``\n`` as str + + This function is sandboxed and won't reach anything outside ``root_dir`` + + (By default ``root_dir`` is the current directory). + """ + from setuptools.extern.more_itertools import always_iterable + + root_dir = os.path.abspath(root_dir or os.getcwd()) + _filepaths = (os.path.join(root_dir, path) for path in always_iterable(filepaths)) + return '\n'.join( + _read_file(path) + for path in _filter_existing_files(_filepaths) + if _assert_local(path, root_dir) + ) + + +def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]: + for path in filepaths: + if os.path.isfile(path): + yield path + else: + warnings.warn(f"File {path!r} cannot be found") + + +def _read_file(filepath: Union[bytes, _Path]) -> str: + with io.open(filepath, encoding='utf-8') as f: + return f.read() + + +def _assert_local(filepath: _Path, root_dir: str): + if Path(os.path.abspath(root_dir)) not in Path(os.path.abspath(filepath)).parents: + msg = f"Cannot access {filepath!r} (or anything outside {root_dir!r})" + raise DistutilsOptionError(msg) + + return True + + +def read_attr( + attr_desc: str, + package_dir: Optional[Mapping[str, str]] = None, + root_dir: Optional[_Path] = None +): + """Reads the value of an attribute from a module. + + This function will try to read the attributed statically first + (via :func:`ast.literal_eval`), and only evaluate the module if it fails. + + Examples: + read_attr("package.attr") + read_attr("package.module.attr") + + :param str attr_desc: Dot-separated string describing how to reach the + attribute (see examples above) + :param dict[str, str] package_dir: Mapping of package names to their + location in disk (represented by paths relative to ``root_dir``). + :param str root_dir: Path to directory containing all the packages in + ``package_dir`` (current directory by default). + :rtype: str + """ + root_dir = root_dir or os.getcwd() + attrs_path = attr_desc.strip().split('.') + attr_name = attrs_path.pop() + module_name = '.'.join(attrs_path) + module_name = module_name or '__init__' + _parent_path, path, module_name = _find_module(module_name, package_dir, root_dir) + spec = _find_spec(module_name, path) + + try: + return getattr(StaticModule(module_name, spec), attr_name) + except Exception: + # fallback to evaluate module + module = _load_spec(spec, module_name) + return getattr(module, attr_name) + + +def _find_spec(module_name: str, module_path: Optional[_Path]) -> ModuleSpec: + spec = importlib.util.spec_from_file_location(module_name, module_path) + spec = spec or importlib.util.find_spec(module_name) + + if spec is None: + raise ModuleNotFoundError(module_name) + + return spec + + +def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType: + name = getattr(spec, "__name__", module_name) + if name in sys.modules: + return sys.modules[name] + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module # cache (it also ensures `==` works on loaded items) + spec.loader.exec_module(module) # type: ignore + return module + + +def _find_module( + module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: _Path +) -> Tuple[_Path, Optional[str], str]: + """Given a module (that could normally be imported by ``module_name`` + after the build is complete), find the path to the parent directory where + it is contained and the canonical name that could be used to import it + considering the ``package_dir`` in the build configuration and ``root_dir`` + """ + parent_path = root_dir + module_parts = module_name.split('.') + if package_dir: + if module_parts[0] in package_dir: + # A custom path was specified for the module we want to import + custom_path = package_dir[module_parts[0]] + parts = custom_path.rsplit('/', 1) + if len(parts) > 1: + parent_path = os.path.join(root_dir, parts[0]) + parent_module = parts[1] + else: + parent_module = custom_path + module_name = ".".join([parent_module, *module_parts[1:]]) + elif '' in package_dir: + # A custom parent directory was specified for all root modules + parent_path = os.path.join(root_dir, package_dir['']) + + path_start = os.path.join(parent_path, *module_name.split(".")) + candidates = chain( + (f"{path_start}.py", os.path.join(path_start, "__init__.py")), + iglob(f"{path_start}.*") + ) + module_path = next((x for x in candidates if os.path.isfile(x)), None) + return parent_path, module_path, module_name + + +def resolve_class( + qualified_class_name: str, + package_dir: Optional[Mapping[str, str]] = None, + root_dir: Optional[_Path] = None +) -> Callable: + """Given a qualified class name, return the associated class object""" + root_dir = root_dir or os.getcwd() + idx = qualified_class_name.rfind('.') + class_name = qualified_class_name[idx + 1 :] + pkg_name = qualified_class_name[:idx] + + _parent_path, path, module_name = _find_module(pkg_name, package_dir, root_dir) + module = _load_spec(_find_spec(module_name, path), module_name) + return getattr(module, class_name) + + +def cmdclass( + values: Dict[str, str], + package_dir: Optional[Mapping[str, str]] = None, + root_dir: Optional[_Path] = None +) -> Dict[str, Callable]: + """Given a dictionary mapping command names to strings for qualified class + names, apply :func:`resolve_class` to the dict values. + """ + return {k: resolve_class(v, package_dir, root_dir) for k, v in values.items()} + + +def find_packages( + *, + namespaces=True, + fill_package_dir: Optional[Dict[str, str]] = None, + root_dir: Optional[_Path] = None, + **kwargs +) -> List[str]: + """Works similarly to :func:`setuptools.find_packages`, but with all + arguments given as keyword arguments. Moreover, ``where`` can be given + as a list (the results will be simply concatenated). + + When the additional keyword argument ``namespaces`` is ``True``, it will + behave like :func:`setuptools.find_namespace_packages`` (i.e. include + implicit namespaces as per :pep:`420`). + + The ``where`` argument will be considered relative to ``root_dir`` (or the current + working directory when ``root_dir`` is not given). + + If the ``fill_package_dir`` argument is passed, this function will consider it as a + similar data structure to the ``package_dir`` configuration parameter add fill-in + any missing package location. + + :rtype: list + """ + from setuptools.discovery import construct_package_dir + from setuptools.extern.more_itertools import unique_everseen, always_iterable + + if namespaces: + from setuptools.discovery import PEP420PackageFinder as PackageFinder + else: + from setuptools.discovery import PackageFinder # type: ignore + + root_dir = root_dir or os.curdir + where = kwargs.pop('where', ['.']) + packages: List[str] = [] + fill_package_dir = {} if fill_package_dir is None else fill_package_dir + search = list(unique_everseen(always_iterable(where))) + + if len(search) == 1 and all(not _same_path(search[0], x) for x in (".", root_dir)): + fill_package_dir.setdefault("", search[0]) + + for path in search: + package_path = _nest_path(root_dir, path) + pkgs = PackageFinder.find(package_path, **kwargs) + packages.extend(pkgs) + if pkgs and not ( + fill_package_dir.get("") == path + or os.path.samefile(package_path, root_dir) + ): + fill_package_dir.update(construct_package_dir(pkgs, path)) + + return packages + + +def _nest_path(parent: _Path, path: _Path) -> str: + path = parent if path in {".", ""} else os.path.join(parent, path) + return os.path.normpath(path) + + +def version(value: Union[Callable, Iterable[Union[str, int]], str]) -> str: + """When getting the version directly from an attribute, + it should be normalised to string. + """ + if callable(value): + value = value() + + value = cast(Iterable[Union[str, int]], value) + + if not isinstance(value, str): + if hasattr(value, '__iter__'): + value = '.'.join(map(str, value)) + else: + value = '%s' % value + + return value + + +def canonic_package_data(package_data: dict) -> dict: + if "*" in package_data: + package_data[""] = package_data.pop("*") + return package_data + + +def canonic_data_files( + data_files: Union[list, dict], root_dir: Optional[_Path] = None +) -> List[Tuple[str, List[str]]]: + """For compatibility with ``setup.py``, ``data_files`` should be a list + of pairs instead of a dict. + + This function also expands glob patterns. + """ + if isinstance(data_files, list): + return data_files + + return [ + (dest, glob_relative(patterns, root_dir)) + for dest, patterns in data_files.items() + ] + + +def entry_points(text: str, text_source="entry-points") -> Dict[str, dict]: + """Given the contents of entry-points file, + process it into a 2-level dictionary (``dict[str, dict[str, str]]``). + The first level keys are entry-point groups, the second level keys are + entry-point names, and the second level values are references to objects + (that correspond to the entry-point value). + """ + parser = ConfigParser(default_section=None, delimiters=("=",)) # type: ignore + parser.optionxform = str # case sensitive + parser.read_string(text, text_source) + groups = {k: dict(v.items()) for k, v in parser.items()} + groups.pop(parser.default_section, None) + return groups + + +class EnsurePackagesDiscovered: + """Some expand functions require all the packages to already be discovered before + they run, e.g. :func:`read_attr`, :func:`resolve_class`, :func:`cmdclass`. + + Therefore in some cases we will need to run autodiscovery during the evaluation of + the configuration. However, it is better to postpone calling package discovery as + much as possible, because some parameters can influence it (e.g. ``package_dir``), + and those might not have been processed yet. + """ + + def __init__(self, distribution: "Distribution"): + self._dist = distribution + self._called = False + + def __call__(self): + """Trigger the automatic package discovery, if it is still necessary.""" + if not self._called: + self._called = True + self._dist.set_defaults(name=False) # Skip name, we can still be parsing + + def __enter__(self): + return self + + def __exit__(self, _exc_type, _exc_value, _traceback): + if self._called: + self._dist.set_defaults.analyse_name() # Now we can set a default name + + def _get_package_dir(self) -> Mapping[str, str]: + self() + pkg_dir = self._dist.package_dir + return {} if pkg_dir is None else pkg_dir + + @property + def package_dir(self) -> Mapping[str, str]: + """Proxy to ``package_dir`` that may trigger auto-discovery when used.""" + return LazyMappingProxy(self._get_package_dir) + + +class LazyMappingProxy(Mapping[_K, _V]): + """Mapping proxy that delays resolving the target object, until really needed. + + >>> def obtain_mapping(): + ... print("Running expensive function!") + ... return {"key": "value", "other key": "other value"} + >>> mapping = LazyMappingProxy(obtain_mapping) + >>> mapping["key"] + Running expensive function! + 'value' + >>> mapping["other key"] + 'other value' + """ + + def __init__(self, obtain_mapping_value: Callable[[], Mapping[_K, _V]]): + self._obtain = obtain_mapping_value + self._value: Optional[Mapping[_K, _V]] = None + + def _target(self) -> Mapping[_K, _V]: + if self._value is None: + self._value = self._obtain() + return self._value + + def __getitem__(self, key: _K) -> _V: + return self._target()[key] + + def __len__(self) -> int: + return len(self._target()) + + def __iter__(self) -> Iterator[_K]: + return iter(self._target()) -- cgit v1.2.3