From 2cf1b25533d43b0398510202a6f38ca6746bb22e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 6 May 2025 17:27:03 +0100 Subject: [PATCH 1/6] Reduce the amount of patching in build_meta and setup() in favour of explicit CLI flags --- setuptools/__init__.py | 65 ++++++++++++++++--------------------- setuptools/build_meta.py | 69 +++++++--------------------------------- 2 files changed, 39 insertions(+), 95 deletions(-) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index f1b9bfe9b8..cfd040a73d 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -11,8 +11,8 @@ import os import sys from abc import abstractmethod -from collections.abc import Mapping -from typing import TYPE_CHECKING, TypeVar, overload +from collections.abc import MutableMapping +from typing import TYPE_CHECKING, Any, TypeVar, overload sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip # workaround for #4476 @@ -49,40 +49,19 @@ find_namespace_packages = PEP420PackageFinder.find -def _install_setup_requires(attrs): - # Note: do not use `setuptools.Distribution` directly, as - # our PEP 517 backend patch `distutils.core.Distribution`. - class MinimalDistribution(distutils.core.Distribution): - """ - A minimal version of a distribution for supporting the - fetch_build_eggs interface. - """ +def _expand_setupcfg(attrs: MutableMapping[str, Any]) -> Distribution: + """Bare minimum setup.cfg parsing so that we can extract setup_requires""" + from setuptools.config.setupcfg import _apply + + dist = Distribution(attrs) + dist.set_defaults._disable() + if os.path.exists("setup.cfg"): + _apply(dist, "setup.cfg", ignore_option_errors=True) + return dist + - def __init__(self, attrs: Mapping[str, object]) -> None: - _incl = 'dependency_links', 'setup_requires' - filtered = {k: attrs[k] for k in set(_incl) & set(attrs)} - super().__init__(filtered) - # Prevent accidentally triggering discovery with incomplete set of attrs - self.set_defaults._disable() - - def _get_project_config_files(self, filenames=None): - """Ignore ``pyproject.toml``, they are not related to setup_requires""" - try: - cfg, _toml = super()._split_standard_project_metadata(filenames) - except Exception: - return filenames, () - return cfg, () - - def finalize_options(self): - """ - Disable finalize_options to avoid building the working set. - Ref #2158. - """ - - dist = MinimalDistribution(attrs) - - # Honor setup.cfg's options. - dist.parse_config_files(ignore_option_errors=True) +def _install_setup_requires(attrs: MutableMapping[str, Any]) -> None: + dist = _expand_setupcfg(attrs) if dist.setup_requires: _fetch_build_eggs(dist) @@ -109,9 +88,16 @@ def _fetch_build_eggs(dist: Distribution): def setup(**attrs): + if "--private-interrupt-setuppy" in sys.argv: + raise _SetupPyInterruption(_expand_setupcfg(attrs)) + logging.configure() - # Make sure we have any requirements needed to interpret 'attrs'. - _install_setup_requires(attrs) + + if "--private-skip-setup-requires" in sys.argv: + sys.argv.remove("--private-skip-setup-requires") + else: + # Make sure we have any requirements needed to interpret 'attrs'. + _install_setup_requires(attrs) return distutils.core.setup(**attrs) @@ -244,5 +230,10 @@ class sic(str): """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)""" +class _SetupPyInterruption(Exception): + def __init__(self, dist: Distribution): + self.dist = dist + + # Apply monkey patches monkey.patch_all() diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 8f2e930c73..dffc4fa2e5 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -48,7 +48,6 @@ from ._reqs import parse_strings from .warnings import SetuptoolsDeprecationWarning -import distutils from distutils.util import strtobool if TYPE_CHECKING: @@ -64,53 +63,9 @@ 'prepare_metadata_for_build_editable', 'build_editable', '__legacy__', - 'SetupRequirementsError', ] -class SetupRequirementsError(BaseException): - def __init__(self, specifiers) -> None: - self.specifiers = specifiers - - -class Distribution(setuptools.dist.Distribution): - def fetch_build_eggs(self, specifiers): - specifier_list = list(parse_strings(specifiers)) - - raise SetupRequirementsError(specifier_list) - - @classmethod - @contextlib.contextmanager - def patch(cls): - """ - Replace - distutils.dist.Distribution with this class - for the duration of this context. - """ - orig = distutils.core.Distribution - distutils.core.Distribution = cls # type: ignore[misc] # monkeypatching - try: - yield - finally: - distutils.core.Distribution = orig # type: ignore[misc] # monkeypatching - - -@contextlib.contextmanager -def no_install_setup_requires(): - """Temporarily disable installing setup_requires - - Under PEP 517, the backend reports build dependencies to the frontend, - and the frontend is responsible for ensuring they're installed. - So setuptools (acting as a backend) should not try to install them. - """ - orig = setuptools._install_setup_requires - setuptools._install_setup_requires = lambda attrs: None - try: - yield - finally: - setuptools._install_setup_requires = orig - - def _get_immediate_subdirectories(a_dir): return [ name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name)) @@ -291,16 +246,14 @@ class _BuildMetaBackend(_ConfigSettingsTranslator): def _get_build_requires( self, config_settings: _ConfigSettings, requirements: list[str] ): - sys.argv = [ - *sys.argv[:1], - *self._global_args(config_settings), - "egg_info", - ] + sys.argv = [*sys.argv[:1], "--private-interrupt-setuppy"] try: - with Distribution.patch(): - self.run_setup() - except SetupRequirementsError as e: - requirements += e.specifiers + self.run_setup() + except setuptools._SetupPyInterruption as ex: + setup_requires = parse_strings(ex.dist.setup_requires or "") + return requirements + list(setup_requires) + except Exception: + pass # Ignore other arbitrary exceptions from setup.py, e.g. SystemExit return requirements @@ -313,6 +266,8 @@ def run_setup(self, setup_script: str = 'setup.py'): with _open_setup_script(__file__) as f: code = f.read().replace(r'\r\n', r'\n') + sys.argv.append("--private-skip-setup-requires") + try: exec(code, locals()) except SystemExit as e: @@ -370,8 +325,7 @@ def prepare_metadata_for_build_wheel( str(metadata_directory), "--keep-egg-info", ] - with no_install_setup_requires(): - self.run_setup() + self.run_setup() self._bubble_up_info_directory(metadata_directory, ".egg-info") return self._bubble_up_info_directory(metadata_directory, ".dist-info") @@ -400,8 +354,7 @@ def _build_with_temp_dir( tmp_dist_dir, *arbitrary_args, ] - with no_install_setup_requires(): - self.run_setup() + self.run_setup() result_basename = _file_with_extension(tmp_dist_dir, result_extension) result_path = os.path.join(result_directory, result_basename) From 56045d707dd052192504f753a6fbf005655b9609 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 12 May 2025 16:30:20 +0100 Subject: [PATCH 2/6] Use compat module for PEP 678 insted of custom fallback --- setuptools/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index cfd040a73d..8943f9b618 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -21,6 +21,7 @@ import _distutils_hack.override # noqa: F401 from . import logging, monkey +from .compat import py310 from .depends import Require from .discovery import PackageFinder, PEP420PackageFinder from .dist import Distribution @@ -80,10 +81,7 @@ def _fetch_build_eggs(dist: Distribution): please contact that package's maintainers or distributors. """ if "InvalidVersion" in ex.__class__.__name__: - if hasattr(ex, "add_note"): - ex.add_note(msg) # PEP 678 - else: - dist.announce(f"\n{msg}\n") + py310.add_note(ex, msg) raise From 721a3099d84339c6f8de5198ec23951a53c3229d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 12 May 2025 16:42:20 +0100 Subject: [PATCH 3/6] Preserve SetupRequirementsError as deprecated for backwards compatibility --- setuptools/build_meta.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index dffc4fa2e5..84438df583 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -499,3 +499,20 @@ class _IncompatibleBdistWheel(SetuptoolsDeprecationWarning): # The legacy backend __legacy__ = _BuildMetaLegacyBackend() + + +def __getattr__(name): + if name == "SetupRequirementsError": + SetuptoolsDeprecationWarning.emit( + "SetupRequirementsError is no longer part of the public API.", + "Please do not import SetupRequirementsError.", + due_date=(2026, 5, 12), + ) + + class SetupRequirementsError(BaseException): + def __init__(self, specifiers) -> None: + self.specifiers = specifiers + + return SetupRequirementsError + + raise AttributeError(name) From 5f32d3f48966619fb01d44f26217ac625539a1b5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 12 May 2025 17:25:18 +0100 Subject: [PATCH 4/6] Ensure dist-info command can be bootstrapped --- bootstrap.egg-info/entry_points.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/bootstrap.egg-info/entry_points.txt b/bootstrap.egg-info/entry_points.txt index a21ca22709..c84c0e8ab9 100644 --- a/bootstrap.egg-info/entry_points.txt +++ b/bootstrap.egg-info/entry_points.txt @@ -1,5 +1,6 @@ [distutils.commands] egg_info = setuptools.command.egg_info:egg_info +dist_info = setuptools.command.dist_info:dist_info build_py = setuptools.command.build_py:build_py sdist = setuptools.command.sdist:sdist editable_wheel = setuptools.command.editable_wheel:editable_wheel From 103761cd9600ff8ffb8b09df163de9b790988618 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 19 May 2025 12:37:26 +0100 Subject: [PATCH 5/6] Simplify type annotations --- setuptools/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 8943f9b618..dc66c54ab8 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -11,7 +11,6 @@ import os import sys from abc import abstractmethod -from collections.abc import MutableMapping from typing import TYPE_CHECKING, Any, TypeVar, overload sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip @@ -50,7 +49,7 @@ find_namespace_packages = PEP420PackageFinder.find -def _expand_setupcfg(attrs: MutableMapping[str, Any]) -> Distribution: +def _expand_setupcfg(attrs: dict[str, Any]) -> Distribution: """Bare minimum setup.cfg parsing so that we can extract setup_requires""" from setuptools.config.setupcfg import _apply @@ -61,7 +60,7 @@ def _expand_setupcfg(attrs: MutableMapping[str, Any]) -> Distribution: return dist -def _install_setup_requires(attrs: MutableMapping[str, Any]) -> None: +def _install_setup_requires(attrs: dict[str, Any]) -> None: dist = _expand_setupcfg(attrs) if dist.setup_requires: _fetch_build_eggs(dist) From 61b13d90c95616171d8985b84c1fe0264ef1effb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 19 May 2025 12:45:20 +0100 Subject: [PATCH 6/6] Add comment about assumptions --- setuptools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index dc66c54ab8..c0aeb7e15a 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -55,7 +55,7 @@ def _expand_setupcfg(attrs: dict[str, Any]) -> Distribution: dist = Distribution(attrs) dist.set_defaults._disable() - if os.path.exists("setup.cfg"): + if os.path.exists("setup.cfg"): # Assumes no other config contains setup_requires _apply(dist, "setup.cfg", ignore_option_errors=True) return dist