summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/setuptools/config/expand.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.11/site-packages/setuptools/config/expand.py')
-rw-r--r--venv/lib/python3.11/site-packages/setuptools/config/expand.py462
1 files changed, 462 insertions, 0 deletions
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())