From 32138db92eed5f6ce6180acabdc605db547acd24 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 12 Aug 2024 13:50:04 -0400 Subject: [PATCH 01/20] Update platformdirs to >= 4.2.2 --- newsfragments/4560.misc.rst | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4560.misc.rst diff --git a/newsfragments/4560.misc.rst b/newsfragments/4560.misc.rst new file mode 100644 index 0000000000..0878f09abd --- /dev/null +++ b/newsfragments/4560.misc.rst @@ -0,0 +1 @@ +Bumped declared ``platformdirs`` dependency to ``>= 4.2.2`` to help platforms lacking `ctypes` support install setuptools seamlessly -- by :user:`Avasam` diff --git a/pyproject.toml b/pyproject.toml index f623e16445..79dc7e8466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ core = [ "wheel>=0.43.0", # pkg_resources - "platformdirs >= 2.6.2", + "platformdirs >= 4.2.2", # Made ctypes optional (see #4461) # for distutils "jaraco.collections", From d457d0e87889aefe2093cd79ab4d1ee35d3101e7 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 20 Aug 2024 12:57:29 -0400 Subject: [PATCH 02/20] Type sequence checks in setuptools/dist.py --- newsfragments/4578.bugfix.rst | 1 + newsfragments/4578.feature.rst | 1 + setuptools/dist.py | 48 ++++++++++++++++++++++++---------- setuptools/tests/test_dist.py | 8 +++--- 4 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 newsfragments/4578.bugfix.rst create mode 100644 newsfragments/4578.feature.rst diff --git a/newsfragments/4578.bugfix.rst b/newsfragments/4578.bugfix.rst new file mode 100644 index 0000000000..e9bde46269 --- /dev/null +++ b/newsfragments/4578.bugfix.rst @@ -0,0 +1 @@ +Fix a `TypeError` when a ``Distribution``'s old included attribute was a `tuple` -- by :user:`Avasam` diff --git a/newsfragments/4578.feature.rst b/newsfragments/4578.feature.rst new file mode 100644 index 0000000000..48f57edce3 --- /dev/null +++ b/newsfragments/4578.feature.rst @@ -0,0 +1 @@ +Made errors when parsing ``Distribution`` data more explicit about the expected type (``tuple[str, ...] | list[str]``) -- by :user:`Avasam` diff --git a/setuptools/dist.py b/setuptools/dist.py index 7c516fefb8..bb9a2a9951 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -8,7 +8,7 @@ import sys from glob import iglob from pathlib import Path -from typing import TYPE_CHECKING, MutableMapping +from typing import TYPE_CHECKING, List, MutableMapping, NoReturn, Tuple, Union, overload from more_itertools import partition, unique_everseen from packaging.markers import InvalidMarker, Marker @@ -21,6 +21,7 @@ command as _, # noqa: F401 # imported for side-effects ) from ._importlib import metadata +from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery from .monkey import get_unpatched @@ -36,9 +37,22 @@ from distutils.fancy_getopt import translate_longopt from distutils.util import strtobool +if TYPE_CHECKING: + from typing_extensions import TypeAlias + __all__ = ['Distribution'] sequence = tuple, list +""" +Supported iterable types that are known to be: +- ordered (which `set` isn't) +- not match a str (which `Sequence[str]` does) +- not imply a nested type (like `dict`) +for use with `isinstance`. +""" +_Sequence: TypeAlias = Union[Tuple[str, ...], List[str]] +# This is how stringifying _Sequence would look in Python 3.10 +_requence_type_repr = "tuple[str, ...] | list[str]" def check_importable(dist, attr, value): @@ -51,7 +65,7 @@ def check_importable(dist, attr, value): ) from e -def assert_string_list(dist, attr, value): +def assert_string_list(dist, attr: str, value: _Sequence) -> None: """Verify that value is a string list""" try: # verify that value is a list or tuple to exclude unordered @@ -61,7 +75,7 @@ def assert_string_list(dist, attr, value): assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError) as e: raise DistutilsSetupError( - "%r must be a list of strings (got %r)" % (attr, value) + f"{attr!r} must be of type <{_requence_type_repr}> (got {value!r})" ) from e @@ -138,7 +152,11 @@ def invalid_unless_false(dist, attr, value): raise DistutilsSetupError(f"{attr} is invalid.") -def check_requirements(dist, attr, value): +@overload +def check_requirements(dist, attr: str, value: set | dict) -> NoReturn: ... +@overload +def check_requirements(dist, attr: str, value: _StrOrIter) -> None: ... +def check_requirements(dist, attr: str, value: _StrOrIter) -> None: """Verify that install_requires is a valid requirements list""" try: list(_reqs.parse(value)) @@ -146,10 +164,10 @@ def check_requirements(dist, attr, value): raise TypeError("Unordered types are not allowed") except (TypeError, ValueError) as error: tmpl = ( - "{attr!r} must be a string or list of strings " - "containing valid project/version requirement specifiers; {error}" + f"{attr!r} must be a string or iterable of strings " + f"containing valid project/version requirement specifiers; {error}" ) - raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error + raise DistutilsSetupError(tmpl) from error def check_specifier(dist, attr, value): @@ -767,11 +785,11 @@ def has_contents_for(self, package): return False - def _exclude_misc(self, name, value): + def _exclude_misc(self, name: str, value: _Sequence) -> None: """Handle 'exclude()' for list/tuple attrs without a special handler""" if not isinstance(value, sequence): raise DistutilsSetupError( - "%s: setting must be a list or tuple (%r)" % (name, value) + f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" ) try: old = getattr(self, name) @@ -784,11 +802,13 @@ def _exclude_misc(self, name, value): elif old: setattr(self, name, [item for item in old if item not in value]) - def _include_misc(self, name, value): + def _include_misc(self, name: str, value: _Sequence) -> None: """Handle 'include()' for list/tuple attrs without a special handler""" if not isinstance(value, sequence): - raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value)) + raise DistutilsSetupError( + f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" + ) try: old = getattr(self, name) except AttributeError as e: @@ -801,7 +821,7 @@ def _include_misc(self, name, value): ) else: new = [item for item in value if item not in old] - setattr(self, name, old + new) + setattr(self, name, list(old) + new) def exclude(self, **attrs): """Remove items from distribution that are named in keyword arguments @@ -826,10 +846,10 @@ def exclude(self, **attrs): else: self._exclude_misc(k, v) - def _exclude_packages(self, packages): + def _exclude_packages(self, packages: _Sequence) -> None: if not isinstance(packages, sequence): raise DistutilsSetupError( - "packages: setting must be a list or tuple (%r)" % (packages,) + f"packages: setting must be of type <{_requence_type_repr}> (got {packages!r})" ) list(map(self.exclude_package, packages)) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 597785b519..fde0de99ac 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -118,8 +118,8 @@ def test_provides_extras_deterministic_order(): 'hello': '*.msg', }, ( - "\"values of 'package_data' dict\" " - "must be a list of strings (got '*.msg')" + "\"values of 'package_data' dict\" must be of type " + " (got '*.msg')" ), ), # Invalid value type (generators are single use) @@ -128,8 +128,8 @@ def test_provides_extras_deterministic_order(): 'hello': (x for x in "generator"), }, ( - "\"values of 'package_data' dict\" must be a list of strings " - "(got " + " (got Date: Tue, 27 Aug 2024 11:32:59 +0100 Subject: [PATCH 03/20] Use variable msg instead of tmpl in setuptools/dist --- setuptools/dist.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index bb9a2a9951..b5d78aa37d 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -140,8 +140,7 @@ def _check_marker(marker): def assert_bool(dist, attr, value): """Verify that value is True, False, 0, or 1""" if bool(value) != value: - tmpl = "{attr!r} must be a boolean value (got {value!r})" - raise DistutilsSetupError(tmpl.format(attr=attr, value=value)) + raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})") def invalid_unless_false(dist, attr, value): @@ -163,11 +162,11 @@ def check_requirements(dist, attr: str, value: _StrOrIter) -> None: if isinstance(value, (dict, set)): raise TypeError("Unordered types are not allowed") except (TypeError, ValueError) as error: - tmpl = ( + msg = ( f"{attr!r} must be a string or iterable of strings " f"containing valid project/version requirement specifiers; {error}" ) - raise DistutilsSetupError(tmpl) from error + raise DistutilsSetupError(msg) from error def check_specifier(dist, attr, value): @@ -175,8 +174,8 @@ def check_specifier(dist, attr, value): try: SpecifierSet(value) except (InvalidSpecifier, AttributeError) as error: - tmpl = "{attr!r} must be a string containing valid version specifiers; {error}" - raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error + msg = f"{attr!r} must be a string containing valid version specifiers; {error}" + raise DistutilsSetupError(msg) from error def check_entry_points(dist, attr, value): From 000a413e2af9c271166cebe6909ad664907887f1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 27 Aug 2024 11:49:33 +0100 Subject: [PATCH 04/20] Deprecate public access to setuptools.dist.sequence --- setuptools/dist.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index b5d78aa37d..f22e3eea54 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -8,7 +8,16 @@ import sys from glob import iglob from pathlib import Path -from typing import TYPE_CHECKING, List, MutableMapping, NoReturn, Tuple, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + List, + MutableMapping, + NoReturn, + Tuple, + Union, + overload, +) from more_itertools import partition, unique_everseen from packaging.markers import InvalidMarker, Marker @@ -42,8 +51,10 @@ __all__ = ['Distribution'] -sequence = tuple, list +_sequence = tuple, list """ +:meta private: + Supported iterable types that are known to be: - ordered (which `set` isn't) - not match a str (which `Sequence[str]` does) @@ -55,6 +66,17 @@ _requence_type_repr = "tuple[str, ...] | list[str]" +def __getattr__(name: str) -> Any: # pragma: no cover + if name == "sequence": + SetuptoolsDeprecationWarning.emit( + "`setuptools.dist.sequence` is an internal implementation detail.", + "Please define your own `sequence = tuple, list` instead.", + due_date=(2025, 8, 28), # Originally added on 2024-08-27 + ) + return _sequence + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + def check_importable(dist, attr, value): try: ep = metadata.EntryPoint(value=value, name=None, group=None) @@ -70,7 +92,7 @@ def assert_string_list(dist, attr: str, value: _Sequence) -> None: try: # verify that value is a list or tuple to exclude unordered # or single-use iterables - assert isinstance(value, sequence) + assert isinstance(value, _sequence) # verify that elements of value are strings assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError) as e: @@ -786,7 +808,7 @@ def has_contents_for(self, package): def _exclude_misc(self, name: str, value: _Sequence) -> None: """Handle 'exclude()' for list/tuple attrs without a special handler""" - if not isinstance(value, sequence): + if not isinstance(value, _sequence): raise DistutilsSetupError( f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" ) @@ -794,7 +816,7 @@ def _exclude_misc(self, name: str, value: _Sequence) -> None: old = getattr(self, name) except AttributeError as e: raise DistutilsSetupError("%s: No such distribution setting" % name) from e - if old is not None and not isinstance(old, sequence): + if old is not None and not isinstance(old, _sequence): raise DistutilsSetupError( name + ": this setting cannot be changed via include/exclude" ) @@ -804,7 +826,7 @@ def _exclude_misc(self, name: str, value: _Sequence) -> None: def _include_misc(self, name: str, value: _Sequence) -> None: """Handle 'include()' for list/tuple attrs without a special handler""" - if not isinstance(value, sequence): + if not isinstance(value, _sequence): raise DistutilsSetupError( f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" ) @@ -814,7 +836,7 @@ def _include_misc(self, name: str, value: _Sequence) -> None: raise DistutilsSetupError("%s: No such distribution setting" % name) from e if old is None: setattr(self, name, value) - elif not isinstance(old, sequence): + elif not isinstance(old, _sequence): raise DistutilsSetupError( name + ": this setting cannot be changed via include/exclude" ) @@ -846,7 +868,7 @@ def exclude(self, **attrs): self._exclude_misc(k, v) def _exclude_packages(self, packages: _Sequence) -> None: - if not isinstance(packages, sequence): + if not isinstance(packages, _sequence): raise DistutilsSetupError( f"packages: setting must be of type <{_requence_type_repr}> (got {packages!r})" ) From 96be735ca2e77b7db876133dfda0b4df3ced4ac0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Oct 2024 18:12:02 +0100 Subject: [PATCH 05/20] Workaround for bdist_wheel.dist_info_dir problems --- setuptools/build_meta.py | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index ecf434bbf3..3231105e69 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -417,17 +417,27 @@ def build_wheel( config_settings: _ConfigSettings = None, metadata_directory: StrPath | None = None, ): - cmd = ['bdist_wheel'] - if metadata_directory: - cmd.extend(['--dist-info-dir', metadata_directory]) - with suppress_known_deprecation(): - return self._build_with_temp_dir( - cmd, - '.whl', - wheel_directory, - config_settings, - self._arbitrary_args(config_settings), - ) + def _build(cmd: list[str]): + with suppress_known_deprecation(): + return self._build_with_temp_dir( + cmd, + '.whl', + wheel_directory, + config_settings, + self._arbitrary_args(config_settings), + ) + + if metadata_directory is None: + return _build(['bdist_wheel']) + + try: + return _build(['bdist_wheel', '--dist-info-dir', metadata_directory]) + except SystemExit as ex: + # pypa/setuptools#4683 + if "--dist-info-dir" not in str(ex): + raise + _IncompatibleBdistWheel.emit() + return _build(['bdist_wheel']) def build_sdist( self, sdist_directory: StrPath, config_settings: _ConfigSettings = None @@ -514,6 +524,17 @@ def run_setup(self, setup_script='setup.py'): sys.argv[0] = sys_argv_0 +class _IncompatibleBdistWheel(SetuptoolsDeprecationWarning): + _SUMMARY = "wheel.bdist_wheel is deprecated, please import it from setuptools" + _DETAILS = """ + Ensure that any custom bdist_wheel implementation is a subclass of + setuptools.command.bdist_wheel.bdist_wheel. + """ + _DUE_DATE = (2025, 10, 15) + # Initially introduced in 2024/10/15, but maybe too disruptive to be enforced? + _SEE_URL = "https://github.com/pypa/wheel/pull/631" + + # The primary backend _BACKEND = _BuildMetaBackend() From a663287c9c5f0bfc5e05addfb3a15fea7fc716c3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Oct 2024 20:31:58 +0100 Subject: [PATCH 06/20] Add pragma for edge-case code path --- setuptools/build_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 3231105e69..19d0e1688e 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -432,7 +432,7 @@ def _build(cmd: list[str]): try: return _build(['bdist_wheel', '--dist-info-dir', metadata_directory]) - except SystemExit as ex: + except SystemExit as ex: # pragma: nocover # pypa/setuptools#4683 if "--dist-info-dir" not in str(ex): raise From 50b732a4006f3b84315d4473f7c203e4fe13aed9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Oct 2024 20:34:44 +0100 Subject: [PATCH 07/20] Check for more specific error message --- setuptools/build_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 19d0e1688e..e730a27f25 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -434,7 +434,7 @@ def _build(cmd: list[str]): return _build(['bdist_wheel', '--dist-info-dir', metadata_directory]) except SystemExit as ex: # pragma: nocover # pypa/setuptools#4683 - if "--dist-info-dir" not in str(ex): + if "--dist-info-dir not recognized" not in str(ex): raise _IncompatibleBdistWheel.emit() return _build(['bdist_wheel']) From 0534fde847e0bd0c2214d6821c042c0eb5c0ffc3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Oct 2024 20:34:54 +0100 Subject: [PATCH 08/20] Add news fragment --- newsfragments/4684.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/4684.bugfix.rst diff --git a/newsfragments/4684.bugfix.rst b/newsfragments/4684.bugfix.rst new file mode 100644 index 0000000000..40f554cccc --- /dev/null +++ b/newsfragments/4684.bugfix.rst @@ -0,0 +1,2 @@ +Add workaround for ``bdist_wheel --dist-info-dir`` errors +when customisation does not inherit from setuptools. From 61a5a03fbf8acc59e6e12144011aa06b85162bda Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Oct 2024 11:04:49 +0100 Subject: [PATCH 09/20] =?UTF-8?q?Bump=20version:=2075.1.1=20=E2=86=92=2075?= =?UTF-8?q?.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- NEWS.rst | 17 +++++++++++++++++ newsfragments/4578.bugfix.rst | 1 - newsfragments/4578.feature.rst | 1 - newsfragments/4684.bugfix.rst | 2 -- pyproject.toml | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) delete mode 100644 newsfragments/4578.bugfix.rst delete mode 100644 newsfragments/4578.feature.rst delete mode 100644 newsfragments/4684.bugfix.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5247cca130..91921ce92d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 75.1.1 +current_version = 75.2.0 commit = True tag = True diff --git a/NEWS.rst b/NEWS.rst index 75ef319dd5..e79b45a623 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,20 @@ +v75.2.0 +======= + +Features +-------- + +- Made errors when parsing ``Distribution`` data more explicit about the expected type (``tuple[str, ...] | list[str]``) -- by :user:`Avasam` (#4578) + + +Bugfixes +-------- + +- Fix a `TypeError` when a ``Distribution``'s old included attribute was a `tuple` -- by :user:`Avasam` (#4578) +- Add workaround for ``bdist_wheel --dist-info-dir`` errors + when customisation does not inherit from setuptools. (#4684) + + v75.1.1 ======= diff --git a/newsfragments/4578.bugfix.rst b/newsfragments/4578.bugfix.rst deleted file mode 100644 index e9bde46269..0000000000 --- a/newsfragments/4578.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix a `TypeError` when a ``Distribution``'s old included attribute was a `tuple` -- by :user:`Avasam` diff --git a/newsfragments/4578.feature.rst b/newsfragments/4578.feature.rst deleted file mode 100644 index 48f57edce3..0000000000 --- a/newsfragments/4578.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Made errors when parsing ``Distribution`` data more explicit about the expected type (``tuple[str, ...] | list[str]``) -- by :user:`Avasam` diff --git a/newsfragments/4684.bugfix.rst b/newsfragments/4684.bugfix.rst deleted file mode 100644 index 40f554cccc..0000000000 --- a/newsfragments/4684.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add workaround for ``bdist_wheel --dist-info-dir`` errors -when customisation does not inherit from setuptools. diff --git a/pyproject.toml b/pyproject.toml index c27a988afd..bfa4d154a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ backend-path = ["."] [project] name = "setuptools" -version = "75.1.1" +version = "75.2.0" authors = [ { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, ] From ecde60bfa50de155aa88c3410bd00b7dbaa0afd4 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Aug 2024 10:54:32 -0400 Subject: [PATCH 10/20] Update mypy.ini from skeleton --- mypy.ini | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 7de7e5a508..a9d0fdd7df 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,17 +1,27 @@ [mypy] -# CI should test for all versions, local development gets hints for oldest supported -# But our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually. -# python_version = 3.8 +## upstream + +# Is the project well-typed? strict = False + +# Early opt-in even when strict = False warn_unused_ignores = True warn_redundant_casts = True -# required to support namespace packages: https://github.com/python/mypy/issues/14057 +enable_error_code = ignore-without-code + +# Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True disable_error_code = # Disable due to many false positives overload-overlap, +## local + +# CI should test for all versions, local development gets hints for oldest supported +# But our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually. +# python_version = 3.8 + exclude = (?x)( # Avoid scanning Python files in generated folders ^build/ From 3403ffd553a8781f07cadd94ecc1680d8e2003c2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Aug 2024 11:59:12 -0400 Subject: [PATCH 11/20] Re-enable mypy & Resolve all [ignore-without-code] --- mypy.ini | 2 +- pkg_resources/__init__.py | 2 +- pyproject.toml | 4 ---- setuptools/build_meta.py | 2 +- setuptools/config/expand.py | 13 ++++++++----- setuptools/config/pyprojecttoml.py | 4 ++-- setuptools/msvc.py | 2 +- setuptools/tests/test_build_ext.py | 2 +- setuptools/tests/test_editable_install.py | 14 +++++++++----- 9 files changed, 24 insertions(+), 21 deletions(-) diff --git a/mypy.ini b/mypy.ini index a9d0fdd7df..26692755ea 100644 --- a/mypy.ini +++ b/mypy.ini @@ -64,6 +64,6 @@ ignore_missing_imports = True # Even when excluding a module, import issues can show up due to following import # https://github.com/python/mypy/issues/11936#issuecomment-1466764006 -[mypy-setuptools.config._validate_pyproject.*,setuptools._distutils.*] +[mypy-setuptools.config._validate_pyproject.*,setuptools._vendor.*,setuptools._distutils.*] follow_imports = silent # silent => ignore errors when following imports diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 4e9b83d83d..f1f0ef2535 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2777,7 +2777,7 @@ def load( if require: # We could pass `env` and `installer` directly, # but keeping `*args` and `**kwargs` for backwards compatibility - self.require(*args, **kwargs) # type: ignore + self.require(*args, **kwargs) # type: ignore[arg-type] return self.resolve() def resolve(self) -> _ResolvedEntryPoint: diff --git a/pyproject.toml b/pyproject.toml index bfa4d154a7..eb6ec11041 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,7 +216,3 @@ formats = "zip" [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index e730a27f25..a3c83c7002 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -431,7 +431,7 @@ def _build(cmd: list[str]): return _build(['bdist_wheel']) try: - return _build(['bdist_wheel', '--dist-info-dir', metadata_directory]) + return _build(['bdist_wheel', '--dist-info-dir', str(metadata_directory)]) except SystemExit as ex: # pragma: nocover # pypa/setuptools#4683 if "--dist-info-dir not recognized" not in str(ex): diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index e11bcf9b42..8f2040fefa 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -203,7 +203,8 @@ def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType: 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 + assert spec.loader is not None + spec.loader.exec_module(module) return module @@ -285,10 +286,11 @@ def find_packages( from setuptools.discovery import construct_package_dir - if namespaces: - from setuptools.discovery import PEP420PackageFinder as PackageFinder + # check "not namespaces" first due to python/mypy#6232 + if not namespaces: + from setuptools.discovery import PackageFinder else: - from setuptools.discovery import PackageFinder # type: ignore + from setuptools.discovery import PEP420PackageFinder as PackageFinder root_dir = root_dir or os.curdir where = kwargs.pop('where', ['.']) @@ -359,7 +361,8 @@ def entry_points(text: str, text_source="entry-points") -> dict[str, dict]: 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 + # Using undocumented behaviour, see python/typeshed#12700 + parser = ConfigParser(default_section=None, delimiters=("=",)) # type: ignore[call-overload] parser.optionxform = str # case sensitive parser.read_string(text, text_source) groups = {k: dict(v.items()) for k, v in parser.items()} diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 5d95e18b83..e0040cefbd 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -44,8 +44,8 @@ def validate(config: dict, filepath: StrPath) -> bool: 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 + # Improve reproducibility by default. See abravalheri/validate-pyproject#31 + trove_classifier._disable_download() # type: ignore[union-attr] try: return validator.validate(config) diff --git a/setuptools/msvc.py b/setuptools/msvc.py index de4b05f928..7ee685e023 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -1418,7 +1418,7 @@ def VCRuntimeRedist(self) -> str | None: os.path.join(prefix, arch_subdir, crt_dir, vcruntime) for (prefix, crt_dir) in itertools.product(prefixes, crt_dirs) ) - return next(filter(os.path.isfile, candidate_paths), None) + return next(filter(os.path.isfile, candidate_paths), None) # type: ignore[arg-type] #python/mypy#12682 def return_env(self, exists=True): """ diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index dab8b41cc9..f3e4ccd364 100644 --- a/setuptools/tests/test_build_ext.py +++ b/setuptools/tests/test_build_ext.py @@ -183,7 +183,7 @@ def get_build_ext_cmd(self, optional: bool, **opts): "eggs.c": "#include missingheader.h\n", ".build": {"lib": {}, "tmp": {}}, } - path.build(files) # type: ignore[arg-type] # jaraco/path#232 + path.build(files) # jaraco/path#232 extension = Extension('spam.eggs', ['eggs.c'], optional=optional) dist = Distribution(dict(ext_modules=[extension])) dist.script_name = 'setup.py' diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 287367ac18..bdbaa3c7e7 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import platform import stat @@ -8,6 +10,7 @@ from importlib.machinery import EXTENSION_SUFFIXES from pathlib import Path from textwrap import dedent +from typing import Any from unittest.mock import Mock from uuid import uuid4 @@ -840,7 +843,8 @@ class TestOverallBehaviour: version = "3.14159" """ - FLAT_LAYOUT = { + # Any: Would need a TypedDict. Keep it simple for tests + FLAT_LAYOUT: dict[str, Any] = { "pyproject.toml": dedent(PYPROJECT), "MANIFEST.in": EXAMPLE["MANIFEST.in"], "otherfile.py": "", @@ -878,9 +882,9 @@ class TestOverallBehaviour: "otherfile.py": "", "mypkg": { "__init__.py": "", - "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], # type: ignore + "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], }, - "other": FLAT_LAYOUT["mypkg"]["subpackage"], # type: ignore + "other": FLAT_LAYOUT["mypkg"]["subpackage"], }, "namespace": { "pyproject.toml": dedent(PYPROJECT), @@ -888,8 +892,8 @@ class TestOverallBehaviour: "otherfile.py": "", "src": { "mypkg": { - "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], # type: ignore - "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"], # type: ignore + "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], + "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"], }, }, }, From 1429bf595df92ce2cf901a54d4e17d5d8e44f4f4 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Aug 2024 12:33:34 -0400 Subject: [PATCH 12/20] Fix ConfigHandler generic --- setuptools/config/setupcfg.py | 17 ++++++++++------- setuptools/tests/config/test_setupcfg.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index 54469f74a3..4fee109e26 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -27,7 +27,6 @@ List, Tuple, TypeVar, - Union, cast, ) @@ -53,7 +52,7 @@ while the second element of the tuple is the option value itself """ AllCommandOptions = Dict["str", SingleCommandOptions] # cmd name => its options -Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"]) +Target = TypeVar("Target", "Distribution", "DistributionMetadata") def read_configuration( @@ -96,7 +95,7 @@ def _apply( filepath: StrPath, other_files: Iterable[StrPath] = (), ignore_option_errors: bool = False, -) -> tuple[ConfigHandler, ...]: +) -> tuple[ConfigMetadataHandler, ConfigOptionsHandler]: """Read configuration from ``filepath`` and applies to the ``dist`` object.""" from setuptools.dist import _Distribution @@ -122,7 +121,7 @@ def _apply( return handlers -def _get_option(target_obj: Target, key: str): +def _get_option(target_obj: Distribution | DistributionMetadata, key: str): """ Given a target object and option key, get that option from the target object, either through a get_{key} method or @@ -134,10 +133,14 @@ def _get_option(target_obj: Target, key: str): return getter() -def configuration_to_dict(handlers: tuple[ConfigHandler, ...]) -> dict: +def configuration_to_dict( + handlers: Iterable[ + ConfigHandler[Distribution] | ConfigHandler[DistributionMetadata] + ], +) -> dict: """Returns configuration data gathered by given handlers as a dict. - :param list[ConfigHandler] handlers: Handlers list, + :param Iterable[ConfigHandler] handlers: Handlers list, usually from parse_configuration() :rtype: dict @@ -254,7 +257,7 @@ def __init__( ensure_discovered: expand.EnsurePackagesDiscovered, ): self.ignore_option_errors = ignore_option_errors - self.target_obj = target_obj + self.target_obj: Target = target_obj self.sections = dict(self._section_options(options)) self.set_options: list[str] = [] self.ensure_discovered = ensure_discovered diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index 4f0a7349f5..8d95798123 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -7,7 +7,7 @@ import pytest from packaging.requirements import InvalidRequirement -from setuptools.config.setupcfg import ConfigHandler, read_configuration +from setuptools.config.setupcfg import ConfigHandler, Target, read_configuration from setuptools.dist import Distribution, _Distribution from setuptools.warnings import SetuptoolsDeprecationWarning @@ -16,7 +16,7 @@ from distutils.errors import DistutilsFileError, DistutilsOptionError -class ErrConfigHandler(ConfigHandler): +class ErrConfigHandler(ConfigHandler[Target]): """Erroneous handler. Fails to implement required methods.""" section_prefix = "**err**" From 2072d9876f813dde19ea856751b076265c1d0305 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 17 Oct 2024 12:20:14 -0400 Subject: [PATCH 13/20] Using `dict` as an `OrderedDict` and allowed using `dict` as an ordered type in `setuptools.dist.check_requirements` --- newsfragments/4575.feature.rst | 1 + setuptools/command/_requirestxt.py | 3 +-- setuptools/command/egg_info.py | 5 +---- setuptools/dist.py | 29 +++++++++++++------------- setuptools/tests/test_core_metadata.py | 1 - setuptools/tests/test_dist.py | 10 ++------- 6 files changed, 20 insertions(+), 29 deletions(-) create mode 100644 newsfragments/4575.feature.rst diff --git a/newsfragments/4575.feature.rst b/newsfragments/4575.feature.rst new file mode 100644 index 0000000000..64ab49830f --- /dev/null +++ b/newsfragments/4575.feature.rst @@ -0,0 +1 @@ +Allowed using `dict` as an ordered type in ``setuptools.dist.check_requirements`` -- by :user:`Avasam` diff --git a/setuptools/command/_requirestxt.py b/setuptools/command/_requirestxt.py index b87476d6f4..d426f5dffb 100644 --- a/setuptools/command/_requirestxt.py +++ b/setuptools/command/_requirestxt.py @@ -18,12 +18,11 @@ from packaging.requirements import Requirement from .. import _reqs +from .._reqs import _StrOrIter # dict can work as an ordered set _T = TypeVar("_T") _Ordered = Dict[_T, None] -_ordered = dict -_StrOrIter = _reqs._StrOrIter def _prepare( diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index f4d3a2a57e..bc6c677878 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -2,7 +2,6 @@ Create a distribution's .egg-info directory and contents""" -import collections import functools import os import re @@ -211,11 +210,9 @@ def save_version_info(self, filename): build tag. Install build keys in a deterministic order to avoid arbitrary reordering on subsequent builds. """ - egg_info = collections.OrderedDict() # follow the order these keys would have been added # when PYTHONHASHSEED=0 - egg_info['tag_build'] = self.tags() - egg_info['tag_date'] = 0 + egg_info = dict(tag_build=self.tags(), tag_date=0) edit_config(filename, dict(egg_info=egg_info)) def finalize_options(self): diff --git a/setuptools/dist.py b/setuptools/dist.py index f22e3eea54..d6b8e08214 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -11,12 +11,12 @@ from typing import ( TYPE_CHECKING, Any, + Dict, List, MutableMapping, - NoReturn, + Sequence, Tuple, Union, - overload, ) from more_itertools import partition, unique_everseen @@ -30,7 +30,6 @@ command as _, # noqa: F401 # imported for side-effects ) from ._importlib import metadata -from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery from .monkey import get_unpatched @@ -63,7 +62,13 @@ """ _Sequence: TypeAlias = Union[Tuple[str, ...], List[str]] # This is how stringifying _Sequence would look in Python 3.10 -_requence_type_repr = "tuple[str, ...] | list[str]" +_sequence_type_repr = "tuple[str, ...] | list[str]" +_OrderedStrSequence: TypeAlias = Union[str, Dict[str, Any], Sequence[str]] +""" +:meta private: +Avoid single-use iterable. Disallow sets. +A poor approximation of an OrderedSequence (dict doesn't match a Sequence). +""" def __getattr__(name: str) -> Any: # pragma: no cover @@ -97,7 +102,7 @@ def assert_string_list(dist, attr: str, value: _Sequence) -> None: assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError) as e: raise DistutilsSetupError( - f"{attr!r} must be of type <{_requence_type_repr}> (got {value!r})" + f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})" ) from e @@ -173,15 +178,11 @@ def invalid_unless_false(dist, attr, value): raise DistutilsSetupError(f"{attr} is invalid.") -@overload -def check_requirements(dist, attr: str, value: set | dict) -> NoReturn: ... -@overload -def check_requirements(dist, attr: str, value: _StrOrIter) -> None: ... -def check_requirements(dist, attr: str, value: _StrOrIter) -> None: +def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None: """Verify that install_requires is a valid requirements list""" try: list(_reqs.parse(value)) - if isinstance(value, (dict, set)): + if isinstance(value, set): raise TypeError("Unordered types are not allowed") except (TypeError, ValueError) as error: msg = ( @@ -810,7 +811,7 @@ def _exclude_misc(self, name: str, value: _Sequence) -> None: """Handle 'exclude()' for list/tuple attrs without a special handler""" if not isinstance(value, _sequence): raise DistutilsSetupError( - f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" + f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" ) try: old = getattr(self, name) @@ -828,7 +829,7 @@ def _include_misc(self, name: str, value: _Sequence) -> None: if not isinstance(value, _sequence): raise DistutilsSetupError( - f"{name}: setting must be of type <{_requence_type_repr}> (got {value!r})" + f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" ) try: old = getattr(self, name) @@ -870,7 +871,7 @@ def exclude(self, **attrs): def _exclude_packages(self, packages: _Sequence) -> None: if not isinstance(packages, _sequence): raise DistutilsSetupError( - f"packages: setting must be of type <{_requence_type_repr}> (got {packages!r})" + f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})" ) list(map(self.exclude_package, packages)) diff --git a/setuptools/tests/test_core_metadata.py b/setuptools/tests/test_core_metadata.py index 34828ac750..51d4a10810 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -310,7 +310,6 @@ def test_parity_with_metadata_from_pypa_wheel(tmp_path): python_requires=">=3.8", install_requires=""" packaging==23.2 - ordered-set==3.1.1 more-itertools==8.8.0; extra == "other" jaraco.text==3.7.0 importlib-resources==5.10.2; python_version<"3.8" diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index fde0de99ac..1bc4923032 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -1,4 +1,3 @@ -import collections import os import re import urllib.parse @@ -72,15 +71,10 @@ def sdist_with_index(distname, version): def test_provides_extras_deterministic_order(): - extras = collections.OrderedDict() - extras['a'] = ['foo'] - extras['b'] = ['bar'] - attrs = dict(extras_require=extras) + attrs = dict(extras_require=dict(a=['foo'], b=['bar'])) dist = Distribution(attrs) assert list(dist.metadata.provides_extras) == ['a', 'b'] - attrs['extras_require'] = collections.OrderedDict( - reversed(list(attrs['extras_require'].items())) - ) + attrs['extras_require'] = dict(reversed(attrs['extras_require'].items())) dist = Distribution(attrs) assert list(dist.metadata.provides_extras) == ['b', 'a'] From 206c4b8913b0cb792e1077673b9db7f3c73e8ac5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 17 Oct 2024 12:57:21 -0400 Subject: [PATCH 14/20] Bump mypy to 1.12 and pyright to 1.1.385 --- .github/workflows/pyright.yml | 3 ++- mypy.ini | 3 ++- pkg_resources/__init__.py | 2 +- pyproject.toml | 2 +- setuptools/_path.py | 3 ++- setuptools/command/__init__.py | 10 +++++++++- setuptools/command/sdist.py | 3 ++- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pyright.yml b/.github/workflows/pyright.yml index 38fb910d85..17a1e2dbbe 100644 --- a/.github/workflows/pyright.yml +++ b/.github/workflows/pyright.yml @@ -26,7 +26,7 @@ env: # For help with static-typing issues, or pyright update, ping @Avasam # # An exact version from https://github.com/microsoft/pyright/releases or "latest" - PYRIGHT_VERSION: "1.1.377" + PYRIGHT_VERSION: "1.1.385" # Environment variable to support color support (jaraco/skeleton#66) FORCE_COLOR: 1 @@ -73,4 +73,5 @@ jobs: uses: jakebailey/pyright-action@v2 with: version: ${{ env.PYRIGHT_VERSION }} + python-version: ${{ matrix.python }} extra-args: --threads diff --git a/mypy.ini b/mypy.ini index 26692755ea..2dc8aab56f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -52,6 +52,7 @@ disable_error_code = import-not-found # for setuptools to import `_distutils` directly # - or non-stdlib distutils typings are exposed # - 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 # - jaraco.develop: https://github.com/jaraco/jaraco.develop/issues/22 # - jaraco.envs: https://github.com/jaraco/jaraco.envs/issues/7 # - jaraco.packaging: https://github.com/jaraco/jaraco.packaging/issues/20 @@ -59,7 +60,7 @@ disable_error_code = import-not-found # - jaraco.test: https://github.com/jaraco/jaraco.test/issues/7 # - jaraco.text: https://github.com/jaraco/jaraco.text/issues/17 # - wheel: does not intend on exposing a programmatic API https://github.com/pypa/wheel/pull/610#issuecomment-2081687671 -[mypy-distutils.*,jaraco.develop,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.test.*,jaraco.text,wheel.*] +[mypy-distutils.*,jaraco,jaraco.develop,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.test.*,jaraco.text,wheel.*] ignore_missing_imports = True # Even when excluding a module, import issues can show up due to following import diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index f1f0ef2535..47824ab66e 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2648,7 +2648,7 @@ def _cygwin_patch(filename: StrOrBytesPath): # pragma: nocover would probably better, in Cygwin even more so, except that this seems to be by design... """ - return os.path.abspath(filename) if sys.platform == 'cygwin' else filename + return os.path.abspath(filename) if sys.platform == 'cygwin' else filename # type: ignore[type-var] # python/mypy#17952 if TYPE_CHECKING: diff --git a/pyproject.toml b/pyproject.toml index eb6ec11041..72fc6df93a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ type = [ # pin mypy version so a new version doesn't suddenly cause the CI to fail, # until types-setuptools is removed from typeshed. # For help with static-typing issues, or mypy update, ping @Avasam - "mypy==1.11.*", + "mypy==1.12.*", # Typing fixes in version newer than we require at runtime "importlib_metadata>=7.0.2; python_version < '3.10'", # Imported unconditionally in tools/finalize.py diff --git a/setuptools/_path.py b/setuptools/_path.py index dd4a9db8cb..c7bef83365 100644 --- a/setuptools/_path.py +++ b/setuptools/_path.py @@ -11,9 +11,10 @@ from more_itertools import unique_everseen -if sys.version_info >= (3, 9): +if TYPE_CHECKING: StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath else: + # Python 3.8 support StrPath: TypeAlias = Union[str, os.PathLike] diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index bf011e896d..50e6c2f54f 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -1,12 +1,20 @@ +# mypy: disable_error_code=call-overload +# pyright: reportCallIssue=false, reportArgumentType=false +# Can't disable on the exact line because distutils doesn't exists on Python 3.12 +# and type-checkers aren't aware of distutils_hack, +# causing distutils.command.bdist.bdist.format_commands to be Any. + import sys from distutils.command.bdist import bdist if 'egg' not in bdist.format_commands: try: + # format_commands is a dict in vendored distutils + # It used to be a list in older (stdlib) distutils + # We support both for backwards compatibility bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file") except TypeError: - # For backward compatibility with older distutils (stdlib) bdist.format_command['egg'] = ('bdist_egg', "Python .egg file") bdist.format_commands.append('egg') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index fa9a2c4d81..65ce735dde 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -4,6 +4,7 @@ import os import re from itertools import chain +from typing import ClassVar from .._importlib import metadata from ..dist import Distribution @@ -49,7 +50,7 @@ class sdist(orig.sdist): ] distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution - negative_opt: dict[str, str] = {} + negative_opt: ClassVar[dict[str, str]] = {} README_EXTENSIONS = ['', '.rst', '.txt', '.md'] READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS) From e6ba3ad63812c4160a4b0d585316329ceb750387 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 21 Oct 2024 15:11:21 +0100 Subject: [PATCH 15/20] Remove unused type ignore --- pkg_resources/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 47824ab66e..f1f0ef2535 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2648,7 +2648,7 @@ def _cygwin_patch(filename: StrOrBytesPath): # pragma: nocover would probably better, in Cygwin even more so, except that this seems to be by design... """ - return os.path.abspath(filename) if sys.platform == 'cygwin' else filename # type: ignore[type-var] # python/mypy#17952 + return os.path.abspath(filename) if sys.platform == 'cygwin' else filename if TYPE_CHECKING: From 9cefa0af0cbfda14d8852ad8d55ca2ca29a99983 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 12 Aug 2024 13:02:44 -0400 Subject: [PATCH 16/20] Disable and revert unpacked-list-comprehension (UP027) --- ruff.toml | 5 +++-- setuptools/package_index.py | 5 +---- setuptools/tests/test_egg_info.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ruff.toml b/ruff.toml index b55b4e8067..53d644f6a5 100644 --- a/ruff.toml +++ b/ruff.toml @@ -12,18 +12,19 @@ extend-select = [ # local "ANN2", # missing-return-type-* - "FA", # flake8-future-annotations "F404", # late-future-import + "FA", # flake8-future-annotations "I", # isort "PYI", # flake8-pyi + "TRY", # tryceratops "UP", # pyupgrade - "TRY", "YTT", # flake8-2020 ] ignore = [ "TRY003", # raise-vanilla-args, avoid multitude of exception classes "TRY301", # raise-within-try, it's handy "UP015", # redundant-open-modes, explicit is preferred + "UP027", # unpacked-list-comprehension, is actually slower for cases relevant to unpacking, set for deprecation: https://github.com/astral-sh/ruff/issues/12754 "UP030", # temporarily disabled "UP031", # temporarily disabled "UP032", # temporarily disabled diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 9e01d5e082..9b3769fac9 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -561,10 +561,7 @@ def not_found_in_index(self, requirement): if self[requirement.key]: # we've seen at least one distro meth, msg = self.info, "Couldn't retrieve index page for %r" else: # no distros seen for this name, might be misspelled - meth, msg = ( - self.warn, - "Couldn't find index page for %r (maybe misspelled?)", - ) + meth, msg = self.warn, "Couldn't find index page for %r (maybe misspelled?)" meth(msg, requirement.unsafe_name) self.scan_all() diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 6e8d0c68c3..12d6b30a8b 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -283,9 +283,9 @@ def parametrize(*test_list, **format_dict): else: install_cmd_kwargs = {} name = name_kwargs[0].strip() - setup_py_requires, setup_cfg_requires, expected_requires = ( + setup_py_requires, setup_cfg_requires, expected_requires = [ DALS(a).format(**format_dict) for a in test_params - ) + ] for id_, requires, use_cfg in ( (name, setup_py_requires, False), (name + '_in_setup_cfg', setup_cfg_requires, True), From 5bc3ebfe66a218261ca2884766a1e6c906ff70c4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 21 Oct 2024 12:53:41 +0100 Subject: [PATCH 17/20] Fix possible invalid dynamic behaviour of optional-dependencies --- setuptools/config/_apply_pyprojecttoml.py | 6 ++-- .../config/test_pyprojecttoml_dynamic_deps.py | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 23179f3548..16fe753b58 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -217,8 +217,10 @@ def _dependencies(dist: Distribution, val: list, _root_dir): def _optional_dependencies(dist: Distribution, val: dict, _root_dir): - existing = getattr(dist, "extras_require", None) or {} - dist.extras_require = {**existing, **val} + if getattr(dist, "extras_require", None): + msg = "`extras_require` overwritten in `pyproject.toml` (optional-dependencies)" + SetuptoolsWarning.emit(msg) + dist.extras_require = val def _ext_modules(dist: Distribution, val: list[dict]) -> list[Extension]: diff --git a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py index 37e5234a45..e42f28ffaa 100644 --- a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py +++ b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py @@ -5,6 +5,7 @@ from setuptools.config.pyprojecttoml import apply_configuration from setuptools.dist import Distribution +from setuptools.warnings import SetuptoolsWarning def test_dynamic_dependencies(tmp_path): @@ -77,23 +78,32 @@ def test_mixed_dynamic_optional_dependencies(tmp_path): [tool.setuptools.dynamic.optional-dependencies.images] file = ["requirements-images.txt"] - - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" """ ), } path.build(files, prefix=tmp_path) - - # Test that the mix-and-match doesn't currently validate. pyproject = tmp_path / "pyproject.toml" with pytest.raises(ValueError, match="project.optional-dependencies"): apply_configuration(Distribution(), pyproject) - # Explicitly disable the validation and try again, to see that the mix-and-match - # result would be correct. - dist = Distribution() - dist = apply_configuration(dist, pyproject, ignore_option_errors=True) - assert dist.extras_require == {"docs": ["sphinx"], "images": ["pillow~=42.0"]} + +def test_mixed_extras_require_optional_dependencies(tmp_path): + files = { + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + optional-dependencies.docs = ["sphinx"] + """ + ), + } + + path.build(files, prefix=tmp_path) + pyproject = tmp_path / "pyproject.toml" + + with pytest.warns(SetuptoolsWarning, match=".extras_require. overwritten"): + dist = Distribution({"extras_require": {"hello": ["world"]}}) + dist = apply_configuration(dist, pyproject) + assert dist.extras_require == {"docs": ["sphinx"]} From 446e58f7a147015e0946e76083d86db8e30e7ffb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 21 Oct 2024 11:30:28 +0100 Subject: [PATCH 18/20] Addopt modern syntax sugar in setupcfg.py --- setuptools/config/setupcfg.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index 4fee109e26..5b4e1e8d95 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -304,7 +304,7 @@ def __setitem__(self, option_name, value) -> None: return simple_setter = functools.partial(target_obj.__setattr__, option_name) - setter = getattr(target_obj, 'set_%s' % option_name, simple_setter) + setter = getattr(target_obj, f"set_{option_name}", simple_setter) setter(parsed) self.set_options.append(option_name) @@ -372,8 +372,8 @@ def parser(value): exclude_directive = 'file:' if value.startswith(exclude_directive): raise ValueError( - 'Only strings are accepted for the {0} field, ' - 'files are not accepted'.format(key) + f'Only strings are accepted for the {key} field, ' + 'files are not accepted' ) return value @@ -491,12 +491,12 @@ def parse(self) -> None: for section_name, section_options in self.sections.items(): method_postfix = '' if section_name: # [section.option] variant - method_postfix = '_%s' % section_name + method_postfix = f"_{section_name}" section_parser_method: Callable | None = getattr( self, # Dots in section names are translated into dunderscores. - ('parse_section%s' % method_postfix).replace('.', '__'), + f'parse_section{method_postfix}'.replace('.', '__'), None, ) @@ -701,10 +701,7 @@ def parse_section_packages__find(self, section_options): section_data = self._parse_section_to_dict(section_options, self._parse_list) valid_keys = ['where', 'include', 'exclude'] - - find_kwargs = dict([ - (k, v) for k, v in section_data.items() if k in valid_keys and v - ]) + find_kwargs = {k: v for k, v in section_data.items() if k in valid_keys and v} where = find_kwargs.get('where') if where is not None: From 2c99d1e350512c7020c516ccea0d64848399b5d3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 21 Oct 2024 16:17:01 +0100 Subject: [PATCH 19/20] Add news fragment --- newsfragments/4696.bugfix.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 newsfragments/4696.bugfix.rst diff --git a/newsfragments/4696.bugfix.rst b/newsfragments/4696.bugfix.rst new file mode 100644 index 0000000000..77ebf87c48 --- /dev/null +++ b/newsfragments/4696.bugfix.rst @@ -0,0 +1,4 @@ +Fix clashes for ``optional-dependencies`` in ``pyproject.toml`` and +``extra_requires`` in ``setup.cfg/setup.py``. +As per PEP 621, ``optional-dependencies`` has to be honoured and dynamic +behaviour is not allowed. From 99c75c945ac7afd3d37ca918125a157492e959da Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 21 Oct 2024 12:24:22 -0400 Subject: [PATCH 20/20] Ensured all methods in `setuptools.modified` raise a consistant `distutils.error.DistutilsError` type (#4567) * Ensured all methods in `setuptools.modified` raise a consistant `distutils.error.DistutilsError` type * Update tests to reflect runtime behaviour with SETUPTOOLS_USE_DISTUTILS=stdlib * Update newsfragments/4567.bugfix.rst Co-authored-by: Anderson Bravalheri * Attempt to fix setuptools/tests/test_distutils_adoption.py * can't use setuptoolsrelative import in test * Fix formatting error --------- Co-authored-by: Anderson Bravalheri Co-authored-by: Anderson Bravalheri --- newsfragments/4567.bugfix.rst | 4 ++ setuptools/command/build_clib.py | 9 +--- setuptools/modified.py | 22 +++++++--- setuptools/tests/test_distutils_adoption.py | 47 +++++++++++++++++++-- 4 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 newsfragments/4567.bugfix.rst diff --git a/newsfragments/4567.bugfix.rst b/newsfragments/4567.bugfix.rst new file mode 100644 index 0000000000..7d7bb282e1 --- /dev/null +++ b/newsfragments/4567.bugfix.rst @@ -0,0 +1,4 @@ +Ensured methods in ``setuptools.modified`` preferably raise a consistent +``distutils.errors.DistutilsError`` type +(except in the deprecated use case of ``SETUPTOOLS_USE_DISTUTILS=stdlib``) +-- by :user:`Avasam` diff --git a/setuptools/command/build_clib.py b/setuptools/command/build_clib.py index d532762ebe..eab08e70f2 100644 --- a/setuptools/command/build_clib.py +++ b/setuptools/command/build_clib.py @@ -1,17 +1,10 @@ from ..dist import Distribution +from ..modified import newer_pairwise_group import distutils.command.build_clib as orig from distutils import log from distutils.errors import DistutilsSetupError -try: - from distutils._modified import ( # pyright: ignore[reportMissingImports] - newer_pairwise_group, - ) -except ImportError: - # fallback for SETUPTOOLS_USE_DISTUTILS=stdlib - from .._distutils._modified import newer_pairwise_group - class build_clib(orig.build_clib): """ diff --git a/setuptools/modified.py b/setuptools/modified.py index 245a61580b..6ba02fab68 100644 --- a/setuptools/modified.py +++ b/setuptools/modified.py @@ -1,8 +1,18 @@ -from ._distutils._modified import ( - newer, - newer_group, - newer_pairwise, - newer_pairwise_group, -) +try: + # Ensure a DistutilsError raised by these methods is the same as distutils.errors.DistutilsError + from distutils._modified import ( + newer, + newer_group, + newer_pairwise, + newer_pairwise_group, + ) +except ImportError: + # fallback for SETUPTOOLS_USE_DISTUTILS=stdlib, because _modified never existed in stdlib + from ._distutils._modified import ( + newer, + newer_group, + newer_pairwise, + newer_pairwise_group, + ) __all__ = ['newer', 'newer_pairwise', 'newer_group', 'newer_pairwise_group'] diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 0b020ba9fc..aabfdd283a 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -114,6 +114,7 @@ def test_distutils_has_origin(): """ +@pytest.mark.usefixtures("tmpdir_cwd") @pytest.mark.parametrize( "distutils_version, imported_module", [ @@ -125,9 +126,7 @@ def test_distutils_has_origin(): ("local", "archive_util"), ], ) -def test_modules_are_not_duplicated_on_import( - distutils_version, imported_module, tmpdir_cwd, venv -): +def test_modules_are_not_duplicated_on_import(distutils_version, imported_module, venv): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module) cmd = ['python', '-c', script] @@ -145,6 +144,7 @@ def test_modules_are_not_duplicated_on_import( """ +@pytest.mark.usefixtures("tmpdir_cwd") @pytest.mark.parametrize( "distutils_version", [ @@ -152,8 +152,47 @@ def test_modules_are_not_duplicated_on_import( pytest.param("stdlib", marks=skip_without_stdlib_distutils), ], ) -def test_log_module_is_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): +def test_log_module_is_not_duplicated_on_import(distutils_version, venv): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() assert output == "success" + + +ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY = r""" +from setuptools.modified import newer +from {imported_module}.errors import DistutilsError + +# Can't use pytest.raises in this context +try: + newer("", "") +except DistutilsError: + print("success") +else: + raise AssertionError("Expected to raise") +""" + + +@pytest.mark.usefixtures("tmpdir_cwd") +@pytest.mark.parametrize( + "distutils_version, imported_module", + [ + ("local", "distutils"), + # Unfortunately we still get ._distutils.errors.DistutilsError with SETUPTOOLS_USE_DISTUTILS=stdlib + # But that's a deprecated use-case we don't mind not fully supporting in newer code + pytest.param( + "stdlib", "setuptools._distutils", marks=skip_without_stdlib_distutils + ), + ], +) +def test_consistent_error_from_modified_py(distutils_version, imported_module, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + cmd = [ + 'python', + '-c', + ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY.format( + imported_module=imported_module + ), + ] + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() + assert output == "success"