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