setuptools
Advanced tools
Sorry, the diff of this file is too big to display
| =========================================================================================== | ||
| Drawbacks of installing source distributions (``sdist``) and how to improve reproducibility | ||
| =========================================================================================== | ||
| .. admonition:: Scope and audience | ||
| This page contains relevant information for **package consumers** (people | ||
| installing third-party projects that may have been built with Setuptools). | ||
| It explains why installing from :external+PyPUG:term:`Source Distributions | ||
| <Source Distribution (or "sdist")>` can be less predictable than installing | ||
| from :external+PyPUG:term:`wheels <Wheel>`, and contains tips on how to improve | ||
| **installation-time** reproducibility. It does **not** describe how to build | ||
| packages with Setuptools, nor is it a statement of policy about what | ||
| publishers must do [#publishing]_. | ||
| The ``sdist`` format was one of the first packaging formats to be created by the | ||
| Python community (predating the advent of ``wheel``). Although very | ||
| useful today to distribute and share Python libraries and applications, | ||
| ``sdist``\s are notoriously difficult to work with in circumstances that | ||
| require high build reproducibility and tolerance to disruptions. | ||
| This guide reviews the concept of ``sdist``, highlights its potential uses | ||
| and drawbacks and explores potential practices to improve build reproducibility | ||
| when relying on ``sdist``\s. | ||
| What is an ``sdist``? | ||
| ===================== | ||
| You can read more about the ``sdist`` format and its ``wheel`` counterpart in | ||
| :external+PyPUG:doc:`discussions/package-formats`, but for the sake of this | ||
| document an ``sdist`` can be considered a simple ``.tar.gz`` archive that | ||
| contains all the files necessary to build a Python project that later will be | ||
| installed in the end-user's environment. | ||
| The most defining characteristic of the ``sdist`` format is its | ||
| platform-independence, as the distributions do not include binary executable files. | ||
| This format is very flexible and, although usually composed by a simple copy | ||
| of the source code files with some extra metadata files added, it can also include | ||
| platform-independent code automatically generated during the build | ||
| phase [#examples]_. | ||
| When is an ``sdist`` useful? | ||
| ============================ | ||
| Sometimes it can be tricky to distribute Python packages that contain binary | ||
| extensions, especially when they are built for platforms that do not define a | ||
| cross-version stable ABI_. | ||
| Moreover package indexes like PyPI_ may restrict their offer to a handful of | ||
| well-known platforms. | ||
| Finally, for certain edge cases, the build process may require machine specific | ||
| parameters. | ||
| In this context, distributing code via ``sdist``\s becomes a valuable fallback. | ||
| It allows users in other platforms to access the source code | ||
| and attempt to recompile the extensions locally. | ||
| What are the drawbacks of an ``sdist``? | ||
| ======================================= | ||
| Despite their usefulness, working with ``sdist``\s can be challenging. One | ||
| major difficulty is reconstructing a compatible build environment in which the | ||
| ``sdist`` can be processed into a ``wheel``, especially when it comes to build | ||
| dependencies. | ||
| While :pep:`518` introduced a standard for declaring build dependencies | ||
| distributed as Python packages (e.g. via PyPI), many projects also rely on | ||
| non-Python dependencies, such as compilers and binary system-level libraries, | ||
| that are not declared as a standard metadata. These dependencies can vary | ||
| significantly across systems and its installation is often not automated and | ||
| undocumented, i.e., simply assumed to be present. | ||
| Another issue is *tooling drift*: even if a project was originally buildable | ||
| from its ``sdist``, changes in the build dependencies (e.g., updates, | ||
| deprecations and security fixes) can break compatibility over time [#pinning]_. | ||
| This is a natural tendency of software systems and especially true for older | ||
| projects. | ||
| Therefore, mission-critical systems and environments that cannot afford | ||
| unforeseen/unintended interruptions should not rely on ``sdist``\s. | ||
| If your project or product requires high reliability and minimal disruption, | ||
| you should adapt your workflow to increase resiliency and reproducibility or | ||
| disallow ``sdist``\s all together. | ||
| How to improve reproducibility in your workflow and avoid ``sdist`` drawbacks? | ||
| ============================================================================== | ||
| The first step to improve your workflow is to determine whether your workflow | ||
| is directly or indirectly relying on ``sdist``\s — and to prevent them from being | ||
| compiled on demand. | ||
| Installers like ``pip`` or ``uv`` have options that help with this. | ||
| For example, you can set the environment variable |PIP_ONLY_BINARY|_ with | ||
| the value ``:all:``, to prevent ``sdist``\s from being installed | ||
| (see the corresponding `uv alternative`_). | ||
| When this setting is enabled, any installation that fails will indicate which | ||
| packages are not available as ``wheel``\s, helping you pinpoint installations | ||
| relying on ``sdist``\s. | ||
| Once these packages are identified, the next step is to build them in | ||
| a controlled environment. | ||
| You can use ``pip``\'s |PIP_CONSTRAINT|_ / |PIP_BUILD_CONSTRAINT|_ | ||
| environment variables or the | ||
| |build-constraint|_ ``uv``\'s CLI option to enforce specific versions of | ||
| Python packages [#build-isolation]_. | ||
| To further improve the consistency of OS-level tools and libraries, | ||
| you can leverage your CI/CD provider's configuration method, for example | ||
| `GitHub Workflows`_, `Bitbucket Pipelines`_, `GitLab CI/CD`_, Jenkins_, | ||
| CircleCI_ or Semaphore_. | ||
| Alternatively, you can use containers (e.g. docker_, nerdctl_ or podman_), | ||
| immutable operating system distributions or package managers (e.g. `NixOS/Nix`_) | ||
| or configuration management tools (e.g. Ansible_, chef_ or puppet_) | ||
| to implement `Infrastructure as Code`_ (IaC) and ensure build environments | ||
| are reproducible and version-controlled. | ||
| Consider caching the resulting ``wheel``\s | ||
| locally via |wheelhouse directories|_ or hosting them in | ||
| *private package indexes* (such as devpi_). | ||
| This allows you to serve pre-built distributions internally, | ||
| which reduces reliance on external sources, improves build stability, | ||
| and often results in faster workflows as a welcome side effect. | ||
| Finally, it's important to regularly audit your pinned or cached (build) | ||
| dependencies for known security vulnerabilities and critical bug fixes and/or | ||
| update them accordingly. | ||
| This can be done through an *out-of-band* workflow — such as a scheduled job | ||
| or a monthly CI/CD pipeline — that does not interfere with your | ||
| mission-critical or low-tolerance environments. This approach ensures that your | ||
| systems remain secure and up to date without compromising the stability of your | ||
| primary workflows. | ||
| .. rubric:: Footnotes | ||
| .. [#publishing] | ||
| The PyPA recommendation, documented in the `packaging tutorial`_, is to | ||
| publish both ``sdists`` and ``wheels``. | ||
| .. [#examples] | ||
| Examples of platform-independent generated code in ``sdist``\s include | ||
| ``.pyx`` files transpiled into ``.c`` and Python code created from | ||
| ``.proto``, JSON schema or grammar files, etc. | ||
| .. [#pinning] | ||
| Although developers can try to minimize the impact of tooling drift by | ||
| locking the version of build dependencies, this approach also has | ||
| its own drawbacks. In fact, it is very common in the Python community to | ||
| avoid specifying version caps. For a deeper discussion on this topic, see: | ||
| https://iscinumpy.dev/post/bound-version-constraints/ and | ||
| https://hynek.me/articles/semver-will-not-save-you/. | ||
| .. [#build-isolation] | ||
| When a virtual environment with hand picked versions of build dependencies | ||
| is crafted (either manually or via tools supporting one of the | ||
| :external+PyPUG:doc:`specifications/pylock-toml` or | ||
| :external+pip:doc:`reference/requirements-file-format`), it is also possible | ||
| to use features like |no-isolation|_, |no-build-isolation|_ or the | ||
| `equivalent uv settings`_ to ensure packages are built against the currently | ||
| active virtual environment. | ||
| .. _ABI: https://en.wikipedia.org/wiki/Application_binary_interface | ||
| .. _PyPI: https://pypi.org | ||
| .. |PIP_ONLY_BINARY| replace:: ``PIP_ONLY_BINARY`` | ||
| .. _PIP_ONLY_BINARY: https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-only-binary | ||
| .. _uv alternative: https://docs.astral.sh/uv/reference/settings/#pip_only-binary | ||
| .. |PIP_CONSTRAINT| replace:: ``PIP_CONSTRAINT`` | ||
| .. _PIP_CONSTRAINT: https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-c | ||
| .. |PIP_BUILD_CONSTRAINT| replace:: ``PIP_BUILD_CONSTRAINT`` | ||
| .. _PIP_BUILD_CONSTRAINT: https://pip.pypa.io/en/stable/cli/pip_download/#cmdoption-build-constraint | ||
| .. |build-constraint| replace:: ``--build-constraint`` | ||
| .. _build-constraint: https://docs.astral.sh/uv/concepts/projects/build/#build-constraints | ||
| .. _GitHub Workflows: https://docs.github.com/en/actions/writing-workflows | ||
| .. _Bitbucket Pipelines: https://www.atlassian.com/software/bitbucket/features/pipelines | ||
| .. _GitLab CI/CD: https://docs.gitlab.com/ci/ | ||
| .. _Jenkins: https://www.jenkins.io/doc/ | ||
| .. _CircleCI: https://circleci.com | ||
| .. _Semaphore: https://semaphore.io | ||
| .. _docker: https://www.docker.com | ||
| .. _nerdctl: https://github.com/containerd/nerdctl | ||
| .. _podman: https://podman.io | ||
| .. _NixOS/Nix: https://nixos.org | ||
| .. _Ansible: https://docs.ansible.com | ||
| .. _chef: https://docs.chef.io | ||
| .. _puppet: https://www.puppet.com/docs/index.html | ||
| .. _Infrastructure as Code: https://en.wikipedia.org/wiki/Infrastructure_as_code | ||
| .. |wheelhouse directories| replace:: *"wheelhouse" directories* | ||
| .. _wheelhouse directories: https://pip.pypa.io/en/stable/cli/pip_wheel/#examples | ||
| .. _devpi: https://doc.devpi.net/ | ||
| .. |no-isolation| replace:: ``--no-isolation`` | ||
| .. _no-isolation: https://build.pypa.io/en/stable/#python--m-build---no-isolation | ||
| .. |no-build-isolation| replace:: ``--no-build-isolation`` | ||
| .. _no-build-isolation: https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-no-build-isolation | ||
| .. _equivalent uv settings: https://docs.astral.sh/uv/concepts/projects/config/#build-isolation | ||
| .. _packaging tutorial: https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives |
| <?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
| <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> | ||
| <assemblyIdentity version="1.0.0.0" | ||
| processorArchitecture="X86" | ||
| name="%(name)s" | ||
| type="win32"/> | ||
| <!-- Identify the application security requirements. --> | ||
| <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> | ||
| <security> | ||
| <requestedPrivileges> | ||
| <requestedExecutionLevel level="asInvoker" uiAccess="false"/> | ||
| </requestedPrivileges> | ||
| </security> | ||
| </trustInfo> | ||
| </assembly> |
+0
-1
@@ -35,3 +35,2 @@ import platform | ||
| '_distutils_hack', | ||
| 'pkg_resources/tests/data', | ||
| 'setuptools/_vendor', | ||
@@ -38,0 +37,0 @@ 'setuptools/config/_validate_pyproject', |
+3
-2
@@ -149,2 +149,3 @@ from __future__ import annotations | ||
| "userguide/commands": "/deprecated/commands.html", | ||
| "pkg_resources": "/deprecated/pkg_resources.html", | ||
| } | ||
@@ -242,4 +243,4 @@ | ||
| intersphinx_mapping.update({ | ||
| 'pip': ('https://pip.pypa.io/en/latest', None), | ||
| 'build': ('https://build.pypa.io/en/latest', None), | ||
| 'pip': ('https://pip.pypa.io/en/stable', None), | ||
| 'build': ('https://build.pypa.io/en/stable', None), | ||
| 'PyPUG': ('https://packaging.python.org/en/latest', None), | ||
@@ -246,0 +247,0 @@ 'pytest': ('https://docs.pytest.org/en/stable', None), |
@@ -26,1 +26,11 @@ ====================================================== | ||
| commands | ||
| pkg_resources | ||
| Notes for Consumers of Packages Built with Setuptools | ||
| ===================================================== | ||
| .. toctree:: | ||
| :maxdepth: 1 | ||
| sdist-reproducibility |
@@ -125,4 +125,3 @@ ================================ | ||
| cannot declare dependencies other than through | ||
| ``setuptools/_vendor/vendored.txt`` and | ||
| ``pkg_resources/_vendor/vendored.txt``. | ||
| ``setuptools/_vendor/vendored.txt``. | ||
@@ -129,0 +128,0 @@ All the dependencies specified in these files are "vendorized" using a |
+0
-1
@@ -24,3 +24,2 @@ .. image:: images/banner-640x320.svg | ||
| build_meta | ||
| pkg_resources | ||
| references/keywords | ||
@@ -27,0 +26,0 @@ setuptools |
@@ -6,5 +6,4 @@ ================================================== | ||
| ``Setuptools`` is a collection of enhancements to the Python ``distutils`` | ||
| that allow developers to more easily build and | ||
| distribute Python packages, especially ones that have dependencies on other | ||
| packages. | ||
| that allow developers to more easily build Python packages, including those | ||
| that have dependencies on other packages and C/C++ extension modules. | ||
@@ -11,0 +10,0 @@ Packages built and distributed using ``setuptools`` look to the user like |
@@ -296,3 +296,3 @@ .. _Creating ``distutils`` Extensions: | ||
| .. _distutils: https://docs.python.org/3.9/library/distutils.html | ||
| .. _distutils: https://setuptools.pypa.io/en/latest/deprecated/distutils/index.html | ||
@@ -299,0 +299,0 @@ |
+0
-2
@@ -6,4 +6,2 @@ recursive-include setuptools *.py *.exe *.xml *.tmpl | ||
| recursive-include setuptools/_vendor * | ||
| recursive-include pkg_resources *.py *.txt | ||
| recursive-include pkg_resources/tests/data * | ||
| recursive-include tools * | ||
@@ -10,0 +8,0 @@ recursive-include newsfragments * |
+10
-16
@@ -34,5 +34,4 @@ [mypy] | ||
| | ^setuptools/_distutils/ | ||
| # Duplicate module name | ||
| | ^pkg_resources/tests/data/my-test-package-source/setup.py$ | ||
| ) | ||
| [mypy-setuptools.*] | ||
@@ -47,19 +46,15 @@ disable_error_code = | ||
| # - pkg_resources tests create modules that won't exists statically before the test is run. | ||
| # Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail. | ||
| [mypy-pkg_resources.tests.*] | ||
| disable_error_code = import-not-found | ||
| # - distutils doesn't exist on Python 3.12, unfortunately, this means typing | ||
| # will be missing for subclasses of distutils on Python 3.12 until either: | ||
| # - support for `SETUPTOOLS_USE_DISTUTILS=stdlib` is dropped (#3625) | ||
| # for setuptools to import `_distutils` directly | ||
| # - or non-stdlib distutils typings are exposed | ||
| # distutils doesn't exist on Python 3.12, unfortunately, this means typing | ||
| # will be missing for subclasses of distutils on Python 3.12 until either: | ||
| # - support for `SETUPTOOLS_USE_DISTUTILS=stdlib` is dropped (#3625) | ||
| # for setuptools to import `_distutils` directly | ||
| # - or non-stdlib distutils typings are exposed | ||
| [mypy-distutils.*] | ||
| ignore_missing_imports = True | ||
| # - wheel: does not intend on exposing a programmatic API https://github.com/pypa/wheel/pull/610#issuecomment-2081687671 | ||
| # wheel: does not intend on exposing a programmatic API https://github.com/pypa/wheel/pull/610#issuecomment-2081687671 | ||
| [mypy-wheel.*] | ||
| follow_untyped_imports = True | ||
| # - The following are not marked as py.typed: | ||
| # The following are not marked as py.typed: | ||
| # - jaraco: Since mypy 1.12, the root name of the untyped namespace package gets called-out too | ||
@@ -70,4 +65,3 @@ # - jaraco.develop: https://github.com/jaraco/jaraco.develop/issues/22 | ||
| # - jaraco.path: https://github.com/jaraco/jaraco.path/issues/2 | ||
| # - jaraco.text: https://github.com/jaraco/jaraco.text/issues/17 | ||
| [mypy-jaraco,jaraco.develop.*,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.text] | ||
| [mypy-jaraco,jaraco.develop.*,jaraco.envs,jaraco.packaging.*,jaraco.path] | ||
| follow_untyped_imports = True | ||
@@ -74,0 +68,0 @@ |
+2
-3
| Metadata-Version: 2.4 | ||
| Name: setuptools | ||
| Version: 82.0.0 | ||
| Summary: Easily download, build, install, upgrade, and uninstall Python packages | ||
| Version: 82.0.1 | ||
| Summary: Most extensible Python build backend with support for C/C++ extension modules | ||
| Author-email: Python Packaging Authority <distutils-sig@python.org> | ||
@@ -66,3 +66,2 @@ License-Expression: MIT | ||
| Requires-Dist: wheel>=0.43.0; extra == "core" | ||
| Requires-Dist: platformdirs>=4.2.2; extra == "core" | ||
| Requires-Dist: jaraco.functools>=4; extra == "core" | ||
@@ -69,0 +68,0 @@ Requires-Dist: more_itertools; extra == "core" |
+2
-5
@@ -13,7 +13,7 @@ [build-system] | ||
| name = "setuptools" | ||
| version = "82.0.0" | ||
| version = "82.0.1" | ||
| authors = [ | ||
| { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, | ||
| ] | ||
| description = "Easily download, build, install, upgrade, and uninstall Python packages" | ||
| description = "Most extensible Python build backend with support for C/C++ extension modules" | ||
| readme = "README.rst" | ||
@@ -106,5 +106,2 @@ classifiers = [ | ||
| # pkg_resources | ||
| "platformdirs >= 4.2.2", # Made ctypes optional (see #4461) | ||
| # for distutils | ||
@@ -111,0 +108,0 @@ "jaraco.functools >= 4", |
| Metadata-Version: 2.4 | ||
| Name: setuptools | ||
| Version: 82.0.0 | ||
| Summary: Easily download, build, install, upgrade, and uninstall Python packages | ||
| Version: 82.0.1 | ||
| Summary: Most extensible Python build backend with support for C/C++ extension modules | ||
| Author-email: Python Packaging Authority <distutils-sig@python.org> | ||
@@ -66,3 +66,2 @@ License-Expression: MIT | ||
| Requires-Dist: wheel>=0.43.0; extra == "core" | ||
| Requires-Dist: platformdirs>=4.2.2; extra == "core" | ||
| Requires-Dist: jaraco.functools>=4; extra == "core" | ||
@@ -69,0 +68,0 @@ Requires-Dist: more_itertools; extra == "core" |
@@ -16,3 +16,2 @@ | ||
| wheel>=0.43.0 | ||
| platformdirs>=4.2.2 | ||
| jaraco.functools>=4 | ||
@@ -19,0 +18,0 @@ more_itertools |
@@ -20,3 +20,2 @@ LICENSE | ||
| docs/index.rst | ||
| docs/pkg_resources.rst | ||
| docs/python 2 sunset.rst | ||
@@ -32,4 +31,6 @@ docs/roadmap.rst | ||
| docs/deprecated/index.rst | ||
| docs/deprecated/pkg_resources.rst | ||
| docs/deprecated/python_eggs.rst | ||
| docs/deprecated/resource_extraction.rst | ||
| docs/deprecated/sdist-reproducibility.rst | ||
| docs/deprecated/zip_safe.rst | ||
@@ -100,2 +101,3 @@ docs/deprecated/distutils/_setuptools_disclaimer.rst | ||
| setuptools/launch.py | ||
| setuptools/launcher manifest.xml | ||
| setuptools/logging.py | ||
@@ -419,3 +421,2 @@ setuptools/modified.py | ||
| setuptools/command/install_scripts.py | ||
| setuptools/command/launcher manifest.xml | ||
| setuptools/command/rotate.py | ||
@@ -422,0 +423,0 @@ setuptools/command/saveopts.py |
@@ -271,6 +271,14 @@ """Translation layer between pyproject config and setuptools distribution and | ||
| args = ({k.replace("-", "_"): v for k, v in x.items()} for x in val) | ||
| new = [Extension(**kw) for kw in args] | ||
| new = (Extension(**_adjust_ext_attrs(kw)) for kw in args) | ||
| return [*existing, *new] | ||
| def _adjust_ext_attrs(attrs: dict) -> dict: | ||
| # https://github.com/pypa/setuptools/issues/4810 | ||
| # In TOML there is no differentiation between tuples and lists, | ||
| # and distutils requires tuples... | ||
| attrs["define_macros"] = list(map(tuple, attrs.get("define_macros") or [])) | ||
| return attrs | ||
| def _noop(_dist: Distribution, val: _T) -> _T: | ||
@@ -277,0 +285,0 @@ return val |
@@ -0,1 +1,3 @@ | ||
| from __future__ import annotations | ||
| import io | ||
@@ -9,3 +11,3 @@ import json | ||
| from textwrap import indent, wrap | ||
| from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence, Union | ||
| from typing import Any, Generator, Iterator, Sequence | ||
@@ -40,3 +42,3 @@ from .fastjsonschema_exceptions import JsonSchemaValueException | ||
| _CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)") | ||
| _IDENTIFIER = re.compile(r"^[\w_]+$", re.I) | ||
| _IDENTIFIER = re.compile(r"^[\w_]+$", re.IGNORECASE) | ||
@@ -78,3 +80,3 @@ _TOML_JARGON = { | ||
| @classmethod | ||
| def _from_jsonschema(cls, ex: JsonSchemaValueException) -> "Self": | ||
| def _from_jsonschema(cls, ex: JsonSchemaValueException) -> Self: | ||
| formatter = _ErrorFormatting(ex) | ||
@@ -179,4 +181,4 @@ obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule) | ||
| def __init__(self, jargon: Optional[Dict[str, str]] = None): | ||
| self.jargon: Dict[str, str] = jargon or {} | ||
| def __init__(self, jargon: dict[str, str] | None = None): | ||
| self.jargon: dict[str, str] = jargon or {} | ||
| # Clarify confusing terms | ||
@@ -214,3 +216,3 @@ self._terms = { | ||
| def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]: | ||
| def _jargon(self, term: str | list[str]) -> str | list[str]: | ||
| if isinstance(term, list): | ||
@@ -222,3 +224,3 @@ return [self.jargon.get(t, t) for t in term] | ||
| self, | ||
| schema: Union[dict, List[dict]], | ||
| schema: dict | list[dict], | ||
| prefix: str = "", | ||
@@ -270,4 +272,4 @@ *, | ||
| def _filter_unecessary( | ||
| self, schema: Dict[str, Any], path: Sequence[str] | ||
| ) -> Dict[str, Any]: | ||
| self, schema: dict[str, Any], path: Sequence[str] | ||
| ) -> dict[str, Any]: | ||
| return { | ||
@@ -279,3 +281,3 @@ key: value | ||
| def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> Optional[str]: | ||
| def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> str | None: | ||
| inline = any(p in value for p in self._guess_inline_defs) | ||
@@ -339,3 +341,3 @@ simple = not any(isinstance(v, (list, dict)) for v in value.values()) | ||
| def _separate_terms(word: str) -> List[str]: | ||
| def _separate_terms(word: str) -> list[str]: | ||
| """ | ||
@@ -342,0 +344,0 @@ >>> _separate_terms("FooBar-foo") |
@@ -6,4 +6,6 @@ """The purpose of this module is implement PEP 621 validations that are | ||
| import collections | ||
| import itertools | ||
| from inspect import cleandoc | ||
| from typing import Mapping, TypeVar | ||
| from typing import Generator, Iterable, Mapping, TypeVar | ||
@@ -23,4 +25,3 @@ from .error_reporting import ValidationError | ||
| _URL = ( | ||
| "https://packaging.python.org/en/latest/specifications/" | ||
| "pyproject-toml/#dynamic" | ||
| "https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic" | ||
| ) | ||
@@ -36,2 +37,20 @@ | ||
| class ImportNameCollision(ValidationError): | ||
| _DESC = """According to PEP 794: | ||
| All import-names and import-namespaces items must be unique. | ||
| """ | ||
| __doc__ = _DESC | ||
| _URL = "https://peps.python.org/pep-0794/" | ||
| class ImportNameMissing(ValidationError): | ||
| _DESC = """According to PEP 794: | ||
| An import name must have all parents listed. | ||
| """ | ||
| __doc__ = _DESC | ||
| _URL = "https://peps.python.org/pep-0794/" | ||
| def validate_project_dynamic(pyproject: T) -> T: | ||
@@ -85,2 +104,52 @@ project_table = pyproject.get("project", {}) | ||
| EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency) | ||
| def _remove_private(items: Iterable[str]) -> Generator[str, None, None]: | ||
| for item in items: | ||
| yield item.partition(";")[0].rstrip() | ||
| def validate_import_name_issues(pyproject: T) -> T: | ||
| project = pyproject.get("project", {}) | ||
| import_names = collections.Counter(_remove_private(project.get("import-names", []))) | ||
| import_namespaces = collections.Counter( | ||
| _remove_private(project.get("import-namespaces", [])) | ||
| ) | ||
| duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1] | ||
| if duplicated: | ||
| raise ImportNameCollision( | ||
| message="Duplicated names are not allowed in import-names/import-namespaces", | ||
| value=duplicated, | ||
| name="data.project.importnames(paces)", | ||
| definition={ | ||
| "description": cleandoc(ImportNameCollision._DESC), | ||
| "see": ImportNameCollision._URL, | ||
| }, | ||
| rule="PEP 794", | ||
| ) | ||
| names = frozenset(import_names + import_namespaces) | ||
| for name in names: | ||
| for parent in itertools.accumulate( | ||
| name.split(".")[:-1], lambda a, b: f"{a}.{b}" | ||
| ): | ||
| if parent not in names: | ||
| raise ImportNameMissing( | ||
| message="All parents of an import name must also be listed in import-namespace/import-names", | ||
| value=name, | ||
| name="data.project.importnames(paces)", | ||
| definition={ | ||
| "description": cleandoc(ImportNameMissing._DESC), | ||
| "see": ImportNameMissing._URL, | ||
| }, | ||
| rule="PEP 794", | ||
| ) | ||
| return pyproject | ||
| EXTRA_VALIDATIONS = ( | ||
| validate_project_dynamic, | ||
| validate_include_depenency, | ||
| validate_import_name_issues, | ||
| ) |
@@ -10,3 +10,5 @@ """ | ||
| import builtins | ||
| from __future__ import annotations | ||
| import keyword | ||
| import logging | ||
@@ -20,2 +22,4 @@ import os | ||
| if typing.TYPE_CHECKING: | ||
| import builtins | ||
| from typing_extensions import Literal | ||
@@ -59,3 +63,5 @@ | ||
| VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I) | ||
| VERSION_REGEX = re.compile( | ||
| r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE | ||
| ) | ||
@@ -74,3 +80,3 @@ | ||
| PEP508_IDENTIFIER_PATTERN = r"([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])" | ||
| PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.I) | ||
| PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.IGNORECASE) | ||
@@ -100,5 +106,5 @@ | ||
| _req.Requirement(value) | ||
| return True | ||
| except _req.InvalidRequirement: | ||
| return False | ||
| return True | ||
@@ -112,3 +118,3 @@ except ImportError: # pragma: no cover | ||
| def pep508(value: str) -> bool: | ||
| def pep508(value: str) -> bool: # noqa: ARG001 | ||
| return True | ||
@@ -172,3 +178,3 @@ | ||
| downloaded: typing.Union[None, "Literal[False]", typing.Set[str]] | ||
| downloaded: None | Literal[False] | set[str] | ||
| """ | ||
@@ -210,3 +216,3 @@ None => not cached yet | ||
| self.downloaded = set(_download_classifiers().splitlines()) | ||
| except Exception: | ||
| except Exception: # noqa: BLE001 | ||
| self.downloaded = False | ||
@@ -264,7 +270,7 @@ _logger.debug("Problem with download, skipping validation") | ||
| ) | ||
| if not (value.startswith("/") or value.startswith("\\") or "@" in value): | ||
| if not (value.startswith(("/", "\\")) or "@" in value): | ||
| parts = urlparse(f"http://{value}") | ||
| return bool(parts.scheme and parts.netloc) | ||
| except Exception: | ||
| except Exception: # noqa: BLE001 | ||
| return False | ||
@@ -275,7 +281,9 @@ | ||
| ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?" | ||
| ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I) | ||
| ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.IGNORECASE) | ||
| RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+" | ||
| RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I) | ||
| RECOMMEDED_ENTRYPOINT_REGEX = re.compile( | ||
| f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.IGNORECASE | ||
| ) | ||
| ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*" | ||
| ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I) | ||
| ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.IGNORECASE) | ||
@@ -381,3 +389,8 @@ | ||
| def uint(value: builtins.int) -> bool: | ||
| def uint32(value: builtins.int) -> bool: | ||
| r"""Unsigned 32-bit integer (:math:`0 \leq x < 2^{32}`)""" | ||
| return 0 <= value < 2**32 | ||
| def uint64(value: builtins.int) -> bool: | ||
| r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)""" | ||
@@ -387,2 +400,27 @@ return 0 <= value < 2**64 | ||
| def uint(value: builtins.int) -> bool: | ||
| r"""Signed 64-bit integer (:math:`0 \leq x < 2^{64}`)""" | ||
| return 0 <= value < 2**64 | ||
| def int8(value: builtins.int) -> bool: | ||
| r"""Signed 8-bit integer (:math:`-2^{7} \leq x < 2^{7}`)""" | ||
| return -(2**7) <= value < 2**7 | ||
| def int16(value: builtins.int) -> bool: | ||
| r"""Signed 16-bit integer (:math:`-2^{15} \leq x < 2^{15}`)""" | ||
| return -(2**15) <= value < 2**15 | ||
| def int32(value: builtins.int) -> bool: | ||
| r"""Signed 32-bit integer (:math:`-2^{31} \leq x < 2^{31}`)""" | ||
| return -(2**31) <= value < 2**31 | ||
| def int64(value: builtins.int) -> bool: | ||
| r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)""" | ||
| return -(2**63) <= value < 2**63 | ||
| def int(value: builtins.int) -> bool: | ||
@@ -402,5 +440,5 @@ r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)""" | ||
| _licenses.canonicalize_license_expression(value) | ||
| return True | ||
| except _licenses.InvalidLicenseExpression: | ||
| return False | ||
| return True | ||
@@ -414,3 +452,27 @@ except ImportError: # pragma: no cover | ||
| def SPDX(value: str) -> bool: | ||
| def SPDX(value: str) -> bool: # noqa: ARG001 | ||
| return True | ||
| VALID_IMPORT_NAME = re.compile( | ||
| r""" | ||
| ^ # start of string | ||
| [A-Za-z_][A-Za-z_0-9]+ # a valid Python identifier | ||
| (?:\.[A-Za-z_][A-Za-z_0-9]*)* # optionally followed by .identifier's | ||
| (?:\s*;\s*private)? # optionally followed by ; private | ||
| $ # end of string | ||
| """, | ||
| re.VERBOSE, | ||
| ) | ||
| def import_name(value: str) -> bool: | ||
| """This is a valid import name. It has to be series of python identifiers | ||
| (not keywords), separated by dots, optionally followed by a semicolon and | ||
| the keyword "private". | ||
| """ | ||
| if VALID_IMPORT_NAME.match(value) is None: | ||
| return False | ||
| idents, _, _ = value.partition(";") | ||
| return all(not keyword.iskeyword(ident) for ident in idents.rstrip().split(".")) |
| The code contained in this directory was automatically generated using the | ||
| following command: | ||
| python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t setuptools=setuptools/config/setuptools.schema.json -t distutils=setuptools/config/distutils.schema.json | ||
| python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t distutils=setuptools/config/distutils.schema.json -t setuptools=setuptools/config/setuptools.schema.json | ||
@@ -6,0 +6,0 @@ Please avoid changing it manually. |
@@ -137,2 +137,11 @@ """ | ||
| fields = ("import-names", "import-namespaces") | ||
| places = (project_table, project_table.get("dynamic", [])) | ||
| if any(field in place for field in fields for place in places): | ||
| raise NotImplementedError( | ||
| "Setuptools does not support `import-names` and `import-namespaces`" | ||
| " in `pyproject.toml` yet. If your are interested in this feature, " | ||
| " please consider submitting a contribution via pull requests." | ||
| ) | ||
| with _ignore_errors(ignore_option_errors): | ||
@@ -139,0 +148,0 @@ # Don't complain about unrelated errors (e.g. tools not using the "tool" table) |
@@ -119,3 +119,3 @@ { | ||
| "propertyNames": { | ||
| "anyOf": [{"type": "string", "format": "python-module-name"}, {"const": "*"}] | ||
| "anyOf": [{"$ref": "#/definitions/package-name"}, {"const": "*"}] | ||
| }, | ||
@@ -144,3 +144,3 @@ "patternProperties": { | ||
| "propertyNames": { | ||
| "anyOf": [{"type": "string", "format": "python-module-name"}, {"const": "*"}] | ||
| "anyOf": [{"$ref": "#/definitions/package-name"}, {"const": "*"}] | ||
| }, | ||
@@ -147,0 +147,0 @@ "patternProperties": { |
@@ -538,5 +538,10 @@ """Make sure that applying the configuration from pyproject.toml is equivalent to | ||
| class TestExtModules: | ||
| def make_dist(self, toml_config): | ||
| pyproject = Path("pyproject.toml") | ||
| pyproject.write_text(cleandoc(toml_config), encoding="utf-8") | ||
| with pytest.warns(pyprojecttoml._ExperimentalConfiguration): | ||
| return pyprojecttoml.apply_configuration(Distribution({}), pyproject) | ||
| def test_pyproject_sets_attribute(self, tmp_path, monkeypatch): | ||
| monkeypatch.chdir(tmp_path) | ||
| pyproject = Path("pyproject.toml") | ||
| toml_config = """ | ||
@@ -551,5 +556,3 @@ [project] | ||
| """ | ||
| pyproject.write_text(cleandoc(toml_config), encoding="utf-8") | ||
| with pytest.warns(pyprojecttoml._ExperimentalConfiguration): | ||
| dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject) | ||
| dist = self.make_dist(toml_config) | ||
| assert len(dist.ext_modules) == 1 | ||
@@ -559,3 +562,20 @@ assert dist.ext_modules[0].name == "my.ext" | ||
| def test_pyproject_define_macros_as_tuples(self, tmp_path, monkeypatch): | ||
| # https://github.com/pypa/setuptools/issues/4810 | ||
| monkeypatch.chdir(tmp_path) | ||
| toml_config = """ | ||
| [project] | ||
| name = "test" | ||
| version = "42.0" | ||
| [[tool.setuptools.ext-modules]] | ||
| name = "my.ext" | ||
| sources = ["hello.c", "world.c"] | ||
| define-macros = [["FIRST_SINGLE"], ["SECOND_TWO", "1"]] | ||
| """ | ||
| dist = self.make_dist(toml_config) | ||
| assert isinstance(dist.ext_modules[0].define_macros[0], tuple) | ||
| assert dist.ext_modules[0].define_macros[0] == ("FIRST_SINGLE",) | ||
| assert dist.ext_modules[0].define_macros[1] == ("SECOND_TWO", "1") | ||
| class TestDeprecatedFields: | ||
@@ -562,0 +582,0 @@ def test_namespace_packages(self, tmp_path): |
@@ -289,2 +289,25 @@ import re | ||
| class TestImportNames: | ||
| EXAMPLES = [ | ||
| 'import-names = ["hello", "world"]', | ||
| 'import-namespaces = ["hello", "world"]', | ||
| 'dynamic = ["import-names"]', | ||
| 'dynamic = ["import-namespaces"]', | ||
| ] | ||
| @pytest.mark.parametrize("example", EXAMPLES) | ||
| def test_not_implemented(self, monkeypatch, tmp_path, example): | ||
| monkeypatch.chdir(tmp_path) | ||
| pyproject = Path("pyproject.toml") | ||
| toml_config = f""" | ||
| [project] | ||
| name = 'proj' | ||
| version = '42' | ||
| {example} | ||
| """ | ||
| pyproject.write_text(cleandoc(toml_config), encoding="utf-8") | ||
| with pytest.raises(NotImplementedError, match='import-names'): | ||
| apply_configuration(Distribution({}), pyproject) | ||
| @pytest.mark.parametrize( | ||
@@ -291,0 +314,0 @@ "example", |
@@ -80,26 +80,30 @@ """Tests for the 'setuptools' package""" | ||
| @pytest.fixture | ||
| def sample_module(self, monkeypatch, tmp_path): | ||
| monkeypatch.syspath_prepend(str(tmp_path)) | ||
| module = "mod_with_version" | ||
| version = "2.0.9" | ||
| file = tmp_path / f"{module}.py" | ||
| file.write_text(f"__version__ = {version!r}", encoding="utf-8") | ||
| return (module, version) | ||
| @needs_bytecode | ||
| def testModuleExtract(self): | ||
| from json import __version__ | ||
| assert dep.get_module_constant('json', '__version__') == __version__ | ||
| def testModuleExtract(self, sample_module): | ||
| (module, version) = sample_module | ||
| assert dep.get_module_constant(module, '__version__') == version | ||
| assert dep.get_module_constant('sys', 'version') == sys.version | ||
| assert ( | ||
| dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') | ||
| == __doc__ | ||
| ) | ||
| assert dep.get_module_constant(__name__, '__doc__') == __doc__ | ||
| @needs_bytecode | ||
| def testRequire(self): | ||
| req = Require('Json', '1.0.3', 'json') | ||
| def testRequire(self, sample_module): | ||
| (module, version) = sample_module | ||
| req = Require('GivenName', '1.0.3', module) | ||
| assert req.name == 'Json' | ||
| assert req.module == 'json' | ||
| assert req.name == 'GivenName' | ||
| assert req.module == module | ||
| assert req.requested_version == Version('1.0.3') | ||
| assert req.attribute == '__version__' | ||
| assert req.full_name() == 'Json-1.0.3' | ||
| assert req.full_name() == 'GivenName-1.0.3' | ||
| from json import __version__ | ||
| assert str(req.get_version()) == __version__ | ||
| assert str(req.get_version()) == version | ||
| assert req.version_ok('1.0.9') | ||
@@ -106,0 +110,0 @@ assert not req.version_ok('0.9.1') |
+1
-2
@@ -25,3 +25,2 @@ [testenv] | ||
| SSH_AUTH_SOCK # for exercise.py if repo was checked out with ssh | ||
| windir # required for test_pkg_resources | ||
| # honor git config in pytest-perf | ||
@@ -87,3 +86,3 @@ HOME | ||
| deps = | ||
| validate-pyproject[all]==0.23 | ||
| validate-pyproject[all]==0.25 | ||
| commands = | ||
@@ -90,0 +89,0 @@ python -m tools.generate_validation_code |
Sorry, the diff of this file is too big to display
| <?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
| <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> | ||
| <assemblyIdentity version="1.0.0.0" | ||
| processorArchitecture="X86" | ||
| name="%(name)s" | ||
| type="win32"/> | ||
| <!-- Identify the application security requirements. --> | ||
| <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> | ||
| <security> | ||
| <requestedPrivileges> | ||
| <requestedExecutionLevel level="asInvoker" uiAccess="false"/> | ||
| </requestedPrivileges> | ||
| </security> | ||
| </trustInfo> | ||
| </assembly> |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
4435986
0.67%510
0.2%70175
0.32%