From 3fc912a390f46f1163bdccdfe1be9b84401b96b4 Mon Sep 17 00:00:00 2001 From: Anders Theet Date: Tue, 8 Oct 2024 08:30:18 +0200 Subject: [PATCH 01/34] Use minimum requirement for jaraco.functools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f528752ab..ce2c5988c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ requires-python = ">=3.8" dependencies = [ # Setuptools must require these "packaging", - "jaraco.functools", + "jaraco.functools >= 4", "more_itertools", "jaraco.collections", ] From 22e845ba6049f2417e1f8d14a67bd5a97a9b62c9 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 17 Oct 2024 17:14:33 -0400 Subject: [PATCH 02/34] use a real boolean (False) default for display_option_names generated attributes --- distutils/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/dist.py b/distutils/dist.py index 154301baff..8e1e6d0b4e 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -139,7 +139,7 @@ def __init__(self, attrs=None): # noqa: C901 self.dry_run = False self.help = False for attr in self.display_option_names: - setattr(self, attr, 0) + setattr(self, attr, False) # Store the distribution meta-data (name, version, author, and so # forth) in a separate object -- we're getting to have enough From 68cb6ba936c788f0dc372e8877aab23d35b83320 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 7 Nov 2024 03:11:59 +0530 Subject: [PATCH 03/34] Accept an `Iterable` at runtime for `Extension` --- distutils/extension.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/distutils/extension.py b/distutils/extension.py index 33159079c1..f6e3445bad 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -26,12 +26,14 @@ class Extension: name : string the full name of the extension, including any packages -- ie. *not* a filename or pathname, but Python dotted name - sources : [string | os.PathLike] - list of source filenames, relative to the distribution root - (where the setup script lives), in Unix form (slash-separated) - for portability. Source files may be C, C++, SWIG (.i), - platform-specific resource files, or whatever else is recognized - by the "build_ext" command as source for a Python extension. + sources : Iterable[string | os.PathLike] + iterable of source filenames (except strings, which could be misinterpreted + as a single filename), relative to the distribution root (where the setup + script lives), in Unix form (slash-separated) for portability. Can be any + non-string iterable (list, tuple, set, etc.) containing strings or + PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific + resource files, or whatever else is recognized by the "build_ext" command + as source for a Python extension. include_dirs : [string] list of directories to search for C/C++ header files (in Unix form for portability) @@ -106,12 +108,23 @@ def __init__( ): if not isinstance(name, str): raise AssertionError("'name' must be a string") # noqa: TRY004 - if not ( - isinstance(sources, list) - and all(isinstance(v, (str, os.PathLike)) for v in sources) - ): + + # we handle the string case first; though strings are iterable, we disallow them + if isinstance(sources, str): + raise AssertionError( # noqa: TRY004 + "'sources' must be an iterable of strings or PathLike objects, not a string" + ) + + # mow we check if it's iterable and contains valid types + try: + sources = list(sources) # convert to list for consistency + if not all(isinstance(v, (str, os.PathLike)) for v in sources): + raise AssertionError( + "All elements in 'sources' must be strings or PathLike objects" + ) + except TypeError: raise AssertionError( - "'sources' must be a list of strings or PathLike objects." + "'sources' must be an iterable of strings or PathLike objects" ) self.name = name From 115bb678c722286246ad31dc7cc0cc92fe1111d8 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 7 Nov 2024 03:14:20 +0530 Subject: [PATCH 04/34] Add more tests to cover different iterables --- distutils/tests/test_extension.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 41872e04e8..31d1fc890e 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -69,7 +69,7 @@ def test_extension_init(self): assert ext.name == 'name' # the second argument, which is the list of files, must - # be a list of strings or PathLike objects + # be a list of strings or PathLike objects, and not a string with pytest.raises(AssertionError): Extension('name', 'file') with pytest.raises(AssertionError): @@ -79,6 +79,16 @@ def test_extension_init(self): ext = Extension('name', [pathlib.Path('file1'), pathlib.Path('file2')]) assert ext.sources == ['file1', 'file2'] + # any non-string iterable of strings or PathLike objects should work + ext = Extension('name', ('file1', 'file2')) # tuple + assert ext.sources == ['file1', 'file2'] + ext = Extension('name', {'file1', 'file2'}) # set + assert sorted(ext.sources) == ['file1', 'file2'] + ext = Extension('name', iter(['file1', 'file2'])) # iterator + assert ext.sources == ['file1', 'file2'] + ext = Extension('name', [pathlib.Path('file1'), 'file2']) # mixed types + assert ext.sources == ['file1', 'file2'] + # others arguments have defaults for attr in ( 'include_dirs', From 4a467fac7a98cc5d605afc1ebd951cdd82268f86 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 7 Nov 2024 04:09:16 +0530 Subject: [PATCH 05/34] Delegate to `os.fspath` for type checking Co-Authored-By: Avasam --- distutils/extension.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/distutils/extension.py b/distutils/extension.py index f6e3445bad..8d766f674b 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -117,18 +117,13 @@ def __init__( # mow we check if it's iterable and contains valid types try: - sources = list(sources) # convert to list for consistency - if not all(isinstance(v, (str, os.PathLike)) for v in sources): - raise AssertionError( - "All elements in 'sources' must be strings or PathLike objects" - ) + self.sources = list(map(os.fspath, sources)) except TypeError: raise AssertionError( "'sources' must be an iterable of strings or PathLike objects" ) self.name = name - self.sources = list(map(os.fspath, sources)) self.include_dirs = include_dirs or [] self.define_macros = define_macros or [] self.undef_macros = undef_macros or [] From 2930193c0714e4aa016b68c2d510a5a177c95b8a Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 7 Nov 2024 04:10:50 +0530 Subject: [PATCH 06/34] Fix typo Co-authored-by: Avasam --- distutils/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/extension.py b/distutils/extension.py index 8d766f674b..0b77614507 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -115,7 +115,7 @@ def __init__( "'sources' must be an iterable of strings or PathLike objects, not a string" ) - # mow we check if it's iterable and contains valid types + # now we check if it's iterable and contains valid types try: self.sources = list(map(os.fspath, sources)) except TypeError: From ff9c6842d2581ce3c6db58b3595edb268e792ff7 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 28 Oct 2024 17:51:31 -0400 Subject: [PATCH 07/34] Return real boolean from copy_file --- distutils/file_util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/distutils/file_util.py b/distutils/file_util.py index 85ee4dafcb..0acc8cb84b 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -118,7 +118,7 @@ def copy_file( # noqa: C901 if update and not newer(src, dst): if verbose >= 1: log.debug("not copying %s (output up-to-date)", src) - return (dst, 0) + return (dst, False) try: action = _copy_action[link] @@ -132,7 +132,7 @@ def copy_file( # noqa: C901 log.info("%s %s -> %s", action, src, dst) if dry_run: - return (dst, 1) + return (dst, True) # If linking (hard or symbolic), use the appropriate system call # (Unix only, of course, but that's the caller's responsibility) @@ -146,11 +146,11 @@ def copy_file( # noqa: C901 # even under Unix, see issue #8876). pass else: - return (dst, 1) + return (dst, True) elif link == 'sym': if not (os.path.exists(dst) and os.path.samefile(src, dst)): os.symlink(src, dst) - return (dst, 1) + return (dst, True) # Otherwise (non-Mac, not linking), copy the file contents and # (optionally) copy the times and mode. @@ -165,7 +165,7 @@ def copy_file( # noqa: C901 if preserve_mode: os.chmod(dst, S_IMODE(st[ST_MODE])) - return (dst, 1) + return (dst, True) # XXX I suspect this is Unix-specific -- need porting help! From 9c1bec62b3781ad176b4d674034648452c500d67 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 24 Nov 2024 16:06:18 -0500 Subject: [PATCH 08/34] Fix test_mkpath_exception_uncached --- distutils/tests/test_dir_util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index fcc37ac568..1d4001af6f 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -123,6 +123,10 @@ class FailPath(pathlib.Path): def mkdir(self, *args, **kwargs): raise OSError("Failed to create directory") + _flavor = ( + pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour + ) + target = tmp_path / 'foodir' with pytest.raises(errors.DistutilsFileError): From 89627a77258ea9e333dceac535cab050cfa80adf Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 1 Nov 2024 15:47:29 +0000 Subject: [PATCH 09/34] Set `Py_GIL_DISABLED=1` for free threading on Windows When free threaded CPython is installed from the official Windows installer it doesn't have the macro `Py_GIL_DISABLED` properly set becuase its `pyconfig.h` file is shared across the co-installed default build. Define the macro when building free threaded Python extensions on Windows so that each individual C API extension doesn't have to work around this limitation. See https://github.com/pypa/setuptools/issues/4662 --- distutils/command/build_ext.py | 8 +++++++- distutils/util.py | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index a7e3038be6..8d3dd7688a 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -23,7 +23,7 @@ ) from ..extension import Extension from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version -from ..util import get_platform, is_mingw +from ..util import get_platform, is_mingw, is_freethreaded # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). @@ -333,6 +333,12 @@ def run(self): # noqa: C901 if os.name == 'nt' and self.plat_name != get_platform(): self.compiler.initialize(self.plat_name) + # The official Windows free threaded Python installer doesn't set + # Py_GIL_DISABLED because its pyconfig.h is shared with the + # default build, so we need to define it here. + if os.name == 'nt' and is_freethreaded(): + self.compiler.define_macro('Py_GIL_DISABLED', '1') + # And make sure that any compile/link-related options (which might # come from the command-line or from the setup script) are set in # that CCompiler object -- that way, they automatically apply to diff --git a/distutils/util.py b/distutils/util.py index 609c1a50cd..6ef2c9854a 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -503,3 +503,7 @@ def is_mingw(): get_platform() starts with 'mingw'. """ return sys.platform == 'win32' and get_platform().startswith('mingw') + +def is_freethreaded(): + """Return True if the Python interpreter is built with free threading support.""" + return bool(sysconfig.get_config_var('Py_GIL_DISABLED')) From de1e6245eece2b51df15d42a49bf5a406cc71f78 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 1 Nov 2024 16:18:27 +0000 Subject: [PATCH 10/34] Link to setuptools issue --- distutils/command/build_ext.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index 8d3dd7688a..271378e580 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -335,7 +335,8 @@ def run(self): # noqa: C901 # The official Windows free threaded Python installer doesn't set # Py_GIL_DISABLED because its pyconfig.h is shared with the - # default build, so we need to define it here. + # default build, so we need to define it here + # (see pypa/setuptools#4662). if os.name == 'nt' and is_freethreaded(): self.compiler.define_macro('Py_GIL_DISABLED', '1') From 25120929af621de277c1d386cf6c60e0249f70a8 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 22 Dec 2024 18:29:31 -0500 Subject: [PATCH 11/34] Remove link to jaraco/path#232 --- setuptools/tests/test_build_ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index 88318b26c5..f4bacad8be 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) -> build_ext: "eggs.c": "#include missingheader.h\n", ".build": {"lib": {}, "tmp": {}}, } - path.build(files) # jaraco/path#232 + path.build(files) extension = Extension('spam.eggs', ['eggs.c'], optional=optional) dist = Distribution(dict(ext_modules=[extension])) dist.script_name = 'setup.py' From 11798dfdf00cb35d4b06d285b5fe48e3ddaa5a26 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 22 Dec 2024 23:28:57 -0500 Subject: [PATCH 12/34] Bump mypy to 1.14, jaraco.path to 3.7.2 --- mypy.ini | 6 +++--- pyproject.toml | 4 ++-- setuptools/tests/test_build_ext.py | 4 ++-- setuptools/tests/test_core_metadata.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mypy.ini b/mypy.ini index 57e19efa9e..c1d01a42c3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -58,7 +58,7 @@ ignore_missing_imports = True # - wheel: does not intend on exposing a programmatic API https://github.com/pypa/wheel/pull/610#issuecomment-2081687671 [mypy-wheel.*] -ignore_missing_imports = True +follow_untyped_imports = True # - 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 @@ -66,8 +66,8 @@ ignore_missing_imports = True # - jaraco.packaging: https://github.com/jaraco/jaraco.packaging/issues/20 # - 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] -ignore_missing_imports = True +[mypy-jaraco,jaraco.develop.*,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.text] +follow_untyped_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 diff --git a/pyproject.toml b/pyproject.toml index a19d4ac164..48df917af8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ test = [ "packaging>=24.2", "jaraco.envs>=2.2", "pytest-xdist>=3", # Dropped dependency on pytest-fork and py - "jaraco.path>=3.2.0", + "jaraco.path>=3.7.2", # Typing fixes "build[virtualenv]>=1.0.3", "filelock>=3.4.0", "ini2toml[lite]>=0.14", @@ -135,7 +135,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.12,<1.14", + "mypy==1.14.*", # 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/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index 88318b26c5..d107a272e1 100644 --- a/setuptools/tests/test_build_ext.py +++ b/setuptools/tests/test_build_ext.py @@ -179,11 +179,11 @@ def C(file): class TestBuildExtInplace: def get_build_ext_cmd(self, optional: bool, **opts) -> build_ext: - files = { + files: dict[str, str | dict[str, dict[str, str]]] = { "eggs.c": "#include missingheader.h\n", ".build": {"lib": {}, "tmp": {}}, } - path.build(files) # jaraco/path#232 + path.build(files) 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_core_metadata.py b/setuptools/tests/test_core_metadata.py index cf0bb32e9f..b67373bc37 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -5,7 +5,7 @@ import io from email import message_from_string from email.generator import Generator -from email.message import Message +from email.message import EmailMessage, Message from email.parser import Parser from email.policy import EmailPolicy from pathlib import Path @@ -416,7 +416,7 @@ def _assert_roundtrip_message(metadata: str) -> None: then ensures the metadata generated by setuptools is compatible. """ with io.StringIO(metadata) as buffer: - msg = Parser().parse(buffer) + msg = Parser(EmailMessage).parse(buffer) serialization_policy = EmailPolicy( utf8=True, From 52848a0d32ee377a578b8cafd7090446e240eb9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 11:19:27 -0500 Subject: [PATCH 13/34] Trim the comment a bit. --- distutils/command/build_ext.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index 271378e580..df2524b1ce 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -335,8 +335,7 @@ def run(self): # noqa: C901 # The official Windows free threaded Python installer doesn't set # Py_GIL_DISABLED because its pyconfig.h is shared with the - # default build, so we need to define it here - # (see pypa/setuptools#4662). + # default build, so define it here (pypa/setuptools#4662). if os.name == 'nt' and is_freethreaded(): self.compiler.define_macro('Py_GIL_DISABLED', '1') From 468532edd3ce99dfdbdf88d9a0b70a2b50fccc04 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 12:10:10 -0500 Subject: [PATCH 14/34] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_dir_util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 1d4001af6f..08d71393e5 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -106,8 +106,9 @@ def test_copy_tree_exception_in_listdir(self): """ An exception in listdir should raise a DistutilsFileError """ - with mock.patch("os.listdir", side_effect=OSError()), pytest.raises( - errors.DistutilsFileError + with ( + mock.patch("os.listdir", side_effect=OSError()), + pytest.raises(errors.DistutilsFileError), ): src = self.tempdirs[-1] dir_util.copy_tree(src, None) From bbee59bd0f8a671659674df42286051f59ea96ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 12:10:34 -0500 Subject: [PATCH 15/34] Use alternate spelling for flavor attribute. --- distutils/tests/test_dir_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 08d71393e5..6cb84e3a38 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -124,7 +124,7 @@ class FailPath(pathlib.Path): def mkdir(self, *args, **kwargs): raise OSError("Failed to create directory") - _flavor = ( + _flavour = ( pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour ) From a7fdc064b25bd395a126090dd573198d1b933003 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 12:11:02 -0500 Subject: [PATCH 16/34] Only apply workaround on required Pythons. --- distutils/tests/test_dir_util.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 6cb84e3a38..65a69d8fd6 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -3,6 +3,7 @@ import os import pathlib import stat +import sys import unittest.mock as mock from distutils import dir_util, errors from distutils.dir_util import ( @@ -124,9 +125,12 @@ class FailPath(pathlib.Path): def mkdir(self, *args, **kwargs): raise OSError("Failed to create directory") - _flavour = ( - pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour - ) + if sys.version_info < (3, 12): + _flavour = ( + pathlib._windows_flavour + if os.name == 'nt' + else pathlib._posix_flavour + ) target = tmp_path / 'foodir' From 01fbd65a89697b3631bb4c30809a1ca7b7601835 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 12:12:25 -0500 Subject: [PATCH 17/34] Let pathlib resolve the flavor. --- distutils/tests/test_dir_util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 65a69d8fd6..326cb34614 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -126,11 +126,7 @@ def mkdir(self, *args, **kwargs): raise OSError("Failed to create directory") if sys.version_info < (3, 12): - _flavour = ( - pathlib._windows_flavour - if os.name == 'nt' - else pathlib._posix_flavour - ) + _flavour = pathlib.Path()._flavour target = tmp_path / 'foodir' From b24919b17acda9ec262465687e302deb2fc2cb25 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 12:30:08 -0500 Subject: [PATCH 18/34] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/command/build_ext.py | 2 +- distutils/util.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index df2524b1ce..82c0e9f5e6 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -23,7 +23,7 @@ ) from ..extension import Extension from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version -from ..util import get_platform, is_mingw, is_freethreaded +from ..util import get_platform, is_freethreaded, is_mingw # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). diff --git a/distutils/util.py b/distutils/util.py index 6ef2c9854a..8d8260bc33 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -504,6 +504,7 @@ def is_mingw(): """ return sys.platform == 'win32' and get_platform().startswith('mingw') + def is_freethreaded(): """Return True if the Python interpreter is built with free threading support.""" return bool(sysconfig.get_config_var('Py_GIL_DISABLED')) From 1bae350d30c5ce556d0595394800c8d35c71c4e2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 24 Nov 2024 15:33:47 -0500 Subject: [PATCH 19/34] Run Ruff 0.8.0 --- distutils/command/build_clib.py | 3 +-- distutils/command/build_ext.py | 6 ++--- distutils/command/check.py | 5 +--- distutils/command/install_data.py | 5 ++-- distutils/fancy_getopt.py | 6 ++--- distutils/filelist.py | 5 +--- distutils/spawn.py | 2 +- distutils/tests/__init__.py | 2 +- distutils/tests/test_file_util.py | 13 +++++----- distutils/tests/test_spawn.py | 34 +++++++++++++++++---------- distutils/tests/test_unixccompiler.py | 13 +++++----- distutils/tests/test_version.py | 12 +++++----- distutils/version.py | 3 +-- ruff.toml | 4 ++++ 14 files changed, 58 insertions(+), 55 deletions(-) diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py index a600d09373..1305d5bb3d 100644 --- a/distutils/command/build_clib.py +++ b/distutils/command/build_clib.py @@ -138,8 +138,7 @@ def check_library_list(self, libraries): if '/' in name or (os.sep != '/' and os.sep in name): raise DistutilsSetupError( - f"bad library name '{lib[0]}': " - "may not contain directory separators" + f"bad library name '{lib[0]}': may not contain directory separators" ) if not isinstance(build_info, dict): diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index 82c0e9f5e6..cf60bd0ad8 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -443,8 +443,7 @@ def check_extensions_list(self, extensions): # noqa: C901 for macro in macros: if not (isinstance(macro, tuple) and len(macro) in (1, 2)): raise DistutilsSetupError( - "'macros' element of build info dict " - "must be 1- or 2-tuple" + "'macros' element of build info dict must be 1- or 2-tuple" ) if len(macro) == 1: ext.undef_macros.append(macro[0]) @@ -672,8 +671,7 @@ def find_swig(self): return "swig.exe" else: raise DistutilsPlatformError( - "I don't know how to find (much less run) SWIG " - f"on platform '{os.name}'" + f"I don't know how to find (much less run) SWIG on platform '{os.name}'" ) # -- Name generators ----------------------------------------------- diff --git a/distutils/command/check.py b/distutils/command/check.py index 93d754e73d..1375028e4d 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -46,10 +46,7 @@ class check(Command): ( 'restructuredtext', 'r', - ( - 'Checks if long string meta-data syntax ' - 'are reStructuredText-compliant' - ), + 'Checks if long string meta-data syntax are reStructuredText-compliant', ), ('strict', 's', 'Will exit with an error if a check fails'), ] diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py index a90ec3b4d0..36f5bcc8bf 100644 --- a/distutils/command/install_data.py +++ b/distutils/command/install_data.py @@ -9,7 +9,7 @@ import functools import os -from typing import Iterable +from collections.abc import Iterable from ..core import Command from ..util import change_root, convert_path @@ -22,8 +22,7 @@ class install_data(Command): ( 'install-dir=', 'd', - "base directory for installing data files " - "[default: installation base dir]", + "base directory for installing data files [default: installation base dir]", ), ('root=', None, "install everything relative to this alternate root directory"), ('force', 'f', "force installation (overwrite existing files)"), diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 907cc2b73c..4ea89603fa 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -12,7 +12,8 @@ import re import string import sys -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from .errors import DistutilsArgError, DistutilsGetoptError @@ -167,8 +168,7 @@ def _grok_option_table(self): # noqa: C901 if not ((short is None) or (isinstance(short, str) and len(short) == 1)): raise DistutilsGetoptError( - f"invalid short option '{short}': " - "must a single character or None" + f"invalid short option '{short}': must a single character or None" ) self.repeat[long] = repeat diff --git a/distutils/filelist.py b/distutils/filelist.py index 44ae9e67ef..9857b19549 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -127,10 +127,7 @@ def process_template_line(self, line): # noqa: C901 for pattern in patterns: if not self.exclude_pattern(pattern, anchor=True): log.warning( - ( - "warning: no previously-included files " - "found matching '%s'" - ), + "warning: no previously-included files found matching '%s'", pattern, ) diff --git a/distutils/spawn.py b/distutils/spawn.py index 107b011397..ba280334d1 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -12,7 +12,7 @@ import subprocess import sys import warnings -from typing import Mapping +from collections.abc import Mapping from ._log import log from .debug import DEBUG diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 93fbf49074..5a8ab06100 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -8,7 +8,7 @@ """ import shutil -from typing import Sequence +from collections.abc import Sequence def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no cover diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 85ac2136b3..a75d4a0317 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -44,18 +44,19 @@ def test_move_file_verbosity(self, caplog): def test_move_file_exception_unpacking_rename(self): # see issue 22182 - with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( - DistutilsFileError + with ( + mock.patch("os.rename", side_effect=OSError("wrong", 1)), + pytest.raises(DistutilsFileError), ): jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=False) def test_move_file_exception_unpacking_unlink(self): # see issue 22182 - with mock.patch( - "os.rename", side_effect=OSError(errno.EXDEV, "wrong") - ), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises( - DistutilsFileError + with ( + mock.patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")), + mock.patch("os.unlink", side_effect=OSError("wrong", 1)), + pytest.raises(DistutilsFileError), ): jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=False) diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index fd7b669cbf..fcbc765ef2 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -73,9 +73,12 @@ def test_find_executable(self, tmp_path): # PATH='': no match, except in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = '' - with mock.patch( - 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), mock.patch('distutils.spawn.os.defpath', tmp_dir): + with ( + mock.patch( + 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True + ), + mock.patch('distutils.spawn.os.defpath', tmp_dir), + ): rv = find_executable(program) assert rv is None @@ -87,9 +90,10 @@ def test_find_executable(self, tmp_path): # PATH=':': explicitly looks in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = os.pathsep - with mock.patch( - 'distutils.spawn.os.confstr', return_value='', create=True - ), mock.patch('distutils.spawn.os.defpath', ''): + with ( + mock.patch('distutils.spawn.os.confstr', return_value='', create=True), + mock.patch('distutils.spawn.os.defpath', ''), + ): rv = find_executable(program) assert rv is None @@ -103,16 +107,22 @@ def test_find_executable(self, tmp_path): env.pop('PATH', None) # without confstr - with mock.patch( - 'distutils.spawn.os.confstr', side_effect=ValueError, create=True - ), mock.patch('distutils.spawn.os.defpath', tmp_dir): + with ( + mock.patch( + 'distutils.spawn.os.confstr', side_effect=ValueError, create=True + ), + mock.patch('distutils.spawn.os.defpath', tmp_dir), + ): rv = find_executable(program) assert rv == filename # with confstr - with mock.patch( - 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), mock.patch('distutils.spawn.os.defpath', ''): + with ( + mock.patch( + 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True + ), + mock.patch('distutils.spawn.os.defpath', ''), + ): rv = find_executable(program) assert rv == filename diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 50b66544a8..1695328771 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -272,13 +272,12 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): sysconfig.get_config_var = gcv sysconfig.get_config_vars = gcvs - with mock.patch.object( - self.cc, 'spawn', return_value=None - ) as mock_spawn, mock.patch.object( - self.cc, '_need_link', return_value=True - ), mock.patch.object( - self.cc, 'mkpath', return_value=None - ), EnvironmentVarGuard() as env: + with ( + mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn, + mock.patch.object(self.cc, '_need_link', return_value=True), + mock.patch.object(self.cc, 'mkpath', return_value=None), + EnvironmentVarGuard() as env, + ): env['CC'] = 'ccache my_cc' env['CXX'] = 'my_cxx' del env['LDSHARED'] diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 1508e1cc0a..b68f097724 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -53,9 +53,9 @@ def test_cmp_strict(self): res = StrictVersion(v1)._cmp(v2) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(object()) - assert ( - res is NotImplemented - ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + assert res is NotImplemented, ( + f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + ) def test_cmp(self): versions = ( @@ -75,6 +75,6 @@ def test_cmp(self): res = LooseVersion(v1)._cmp(v2) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(object()) - assert ( - res is NotImplemented - ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + assert res is NotImplemented, ( + f'cmp({v1}, {v2}) should be NotImplemented, got {res}' + ) diff --git a/distutils/version.py b/distutils/version.py index 942b56bf94..2223ee9c8c 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -53,8 +53,7 @@ def __init__(self, vstring=None): if vstring: self.parse(vstring) warnings.warn( - "distutils Version classes are deprecated. " - "Use packaging.version instead.", + "distutils Version classes are deprecated. Use packaging.version instead.", DeprecationWarning, stacklevel=2, ) diff --git a/ruff.toml b/ruff.toml index 9c78018338..0cc5b267d7 100644 --- a/ruff.toml +++ b/ruff.toml @@ -19,6 +19,10 @@ extend-select = [ "YTT", ] ignore = [ + # TODO: Fix these new violations in Ruff 0.8.0 + "UP031", + "UP036", + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", From f5b7336316af0e984e4b55a361aeb29225f7065e Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 26 Dec 2024 23:28:57 +0530 Subject: [PATCH 20/34] Add review suggestions around code comments Co-authored-by: Jason R. Coombs Co-authored-by: Avasam --- distutils/extension.py | 2 +- distutils/tests/test_extension.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/extension.py b/distutils/extension.py index 0b77614507..fa088ec2f5 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -109,7 +109,7 @@ def __init__( if not isinstance(name, str): raise AssertionError("'name' must be a string") # noqa: TRY004 - # we handle the string case first; though strings are iterable, we disallow them + # handle the string case first; since strings are iterable, disallow them if isinstance(sources, str): raise AssertionError( # noqa: TRY004 "'sources' must be an iterable of strings or PathLike objects, not a string" diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 31d1fc890e..7b4612849e 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -69,7 +69,7 @@ def test_extension_init(self): assert ext.name == 'name' # the second argument, which is the list of files, must - # be a list of strings or PathLike objects, and not a string + # be an iterable of strings or PathLike objects, and not a string with pytest.raises(AssertionError): Extension('name', 'file') with pytest.raises(AssertionError): From efeb97c02684965d63e78eb9319458b0e8074f66 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 26 Dec 2024 23:33:03 +0530 Subject: [PATCH 21/34] Use `TypeError` instead of `AssertionError` --- distutils/extension.py | 6 +++--- distutils/tests/test_extension.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/distutils/extension.py b/distutils/extension.py index fa088ec2f5..f925987e84 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -107,11 +107,11 @@ def __init__( **kw, # To catch unknown keywords ): if not isinstance(name, str): - raise AssertionError("'name' must be a string") # noqa: TRY004 + raise TypeError("'name' must be a string") # noqa: TRY004 # handle the string case first; since strings are iterable, disallow them if isinstance(sources, str): - raise AssertionError( # noqa: TRY004 + raise TypeError( "'sources' must be an iterable of strings or PathLike objects, not a string" ) @@ -119,7 +119,7 @@ def __init__( try: self.sources = list(map(os.fspath, sources)) except TypeError: - raise AssertionError( + raise TypeError( "'sources' must be an iterable of strings or PathLike objects" ) diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 7b4612849e..dc998ec55b 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -63,16 +63,16 @@ def test_read_setup_file(self): def test_extension_init(self): # the first argument, which is the name, must be a string - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Extension(1, []) ext = Extension('name', []) assert ext.name == 'name' # the second argument, which is the list of files, must # be an iterable of strings or PathLike objects, and not a string - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Extension('name', 'file') - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Extension('name', ['file', 1]) ext = Extension('name', ['file1', 'file2']) assert ext.sources == ['file1', 'file2'] From a88eace7acc39b76aeb8d967777d285dbeb0341f Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 24 Nov 2024 17:08:38 -0500 Subject: [PATCH 22/34] UP031 manual fixes for Ruff 0.8.0 --- distutils/command/build.py | 3 ++- distutils/command/install.py | 4 ++-- distutils/command/install_egg_info.py | 8 +++----- distutils/command/sdist.py | 3 +-- distutils/dist.py | 2 +- distutils/fancy_getopt.py | 10 +++++----- distutils/sysconfig.py | 2 +- distutils/tests/test_build.py | 4 +++- distutils/tests/test_build_ext.py | 7 ++++--- distutils/text_file.py | 4 ++-- distutils/util.py | 2 +- 11 files changed, 25 insertions(+), 24 deletions(-) diff --git a/distutils/command/build.py b/distutils/command/build.py index caf55073af..ccd2c706a3 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -113,7 +113,8 @@ def finalize_options(self): # noqa: C901 self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier) if self.build_scripts is None: self.build_scripts = os.path.join( - self.build_base, 'scripts-%d.%d' % sys.version_info[:2] + self.build_base, + f'scripts-{sys.version_info.major}.{sys.version_info.minor}', ) if self.executable is None and sys.executable: diff --git a/distutils/command/install.py b/distutils/command/install.py index ceb453e041..9400995024 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -407,8 +407,8 @@ def finalize_options(self): # noqa: C901 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, - 'py_version_short': '%d.%d' % sys.version_info[:2], - 'py_version_nodot': '%d%d' % sys.version_info[:2], + 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', + 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', 'sys_prefix': prefix, 'prefix': prefix, 'sys_exec_prefix': exec_prefix, diff --git a/distutils/command/install_egg_info.py b/distutils/command/install_egg_info.py index 4fbb3440ab..0baeee7bb4 100644 --- a/distutils/command/install_egg_info.py +++ b/distutils/command/install_egg_info.py @@ -31,11 +31,9 @@ def basename(self): Allow basename to be overridden by child class. Ref pypa/distutils#2. """ - return "%s-%s-py%d.%d.egg-info" % ( - to_filename(safe_name(self.distribution.get_name())), - to_filename(safe_version(self.distribution.get_version())), - *sys.version_info[:2], - ) + name = to_filename(safe_name(self.distribution.get_name())) + version = to_filename(safe_version(self.distribution.get_version())) + return f"{name}-{version}-py{sys.version_info.major}.{sys.version_info.minor}.egg-info" def finalize_options(self): self.set_undefined_options('install_lib', ('install_dir', 'install_dir')) diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index d723a1c9fb..003e0bf875 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -362,8 +362,7 @@ def read_template(self): # convert_path function except (DistutilsTemplateError, ValueError) as msg: self.warn( - "%s, line %d: %s" - % (template.filename, template.current_line, msg) + f"{template.filename}, line {int(template.current_line)}: {msg}" ) finally: template.close() diff --git a/distutils/dist.py b/distutils/dist.py index 8e1e6d0b4e..f58159add9 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -722,7 +722,7 @@ def print_command_list(self, commands, header, max_length): except AttributeError: description = "(no description available)" - print(" %-*s %s" % (max_length, cmd, description)) + print(f" {cmd:<{max_length}} {description}") def print_commands(self): """Print out a help message listing all available commands with a diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 4ea89603fa..6f507ad9ea 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -351,18 +351,18 @@ def generate_help(self, header=None): # noqa: C901 # Case 1: no short option at all (makes life easy) if short is None: if text: - lines.append(" --%-*s %s" % (max_opt, long, text[0])) + lines.append(f" --{long:<{max_opt}} {text[0]}") else: - lines.append(" --%-*s " % (max_opt, long)) + lines.append(f" --{long:<{max_opt}}") # Case 2: we have a short option, so we have to include it # just after the long option else: opt_names = f"{long} (-{short})" if text: - lines.append(" --%-*s %s" % (max_opt, opt_names, text[0])) + lines.append(f" --{opt_names:<{max_opt}} {text[0]}") else: - lines.append(" --%-*s" % opt_names) + lines.append(f" --{opt_names:<{max_opt}}") for ell in text[1:]: lines.append(big_indent + ell) @@ -464,6 +464,6 @@ def __init__(self, options: Sequence[Any] = []): say, "How should I know?"].)""" for w in (10, 20, 30, 40): - print("width: %d" % w) + print(f"width: {w}") print("\n".join(wrap_text(text, w))) print() diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index da1eecbe7e..fc0ea78721 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -107,7 +107,7 @@ def get_python_version(): leaving off the patchlevel. Sample return values could be '1.5' or '2.2'. """ - return '%d.%d' % sys.version_info[:2] + return f'{sys.version_info.major}.{sys.version_info.minor}' def get_python_inc(plat_specific=False, prefix=None): diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index d379aca0bb..f7fe69acd1 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -40,7 +40,9 @@ def test_finalize_options(self): assert cmd.build_temp == wanted # build_scripts is build/scripts-x.x - wanted = os.path.join(cmd.build_base, 'scripts-%d.%d' % sys.version_info[:2]) + wanted = os.path.join( + cmd.build_base, f'scripts-{sys.version_info.major}.{sys.version_info.minor}' + ) assert cmd.build_scripts == wanted # executable is os.path.normpath(sys.executable) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 8bd3cef855..3e73d5bf3a 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -522,14 +522,15 @@ def _try_compile_deployment_target(self, operator, target): # pragma: no cover # at least one value we test with will not exist yet. if target[:2] < (10, 10): # for 10.1 through 10.9.x -> "10n0" - target = '%02d%01d0' % target + tmpl = '{:02}{:01}0' else: # for 10.10 and beyond -> "10nn00" if len(target) >= 2: - target = '%02d%02d00' % target + tmpl = '{:02}{:02}00' else: # 11 and later can have no minor version (11 instead of 11.0) - target = '%02d0000' % target + tmpl = '{:02}0000' + target = tmpl.format(*target) deptarget_ext = Extension( 'deptarget', [self.tmp_path / 'deptargetmodule.c'], diff --git a/distutils/text_file.py b/distutils/text_file.py index fec29c73b0..89d9048d59 100644 --- a/distutils/text_file.py +++ b/distutils/text_file.py @@ -133,9 +133,9 @@ def gen_error(self, msg, line=None): line = self.current_line outmsg.append(self.filename + ", ") if isinstance(line, (list, tuple)): - outmsg.append("lines %d-%d: " % tuple(line)) + outmsg.append("lines {}-{}: ".format(*line)) else: - outmsg.append("line %d: " % line) + outmsg.append(f"line {int(line)}: ") outmsg.append(str(msg)) return "".join(outmsg) diff --git a/distutils/util.py b/distutils/util.py index 8d8260bc33..fdc7ba9839 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -288,7 +288,7 @@ def split_quoted(s): elif s[end] == '"': # slurp doubly-quoted string m = _dquote_re.match(s, end) else: - raise RuntimeError("this can't happen (bad char '%c')" % s[end]) + raise RuntimeError(f"this can't happen (bad char '{s[end]}')") if m is None: raise ValueError(f"bad string (mismatched {s[end]} quotes?)") From 2017969f03d94d325b1d1aa3f5c2bcad807bff18 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 28 Oct 2024 14:24:39 -0400 Subject: [PATCH 23/34] Make reinitialize_command's return type Generic when "command" argument is a Command --- distutils/cmd.py | 17 ++++++++++++++++- distutils/dist.py | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/distutils/cmd.py b/distutils/cmd.py index 2bb97956ab..6ffe7bd4b3 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -4,15 +4,20 @@ in the distutils.command package. """ +from __future__ import annotations + import logging import os import re import sys +from typing import TypeVar, overload from . import _modified, archive_util, dir_util, file_util, util from ._log import log from .errors import DistutilsOptionError +_CommandT = TypeVar("_CommandT", bound="Command") + class Command: """Abstract base class for defining command classes, the "worker bees" @@ -305,7 +310,17 @@ def get_finalized_command(self, command, create=True): # XXX rename to 'get_reinitialized_command()'? (should do the # same in dist.py, if so) - def reinitialize_command(self, command, reinit_subcommands=False): + @overload + def reinitialize_command( + self, command: str, reinit_subcommands: bool = False + ) -> Command: ... + @overload + def reinitialize_command( + self, command: _CommandT, reinit_subcommands: bool = False + ) -> _CommandT: ... + def reinitialize_command( + self, command: str | Command, reinit_subcommands=False + ) -> Command: return self.distribution.reinitialize_command(command, reinit_subcommands) def run_command(self, command): diff --git a/distutils/dist.py b/distutils/dist.py index 8e1e6d0b4e..a47945984a 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -4,6 +4,8 @@ being built/installed/distributed. """ +from __future__ import annotations + import contextlib import logging import os @@ -13,6 +15,7 @@ import warnings from collections.abc import Iterable from email import message_from_file +from typing import TYPE_CHECKING, TypeVar, overload from packaging.utils import canonicalize_name, canonicalize_version @@ -27,6 +30,11 @@ from .fancy_getopt import FancyGetopt, translate_longopt from .util import check_environ, rfc822_escape, strtobool +if TYPE_CHECKING: + from .cmd import Command + +_CommandT = TypeVar("_CommandT", bound="Command") + # Regex to define acceptable Distutils command names. This is not *quite* # the same as a Python NAME -- I don't allow leading underscores. The fact # that they're very similar is no coincidence; the default naming scheme is @@ -900,7 +908,17 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 except ValueError as msg: raise DistutilsOptionError(msg) - def reinitialize_command(self, command, reinit_subcommands=False): + @overload + def reinitialize_command( + self, command: str, reinit_subcommands: bool = False + ) -> Command: ... + @overload + def reinitialize_command( + self, command: _CommandT, reinit_subcommands: bool = False + ) -> _CommandT: ... + def reinitialize_command( + self, command: str | Command, reinit_subcommands=False + ) -> Command: """Reinitializes a command to the state it was in when first returned by 'get_command_obj()': ie., initialized but not yet finalized. This provides the opportunity to sneak option From 2a01f314e1f4e0091e4bab2ddb498b4e7c789045 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 25 Nov 2024 17:29:14 -0500 Subject: [PATCH 24/34] Coerce Distribution.script_args to list --- distutils/core.py | 5 ++++- distutils/dist.py | 4 +++- distutils/fancy_getopt.py | 6 ++++-- distutils/tests/test_dist.py | 6 ++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/distutils/core.py b/distutils/core.py index bc06091abb..bd62546bdd 100644 --- a/distutils/core.py +++ b/distutils/core.py @@ -6,9 +6,12 @@ really defined in distutils.dist and distutils.cmd. """ +from __future__ import annotations + import os import sys import tokenize +from collections.abc import Iterable from .cmd import Command from .debug import DEBUG @@ -215,7 +218,7 @@ def run_commands(dist): return dist -def run_setup(script_name, script_args=None, stop_after="run"): +def run_setup(script_name, script_args: Iterable[str] | None = None, stop_after="run"): """Run a setup script in a somewhat controlled environment, and return the Distribution instance that drives things. This is useful if you need to find out the distribution meta-data (passed as diff --git a/distutils/dist.py b/distutils/dist.py index 8e1e6d0b4e..b633a62236 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -169,7 +169,7 @@ def __init__(self, attrs=None): # noqa: C901 # and sys.argv[1:], but they can be overridden when the caller is # not necessarily a setup script run from the command-line. self.script_name = None - self.script_args = None + self.script_args: list[str] | None = None # 'command_options' is where we store command options between # parsing them (from config files, the command-line, etc.) and when @@ -269,6 +269,8 @@ def __init__(self, attrs=None): # noqa: C901 self.want_user_cfg = True if self.script_args is not None: + # Coerce any possible iterable from attrs into a list + self.script_args = list(self.script_args) for arg in self.script_args: if not arg.startswith('-'): break diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 4ea89603fa..c4aeaf2348 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -8,6 +8,8 @@ * options set attributes of a passed-in object """ +from __future__ import annotations + import getopt import re import string @@ -219,7 +221,7 @@ def _grok_option_table(self): # noqa: C901 self.short_opts.append(short) self.short2long[short[0]] = long - def getopt(self, args=None, object=None): # noqa: C901 + def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901 """Parse command-line options in args. Store as attributes on object. If 'args' is None or not supplied, uses 'sys.argv[1:]'. If @@ -375,7 +377,7 @@ def print_help(self, header=None, file=None): file.write(line + "\n") -def fancy_getopt(options, negative_opt, object, args): +def fancy_getopt(options, negative_opt, object, args: Sequence[str] | None): parser = FancyGetopt(options) parser.set_negative_aliases(negative_opt) return parser.getopt(args, object) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 4d78a19803..7f44777eac 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -246,6 +246,12 @@ def test_find_config_files_disable(self, temp_home): # make sure --no-user-cfg disables the user cfg file assert len(all_files) - 1 == len(files) + def test_script_args_list_coercion(self): + d = Distribution(attrs={'script_args': ('build', '--no-user-cfg')}) + + # make sure script_args is a list even if it started as a different iterable + assert d.script_args == ['build', '--no-user-cfg'] + @pytest.mark.skipif( 'platform.system() == "Windows"', reason='Windows does not honor chmod 000', From ac548562ccc1633ff69b721a1c0ef084ffb011ac Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 24 Nov 2024 15:48:15 -0500 Subject: [PATCH 25/34] Remove py38 compat modules --- conftest.py | 2 +- distutils/compat/__init__.py | 4 +-- distutils/compat/py38.py | 34 ------------------ distutils/tests/compat/py38.py | 50 --------------------------- distutils/tests/compat/py39.py | 22 ++++++++++++ distutils/tests/test_bdist_rpm.py | 3 +- distutils/tests/test_build_ext.py | 8 ++--- distutils/tests/test_extension.py | 3 +- distutils/tests/test_filelist.py | 2 +- distutils/tests/test_spawn.py | 2 +- distutils/tests/test_unixccompiler.py | 2 +- distutils/util.py | 12 ++----- ruff.toml | 4 +++ 13 files changed, 37 insertions(+), 111 deletions(-) delete mode 100644 distutils/compat/py38.py delete mode 100644 distutils/tests/compat/py38.py create mode 100644 distutils/tests/compat/py39.py diff --git a/conftest.py b/conftest.py index 98f98d41ab..3b9444f78c 100644 --- a/conftest.py +++ b/conftest.py @@ -48,7 +48,7 @@ def _save_cwd(): @pytest.fixture def distutils_managed_tempdir(request): - from distutils.tests.compat import py38 as os_helper + from distutils.tests.compat import py39 as os_helper self = request.instance self.tempdirs = [] diff --git a/distutils/compat/__init__.py b/distutils/compat/__init__.py index e12534a32c..c715ee9cc5 100644 --- a/distutils/compat/__init__.py +++ b/distutils/compat/__init__.py @@ -1,7 +1,5 @@ from __future__ import annotations -from .py38 import removeprefix - def consolidate_linker_args(args: list[str]) -> list[str] | str: """ @@ -12,4 +10,4 @@ def consolidate_linker_args(args: list[str]) -> list[str] | str: if not all(arg.startswith('-Wl,') for arg in args): return args - return '-Wl,' + ','.join(removeprefix(arg, '-Wl,') for arg in args) + return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args) diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py deleted file mode 100644 index 03ec73ef0e..0000000000 --- a/distutils/compat/py38.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys - -if sys.version_info < (3, 9): - - def removesuffix(self, suffix): - # suffix='' should not call self[:-0]. - if suffix and self.endswith(suffix): - return self[: -len(suffix)] - else: - return self[:] - - def removeprefix(self, prefix): - if self.startswith(prefix): - return self[len(prefix) :] - else: - return self[:] - -else: - - def removesuffix(self, suffix): - return self.removesuffix(suffix) - - def removeprefix(self, prefix): - return self.removeprefix(prefix) - - -def aix_platform(osname, version, release): - try: - import _aix_support # type: ignore - - return _aix_support.aix_platform() - except ImportError: - pass - return f"{osname}-{version}.{release}" diff --git a/distutils/tests/compat/py38.py b/distutils/tests/compat/py38.py deleted file mode 100644 index 211d3a6c50..0000000000 --- a/distutils/tests/compat/py38.py +++ /dev/null @@ -1,50 +0,0 @@ -# flake8: noqa - -import contextlib -import builtins -import sys - -from test.support import requires_zlib -import test.support - - -ModuleNotFoundError = getattr(builtins, 'ModuleNotFoundError', ImportError) - -try: - from test.support.warnings_helper import check_warnings -except (ModuleNotFoundError, ImportError): - from test.support import check_warnings - - -try: - from test.support.os_helper import ( - rmtree, - EnvironmentVarGuard, - unlink, - skip_unless_symlink, - temp_dir, - ) -except (ModuleNotFoundError, ImportError): - from test.support import ( - rmtree, - EnvironmentVarGuard, - unlink, - skip_unless_symlink, - temp_dir, - ) - - -try: - from test.support.import_helper import ( - DirsOnSysPath, - CleanImport, - ) -except (ModuleNotFoundError, ImportError): - from test.support import ( - DirsOnSysPath, - CleanImport, - ) - - -if sys.version_info < (3, 9): - requires_zlib = lambda: test.support.requires_zlib diff --git a/distutils/tests/compat/py39.py b/distutils/tests/compat/py39.py new file mode 100644 index 0000000000..8246883695 --- /dev/null +++ b/distutils/tests/compat/py39.py @@ -0,0 +1,22 @@ +import sys + +if sys.version_info >= (3, 10): + from test.support.import_helper import ( + CleanImport as CleanImport, + DirsOnSysPath as DirsOnSysPath, + ) + from test.support.os_helper import ( + EnvironmentVarGuard as EnvironmentVarGuard, + rmtree as rmtree, + skip_unless_symlink as skip_unless_symlink, + unlink as unlink, + ) +else: + from test.support import ( + CleanImport as CleanImport, + DirsOnSysPath as DirsOnSysPath, + EnvironmentVarGuard as EnvironmentVarGuard, + rmtree as rmtree, + skip_unless_symlink as skip_unless_symlink, + unlink as unlink, + ) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 1109fdf117..75051430e2 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -8,8 +8,7 @@ from distutils.tests import support import pytest - -from .compat.py38 import requires_zlib +from test.support import requires_zlib SETUP_PY = """\ from distutils.core import setup diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 8bd3cef855..8477c9da1d 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -19,11 +19,7 @@ ) from distutils.extension import Extension from distutils.tests import missing_compiler_executable -from distutils.tests.support import ( - TempdirManager, - copy_xxmodule_c, - fixup_build_ext, -) +from distutils.tests.support import TempdirManager, copy_xxmodule_c, fixup_build_ext from io import StringIO import jaraco.path @@ -31,7 +27,7 @@ import pytest from test import support -from .compat import py38 as import_helper +from .compat import py39 as import_helper @pytest.fixture() diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 41872e04e8..e51c1cd8e7 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -6,8 +6,7 @@ from distutils.extension import Extension, read_setup_file import pytest - -from .compat.py38 import check_warnings +from test.support.warnings_helper import check_warnings class TestExtension: diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index ec7e5cf363..130e6fb53b 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -10,7 +10,7 @@ import jaraco.path import pytest -from .compat import py38 as os_helper +from .compat import py39 as os_helper MANIFEST_IN = """\ include ok diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index fcbc765ef2..3b9fc926f6 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -12,7 +12,7 @@ import pytest from test.support import unix_shell -from .compat import py38 as os_helper +from .compat import py39 as os_helper class TestSpawn(support.TempdirManager): diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 1695328771..2c2f4aaec2 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -12,7 +12,7 @@ import pytest from . import support -from .compat.py38 import EnvironmentVarGuard +from .compat.py39 import EnvironmentVarGuard @pytest.fixture(autouse=True) diff --git a/distutils/util.py b/distutils/util.py index 8d8260bc33..1334e2f799 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -25,7 +25,7 @@ from .spawn import spawn -def get_host_platform(): +def get_host_platform() -> str: """ Return a string that identifies the current platform. Use this function to distinguish platform-specific build directories and @@ -34,15 +34,7 @@ def get_host_platform(): # This function initially exposed platforms as defined in Python 3.9 # even with older Python versions when distutils was split out. - # Now it delegates to stdlib sysconfig, but maintains compatibility. - - if sys.version_info < (3, 9): - if os.name == "posix" and hasattr(os, 'uname'): - osname, host, release, version, machine = os.uname() - if osname[:3] == "aix": - from .compat.py38 import aix_platform - - return aix_platform(osname, version, release) + # Now it delegates to stdlib sysconfig. return sysconfig.get_platform() diff --git a/ruff.toml b/ruff.toml index 0cc5b267d7..b09308276e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -47,6 +47,10 @@ ignore = [ "TRY400", ] +[lint.isort] +combine-as-imports = true +split-on-trailing-comma = false + [format] # Enable preview to get hugged parenthesis unwrapping and other nice surprises # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 From 4e6e8fc954fad20d0d869524594d30b12a5aba34 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 20:52:27 -0500 Subject: [PATCH 26/34] Remove UP036 exclusion. --- ruff.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index b09308276e..da3e3f8dab 100644 --- a/ruff.toml +++ b/ruff.toml @@ -21,7 +21,6 @@ extend-select = [ ignore = [ # TODO: Fix these new violations in Ruff 0.8.0 "UP031", - "UP036", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", From fc15d4575aec0c4adeec367c777bac3b642bdc8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 20:54:55 -0500 Subject: [PATCH 27/34] Prefer the standard format for imports, even though it's unnecessarily repetitive. --- distutils/tests/compat/py39.py | 18 ++++++++++++++++++ ruff.toml | 4 ---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/distutils/tests/compat/py39.py b/distutils/tests/compat/py39.py index 8246883695..aca3939a0c 100644 --- a/distutils/tests/compat/py39.py +++ b/distutils/tests/compat/py39.py @@ -3,20 +3,38 @@ if sys.version_info >= (3, 10): from test.support.import_helper import ( CleanImport as CleanImport, + ) + from test.support.import_helper import ( DirsOnSysPath as DirsOnSysPath, ) from test.support.os_helper import ( EnvironmentVarGuard as EnvironmentVarGuard, + ) + from test.support.os_helper import ( rmtree as rmtree, + ) + from test.support.os_helper import ( skip_unless_symlink as skip_unless_symlink, + ) + from test.support.os_helper import ( unlink as unlink, ) else: from test.support import ( CleanImport as CleanImport, + ) + from test.support import ( DirsOnSysPath as DirsOnSysPath, + ) + from test.support import ( EnvironmentVarGuard as EnvironmentVarGuard, + ) + from test.support import ( rmtree as rmtree, + ) + from test.support import ( skip_unless_symlink as skip_unless_symlink, + ) + from test.support import ( unlink as unlink, ) diff --git a/ruff.toml b/ruff.toml index da3e3f8dab..0d8179b35e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -46,10 +46,6 @@ ignore = [ "TRY400", ] -[lint.isort] -combine-as-imports = true -split-on-trailing-comma = false - [format] # Enable preview to get hugged parenthesis unwrapping and other nice surprises # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 From deb1d5a9f4b8c1b8c722e4ab844863469b882387 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 25 Oct 2024 13:17:22 -0400 Subject: [PATCH 28/34] type `Distribution.get_command_obj` to not return `None` with `create=True` --- distutils/dist.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/distutils/dist.py b/distutils/dist.py index ef4f4e0241..e8c5236be8 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -15,7 +15,7 @@ import warnings from collections.abc import Iterable from email import message_from_file -from typing import TYPE_CHECKING, TypeVar, overload +from typing import TYPE_CHECKING, Literal, TypeVar, overload from packaging.utils import canonicalize_name, canonicalize_version @@ -31,6 +31,7 @@ from .util import check_environ, rfc822_escape, strtobool if TYPE_CHECKING: + # type-only import because of mutual dependence between these modules from .cmd import Command _CommandT = TypeVar("_CommandT", bound="Command") @@ -837,7 +838,15 @@ def get_command_class(self, command): raise DistutilsModuleError(f"invalid command '{command}'") - def get_command_obj(self, command, create=True): + @overload + def get_command_obj( + self, command: str, create: Literal[True] = True + ) -> Command: ... + @overload + def get_command_obj( + self, command: str, create: Literal[False] + ) -> Command | None: ... + def get_command_obj(self, command: str, create: bool = True) -> Command | None: """Return the command object for 'command'. Normally this object is cached on a previous call to 'get_command_obj()'; if no command object for 'command' is in the cache, then we either create and From 9d9887db963e8f3e8e6758e1a3d3d2238a7d1f23 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 26 Dec 2024 19:49:58 -0500 Subject: [PATCH 29/34] ClassVar classvar mutables and tuple from typeshed --- distutils/cmd.py | 12 ++++++++++-- distutils/command/bdist.py | 5 +++-- distutils/command/build_clib.py | 3 ++- distutils/command/build_scripts.py | 3 ++- distutils/command/check.py | 3 ++- distutils/command/command_template | 8 ++++---- distutils/command/install_egg_info.py | 3 ++- distutils/command/install_headers.py | 4 +++- distutils/command/sdist.py | 3 ++- distutils/tests/test_dist.py | 3 ++- 10 files changed, 32 insertions(+), 15 deletions(-) diff --git a/distutils/cmd.py b/distutils/cmd.py index 6ffe7bd4b3..9c6fa6566c 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -10,7 +10,8 @@ import os import re import sys -from typing import TypeVar, overload +from collections.abc import Callable +from typing import Any, ClassVar, TypeVar, overload from . import _modified, archive_util, dir_util, file_util, util from ._log import log @@ -49,7 +50,14 @@ class Command: # 'sub_commands' is usually defined at the *end* of a class, because # predicates can be unbound methods, so they must already have been # defined. The canonical example is the "install" command. - sub_commands = [] + sub_commands: ClassVar[ # Any to work around variance issues + list[tuple[str, Callable[[Any], bool] | None]] + ] = [] + + user_options: ClassVar[ + # Specifying both because list is invariant. Avoids mypy override assignment issues + list[tuple[str, str, str]] | list[tuple[str, str | None, str]] + ] = [] # -- Creation/initialization methods ------------------------------- diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py index f334075159..1ec3c35f40 100644 --- a/distutils/command/bdist.py +++ b/distutils/command/bdist.py @@ -5,6 +5,7 @@ import os import warnings +from typing import ClassVar from ..core import Command from ..errors import DistutilsOptionError, DistutilsPlatformError @@ -23,7 +24,7 @@ def show_formats(): pretty_printer.print_help("List of available distribution formats:") -class ListCompat(dict): +class ListCompat(dict[str, tuple[str, str]]): # adapter to allow for Setuptools compatibility in format_commands def append(self, item): warnings.warn( @@ -70,7 +71,7 @@ class bdist(Command): ] # The following commands do not take a format option from bdist - no_format_option = ('bdist_rpm',) + no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',) # This won't do in reality: will need to distinguish RPM-ish Linux, # Debian-ish Linux, Solaris, FreeBSD, ..., Windows, Mac OS. diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py index 1305d5bb3d..3e1832768b 100644 --- a/distutils/command/build_clib.py +++ b/distutils/command/build_clib.py @@ -16,6 +16,7 @@ import os from distutils._log import log +from typing import ClassVar from ..core import Command from ..errors import DistutilsSetupError @@ -31,7 +32,7 @@ def show_compilers(): class build_clib(Command): description = "build C/C++ libraries used by Python extensions" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('build-clib=', 'b', "directory to build C/C++ libraries to"), ('build-temp=', 't', "directory to put temporary build by-products"), ('debug', 'g', "compile with debugging information"), diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 9e5963c243..1c6fd3caff 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -8,6 +8,7 @@ from distutils import sysconfig from distutils._log import log from stat import ST_MODE +from typing import ClassVar from .._modified import newer from ..core import Command @@ -25,7 +26,7 @@ class build_scripts(Command): description = "\"build\" scripts (copy and fixup #! line)" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('build-dir=', 'd', "directory to \"build\" (copy) to"), ('force', 'f', "forcibly build everything (ignore file timestamps"), ('executable=', 'e', "specify final destination interpreter path"), diff --git a/distutils/command/check.py b/distutils/command/check.py index 1375028e4d..078c1ce87e 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -4,6 +4,7 @@ """ import contextlib +from typing import ClassVar from ..core import Command from ..errors import DistutilsSetupError @@ -41,7 +42,7 @@ class check(Command): """This command checks the meta-data of the package.""" description = "perform some checks on the package" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('metadata', 'm', 'Verify meta-data'), ( 'restructuredtext', diff --git a/distutils/command/command_template b/distutils/command/command_template index 6106819db8..a4a751ad3c 100644 --- a/distutils/command/command_template +++ b/distutils/command/command_template @@ -8,18 +8,18 @@ Implements the Distutils 'x' command. __revision__ = "$Id$" from distutils.core import Command +from typing import ClassVar class x(Command): - # Brief (40-50 characters) description of the command description = "" # List of option tuples: long name, short name (None if no short # name), and help string. - user_options = [('', '', - ""), - ] + user_options: ClassVar[list[tuple[str, str, str]]] = [ + ('', '', ""), + ] def initialize_options(self): self. = None diff --git a/distutils/command/install_egg_info.py b/distutils/command/install_egg_info.py index 0baeee7bb4..230e94ab46 100644 --- a/distutils/command/install_egg_info.py +++ b/distutils/command/install_egg_info.py @@ -8,6 +8,7 @@ import os import re import sys +from typing import ClassVar from .. import dir_util from .._log import log @@ -18,7 +19,7 @@ class install_egg_info(Command): """Install an .egg-info file for the package""" description = "Install package's PKG-INFO metadata as an .egg-info file" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('install-dir=', 'd', "directory to install to"), ] diff --git a/distutils/command/install_headers.py b/distutils/command/install_headers.py index fbb3b242ea..586121e089 100644 --- a/distutils/command/install_headers.py +++ b/distutils/command/install_headers.py @@ -3,6 +3,8 @@ Implements the Distutils 'install_headers' command, to install C/C++ header files to the Python include directory.""" +from typing import ClassVar + from ..core import Command @@ -10,7 +12,7 @@ class install_headers(Command): description = "install C/C++ header files" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ('install-dir=', 'd', "directory to install header files to"), ('force', 'f', "force installation (overwrite existing files)"), ] diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index 003e0bf875..acb3a41650 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -8,6 +8,7 @@ from distutils._log import log from glob import glob from itertools import filterfalse +from typing import ClassVar from ..core import Command from ..errors import DistutilsOptionError, DistutilsTemplateError @@ -114,7 +115,7 @@ def checking_metadata(self): sub_commands = [('check', checking_metadata)] - READMES = ('README', 'README.txt', 'README.rst') + READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst') def initialize_options(self): # 'template' and 'manifest' are, respectively, the names of diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 4d78a19803..cd07bcd048 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -13,6 +13,7 @@ from distutils.cmd import Command from distutils.dist import Distribution, fix_help_options from distutils.tests import support +from typing import ClassVar import jaraco.path import pytest @@ -23,7 +24,7 @@ class test_dist(Command): """Sample distutils extension command.""" - user_options = [ + user_options: ClassVar[list[tuple[str, str, str]]] = [ ("sample-option=", "S", "help text"), ] From cebba7f925b40fdab5fd47f8ec92aa158c989a3c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 21:17:00 -0500 Subject: [PATCH 30/34] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/extension.py b/distutils/extension.py index f925987e84..e053273436 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -107,7 +107,7 @@ def __init__( **kw, # To catch unknown keywords ): if not isinstance(name, str): - raise TypeError("'name' must be a string") # noqa: TRY004 + raise TypeError("'name' must be a string") # handle the string case first; since strings are iterable, disallow them if isinstance(sources, str): From af7fcbb0d56ae14753db53acd8792eddb4d8f814 Mon Sep 17 00:00:00 2001 From: Sam James Date: Sun, 22 Dec 2024 01:44:16 +0000 Subject: [PATCH 31/34] Use CFLAGS if set as-is, match CXXFLAGS behavior Since 2c937116cc0dcd9b26b6070e89a3dc5dcbedc2ae, CXXFLAGS is used as-is if set in the envionment rather than clobbered by whatever CPython happened to be built with. Do the same for CFLAGS: use it as-is if set in the environment, don't prepend CPython's saved flags. Fixes: https://github.com/pypa/distutils/issues/299 --- distutils/sysconfig.py | 1 + distutils/tests/test_sysconfig.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index fc0ea78721..358d1079dc 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -340,6 +340,7 @@ def customize_compiler(compiler): ldshared = _add_flags(ldshared, 'LD') ldcxxshared = _add_flags(ldcxxshared, 'LD') + cflags = os.environ.get('CFLAGS', cflags) cflags = _add_flags(cflags, 'C') ldshared = _add_flags(ldshared, 'C') cxxflags = os.environ.get('CXXFLAGS', cxxflags) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 49274a36ae..3191e7717b 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -130,9 +130,9 @@ def test_customize_compiler(self): comp = self.customize_compiler() assert comp.exes['archiver'] == 'env_ar --env-arflags' assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags' - assert comp.exes['compiler'] == 'env_cc --sc-cflags --env-cflags --env-cppflags' + assert comp.exes['compiler'] == 'env_cc --env-cflags --env-cflags --env-cppflags' assert comp.exes['compiler_so'] == ( - 'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared' + 'env_cc --env-cflags --env-cflags --env-cppflags --sc-ccshared' ) assert ( comp.exes['compiler_cxx'] From 630551a88b9e7394b2996728a0b6a50500b8e45b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 21:38:53 -0500 Subject: [PATCH 32/34] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_sysconfig.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 3191e7717b..867e7dcb39 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -130,7 +130,9 @@ def test_customize_compiler(self): comp = self.customize_compiler() assert comp.exes['archiver'] == 'env_ar --env-arflags' assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags' - assert comp.exes['compiler'] == 'env_cc --env-cflags --env-cflags --env-cppflags' + assert ( + comp.exes['compiler'] == 'env_cc --env-cflags --env-cflags --env-cppflags' + ) assert comp.exes['compiler_so'] == ( 'env_cc --env-cflags --env-cflags --env-cppflags --sc-ccshared' ) From 2296e9fabe70cbd765f8cbf992c7020a3e54a5b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Dec 2024 03:01:25 -0500 Subject: [PATCH 33/34] Add news fragment. --- newsfragments/4478.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4478.feature.rst diff --git a/newsfragments/4478.feature.rst b/newsfragments/4478.feature.rst new file mode 100644 index 0000000000..bd53339464 --- /dev/null +++ b/newsfragments/4478.feature.rst @@ -0,0 +1 @@ +Synced with pypa/distutils@c97a3db2f including better support for free threaded Python on Windows (pypa/distutils#310), improved typing support, and linter accommodations. From 0ebc351ea53236c542dc3f081546e3e0917f38b7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Dec 2024 03:20:57 -0500 Subject: [PATCH 34/34] Fix term reference in quickstart. Closes #4779 --- docs/userguide/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index d303ab9355..606654f86c 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -199,7 +199,7 @@ Package discovery ----------------- For projects that follow a simple directory structure, ``setuptools`` should be able to automatically detect all :term:`packages ` and -:term:`namespaces `. However, complex projects might include +:term:`namespaces `. However, complex projects might include additional folders and supporting files that not necessarily should be distributed (or that can confuse ``setuptools`` auto discovery algorithm).