From 3fc912a390f46f1163bdccdfe1be9b84401b96b4 Mon Sep 17 00:00:00 2001 From: Anders Theet Date: Tue, 8 Oct 2024 08:30:18 +0200 Subject: [PATCH 01/39] 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/39] 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 464735f1f188fd2dd6eaac12d2985e851e4658bd Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Tue, 8 Oct 2024 10:14:57 -0400 Subject: [PATCH 03/39] Fix issue with absolute path with Python 3.13 on Windows With Python 3.13 on Windows, `os.path.isabs()` no longer returns `True` for a path that starts with a slash. Thus, when the argument to `_make_relative()` is an absolute path, the return value starts with a slash on Python 3.13 and does not start with a slash on older Python versions. This causes the extension module build directory to be calculated incorrectly with Python 3.13 on Windows. Fix this by ensuring that the return value does not start with a slash. --- distutils/ccompiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 5e73e56d02..fdbb1ca795 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -989,7 +989,8 @@ def _make_relative(base): # Chop off the drive no_drive = os.path.splitdrive(base)[1] # If abs, chop off leading / - return no_drive[os.path.isabs(no_drive) :] + is_abs = os.path.isabs(no_drive) or sys.platform == 'win32' and (no_drive.startswith('/') or no_drive.startswith('\\')) + return no_drive[is_abs:] def shared_object_filename(self, basename, strip_dir=False, output_dir=''): assert output_dir is not None 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 04/39] 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 05/39] 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 06/39] 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 07/39] 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 08/39] 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 09/39] 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 10/39] 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 11/39] 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 52848a0d32ee377a578b8cafd7090446e240eb9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 11:19:27 -0500 Subject: [PATCH 12/39] 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 13/39] =?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 14/39] 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 15/39] 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 16/39] 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 17/39] =?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 18/39] 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 19/39] 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 20/39] 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 21/39] 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 22/39] 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 23/39] 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 24/39] 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 25/39] 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 26/39] 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 27/39] 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 28/39] 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 29/39] =?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 edfd6d2159374575cdcc16834712243ed366c64f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 26 Dec 2024 21:18:30 -0500 Subject: [PATCH 30/39] Collapse startswith operations Co-authored-by: Avasam --- distutils/ccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index fdbb1ca795..6979d160eb 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -989,7 +989,7 @@ def _make_relative(base): # Chop off the drive no_drive = os.path.splitdrive(base)[1] # If abs, chop off leading / - is_abs = os.path.isabs(no_drive) or sys.platform == 'win32' and (no_drive.startswith('/') or no_drive.startswith('\\')) + is_abs = os.path.isabs(no_drive) or sys.platform == 'win32' and no_drive.startswith(('/', "\\")) return no_drive[is_abs:] def shared_object_filename(self, basename, strip_dir=False, output_dir=''): From af7fcbb0d56ae14753db53acd8792eddb4d8f814 Mon Sep 17 00:00:00 2001 From: Sam James Date: Sun, 22 Dec 2024 01:44:16 +0000 Subject: [PATCH 31/39] 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/39] =?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 bba52647d2e90368e7662d5573bb9a63a9184318 Mon Sep 17 00:00:00 2001 From: Sam James Date: Sat, 28 Dec 2024 16:45:50 +0000 Subject: [PATCH 33/39] Don't duplicate CFLAGS Followup to af7fcbb0d56ae14753db53acd8792eddb4d8f814. I accidentally left that in when trying two approaches. Reported at https://github.com/pypa/distutils/pull/322#discussion_r1898349462. --- distutils/sysconfig.py | 1 - distutils/tests/test_sysconfig.py | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 358d1079dc..ef3def83eb 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -341,7 +341,6 @@ 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) ldcxxshared = _add_flags(ldcxxshared, 'CXX') diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 867e7dcb39..43d77c23fa 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -130,11 +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-cppflags' assert comp.exes['compiler_so'] == ( - 'env_cc --env-cflags --env-cflags --env-cppflags --sc-ccshared' + 'env_cc --env-cflags --env-cppflags --sc-ccshared' ) assert ( comp.exes['compiler_cxx'] From d13d5a7d9210114447aae0ba1a814ae6af8aeffe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 04:39:08 -0500 Subject: [PATCH 34/39] =?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/ccompiler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 6979d160eb..fbf1f7a4cf 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -989,7 +989,11 @@ def _make_relative(base): # Chop off the drive no_drive = os.path.splitdrive(base)[1] # If abs, chop off leading / - is_abs = os.path.isabs(no_drive) or sys.platform == 'win32' and no_drive.startswith(('/', "\\")) + is_abs = ( + os.path.isabs(no_drive) + or sys.platform == 'win32' + and no_drive.startswith(('/', "\\")) + ) return no_drive[is_abs:] def shared_object_filename(self, basename, strip_dir=False, output_dir=''): From 1400152e10663f51109869b9d387c50820a95767 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 04:56:38 -0500 Subject: [PATCH 35/39] Extract a classmethod _make_out_path_exts suitable for isolated testing. --- distutils/ccompiler.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index fbf1f7a4cf..6e303c34e6 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -969,10 +969,16 @@ def out_extensions(self): return dict.fromkeys(self.src_extensions, self.obj_extension) def _make_out_path(self, output_dir, strip_dir, src_name): + return self._make_out_path_exts( + output_dir, strip_dir, src_name, self.out_extensions + ) + + @classmethod + def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions): base, ext = os.path.splitext(src_name) - base = self._make_relative(base) + base = cls._make_relative(base) try: - new_ext = self.out_extensions[ext] + new_ext = extensions[ext] except LookupError: raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: From b16c1de88e33bea41ec5c74abdb225e9f8704a3a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 05:12:54 -0500 Subject: [PATCH 36/39] Re-write _make_relative to rely on pathlib for cross-platform and cross-python compatibility. --- distutils/ccompiler.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 6e303c34e6..9b637f8c98 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -4,6 +4,7 @@ for the Distutils compiler abstraction model.""" import os +import pathlib import re import sys import types @@ -976,31 +977,20 @@ def _make_out_path(self, output_dir, strip_dir, src_name): @classmethod def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions): base, ext = os.path.splitext(src_name) + base = pathlib.PurePath(base) + # Ensure base is relative to honor output_dir (python/cpython#37775). base = cls._make_relative(base) try: new_ext = extensions[ext] except LookupError: raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: - base = os.path.basename(base) - return os.path.join(output_dir, base + new_ext) + base = base.name + return os.path.join(output_dir, base.with_suffix(new_ext)) @staticmethod - def _make_relative(base): - """ - In order to ensure that a filename always honors the - indicated output_dir, make sure it's relative. - Ref python/cpython#37775. - """ - # Chop off the drive - no_drive = os.path.splitdrive(base)[1] - # If abs, chop off leading / - is_abs = ( - os.path.isabs(no_drive) - or sys.platform == 'win32' - and no_drive.startswith(('/', "\\")) - ) - return no_drive[is_abs:] + def _make_relative(base: pathlib.Path): + return base.relative_to(base.anchor) def shared_object_filename(self, basename, strip_dir=False, output_dir=''): assert output_dir is not None From 57152cf9766590e205346752aa632842b5e8a741 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 05:19:35 -0500 Subject: [PATCH 37/39] Move suppress_path_mangle to test_ccompiler to limit the scope. --- conftest.py | 2 +- distutils/tests/test_ccompiler.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 3b9444f78c..578b7aac59 100644 --- a/conftest.py +++ b/conftest.py @@ -92,7 +92,7 @@ def monkeysession(request): mpatch.undo() -@pytest.fixture(autouse=True, scope="session") +@pytest.fixture(scope="module") def suppress_path_mangle(monkeysession): """ Disable the path mangling in CCompiler. Workaround for #169. diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index d23b907cad..7ebfed56be 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -7,6 +7,8 @@ import pytest +pytestmark = pytest.mark.usefixtures('suppress_path_mangle') + def _make_strs(paths): """ From ede1af29d210311f5f033f7226da58a19f197183 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 05:23:51 -0500 Subject: [PATCH 38/39] Add tests and fix failure identified in the tests. --- distutils/ccompiler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 9b637f8c98..7b5baaf9ae 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -976,6 +976,13 @@ def _make_out_path(self, output_dir, strip_dir, src_name): @classmethod def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions): + r""" + >>> exts = {'.c': '.o'} + >>> CCompiler._make_out_path_exts('.', False, '/foo/bar.c', exts).replace('\\', '/') + './foo/bar.o' + >>> CCompiler._make_out_path_exts('.', True, '/foo/bar.c', exts).replace('\\', '/') + './bar.o' + """ base, ext = os.path.splitext(src_name) base = pathlib.PurePath(base) # Ensure base is relative to honor output_dir (python/cpython#37775). @@ -985,7 +992,7 @@ def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions): except LookupError: raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: - base = base.name + base = pathlib.PurePath(base.name) return os.path.join(output_dir, base.with_suffix(new_ext)) @staticmethod From 36ce8b329524088cfa53b9a4bffcce3a8d233539 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2025 05:38:01 -0500 Subject: [PATCH 39/39] Refactor for simplicity. --- distutils/ccompiler.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 7b5baaf9ae..714f13d8d3 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -983,14 +983,13 @@ def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions): >>> CCompiler._make_out_path_exts('.', True, '/foo/bar.c', exts).replace('\\', '/') './bar.o' """ - base, ext = os.path.splitext(src_name) - base = pathlib.PurePath(base) + src = pathlib.PurePath(src_name) # Ensure base is relative to honor output_dir (python/cpython#37775). - base = cls._make_relative(base) + base = cls._make_relative(src) try: - new_ext = extensions[ext] + new_ext = extensions[src.suffix] except LookupError: - raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") + raise UnknownFileError(f"unknown file type '{src.suffix}' (from '{src}')") if strip_dir: base = pathlib.PurePath(base.name) return os.path.join(output_dir, base.with_suffix(new_ext))