From 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Wed, 3 Apr 2024 03:10:44 -0400 Subject: venv --- .../setuptools/config/pyprojecttoml.py | 493 +++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 venv/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py (limited to 'venv/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py') diff --git a/venv/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py b/venv/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py new file mode 100644 index 0000000..d995f0b --- /dev/null +++ b/venv/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py @@ -0,0 +1,493 @@ +""" +Load setuptools configuration from ``pyproject.toml`` files. + +**PRIVATE MODULE**: API reserved for setuptools internal usage only. +""" +import logging +import os +import warnings +from contextlib import contextmanager +from functools import partial +from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union + +from setuptools.errors import FileError, OptionError + +from . import expand as _expand +from ._apply_pyprojecttoml import apply as _apply +from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _WouldIgnoreField + +if TYPE_CHECKING: + from setuptools.dist import Distribution # noqa + +_Path = Union[str, os.PathLike] +_logger = logging.getLogger(__name__) + + +def load_file(filepath: _Path) -> dict: + from setuptools.extern import tomli # type: ignore + + with open(filepath, "rb") as file: + return tomli.load(file) + + +def validate(config: dict, filepath: _Path) -> bool: + from . import _validate_pyproject as validator + + trove_classifier = validator.FORMAT_FUNCTIONS.get("trove-classifier") + if hasattr(trove_classifier, "_disable_download"): + # Improve reproducibility by default. See issue 31 for validate-pyproject. + trove_classifier._disable_download() # type: ignore + + try: + return validator.validate(config) + except validator.ValidationError as ex: + summary = f"configuration error: {ex.summary}" + if ex.name.strip("`") != "project": + # Probably it is just a field missing/misnamed, not worthy the verbosity... + _logger.debug(summary) + _logger.debug(ex.details) + + error = f"invalid pyproject.toml config: {ex.name}." + raise ValueError(f"{error}\n{summary}") from None + + +def apply_configuration( + dist: "Distribution", + filepath: _Path, + ignore_option_errors=False, +) -> "Distribution": + """Apply the configuration from a ``pyproject.toml`` file into an existing + distribution object. + """ + config = read_configuration(filepath, True, ignore_option_errors, dist) + return _apply(dist, config, filepath) + + +def read_configuration( + filepath: _Path, + expand=True, + ignore_option_errors=False, + dist: Optional["Distribution"] = None, +): + """Read given configuration file and returns options from it as a dict. + + :param str|unicode filepath: Path to configuration file in the ``pyproject.toml`` + format. + + :param bool expand: Whether to expand directives and other computed values + (i.e. post-process the given configuration) + + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + + :param Distribution|None: Distribution object to which the configuration refers. + If not given a dummy object will be created and discarded after the + configuration is read. This is used for auto-discovery of packages in the case + a dynamic configuration (e.g. ``attr`` or ``cmdclass``) is expanded. + When ``expand=False`` this object is simply ignored. + + :rtype: dict + """ + filepath = os.path.abspath(filepath) + + if not os.path.isfile(filepath): + raise FileError(f"Configuration file {filepath!r} does not exist.") + + asdict = load_file(filepath) or {} + project_table = asdict.get("project", {}) + tool_table = asdict.get("tool", {}) + setuptools_table = tool_table.get("setuptools", {}) + if not asdict or not (project_table or setuptools_table): + return {} # User is not using pyproject to configure setuptools + + if setuptools_table: + # TODO: Remove the following once the feature stabilizes: + msg = "Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*." + warnings.warn(msg, _BetaConfiguration) + + # There is an overall sense in the community that making include_package_data=True + # the default would be an improvement. + # `ini2toml` backfills include_package_data=False when nothing is explicitly given, + # therefore setting a default here is backwards compatible. + orig_setuptools_table = setuptools_table.copy() + if dist and getattr(dist, "include_package_data") is not None: + setuptools_table.setdefault("include-package-data", dist.include_package_data) + else: + setuptools_table.setdefault("include-package-data", True) + # Persist changes: + asdict["tool"] = tool_table + tool_table["setuptools"] = setuptools_table + + try: + # Don't complain about unrelated errors (e.g. tools not using the "tool" table) + subset = {"project": project_table, "tool": {"setuptools": setuptools_table}} + validate(subset, filepath) + except Exception as ex: + # TODO: Remove the following once the feature stabilizes: + if _skip_bad_config(project_table, orig_setuptools_table, dist): + return {} + # TODO: After the previous statement is removed the try/except can be replaced + # by the _ignore_errors context manager. + if ignore_option_errors: + _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}") + else: + raise # re-raise exception + + if expand: + root_dir = os.path.dirname(filepath) + return expand_configuration(asdict, root_dir, ignore_option_errors, dist) + + return asdict + + +def _skip_bad_config( + project_cfg: dict, setuptools_cfg: dict, dist: Optional["Distribution"] +) -> bool: + """Be temporarily forgiving with invalid ``pyproject.toml``""" + # See pypa/setuptools#3199 and pypa/cibuildwheel#1064 + + if dist is None or ( + dist.metadata.name is None + and dist.metadata.version is None + and dist.install_requires is None + ): + # It seems that the build is not getting any configuration from other places + return False + + if setuptools_cfg: + # If `[tool.setuptools]` is set, then `pyproject.toml` config is intentional + return False + + given_config = set(project_cfg.keys()) + popular_subset = {"name", "version", "python_requires", "requires-python"} + if given_config <= popular_subset: + # It seems that the docs in cibuildtool has been inadvertently encouraging users + # to create `pyproject.toml` files that are not compliant with the standards. + # Let's be forgiving for the time being. + warnings.warn(_InvalidFile.message(), _InvalidFile, stacklevel=2) + return True + + return False + + +def expand_configuration( + config: dict, + root_dir: Optional[_Path] = None, + ignore_option_errors: bool = False, + dist: Optional["Distribution"] = None, +) -> dict: + """Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...) + find their final values. + + :param dict config: Dict containing the configuration for the distribution + :param str root_dir: Top-level directory for the distribution/project + (the same directory where ``pyproject.toml`` is place) + :param bool ignore_option_errors: see :func:`read_configuration` + :param Distribution|None: Distribution object to which the configuration refers. + If not given a dummy object will be created and discarded after the + configuration is read. Used in the case a dynamic configuration + (e.g. ``attr`` or ``cmdclass``). + + :rtype: dict + """ + return _ConfigExpander(config, root_dir, ignore_option_errors, dist).expand() + + +class _ConfigExpander: + def __init__( + self, + config: dict, + root_dir: Optional[_Path] = None, + ignore_option_errors: bool = False, + dist: Optional["Distribution"] = None, + ): + self.config = config + self.root_dir = root_dir or os.getcwd() + self.project_cfg = config.get("project", {}) + self.dynamic = self.project_cfg.get("dynamic", []) + self.setuptools_cfg = config.get("tool", {}).get("setuptools", {}) + self.dynamic_cfg = self.setuptools_cfg.get("dynamic", {}) + self.ignore_option_errors = ignore_option_errors + self._dist = dist + + def _ensure_dist(self) -> "Distribution": + from setuptools.dist import Distribution + + attrs = {"src_root": self.root_dir, "name": self.project_cfg.get("name", None)} + return self._dist or Distribution(attrs) + + def _process_field(self, container: dict, field: str, fn: Callable): + if field in container: + with _ignore_errors(self.ignore_option_errors): + container[field] = fn(container[field]) + + def _canonic_package_data(self, field="package-data"): + package_data = self.setuptools_cfg.get(field, {}) + return _expand.canonic_package_data(package_data) + + def expand(self): + self._expand_packages() + self._canonic_package_data() + self._canonic_package_data("exclude-package-data") + + # A distribution object is required for discovering the correct package_dir + dist = self._ensure_dist() + ctx = _EnsurePackagesDiscovered(dist, self.project_cfg, self.setuptools_cfg) + with ctx as ensure_discovered: + package_dir = ensure_discovered.package_dir + self._expand_data_files() + self._expand_cmdclass(package_dir) + self._expand_all_dynamic(dist, package_dir) + + return self.config + + def _expand_packages(self): + packages = self.setuptools_cfg.get("packages") + if packages is None or isinstance(packages, (list, tuple)): + return + + find = packages.get("find") + if isinstance(find, dict): + find["root_dir"] = self.root_dir + find["fill_package_dir"] = self.setuptools_cfg.setdefault("package-dir", {}) + with _ignore_errors(self.ignore_option_errors): + self.setuptools_cfg["packages"] = _expand.find_packages(**find) + + def _expand_data_files(self): + data_files = partial(_expand.canonic_data_files, root_dir=self.root_dir) + self._process_field(self.setuptools_cfg, "data-files", data_files) + + def _expand_cmdclass(self, package_dir: Mapping[str, str]): + root_dir = self.root_dir + cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir) + self._process_field(self.setuptools_cfg, "cmdclass", cmdclass) + + def _expand_all_dynamic(self, dist: "Distribution", package_dir: Mapping[str, str]): + special = ( # need special handling + "version", + "readme", + "entry-points", + "scripts", + "gui-scripts", + "classifiers", + "dependencies", + "optional-dependencies", + ) + # `_obtain` functions are assumed to raise appropriate exceptions/warnings. + obtained_dynamic = { + field: self._obtain(dist, field, package_dir) + for field in self.dynamic + if field not in special + } + obtained_dynamic.update( + self._obtain_entry_points(dist, package_dir) or {}, + version=self._obtain_version(dist, package_dir), + readme=self._obtain_readme(dist), + classifiers=self._obtain_classifiers(dist), + dependencies=self._obtain_dependencies(dist), + optional_dependencies=self._obtain_optional_dependencies(dist), + ) + # `None` indicates there is nothing in `tool.setuptools.dynamic` but the value + # might have already been set by setup.py/extensions, so avoid overwriting. + updates = {k: v for k, v in obtained_dynamic.items() if v is not None} + self.project_cfg.update(updates) + + def _ensure_previously_set(self, dist: "Distribution", field: str): + previous = _PREVIOUSLY_DEFINED[field](dist) + if previous is None and not self.ignore_option_errors: + msg = ( + f"No configuration found for dynamic {field!r}.\n" + "Some dynamic fields need to be specified via `tool.setuptools.dynamic`" + "\nothers must be specified via the equivalent attribute in `setup.py`." + ) + raise OptionError(msg) + + def _expand_directive( + self, specifier: str, directive, package_dir: Mapping[str, str] + ): + with _ignore_errors(self.ignore_option_errors): + root_dir = self.root_dir + if "file" in directive: + return _expand.read_files(directive["file"], root_dir) + if "attr" in directive: + return _expand.read_attr(directive["attr"], package_dir, root_dir) + raise ValueError(f"invalid `{specifier}`: {directive!r}") + return None + + def _obtain(self, dist: "Distribution", field: str, package_dir: Mapping[str, str]): + if field in self.dynamic_cfg: + return self._expand_directive( + f"tool.setuptools.dynamic.{field}", + self.dynamic_cfg[field], + package_dir, + ) + self._ensure_previously_set(dist, field) + return None + + def _obtain_version(self, dist: "Distribution", package_dir: Mapping[str, str]): + # Since plugins can set version, let's silently skip if it cannot be obtained + if "version" in self.dynamic and "version" in self.dynamic_cfg: + return _expand.version(self._obtain(dist, "version", package_dir)) + return None + + def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]: + if "readme" not in self.dynamic: + return None + + dynamic_cfg = self.dynamic_cfg + if "readme" in dynamic_cfg: + return { + "text": self._obtain(dist, "readme", {}), + "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"), + } + + self._ensure_previously_set(dist, "readme") + return None + + def _obtain_entry_points( + self, dist: "Distribution", package_dir: Mapping[str, str] + ) -> Optional[Dict[str, dict]]: + fields = ("entry-points", "scripts", "gui-scripts") + if not any(field in self.dynamic for field in fields): + return None + + text = self._obtain(dist, "entry-points", package_dir) + if text is None: + return None + + groups = _expand.entry_points(text) + expanded = {"entry-points": groups} + + def _set_scripts(field: str, group: str): + if group in groups: + value = groups.pop(group) + if field not in self.dynamic: + msg = _WouldIgnoreField.message(field, value) + warnings.warn(msg, _WouldIgnoreField) + # TODO: Don't set field when support for pyproject.toml stabilizes + # instead raise an error as specified in PEP 621 + expanded[field] = value + + _set_scripts("scripts", "console_scripts") + _set_scripts("gui-scripts", "gui_scripts") + + return expanded + + def _obtain_classifiers(self, dist: "Distribution"): + if "classifiers" in self.dynamic: + value = self._obtain(dist, "classifiers", {}) + if value: + return value.splitlines() + return None + + def _obtain_dependencies(self, dist: "Distribution"): + if "dependencies" in self.dynamic: + value = self._obtain(dist, "dependencies", {}) + if value: + return _parse_requirements_list(value) + return None + + def _obtain_optional_dependencies(self, dist: "Distribution"): + if "optional-dependencies" not in self.dynamic: + return None + if "optional-dependencies" in self.dynamic_cfg: + optional_dependencies_map = self.dynamic_cfg["optional-dependencies"] + assert isinstance(optional_dependencies_map, dict) + return { + group: _parse_requirements_list(self._expand_directive( + f"tool.setuptools.dynamic.optional-dependencies.{group}", + directive, + {}, + )) + for group, directive in optional_dependencies_map.items() + } + self._ensure_previously_set(dist, "optional-dependencies") + return None + + +def _parse_requirements_list(value): + return [ + line + for line in value.splitlines() + if line.strip() and not line.strip().startswith("#") + ] + + +@contextmanager +def _ignore_errors(ignore_option_errors: bool): + if not ignore_option_errors: + yield + return + + try: + yield + except Exception as ex: + _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}") + + +class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered): + def __init__( + self, distribution: "Distribution", project_cfg: dict, setuptools_cfg: dict + ): + super().__init__(distribution) + self._project_cfg = project_cfg + self._setuptools_cfg = setuptools_cfg + + def __enter__(self): + """When entering the context, the values of ``packages``, ``py_modules`` and + ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``. + """ + dist, cfg = self._dist, self._setuptools_cfg + package_dir: Dict[str, str] = cfg.setdefault("package-dir", {}) + package_dir.update(dist.package_dir or {}) + dist.package_dir = package_dir # needs to be the same object + + dist.set_defaults._ignore_ext_modules() # pyproject.toml-specific behaviour + + # Set `name`, `py_modules` and `packages` in dist to short-circuit + # auto-discovery, but avoid overwriting empty lists purposefully set by users. + if dist.metadata.name is None: + dist.metadata.name = self._project_cfg.get("name") + if dist.py_modules is None: + dist.py_modules = cfg.get("py-modules") + if dist.packages is None: + dist.packages = cfg.get("packages") + + return super().__enter__() + + def __exit__(self, exc_type, exc_value, traceback): + """When exiting the context, if values of ``packages``, ``py_modules`` and + ``package_dir`` are missing in ``setuptools_cfg``, copy from ``dist``. + """ + # If anything was discovered set them back, so they count in the final config. + self._setuptools_cfg.setdefault("packages", self._dist.packages) + self._setuptools_cfg.setdefault("py-modules", self._dist.py_modules) + return super().__exit__(exc_type, exc_value, traceback) + + +class _BetaConfiguration(UserWarning): + """Explicitly inform users that some `pyproject.toml` configuration is *beta*""" + + +class _InvalidFile(UserWarning): + """The given `pyproject.toml` file is invalid and would be ignored. + !!\n\n + ############################ + # Invalid `pyproject.toml` # + ############################ + + Any configurations in `pyproject.toml` will be ignored. + Please note that future releases of setuptools will halt the build process + if an invalid file is given. + + To prevent setuptools from considering `pyproject.toml` please + DO NOT include the `[project]` or `[tool.setuptools]` tables in your file. + \n\n!! + """ + + @classmethod + def message(cls): + from inspect import cleandoc + return cleandoc(cls.__doc__) -- cgit v1.2.3