From e649e936e79fb5cbbf45f63475934faa3cb0f4bc Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Tue, 28 Feb 2023 08:55:11 +0300 Subject: [PATCH 001/232] Fix accumulating flags after compile/link --- distutils/ccompiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 1818fce9018..ae60578ac22 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -382,7 +382,7 @@ def _fix_compile_args(self, output_dir, macros, include_dirs): raise TypeError("'output_dir' must be a string or None") if macros is None: - macros = self.macros + macros = list(self.macros) elif isinstance(macros, list): macros = macros + (self.macros or []) else: @@ -441,14 +441,14 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): fixed versions of all arguments. """ if libraries is None: - libraries = self.libraries + libraries = list(self.libraries) elif isinstance(libraries, (list, tuple)): libraries = list(libraries) + (self.libraries or []) else: raise TypeError("'libraries' (if supplied) must be a list of strings") if library_dirs is None: - library_dirs = self.library_dirs + library_dirs = list(self.library_dirs) elif isinstance(library_dirs, (list, tuple)): library_dirs = list(library_dirs) + (self.library_dirs or []) else: @@ -458,7 +458,7 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): library_dirs += self.__class__.library_dirs if runtime_library_dirs is None: - runtime_library_dirs = self.runtime_library_dirs + runtime_library_dirs = list(self.runtime_library_dirs) elif isinstance(runtime_library_dirs, (list, tuple)): runtime_library_dirs = list(runtime_library_dirs) + ( self.runtime_library_dirs or [] From ef9a76640ab0c64a502377e2c345d34d052fb48d Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sun, 6 Aug 2023 18:45:55 -0400 Subject: [PATCH 002/232] CI: Install git on Cygwin CI runner Cygwin pip now has a chance to resolve everything on the command line. It won't be able to resolve dependencies, due to something pulling in Rust, but it'll get to the point where pip points out that it is not pip's fault that CI doesn't have Rust compilers for Cygwin --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60801acecd2..dbba53e2b7c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,6 +58,7 @@ jobs: gcc-core, gcc-g++, ncompress + git - name: Run tests shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} run: tox From aa3a9968c9c6944645b2bf5e5e714c82d3c392b9 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sun, 6 Aug 2023 19:11:02 -0400 Subject: [PATCH 003/232] CI: Try to fix Cygwin tox configuration. jaraco.text depends on inflect; inflect>=6.0.0 depends on Rust. Add an additional rule installing a version of the dependency that will actually install. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 06657e4eaa2..fd858d182e3 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ deps = pytest-cov pytest-enabler >= 1.3 + inflect<6.0.0; sys.platform=="cygwin" jaraco.envs>=2.4 jaraco.path jaraco.text From 222b249f4f7ee9c1b2fae7f483db88c031fe4302 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 30 Aug 2023 19:19:22 +0100 Subject: [PATCH 004/232] Improve test_rfc822_escape, capturing interoperability requirements --- distutils/tests/test_util.py | 59 ++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 070a277069d..22a003d8ca0 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -1,4 +1,8 @@ """Tests for distutils.util.""" +import email +import email.policy +import email.generator +import io import os import sys import sysconfig as stdlib_sysconfig @@ -184,12 +188,55 @@ def test_strtobool(self): for n in no: assert not strtobool(n) - def test_rfc822_escape(self): - header = 'I am a\npoor\nlonesome\nheader\n' - res = rfc822_escape(header) - wanted = ('I am a%(8s)spoor%(8s)slonesome%(8s)s' 'header%(8s)s') % { - '8s': '\n' + 8 * ' ' - } + indent = 8 * ' ' + + @pytest.mark.parametrize( + "given,wanted", + [ + # 0x0b, 0x0c, ..., etc are also considered a line break by Python + ("hello\x0b\nworld\n", f"hello\x0b{indent}\n{indent}world\n{indent}"), + ("hello\x1eworld", f"hello\x1e{indent}world"), + ("", ""), + ( + "I am a\npoor\nlonesome\nheader\n", + f"I am a\n{indent}poor\n{indent}lonesome\n{indent}header\n{indent}", + ), + ], + ) + def test_rfc822_escape(self, given, wanted): + """ + We want to ensure a multi-line header parses correctly. + + For interoperability, the escaped value should also "round-trip" over + `email.generator.Generator.flatten` and `email.message_from_*` + (see pypa/setuptools#4033). + + The main issue is that internally `email.policy.EmailPolicy` uses + `splitlines` which will split on some control chars. If all the new lines + are not prefixed with spaces, the parser will interrupt reading + the current header and produce an incomplete value, while + incorrectly interpreting the rest of the headers as part of the payload. + """ + res = rfc822_escape(given) + + policy = email.policy.EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with io.StringIO() as buffer: + raw = f"header: {res}\nother-header: 42\n\npayload\n" + orig = email.message_from_string(raw) + email.generator.Generator(buffer, policy=policy).flatten(orig) + buffer.seek(0) + regen = email.message_from_file(buffer) + + for msg in (orig, regen): + assert msg.get_payload() == "payload\n" + assert msg["other-header"] == "42" + # Generator may replace control chars with `\n` + assert set(msg["header"].splitlines()) == set(res.splitlines()) + assert res == wanted def test_dont_write_bytecode(self): From 157fbfed51a405866c9f63cc75c69cfac6b8735e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 30 Aug 2023 19:24:13 +0100 Subject: [PATCH 005/232] Improve TestMetadata, capturing interoperability requirements --- distutils/tests/test_dist.py | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 30a6f9ff2ec..694bf02a60c 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -1,6 +1,9 @@ """Tests for distutils.dist.""" import os import io +import email +import email.policy +import email.generator import sys import warnings import textwrap @@ -510,3 +513,41 @@ def test_read_metadata(self): assert metadata.platforms is None assert metadata.obsoletes is None assert metadata.requires == ['foo'] + + def test_round_trip_through_email_generator(self): + """ + In pypa/setuptools#4033, it was shown that once PKG-INFO is + re-generated using ``email.generator.Generator``, some control + characters might cause problems. + """ + # Given a PKG-INFO file ... + attrs = { + "name": "package", + "version": "1.0", + "long_description": "hello\x0b\nworld\n", + } + dist = Distribution(attrs) + metadata = dist.metadata + + with io.StringIO() as buffer: + metadata.write_pkg_file(buffer) + msg = buffer.getvalue() + + # ... when it is read and re-written using stdlib's email library, + orig = email.message_from_string(msg) + policy = email.policy.EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with io.StringIO() as buffer: + email.generator.Generator(buffer, policy=policy).flatten(orig) + + buffer.seek(0) + regen = email.message_from_file(buffer) + + # ... then it should be the same as the original + # (except for the specific line break characters) + orig_desc = set(orig["Description"].splitlines()) + regen_desc = set(regen["Description"].splitlines()) + assert regen_desc == orig_desc From 0ece9871247625ed3541b66529ca654039a5d8b5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 30 Aug 2023 19:26:11 +0100 Subject: [PATCH 006/232] Fix interoperability of rfc822_escape with stblib's email library --- distutils/util.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/distutils/util.py b/distutils/util.py index 7ef47176e27..4f94e587e22 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -508,6 +508,12 @@ def rfc822_escape(header): """Return a version of the string escaped for inclusion in an RFC-822 header, by ensuring there are 8 spaces space after each newline. """ - lines = header.split('\n') - sep = '\n' + 8 * ' ' - return sep.join(lines) + indent = 8 * " " + lines = header.splitlines(keepends=True) + + # Emulate the behaviour of `str.split` + # (the terminal line break in `splitlines` does not result in an extra line): + ends_in_newline = lines and lines[-1].splitlines()[0] != lines[-1] + suffix = indent if ends_in_newline else "" + + return indent.join(lines) + suffix From a131f83e2967514e2973fb36f2ca64e3ac8efc3c Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Tue, 26 Sep 2023 11:23:08 +0200 Subject: [PATCH 007/232] GNU: use -Wl,-rpath, instead of -Wl,-R The latter is supported in binutils for backwards compatibility, but in general `-R` is equivalent to `--just-symbols=` when `path` is a file; only when it's a directory, it's treated as `-rpath=`. Better avoid that ambiguity and use `-rpath`. Also split `-Wl,--enable-new-dtags` and `-Wl,-rpath,...` into two separate arguments, which is more common, and more likely to be parsed correctly by compiler wrappers. This commit does not attempt to add `--enable-new-dtags` to other linkers than binutils ld/gold that support the flag. --- distutils/tests/test_unixccompiler.py | 5 ++++- distutils/unixccompiler.py | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index a0184424595..23b4eb5a4cb 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -186,7 +186,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == [ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo' + ] # non-GCC GNULD sys.platform = 'bar' diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index 6ca2332ae16..d5c245969dd 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -311,13 +311,14 @@ def runtime_library_dir_option(self, dir): "-L" + dir, ] - # For all compilers, `-Wl` is the presumed way to - # pass a compiler option to the linker and `-R` is - # the way to pass an RPATH. + # For all compilers, `-Wl` is the presumed way to pass a + # compiler option to the linker if sysconfig.get_config_var("GNULD") == "yes": - # GNU ld needs an extra option to get a RUNPATH - # instead of just an RPATH. - return "-Wl,--enable-new-dtags,-R" + dir + return [ + # Force RUNPATH instead of RPATH + "-Wl,--enable-new-dtags", + "-Wl,-rpath," + dir + ] else: return "-Wl,-R" + dir From ee263dc58a6a65f60220b9ba222adc2bbe55f198 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:01:37 +0100 Subject: [PATCH 008/232] =?UTF-8?q?Update=20URLs=20in=20documentation:=20h?= =?UTF-8?q?ttp://=20=E2=86=92=20https://?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update link to an old MSDN article and point to a newer article. --- distutils/command/bdist_rpm.py | 2 +- distutils/msvc9compiler.py | 6 +++--- distutils/tests/test_bdist_rpm.py | 2 +- distutils/tests/test_build_scripts.py | 2 +- distutils/tests/test_sdist.py | 2 +- distutils/unixccompiler.py | 3 +-- docs/distutils/apiref.rst | 2 +- docs/distutils/examples.rst | 2 +- docs/distutils/setupscript.rst | 2 +- 9 files changed, 11 insertions(+), 12 deletions(-) diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 3ed608b479d..696f26751fd 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -435,7 +435,7 @@ def _make_spec_file(self): # noqa: C901 fixed = "brp-python-bytecompile %{__python} \\\n" fixed_hook = vendor_hook.replace(problem, fixed) if fixed_hook != vendor_hook: - spec_file.append('# Workaround for http://bugs.python.org/issue14443') + spec_file.append('# Workaround for https://bugs.python.org/issue14443') spec_file.append('%define __os_install_post ' + fixed_hook + '\n') # put locale summaries into spec file diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index f9f9f2d844e..724986d89d0 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -698,8 +698,8 @@ def link( # noqa: C901 def manifest_setup_ldargs(self, output_filename, build_temp, ld_args): # If we need a manifest at all, an embedded manifest is recommended. # See MSDN article titled - # "How to: Embed a Manifest Inside a C/C++ Application" - # (currently at http://msdn2.microsoft.com/en-us/library/ms235591(VS.80).aspx) + # "Understanding manifest generation for C/C++ programs" + # (currently at https://learn.microsoft.com/en-us/cpp/build/understanding-manifest-generation-for-c-cpp-programs) # Ask the linker to generate the manifest in the temp dir, so # we can check it, and possibly embed it, later. temp_manifest = os.path.join( @@ -710,7 +710,7 @@ def manifest_setup_ldargs(self, output_filename, build_temp, ld_args): def manifest_get_embed_info(self, target_desc, ld_args): # If a manifest should be embedded, return a tuple of # (manifest_filename, resource_id). Returns None if no manifest - # should be embedded. See http://bugs.python.org/issue7833 for why + # should be embedded. See https://bugs.python.org/issue7833 for why # we want to avoid any manifest for extension modules if we can) for arg in ld_args: if arg.startswith("/MANIFESTFILE:"): diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 4a702fb913e..3fd2c7e2ac1 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -89,7 +89,7 @@ def test_quiet(self): @mac_woes @requires_zlib() - # http://bugs.python.org/issue1533164 + # https://bugs.python.org/issue1533164 @pytest.mark.skipif("not find_executable('rpm')") @pytest.mark.skipif("not find_executable('rpmbuild')") def test_no_optimize_flag(self): diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index 1a5753c7729..28cc5632a33 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -88,7 +88,7 @@ def test_version_int(self): ) cmd.finalize_options() - # http://bugs.python.org/issue4524 + # https://bugs.python.org/issue4524 # # On linux-g++-32 with command line `./configure --enable-ipv6 # --with-suffix=3`, python is compiled okay but the build scripts diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index fdb768e73f2..a3fa2902750 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -162,7 +162,7 @@ def test_make_distribution(self): @pytest.mark.usefixtures('needs_zlib') def test_add_defaults(self): - # http://bugs.python.org/issue2279 + # https://bugs.python.org/issue2279 # add_default should also include # data_files and package_data diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index bd8db9ac3fa..294a16b7f46 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -283,8 +283,7 @@ def _is_gcc(self): def runtime_library_dir_option(self, dir): # XXX Hackish, at the very least. See Python bug #445902: - # http://sourceforge.net/tracker/index.php - # ?func=detail&aid=445902&group_id=5470&atid=105470 + # https://bugs.python.org/issue445902 # Linkers on different platforms need different options to # specify that directories need to be added to the list of # directories searched for dependencies when a dynamic library diff --git a/docs/distutils/apiref.rst b/docs/distutils/apiref.rst index 83b8ef5d526..beb17bc3fc2 100644 --- a/docs/distutils/apiref.rst +++ b/docs/distutils/apiref.rst @@ -1021,7 +1021,7 @@ directories. Files in *src* that begin with :file:`.nfs` are skipped (more information on these files is available in answer D2 of the `NFS FAQ page - `_). + `_). .. versionchanged:: 3.3.1 NFS files are ignored. diff --git a/docs/distutils/examples.rst b/docs/distutils/examples.rst index 28582bab36d..d758a8105ed 100644 --- a/docs/distutils/examples.rst +++ b/docs/distutils/examples.rst @@ -335,4 +335,4 @@ loads its values:: .. % \section{Putting it all together} -.. _docutils: http://docutils.sourceforge.net +.. _docutils: https://docutils.sourceforge.io diff --git a/docs/distutils/setupscript.rst b/docs/distutils/setupscript.rst index 3c8e1ab1b3b..71d2439f7ed 100644 --- a/docs/distutils/setupscript.rst +++ b/docs/distutils/setupscript.rst @@ -642,7 +642,7 @@ Notes: 'long string' Multiple lines of plain text in reStructuredText format (see - http://docutils.sourceforge.net/). + https://docutils.sourceforge.io/). 'list of strings' See below. From ff32ae0b43340341719b6b1b0ff15b7598a8644f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 16:57:08 -0500 Subject: [PATCH 009/232] Copy 'missing_compiler_executable from Python 3.12 and customize it for compatibility with distutils. --- distutils/tests/__init__.py | 32 ++++++++++++++++++++++++++++++ distutils/tests/test_build_clib.py | 4 +--- distutils/tests/test_build_ext.py | 5 +++-- distutils/tests/test_config_cmd.py | 3 +-- distutils/tests/test_install.py | 5 ++--- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 27e73393a0a..fdec5a96504 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -6,3 +6,35 @@ distutils.command.tests package, since command identification is done by import rather than matching pre-defined names. """ + +def missing_compiler_executable(cmd_names=[]): + """Check if the compiler components used to build the interpreter exist. + + Check for the existence of the compiler executables whose names are listed + in 'cmd_names' or all the compiler executables when 'cmd_names' is empty + and return the first missing executable or None when none is found + missing. + + """ + from distutils import ccompiler, sysconfig, spawn + from distutils import errors + + compiler = ccompiler.new_compiler() + sysconfig.customize_compiler(compiler) + if compiler.compiler_type == "msvc": + # MSVC has no executables, so check whether initialization succeeds + try: + compiler.initialize() + except errors.PlatformError: + return "msvc" + for name in compiler.executables: + if cmd_names and name not in cmd_names: + continue + cmd = getattr(compiler, name) + if cmd_names: + assert cmd is not None, \ + "the '%s' executable is not configured" % name + elif not cmd: + continue + if spawn.find_executable(cmd[0]) is None: + return cmd[0] diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index b5a392a85f1..98ab0b171fc 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -1,13 +1,11 @@ """Tests for distutils.command.build_clib.""" import os -from test.support import missing_compiler_executable - import pytest from distutils.command.build_clib import build_clib from distutils.errors import DistutilsSetupError -from distutils.tests import support +from distutils.tests import support, missing_compiler_executable class TestBuildCLib(support.TempdirManager): diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index cb61ad74552..3c83cca4d27 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -16,6 +16,7 @@ from distutils.core import Distribution from distutils.command.build_ext import build_ext from distutils import sysconfig +from distutils.tests import missing_compiler_executable from distutils.tests.support import ( TempdirManager, copy_xxmodule_c, @@ -89,7 +90,7 @@ def build_ext(self, *args, **kwargs): return build_ext(*args, **kwargs) def test_build_ext(self): - cmd = support.missing_compiler_executable() + missing_compiler_executable() copy_xxmodule_c(self.tmp_dir) xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') xx_ext = Extension('xx', [xx_c]) @@ -359,7 +360,7 @@ def test_compiler_option(self): assert cmd.compiler == 'unix' def test_get_outputs(self): - cmd = support.missing_compiler_executable() + missing_compiler_executable() tmp_dir = self.mkdtemp() c_file = os.path.join(tmp_dir, 'foo.c') self.write_file(c_file, 'void PyInit_foo(void) {}\n') diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index e72a7c5ff8d..ecb8510246a 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -1,12 +1,11 @@ """Tests for distutils.command.config.""" import os import sys -from test.support import missing_compiler_executable import pytest from distutils.command.config import dump_file, config -from distutils.tests import support +from distutils.tests import support, missing_compiler_executable from distutils._log import log diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 3f525db42a6..082ee1d3491 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -17,8 +17,7 @@ from distutils.errors import DistutilsOptionError from distutils.extension import Extension -from distutils.tests import support -from test import support as test_support +from distutils.tests import support, missing_compiler_executable def _make_ext_name(modname): @@ -213,7 +212,7 @@ def test_record(self): assert found == expected def test_record_extensions(self): - cmd = test_support.missing_compiler_executable() + cmd = missing_compiler_executable() if cmd is not None: pytest.skip('The %r command is not found' % cmd) install_dir = self.mkdtemp() From 5b6638da22121aa215fa5b762379ff4a4d98d09a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:09:59 -0500 Subject: [PATCH 010/232] Remove build and dist from excludes. It appears they are not needed and their presence blocks the names of packages like 'builder' and 'distutils'. Ref pypa/distutils#224. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 574ffc28e64..68c38ac901c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,6 @@ install_requires = [options.packages.find] exclude = - build* - dist* docs* tests* From 0148d7dcd08077e5fb849edc9b8235240a6e6771 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:21:58 -0500 Subject: [PATCH 011/232] Mark this function as uncovered. --- distutils/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index fdec5a96504..85293cbb5bf 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -7,7 +7,7 @@ by import rather than matching pre-defined names. """ -def missing_compiler_executable(cmd_names=[]): +def missing_compiler_executable(cmd_names=[]): # pragma: no cover """Check if the compiler components used to build the interpreter exist. Check for the existence of the compiler executables whose names are listed From 107eff1920a39ab46be57bced32fb1eb23aa5797 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:27:59 -0500 Subject: [PATCH 012/232] Also disable the check --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b9cc6927be..213558aac40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -159,7 +159,8 @@ jobs: needs: - test - collateral - - test_cygwin + # disabled due to disabled job + # - test_cygwin runs-on: ubuntu-latest From c5a16ac3f66c1281354e9d23556905417250c019 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 21:00:22 -0500 Subject: [PATCH 013/232] Remove pin on inflect as it's insufficient to avoid the Rust dependency. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index ff2aade085b..68c38ac901c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,9 +45,6 @@ testing = docutils pyfakefs more_itertools - # workaround for lack of Rust support: pypa/setuptools#3921 - inflect<6.0.0; sys.platform=="cygwin" - docs = # upstream From 178d254379ed260eb537f48722703f819eaa8235 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Feb 2024 16:02:29 -0500 Subject: [PATCH 014/232] Remove Sphinx pin. Ref sphinx-doc/sphinx#11662. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index fe99eaf6e5e..400a72a5ed4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ testing = docs = # upstream sphinx >= 3.5 - # workaround for sphinx/sphinx-doc#11662 - sphinx < 7.2.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo From d9b441939046e965b1bfb8035f907be56c0836fc Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 12 Dec 2023 00:13:38 +0000 Subject: [PATCH 015/232] Fixes pypa/distutils#219 Use sysconfig.get_config_h_filename() to locate pyconfig.h --- distutils/sysconfig.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index a40a7231b30..c89fff4be19 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -195,12 +195,11 @@ def _get_python_inc_posix_prefix(prefix): def _get_python_inc_nt(prefix, spec_prefix, plat_specific): if python_build: - # Include both the include and PC dir to ensure we can find - # pyconfig.h + # Include both include dirs to ensure we can find pyconfig.h return ( os.path.join(prefix, "include") + os.path.pathsep - + os.path.join(prefix, "PC") + + os.path.dirname(sysconfig.get_config_h_filename()) ) return os.path.join(prefix, "include") From d2ddf06d4afd255ae992b4ebfdc3d18e50206152 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 18 Dec 2023 17:46:10 +0000 Subject: [PATCH 016/232] Also use sysconfig.get_config_h_filename() to implement distutils.sysconfig version --- distutils/sysconfig.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index c89fff4be19..fac3259f888 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -360,14 +360,7 @@ def customize_compiler(compiler): # noqa: C901 def get_config_h_filename(): """Return full pathname of installed pyconfig.h file.""" - if python_build: - if os.name == "nt": - inc_dir = os.path.join(_sys_home or project_base, "PC") - else: - inc_dir = _sys_home or project_base - return os.path.join(inc_dir, 'pyconfig.h') - else: - return sysconfig.get_config_h_filename() + return sysconfig.get_config_h_filename() def get_makefile_filename(): From 7f70d7d3173f744cdbf37fdb353492bbe7ae089a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Feb 2024 16:39:11 -0500 Subject: [PATCH 017/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran `ruff --format` on the code. --- conftest.py | 8 +-- distutils/bcppcompiler.py | 1 - distutils/ccompiler.py | 6 +- distutils/command/_framework_compat.py | 1 - distutils/command/bdist.py | 20 +++--- distutils/command/bdist_rpm.py | 84 +++++++++++-------------- distutils/command/build_py.py | 6 +- distutils/command/check.py | 10 ++- distutils/command/install.py | 35 ++++++----- distutils/command/register.py | 16 ++--- distutils/config.py | 1 + distutils/cygwinccompiler.py | 2 +- distutils/extension.py | 2 +- distutils/tests/__init__.py | 4 +- distutils/tests/support.py | 1 + distutils/tests/test_archive_util.py | 1 + distutils/tests/test_bdist.py | 1 + distutils/tests/test_bdist_dumb.py | 18 +++--- distutils/tests/test_bdist_rpm.py | 36 +++++------ distutils/tests/test_build.py | 1 + distutils/tests/test_build_clib.py | 1 + distutils/tests/test_build_ext.py | 4 +- distutils/tests/test_build_py.py | 12 ++-- distutils/tests/test_check.py | 1 + distutils/tests/test_clean.py | 1 + distutils/tests/test_cmd.py | 1 + distutils/tests/test_config.py | 1 + distutils/tests/test_config_cmd.py | 1 + distutils/tests/test_cygwinccompiler.py | 1 + distutils/tests/test_dir_util.py | 1 + distutils/tests/test_dist.py | 31 ++++----- distutils/tests/test_extension.py | 1 + distutils/tests/test_file_util.py | 1 + distutils/tests/test_filelist.py | 1 + distutils/tests/test_install_data.py | 1 + distutils/tests/test_install_headers.py | 1 + distutils/tests/test_install_lib.py | 1 + distutils/tests/test_modified.py | 1 + distutils/tests/test_msvc9compiler.py | 1 + distutils/tests/test_msvccompiler.py | 1 + distutils/tests/test_register.py | 1 + distutils/tests/test_sdist.py | 1 + distutils/tests/test_spawn.py | 1 + distutils/tests/test_sysconfig.py | 1 + distutils/tests/test_text_file.py | 1 + distutils/tests/test_unixccompiler.py | 1 + distutils/tests/test_upload.py | 1 + distutils/tests/test_util.py | 13 +++- distutils/tests/test_version.py | 1 + distutils/version.py | 2 - distutils/versionpredicate.py | 4 +- 51 files changed, 181 insertions(+), 164 deletions(-) diff --git a/conftest.py b/conftest.py index b01b313085a..ca808a6ab70 100644 --- a/conftest.py +++ b/conftest.py @@ -12,11 +12,9 @@ if platform.system() != 'Windows': - collect_ignore.extend( - [ - 'distutils/msvc9compiler.py', - ] - ) + collect_ignore.extend([ + 'distutils/msvc9compiler.py', + ]) @pytest.fixture diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index 3c2ba15410d..14d51472f29 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -11,7 +11,6 @@ # someone should sit down and factor out the common code as # WindowsCCompiler! --GPW - import os import warnings diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index c1c7d5476eb..6935e2c37fb 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -1004,7 +1004,11 @@ def executable_filename(self, basename, strip_dir=0, output_dir=''): return os.path.join(output_dir, basename + (self.exe_extension or '')) def library_filename( - self, libname, lib_type='static', strip_dir=0, output_dir='' # or 'shared' + self, + libname, + lib_type='static', + strip_dir=0, + output_dir='', # or 'shared' ): assert output_dir is not None expected = '"static", "shared", "dylib", "xcode_stub"' diff --git a/distutils/command/_framework_compat.py b/distutils/command/_framework_compat.py index cffa27cb082..b4228299f4f 100644 --- a/distutils/command/_framework_compat.py +++ b/distutils/command/_framework_compat.py @@ -2,7 +2,6 @@ Backward compatibility for homebrew builds on macOS. """ - import sys import os import functools diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py index 6329039ce46..237b14656f8 100644 --- a/distutils/command/bdist.py +++ b/distutils/command/bdist.py @@ -76,17 +76,15 @@ class bdist(Command): default_format = {'posix': 'gztar', 'nt': 'zip'} # Define commands in preferred order for the --help-formats option - format_commands = ListCompat( - { - 'rpm': ('bdist_rpm', "RPM distribution"), - 'gztar': ('bdist_dumb', "gzip'ed tar file"), - 'bztar': ('bdist_dumb', "bzip2'ed tar file"), - 'xztar': ('bdist_dumb', "xz'ed tar file"), - 'ztar': ('bdist_dumb', "compressed tar file"), - 'tar': ('bdist_dumb', "tar file"), - 'zip': ('bdist_dumb', "ZIP file"), - } - ) + format_commands = ListCompat({ + 'rpm': ('bdist_rpm', "RPM distribution"), + 'gztar': ('bdist_dumb', "gzip'ed tar file"), + 'bztar': ('bdist_dumb', "bzip2'ed tar file"), + 'xztar': ('bdist_dumb', "xz'ed tar file"), + 'ztar': ('bdist_dumb', "compressed tar file"), + 'tar': ('bdist_dumb', "tar file"), + 'zip': ('bdist_dumb', "ZIP file"), + }) # for compatibility until consumers only reference format_commands format_command = format_commands diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 696f26751fd..e96db22bed7 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -401,9 +401,11 @@ def run(self): # noqa: C901 if os.path.exists(rpm): self.move_file(rpm, self.dist_dir) filename = os.path.join(self.dist_dir, os.path.basename(rpm)) - self.distribution.dist_files.append( - ('bdist_rpm', pyversion, filename) - ) + self.distribution.dist_files.append(( + 'bdist_rpm', + pyversion, + filename, + )) def _dist_path(self, path): return os.path.join(self.dist_dir, os.path.basename(path)) @@ -428,9 +430,9 @@ def _make_spec_file(self): # noqa: C901 # Generate a potential replacement value for __os_install_post (whilst # normalizing the whitespace to simplify the test for whether the # invocation of brp-python-bytecompile passes in __python): - vendor_hook = '\n'.join( - [' %s \\' % line.strip() for line in vendor_hook.splitlines()] - ) + vendor_hook = '\n'.join([ + ' %s \\' % line.strip() for line in vendor_hook.splitlines() + ]) problem = "brp-python-bytecompile \\\n" fixed = "brp-python-bytecompile %{__python} \\\n" fixed_hook = vendor_hook.replace(problem, fixed) @@ -445,13 +447,11 @@ def _make_spec_file(self): # noqa: C901 # spec_file.append('Summary(%s): %s' % (locale, # self.summaries[locale])) - spec_file.extend( - [ - 'Name: %{name}', - 'Version: %{version}', - 'Release: %{release}', - ] - ) + spec_file.extend([ + 'Name: %{name}', + 'Version: %{version}', + 'Release: %{release}', + ]) # XXX yuck! this filename is available from the "sdist" command, # but only after it has run: and we create the spec file before @@ -461,14 +461,12 @@ def _make_spec_file(self): # noqa: C901 else: spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz') - spec_file.extend( - [ - 'License: ' + (self.distribution.get_license() or "UNKNOWN"), - 'Group: ' + self.group, - 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', - 'Prefix: %{_prefix}', - ] - ) + spec_file.extend([ + 'License: ' + (self.distribution.get_license() or "UNKNOWN"), + 'Group: ' + self.group, + 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', + 'Prefix: %{_prefix}', + ]) if not self.force_arch: # noarch if no extension modules @@ -506,13 +504,11 @@ def _make_spec_file(self): # noqa: C901 if self.no_autoreq: spec_file.append('AutoReq: 0') - spec_file.extend( - [ - '', - '%description', - self.distribution.get_long_description() or "", - ] - ) + spec_file.extend([ + '', + '%description', + self.distribution.get_long_description() or "", + ]) # put locale descriptions into spec file # XXX again, suppressed because config file syntax doesn't @@ -558,12 +554,10 @@ def _make_spec_file(self): # noqa: C901 # use 'default' as contents of script val = getattr(self, attr) if val or default: - spec_file.extend( - [ - '', - '%' + rpm_opt, - ] - ) + spec_file.extend([ + '', + '%' + rpm_opt, + ]) if val: with open(val) as f: spec_file.extend(f.read().split('\n')) @@ -571,24 +565,20 @@ def _make_spec_file(self): # noqa: C901 spec_file.append(default) # files section - spec_file.extend( - [ - '', - '%files -f INSTALLED_FILES', - '%defattr(-,root,root)', - ] - ) + spec_file.extend([ + '', + '%files -f INSTALLED_FILES', + '%defattr(-,root,root)', + ]) if self.doc_files: spec_file.append('%doc ' + ' '.join(self.doc_files)) if self.changelog: - spec_file.extend( - [ - '', - '%changelog', - ] - ) + spec_file.extend([ + '', + '%changelog', + ]) spec_file.extend(self.changelog) return spec_file diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py index d9df95922f3..e16011d46a9 100644 --- a/distutils/command/build_py.py +++ b/distutils/command/build_py.py @@ -129,9 +129,9 @@ def find_data_files(self, package, src_dir): os.path.join(glob.escape(src_dir), convert_path(pattern)) ) # Files that match more than one pattern are only added once - files.extend( - [fn for fn in filelist if fn not in files and os.path.isfile(fn)] - ) + files.extend([ + fn for fn in filelist if fn not in files and os.path.isfile(fn) + ]) return files def build_package_data(self): diff --git a/distutils/command/check.py b/distutils/command/check.py index 575e49fb4b1..b59cc237312 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -2,6 +2,7 @@ Implements the Distutils 'check' command. """ + import contextlib from ..core import Command @@ -144,8 +145,11 @@ def _check_rst_data(self, data): try: parser.parse(data, document) except AttributeError as e: - reporter.messages.append( - (-1, 'Could not finish the parsing: %s.' % e, '', {}) - ) + reporter.messages.append(( + -1, + 'Could not finish the parsing: %s.' % e, + '', + {}, + )) return reporter.messages diff --git a/distutils/command/install.py b/distutils/command/install.py index a7ac4e6077b..927c3ed3a29 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -245,9 +245,11 @@ class install(Command): boolean_options = ['compile', 'force', 'skip-build'] if HAS_USER_SITE: - user_options.append( - ('user', None, "install in user site-package '%s'" % USER_SITE) - ) + user_options.append(( + 'user', + None, + "install in user site-package '%s'" % USER_SITE, + )) boolean_options.append('user') negative_opt = {'no-compile': 'compile'} @@ -432,9 +434,12 @@ def finalize_options(self): # noqa: C901 local_vars['userbase'] = self.install_userbase local_vars['usersite'] = self.install_usersite - self.config_vars = _collections.DictStack( - [fw.vars(), compat_vars, sysconfig.get_config_vars(), local_vars] - ) + self.config_vars = _collections.DictStack([ + fw.vars(), + compat_vars, + sysconfig.get_config_vars(), + local_vars, + ]) self.expand_basedirs() @@ -620,16 +625,14 @@ def expand_basedirs(self): def expand_dirs(self): """Calls `os.path.expanduser` on install dirs.""" - self._expand_attrs( - [ - 'install_purelib', - 'install_platlib', - 'install_lib', - 'install_headers', - 'install_scripts', - 'install_data', - ] - ) + self._expand_attrs([ + 'install_purelib', + 'install_platlib', + 'install_lib', + 'install_headers', + 'install_scripts', + 'install_data', + ]) def convert_paths(self, *names): """Call `convert_path` over `names`.""" diff --git a/distutils/command/register.py b/distutils/command/register.py index c19aabb91ff..cf1afc8c1f1 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -77,7 +77,7 @@ def check_metadata(self): check.run() def _set_config(self): - '''Reads the configuration file and set attributes.''' + """Reads the configuration file and set attributes.""" config = self._read_pypirc() if config != {}: self.username = config['username'] @@ -93,19 +93,19 @@ def _set_config(self): self.has_config = False def classifiers(self): - '''Fetch the list of classifiers from the server.''' + """Fetch the list of classifiers from the server.""" url = self.repository + '?:action=list_classifiers' response = urllib.request.urlopen(url) log.info(self._read_pypi_response(response)) def verify_metadata(self): - '''Send the metadata to the package index server to be checked.''' + """Send the metadata to the package index server to be checked.""" # send the info to the server and report the result (code, result) = self.post_to_server(self.build_post_data('verify')) log.info('Server response (%s): %s', code, result) def send_metadata(self): # noqa: C901 - '''Send the metadata to the package index server. + """Send the metadata to the package index server. Well, do the following: 1. figure who the user is, and then @@ -131,7 +131,7 @@ def send_metadata(self): # noqa: C901 2. register as a new user, or 3. set the password to a random string and email the user. - ''' + """ # see if we can short-cut and get the username/password from the # config if self.has_config: @@ -146,13 +146,13 @@ def send_metadata(self): # noqa: C901 choices = '1 2 3 4'.split() while choice not in choices: self.announce( - '''\ + """\ We need to know who you are, so please choose either: 1. use your existing login, 2. register as a new user, 3. have the server generate a new password for you (and email it to you), or 4. quit -Your selection [default 1]: ''', +Your selection [default 1]: """, logging.INFO, ) choice = input() @@ -262,7 +262,7 @@ def build_post_data(self, action): return data def post_to_server(self, data, auth=None): # noqa: C901 - '''Post a query to the server, and return a string response.''' + """Post a query to the server, and return a string response.""" if 'name' in data: self.announce( 'Registering {} to {}'.format(data['name'], self.repository), diff --git a/distutils/config.py b/distutils/config.py index 9a4044adaf8..a55951ed7cf 100644 --- a/distutils/config.py +++ b/distutils/config.py @@ -3,6 +3,7 @@ Provides the PyPIRCCommand class, the base class for the command classes that uses .pypirc in the distutils.command package. """ + import os from configparser import RawConfigParser diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 47efa377c5b..b3dbc3be15c 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -344,7 +344,7 @@ def check_config_h(): def is_cygwincc(cc): - '''Try to determine if the compiler that would be used is from cygwin.''' + """Try to determine if the compiler that would be used is from cygwin.""" out_string = check_output(shlex.split(cc) + ['-dumpmachine']) return out_string.strip().endswith(b'cygwin') diff --git a/distutils/extension.py b/distutils/extension.py index 6b8575de294..8f186b72ffc 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -102,7 +102,7 @@ def __init__( depends=None, language=None, optional=None, - **kw # To catch unknown keywords + **kw, # To catch unknown keywords ): if not isinstance(name, str): raise AssertionError("'name' must be a string") diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 85293cbb5bf..aad8edb242d 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -7,6 +7,7 @@ by import rather than matching pre-defined names. """ + def missing_compiler_executable(cmd_names=[]): # pragma: no cover """Check if the compiler components used to build the interpreter exist. @@ -32,8 +33,7 @@ def missing_compiler_executable(cmd_names=[]): # pragma: no cover continue cmd = getattr(compiler, name) if cmd_names: - assert cmd is not None, \ - "the '%s' executable is not configured" % name + assert cmd is not None, "the '%s' executable is not configured" % name elif not cmd: continue if spawn.find_executable(cmd[0]) is None: diff --git a/distutils/tests/support.py b/distutils/tests/support.py index fd4b11bf750..2080604982d 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -1,4 +1,5 @@ """Support code for distutils test cases.""" + import os import sys import shutil diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 89c415d7616..2b5eafd27e6 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -1,4 +1,5 @@ """Tests for distutils.archive_util.""" + import os import sys import tarfile diff --git a/distutils/tests/test_bdist.py b/distutils/tests/test_bdist.py index af330a06e7d..18048077521 100644 --- a/distutils/tests/test_bdist.py +++ b/distutils/tests/test_bdist.py @@ -1,4 +1,5 @@ """Tests for distutils.command.bdist.""" + from distutils.command.bdist import bdist from distutils.tests import support diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index 6fb50c4b8ef..95532e83b95 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -38,16 +38,14 @@ def test_simple_built(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 3fd2c7e2ac1..e6804088da5 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -58,16 +58,14 @@ def test_quiet(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) @@ -103,16 +101,14 @@ def test_no_optimize_flag(self): self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') self.write_file((pkg_dir, 'README'), '') - dist = Distribution( - { - 'name': 'foo', - 'version': '0.1', - 'py_modules': ['foo'], - 'url': 'xxx', - 'author': 'xxx', - 'author_email': 'xxx', - } - ) + dist = Distribution({ + 'name': 'foo', + 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', + 'author': 'xxx', + 'author_email': 'xxx', + }) dist.script_name = 'setup.py' os.chdir(pkg_dir) diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index 66d8af50aca..c2cff445233 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -1,4 +1,5 @@ """Tests for distutils.command.build.""" + import os import sys diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index 98ab0b171fc..f8554542564 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -1,4 +1,5 @@ """Tests for distutils.command.build_clib.""" + import os import pytest diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 3c83cca4d27..537959fed66 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -501,7 +501,7 @@ def _try_compile_deployment_target(self, operator, target): with open(deptarget_c, 'w') as fp: fp.write( textwrap.dedent( - '''\ + """\ #include int dummy; @@ -511,7 +511,7 @@ def _try_compile_deployment_target(self, operator, target): #error "Unexpected target" #endif - ''' + """ % operator ) ) diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 3bef9d79ece..77c9ad75730 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -69,13 +69,11 @@ def test_empty_package_dir(self): open(os.path.join(testdir, "testfile"), "w").close() os.chdir(sources) - dist = Distribution( - { - "packages": ["pkg"], - "package_dir": {"pkg": ""}, - "package_data": {"pkg": ["doc/*"]}, - } - ) + dist = Distribution({ + "packages": ["pkg"], + "package_dir": {"pkg": ""}, + "package_data": {"pkg": ["doc/*"]}, + }) # script_name need not exist, it just need to be initialized dist.script_name = os.path.join(sources, "setup.py") dist.script_args = ["build"] diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index 6d240b8b2bb..8215300b976 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -1,4 +1,5 @@ """Tests for distutils.command.check.""" + import os import textwrap diff --git a/distutils/tests/test_clean.py b/distutils/tests/test_clean.py index 157b60a1e98..e2459aa0c1e 100644 --- a/distutils/tests/test_clean.py +++ b/distutils/tests/test_clean.py @@ -1,4 +1,5 @@ """Tests for distutils.command.clean.""" + import os from distutils.command.clean import clean diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py index cc740d1a8b1..684662d32e2 100644 --- a/distutils/tests/test_cmd.py +++ b/distutils/tests/test_cmd.py @@ -1,4 +1,5 @@ """Tests for distutils.cmd.""" + import os from distutils.cmd import Command diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 1ae615db955..11c23d837ed 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -1,4 +1,5 @@ """Tests for distutils.pypirc.pypirc.""" + import os import pytest diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index ecb8510246a..2519ed6a104 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -1,4 +1,5 @@ """Tests for distutils.command.config.""" + import os import sys diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 6fb449a6c20..fc67d75f829 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -1,4 +1,5 @@ """Tests for distutils.cygwinccompiler.""" + import sys import os diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 72aca4ee558..0738b7c8770 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -1,4 +1,5 @@ """Tests for distutils.dir_util.""" + import os import stat import unittest.mock as mock diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 694bf02a60c..fe979efed5b 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -1,4 +1,5 @@ """Tests for distutils.dist.""" + import os import io import email @@ -69,14 +70,12 @@ def test_command_packages_unspecified(self, clear_argv): def test_command_packages_cmdline(self, clear_argv): from distutils.tests.test_dist import test_dist - sys.argv.extend( - [ - "--command-packages", - "foo.bar,distutils.tests", - "test_dist", - "-Ssometext", - ] - ) + sys.argv.extend([ + "--command-packages", + "foo.bar,distutils.tests", + "test_dist", + "-Ssometext", + ]) d = self.create_distribution() # let's actually try to load our test command: assert d.get_command_packages() == [ @@ -98,9 +97,8 @@ def test_venv_install_options(self, tmp_path): fakepath = '/somedir' - jaraco.path.build( - { - file: f""" + jaraco.path.build({ + file: f""" [install] install-base = {fakepath} install-platbase = {fakepath} @@ -116,8 +114,7 @@ def test_venv_install_options(self, tmp_path): user = {fakepath} root = {fakepath} """, - } - ) + }) # Base case: Not in a Virtual Environment with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'): @@ -158,14 +155,12 @@ def test_venv_install_options(self, tmp_path): def test_command_packages_configfile(self, tmp_path, clear_argv): sys.argv.append("build") file = str(tmp_path / "file") - jaraco.path.build( - { - file: """ + jaraco.path.build({ + file: """ [global] command_packages = foo.bar, splat """, - } - ) + }) d = self.create_distribution([file]) assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"] diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index f86af07376f..297ae44bfee 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -1,4 +1,5 @@ """Tests for distutils.extension.""" + import os import warnings diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 9f44f91dfa8..3b9f82b71e9 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -1,4 +1,5 @@ """Tests for distutils.file_util.""" + import os import errno import unittest.mock as mock diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index 2cee42cddd1..bfffbb1da0f 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -1,4 +1,5 @@ """Tests for distutils.filelist.""" + import os import re import logging diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py index 9badbc264f8..198c10da8d9 100644 --- a/distutils/tests/test_install_data.py +++ b/distutils/tests/test_install_data.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_data.""" + import os import pytest diff --git a/distutils/tests/test_install_headers.py b/distutils/tests/test_install_headers.py index 1e8ccf7991c..8b86b6eaed6 100644 --- a/distutils/tests/test_install_headers.py +++ b/distutils/tests/test_install_headers.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_headers.""" + import os import pytest diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py index 0bd67cd04d8..0efe39fe86d 100644 --- a/distutils/tests/test_install_lib.py +++ b/distutils/tests/test_install_lib.py @@ -1,4 +1,5 @@ """Tests for distutils.command.install_data.""" + import sys import os import importlib.util diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index ca07c7e853f..5fde7a5971d 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -1,4 +1,5 @@ """Tests for distutils._modified.""" + import os import types diff --git a/distutils/tests/test_msvc9compiler.py b/distutils/tests/test_msvc9compiler.py index fe5693e1d86..dfb34122bcb 100644 --- a/distutils/tests/test_msvc9compiler.py +++ b/distutils/tests/test_msvc9compiler.py @@ -1,4 +1,5 @@ """Tests for distutils.msvc9compiler.""" + import sys import os diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index f63537b8e5b..f65a5a25a3c 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -1,4 +1,5 @@ """Tests for distutils._msvccompiler.""" + import sys import os import threading diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 34e593244e0..5d3826a1b7e 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -1,4 +1,5 @@ """Tests for distutils.command.register.""" + import os import getpass import urllib diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index a3fa2902750..00718a37bdc 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,4 +1,5 @@ """Tests for distutils.command.sdist.""" + import os import tarfile import warnings diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index 08a34ee2b8e..57cf1a525ca 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -1,4 +1,5 @@ """Tests for distutils.spawn.""" + import os import stat import sys diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index bfeaf9a6b93..6cbf51681bd 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -1,4 +1,5 @@ """Tests for distutils.sysconfig.""" + import contextlib import os import subprocess diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py index 7c8dc5be545..4a721b691c0 100644 --- a/distutils/tests/test_text_file.py +++ b/distutils/tests/test_text_file.py @@ -1,4 +1,5 @@ """Tests for distutils.text_file.""" + import os from distutils.text_file import TextFile from distutils.tests import support diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index a0184424595..c1e57a016f3 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -1,4 +1,5 @@ """Tests for distutils.unixccompiler.""" + import os import sys import unittest.mock as mock diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index af113b8b6ea..5c5bc59a40e 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -1,4 +1,5 @@ """Tests for distutils.command.upload.""" + import os import unittest.mock as mock from urllib.request import HTTPError diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 22a003d8ca0..c632b3910f1 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -1,4 +1,5 @@ """Tests for distutils.util.""" + import email import email.policy import email.generator @@ -155,9 +156,15 @@ def test_check_environ_getpwuid(self): import pwd # only set pw_dir field, other fields are not used - result = pwd.struct_passwd( - (None, None, None, None, None, '/home/distutils', None) - ) + result = pwd.struct_passwd(( + None, + None, + None, + None, + None, + '/home/distutils', + None, + )) with mock.patch.object(pwd, 'getpwuid', return_value=result): check_environ() assert os.environ['HOME'] == '/home/distutils' diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index ff52ea46830..900edafa7c8 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -1,4 +1,5 @@ """Tests for distutils.version.""" + import pytest import distutils diff --git a/distutils/version.py b/distutils/version.py index 74c40d7bfd5..18385cfef2d 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -111,7 +111,6 @@ def __ge__(self, other): class StrictVersion(Version): - """Version numbering for anal retentives and software idealists. Implements the standard interface for version number classes as described above. A version number consists of two or three @@ -286,7 +285,6 @@ def _cmp(self, other): # noqa: C901 class LooseVersion(Version): - """Version numbering for anarchists and software realists. Implements the standard interface for version number classes as described above. A version number consists of a series of numbers, diff --git a/distutils/versionpredicate.py b/distutils/versionpredicate.py index d6c0c007aad..c75e49486f3 100644 --- a/distutils/versionpredicate.py +++ b/distutils/versionpredicate.py @@ -1,5 +1,5 @@ -"""Module for parsing and testing package version predicate strings. -""" +"""Module for parsing and testing package version predicate strings.""" + import re from . import version import operator From a55a44168cfedfb4f52ad3aa93728d91ca218880 Mon Sep 17 00:00:00 2001 From: Steven Pitman Date: Mon, 2 Oct 2023 11:10:34 -0400 Subject: [PATCH 018/232] Add support for z/OS compilers; Fixes pypa/distutils#215 --- distutils/ccompiler.py | 2 + distutils/command/build_ext.py | 11 +- distutils/zosccompiler.py | 228 +++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 distutils/zosccompiler.py diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 6935e2c37fb..d5ca761f5a3 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -1060,6 +1060,7 @@ def mkpath(self, name, mode=0o777): # on a cygwin built python we can use gcc like an ordinary UNIXish # compiler ('cygwin.*', 'unix'), + ('zos', 'zos'), # OS name mappings ('posix', 'unix'), ('nt', 'msvc'), @@ -1107,6 +1108,7 @@ def get_default_compiler(osname=None, platform=None): "Mingw32 port of GNU C Compiler for Win32", ), 'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"), + 'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'), } diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index b48f4626268..98938babd08 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -236,8 +236,15 @@ def finalize_options(self): # noqa: C901 # See Issues: #1600860, #4366 if sysconfig.get_config_var('Py_ENABLE_SHARED'): if not sysconfig.python_build: - # building third party extensions - self.library_dirs.append(sysconfig.get_config_var('LIBDIR')) + if sys.platform == 'zos': + # On z/OS, a user is not required to install Python to + # a predetermined path, but can use Python portably + installed_dir = sysconfig.get_config_var('base') + lib_dir = sysconfig.get_config_var('platlibdir') + self.library_dirs.append(os.path.join(installed_dir, lib_dir)) + else: + # building third party extensions + self.library_dirs.append(sysconfig.get_config_var('LIBDIR')) else: # building python standard extensions self.library_dirs.append('.') diff --git a/distutils/zosccompiler.py b/distutils/zosccompiler.py new file mode 100644 index 00000000000..6d70b7f04f1 --- /dev/null +++ b/distutils/zosccompiler.py @@ -0,0 +1,228 @@ +"""distutils.zosccompiler + +Contains the selection of the c & c++ compilers on z/OS. There are several +different c compilers on z/OS, all of them are optional, so the correct +one needs to be chosen based on the users input. This is compatible with +the following compilers: + +IBM C/C++ For Open Enterprise Languages on z/OS 2.0 +IBM Open XL C/C++ 1.1 for z/OS +IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5 +IBM z/OS XL C/C++ +""" + +import os +from .unixccompiler import UnixCCompiler +from . import sysconfig +from .errors import DistutilsExecError, CompileError + +_cc_args = { + 'ibm-openxl': [ + '-m64', + '-fvisibility=default', + '-fzos-le-char-mode=ascii', + '-fno-short-enums', + ], + 'ibm-xlclang': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + ], + 'ibm-xlc': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + '-qlanglvl=extc99', + ], +} + +_cxx_args = { + 'ibm-openxl': [ + '-m64', + '-fvisibility=default', + '-fzos-le-char-mode=ascii', + '-fno-short-enums', + ], + 'ibm-xlclang': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + ], + 'ibm-xlc': [ + '-q64', + '-qexportall', + '-qascii', + '-qstrict', + '-qnocsect', + '-Wa,asa,goff', + '-Wa,xplink', + '-qgonumber', + '-qenum=int', + '-Wc,DLL', + '-qlanglvl=extended0x', + ], +} + +_asm_args = { + 'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'], + 'ibm-xlclang': [], + 'ibm-xlc': [], +} + +_ld_args = { + 'ibm-openxl': [], + 'ibm-xlclang': ['-Wl,dll', '-q64'], + 'ibm-xlc': ['-Wl,dll', '-q64'], +} + + +# Python on z/OS is built with no compiler specific options in it's CFLAGS. +# But each compiler requires it's own specific options to build successfully, +# though some of the options are common between them +class zOSCCompiler(UnixCCompiler): + src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s'] + _cpp_extensions = ['.cc', '.cpp', '.cxx', '.C'] + _asm_extensions = ['.s'] + + def _get_zos_compiler_name(self): + zos_compiler_names = [ + os.path.basename(binary) + for envvar in ('CC', 'CXX', 'LDSHARED') + if (binary := os.environ.get(envvar, None)) + ] + if len(zos_compiler_names) == 0: + return 'ibm-openxl' + + zos_compilers = {} + for compiler in ( + 'ibm-clang', + 'ibm-clang64', + 'ibm-clang++', + 'ibm-clang++64', + 'clang', + 'clang++', + 'clang-14', + ): + zos_compilers[compiler] = 'ibm-openxl' + + for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'): + zos_compilers[compiler] = 'ibm-xlclang' + + for compiler in ('xlc', 'xlC', 'xlc++'): + zos_compilers[compiler] = 'ibm-xlc' + + return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl') + + def __init__(self, verbose=0, dry_run=0, force=0): + super().__init__(verbose, dry_run, force) + self.zos_compiler = self._get_zos_compiler_name() + sysconfig.customize_compiler(self) + + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): + local_args = [] + if ext in self._cpp_extensions: + compiler = self.compiler_cxx + local_args.extend(_cxx_args[self.zos_compiler]) + elif ext in self._asm_extensions: + compiler = self.compiler_so + local_args.extend(_cc_args[self.zos_compiler]) + local_args.extend(_asm_args[self.zos_compiler]) + else: + compiler = self.compiler_so + local_args.extend(_cc_args[self.zos_compiler]) + local_args.extend(cc_args) + + try: + self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs) + except DistutilsExecError as msg: + raise CompileError(msg) + + def runtime_library_dir_option(self, dir): + return '-L' + dir + + def link( + self, + target_desc, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, + ): + # For a built module to use functions from cpython, it needs to use Pythons + # side deck file. The side deck is located beside the libpython3.xx.so + ldversion = sysconfig.get_config_var('LDVERSION') + if sysconfig.python_build: + side_deck_path = os.path.join( + sysconfig.get_config_var('abs_builddir'), + f'libpython{ldversion}.x', + ) + else: + side_deck_path = os.path.join( + sysconfig.get_config_var('installed_base'), + sysconfig.get_config_var('platlibdir'), + f'libpython{ldversion}.x', + ) + + if os.path.exists(side_deck_path): + if extra_postargs: + extra_postargs.append(side_deck_path) + else: + extra_postargs = [side_deck_path] + + # Check and replace libraries included side deck files + if runtime_library_dirs: + for dir in runtime_library_dirs: + for library in libraries[:]: + library_side_deck = os.path.join(dir, f'{library}.x') + if os.path.exists(library_side_deck): + libraries.remove(library) + extra_postargs.append(library_side_deck) + break + + # Any required ld args for the given compiler + extra_postargs.extend(_ld_args[self.zos_compiler]) + + super().link( + target_desc, + objects, + output_filename, + output_dir, + libraries, + library_dirs, + runtime_library_dirs, + export_symbols, + debug, + extra_preargs, + extra_postargs, + build_temp, + target_lang, + ) From 88eb8cc66f8762e37ec78913c07ccf3e3dba05e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Oct 2023 14:16:55 -0400 Subject: [PATCH 019/232] Extracted method for resolving python lib dir. --- distutils/command/build_ext.py | 43 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index 98938babd08..ba6580c71ee 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -130,6 +130,31 @@ def initialize_options(self): self.user = None self.parallel = None + @staticmethod + def _python_lib_dir(sysconfig): + """ + Resolve Python's library directory for building extensions + that rely on a shared Python library. + + See python/cpython#44264 and python/cpython#48686 + """ + if not sysconfig.get_config_var('Py_ENABLE_SHARED'): + return + + if sysconfig.python_build: + yield '.' + return + + if sys.platform == 'zos': + # On z/OS, a user is not required to install Python to + # a predetermined path, but can use Python portably + installed_dir = sysconfig.get_config_var('base') + lib_dir = sysconfig.get_config_var('platlibdir') + yield os.path.join(installed_dir, lib_dir) + else: + # building third party extensions + yield sysconfig.get_config_var('LIBDIR') + def finalize_options(self): # noqa: C901 from distutils import sysconfig @@ -231,23 +256,7 @@ def finalize_options(self): # noqa: C901 # building python standard extensions self.library_dirs.append('.') - # For building extensions with a shared Python library, - # Python's library directory must be appended to library_dirs - # See Issues: #1600860, #4366 - if sysconfig.get_config_var('Py_ENABLE_SHARED'): - if not sysconfig.python_build: - if sys.platform == 'zos': - # On z/OS, a user is not required to install Python to - # a predetermined path, but can use Python portably - installed_dir = sysconfig.get_config_var('base') - lib_dir = sysconfig.get_config_var('platlibdir') - self.library_dirs.append(os.path.join(installed_dir, lib_dir)) - else: - # building third party extensions - self.library_dirs.append(sysconfig.get_config_var('LIBDIR')) - else: - # building python standard extensions - self.library_dirs.append('.') + self.library_dirs.extend(self._python_lib_dir(sysconfig)) # The argument parsing will result in self.define being a string, but # it has to be a list of 2-tuples. All the preprocessor symbols From 0136c373d4be1a7cfee4683d77d659a7a5dff832 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 13 Feb 2024 11:01:51 -0500 Subject: [PATCH 020/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_unixccompiler.py | 2 +- distutils/unixccompiler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index e8c34ce63e6..62efce436f2 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -189,7 +189,7 @@ def gcv(v): sysconfig.get_config_var = gcv assert self.cc.rpath_foo() == [ '-Wl,--enable-new-dtags', - '-Wl,-rpath,/foo' + '-Wl,-rpath,/foo', ] # non-GCC GNULD diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index b676a6a8af6..d749fe25291 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -316,7 +316,7 @@ def runtime_library_dir_option(self, dir): return [ # Force RUNPATH instead of RPATH "-Wl,--enable-new-dtags", - "-Wl,-rpath," + dir + "-Wl,-rpath," + dir, ] else: return "-Wl,-R" + dir From 91cb3279ec9c17d00c5d8b823aa8f3b65bd9f76e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 13 Feb 2024 13:15:51 -0500 Subject: [PATCH 021/232] Update more tests to match the new expectation. --- distutils/tests/test_unixccompiler.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 62efce436f2..a313da3e75a 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -153,7 +153,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == [ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ] def gcv(v): if v == 'CC': @@ -162,7 +165,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == [ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ] # GCC non-GNULD sys.platform = 'bar' @@ -202,7 +208,10 @@ def gcv(v): return 'yes' sysconfig.get_config_var = gcv - assert self.cc.rpath_foo() == '-Wl,--enable-new-dtags,-R/foo' + assert self.cc.rpath_foo() == [ + '-Wl,--enable-new-dtags', + '-Wl,-rpath,/foo', + ] # non-GCC non-GNULD sys.platform = 'bar' From 0f23a0e35f960ffe5da7f52a36e5080e0cb6aa9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 13 Feb 2024 11:20:20 -0500 Subject: [PATCH 022/232] Rely on always_iterable to conditionally extend the lib_opts. --- distutils/_itertools.py | 52 +++++++++++++++++++++++++++++++++++++++++ distutils/ccompiler.py | 7 ++---- 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 distutils/_itertools.py diff --git a/distutils/_itertools.py b/distutils/_itertools.py new file mode 100644 index 00000000000..85b29511861 --- /dev/null +++ b/distutils/_itertools.py @@ -0,0 +1,52 @@ +# from more_itertools 10.2 +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index d5ca761f5a3..28d2da5c58e 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -21,6 +21,7 @@ from ._modified import newer_group from .util import split_quoted, execute from ._log import log +from ._itertools import always_iterable class CCompiler: @@ -1233,11 +1234,7 @@ def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries): lib_opts.append(compiler.library_dir_option(dir)) for dir in runtime_library_dirs: - opt = compiler.runtime_library_dir_option(dir) - if isinstance(opt, list): - lib_opts = lib_opts + opt - else: - lib_opts.append(opt) + lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir))) # XXX it's important that we *not* remove redundant library mentions! # sometimes you really do have to say "-lfoo -lbar -lfoo" in order to From dcd70baa3bdeba64d2072dc06cc50e52501de7aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 13 Feb 2024 17:30:38 -0500 Subject: [PATCH 023/232] Restore integration test with Setuptools --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45c66794f03..473c2e0fccd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,7 +117,6 @@ jobs: ci_setuptools: # Integration testing with setuptools - if: ${{ false }} # disabled for deprecation warnings strategy: matrix: python: From 779219ce3ecbf4477da062658a1d0b2d5bf4f77f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Feb 2024 10:38:06 -0500 Subject: [PATCH 024/232] Include deps from the base config in diffcov. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 331eeed93f6..4c39a5b139c 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ extras = [testenv:diffcov] description = run tests and check that diff from main is covered deps = + {[testenv]deps} diff-cover commands = pytest {posargs} --cov-report xml From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 025/232] Enable complexity check and pycodestyle warnings. Closes jaraco/skeleton#110. --- ruff.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruff.toml b/ruff.toml index e61ca8b0d62..6c5b00092e2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,8 @@ [lint] +select = [ + "C901", + "W", +] ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", From 853d0f5feffb01abc3f190c55f48e76ae8a4d24c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 09:52:04 -0500 Subject: [PATCH 026/232] Extract a method for customizing the compiler for macOS. --- distutils/sysconfig.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index fac3259f888..a88fd021df5 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -266,6 +266,27 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): ) +def _customize_macos(): + if sys.platform != "darwin": + return + + # Perform first-time customization of compiler-related + # config vars on OS X now that we know we need a compiler. + # This is primarily to support Pythons from binary + # installers. The kind and paths to build tools on + # the user system may vary significantly from the system + # that Python itself was built on. Also the user OS + # version and build tools may not support the same set + # of CPU architectures for universal builds. + global _config_vars + # Use get_config_var() to ensure _config_vars is initialized. + if not get_config_var('CUSTOMIZED_OSX_COMPILER'): + import _osx_support + + _osx_support.customize_compiler(_config_vars) + _config_vars['CUSTOMIZED_OSX_COMPILER'] = 'True' + + def customize_compiler(compiler): # noqa: C901 """Do any platform-specific customization of a CCompiler instance. @@ -273,22 +294,7 @@ def customize_compiler(compiler): # noqa: C901 varies across Unices and is stored in Python's Makefile. """ if compiler.compiler_type == "unix": - if sys.platform == "darwin": - # Perform first-time customization of compiler-related - # config vars on OS X now that we know we need a compiler. - # This is primarily to support Pythons from binary - # installers. The kind and paths to build tools on - # the user system may vary significantly from the system - # that Python itself was built on. Also the user OS - # version and build tools may not support the same set - # of CPU architectures for universal builds. - global _config_vars - # Use get_config_var() to ensure _config_vars is initialized. - if not get_config_var('CUSTOMIZED_OSX_COMPILER'): - import _osx_support - - _osx_support.customize_compiler(_config_vars) - _config_vars['CUSTOMIZED_OSX_COMPILER'] = 'True' + _customize_macos() ( cc, From 9ce8a1088bb0053550debabb73fb92c763f4e7b3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:03:33 -0500 Subject: [PATCH 027/232] Convert comment to docstring; update wording. --- distutils/sysconfig.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index a88fd021df5..b1d8e7c7ae1 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -267,17 +267,20 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): def _customize_macos(): + """ + Perform first-time customization of compiler-related + config vars on macOS. Use after a compiler is known + to be needed. This customization exists primarily to support Pythons + from binary installers. The kind and paths to build tools on + the user system may vary significantly from the system + that Python itself was built on. Also the user OS + version and build tools may not support the same set + of CPU architectures for universal builds. + """ + if sys.platform != "darwin": return - # Perform first-time customization of compiler-related - # config vars on OS X now that we know we need a compiler. - # This is primarily to support Pythons from binary - # installers. The kind and paths to build tools on - # the user system may vary significantly from the system - # that Python itself was built on. Also the user OS - # version and build tools may not support the same set - # of CPU architectures for universal builds. global _config_vars # Use get_config_var() to ensure _config_vars is initialized. if not get_config_var('CUSTOMIZED_OSX_COMPILER'): From e58492bee26dbe58c600a72871144dd1a2a45f26 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:14:16 -0500 Subject: [PATCH 028/232] Create a fixture to patch-out compiler customization on macOS. --- conftest.py | 7 +++++++ distutils/tests/test_sysconfig.py | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index ca808a6ab70..06ce3bc6c86 100644 --- a/conftest.py +++ b/conftest.py @@ -152,3 +152,10 @@ def temp_home(tmp_path, monkeypatch): def fake_home(fs, monkeypatch): home = fs.create_dir('/fakehome') return _set_home(monkeypatch, pathlib.Path(home.path)) + + +@pytest.fixture +def disable_macos_customization(monkeypatch): + from distutils import sysconfig + + monkeypatch.setattr(sysconfig, '_customize_macos', lambda: None) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 6cbf51681bd..f656be6089c 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -98,8 +98,6 @@ def set_executables(self, **kw): 'CCSHARED': '--sc-ccshared', 'LDSHARED': 'sc_ldshared', 'SHLIB_SUFFIX': 'sc_shutil_suffix', - # On macOS, disable _osx_support.customize_compiler() - 'CUSTOMIZED_OSX_COMPILER': 'True', } comp = compiler() @@ -111,6 +109,7 @@ def set_executables(self, **kw): return comp @pytest.mark.skipif("get_default_compiler() != 'unix'") + @pytest.mark.usefixtures('disable_macos_customization') def test_customize_compiler(self): # Make sure that sysconfig._config_vars is initialized sysconfig.get_config_vars() From cc455d09fb862d4827e4efd7f6ae858fa5dde4ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:14:54 -0500 Subject: [PATCH 029/232] Utilize the fixture for disabling compiler customization on macOS for cxx test. Closes #231. --- distutils/tests/test_unixccompiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index a313da3e75a..2763db9c026 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -248,6 +248,7 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): assert self.cc.linker_so[0] == 'my_cc' @pytest.mark.skipif('platform.system == "Windows"') + @pytest.mark.usefixtures('disable_macos_customization') def test_cc_overrides_ldshared_for_cxx_correctly(self): """ Ensure that setting CC env variable also changes default linker From 9e83319a786cf55e6c3f8d3b45acba1f577924fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:25:15 -0500 Subject: [PATCH 030/232] Limit mutating global state and simply rely on functools.lru_cache to limit the behavior to a single invocation. --- distutils/sysconfig.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index b1d8e7c7ae1..5fb811c406e 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -10,6 +10,7 @@ """ import os +import functools import re import sys import sysconfig @@ -266,6 +267,7 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): ) +@functools.lru_cache() def _customize_macos(): """ Perform first-time customization of compiler-related @@ -278,16 +280,9 @@ def _customize_macos(): of CPU architectures for universal builds. """ - if sys.platform != "darwin": - return - - global _config_vars - # Use get_config_var() to ensure _config_vars is initialized. - if not get_config_var('CUSTOMIZED_OSX_COMPILER'): - import _osx_support - - _osx_support.customize_compiler(_config_vars) - _config_vars['CUSTOMIZED_OSX_COMPILER'] = 'True' + sys.platform == "darwin" and __import__('_osx_support').customize_compiler( + get_config_vars() + ) def customize_compiler(compiler): # noqa: C901 From b434f69238b4ee517ae20978afa19f3cd1ed8f1f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:05:46 -0500 Subject: [PATCH 031/232] Use 'extend-select' to avoid disabling the default config. Ref jaraco/skeleton#110. --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 6c5b00092e2..70612985a7a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -select = [ +extend-select = [ "C901", "W", ] From bdbe5e385a282d30611e95c3e252c9a123ade331 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:02:41 -0500 Subject: [PATCH 032/232] In test_build_ext, expose Path objects and use a path builder to build content. Fixes some EncodingWarnings. Ref pypa/distutils#232. --- distutils/tests/test_build_ext.py | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 537959fed66..51e5cd00cce 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -4,6 +4,7 @@ import textwrap import site import contextlib +import pathlib import platform import tempfile import importlib @@ -12,6 +13,7 @@ import path import pytest +import jaraco.path from distutils.core import Distribution from distutils.command.build_ext import build_ext @@ -38,6 +40,7 @@ def user_site_dir(request): self = request.instance self.tmp_dir = self.mkdtemp() + self.tmp_path = path.Path(self.tmp_dir) from distutils.command import build_ext orig_user_base = site.USER_BASE @@ -48,7 +51,7 @@ def user_site_dir(request): # bpo-30132: On Windows, a .pdb file may be created in the current # working directory. Create a temporary working directory to cleanup # everything at the end of the test. - with path.Path(self.tmp_dir): + with self.tmp_path: yield site.USER_BASE = orig_user_base @@ -496,25 +499,22 @@ def _try_compile_deployment_target(self, operator, target): else: os.environ['MACOSX_DEPLOYMENT_TARGET'] = target - deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c') + jaraco.path.build( + { + 'deptargetmodule.c': textwrap.dedent(f"""\ + #include - with open(deptarget_c, 'w') as fp: - fp.write( - textwrap.dedent( - """\ - #include + int dummy; - int dummy; + #if TARGET {operator} MAC_OS_X_VERSION_MIN_REQUIRED + #else + #error "Unexpected target" + #endif - #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED - #else - #error "Unexpected target" - #endif - - """ - % operator - ) - ) + """), + }, + self.tmp_path, + ) # get the deployment target that the interpreter was built with target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') @@ -534,7 +534,7 @@ def _try_compile_deployment_target(self, operator, target): target = '%02d0000' % target deptarget_ext = Extension( 'deptarget', - [deptarget_c], + [self.tmp_path / 'deptargetmodule.c'], extra_compile_args=['-DTARGET={}'.format(target)], ) dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]}) From 536553507947698491bc0e64a29491a6d2f8442b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:05:25 -0500 Subject: [PATCH 033/232] In support, specify encoding. Ref pypa/distutils#232. --- distutils/tests/support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/support.py b/distutils/tests/support.py index 2080604982d..ddf7bf1dba9 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -34,7 +34,7 @@ def write_file(self, path, content='xxx'): path can be a string or a sequence. """ - pathlib.Path(*always_iterable(path)).write_text(content) + pathlib.Path(*always_iterable(path)).write_text(content, encoding='utf-8') def create_dist(self, pkg_name='foo', **kw): """Will generate a test environment. From ba09295a480ec95569c393084c2e0a7846ffa384 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:14:54 -0500 Subject: [PATCH 034/232] In test_build_py, rely on tree builder to build trees. Ref pypa/distutils#232. --- distutils/tests/test_build_py.py | 51 ++++++++++++++------------------ 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 77c9ad75730..6730878e962 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -4,6 +4,7 @@ import sys import pytest +import jaraco.path from distutils.command.build_py import build_py from distutils.core import Distribution @@ -16,16 +17,13 @@ class TestBuildPy(support.TempdirManager): def test_package_data(self): sources = self.mkdtemp() - f = open(os.path.join(sources, "__init__.py"), "w") - try: - f.write("# Pretend this is a package.") - finally: - f.close() - f = open(os.path.join(sources, "README.txt"), "w") - try: - f.write("Info about this package") - finally: - f.close() + jaraco.path.build( + { + '__init__.py': "# Pretend this is a package.", + 'README.txt': 'Info about this package', + }, + sources, + ) destination = self.mkdtemp() @@ -62,11 +60,7 @@ def test_package_data(self): def test_empty_package_dir(self): # See bugs #1668596/#1720897 sources = self.mkdtemp() - open(os.path.join(sources, "__init__.py"), "w").close() - - testdir = os.path.join(sources, "doc") - os.mkdir(testdir) - open(os.path.join(testdir, "testfile"), "w").close() + jaraco.path.build({'__init__.py': '', 'doc': {'testfile': ''}}, sources) os.chdir(sources) dist = Distribution({ @@ -124,17 +118,19 @@ def test_dir_in_package_data(self): """ # See bug 19286 sources = self.mkdtemp() - pkg_dir = os.path.join(sources, "pkg") - - os.mkdir(pkg_dir) - open(os.path.join(pkg_dir, "__init__.py"), "w").close() - - docdir = os.path.join(pkg_dir, "doc") - os.mkdir(docdir) - open(os.path.join(docdir, "testfile"), "w").close() - - # create the directory that could be incorrectly detected as a file - os.mkdir(os.path.join(docdir, 'otherdir')) + jaraco.path.build( + { + 'pkg': { + '__init__.py': '', + 'doc': { + 'testfile': '', + # create a directory that could be incorrectly detected as a file + 'otherdir': {}, + }, + } + }, + sources, + ) os.chdir(sources) dist = Distribution({"packages": ["pkg"], "package_data": {"pkg": ["doc/*"]}}) @@ -174,9 +170,8 @@ def test_namespace_package_does_not_warn(self, caplog): """ # Create a fake project structure with a package namespace: tmp = self.mkdtemp() + jaraco.path.build({'ns': {'pkg': {'module.py': ''}}}, tmp) os.chdir(tmp) - os.makedirs("ns/pkg") - open("ns/pkg/module.py", "w").close() # Configure the package: attrs = { From f5bc9d2abfd66f3e95dcf9dcfd9aab4203ed7428 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:20:44 -0500 Subject: [PATCH 035/232] Specify encoding in util.byte_compile. Ref pypa/distutils#232. --- distutils/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/util.py b/distutils/util.py index 5408b16032e..aa0c90cfcd1 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -423,9 +423,9 @@ def byte_compile( # noqa: C901 log.info("writing byte-compilation script '%s'", script_name) if not dry_run: if script_fd is not None: - script = os.fdopen(script_fd, "w") + script = os.fdopen(script_fd, "w", encoding='utf-8') else: - script = open(script_name, "w") + script = open(script_name, "w", encoding='utf-8') with script: script.write( From 66d9341ddd33d363a7fdeafa065811ba73b8077f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:28:01 -0500 Subject: [PATCH 036/232] Rely on tree builder in test_build_scripts. Ref pypa/distutils#232. --- distutils/tests/test_build_scripts.py | 53 +++++++++++---------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index 28cc5632a33..8005b81c646 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -1,6 +1,9 @@ """Tests for distutils.command.build_scripts.""" import os +import textwrap + +import jaraco.path from distutils.command.build_scripts import build_scripts from distutils.core import Distribution @@ -46,37 +49,25 @@ def get_build_scripts_cmd(self, target, scripts): return build_scripts(dist) def write_sample_scripts(self, dir): - expected = [] - expected.append("script1.py") - self.write_script( - dir, - "script1.py", - ( - "#! /usr/bin/env python2.3\n" - "# bogus script w/ Python sh-bang\n" - "pass\n" - ), - ) - expected.append("script2.py") - self.write_script( - dir, - "script2.py", - ("#!/usr/bin/python\n" "# bogus script w/ Python sh-bang\n" "pass\n"), - ) - expected.append("shell.sh") - self.write_script( - dir, - "shell.sh", - ("#!/bin/sh\n" "# bogus shell script w/ sh-bang\n" "exit 0\n"), - ) - return expected - - def write_script(self, dir, name, text): - f = open(os.path.join(dir, name), "w") - try: - f.write(text) - finally: - f.close() + spec = { + 'script1.py': textwrap.dedent(""" + #! /usr/bin/env python2.3 + # bogus script w/ Python sh-bang + pass + """).lstrip(), + 'script2.py': textwrap.dedent(""" + #!/usr/bin/python + # bogus script w/ Python sh-bang + pass + """).lstrip(), + 'shell.sh': textwrap.dedent(""" + #!/bin/sh + # bogus shell script w/ sh-bang + exit 0 + """).lstrip(), + } + jaraco.path.build(spec, dir) + return list(spec) def test_version_int(self): source = self.mkdtemp() From b11410214a9c7398cfd3c0d6c9129f6a8f9d7599 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:39:02 -0500 Subject: [PATCH 037/232] Rely on Path object to replace the suffix, open the file, and count the lines. Ref pypa/distutils#232. --- distutils/tests/test_ccompiler.py | 2 +- distutils/tests/test_config_cmd.py | 11 +++++------ setup.cfg | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 49691d4b9b5..b6512e6d778 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -36,7 +36,7 @@ def c_file(tmp_path): .lstrip() .replace('#headers', headers) ) - c_file.write_text(payload) + c_file.write_text(payload, encoding='utf-8') return c_file diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 2519ed6a104..90c8f906791 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -3,6 +3,8 @@ import os import sys +import more_itertools +import path import pytest from distutils.command.config import dump_file, config @@ -24,12 +26,9 @@ def _info(self, msg, *args): self._logs.append(line) def test_dump_file(self): - this_file = os.path.splitext(__file__)[0] + '.py' - f = open(this_file) - try: - numlines = len(f.readlines()) - finally: - f.close() + this_file = path.Path(__file__).with_suffix('.py') + with this_file.open(encoding='utf-8') as f: + numlines = more_itertools.ilen(f) dump_file(this_file, 'I am the header') assert len(self._logs) == numlines + 1 diff --git a/setup.cfg b/setup.cfg index ba2d6599844..d1c98554502 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ testing = jaraco.envs>=2.4 jaraco.path jaraco.text - path + path >= 10.6 docutils pyfakefs more_itertools From 3dcd43668abc4d7156eada8f63b076067fe5322b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:40:50 -0500 Subject: [PATCH 038/232] Fix EncodingWarnings in test_core. Ref pypa/distutils#232. --- distutils/tests/test_core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index 2c11ff769ee..95aa299889a 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -70,20 +70,20 @@ class TestCore: def test_run_setup_provides_file(self, temp_file): # Make sure the script can use __file__; if that's missing, the test # setup.py script will raise NameError. - temp_file.write_text(setup_using___file__) + temp_file.write_text(setup_using___file__, encoding='utf-8') distutils.core.run_setup(temp_file) def test_run_setup_preserves_sys_argv(self, temp_file): # Make sure run_setup does not clobber sys.argv argv_copy = sys.argv.copy() - temp_file.write_text(setup_does_nothing) + temp_file.write_text(setup_does_nothing, encoding='utf-8') distutils.core.run_setup(temp_file) assert sys.argv == argv_copy def test_run_setup_defines_subclass(self, temp_file): # Make sure the script can use __file__; if that's missing, the test # setup.py script will raise NameError. - temp_file.write_text(setup_defines_subclass) + temp_file.write_text(setup_defines_subclass, encoding='utf-8') dist = distutils.core.run_setup(temp_file) install = dist.get_command_obj('install') assert 'cmd' in install.sub_commands @@ -98,7 +98,7 @@ def test_run_setup_uses_current_dir(self, tmp_path): # Create a directory and write the setup.py file there: setup_py = tmp_path / 'setup.py' - setup_py.write_text(setup_prints_cwd) + setup_py.write_text(setup_prints_cwd, encoding='utf-8') distutils.core.run_setup(setup_py) output = sys.stdout.getvalue() @@ -107,14 +107,14 @@ def test_run_setup_uses_current_dir(self, tmp_path): assert cwd == output def test_run_setup_within_if_main(self, temp_file): - temp_file.write_text(setup_within_if_main) + temp_file.write_text(setup_within_if_main, encoding='utf-8') dist = distutils.core.run_setup(temp_file, stop_after="config") assert isinstance(dist, Distribution) assert dist.get_name() == "setup_within_if_main" def test_run_commands(self, temp_file): sys.argv = ['setup.py', 'build'] - temp_file.write_text(setup_within_if_main) + temp_file.write_text(setup_within_if_main, encoding='utf-8') dist = distutils.core.run_setup(temp_file, stop_after="commandline") assert 'build' not in dist.have_run distutils.core.run_commands(dist) From cae489b96c3ebeadcee4f0efda008d25f7623516 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:43:36 -0500 Subject: [PATCH 039/232] Ran pyupgrade for Python 3.8+ followed by ruff format. --- distutils/bcppcompiler.py | 6 ++---- distutils/ccompiler.py | 4 +--- distutils/cmd.py | 8 +++----- distutils/command/_framework_compat.py | 4 ++-- distutils/command/bdist_rpm.py | 4 ++-- distutils/command/build.py | 2 +- distutils/command/build_ext.py | 2 +- distutils/command/check.py | 2 +- distutils/command/register.py | 2 +- distutils/command/upload.py | 8 +++----- distutils/core.py | 6 +++--- distutils/cygwinccompiler.py | 10 ++++------ distutils/dir_util.py | 10 +++------- distutils/dist.py | 8 ++++---- distutils/fancy_getopt.py | 6 +++--- distutils/file_util.py | 26 ++++++++------------------ distutils/filelist.py | 4 ++-- distutils/msvc9compiler.py | 14 +++++--------- distutils/msvccompiler.py | 6 ++---- distutils/py38compat.py | 2 +- distutils/spawn.py | 8 ++------ distutils/sysconfig.py | 2 +- distutils/tests/test_bdist_dumb.py | 2 +- distutils/tests/test_build.py | 2 +- distutils/tests/test_build_ext.py | 2 +- distutils/tests/test_dir_util.py | 2 +- distutils/tests/test_file_util.py | 4 ++-- distutils/tests/test_version.py | 4 ++-- distutils/util.py | 6 +++--- distutils/version.py | 2 +- 30 files changed, 67 insertions(+), 101 deletions(-) diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index 14d51472f29..d496d5d452d 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -238,7 +238,7 @@ def link( # noqa: C901 def_file = os.path.join(temp_dir, '%s.def' % modname) contents = ['EXPORTS'] for sym in export_symbols or []: - contents.append(' {}=_{}'.format(sym, sym)) + contents.append(f' {sym}=_{sym}') self.execute(write_file, (def_file, contents), "writing %s" % def_file) # Borland C++ has problems with '/' in paths @@ -348,9 +348,7 @@ def object_filenames(self, source_filenames, strip_dir=0, output_dir=''): # use normcase to make sure '.rc' is really '.rc' and not '.RC' (base, ext) = os.path.splitext(os.path.normcase(src_name)) if ext not in (self.src_extensions + ['.rc', '.res']): - raise UnknownFileError( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: base = os.path.basename(base) if ext == '.res': diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 67feb164867..6faf546cfe2 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -973,9 +973,7 @@ def _make_out_path(self, output_dir, strip_dir, src_name): try: new_ext = self.out_extensions[ext] except LookupError: - raise UnknownFileError( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + 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) diff --git a/distutils/cmd.py b/distutils/cmd.py index 8fdcbc0ea22..8849474cd7d 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -165,7 +165,7 @@ def dump_options(self, header=None, indent=""): if option[-1] == "=": option = option[:-1] value = getattr(self, option) - self.announce(indent + "{} = {}".format(option, value), level=logging.INFO) + self.announce(indent + f"{option} = {value}", level=logging.INFO) def run(self): """A command's raison d'etre: carry out the action it exists to @@ -213,9 +213,7 @@ def _ensure_stringlike(self, option, what, default=None): setattr(self, option, default) return default elif not isinstance(val, str): - raise DistutilsOptionError( - "'{}' must be a {} (got `{}`)".format(option, what, val) - ) + raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)") return val def ensure_string(self, option, default=None): @@ -242,7 +240,7 @@ def ensure_string_list(self, option): ok = False if not ok: raise DistutilsOptionError( - "'{}' must be a list of strings (got {!r})".format(option, val) + f"'{option}' must be a list of strings (got {val!r})" ) def _ensure_tested_string(self, option, tester, what, error_fmt, default=None): diff --git a/distutils/command/_framework_compat.py b/distutils/command/_framework_compat.py index b4228299f4f..397ebf823e4 100644 --- a/distutils/command/_framework_compat.py +++ b/distutils/command/_framework_compat.py @@ -9,7 +9,7 @@ import sysconfig -@functools.lru_cache() +@functools.lru_cache def enabled(): """ Only enabled for Python 3.9 framework homebrew builds @@ -37,7 +37,7 @@ def enabled(): ) -@functools.lru_cache() +@functools.lru_cache def vars(): if not enabled(): return {} diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index e96db22bed7..675bcebdad6 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -487,7 +487,7 @@ def _make_spec_file(self): # noqa: C901 if isinstance(val, list): spec_file.append('{}: {}'.format(field, ' '.join(val))) elif val is not None: - spec_file.append('{}: {}'.format(field, val)) + spec_file.append(f'{field}: {val}') if self.distribution.get_url(): spec_file.append('Url: ' + self.distribution.get_url()) @@ -522,7 +522,7 @@ def _make_spec_file(self): # noqa: C901 # rpm scripts # figure out default build script - def_setup_call = "{} {}".format(self.python, os.path.basename(sys.argv[0])) + def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}" def_build = "%s build" % def_setup_call if self.use_rpm_opt_flags: def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build diff --git a/distutils/command/build.py b/distutils/command/build.py index cc9b367ef92..d8704e35838 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -78,7 +78,7 @@ def finalize_options(self): # noqa: C901 "using './configure --help' on your platform)" ) - plat_specifier = ".{}-{}".format(self.plat_name, sys.implementation.cache_tag) + plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}" # Make it so Python 2.x and Python 2.x with --with-pydebug don't # share the same build directories. Doing so confuses the build diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index ba6580c71ee..a15781f28a2 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -515,7 +515,7 @@ def _filter_build_errors(self, ext): except (CCompilerError, DistutilsError, CompileError) as e: if not ext.optional: raise - self.warn('building extension "{}" failed: {}'.format(ext.name, e)) + self.warn(f'building extension "{ext.name}" failed: {e}') def build_extension(self, ext): sources = ext.sources diff --git a/distutils/command/check.py b/distutils/command/check.py index b59cc237312..28f55fb9142 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -116,7 +116,7 @@ def check_restructuredtext(self): if line is None: warning = warning[1] else: - warning = '{} (line {})'.format(warning[1], line) + warning = f'{warning[1]} (line {line})' self.warn(warning) def _check_rst_data(self, data): diff --git a/distutils/command/register.py b/distutils/command/register.py index cf1afc8c1f1..5a24246ccba 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -174,7 +174,7 @@ def send_metadata(self): # noqa: C901 auth.add_password(self.realm, host, username, password) # send the info to the server and report the result code, result = self.post_to_server(self.build_post_data('submit'), auth) - self.announce('Server response ({}): {}'.format(code, result), logging.INFO) + self.announce(f'Server response ({code}): {result}', logging.INFO) # possibly save the login if code == 200: diff --git a/distutils/command/upload.py b/distutils/command/upload.py index caf15f04a60..a9124f2b718 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -169,7 +169,7 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 body.write(end_boundary) body = body.getvalue() - msg = "Submitting {} to {}".format(filename, self.repository) + msg = f"Submitting {filename} to {self.repository}" self.announce(msg, logging.INFO) # build the Request @@ -193,14 +193,12 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 raise if status == 200: - self.announce( - 'Server response ({}): {}'.format(status, reason), logging.INFO - ) + self.announce(f'Server response ({status}): {reason}', logging.INFO) if self.show_response: text = self._read_pypi_response(result) msg = '\n'.join(('-' * 75, text, '-' * 75)) self.announce(msg, logging.INFO) else: - msg = 'Upload failed ({}): {}'.format(status, reason) + msg = f'Upload failed ({status}): {reason}' self.announce(msg, logging.ERROR) raise DistutilsError(msg) diff --git a/distutils/core.py b/distutils/core.py index 05d2971994d..799de9489ce 100644 --- a/distutils/core.py +++ b/distutils/core.py @@ -203,10 +203,10 @@ def run_commands(dist): raise SystemExit("interrupted") except OSError as exc: if DEBUG: - sys.stderr.write("error: {}\n".format(exc)) + sys.stderr.write(f"error: {exc}\n") raise else: - raise SystemExit("error: {}".format(exc)) + raise SystemExit(f"error: {exc}") except (DistutilsError, CCompilerError) as msg: if DEBUG: @@ -249,7 +249,7 @@ def run_setup(script_name, script_args=None, stop_after="run"): used to drive the Distutils. """ if stop_after not in ('init', 'config', 'commandline', 'run'): - raise ValueError("invalid value for 'stop_after': {!r}".format(stop_after)) + raise ValueError(f"invalid value for 'stop_after': {stop_after!r}") global _setup_stop_after, _setup_distribution _setup_stop_after = stop_after diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index b3dbc3be15c..84151b7eb9c 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -87,9 +87,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): super().__init__(verbose, dry_run, force) status, details = check_config_h() - self.debug_print( - "Python's GCC status: {} (details: {})".format(status, details) - ) + self.debug_print(f"Python's GCC status: {status} (details: {details})") if status is not CONFIG_H_OK: self.warn( "Python's pyconfig.h doesn't seem to support your compiler. " @@ -108,7 +106,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mcygwin -mdll -O -Wall' % self.cc, compiler_cxx='%s -mcygwin -O -Wall' % self.cxx, linker_exe='%s -mcygwin' % self.cc, - linker_so=('{} -mcygwin {}'.format(self.linker_dll, shared_option)), + linker_so=(f'{self.linker_dll} -mcygwin {shared_option}'), ) # Include the appropriate MSVC runtime library if Python was built @@ -280,7 +278,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mdll -O -Wall' % self.cc, compiler_cxx='%s -O -Wall' % self.cxx, linker_exe='%s' % self.cc, - linker_so='{} {}'.format(self.linker_dll, shared_option), + linker_so=f'{self.linker_dll} {shared_option}', ) def runtime_library_dir_option(self, dir): @@ -340,7 +338,7 @@ def check_config_h(): finally: config_h.close() except OSError as exc: - return (CONFIG_H_UNCERTAIN, "couldn't read '{}': {}".format(fn, exc.strerror)) + return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}") def is_cygwincc(cc): diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 23dc3392a2c..819fe56f6db 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -33,9 +33,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 # Detect a common bug -- name is None if not isinstance(name, str): - raise DistutilsInternalError( - "mkpath: 'name' must be a string (got {!r})".format(name) - ) + raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})") # XXX what's the better way to handle verbosity? print as we create # each directory in the path (the current behaviour), or only announce @@ -76,7 +74,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 except OSError as exc: if not (exc.errno == errno.EEXIST and os.path.isdir(head)): raise DistutilsFileError( - "could not create '{}': {}".format(head, exc.args[-1]) + f"could not create '{head}': {exc.args[-1]}" ) created_dirs.append(head) @@ -143,9 +141,7 @@ def copy_tree( # noqa: C901 if dry_run: names = [] else: - raise DistutilsFileError( - "error listing files in '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}") if not dry_run: mkpath(dst, verbose=verbose) diff --git a/distutils/dist.py b/distutils/dist.py index 7c0f0e5b78c..659583943b8 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -821,7 +821,7 @@ def get_command_class(self, command): return klass for pkgname in self.get_command_packages(): - module_name = "{}.{}".format(pkgname, command) + module_name = f"{pkgname}.{command}" klass_name = command try: @@ -889,7 +889,7 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 self.announce(" setting options for '%s' command:" % command_name) for option, (source, value) in option_dict.items(): if DEBUG: - self.announce(" {} = {} (from {})".format(option, value, source)) + self.announce(f" {option} = {value} (from {source})") try: bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] except AttributeError: @@ -1178,7 +1178,7 @@ def maybe_write(header, val): def _write_list(self, file, name, values): values = values or [] for value in values: - file.write('{}: {}\n'.format(name, value)) + file.write(f'{name}: {value}\n') # -- Metadata query methods ---------------------------------------- @@ -1189,7 +1189,7 @@ def get_version(self): return self.version or "0.0.0" def get_fullname(self): - return "{}-{}".format(self.get_name(), self.get_version()) + return f"{self.get_name()}-{self.get_version()}" def get_author(self): return self.author diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 3b887dc5a41..c025f12062c 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -22,7 +22,7 @@ longopt_re = re.compile(r'^%s$' % longopt_pat) # For recognizing "negative alias" options, eg. "quiet=!verbose" -neg_alias_re = re.compile("^({})=!({})$".format(longopt_pat, longopt_pat)) +neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$") # This is used to translate long options to legitimate Python identifiers # (for use as attributes of some object). @@ -157,7 +157,7 @@ def _grok_option_table(self): # noqa: C901 else: # the option table is part of the code, so simply # assert that it is correct - raise ValueError("invalid option tuple: {!r}".format(option)) + raise ValueError(f"invalid option tuple: {option!r}") # Type- and value-check the option names if not isinstance(long, str) or len(long) < 2: @@ -359,7 +359,7 @@ def generate_help(self, header=None): # noqa: C901 # Case 2: we have a short option, so we have to include it # just after the long option else: - opt_names = "{} (-{})".format(long, short) + opt_names = f"{long} (-{short})" if text: lines.append(" --%-*s %s" % (max_opt, opt_names, text[0])) else: diff --git a/distutils/file_util.py b/distutils/file_util.py index 3f3e21b5673..8ebd2a790f6 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -26,30 +26,24 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fsrc = open(src, 'rb') except OSError as e: - raise DistutilsFileError("could not open '{}': {}".format(src, e.strerror)) + raise DistutilsFileError(f"could not open '{src}': {e.strerror}") if os.path.exists(dst): try: os.unlink(dst) except OSError as e: - raise DistutilsFileError( - "could not delete '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}") try: fdst = open(dst, 'wb') except OSError as e: - raise DistutilsFileError( - "could not create '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not create '{dst}': {e.strerror}") while True: try: buf = fsrc.read(buffer_size) except OSError as e: - raise DistutilsFileError( - "could not read from '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"could not read from '{src}': {e.strerror}") if not buf: break @@ -57,9 +51,7 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fdst.write(buf) except OSError as e: - raise DistutilsFileError( - "could not write to '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}") finally: if fdst: fdst.close() @@ -199,12 +191,12 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 dst = os.path.join(dst, basename(src)) elif exists(dst): raise DistutilsFileError( - "can't move '{}': destination '{}' already exists".format(src, dst) + f"can't move '{src}': destination '{dst}' already exists" ) if not isdir(dirname(dst)): raise DistutilsFileError( - "can't move '{}': destination '{}' not a valid path".format(src, dst) + f"can't move '{src}': destination '{dst}' not a valid path" ) copy_it = False @@ -215,9 +207,7 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 if num == errno.EXDEV: copy_it = True else: - raise DistutilsFileError( - "couldn't move '{}' to '{}': {}".format(src, dst, msg) - ) + raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}") if copy_it: copy_file(src, dst, verbose=verbose) diff --git a/distutils/filelist.py b/distutils/filelist.py index 6dadf923d71..3205762654d 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -363,9 +363,9 @@ def translate_pattern(pattern, anchor=1, prefix=None, is_regex=0): if os.sep == '\\': sep = r'\\' pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)] - pattern_re = r'{}\A{}{}.*{}{}'.format(start, prefix_re, sep, pattern_re, end) + pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}' else: # no prefix -- respect anchor flag if anchor: - pattern_re = r'{}\A{}'.format(start, pattern_re[len(start) :]) + pattern_re = rf'{start}\A{pattern_re[len(start) :]}' return re.compile(pattern_re) diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index 724986d89d0..402c0c0620e 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -175,7 +175,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = Reg.get_value(base, r"{}\{}".format(p, key)) + d = Reg.get_value(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -281,7 +281,7 @@ def query_vcvarsall(version, arch="x86"): raise DistutilsPlatformError("Unable to find vcvarsall.bat") log.debug("Calling 'vcvarsall.bat %s' (version=%s)", arch, version) popen = subprocess.Popen( - '"{}" {} & set'.format(vcvarsall, arch), + f'"{vcvarsall}" {arch} & set', stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) @@ -370,9 +370,7 @@ def initialize(self, plat_name=None): # noqa: C901 # sanity check for platforms to prevent obscure errors later. ok_plats = 'win32', 'win-amd64' if plat_name not in ok_plats: - raise DistutilsPlatformError( - "--plat-name must be one of {}".format(ok_plats) - ) + raise DistutilsPlatformError(f"--plat-name must be one of {ok_plats}") if ( "DISTUTILS_USE_SDK" in os.environ @@ -564,9 +562,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: @@ -687,7 +683,7 @@ def link( # noqa: C901 mfinfo = self.manifest_get_embed_info(target_desc, ld_args) if mfinfo is not None: mffilename, mfid = mfinfo - out_arg = '-outputresource:{};{}'.format(output_filename, mfid) + out_arg = f'-outputresource:{output_filename};{mfid}' try: self.spawn(['mt.exe', '-nologo', '-manifest', mffilename, out_arg]) except DistutilsExecError as msg: diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index c3823e257ef..1a07746bc70 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -159,7 +159,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = read_values(base, r"{}\{}".format(p, key)) + d = read_values(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -454,9 +454,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: diff --git a/distutils/py38compat.py b/distutils/py38compat.py index 59224e71e50..ab12119fa5f 100644 --- a/distutils/py38compat.py +++ b/distutils/py38compat.py @@ -5,4 +5,4 @@ def aix_platform(osname, version, release): return _aix_support.aix_platform() except ImportError: pass - return "{}-{}.{}".format(osname, version, release) + return f"{osname}-{version}.{release}" diff --git a/distutils/spawn.py b/distutils/spawn.py index afefe525ef1..48adceb1146 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -60,16 +60,12 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 except OSError as exc: if not DEBUG: cmd = cmd[0] - raise DistutilsExecError( - "command {!r} failed: {}".format(cmd, exc.args[-1]) - ) from exc + raise DistutilsExecError(f"command {cmd!r} failed: {exc.args[-1]}") from exc if exitcode: if not DEBUG: cmd = cmd[0] - raise DistutilsExecError( - "command {!r} failed with exit code {}".format(cmd, exitcode) - ) + raise DistutilsExecError(f"command {cmd!r} failed with exit code {exitcode}") def find_executable(executable, path=None): diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 5fb811c406e..40215b83478 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -267,7 +267,7 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): ) -@functools.lru_cache() +@functools.lru_cache def _customize_macos(): """ Perform first-time customization of compiler-related diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index 95532e83b95..cb4db4e1924 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -61,7 +61,7 @@ def test_simple_built(self): # see what we have dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) - base = "{}.{}.zip".format(dist.get_fullname(), cmd.plat_name) + base = f"{dist.get_fullname()}.{cmd.plat_name}.zip" assert dist_created == [base] diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index c2cff445233..8617fa99197 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -24,7 +24,7 @@ def test_finalize_options(self): # build_platlib is 'build/lib.platform-cache_tag[-pydebug]' # examples: # build/lib.macosx-10.3-i386-cpython39 - plat_spec = '.{}-{}'.format(cmd.plat_name, sys.implementation.cache_tag) + plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}' if hasattr(sys, 'gettotalrefcount'): assert cmd.build_platlib.endswith('-pydebug') plat_spec += '-pydebug' diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 51e5cd00cce..e24dea3603a 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -535,7 +535,7 @@ def _try_compile_deployment_target(self, operator, target): deptarget_ext = Extension( 'deptarget', [self.tmp_path / 'deptargetmodule.c'], - extra_compile_args=['-DTARGET={}'.format(target)], + extra_compile_args=[f'-DTARGET={target}'], ) dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]}) dist.package_dir = self.tmp_dir diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 0738b7c8770..e7d69bb6ef1 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -75,7 +75,7 @@ def test_copy_tree_verbosity(self, caplog): with open(a_file, 'w') as f: f.write('some content') - wanted = ['copying {} -> {}'.format(a_file, self.target2)] + wanted = [f'copying {a_file} -> {self.target2}'] copy_tree(self.target, self.target2, verbose=1) assert caplog.messages == wanted diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 3b9f82b71e9..e441186e3a9 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -35,7 +35,7 @@ def test_move_file_verbosity(self, caplog): move_file(self.target, self.source, verbose=0) move_file(self.source, self.target, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target)] + wanted = [f'moving {self.source} -> {self.target}'] assert caplog.messages == wanted # back to original state @@ -45,7 +45,7 @@ def test_move_file_verbosity(self, caplog): # now the target is a dir os.mkdir(self.target_dir) move_file(self.source, self.target_dir, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target_dir)] + wanted = [f'moving {self.source} -> {self.target_dir}'] assert caplog.messages == wanted def test_move_file_exception_unpacking_rename(self): diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 900edafa7c8..0aaf0a534cd 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -62,7 +62,7 @@ def test_cmp_strict(self): res = StrictVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' def test_cmp(self): versions = ( @@ -88,4 +88,4 @@ def test_cmp(self): res = LooseVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' diff --git a/distutils/util.py b/distutils/util.py index aa0c90cfcd1..c26e61ab4a9 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -172,7 +172,7 @@ def change_root(new_root, pathname): raise DistutilsPlatformError(f"nothing known about platform '{os.name}'") -@functools.lru_cache() +@functools.lru_cache def check_environ(): """Ensure that 'os.environ' has all the environment variables we guarantee that users can use in config files, command-line options, @@ -328,7 +328,7 @@ def execute(func, args, msg=None, verbose=0, dry_run=0): print. """ if msg is None: - msg = "{}{!r}".format(func.__name__, args) + msg = f"{func.__name__}{args!r}" if msg[-2:] == ',)': # correct for singleton tuple msg = msg[0:-2] + ')' @@ -350,7 +350,7 @@ def strtobool(val): elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 else: - raise ValueError("invalid truth value {!r}".format(val)) + raise ValueError(f"invalid truth value {val!r}") def byte_compile( # noqa: C901 diff --git a/distutils/version.py b/distutils/version.py index 18385cfef2d..8ab76ddef4e 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -60,7 +60,7 @@ def __init__(self, vstring=None): ) def __repr__(self): - return "{} ('{}')".format(self.__class__.__name__, str(self)) + return f"{self.__class__.__name__} ('{str(self)}')" def __eq__(self, other): c = self._cmp(other) From b060f26530bb8570f1577b8b4ff562760c336cdf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:47:11 -0500 Subject: [PATCH 040/232] Rely on tree builder in test_dir_util. Ref pypa/distutils#232. --- distutils/tests/test_dir_util.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index e7d69bb6ef1..6fc9ed08835 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -4,6 +4,10 @@ import stat import unittest.mock as mock +import jaraco.path +import path +import pytest + from distutils import dir_util, errors from distutils.dir_util import ( mkpath, @@ -14,7 +18,6 @@ ) from distutils.tests import support -import pytest @pytest.fixture(autouse=True) @@ -71,9 +74,8 @@ def test_copy_tree_verbosity(self, caplog): remove_tree(self.root_target, verbose=0) mkpath(self.target, verbose=0) - a_file = os.path.join(self.target, 'ok.txt') - with open(a_file, 'w') as f: - f.write('some content') + a_file = path.Path(self.target) / 'ok.txt' + jaraco.path.build({'ok.txt': 'some content'}, self.target) wanted = [f'copying {a_file} -> {self.target2}'] copy_tree(self.target, self.target2, verbose=1) @@ -85,11 +87,7 @@ def test_copy_tree_verbosity(self, caplog): def test_copy_tree_skips_nfs_temp_files(self): mkpath(self.target, verbose=0) - a_file = os.path.join(self.target, 'ok.txt') - nfs_file = os.path.join(self.target, '.nfs123abc') - for f in a_file, nfs_file: - with open(f, 'w') as fh: - fh.write('some content') + jaraco.path.build({'ok.txt': 'some content', '.nfs123abc': ''}, self.target) copy_tree(self.target, self.target2) assert os.listdir(self.target2) == ['ok.txt'] From 438b37afae271c08dad74e96f59a5b68a80e333c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:56:46 -0500 Subject: [PATCH 041/232] Rely on tree builder and path objects. Ref pypa/distutils#232. --- distutils/tests/test_file_util.py | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index e441186e3a9..888e27b5b5d 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -4,29 +4,28 @@ import errno import unittest.mock as mock +import jaraco.path +import path +import pytest + from distutils.file_util import move_file, copy_file from distutils.tests import support from distutils.errors import DistutilsFileError from .py38compat import unlink -import pytest @pytest.fixture(autouse=True) def stuff(request, monkeypatch, distutils_managed_tempdir): self = request.instance - tmp_dir = self.mkdtemp() - self.source = os.path.join(tmp_dir, 'f1') - self.target = os.path.join(tmp_dir, 'f2') - self.target_dir = os.path.join(tmp_dir, 'd1') + tmp_dir = path.Path(self.mkdtemp()) + self.source = tmp_dir / 'f1' + self.target = tmp_dir / 'f2' + self.target_dir = tmp_dir / 'd1' class TestFileUtil(support.TempdirManager): def test_move_file_verbosity(self, caplog): - f = open(self.source, 'w') - try: - f.write('some content') - finally: - f.close() + jaraco.path.build({self.source: 'some content'}) move_file(self.source, self.target, verbose=0) assert not caplog.messages @@ -53,8 +52,7 @@ def test_move_file_exception_unpacking_rename(self): with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( DistutilsFileError ): - with open(self.source, 'w') as fobj: - fobj.write('spam eggs') + jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=0) def test_move_file_exception_unpacking_unlink(self): @@ -64,36 +62,32 @@ def test_move_file_exception_unpacking_unlink(self): ), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises( DistutilsFileError ): - with open(self.source, 'w') as fobj: - fobj.write('spam eggs') + jaraco.path.build({self.source: 'spam eggs'}) move_file(self.source, self.target, verbose=0) def test_copy_file_hard_link(self): - with open(self.source, 'w') as f: - f.write('some content') + jaraco.path.build({self.source: 'some content'}) # Check first that copy_file() will not fall back on copying the file # instead of creating the hard link. try: - os.link(self.source, self.target) + self.source.link(self.target) except OSError as e: self.skipTest('os.link: %s' % e) else: - unlink(self.target) + self.target.unlink() st = os.stat(self.source) copy_file(self.source, self.target, link='hard') st2 = os.stat(self.source) st3 = os.stat(self.target) assert os.path.samestat(st, st2), (st, st2) assert os.path.samestat(st2, st3), (st2, st3) - with open(self.source) as f: - assert f.read() == 'some content' + assert self.source.read_text(encoding='utf-8') == 'some content' def test_copy_file_hard_link_failure(self): # If hard linking fails, copy_file() falls back on copying file # (some special filesystems don't support hard linking even under # Unix, see issue #8876). - with open(self.source, 'w') as f: - f.write('some content') + jaraco.path.build({self.source: 'some content'}) st = os.stat(self.source) with mock.patch("os.link", side_effect=OSError(0, "linking unsupported")): copy_file(self.source, self.target, link='hard') @@ -102,5 +96,4 @@ def test_copy_file_hard_link_failure(self): assert os.path.samestat(st, st2), (st, st2) assert not os.path.samestat(st2, st3), (st2, st3) for fn in (self.source, self.target): - with open(fn) as f: - assert f.read() == 'some content' + assert fn.read_text(encoding='utf-8') == 'some content' From 43ee1e22f58c36d26851a779ea00aa6ec72839a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 13:59:11 -0500 Subject: [PATCH 042/232] Remove reliance on TempdirManager in test_file_util. --- distutils/tests/test_file_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 888e27b5b5d..27796d9fd5e 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -15,15 +15,15 @@ @pytest.fixture(autouse=True) -def stuff(request, monkeypatch, distutils_managed_tempdir): +def stuff(request, tmp_path): self = request.instance - tmp_dir = path.Path(self.mkdtemp()) + tmp_dir = path.Path(tmp_path) self.source = tmp_dir / 'f1' self.target = tmp_dir / 'f2' self.target_dir = tmp_dir / 'd1' -class TestFileUtil(support.TempdirManager): +class TestFileUtil: def test_move_file_verbosity(self, caplog): jaraco.path.build({self.source: 'some content'}) From 5c998067eb1ab64befb831abe891ab67f69ca143 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:01:40 -0500 Subject: [PATCH 043/232] Rely on tmp_path fixture directly. --- distutils/tests/test_file_util.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 27796d9fd5e..08f9e19facf 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -5,7 +5,6 @@ import unittest.mock as mock import jaraco.path -import path import pytest from distutils.file_util import move_file, copy_file @@ -17,10 +16,9 @@ @pytest.fixture(autouse=True) def stuff(request, tmp_path): self = request.instance - tmp_dir = path.Path(tmp_path) - self.source = tmp_dir / 'f1' - self.target = tmp_dir / 'f2' - self.target_dir = tmp_dir / 'd1' + self.source = tmp_path / 'f1' + self.target = tmp_path / 'f2' + self.target_dir = tmp_path / 'd1' class TestFileUtil: @@ -70,7 +68,7 @@ def test_copy_file_hard_link(self): # Check first that copy_file() will not fall back on copying the file # instead of creating the hard link. try: - self.source.link(self.target) + os.link(self.source, self.target) except OSError as e: self.skipTest('os.link: %s' % e) else: From e2c4a88b6f4f31c7c8cc205917aa6d71496e97c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:06:19 -0500 Subject: [PATCH 044/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_build_ext.py | 1 - distutils/tests/test_file_util.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index e24dea3603a..4ae81a22e4b 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -4,7 +4,6 @@ import textwrap import site import contextlib -import pathlib import platform import tempfile import importlib diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 08f9e19facf..6c7019140e6 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -8,9 +8,7 @@ import pytest from distutils.file_util import move_file, copy_file -from distutils.tests import support from distutils.errors import DistutilsFileError -from .py38compat import unlink @pytest.fixture(autouse=True) From 1e3fe05c6b02b6ff7dffa8bd902a8643ce2bca20 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:13:59 -0500 Subject: [PATCH 045/232] Rely on tree builder. Ref pypa/distutils#232. --- distutils/tests/test_filelist.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index bfffbb1da0f..bf1a9d9b45f 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -322,14 +322,18 @@ def test_non_local_discovery(self, tmp_path): When findall is called with another path, the full path name should be returned. """ - filename = tmp_path / 'file1.txt' - filename.write_text('') - expected = [str(filename)] + jaraco.path.build({'file1.txt': ''}, tmp_path) + expected = [str(tmp_path / 'file1.txt')] assert filelist.findall(tmp_path) == expected @os_helper.skip_unless_symlink def test_symlink_loop(self, tmp_path): - tmp_path.joinpath('link-to-parent').symlink_to('.') - tmp_path.joinpath('somefile').write_text('') + jaraco.path.build( + { + 'link-to-parent': jaraco.path.Symlink('.'), + 'somefile': '', + }, + tmp_path, + ) files = filelist.findall(tmp_path) assert len(files) == 1 From acff48deeb93775bbf7fa90750baf53f4e99cf42 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:16:27 -0500 Subject: [PATCH 046/232] Specify encoding in test_install. Ref pypa/distutils#232. --- distutils/tests/test_install.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 082ee1d3491..16ac5ca7462 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -196,13 +196,9 @@ def test_record(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.record) - try: - content = f.read() - finally: - f.close() + content = pathlib.Path(cmd.record).read_text(encoding='utf-8') - found = [os.path.basename(line) for line in content.splitlines()] + found = [pathlib.Path(line).name for line in content.splitlines()] expected = [ 'hello.py', 'hello.%s.pyc' % sys.implementation.cache_tag, @@ -234,9 +230,9 @@ def test_record_extensions(self): cmd.ensure_finalized() cmd.run() - content = pathlib.Path(cmd.record).read_text() + content = pathlib.Path(cmd.record).read_text(encoding='utf-8') - found = [os.path.basename(line) for line in content.splitlines()] + found = [pathlib.Path(line).name for line in content.splitlines()] expected = [ _make_ext_name('xx'), 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2], From d3f79e28842d4fd798d0d98eb82460dc7c3e9f8f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:20:13 -0500 Subject: [PATCH 047/232] Re-use write_sample_scripts in test_install_scripts. Ref pypa/distutils#232. --- distutils/tests/test_build_scripts.py | 3 ++- distutils/tests/test_install_scripts.py | 26 ++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index 8005b81c646..7e05ec5f9a6 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -48,7 +48,8 @@ def get_build_scripts_cmd(self, target, scripts): ) return build_scripts(dist) - def write_sample_scripts(self, dir): + @staticmethod + def write_sample_scripts(dir): spec = { 'script1.py': textwrap.dedent(""" #! /usr/bin/env python2.3 diff --git a/distutils/tests/test_install_scripts.py b/distutils/tests/test_install_scripts.py index 58313f28649..4da2acb6a87 100644 --- a/distutils/tests/test_install_scripts.py +++ b/distutils/tests/test_install_scripts.py @@ -6,6 +6,7 @@ from distutils.core import Distribution from distutils.tests import support +from . import test_build_scripts class TestInstallScripts(support.TempdirManager): @@ -32,31 +33,8 @@ def test_default_settings(self): def test_installation(self): source = self.mkdtemp() - expected = [] - def write_script(name, text): - expected.append(name) - f = open(os.path.join(source, name), "w") - try: - f.write(text) - finally: - f.close() - - write_script( - "script1.py", - ( - "#! /usr/bin/env python2.3\n" - "# bogus script w/ Python sh-bang\n" - "pass\n" - ), - ) - write_script( - "script2.py", - ("#!/usr/bin/python\n" "# bogus script w/ Python sh-bang\n" "pass\n"), - ) - write_script( - "shell.sh", ("#!/bin/sh\n" "# bogus shell script w/ sh-bang\n" "exit 0\n") - ) + expected = test_build_scripts.TestBuildScripts.write_sample_scripts(source) target = self.mkdtemp() dist = Distribution() From 8b7cee81ac5651691a5d92a6fa805f06fa33fb21 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:24:56 -0500 Subject: [PATCH 048/232] Use Path objects in test_register. Ref pypa/distutils#232. --- distutils/tests/test_register.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 5d3826a1b7e..591c5ce0add 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -1,7 +1,8 @@ """Tests for distutils.command.register.""" -import os import getpass +import os +import pathlib import urllib from distutils.command import register as register_module @@ -126,16 +127,8 @@ def test_create_pypirc(self): finally: del register_module.input - # we should have a brand new .pypirc file - assert os.path.exists(self.rc) - - # with the content similar to WANTED_PYPIRC - f = open(self.rc) - try: - content = f.read() - assert content == WANTED_PYPIRC - finally: - f.close() + # A new .pypirc file should contain WANTED_PYPIRC + assert pathlib.Path(self.rc).read_text(encoding='utf-8') == WANTED_PYPIRC # now let's make sure the .pypirc file generated # really works : we shouldn't be asked anything From 5377c3311b5c89cfdd53a044d4ad65688af77802 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:36:10 -0500 Subject: [PATCH 049/232] Specify encoding in test_sdist. Ref pypa/distutils#232. --- distutils/tests/test_sdist.py | 54 ++++++++++------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 00718a37bdc..450f68c9936 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,6 +1,7 @@ """Tests for distutils.command.sdist.""" import os +import pathlib import tarfile import warnings import zipfile @@ -11,6 +12,7 @@ import pytest import path import jaraco.path +from more_itertools import ilen from .py38compat import check_warnings @@ -62,6 +64,11 @@ def project_dir(request, pypirc): yield +def clean_lines(filepath): + with pathlib.Path(filepath).open(encoding='utf-8') as f: + yield from filter(None, map(str.strip, f)) + + class TestSDist(BasePyPIRCCommandTestCase): def get_cmd(self, metadata=None): """Returns a cmd""" @@ -243,11 +250,7 @@ def test_add_defaults(self): assert sorted(content) == ['fake-1.0/' + x for x in expected] # checking the MANIFEST - f = open(join(self.tmp_dir, 'MANIFEST')) - try: - manifest = f.read() - finally: - f.close() + manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8') assert manifest == MANIFEST % {'sep': os.sep} @staticmethod @@ -352,15 +355,7 @@ def test_get_file_list(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert len(manifest) == 5 + assert ilen(clean_lines(cmd.manifest)) == 5 # adding a file self.write_file((self.tmp_dir, 'somecode', 'doc2.txt'), '#') @@ -372,13 +367,7 @@ def test_get_file_list(self): cmd.run() - f = open(cmd.manifest) - try: - manifest2 = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() + manifest2 = list(clean_lines(cmd.manifest)) # do we have the new file in MANIFEST ? assert len(manifest2) == 6 @@ -391,15 +380,10 @@ def test_manifest_marker(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert manifest[0] == '# file GENERATED by distutils, do NOT edit' + assert ( + next(clean_lines(cmd.manifest)) + == '# file GENERATED by distutils, do NOT edit' + ) @pytest.mark.usefixtures('needs_zlib') def test_manifest_comments(self): @@ -434,15 +418,7 @@ def test_manual_manifest(self): cmd.run() assert cmd.filelist.files == ['README.manual'] - f = open(cmd.manifest) - try: - manifest = [ - line.strip() for line in f.read().split('\n') if line.strip() != '' - ] - finally: - f.close() - - assert manifest == ['README.manual'] + assert list(clean_lines(cmd.manifest)) == ['README.manual'] archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') archive = tarfile.open(archive_name) From deb159392d3e925e5d250046c33810b8c7f034e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:44:10 -0500 Subject: [PATCH 050/232] Fix EncodingWarning in test_spawn. Ref pypa/distutils#232. --- distutils/tests/test_spawn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index 57cf1a525ca..ec4c9982ad1 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -54,7 +54,7 @@ def test_find_executable(self, tmp_path): program = program_noeext + ".exe" program_path = tmp_path / program - program_path.write_text("") + program_path.write_text("", encoding='utf-8') program_path.chmod(stat.S_IXUSR) filename = str(program_path) tmp_dir = path.Path(tmp_path) From 433bb4a67460ae2cf130c9f641b515fcda2e827a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:51:23 -0500 Subject: [PATCH 051/232] Fix EncodingWarnings in test_sdist. Ref pypa/distutils#232. --- distutils/tests/test_sysconfig.py | 62 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index f656be6089c..131c1344bbf 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -20,6 +20,11 @@ from . import py37compat +def _gen_makefile(root, contents): + jaraco.path.build({'Makefile': trim(contents)}, root) + return root / 'Makefile' + + @pytest.mark.usefixtures('save_env') class TestSysconfig: def test_get_config_h_filename(self): @@ -167,29 +172,25 @@ def test_customize_compiler(self): assert 'ranlib' not in comp.exes def test_parse_makefile_base(self, tmp_path): - makefile = tmp_path / 'Makefile' - makefile.write_text( - trim( - """ - CONFIG_ARGS= '--arg1=optarg1' 'ENV=LIB' - VAR=$OTHER - OTHER=foo - """ - ) + makefile = _gen_makefile( + tmp_path, + """ + CONFIG_ARGS= '--arg1=optarg1' 'ENV=LIB' + VAR=$OTHER + OTHER=foo + """, ) d = sysconfig.parse_makefile(makefile) assert d == {'CONFIG_ARGS': "'--arg1=optarg1' 'ENV=LIB'", 'OTHER': 'foo'} def test_parse_makefile_literal_dollar(self, tmp_path): - makefile = tmp_path / 'Makefile' - makefile.write_text( - trim( - """ - CONFIG_ARGS= '--arg1=optarg1' 'ENV=\\$$LIB' - VAR=$OTHER - OTHER=foo - """ - ) + makefile = _gen_makefile( + tmp_path, + """ + CONFIG_ARGS= '--arg1=optarg1' 'ENV=\\$$LIB' + VAR=$OTHER + OTHER=foo + """, ) d = sysconfig.parse_makefile(makefile) assert d == {'CONFIG_ARGS': r"'--arg1=optarg1' 'ENV=\$LIB'", 'OTHER': 'foo'} @@ -238,23 +239,24 @@ def test_customize_compiler_before_get_config_vars(self, tmp_path): # Issue #21923: test that a Distribution compiler # instance can be called without an explicit call to # get_config_vars(). - file = tmp_path / 'file' - file.write_text( - trim( - """ - from distutils.core import Distribution - config = Distribution().get_command_obj('config') - # try_compile may pass or it may fail if no compiler - # is found but it should not raise an exception. - rc = config.try_compile('int x;') - """ - ) + jaraco.path.build( + { + 'file': trim(""" + from distutils.core import Distribution + config = Distribution().get_command_obj('config') + # try_compile may pass or it may fail if no compiler + # is found but it should not raise an exception. + rc = config.try_compile('int x;') + """) + }, + tmp_path, ) p = subprocess.Popen( - py37compat.subprocess_args(sys.executable, file), + py37compat.subprocess_args(sys.executable, tmp_path / 'file'), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, + encoding='utf-8', ) outs, errs = p.communicate() assert 0 == p.returncode, "Subprocess failed: " + outs From b6f0ec38c1db2b750b32866ef8a02d5df5a9406c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:54:08 -0500 Subject: [PATCH 052/232] Rely on tree builder. Ref pypa/distutils#232. --- distutils/tests/test_text_file.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py index 4a721b691c0..fe787f44c84 100644 --- a/distutils/tests/test_text_file.py +++ b/distutils/tests/test_text_file.py @@ -1,6 +1,8 @@ """Tests for distutils.text_file.""" -import os +import jaraco.path +import path + from distutils.text_file import TextFile from distutils.tests import support @@ -53,13 +55,9 @@ def test_input(count, description, file, expected_result): result = file.readlines() assert result == expected_result - tmpdir = self.mkdtemp() - filename = os.path.join(tmpdir, "test.txt") - out_file = open(filename, "w") - try: - out_file.write(TEST_DATA) - finally: - out_file.close() + tmp_path = path.Path(self.mkdtemp()) + filename = tmp_path / 'test.txt' + jaraco.path.build({filename.name: TEST_DATA}, tmp_path) in_file = TextFile( filename, strip_comments=0, skip_blanks=0, lstrip_ws=0, rstrip_ws=0 From 826d6fd72e146e2719048003e831de68d64e156b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:00:03 -0500 Subject: [PATCH 053/232] Ran pyupgrade for Python 3.8+ followed by ruff format. --- distutils/bcppcompiler.py | 6 ++---- distutils/ccompiler.py | 4 +--- distutils/cmd.py | 8 +++----- distutils/command/_framework_compat.py | 4 ++-- distutils/command/bdist_rpm.py | 4 ++-- distutils/command/build.py | 2 +- distutils/command/build_ext.py | 2 +- distutils/command/check.py | 2 +- distutils/command/register.py | 2 +- distutils/command/upload.py | 8 +++----- distutils/core.py | 6 +++--- distutils/cygwinccompiler.py | 10 ++++------ distutils/dir_util.py | 10 +++------- distutils/dist.py | 8 ++++---- distutils/fancy_getopt.py | 6 +++--- distutils/file_util.py | 26 ++++++++------------------ distutils/filelist.py | 4 ++-- distutils/msvc9compiler.py | 14 +++++--------- distutils/msvccompiler.py | 6 ++---- distutils/py38compat.py | 2 +- distutils/spawn.py | 8 ++------ distutils/sysconfig.py | 2 +- distutils/tests/test_bdist_dumb.py | 2 +- distutils/tests/test_build.py | 2 +- distutils/tests/test_build_ext.py | 2 +- distutils/tests/test_dir_util.py | 2 +- distutils/tests/test_file_util.py | 4 ++-- distutils/tests/test_version.py | 4 ++-- distutils/util.py | 6 +++--- distutils/version.py | 2 +- 30 files changed, 67 insertions(+), 101 deletions(-) diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index 14d51472f29..d496d5d452d 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -238,7 +238,7 @@ def link( # noqa: C901 def_file = os.path.join(temp_dir, '%s.def' % modname) contents = ['EXPORTS'] for sym in export_symbols or []: - contents.append(' {}=_{}'.format(sym, sym)) + contents.append(f' {sym}=_{sym}') self.execute(write_file, (def_file, contents), "writing %s" % def_file) # Borland C++ has problems with '/' in paths @@ -348,9 +348,7 @@ def object_filenames(self, source_filenames, strip_dir=0, output_dir=''): # use normcase to make sure '.rc' is really '.rc' and not '.RC' (base, ext) = os.path.splitext(os.path.normcase(src_name)) if ext not in (self.src_extensions + ['.rc', '.res']): - raise UnknownFileError( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')") if strip_dir: base = os.path.basename(base) if ext == '.res': diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 67feb164867..6faf546cfe2 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -973,9 +973,7 @@ def _make_out_path(self, output_dir, strip_dir, src_name): try: new_ext = self.out_extensions[ext] except LookupError: - raise UnknownFileError( - "unknown file type '{}' (from '{}')".format(ext, src_name) - ) + 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) diff --git a/distutils/cmd.py b/distutils/cmd.py index 8fdcbc0ea22..8849474cd7d 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -165,7 +165,7 @@ def dump_options(self, header=None, indent=""): if option[-1] == "=": option = option[:-1] value = getattr(self, option) - self.announce(indent + "{} = {}".format(option, value), level=logging.INFO) + self.announce(indent + f"{option} = {value}", level=logging.INFO) def run(self): """A command's raison d'etre: carry out the action it exists to @@ -213,9 +213,7 @@ def _ensure_stringlike(self, option, what, default=None): setattr(self, option, default) return default elif not isinstance(val, str): - raise DistutilsOptionError( - "'{}' must be a {} (got `{}`)".format(option, what, val) - ) + raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)") return val def ensure_string(self, option, default=None): @@ -242,7 +240,7 @@ def ensure_string_list(self, option): ok = False if not ok: raise DistutilsOptionError( - "'{}' must be a list of strings (got {!r})".format(option, val) + f"'{option}' must be a list of strings (got {val!r})" ) def _ensure_tested_string(self, option, tester, what, error_fmt, default=None): diff --git a/distutils/command/_framework_compat.py b/distutils/command/_framework_compat.py index b4228299f4f..397ebf823e4 100644 --- a/distutils/command/_framework_compat.py +++ b/distutils/command/_framework_compat.py @@ -9,7 +9,7 @@ import sysconfig -@functools.lru_cache() +@functools.lru_cache def enabled(): """ Only enabled for Python 3.9 framework homebrew builds @@ -37,7 +37,7 @@ def enabled(): ) -@functools.lru_cache() +@functools.lru_cache def vars(): if not enabled(): return {} diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index e96db22bed7..675bcebdad6 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -487,7 +487,7 @@ def _make_spec_file(self): # noqa: C901 if isinstance(val, list): spec_file.append('{}: {}'.format(field, ' '.join(val))) elif val is not None: - spec_file.append('{}: {}'.format(field, val)) + spec_file.append(f'{field}: {val}') if self.distribution.get_url(): spec_file.append('Url: ' + self.distribution.get_url()) @@ -522,7 +522,7 @@ def _make_spec_file(self): # noqa: C901 # rpm scripts # figure out default build script - def_setup_call = "{} {}".format(self.python, os.path.basename(sys.argv[0])) + def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}" def_build = "%s build" % def_setup_call if self.use_rpm_opt_flags: def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build diff --git a/distutils/command/build.py b/distutils/command/build.py index cc9b367ef92..d8704e35838 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -78,7 +78,7 @@ def finalize_options(self): # noqa: C901 "using './configure --help' on your platform)" ) - plat_specifier = ".{}-{}".format(self.plat_name, sys.implementation.cache_tag) + plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}" # Make it so Python 2.x and Python 2.x with --with-pydebug don't # share the same build directories. Doing so confuses the build diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index ba6580c71ee..a15781f28a2 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -515,7 +515,7 @@ def _filter_build_errors(self, ext): except (CCompilerError, DistutilsError, CompileError) as e: if not ext.optional: raise - self.warn('building extension "{}" failed: {}'.format(ext.name, e)) + self.warn(f'building extension "{ext.name}" failed: {e}') def build_extension(self, ext): sources = ext.sources diff --git a/distutils/command/check.py b/distutils/command/check.py index b59cc237312..28f55fb9142 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -116,7 +116,7 @@ def check_restructuredtext(self): if line is None: warning = warning[1] else: - warning = '{} (line {})'.format(warning[1], line) + warning = f'{warning[1]} (line {line})' self.warn(warning) def _check_rst_data(self, data): diff --git a/distutils/command/register.py b/distutils/command/register.py index cf1afc8c1f1..5a24246ccba 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -174,7 +174,7 @@ def send_metadata(self): # noqa: C901 auth.add_password(self.realm, host, username, password) # send the info to the server and report the result code, result = self.post_to_server(self.build_post_data('submit'), auth) - self.announce('Server response ({}): {}'.format(code, result), logging.INFO) + self.announce(f'Server response ({code}): {result}', logging.INFO) # possibly save the login if code == 200: diff --git a/distutils/command/upload.py b/distutils/command/upload.py index caf15f04a60..a9124f2b718 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -169,7 +169,7 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 body.write(end_boundary) body = body.getvalue() - msg = "Submitting {} to {}".format(filename, self.repository) + msg = f"Submitting {filename} to {self.repository}" self.announce(msg, logging.INFO) # build the Request @@ -193,14 +193,12 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 raise if status == 200: - self.announce( - 'Server response ({}): {}'.format(status, reason), logging.INFO - ) + self.announce(f'Server response ({status}): {reason}', logging.INFO) if self.show_response: text = self._read_pypi_response(result) msg = '\n'.join(('-' * 75, text, '-' * 75)) self.announce(msg, logging.INFO) else: - msg = 'Upload failed ({}): {}'.format(status, reason) + msg = f'Upload failed ({status}): {reason}' self.announce(msg, logging.ERROR) raise DistutilsError(msg) diff --git a/distutils/core.py b/distutils/core.py index 05d2971994d..799de9489ce 100644 --- a/distutils/core.py +++ b/distutils/core.py @@ -203,10 +203,10 @@ def run_commands(dist): raise SystemExit("interrupted") except OSError as exc: if DEBUG: - sys.stderr.write("error: {}\n".format(exc)) + sys.stderr.write(f"error: {exc}\n") raise else: - raise SystemExit("error: {}".format(exc)) + raise SystemExit(f"error: {exc}") except (DistutilsError, CCompilerError) as msg: if DEBUG: @@ -249,7 +249,7 @@ def run_setup(script_name, script_args=None, stop_after="run"): used to drive the Distutils. """ if stop_after not in ('init', 'config', 'commandline', 'run'): - raise ValueError("invalid value for 'stop_after': {!r}".format(stop_after)) + raise ValueError(f"invalid value for 'stop_after': {stop_after!r}") global _setup_stop_after, _setup_distribution _setup_stop_after = stop_after diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index b3dbc3be15c..84151b7eb9c 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -87,9 +87,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): super().__init__(verbose, dry_run, force) status, details = check_config_h() - self.debug_print( - "Python's GCC status: {} (details: {})".format(status, details) - ) + self.debug_print(f"Python's GCC status: {status} (details: {details})") if status is not CONFIG_H_OK: self.warn( "Python's pyconfig.h doesn't seem to support your compiler. " @@ -108,7 +106,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mcygwin -mdll -O -Wall' % self.cc, compiler_cxx='%s -mcygwin -O -Wall' % self.cxx, linker_exe='%s -mcygwin' % self.cc, - linker_so=('{} -mcygwin {}'.format(self.linker_dll, shared_option)), + linker_so=(f'{self.linker_dll} -mcygwin {shared_option}'), ) # Include the appropriate MSVC runtime library if Python was built @@ -280,7 +278,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): compiler_so='%s -mdll -O -Wall' % self.cc, compiler_cxx='%s -O -Wall' % self.cxx, linker_exe='%s' % self.cc, - linker_so='{} {}'.format(self.linker_dll, shared_option), + linker_so=f'{self.linker_dll} {shared_option}', ) def runtime_library_dir_option(self, dir): @@ -340,7 +338,7 @@ def check_config_h(): finally: config_h.close() except OSError as exc: - return (CONFIG_H_UNCERTAIN, "couldn't read '{}': {}".format(fn, exc.strerror)) + return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}") def is_cygwincc(cc): diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 23dc3392a2c..819fe56f6db 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -33,9 +33,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 # Detect a common bug -- name is None if not isinstance(name, str): - raise DistutilsInternalError( - "mkpath: 'name' must be a string (got {!r})".format(name) - ) + raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})") # XXX what's the better way to handle verbosity? print as we create # each directory in the path (the current behaviour), or only announce @@ -76,7 +74,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C901 except OSError as exc: if not (exc.errno == errno.EEXIST and os.path.isdir(head)): raise DistutilsFileError( - "could not create '{}': {}".format(head, exc.args[-1]) + f"could not create '{head}': {exc.args[-1]}" ) created_dirs.append(head) @@ -143,9 +141,7 @@ def copy_tree( # noqa: C901 if dry_run: names = [] else: - raise DistutilsFileError( - "error listing files in '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}") if not dry_run: mkpath(dst, verbose=verbose) diff --git a/distutils/dist.py b/distutils/dist.py index 7c0f0e5b78c..659583943b8 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -821,7 +821,7 @@ def get_command_class(self, command): return klass for pkgname in self.get_command_packages(): - module_name = "{}.{}".format(pkgname, command) + module_name = f"{pkgname}.{command}" klass_name = command try: @@ -889,7 +889,7 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 self.announce(" setting options for '%s' command:" % command_name) for option, (source, value) in option_dict.items(): if DEBUG: - self.announce(" {} = {} (from {})".format(option, value, source)) + self.announce(f" {option} = {value} (from {source})") try: bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] except AttributeError: @@ -1178,7 +1178,7 @@ def maybe_write(header, val): def _write_list(self, file, name, values): values = values or [] for value in values: - file.write('{}: {}\n'.format(name, value)) + file.write(f'{name}: {value}\n') # -- Metadata query methods ---------------------------------------- @@ -1189,7 +1189,7 @@ def get_version(self): return self.version or "0.0.0" def get_fullname(self): - return "{}-{}".format(self.get_name(), self.get_version()) + return f"{self.get_name()}-{self.get_version()}" def get_author(self): return self.author diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index 3b887dc5a41..c025f12062c 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -22,7 +22,7 @@ longopt_re = re.compile(r'^%s$' % longopt_pat) # For recognizing "negative alias" options, eg. "quiet=!verbose" -neg_alias_re = re.compile("^({})=!({})$".format(longopt_pat, longopt_pat)) +neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$") # This is used to translate long options to legitimate Python identifiers # (for use as attributes of some object). @@ -157,7 +157,7 @@ def _grok_option_table(self): # noqa: C901 else: # the option table is part of the code, so simply # assert that it is correct - raise ValueError("invalid option tuple: {!r}".format(option)) + raise ValueError(f"invalid option tuple: {option!r}") # Type- and value-check the option names if not isinstance(long, str) or len(long) < 2: @@ -359,7 +359,7 @@ def generate_help(self, header=None): # noqa: C901 # Case 2: we have a short option, so we have to include it # just after the long option else: - opt_names = "{} (-{})".format(long, short) + opt_names = f"{long} (-{short})" if text: lines.append(" --%-*s %s" % (max_opt, opt_names, text[0])) else: diff --git a/distutils/file_util.py b/distutils/file_util.py index 3f3e21b5673..8ebd2a790f6 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -26,30 +26,24 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fsrc = open(src, 'rb') except OSError as e: - raise DistutilsFileError("could not open '{}': {}".format(src, e.strerror)) + raise DistutilsFileError(f"could not open '{src}': {e.strerror}") if os.path.exists(dst): try: os.unlink(dst) except OSError as e: - raise DistutilsFileError( - "could not delete '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}") try: fdst = open(dst, 'wb') except OSError as e: - raise DistutilsFileError( - "could not create '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not create '{dst}': {e.strerror}") while True: try: buf = fsrc.read(buffer_size) except OSError as e: - raise DistutilsFileError( - "could not read from '{}': {}".format(src, e.strerror) - ) + raise DistutilsFileError(f"could not read from '{src}': {e.strerror}") if not buf: break @@ -57,9 +51,7 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901 try: fdst.write(buf) except OSError as e: - raise DistutilsFileError( - "could not write to '{}': {}".format(dst, e.strerror) - ) + raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}") finally: if fdst: fdst.close() @@ -199,12 +191,12 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 dst = os.path.join(dst, basename(src)) elif exists(dst): raise DistutilsFileError( - "can't move '{}': destination '{}' already exists".format(src, dst) + f"can't move '{src}': destination '{dst}' already exists" ) if not isdir(dirname(dst)): raise DistutilsFileError( - "can't move '{}': destination '{}' not a valid path".format(src, dst) + f"can't move '{src}': destination '{dst}' not a valid path" ) copy_it = False @@ -215,9 +207,7 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 if num == errno.EXDEV: copy_it = True else: - raise DistutilsFileError( - "couldn't move '{}' to '{}': {}".format(src, dst, msg) - ) + raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}") if copy_it: copy_file(src, dst, verbose=verbose) diff --git a/distutils/filelist.py b/distutils/filelist.py index 6dadf923d71..3205762654d 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -363,9 +363,9 @@ def translate_pattern(pattern, anchor=1, prefix=None, is_regex=0): if os.sep == '\\': sep = r'\\' pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)] - pattern_re = r'{}\A{}{}.*{}{}'.format(start, prefix_re, sep, pattern_re, end) + pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}' else: # no prefix -- respect anchor flag if anchor: - pattern_re = r'{}\A{}'.format(start, pattern_re[len(start) :]) + pattern_re = rf'{start}\A{pattern_re[len(start) :]}' return re.compile(pattern_re) diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index 724986d89d0..402c0c0620e 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -175,7 +175,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = Reg.get_value(base, r"{}\{}".format(p, key)) + d = Reg.get_value(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -281,7 +281,7 @@ def query_vcvarsall(version, arch="x86"): raise DistutilsPlatformError("Unable to find vcvarsall.bat") log.debug("Calling 'vcvarsall.bat %s' (version=%s)", arch, version) popen = subprocess.Popen( - '"{}" {} & set'.format(vcvarsall, arch), + f'"{vcvarsall}" {arch} & set', stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) @@ -370,9 +370,7 @@ def initialize(self, plat_name=None): # noqa: C901 # sanity check for platforms to prevent obscure errors later. ok_plats = 'win32', 'win-amd64' if plat_name not in ok_plats: - raise DistutilsPlatformError( - "--plat-name must be one of {}".format(ok_plats) - ) + raise DistutilsPlatformError(f"--plat-name must be one of {ok_plats}") if ( "DISTUTILS_USE_SDK" in os.environ @@ -564,9 +562,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: @@ -687,7 +683,7 @@ def link( # noqa: C901 mfinfo = self.manifest_get_embed_info(target_desc, ld_args) if mfinfo is not None: mffilename, mfid = mfinfo - out_arg = '-outputresource:{};{}'.format(output_filename, mfid) + out_arg = f'-outputresource:{output_filename};{mfid}' try: self.spawn(['mt.exe', '-nologo', '-manifest', mffilename, out_arg]) except DistutilsExecError as msg: diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index c3823e257ef..1a07746bc70 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -159,7 +159,7 @@ def load_macros(self, version): except RegError: continue key = RegEnumKey(h, 0) - d = read_values(base, r"{}\{}".format(p, key)) + d = read_values(base, rf"{p}\{key}") self.macros["$(FrameworkVersion)"] = d["version"] def sub(self, s): @@ -454,9 +454,7 @@ def compile( # noqa: C901 continue else: # how to handle this file? - raise CompileError( - "Don't know how to compile {} to {}".format(src, obj) - ) + raise CompileError(f"Don't know how to compile {src} to {obj}") output_opt = "/Fo" + obj try: diff --git a/distutils/py38compat.py b/distutils/py38compat.py index 59224e71e50..ab12119fa5f 100644 --- a/distutils/py38compat.py +++ b/distutils/py38compat.py @@ -5,4 +5,4 @@ def aix_platform(osname, version, release): return _aix_support.aix_platform() except ImportError: pass - return "{}-{}.{}".format(osname, version, release) + return f"{osname}-{version}.{release}" diff --git a/distutils/spawn.py b/distutils/spawn.py index afefe525ef1..48adceb1146 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -60,16 +60,12 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 except OSError as exc: if not DEBUG: cmd = cmd[0] - raise DistutilsExecError( - "command {!r} failed: {}".format(cmd, exc.args[-1]) - ) from exc + raise DistutilsExecError(f"command {cmd!r} failed: {exc.args[-1]}") from exc if exitcode: if not DEBUG: cmd = cmd[0] - raise DistutilsExecError( - "command {!r} failed with exit code {}".format(cmd, exitcode) - ) + raise DistutilsExecError(f"command {cmd!r} failed with exit code {exitcode}") def find_executable(executable, path=None): diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 5fb811c406e..40215b83478 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -267,7 +267,7 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): ) -@functools.lru_cache() +@functools.lru_cache def _customize_macos(): """ Perform first-time customization of compiler-related diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index 95532e83b95..cb4db4e1924 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -61,7 +61,7 @@ def test_simple_built(self): # see what we have dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) - base = "{}.{}.zip".format(dist.get_fullname(), cmd.plat_name) + base = f"{dist.get_fullname()}.{cmd.plat_name}.zip" assert dist_created == [base] diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index c2cff445233..8617fa99197 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -24,7 +24,7 @@ def test_finalize_options(self): # build_platlib is 'build/lib.platform-cache_tag[-pydebug]' # examples: # build/lib.macosx-10.3-i386-cpython39 - plat_spec = '.{}-{}'.format(cmd.plat_name, sys.implementation.cache_tag) + plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}' if hasattr(sys, 'gettotalrefcount'): assert cmd.build_platlib.endswith('-pydebug') plat_spec += '-pydebug' diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 537959fed66..da4663076b5 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -535,7 +535,7 @@ def _try_compile_deployment_target(self, operator, target): deptarget_ext = Extension( 'deptarget', [deptarget_c], - extra_compile_args=['-DTARGET={}'.format(target)], + extra_compile_args=[f'-DTARGET={target}'], ) dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]}) dist.package_dir = self.tmp_dir diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 0738b7c8770..e7d69bb6ef1 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -75,7 +75,7 @@ def test_copy_tree_verbosity(self, caplog): with open(a_file, 'w') as f: f.write('some content') - wanted = ['copying {} -> {}'.format(a_file, self.target2)] + wanted = [f'copying {a_file} -> {self.target2}'] copy_tree(self.target, self.target2, verbose=1) assert caplog.messages == wanted diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 3b9f82b71e9..e441186e3a9 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -35,7 +35,7 @@ def test_move_file_verbosity(self, caplog): move_file(self.target, self.source, verbose=0) move_file(self.source, self.target, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target)] + wanted = [f'moving {self.source} -> {self.target}'] assert caplog.messages == wanted # back to original state @@ -45,7 +45,7 @@ def test_move_file_verbosity(self, caplog): # now the target is a dir os.mkdir(self.target_dir) move_file(self.source, self.target_dir, verbose=1) - wanted = ['moving {} -> {}'.format(self.source, self.target_dir)] + wanted = [f'moving {self.source} -> {self.target_dir}'] assert caplog.messages == wanted def test_move_file_exception_unpacking_rename(self): diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 900edafa7c8..0aaf0a534cd 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -62,7 +62,7 @@ def test_cmp_strict(self): res = StrictVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' def test_cmp(self): versions = ( @@ -88,4 +88,4 @@ def test_cmp(self): res = LooseVersion(v1)._cmp(object()) assert ( res is NotImplemented - ), 'cmp({}, {}) should be NotImplemented, got {}'.format(v1, v2, res) + ), f'cmp({v1}, {v2}) should be NotImplemented, got {res}' diff --git a/distutils/util.py b/distutils/util.py index 5408b16032e..a2ba1fc9616 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -172,7 +172,7 @@ def change_root(new_root, pathname): raise DistutilsPlatformError(f"nothing known about platform '{os.name}'") -@functools.lru_cache() +@functools.lru_cache def check_environ(): """Ensure that 'os.environ' has all the environment variables we guarantee that users can use in config files, command-line options, @@ -328,7 +328,7 @@ def execute(func, args, msg=None, verbose=0, dry_run=0): print. """ if msg is None: - msg = "{}{!r}".format(func.__name__, args) + msg = f"{func.__name__}{args!r}" if msg[-2:] == ',)': # correct for singleton tuple msg = msg[0:-2] + ')' @@ -350,7 +350,7 @@ def strtobool(val): elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 else: - raise ValueError("invalid truth value {!r}".format(val)) + raise ValueError(f"invalid truth value {val!r}") def byte_compile( # noqa: C901 diff --git a/distutils/version.py b/distutils/version.py index 18385cfef2d..8ab76ddef4e 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -60,7 +60,7 @@ def __init__(self, vstring=None): ) def __repr__(self): - return "{} ('{}')".format(self.__class__.__name__, str(self)) + return f"{self.__class__.__name__} ('{str(self)}')" def __eq__(self, other): c = self._cmp(other) From 592b0d80d781369a2c622ccc73fb8f48ba906f5b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:05:19 -0500 Subject: [PATCH 054/232] Suppress diffcov error. --- distutils/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/util.py b/distutils/util.py index c26e61ab4a9..bfd30700fa9 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -424,7 +424,7 @@ def byte_compile( # noqa: C901 if not dry_run: if script_fd is not None: script = os.fdopen(script_fd, "w", encoding='utf-8') - else: + else: # pragma: no cover script = open(script_name, "w", encoding='utf-8') with script: From 7a7531b9addbf7fc46280d8d4a629f98c193b01d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:25:25 -0500 Subject: [PATCH 055/232] Suppress more diffcov errors. --- distutils/tests/test_build_ext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 4ae81a22e4b..ae66bc4eb84 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -479,7 +479,7 @@ def test_deployment_target_too_low(self): @pytest.mark.skipif('platform.system() != "Darwin"') @pytest.mark.usefixtures('save_env') - def test_deployment_target_higher_ok(self): + def test_deployment_target_higher_ok(self): # pragma: no cover # Issue 9516: Test that an extension module can be compiled with a # deployment target higher than that of the interpreter: the ext # module may depend on some newer OS feature. @@ -491,7 +491,7 @@ def test_deployment_target_higher_ok(self): deptarget = '.'.join(str(i) for i in deptarget) self._try_compile_deployment_target('<', deptarget) - def _try_compile_deployment_target(self, operator, target): + def _try_compile_deployment_target(self, operator, target): # pragma: no cover if target is None: if os.environ.get('MACOSX_DEPLOYMENT_TARGET'): del os.environ['MACOSX_DEPLOYMENT_TARGET'] From 4fd512859b234179879cd9a213bd6288363ff26f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:31:22 -0500 Subject: [PATCH 056/232] Address EncodingWarning in ccompiler. Ref pypa/distutils#232. --- distutils/ccompiler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 6faf546cfe2..bcf9580c7af 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -858,8 +858,7 @@ def has_function( # noqa: C901 if library_dirs is None: library_dirs = [] fd, fname = tempfile.mkstemp(".c", funcname, text=True) - f = os.fdopen(fd, "w") - try: + with os.fdopen(fd, "w", encoding='utf-8') as f: for incl in includes: f.write("""#include "%s"\n""" % incl) if not includes: @@ -888,8 +887,7 @@ def has_function( # noqa: C901 """ % funcname ) - finally: - f.close() + try: objects = self.compile([fname], include_dirs=include_dirs) except CompileError: From 03ec237712b26d926362a349f837f9cc65e3b547 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:38:01 -0500 Subject: [PATCH 057/232] Fix EncodingWarnings in distutils/command/config.py. Ref pypa/distutils#232. --- distutils/command/config.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/distutils/command/config.py b/distutils/command/config.py index 494d97d16f6..573741d7726 100644 --- a/distutils/command/config.py +++ b/distutils/command/config.py @@ -10,6 +10,7 @@ """ import os +import pathlib import re from ..core import Command @@ -102,7 +103,7 @@ def _check_compiler(self): def _gen_temp_sourcefile(self, body, headers, lang): filename = "_configtest" + LANG_EXT[lang] - with open(filename, "w") as file: + with open(filename, "w", encoding='utf-8') as file: if headers: for header in headers: file.write("#include <%s>\n" % header) @@ -199,15 +200,8 @@ def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang=" if isinstance(pattern, str): pattern = re.compile(pattern) - with open(out) as file: - match = False - while True: - line = file.readline() - if line == '': - break - if pattern.search(line): - match = True - break + with open(out, encoding='utf-8') as file: + match = any(pattern.search(line) for line in file) self._clean() return match @@ -369,8 +363,4 @@ def dump_file(filename, head=None): log.info('%s', filename) else: log.info(head) - file = open(filename) - try: - log.info(file.read()) - finally: - file.close() + log.info(pathlib.Path(filename).read_text(encoding='utf-8')) From b894d6f341b626b289c4d50dc00909606d1bd164 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:40:06 -0500 Subject: [PATCH 058/232] Fix EncodingWarnings in distutils/config.py. Ref pypa/distutils#232. --- distutils/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distutils/config.py b/distutils/config.py index a55951ed7cf..f92ecb9638d 100644 --- a/distutils/config.py +++ b/distutils/config.py @@ -42,7 +42,8 @@ def _get_rc_file(self): def _store_pypirc(self, username, password): """Creates a default .pypirc file.""" rc = self._get_rc_file() - with os.fdopen(os.open(rc, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: + raw = os.open(rc, os.O_CREAT | os.O_WRONLY, 0o600) + with os.fdopen(raw, 'w', encoding='utf-8') as f: f.write(DEFAULT_PYPIRC % (username, password)) def _read_pypirc(self): # noqa: C901 @@ -53,7 +54,7 @@ def _read_pypirc(self): # noqa: C901 repository = self.repository or self.DEFAULT_REPOSITORY config = RawConfigParser() - config.read(rc) + config.read(rc, encoding='utf-8') sections = config.sections() if 'distutils' in sections: # let's get the list of servers From f0692cf4ccdec21debcfef57202f4af97043f135 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:47:35 -0500 Subject: [PATCH 059/232] Fix EncodingWarnings in sdist.py. Ref pypa/distutils#232. --- distutils/command/sdist.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index ac489726cae..b76cb9bc73e 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -6,6 +6,7 @@ import sys from glob import glob from warnings import warn +from itertools import filterfalse from ..core import Command from distutils import dir_util @@ -429,11 +430,8 @@ def _manifest_is_not_generated(self): if not os.path.isfile(self.manifest): return False - fp = open(self.manifest) - try: - first_line = fp.readline() - finally: - fp.close() + with open(self.manifest, encoding='utf-8') as fp: + first_line = next(fp) return first_line != '# file GENERATED by distutils, do NOT edit\n' def read_manifest(self): @@ -442,13 +440,11 @@ def read_manifest(self): distribution. """ log.info("reading manifest file '%s'", self.manifest) - with open(self.manifest) as manifest: - for line in manifest: + with open(self.manifest, encoding='utf-8') as lines: + self.filelist.extend( # ignore comments and blank lines - line = line.strip() - if line.startswith('#') or not line: - continue - self.filelist.append(line) + filter(None, filterfalse(is_comment, map(str.strip, lines))) + ) def make_release_tree(self, base_dir, files): """Create the directory tree that will become the source @@ -528,3 +524,7 @@ def get_archive_files(self): was run, or None if the command hasn't run yet. """ return self.archive_files + + +def is_comment(line): + return line.startswith('#') From b420f2dd8ed44251faa2880e791c113f8ea7823c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:49:07 -0500 Subject: [PATCH 060/232] Fix EncodingWarnings in text_file.py. Ref pypa/distutils#232. --- distutils/text_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/text_file.py b/distutils/text_file.py index 36f947e51c8..6f90cfe21d8 100644 --- a/distutils/text_file.py +++ b/distutils/text_file.py @@ -115,7 +115,7 @@ def open(self, filename): """Open a new file named 'filename'. This overrides both the 'filename' and 'file' arguments to the constructor.""" self.filename = filename - self.file = open(self.filename, errors=self.errors) + self.file = open(self.filename, errors=self.errors, encoding='utf-8') self.current_line = 0 def close(self): From 559a4f355fadc8017a9ebdf31afed06ce4e03445 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:50:06 -0500 Subject: [PATCH 061/232] Fix EncodingWarnings in dist.py. Ref pypa/distutils#232. --- distutils/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/dist.py b/distutils/dist.py index 659583943b8..c4d2a45dc26 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -395,7 +395,7 @@ def parse_config_files(self, filenames=None): # noqa: C901 for filename in filenames: if DEBUG: self.announce(" reading %s" % filename) - parser.read(filename) + parser.read(filename, encoding='utf-8') for section in parser.sections(): options = parser.options(section) opt_dict = self.get_option_dict(section) From 61d103fba380d5e56a4081b11a6680a4a0ba319a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 15:59:16 -0500 Subject: [PATCH 062/232] Fix EncodingWarning in cygwinccompiler. Ref pypa/distutils#232. --- distutils/cygwinccompiler.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 84151b7eb9c..20609504154 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -7,6 +7,7 @@ """ import os +import pathlib import re import sys import copy @@ -329,14 +330,15 @@ def check_config_h(): # let's see if __GNUC__ is mentioned in python.h fn = sysconfig.get_config_h_filename() try: - config_h = open(fn) - try: - if "__GNUC__" in config_h.read(): - return CONFIG_H_OK, "'%s' mentions '__GNUC__'" % fn - else: - return CONFIG_H_NOTOK, "'%s' does not mention '__GNUC__'" % fn - finally: - config_h.close() + config_h = pathlib.Path(fn).read_text(encoding='utf-8') + substring = '__GNUC__' + if substring in config_h: + code = CONFIG_H_OK + mention_inflected = 'mentions' + else: + code = CONFIG_H_NOTOK + mention_inflected = 'does not mention' + return code, f"{fn!r} {mention_inflected} {substring!r}" except OSError as exc: return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}") From 2b93ccc7e3b7561ef90bac952f52de33ad46735e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 16:06:30 -0500 Subject: [PATCH 063/232] Fix EncodingWarning in file_util. Ref pypa/distutils#232. --- distutils/file_util.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/distutils/file_util.py b/distutils/file_util.py index 8ebd2a790f6..0eb9b86107f 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -230,9 +230,5 @@ def write_file(filename, contents): """Create a file with the specified name and write 'contents' (a sequence of strings without line terminators) to it. """ - f = open(filename, "w") - try: - for line in contents: - f.write(line + "\n") - finally: - f.close() + with open(filename, 'w', encoding='utf-8') as f: + f.writelines(line + '\n' for line in contents) From 9508489953a84a1412ad24e6613650351369462c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 16:10:53 -0500 Subject: [PATCH 064/232] Suppress EncodingWarnings in pyfakefs. Ref pypa/distutils#232. Workaround for pytest-dev/pyfakefs#957. --- pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytest.ini b/pytest.ini index 3ee2f886ba5..42820fc7ed4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -34,3 +34,7 @@ filterwarnings= # suppress well know deprecation warning ignore:distutils.log.Log is deprecated + + # pytest-dev/pyfakefs#957 + ignore:UTF-8 Mode affects locale.getpreferredencoding::pyfakefs.fake_file + ignore:'encoding' argument not specified::pyfakefs.helpers From 57d567de0ab8798d418e0b2e48d4048bb86713b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 16:19:04 -0500 Subject: [PATCH 065/232] Replaced deprecated cgi module with email module. Ref pypa/distutils#232. --- distutils/config.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/distutils/config.py b/distutils/config.py index f92ecb9638d..e0defd77e6f 100644 --- a/distutils/config.py +++ b/distutils/config.py @@ -5,6 +5,7 @@ """ import os +import email.message from configparser import RawConfigParser from .cmd import Command @@ -121,11 +122,8 @@ def _read_pypirc(self): # noqa: C901 def _read_pypi_response(self, response): """Read and decode a PyPI HTTP response.""" - import cgi - content_type = response.getheader('content-type', 'text/plain') - encoding = cgi.parse_header(content_type)[1].get('charset', 'ascii') - return response.read().decode(encoding) + return response.read().decode(_extract_encoding(content_type)) def initialize_options(self): """Initialize options.""" @@ -139,3 +137,15 @@ def finalize_options(self): self.repository = self.DEFAULT_REPOSITORY if self.realm is None: self.realm = self.DEFAULT_REALM + + +def _extract_encoding(content_type): + """ + >>> _extract_encoding('text/plain') + 'ascii' + >>> _extract_encoding('text/html; charset="utf8"') + 'utf8' + """ + msg = email.message.EmailMessage() + msg['content-type'] = content_type + return msg['content-type'].params.get('charset', 'ascii') From 3ff7b64b324cdbf7a12dd406b9bdddcf4add860e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 20:05:30 -0500 Subject: [PATCH 066/232] Fix exception reference in missing_compiler_executable. Ref pypa/distutils#225. Closes pypa/distutils#238. --- distutils/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index aad8edb242d..6d9b853215a 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -26,7 +26,7 @@ def missing_compiler_executable(cmd_names=[]): # pragma: no cover # MSVC has no executables, so check whether initialization succeeds try: compiler.initialize() - except errors.PlatformError: + except errors.DistutilsPlatformError: return "msvc" for name in compiler.executables: if cmd_names and name not in cmd_names: From 38b58a5b3fc343aebdb08f46089049780de4dc44 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Mar 2024 06:21:04 -0500 Subject: [PATCH 067/232] Satisfy EncodingWarning by passing the encoding. --- distutils/tests/test_dist.py | 2 +- pytest.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index fe979efed5b..8e52873dced 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -257,7 +257,7 @@ def test_find_config_files_permission_error(self, fake_home): """ Finding config files should not fail when directory is inaccessible. """ - fake_home.joinpath(pydistutils_cfg).write_text('') + fake_home.joinpath(pydistutils_cfg).write_text('', encoding='utf-8') fake_home.chmod(0o000) Distribution().find_config_files() diff --git a/pytest.ini b/pytest.ini index 42820fc7ed4..fa31fb33dc0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -37,4 +37,3 @@ filterwarnings= # pytest-dev/pyfakefs#957 ignore:UTF-8 Mode affects locale.getpreferredencoding::pyfakefs.fake_file - ignore:'encoding' argument not specified::pyfakefs.helpers From 03166bcd5d86426ef055d147697dea1c9a9215e9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 15:05:39 +0000 Subject: [PATCH 068/232] Add compat.py39.LOCALE_ENCODING --- setuptools/compat/py39.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 setuptools/compat/py39.py diff --git a/setuptools/compat/py39.py b/setuptools/compat/py39.py new file mode 100644 index 00000000000..04a4abe5a9e --- /dev/null +++ b/setuptools/compat/py39.py @@ -0,0 +1,9 @@ +import sys + +# Explicitly use the ``"locale"`` encoding in versions that support it, +# otherwise just rely on the implicit handling of ``encoding=None``. +# Since all platforms that support ``EncodingWarning`` also support +# ``encoding="locale"``, this can be used to suppress the warning. +# However, please try to use UTF-8 when possible +# (.pth files are the notorious exception: python/cpython#77102, pypa/setuptools#3937). +LOCALE_ENCODING = "locale" if sys.version_info >= (3, 10) else None From ff99234609151de8abdfbe1d97e41071f93964ce Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 09:11:21 +0000 Subject: [PATCH 069/232] Re-use compat.py39.LOCALE_ENCODING in editable_wheel --- setuptools/command/editable_wheel.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 9d319398c91..5f08ab53fca 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -14,7 +14,6 @@ import io import os import shutil -import sys import traceback from contextlib import suppress from enum import Enum @@ -44,6 +43,7 @@ namespaces, ) from .._path import StrPath +from ..compat import py39 from ..discovery import find_package_path from ..dist import Distribution from ..warnings import ( @@ -558,9 +558,8 @@ def _encode_pth(content: str) -> bytes: (There seems to be some variety in the way different version of Python handle ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``). """ - encoding = "locale" if sys.version_info >= (3, 10) else None with io.BytesIO() as buffer: - wrapper = io.TextIOWrapper(buffer, encoding) + wrapper = io.TextIOWrapper(buffer, encoding=py39.LOCALE_ENCODING) wrapper.write(content) wrapper.flush() buffer.seek(0) From 76ac799acfbb4bec9fec0815d282c444eb92f49f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 09:44:05 +0000 Subject: [PATCH 070/232] Explicitly use 'locale' encoding for .pth files in easy_install --- setuptools/command/easy_install.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 402355bd816..c256770239e 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -74,7 +74,7 @@ DEVELOP_DIST, ) import pkg_resources -from ..compat import py311 +from ..compat import py39, py311 from .._path import ensure_directory from ..extern.jaraco.text import yield_lines @@ -491,7 +491,7 @@ def check_site_dir(self): # noqa: C901 # is too complex (12) # FIXME try: if test_exists: os.unlink(testfile) - open(testfile, 'w').close() + open(testfile, 'wb').close() os.unlink(testfile) except OSError: self.cant_write_to_target() @@ -576,7 +576,7 @@ def check_pth_processing(self): _one_liner( """ import os - f = open({ok_file!r}, 'w') + f = open({ok_file!r}, 'w', encoding="utf-8") f.write('OK') f.close() """ @@ -588,7 +588,8 @@ def check_pth_processing(self): os.unlink(ok_file) dirname = os.path.dirname(ok_file) os.makedirs(dirname, exist_ok=True) - f = open(pth_file, 'w') + f = open(pth_file, 'w', encoding=py39.LOCALE_ENCODING) + # ^-- Requires encoding="locale" instead of "utf-8" (python/cpython#77102). except OSError: self.cant_write_to_target() else: @@ -872,7 +873,7 @@ def write_script(self, script_name, contents, mode="t", blockers=()): ensure_directory(target) if os.path.exists(target): os.unlink(target) - with open(target, "w" + mode) as f: + with open(target, "w" + mode) as f: # TODO: is it safe to use "utf-8"? f.write(contents) chmod(target, 0o777 - mask) @@ -1016,7 +1017,7 @@ def install_exe(self, dist_filename, tmpdir): # Write EGG-INFO/PKG-INFO if not os.path.exists(pkg_inf): - f = open(pkg_inf, 'w') + f = open(pkg_inf, 'w') # TODO: probably it is safe to use "utf-8" f.write('Metadata-Version: 1.0\n') for k, v in cfg.items('metadata'): if k != 'target_version': @@ -1277,7 +1278,9 @@ def update_pth(self, dist): # noqa: C901 # is too complex (11) # FIXME filename = os.path.join(self.install_dir, 'setuptools.pth') if os.path.islink(filename): os.unlink(filename) - with open(filename, 'wt') as f: + + with open(filename, 'wt', encoding=py39.LOCALE_ENCODING) as f: + # Requires encoding="locale" instead of "utf-8" (python/cpython#77102). f.write(self.pth_file.make_relative(dist.location) + '\n') def unpack_progress(self, src, dst): @@ -1503,9 +1506,9 @@ def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME continue # Read the .pth file - f = open(os.path.join(dirname, name)) - lines = list(yield_lines(f)) - f.close() + with open(os.path.join(dirname, name), encoding=py39.LOCALE_ENCODING) as f: + # Requires encoding="locale" instead of "utf-8" (python/cpython#77102). + lines = list(yield_lines(f)) # Yield existing non-dupe, non-import directory lines from it for line in lines: @@ -1619,7 +1622,8 @@ def _load_raw(self): paths = [] dirty = saw_import = False seen = dict.fromkeys(self.sitedirs) - f = open(self.filename, 'rt') + f = open(self.filename, 'rt', encoding=py39.LOCALE_ENCODING) + # ^-- Requires encoding="locale" instead of "utf-8" (python/cpython#77102). for line in f: path = line.rstrip() # still keep imports and empty/commented lines for formatting @@ -1690,7 +1694,8 @@ def save(self): data = '\n'.join(lines) + '\n' if os.path.islink(self.filename): os.unlink(self.filename) - with open(self.filename, 'wt') as f: + with open(self.filename, 'wt', encoding=py39.LOCALE_ENCODING) as f: + # Requires encoding="locale" instead of "utf-8" (python/cpython#77102). f.write(data) elif os.path.exists(self.filename): log.debug("Deleting empty %s", self.filename) From fc93ece16304292e6931f8a5730610098dae40dc Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 09:44:36 +0000 Subject: [PATCH 071/232] Add comments to remind about utf-8 in easy-install --- setuptools/command/easy_install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index c256770239e..858fb20f839 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -873,7 +873,7 @@ def write_script(self, script_name, contents, mode="t", blockers=()): ensure_directory(target) if os.path.exists(target): os.unlink(target) - with open(target, "w" + mode) as f: # TODO: is it safe to use "utf-8"? + with open(target, "w" + mode) as f: # TODO: is it safe to use utf-8? f.write(contents) chmod(target, 0o777 - mask) @@ -1017,7 +1017,7 @@ def install_exe(self, dist_filename, tmpdir): # Write EGG-INFO/PKG-INFO if not os.path.exists(pkg_inf): - f = open(pkg_inf, 'w') # TODO: probably it is safe to use "utf-8" + f = open(pkg_inf, 'w') # TODO: probably it is safe to use utf-8 f.write('Metadata-Version: 1.0\n') for k, v in cfg.items('metadata'): if k != 'target_version': @@ -1088,7 +1088,7 @@ def process(src, dst): if locals()[name]: txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt') if not os.path.exists(txt): - f = open(txt, 'w') + f = open(txt, 'w') # TODO: probably it is safe to use utf-8 f.write('\n'.join(locals()[name]) + '\n') f.close() From 98c877396b9ecd0e94b6c46a41ea9cef87dc2965 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 09:47:56 +0000 Subject: [PATCH 072/232] Explicitly use 'locale' encoding for .pth files in setuptools.namespaces --- setuptools/namespaces.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py index e8f2941d45a..0185d55f94c 100644 --- a/setuptools/namespaces.py +++ b/setuptools/namespaces.py @@ -2,6 +2,8 @@ from distutils import log import itertools +from .compat import py39 + flatten = itertools.chain.from_iterable @@ -23,7 +25,8 @@ def install_namespaces(self): list(lines) return - with open(filename, 'wt') as f: + with open(filename, 'wt', encoding=py39.LOCALE_ENCODING) as f: + # Requires encoding="locale" instead of "utf-8" (python/cpython#77102). f.writelines(lines) def uninstall_namespaces(self): From 1dd135cba9c40e25b4cd2b650de4b7299aae5e1c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 10:08:57 +0000 Subject: [PATCH 073/232] Add news fragment --- docs/conf.py | 4 ++++ newsfragments/4265.feature.rst | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 newsfragments/4265.feature.rst diff --git a/docs/conf.py b/docs/conf.py index be8856849b3..534da15a379 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,6 +55,10 @@ pattern=r'(Python #|bpo-)(?P\d+)', url='https://bugs.python.org/issue{python}', ), + dict( + pattern=r'\bpython/cpython#(?P\d+)', + url='{GH}/python/cpython/issues/{cpython}', + ), dict( pattern=r'Interop #(?P\d+)', url='{GH}/pypa/interoperability-peps/issues/{interop}', diff --git a/newsfragments/4265.feature.rst b/newsfragments/4265.feature.rst new file mode 100644 index 00000000000..bcb04672054 --- /dev/null +++ b/newsfragments/4265.feature.rst @@ -0,0 +1,3 @@ +Explicitly use ``encoding="locale"`` for ``.pth`` files whenever possible, +to reduce ``EncodingWarnings``. +This avoid errors with UTF-8 (see discussion in python/cpython#77102). From 5a2add23c8f48ed150de37a4c75b27f849b84f54 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 17:25:56 -0500 Subject: [PATCH 074/232] Update mypy to 1.9 --- mypy.ini | 2 ++ setup.cfg | 1 + setuptools/_core_metadata.py | 2 +- setuptools/command/editable_wheel.py | 3 +-- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 42ade6537ee..90c8ff13e70 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,6 @@ [mypy] # CI should test for all versions, local development gets hints for oldest supported +# Some upstream typeshed distutils stubs fixes are necessary before we can start testing on Python 3.12 python_version = 3.8 strict = False warn_unused_ignores = True @@ -8,6 +9,7 @@ explicit_package_bases = True exclude = (?x)( ^build/ | ^.tox/ + | ^.egg/ | ^pkg_resources/tests/data/my-test-package-source/setup.py$ # Duplicate module name | ^.+?/(_vendor|extern)/ # Vendored | ^setuptools/_distutils/ # Vendored diff --git a/setup.cfg b/setup.cfg index 4d1155e884d..5231358289b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,7 @@ testing = # for tools/finalize.py jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin" pytest-home >= 0.5 + mypy==1.9 # No Python 3.11 dependencies require tomli, but needed for type-checking since we import it directly tomli # No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 4bf3c7c947c..5dd97c7719f 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -62,7 +62,7 @@ def _read_list_from_msg(msg: Message, field: str) -> Optional[List[str]]: def _read_payload_from_msg(msg: Message) -> Optional[str]: - value = msg.get_payload().strip() + value = str(msg.get_payload()).strip() if value == 'UNKNOWN' or not value: return None return value diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 5f08ab53fca..4d21e2253fa 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -620,8 +620,7 @@ def _simple_layout( layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages} if not layout: return set(package_dir) in ({}, {""}) - # TODO: has been fixed upstream, waiting for new mypy release https://github.com/python/typeshed/pull/11310 - parent = os.path.commonpath(starmap(_parent_path, layout.items())) # type: ignore[call-overload] + parent = os.path.commonpath(starmap(_parent_path, layout.items())) return all( _path.same_path(Path(parent, *key.split('.')), value) for key, value in layout.items() From 5bb594c12ef7ddc2cbfd2470266bb85de36d5c86 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 19:20:05 -0500 Subject: [PATCH 075/232] update setup-python action to v5 --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e841bde57c7..d2beaa0c484 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,7 +65,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python id: python-install - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -122,7 +122,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install tox @@ -171,7 +171,7 @@ jobs: git, - name: Record the currently selected Python version id: python-install - # NOTE: This roughly emulates what `actions/setup-python@v4` provides + # NOTE: This roughly emulates what `actions/setup-python` provides # NOTE: except the action gets the version from the installation path # NOTE: on disk and we get it from runtime. run: | @@ -220,7 +220,7 @@ jobs: sudo apt-get update sudo apt-get install build-essential gfortran libopenblas-dev libyaml-dev - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: # Use a release that is not very new but still have a long life: python-version: "3.10" @@ -241,7 +241,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install tox From dec00d13c2c781b2bee498054d1b8ff4cd3122b4 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 19:22:18 -0500 Subject: [PATCH 076/232] Update checkout action to v4 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e841bde57c7..715eb1db7c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,7 +158,7 @@ jobs: runs-on: ${{ matrix.platform }} timeout-minutes: 75 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Cygwin with Python uses: cygwin/cygwin-install-action@v2 with: @@ -214,7 +214,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 75 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install OS-level dependencies run: | sudo apt-get update From 1749aea0e4c1f92e7e46f4b6dcd250fc0b992933 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 19:27:59 -0500 Subject: [PATCH 077/232] Update cache action to v4 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e841bde57c7..4d65b0dad48 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,7 +69,7 @@ jobs: with: python-version: ${{ matrix.python }} allow-prereleases: true - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: setuptools/tests/config/downloads/*.cfg From 0575cc5fadf3e49944c901bbdeb6cf3ca94a73ae Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 11 Mar 2024 11:45:04 +0000 Subject: [PATCH 078/232] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5231358289b..c41b226e0cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,7 +73,7 @@ testing = # for tools/finalize.py jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin" pytest-home >= 0.5 - mypy==1.9 + mypy==1.9 # pin mypy version so a new version doesn't suddenly cause the CI to fail # No Python 3.11 dependencies require tomli, but needed for type-checking since we import it directly tomli # No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly From 6efc720f0fdd79e0689f81acba3fe45878ec43a3 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:57:58 +0100 Subject: [PATCH 079/232] Fix a couple typos found by codespell --- pkg_resources/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 163a5521d67..c2ba0476e51 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3193,7 +3193,7 @@ def _find_adapter(registry, ob): for t in types: if t in registry: return registry[t] - # _find_adapter would previously return None, and immediatly be called. + # _find_adapter would previously return None, and immediately be called. # So we're raising a TypeError to keep backward compatibility if anyone depended on that behaviour. raise TypeError(f"Could not find adapter for {registry} and {ob}") diff --git a/setup.py b/setup.py index 1a6074766a5..542edaea681 100755 --- a/setup.py +++ b/setup.py @@ -88,6 +88,6 @@ def _restore_install_lib(self): if __name__ == '__main__': # allow setup.py to run from another directory - # TODO: Use a proper conditonal statement here + # TODO: Use a proper conditional statement here here and os.chdir(here) # type: ignore[func-returns-value] dist = setuptools.setup(**setup_params) From e0cb8e8fb5e0561da909e22703d5c8a1ce4a0f1d Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 18:13:25 -0500 Subject: [PATCH 080/232] Update cygwin-install-action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc0b67003ff..87b7317f13f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -160,7 +160,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Cygwin with Python - uses: cygwin/cygwin-install-action@v2 + uses: cygwin/cygwin-install-action@v4 with: platform: x86_64 packages: >- From c9e6b2ae2770286aeab5f95063eccb2dc6deb05a Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 8 Mar 2024 19:26:41 -0500 Subject: [PATCH 081/232] Update upload-artefact action to v4 --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 88cc75cabb7..a37f30294d3 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -73,7 +73,7 @@ jobs: && echo "sage-package create ${{ env.SPKG }} --pypi --source normal --type standard; sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ && ls -l upstream/ - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: path: upstream name: upstream From 50f0459cbd195e548bdfecc08e567c54c76c7f44 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 11 Mar 2024 16:55:55 -0400 Subject: [PATCH 082/232] Update .github/workflows/ci-sage.yml --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index a37f30294d3..3da7141573c 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -73,7 +73,7 @@ jobs: && echo "sage-package create ${{ env.SPKG }} --pypi --source normal --type standard; sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ && ls -l upstream/ - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: path: upstream name: upstream From 6ee23bf0579c52e1cbe7c97fc20fd085ff2a25c7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 13 Mar 2024 10:54:30 +0000 Subject: [PATCH 083/232] =?UTF-8?q?Bump=20version:=2069.1.1=20=E2=86=92=20?= =?UTF-8?q?69.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- NEWS.rst | 25 +++++++++++++++++++++++++ newsfragments/4237.misc.rst | 1 - newsfragments/4238.misc.rst | 1 - newsfragments/4241.misc.rst | 1 - newsfragments/4243.bugfix.rst | 1 - newsfragments/4244.bugfix.rst | 1 - newsfragments/4254.bugfix.rst | 1 - newsfragments/4260.misc.rst | 1 - newsfragments/4261.misc.rst | 1 - newsfragments/4263.misc.rst | 1 - newsfragments/4265.feature.rst | 3 --- setup.cfg | 2 +- 13 files changed, 27 insertions(+), 14 deletions(-) delete mode 100644 newsfragments/4237.misc.rst delete mode 100644 newsfragments/4238.misc.rst delete mode 100644 newsfragments/4241.misc.rst delete mode 100644 newsfragments/4243.bugfix.rst delete mode 100644 newsfragments/4244.bugfix.rst delete mode 100644 newsfragments/4254.bugfix.rst delete mode 100644 newsfragments/4260.misc.rst delete mode 100644 newsfragments/4261.misc.rst delete mode 100644 newsfragments/4263.misc.rst delete mode 100644 newsfragments/4265.feature.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8d101ab5af6..1236141a7cd 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 69.1.1 +current_version = 69.2.0 commit = True tag = True diff --git a/NEWS.rst b/NEWS.rst index abc4bb3f04a..2e849bdc5fb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,28 @@ +v69.2.0 +======= + +Features +-------- + +- Explicitly use ``encoding="locale"`` for ``.pth`` files whenever possible, + to reduce ``EncodingWarnings``. + This avoid errors with UTF-8 (see discussion in python/cpython#77102). (#4265) + + +Bugfixes +-------- + +- Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in ``EmptyProvider._get`` -- by :user:`Avasam` (#4243) +- Return an empty `list` by default in ``pkg_resources.ResourceManager.cleanup_resources`` -- by :user:`Avasam` (#4244) +- Made ``pkg_resoursces.NullProvider``'s ``has_metadata`` and ``metadata_isdir`` methods return actual booleans like all other Providers. -- by :user:`Avasam` (#4254) + + +Misc +---- + +- #4237, #4238, #4241, #4260, #4261, #4263 + + v69.1.1 ======= diff --git a/newsfragments/4237.misc.rst b/newsfragments/4237.misc.rst deleted file mode 100644 index 995bee20e1f..00000000000 --- a/newsfragments/4237.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Drop dependency on `py`. Bump ``pytest-xdist`` to ``>=3`` and use `pathlib` instead in tests -- by :user:`Avasam` diff --git a/newsfragments/4238.misc.rst b/newsfragments/4238.misc.rst deleted file mode 100644 index a7ccfc911ed..00000000000 --- a/newsfragments/4238.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Drop dependency on Flake8 by using Ruff's YTT rules instead of flake8-2020 -- by :user:`Avasam` diff --git a/newsfragments/4241.misc.rst b/newsfragments/4241.misc.rst deleted file mode 100644 index ef6da2c323f..00000000000 --- a/newsfragments/4241.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Improvements to `Path`-related type annotations when it could be ``str | PathLike`` -- by :user:`Avasam` diff --git a/newsfragments/4243.bugfix.rst b/newsfragments/4243.bugfix.rst deleted file mode 100644 index e8212721f3e..00000000000 --- a/newsfragments/4243.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in ``EmptyProvider._get`` -- by :user:`Avasam` diff --git a/newsfragments/4244.bugfix.rst b/newsfragments/4244.bugfix.rst deleted file mode 100644 index 5d606de718b..00000000000 --- a/newsfragments/4244.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Return an empty `list` by default in ``pkg_resources.ResourceManager.cleanup_resources`` -- by :user:`Avasam` diff --git a/newsfragments/4254.bugfix.rst b/newsfragments/4254.bugfix.rst deleted file mode 100644 index e944fcfb496..00000000000 --- a/newsfragments/4254.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Made ``pkg_resoursces.NullProvider``'s ``has_metadata`` and ``metadata_isdir`` methods return actual booleans like all other Providers. -- by :user:`Avasam` diff --git a/newsfragments/4260.misc.rst b/newsfragments/4260.misc.rst deleted file mode 100644 index 9dfde3498d2..00000000000 --- a/newsfragments/4260.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Remove unused ``resources_stream`` ``resource_dir`` and shadowed functions from `pkg_resources` -- by :user:`Avasam` diff --git a/newsfragments/4261.misc.rst b/newsfragments/4261.misc.rst deleted file mode 100644 index 83c10f0f66a..00000000000 --- a/newsfragments/4261.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid implicit ``encoding`` parameter in ``setuptools/tests``. diff --git a/newsfragments/4263.misc.rst b/newsfragments/4263.misc.rst deleted file mode 100644 index f84eb8dd426..00000000000 --- a/newsfragments/4263.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid implicit ``encoding`` parameter in ``pkg_resources/tests``. diff --git a/newsfragments/4265.feature.rst b/newsfragments/4265.feature.rst deleted file mode 100644 index bcb04672054..00000000000 --- a/newsfragments/4265.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Explicitly use ``encoding="locale"`` for ``.pth`` files whenever possible, -to reduce ``EncodingWarnings``. -This avoid errors with UTF-8 (see discussion in python/cpython#77102). diff --git a/setup.cfg b/setup.cfg index c41b226e0cf..9b504dd39b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 69.1.1 +version = 69.2.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages From 2b339b98bcc26fe9147647054c8fa09344f581ec Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 15 Mar 2024 00:44:48 -0400 Subject: [PATCH 084/232] Avoid leaking "name" variable in AbstractSandbox --- newsfragments/4280.misc.rst | 1 + setuptools/sandbox.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 newsfragments/4280.misc.rst diff --git a/newsfragments/4280.misc.rst b/newsfragments/4280.misc.rst new file mode 100644 index 00000000000..aff6a7ca1c6 --- /dev/null +++ b/newsfragments/4280.misc.rst @@ -0,0 +1 @@ +Avoid leaking loop variable ``name`` in ``AbstractSandbox`` -- by :user:`Avasam` diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 6c095e029e4..e5da9d86f0c 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -309,9 +309,9 @@ def wrap(self, src, dst, *args, **kw): return wrap - for name in ["rename", "link", "symlink"]: - if hasattr(_os, name): - locals()[name] = _mk_dual_path_wrapper(name) + for __name in ["rename", "link", "symlink"]: + if hasattr(_os, __name): + locals()[__name] = _mk_dual_path_wrapper(__name) def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = original or getattr(_os, name) @@ -326,7 +326,7 @@ def wrap(self, path, *args, **kw): if _file: _file = _mk_single_path_wrapper('file', _file) _open = _mk_single_path_wrapper('open', _open) - for name in [ + for __name in [ "stat", "listdir", "chdir", @@ -347,8 +347,8 @@ def wrap(self, path, *args, **kw): "pathconf", "access", ]: - if hasattr(_os, name): - locals()[name] = _mk_single_path_wrapper(name) + if hasattr(_os, __name): + locals()[__name] = _mk_single_path_wrapper(__name) def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) @@ -361,9 +361,9 @@ def wrap(self, path, *args, **kw): return wrap - for name in ['readlink', 'tempnam']: - if hasattr(_os, name): - locals()[name] = _mk_single_with_return(name) + for __name in ['readlink', 'tempnam']: + if hasattr(_os, __name): + locals()[__name] = _mk_single_with_return(__name) def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) @@ -376,9 +376,9 @@ def wrap(self, *args, **kw): return wrap - for name in ['getcwd', 'tmpnam']: - if hasattr(_os, name): - locals()[name] = _mk_query(name) + for __name in ['getcwd', 'tmpnam']: + if hasattr(_os, __name): + locals()[__name] = _mk_query(__name) def _validate_path(self, path): """Called to remap or validate any path, whether input or output""" From d377ff738350743ce5e134e04031707605ec3dd3 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 16 Mar 2024 02:20:46 -0400 Subject: [PATCH 085/232] Update codecov/codecov-action to v4 --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87b7317f13f..76178067b82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -100,7 +100,7 @@ jobs: run: pipx run coverage xml --ignore-errors - name: Publish coverage if: hashFiles('coverage.xml') != '' # Rudimentary `file.exists()` - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: >- # Mark which lines are covered by which envs CI-GHA, @@ -108,6 +108,7 @@ jobs: OS-${{ runner.os }}, VM-${{ matrix.platform }}, Py-${{ steps.python-install.outputs.python-version }} + token: ${{ secrets.CODECOV_TOKEN }} collateral: strategy: @@ -190,7 +191,7 @@ jobs: shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} - name: Publish coverage if: hashFiles('coverage.xml') != '' # Rudimentary `file.exists()` - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: >- ${{ github.workspace }}\coverage.xml @@ -200,6 +201,7 @@ jobs: OS-${{ runner.os }}, VM-${{ matrix.platform }}, Py-${{ steps.python-install.outputs.python-version }} + token: ${{ secrets.CODECOV_TOKEN }} integration-test: needs: test From b0135f5097f32a27b7a14e2c6296ba14bcb4e10b Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Mar 2024 20:46:50 +0000 Subject: [PATCH 086/232] Support PEP 625 --- setuptools/_distutils/dist.py | 5 ++++- setuptools/tests/test_config_discovery.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index 7c0f0e5b78c..d20ce33e00a 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_distutils/dist.py @@ -23,6 +23,7 @@ DistutilsArgError, DistutilsClassError, ) +from setuptools.extern.packaging.utils import canonicalize_name, canonicalize_version from .fancy_getopt import FancyGetopt, translate_longopt from .util import check_environ, strtobool, rfc822_escape from ._log import log @@ -1189,7 +1190,9 @@ def get_version(self): return self.version or "0.0.0" def get_fullname(self): - return "{}-{}".format(self.get_name(), self.get_version()) + return "{}-{}".format( + canonicalize_name(self.get_name()), canonicalize_version(self.get_version()) + ) def get_author(self): return self.author diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 72772caebfe..7d51a470124 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -255,7 +255,7 @@ def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path): class TestNoConfig: - DEFAULT_VERSION = "0.0.0" # Default version given by setuptools + CANONICAL_DEFAULT_VERSION = "0" # Canonical default version given by setuptools EXAMPLES = { "pkg1": ["src/pkg1.py"], @@ -277,7 +277,9 @@ def test_build_with_discovered_name(self, tmp_path): _populate_project_dir(tmp_path, files, {}) _run_build(tmp_path, "--sdist") # Expected distribution file - dist_file = tmp_path / f"dist/ns.nested.pkg-{self.DEFAULT_VERSION}.tar.gz" + dist_file = ( + tmp_path / f"dist/ns-nested-pkg-{self.CANONICAL_DEFAULT_VERSION}.tar.gz" + ) assert dist_file.is_file() From b93e7afba85c7d55d0419c3f544e9348f283a7d6 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Mar 2024 20:51:32 +0000 Subject: [PATCH 087/232] Add news fragment --- newsfragments/3593.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3593.feature.rst diff --git a/newsfragments/3593.feature.rst b/newsfragments/3593.feature.rst new file mode 100644 index 00000000000..2ec6f9714e4 --- /dev/null +++ b/newsfragments/3593.feature.rst @@ -0,0 +1 @@ +Support PEP 625 by canonicalizing package name and version in filenames. From 44f67acbbd262ca9376e86c4671ecbea0173147b Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Wed, 20 Mar 2024 22:54:58 +0100 Subject: [PATCH 088/232] Add mypy.ini to MANIFEST.in --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 116840bfa2c..c4f12dc68ab 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ include MANIFEST.in include LICENSE include launcher.c include msvc-build-launcher.cmd +include mypy.ini include pytest.ini include tox.ini include setuptools/tests/config/setupcfg_examples.txt From a0d0c4b7e87fbfd04cee2546ba452858587516fd Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 21 Mar 2024 15:34:23 -0400 Subject: [PATCH 089/232] Allow mypy on PyPy (jaraco/skeleton#111) https://github.com/pypa/setuptools/pull/4257 shows that mypy now works with PyPy --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 400a72a5ed4..6fa73b6a09f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,7 @@ testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-cov - pytest-mypy; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" + pytest-mypy pytest-enabler >= 2.2 pytest-ruff >= 0.2.1 From f0aaeb5c00e5767ce37a760e0199a9fb74f07cc6 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 21 Mar 2024 20:56:54 +0000 Subject: [PATCH 090/232] Revert changes to distutils --- setuptools/_distutils/dist.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index d20ce33e00a..7c0f0e5b78c 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_distutils/dist.py @@ -23,7 +23,6 @@ DistutilsArgError, DistutilsClassError, ) -from setuptools.extern.packaging.utils import canonicalize_name, canonicalize_version from .fancy_getopt import FancyGetopt, translate_longopt from .util import check_environ, strtobool, rfc822_escape from ._log import log @@ -1190,9 +1189,7 @@ def get_version(self): return self.version or "0.0.0" def get_fullname(self): - return "{}-{}".format( - canonicalize_name(self.get_name()), canonicalize_version(self.get_version()) - ) + return "{}-{}".format(self.get_name(), self.get_version()) def get_author(self): return self.author From cfc9a82db67324a05986abf349a27b85e74a4aac Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 21 Mar 2024 20:57:10 +0000 Subject: [PATCH 091/232] Try monkeypatching right before we use it instead --- setuptools/dist.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 6350e381006..c7a3e5175d9 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -7,6 +7,7 @@ import os import re import sys +import contextlib from contextlib import suppress from glob import iglob from pathlib import Path @@ -26,6 +27,7 @@ from .extern.ordered_set import OrderedSet from .extern.packaging.markers import InvalidMarker, Marker from .extern.packaging.specifiers import InvalidSpecifier, SpecifierSet +from .extern.packaging.utils import canonicalize_name, canonicalize_version from .extern.packaging.version import Version from . import _entry_points @@ -964,8 +966,28 @@ def run_command(self, command): # Postpone defaults until all explicit configuration is considered # (setup() args, config files, command line and plugins) - super().run_command(command) + with self._override_get_fullname(): + super().run_command(command) + @contextlib.contextmanager + def _override_get_fullname(self): + def _get_fullname_canonicalized(self): + return "{}-{}".format( + canonicalize_name(self.get_name()), + canonicalize_version(self.get_version()), + ) + + class NoValue: + pass + + orig_val = getattr(self, 'get_fullname', NoValue) + self.get_fullname = _get_fullname_canonicalized.__get__(self) + + try: + yield + finally: + if orig_val is not NoValue: + self.get_fullname = orig_val class DistDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in dist in From 22b81c444cb65e256dcbea191e1b2d60f7e4dab6 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 21 Mar 2024 22:11:28 +0000 Subject: [PATCH 092/232] Linting --- setuptools/dist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/dist.py b/setuptools/dist.py index c7a3e5175d9..c62187ec253 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -989,6 +989,7 @@ class NoValue: if orig_val is not NoValue: self.get_fullname = orig_val + class DistDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning.""" From c9a7f97ba83be124e173713f5c24564c2b6dd49e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 15:49:52 -0400 Subject: [PATCH 093/232] Re-enable ignoring of temporary merge queue branches. Closes jaraco/skeleton#103. --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf94f7d8164..143b0984b01 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,8 +4,11 @@ on: merge_group: push: branches-ignore: - # disabled for jaraco/skeleton#103 - # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # temporary GH branches relating to merge queues (jaraco/skeleton#93) + - gh-readonly-queue/** + tags: + # required if branches-ignore is supplied (jaraco/skeleton#103) + - '**' pull_request: permissions: From d72c6a081b67ce18eae654bf3c8d2d627af6939e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 Mar 2024 13:46:21 -0400 Subject: [PATCH 094/232] Fetch unshallow clones in readthedocs. Closes jaraco/skeleton#114. --- .readthedocs.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 68489063748..85dfea9d42d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,3 +10,7 @@ build: os: ubuntu-lts-latest tools: python: latest + # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 + jobs: + post_checkout: + - git fetch --unshallow || true From 3fc7a935dfc0e5c8e330a29efc5518c464795cf8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Mar 2024 21:11:46 -0400 Subject: [PATCH 095/232] Move Python 3.11 out of the test matrix. Probably should have done this when moving continue-on-error to Python 3.13. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 143b0984b01..a15c74a6183 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,6 @@ jobs: matrix: python: - "3.8" - - "3.11" - "3.12" platform: - ubuntu-latest @@ -45,6 +44,8 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest + - python: "3.11" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} From 6ff02e0eefcd90e271cefd326b460ecfa0e3eb9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Mar 2024 04:27:11 -0400 Subject: [PATCH 096/232] Configure pytest to support namespace packages. Ref pytest-dev/pytest#12112. --- pytest.ini | 5 ++++- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 022a723e7e3..9a0f3bce139 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules +addopts= + --doctest-modules + --import-mode importlib +consider_namespace_packages=true filterwarnings= ## upstream diff --git a/setup.cfg b/setup.cfg index 6fa73b6a09f..f46b6cbff4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = [options.extras_require] testing = # upstream - pytest >= 6 + pytest >= 6, != 8.1.1 pytest-checkdocs >= 2.4 pytest-cov pytest-mypy From 9b58da5c84b58743ef9e0f0346d31150afd2229f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Apr 2024 18:13:25 -0400 Subject: [PATCH 097/232] Revert "Suppress EncodingWarnings in pyfakefs. Ref pypa/distutils#232. Workaround for pytest-dev/pyfakefs#957." This reverts commit 9508489953a84a1412ad24e6613650351369462c. --- pytest.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index fa31fb33dc0..3ee2f886ba5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -34,6 +34,3 @@ filterwarnings= # suppress well know deprecation warning ignore:distutils.log.Log is deprecated - - # pytest-dev/pyfakefs#957 - ignore:UTF-8 Mode affects locale.getpreferredencoding::pyfakefs.fake_file From 34ba6b2ec0650c8c70d9285a0c7ee1a126406807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 1 Apr 2024 17:47:04 +0200 Subject: [PATCH 098/232] Add link to blog entry from jaraco/skeleton#115 above CI build matrix. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a15c74a6183..ac0ff69e225 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ env: jobs: test: strategy: + # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - "3.8" From bf33f79fee5ba88dba5dde8beb57ba03d856dc31 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 11 Apr 2024 15:43:12 +0000 Subject: [PATCH 099/232] Fix canonicalization --- setuptools/dist.py | 2 +- setuptools/tests/test_config_discovery.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index c62187ec253..202430fb695 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -973,7 +973,7 @@ def run_command(self, command): def _override_get_fullname(self): def _get_fullname_canonicalized(self): return "{}-{}".format( - canonicalize_name(self.get_name()), + canonicalize_name(self.get_name()).replace('-', '_'), canonicalize_version(self.get_version()), ) diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 7d51a470124..e1e67ffe111 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -278,7 +278,7 @@ def test_build_with_discovered_name(self, tmp_path): _run_build(tmp_path, "--sdist") # Expected distribution file dist_file = ( - tmp_path / f"dist/ns-nested-pkg-{self.CANONICAL_DEFAULT_VERSION}.tar.gz" + tmp_path / f"dist/ns_nested_pkg-{self.CANONICAL_DEFAULT_VERSION}.tar.gz" ) assert dist_file.is_file() From af38e1cd6db5ad272cf2e3c0747c0b478a0c269c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 14:05:15 -0400 Subject: [PATCH 100/232] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under some circumstances not fully understood, mypy has started complaining when `_validate_project` tries to import `trove_classifiers` (and it doesn't exist), even though `_validate_project` is excluded from mypy checks. Mysteriously, adding `trove_classifiers` itself to the list of modules for which to ignore imports suppresses this mysterious failure. Ref #4296. --- mypy.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 90c8ff13e70..ee12ebb193b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -25,7 +25,8 @@ disable_error_code = attr-defined # https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 # - distutils._modified has different errors on Python 3.8 [import-untyped], on Python 3.9+ [import-not-found] # - All jaraco modules are still untyped -[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*] +# - _validate_project sometimes complains about trove_classifiers (#4296) +[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*,trove_classifiers] ignore_missing_imports = True # - pkg_resources tests create modules that won't exists statically before the test is run. From 230bde5008fbc7b0764649f39aa8640befd9ec0b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 17:01:51 -0400 Subject: [PATCH 101/232] Fix ruff.toml syntax and suppress emergent failure. --- ruff.toml | 8 ++++---- setuptools/command/easy_install.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index bd1a86ff17f..6f620cb8905 100644 --- a/ruff.toml +++ b/ruff.toml @@ -2,6 +2,10 @@ extend-select = [ "C901", "W", + + # local + "UP", # pyupgrade + "YTT", # flake8-2020 ] ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules @@ -20,10 +24,6 @@ ignore = [ "ISC001", "ISC002", ] -extend-select = [ - "UP", # pyupgrade - "YTT", # flake8-2020 -] extend-ignore = [ "UP015", # redundant-open-modes, explicit is preferred "UP030", # temporarily disabled diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 858fb20f839..87a68c292a1 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -565,7 +565,7 @@ def cant_write_to_target(self): msg += '\n' + self.__access_msg raise DistutilsError(msg) - def check_pth_processing(self): + def check_pth_processing(self): # noqa: C901 """Empirically verify whether .pth files are supported in inst. dir""" instdir = self.install_dir log.info("Checking .pth file support in %s", instdir) From 6e74c881b0a71a06620e7e112ae0f17973e348f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 20:24:09 -0400 Subject: [PATCH 102/232] Move implementation to monkey.patch. --- setuptools/_core_metadata.py | 9 +++++++++ setuptools/dist.py | 25 +------------------------ setuptools/monkey.py | 1 + 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 5dd97c7719f..d8732c49bb4 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -17,6 +17,7 @@ from . import _normalization, _reqs from .extern.packaging.markers import Marker from .extern.packaging.requirements import Requirement +from .extern.packaging.utils import canonicalize_name, canonicalize_version from .extern.packaging.version import Version from .warnings import SetuptoolsDeprecationWarning @@ -257,3 +258,11 @@ def _write_provides_extra(file, processed_extras, safe, unsafe): else: processed_extras[safe] = unsafe file.write(f"Provides-Extra: {safe}\n") + + +# from pypa/distutils#244; needed only until that logic is always available +def get_fullname(self): + return "{}-{}".format( + canonicalize_name(self.get_name()).replace('-', '_'), + canonicalize_version(self.get_version()), + ) diff --git a/setuptools/dist.py b/setuptools/dist.py index 202430fb695..6350e381006 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -7,7 +7,6 @@ import os import re import sys -import contextlib from contextlib import suppress from glob import iglob from pathlib import Path @@ -27,7 +26,6 @@ from .extern.ordered_set import OrderedSet from .extern.packaging.markers import InvalidMarker, Marker from .extern.packaging.specifiers import InvalidSpecifier, SpecifierSet -from .extern.packaging.utils import canonicalize_name, canonicalize_version from .extern.packaging.version import Version from . import _entry_points @@ -966,28 +964,7 @@ def run_command(self, command): # Postpone defaults until all explicit configuration is considered # (setup() args, config files, command line and plugins) - with self._override_get_fullname(): - super().run_command(command) - - @contextlib.contextmanager - def _override_get_fullname(self): - def _get_fullname_canonicalized(self): - return "{}-{}".format( - canonicalize_name(self.get_name()).replace('-', '_'), - canonicalize_version(self.get_version()), - ) - - class NoValue: - pass - - orig_val = getattr(self, 'get_fullname', NoValue) - self.get_fullname = _get_fullname_canonicalized.__get__(self) - - try: - yield - finally: - if orig_val is not NoValue: - self.get_fullname = orig_val + super().run_command(command) class DistDeprecationWarning(SetuptoolsDeprecationWarning): diff --git a/setuptools/monkey.py b/setuptools/monkey.py index fd07d91dece..1f8d8ffe0f2 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -95,6 +95,7 @@ def _patch_distribution_metadata(): 'write_pkg_file', 'read_pkg_file', 'get_metadata_version', + 'get_fullname', ): new_val = getattr(_core_metadata, attr) setattr(distutils.dist.DistributionMetadata, attr, new_val) From 842cc23a1b0af16fa09b8e4b86433531716ffc8d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:03:46 -0400 Subject: [PATCH 103/232] Update readme to reflect current state. --- README.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 822809de2bc..aa3b65f15e6 100644 --- a/README.rst +++ b/README.rst @@ -19,12 +19,9 @@ Python Module Distribution Utilities extracted from the Python Standard Library -Synchronizing -============= +This package is unsupported except as integrated into and exposed by Setuptools. -This project is no longer kept in sync with the code still in stdlib, which is deprecated and scheduled for removal. - -To Setuptools -------------- +Integration +----------- Simply merge the changes directly into setuptools' repo. From 62b9a8edb7871d165f3503bc1cb671f75a7e84ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:04:55 -0400 Subject: [PATCH 104/232] Apply ruff --select UP safe fixes. --- distutils/command/bdist_dumb.py | 4 +--- distutils/command/bdist_rpm.py | 6 +----- distutils/command/build_scripts.py | 6 +++--- distutils/extension.py | 7 +------ distutils/msvccompiler.py | 5 +---- distutils/tests/test_unixccompiler.py | 5 +---- distutils/tests/test_util.py | 2 +- distutils/tests/test_version.py | 16 ++++------------ 8 files changed, 13 insertions(+), 38 deletions(-) diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py index 01dd79079b0..4beb123630e 100644 --- a/distutils/command/bdist_dumb.py +++ b/distutils/command/bdist_dumb.py @@ -104,9 +104,7 @@ def run(self): # And make an archive relative to the root of the # pseudo-installation tree. - archive_basename = "{}.{}".format( - self.distribution.get_fullname(), self.plat_name - ) + archive_basename = f"{self.distribution.get_fullname()}.{self.plat_name}" pseudoinstall_root = os.path.join(self.dist_dir, archive_basename) if not self.relative: diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 675bcebdad6..bb3bee7eb9a 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -352,11 +352,7 @@ def run(self): # noqa: C901 nvr_string = "%{name}-%{version}-%{release}" src_rpm = nvr_string + ".src.rpm" non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm" - q_cmd = r"rpm -q --qf '{} {}\n' --specfile '{}'".format( - src_rpm, - non_src_rpm, - spec_path, - ) + q_cmd = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'" out = os.popen(q_cmd) try: diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 1a4d67f4921..68caf5a65b2 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -157,7 +157,7 @@ def _validate_shebang(shebang, encoding): shebang.encode('utf-8') except UnicodeEncodeError: raise ValueError( - "The shebang ({!r}) is not encodable " "to utf-8".format(shebang) + f"The shebang ({shebang!r}) is not encodable " "to utf-8" ) # If the script is encoded to a custom encoding (use a @@ -167,6 +167,6 @@ def _validate_shebang(shebang, encoding): shebang.encode(encoding) except UnicodeEncodeError: raise ValueError( - "The shebang ({!r}) is not encodable " - "to the script encoding ({})".format(shebang, encoding) + f"The shebang ({shebang!r}) is not encodable " + f"to the script encoding ({encoding})" ) diff --git a/distutils/extension.py b/distutils/extension.py index 8f186b72ffc..00ca61d569b 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -134,12 +134,7 @@ def __init__( warnings.warn(msg) def __repr__(self): - return '<{}.{}({!r}) at {:#x}>'.format( - self.__class__.__module__, - self.__class__.__qualname__, - self.name, - id(self), - ) + return f'<{self.__class__.__module__}.{self.__class__.__qualname__}({self.name!r}) at {id(self):#x}>' def read_setup_file(filename): # noqa: C901 diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index 1a07746bc70..8b4f7046c7c 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -635,10 +635,7 @@ def get_msvc_paths(self, path, platform='x86'): path = path + " dirs" if self.__version >= 7: - key = r"{}\{:0.1f}\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories".format( - self.__root, - self.__version, - ) + key = rf"{self.__root}\{self.__version:0.1f}\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories" else: key = ( r"%s\6.0\Build System\Components\Platforms" diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 2763db9c026..ca198873ade 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -73,10 +73,7 @@ def gcv(var): def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag): env = os.environ - msg = "macOS version = (sysconfig={!r}, env={!r})".format( - syscfg_macosx_ver, - env_macosx_ver, - ) + msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})" # Save old_gcv = sysconfig.get_config_var diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index c632b3910f1..53c131e9e5b 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -259,6 +259,6 @@ def test_dont_write_bytecode(self): def test_grok_environment_error(self): # test obsolete function to ensure backward compat (#4931) - exc = IOError("Unable to find batch file") + exc = OSError("Unable to find batch file") msg = grok_environment_error(exc) assert msg == "error: Unable to find batch file" diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 0aaf0a534cd..7e42227e19a 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -52,13 +52,9 @@ def test_cmp_strict(self): raise AssertionError( ("cmp(%s, %s) " "shouldn't raise ValueError") % (v1, v2) ) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(v2) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(object()) assert ( res is NotImplemented @@ -78,13 +74,9 @@ def test_cmp(self): for v1, v2, wanted in versions: res = LooseVersion(v1)._cmp(LooseVersion(v2)) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(v2) - assert res == wanted, 'cmp({}, {}) should be {}, got {}'.format( - v1, v2, wanted, res - ) + assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = LooseVersion(v1)._cmp(object()) assert ( res is NotImplemented From f8ab1e8b72f4ab82bdb1402d6b66ddb02d6ef657 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:06:12 -0400 Subject: [PATCH 105/232] Apply ruff --select UP unsafe fixes. --- distutils/ccompiler.py | 9 ++++----- distutils/command/bdist_dumb.py | 3 +-- distutils/command/bdist_rpm.py | 3 +-- distutils/command/build_scripts.py | 3 +-- distutils/command/install_data.py | 2 +- distutils/dist.py | 9 +++------ distutils/fancy_getopt.py | 16 +++++++--------- distutils/file_util.py | 4 ++-- distutils/msvccompiler.py | 4 ++-- distutils/tests/test_bdist_dumb.py | 2 +- distutils/tests/test_install.py | 4 ++-- distutils/tests/test_version.py | 2 +- distutils/util.py | 22 +++++++--------------- 13 files changed, 33 insertions(+), 50 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index bcf9580c7af..cdfe9d74eff 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -169,8 +169,7 @@ class (via the 'executables' class attribute), but most will have: for key in kwargs: if key not in self.executables: raise ValueError( - "unknown executable '%s' for class %s" - % (key, self.__class__.__name__) + f"unknown executable '{key}' for class {self.__class__.__name__}" ) self.set_executable(key, kwargs[key]) @@ -1162,8 +1161,8 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0): ) except KeyError: raise DistutilsModuleError( - "can't compile C/C++ code: unable to find class '%s' " - "in module '%s'" % (class_name, module_name) + f"can't compile C/C++ code: unable to find class '{class_name}' " + f"in module '{module_name}'" ) # XXX The None is necessary to preserve backwards compatibility @@ -1210,7 +1209,7 @@ def gen_preprocess_options(macros, include_dirs): # XXX *don't* need to be clever about quoting the # macro value here, because we're going to avoid the # shell at all costs when we spawn the command! - pp_opts.append("-D%s=%s" % macro) + pp_opts.append("-D{}={}".format(*macro)) for dir in include_dirs: pp_opts.append("-I%s" % dir) diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py index 4beb123630e..5880ad2ba40 100644 --- a/distutils/command/bdist_dumb.py +++ b/distutils/command/bdist_dumb.py @@ -115,8 +115,7 @@ def run(self): ): raise DistutilsPlatformError( "can't make a dumb built distribution where " - "base and platbase are different (%s, %s)" - % (repr(install.install_base), repr(install.install_platbase)) + f"base and platbase are different ({repr(install.install_base)}, {repr(install.install_platbase)})" ) else: archive_root = os.path.join( diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index bb3bee7eb9a..64af0db0cf3 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -232,8 +232,7 @@ def finalize_package_data(self): self.ensure_string('group', "Development/Libraries") self.ensure_string( 'vendor', - "%s <%s>" - % (self.distribution.get_contact(), self.distribution.get_contact_email()), + f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>", ) self.ensure_string('packager') self.ensure_string_list('doc_files') diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 68caf5a65b2..6a5e6ed0815 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -109,8 +109,7 @@ def _copy_script(self, script, outfiles, updated_files): # noqa: C901 else: executable = os.path.join( sysconfig.get_config_var("BINDIR"), - "python%s%s" - % ( + "python{}{}".format( sysconfig.get_config_var("VERSION"), sysconfig.get_config_var("EXE"), ), diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py index 7ba35eef827..31ae4350dc1 100644 --- a/distutils/command/install_data.py +++ b/distutils/command/install_data.py @@ -51,7 +51,7 @@ def run(self): if self.warn_dir: self.warn( "setup script did not provide a directory for " - "'%s' -- installing right in '%s'" % (f, self.install_dir) + f"'{f}' -- installing right in '{self.install_dir}'" ) (out, _) = self.copy_file(f, self.install_dir) self.outfiles.append(out) diff --git a/distutils/dist.py b/distutils/dist.py index c4d2a45dc26..bbea155556a 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -592,9 +592,8 @@ def _parse_command_opts(self, parser, args): # noqa: C901 func() else: raise DistutilsClassError( - "invalid help function %r for help option '%s': " + f"invalid help function {func!r} for help option '{help_option}': " "must be a callable object (function, etc.)" - % (func, help_option) ) if help_option_found: @@ -834,8 +833,7 @@ def get_command_class(self, command): klass = getattr(module, klass_name) except AttributeError: raise DistutilsModuleError( - "invalid command '%s' (no class '%s' in module '%s')" - % (command, klass_name, module_name) + f"invalid command '{command}' (no class '{klass_name}' in module '{module_name}')" ) self.cmdclass[command] = klass @@ -909,8 +907,7 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 setattr(command_obj, option, value) else: raise DistutilsOptionError( - "error in %s: command '%s' has no such option '%s'" - % (source, command_name, option) + f"error in {source}: command '{command_name}' has no such option '{option}'" ) except ValueError as msg: raise DistutilsOptionError(msg) diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index c025f12062c..e41b6064bd0 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -116,13 +116,11 @@ def _check_alias_dict(self, aliases, what): for alias, opt in aliases.items(): if alias not in self.option_index: raise DistutilsGetoptError( - ("invalid %s '%s': " "option '%s' not defined") - % (what, alias, alias) + f"invalid {what} '{alias}': " f"option '{alias}' not defined" ) if opt not in self.option_index: raise DistutilsGetoptError( - ("invalid %s '%s': " "aliased option '%s' not defined") - % (what, alias, opt) + f"invalid {what} '{alias}': " f"aliased option '{opt}' not defined" ) def set_aliases(self, alias): @@ -187,8 +185,8 @@ def _grok_option_table(self): # noqa: C901 if alias_to is not None: if self.takes_arg[alias_to]: raise DistutilsGetoptError( - "invalid negative alias '%s': " - "aliased option '%s' takes a value" % (long, alias_to) + f"invalid negative alias '{long}': " + f"aliased option '{alias_to}' takes a value" ) self.long_opts[-1] = long # XXX redundant?! @@ -200,9 +198,9 @@ def _grok_option_table(self): # noqa: C901 if alias_to is not None: if self.takes_arg[long] != self.takes_arg[alias_to]: raise DistutilsGetoptError( - "invalid alias '%s': inconsistent with " - "aliased option '%s' (one of them takes a value, " - "the other doesn't" % (long, alias_to) + f"invalid alias '{long}': inconsistent with " + f"aliased option '{alias_to}' (one of them takes a value, " + "the other doesn't" ) # Now enforce some bondage on the long option name, so we can diff --git a/distutils/file_util.py b/distutils/file_util.py index 0eb9b86107f..6c8193e9b7b 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -220,8 +220,8 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 except OSError: pass raise DistutilsFileError( - "couldn't move '%s' to '%s' by copy/delete: " - "delete '%s' failed: %s" % (src, dst, src, msg) + f"couldn't move '{src}' to '{dst}' by copy/delete: " + f"delete '{src}' failed: {msg}" ) return dst diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index 8b4f7046c7c..b8694dd6d83 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -638,8 +638,8 @@ def get_msvc_paths(self, path, platform='x86'): key = rf"{self.__root}\{self.__version:0.1f}\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories" else: key = ( - r"%s\6.0\Build System\Components\Platforms" - r"\Win32 (%s)\Directories" % (self.__root, platform) + rf"{self.__root}\6.0\Build System\Components\Platforms" + rf"\Win32 ({platform})\Directories" ) for base in HKEYS: diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index cb4db4e1924..cfe7fa9e62b 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -73,7 +73,7 @@ def test_simple_built(self): fp.close() contents = sorted(filter(None, map(os.path.basename, contents))) - wanted = ['foo-0.1-py%s.%s.egg-info' % sys.version_info[:2], 'foo.py'] + wanted = ['foo-0.1-py{}.{}.egg-info'.format(*sys.version_info[:2]), 'foo.py'] if not sys.dont_write_bytecode: wanted.append('foo.%s.pyc' % sys.implementation.cache_tag) assert contents == sorted(wanted) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 16ac5ca7462..08c72c1be00 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -203,7 +203,7 @@ def test_record(self): 'hello.py', 'hello.%s.pyc' % sys.implementation.cache_tag, 'sayhi', - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2], + 'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]), ] assert found == expected @@ -235,7 +235,7 @@ def test_record_extensions(self): found = [pathlib.Path(line).name for line in content.splitlines()] expected = [ _make_ext_name('xx'), - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2], + 'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]), ] assert found == expected diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 7e42227e19a..f89d1b35805 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -50,7 +50,7 @@ def test_cmp_strict(self): continue else: raise AssertionError( - ("cmp(%s, %s) " "shouldn't raise ValueError") % (v1, v2) + f"cmp({v1}, {v2}) " "shouldn't raise ValueError" ) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(v2) diff --git a/distutils/util.py b/distutils/util.py index bfd30700fa9..ce5bc55f364 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -30,12 +30,6 @@ def get_host_platform(): # even with older Python versions when distutils was split out. # Now it delegates to stdlib sysconfig, but maintains compatibility. - if sys.version_info < (3, 8): - if os.name == 'nt': - if '(arm)' in sys.version.lower(): - return 'win-arm32' - if '(arm64)' in sys.version.lower(): - return 'win-arm64' if sys.version_info < (3, 9): if os.name == "posix" and hasattr(os, 'uname'): @@ -109,8 +103,8 @@ def get_macosx_target_ver(): ): my_msg = ( '$' + MACOSX_VERSION_VAR + ' mismatch: ' - 'now "%s" but "%s" during configure; ' - 'must use 10.3 or later' % (env_ver, syscfg_ver) + f'now "{env_ver}" but "{syscfg_ver}" during configure; ' + 'must use 10.3 or later' ) raise DistutilsPlatformError(my_msg) return env_ver @@ -447,13 +441,12 @@ def byte_compile( # noqa: C901 script.write(",\n".join(map(repr, py_files)) + "]\n") script.write( - """ -byte_compile(files, optimize=%r, force=%r, - prefix=%r, base_dir=%r, - verbose=%r, dry_run=0, + f""" +byte_compile(files, optimize={optimize!r}, force={force!r}, + prefix={prefix!r}, base_dir={base_dir!r}, + verbose={verbose!r}, dry_run=0, direct=1) """ - % (optimize, force, prefix, base_dir, verbose) ) cmd = [sys.executable] @@ -487,8 +480,7 @@ def byte_compile( # noqa: C901 if prefix: if file[: len(prefix)] != prefix: raise ValueError( - "invalid prefix: filename %r doesn't start with %r" - % (file, prefix) + f"invalid prefix: filename {file!r} doesn't start with {prefix!r}" ) dfile = dfile[len(prefix) :] if base_dir: From 13b1f91e5d883bcd2132c9e7ae08940841bbee34 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:06:18 -0400 Subject: [PATCH 106/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/command/build_scripts.py | 4 +--- distutils/util.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 6a5e6ed0815..29d9c27829a 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -155,9 +155,7 @@ def _validate_shebang(shebang, encoding): try: shebang.encode('utf-8') except UnicodeEncodeError: - raise ValueError( - f"The shebang ({shebang!r}) is not encodable " "to utf-8" - ) + raise ValueError(f"The shebang ({shebang!r}) is not encodable " "to utf-8") # If the script is encoded to a custom encoding (use a # #coding:xxx cookie), the shebang has to be encodable to diff --git a/distutils/util.py b/distutils/util.py index ce5bc55f364..a24c9401027 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -30,7 +30,6 @@ def get_host_platform(): # 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() From 2415d50bf5f9034b1c7661795368a68c8293c3b1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:06:56 -0400 Subject: [PATCH 107/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply isort rules using `ruff --select I --fix`. --- conftest.py | 12 +++---- distutils/__init__.py | 2 +- distutils/_log.py | 1 - distutils/_macos_compat.py | 2 +- distutils/_modified.py | 2 +- distutils/_msvccompiler.py | 14 ++++---- distutils/archive_util.py | 6 ++-- distutils/bcppcompiler.py | 9 +++-- distutils/ccompiler.py | 18 +++++----- distutils/cmd.py | 8 ++--- distutils/command/_framework_compat.py | 4 +-- distutils/command/bdist.py | 2 +- distutils/command/bdist_dumb.py | 7 ++-- distutils/command/bdist_rpm.py | 10 +++--- distutils/command/build.py | 3 +- distutils/command/build_clib.py | 3 +- distutils/command/build_ext.py | 17 +++++----- distutils/command/build_py.py | 8 ++--- distutils/command/build_scripts.py | 9 ++--- distutils/command/check.py | 4 +-- distutils/command/clean.py | 3 +- distutils/command/config.py | 2 +- distutils/command/install.py | 21 +++++------- distutils/command/install_data.py | 1 + distutils/command/install_egg_info.py | 4 +-- distutils/command/install_lib.py | 3 +- distutils/command/install_scripts.py | 3 +- distutils/command/register.py | 2 +- distutils/command/sdist.py | 14 ++++---- distutils/command/upload.py | 10 +++--- distutils/config.py | 2 +- distutils/core.py | 15 ++++----- distutils/cygwinccompiler.py | 15 ++++----- distutils/dir_util.py | 5 +-- distutils/dist.py | 20 +++++------ distutils/extension.py | 3 +- distutils/fancy_getopt.py | 9 ++--- distutils/file_util.py | 7 ++-- distutils/filelist.py | 8 ++--- distutils/log.py | 1 - distutils/msvc9compiler.py | 11 +++--- distutils/msvccompiler.py | 16 +++++---- distutils/spawn.py | 6 ++-- distutils/sysconfig.py | 6 ++-- distutils/tests/__init__.py | 3 +- distutils/tests/py37compat.py | 2 +- distutils/tests/support.py | 11 +++--- distutils/tests/test_archive_util.py | 21 ++++++------ distutils/tests/test_bdist_dumb.py | 7 ++-- distutils/tests/test_bdist_rpm.py | 12 +++---- distutils/tests/test_build.py | 1 - distutils/tests/test_build_clib.py | 7 ++-- distutils/tests/test_build_ext.py | 45 ++++++++++++------------- distutils/tests/test_build_py.py | 8 ++--- distutils/tests/test_build_scripts.py | 8 ++--- distutils/tests/test_ccompiler.py | 7 ++-- distutils/tests/test_check.py | 7 ++-- distutils/tests/test_clean.py | 1 - distutils/tests/test_cmd.py | 4 +-- distutils/tests/test_config.py | 3 +- distutils/tests/test_config_cmd.py | 7 ++-- distutils/tests/test_core.py | 5 ++- distutils/tests/test_cygwinccompiler.py | 13 ++++--- distutils/tests/test_dir_util.py | 16 ++++----- distutils/tests/test_dist.py | 19 +++++------ distutils/tests/test_extension.py | 4 +-- distutils/tests/test_file_util.py | 7 ++-- distutils/tests/test_filelist.py | 11 +++--- distutils/tests/test_install.py | 15 ++++----- distutils/tests/test_install_data.py | 5 ++- distutils/tests/test_install_headers.py | 5 ++- distutils/tests/test_install_lib.py | 11 +++--- distutils/tests/test_install_scripts.py | 3 +- distutils/tests/test_log.py | 1 - distutils/tests/test_modified.py | 7 ++-- distutils/tests/test_msvc9compiler.py | 4 +-- distutils/tests/test_msvccompiler.py | 8 ++--- distutils/tests/test_register.py | 3 +- distutils/tests/test_sdist.py | 21 ++++++------ distutils/tests/test_spawn.py | 11 +++--- distutils/tests/test_sysconfig.py | 15 ++++----- distutils/tests/test_text_file.py | 6 ++-- distutils/tests/test_unixccompiler.py | 7 ++-- distutils/tests/test_upload.py | 6 ++-- distutils/tests/test_util.py | 24 ++++++------- distutils/tests/test_version.py | 7 ++-- distutils/tests/unix_compat.py | 1 - distutils/unixccompiler.py | 10 +++--- distutils/util.py | 6 ++-- distutils/version.py | 2 +- distutils/versionpredicate.py | 4 +-- distutils/zosccompiler.py | 5 +-- 92 files changed, 344 insertions(+), 400 deletions(-) diff --git a/conftest.py b/conftest.py index 06ce3bc6c86..3ce3411535f 100644 --- a/conftest.py +++ b/conftest.py @@ -1,12 +1,11 @@ +import logging import os -import sys -import platform import pathlib -import logging +import platform +import sys -import pytest import path - +import pytest collect_ignore = [] @@ -93,8 +92,7 @@ def temp_cwd(tmp_path): @pytest.fixture def pypirc(request, save_env, distutils_managed_tempdir): - from distutils.core import PyPIRCCommand - from distutils.core import Distribution + from distutils.core import Distribution, PyPIRCCommand self = request.instance self.tmp_dir = self.mkdtemp() diff --git a/distutils/__init__.py b/distutils/__init__.py index 1a188c35cb6..e374d5c5604 100644 --- a/distutils/__init__.py +++ b/distutils/__init__.py @@ -1,5 +1,5 @@ -import sys import importlib +import sys __version__, _, _ = sys.version.partition(' ') diff --git a/distutils/_log.py b/distutils/_log.py index 4a2ae0acb86..0148f157ff3 100644 --- a/distutils/_log.py +++ b/distutils/_log.py @@ -1,4 +1,3 @@ import logging - log = logging.getLogger() diff --git a/distutils/_macos_compat.py b/distutils/_macos_compat.py index 17769e9154b..76ecb96abe4 100644 --- a/distutils/_macos_compat.py +++ b/distutils/_macos_compat.py @@ -1,5 +1,5 @@ -import sys import importlib +import sys def bypass_compiler_fixup(cmd, args): diff --git a/distutils/_modified.py b/distutils/_modified.py index fbb95a8f279..78485dc25e5 100644 --- a/distutils/_modified.py +++ b/distutils/_modified.py @@ -3,9 +3,9 @@ import functools import os.path +from ._functools import splat from .errors import DistutilsFileError from .py39compat import zip_strict -from ._functools import splat def _newer(source, target): diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 4f081c7e925..d08910ecf97 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -13,28 +13,28 @@ # ported to VS 2005 and VS 2008 by Christian Heimes # ported to VS 2015 by Steve Dower +import contextlib import os import subprocess -import contextlib -import warnings import unittest.mock as mock +import warnings with contextlib.suppress(ImportError): import winreg +from itertools import count + +from ._log import log +from .ccompiler import CCompiler, gen_lib_options from .errors import ( + CompileError, DistutilsExecError, DistutilsPlatformError, - CompileError, LibError, LinkError, ) -from .ccompiler import CCompiler, gen_lib_options -from ._log import log from .util import get_platform -from itertools import count - def _find_vc2015(): try: diff --git a/distutils/archive_util.py b/distutils/archive_util.py index 7f9e1e00ccd..052f6e46466 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -4,8 +4,8 @@ that sort of thing).""" import os -from warnings import warn import sys +from warnings import warn try: import zipfile @@ -13,10 +13,10 @@ zipfile = None +from ._log import log +from .dir_util import mkpath from .errors import DistutilsExecError from .spawn import spawn -from .dir_util import mkpath -from ._log import log try: from pwd import getpwnam diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index d496d5d452d..c1341e43cb4 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -14,18 +14,17 @@ import os import warnings +from ._log import log +from ._modified import newer +from .ccompiler import CCompiler, gen_preprocess_options from .errors import ( - DistutilsExecError, CompileError, + DistutilsExecError, LibError, LinkError, UnknownFileError, ) -from .ccompiler import CCompiler, gen_preprocess_options from .file_util import write_file -from ._modified import newer -from ._log import log - warnings.warn( "bcppcompiler is deprecated and slated to be removed " diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index cdfe9d74eff..03181cfb7cf 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -3,25 +3,25 @@ Contains CCompiler, an abstract base class that defines the interface for the Distutils compiler abstraction model.""" -import sys import os import re +import sys import warnings +from ._itertools import always_iterable +from ._log import log +from ._modified import newer_group +from .dir_util import mkpath from .errors import ( CompileError, + DistutilsModuleError, + DistutilsPlatformError, LinkError, UnknownFileError, - DistutilsPlatformError, - DistutilsModuleError, ) -from .spawn import spawn from .file_util import move_file -from .dir_util import mkpath -from ._modified import newer_group -from .util import split_quoted, execute -from ._log import log -from ._itertools import always_iterable +from .spawn import spawn +from .util import execute, split_quoted class CCompiler: diff --git a/distutils/cmd.py b/distutils/cmd.py index 8849474cd7d..02dbf165f5c 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -4,14 +4,14 @@ in the distutils.command package. """ -import sys +import logging import os import re -import logging +import sys -from .errors import DistutilsOptionError -from . import util, dir_util, file_util, archive_util, _modified +from . import _modified, archive_util, dir_util, file_util, util from ._log import log +from .errors import DistutilsOptionError class Command: diff --git a/distutils/command/_framework_compat.py b/distutils/command/_framework_compat.py index 397ebf823e4..00d34bc7d8c 100644 --- a/distutils/command/_framework_compat.py +++ b/distutils/command/_framework_compat.py @@ -2,10 +2,10 @@ Backward compatibility for homebrew builds on macOS. """ -import sys -import os import functools +import os import subprocess +import sys import sysconfig diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py index 237b14656f8..f681b5531d9 100644 --- a/distutils/command/bdist.py +++ b/distutils/command/bdist.py @@ -7,7 +7,7 @@ import warnings from ..core import Command -from ..errors import DistutilsPlatformError, DistutilsOptionError +from ..errors import DistutilsOptionError, DistutilsPlatformError from ..util import get_platform diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py index 5880ad2ba40..41adf014187 100644 --- a/distutils/command/bdist_dumb.py +++ b/distutils/command/bdist_dumb.py @@ -5,12 +5,13 @@ $exec_prefix).""" import os +from distutils._log import log + from ..core import Command -from ..util import get_platform -from ..dir_util import remove_tree, ensure_relative +from ..dir_util import ensure_relative, remove_tree from ..errors import DistutilsPlatformError from ..sysconfig import get_python_version -from distutils._log import log +from ..util import get_platform class bdist_dumb(Command): diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 64af0db0cf3..6a75e32fb12 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -3,21 +3,21 @@ Implements the Distutils 'bdist_rpm' command (create RPM source and binary distributions).""" +import os import subprocess import sys -import os +from distutils._log import log from ..core import Command from ..debug import DEBUG -from ..file_util import write_file from ..errors import ( + DistutilsExecError, + DistutilsFileError, DistutilsOptionError, DistutilsPlatformError, - DistutilsFileError, - DistutilsExecError, ) +from ..file_util import write_file from ..sysconfig import get_python_version -from distutils._log import log class bdist_rpm(Command): diff --git a/distutils/command/build.py b/distutils/command/build.py index d8704e35838..d18ed503e36 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -2,8 +2,9 @@ Implements the Distutils 'build' command.""" -import sys import os +import sys + from ..core import Command from ..errors import DistutilsOptionError from ..util import get_platform diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py index b3f679b67da..811e607e70a 100644 --- a/distutils/command/build_clib.py +++ b/distutils/command/build_clib.py @@ -15,10 +15,11 @@ # cut 'n paste. Sigh. import os +from distutils._log import log + from ..core import Command from ..errors import DistutilsSetupError from ..sysconfig import customize_compiler -from distutils._log import log def show_compilers(): diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index a15781f28a2..aa9ed578f86 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -8,25 +8,24 @@ import os import re import sys +from distutils._log import log +from site import USER_BASE + +from .._modified import newer_group from ..core import Command from ..errors import ( - DistutilsOptionError, - DistutilsSetupError, CCompilerError, - DistutilsError, CompileError, + DistutilsError, + DistutilsOptionError, DistutilsPlatformError, + DistutilsSetupError, ) -from ..sysconfig import customize_compiler, get_python_version -from ..sysconfig import get_config_h_filename -from .._modified import newer_group from ..extension import Extension +from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version from ..util import get_platform -from distutils._log import log from . import py37compat -from site import USER_BASE - # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). extension_name_re = re.compile(r'^[a-zA-Z_][a-zA-Z_0-9]*(\.[a-zA-Z_][a-zA-Z_0-9]*)*$') diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py index e16011d46a9..a15d0af5190 100644 --- a/distutils/command/build_py.py +++ b/distutils/command/build_py.py @@ -2,15 +2,15 @@ Implements the Distutils 'build_py' command.""" -import os +import glob import importlib.util +import os import sys -import glob +from distutils._log import log from ..core import Command -from ..errors import DistutilsOptionError, DistutilsFileError +from ..errors import DistutilsFileError, DistutilsOptionError from ..util import convert_path -from distutils._log import log class build_py(Command): diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 29d9c27829a..37bc5850383 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -4,13 +4,14 @@ import os import re -from stat import ST_MODE +import tokenize from distutils import sysconfig -from ..core import Command +from distutils._log import log +from stat import ST_MODE + from .._modified import newer +from ..core import Command from ..util import convert_path -from distutils._log import log -import tokenize shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$') """ diff --git a/distutils/command/check.py b/distutils/command/check.py index 28f55fb9142..6b42a34f6d5 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -9,10 +9,10 @@ from ..errors import DistutilsSetupError with contextlib.suppress(ImportError): - import docutils.utils - import docutils.parsers.rst import docutils.frontend import docutils.nodes + import docutils.parsers.rst + import docutils.utils class SilentReporter(docutils.utils.Reporter): def __init__( diff --git a/distutils/command/clean.py b/distutils/command/clean.py index 9413f7cfcb4..4167a83fb3b 100644 --- a/distutils/command/clean.py +++ b/distutils/command/clean.py @@ -5,9 +5,10 @@ # contributed by Bastian Kleineidam , added 2000-03-18 import os +from distutils._log import log + from ..core import Command from ..dir_util import remove_tree -from distutils._log import log class clean(Command): diff --git a/distutils/command/config.py b/distutils/command/config.py index 573741d7726..38a5ff5159b 100644 --- a/distutils/command/config.py +++ b/distutils/command/config.py @@ -12,11 +12,11 @@ import os import pathlib import re +from distutils._log import log from ..core import Command from ..errors import DistutilsExecError from ..sysconfig import customize_compiler -from distutils._log import log LANG_EXT = {"c": ".c", "c++": ".cxx"} diff --git a/distutils/command/install.py b/distutils/command/install.py index 927c3ed3a29..575cebdbc8a 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -2,25 +2,22 @@ Implements the Distutils 'install' command.""" -import sys -import os import contextlib -import sysconfig import itertools - +import os +import sys +import sysconfig from distutils._log import log +from site import USER_BASE, USER_SITE + +from .. import _collections from ..core import Command from ..debug import DEBUG -from ..sysconfig import get_config_vars -from ..file_util import write_file -from ..util import convert_path, subst_vars, change_root -from ..util import get_platform from ..errors import DistutilsOptionError, DistutilsPlatformError +from ..file_util import write_file +from ..sysconfig import get_config_vars +from ..util import change_root, convert_path, get_platform, subst_vars from . import _framework_compat as fw -from .. import _collections - -from site import USER_BASE -from site import USER_SITE HAS_USER_SITE = True diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py index 31ae4350dc1..b63a1af25ea 100644 --- a/distutils/command/install_data.py +++ b/distutils/command/install_data.py @@ -6,6 +6,7 @@ # contributed by Bastian Kleineidam import os + from ..core import Command from ..util import change_root, convert_path diff --git a/distutils/command/install_egg_info.py b/distutils/command/install_egg_info.py index f3e8f3447dc..4fbb3440abc 100644 --- a/distutils/command/install_egg_info.py +++ b/distutils/command/install_egg_info.py @@ -6,12 +6,12 @@ """ import os -import sys import re +import sys -from ..cmd import Command from .. import dir_util from .._log import log +from ..cmd import Command class install_egg_info(Command): diff --git a/distutils/command/install_lib.py b/distutils/command/install_lib.py index be4c2433212..b1f346f018b 100644 --- a/distutils/command/install_lib.py +++ b/distutils/command/install_lib.py @@ -3,14 +3,13 @@ Implements the Distutils 'install_lib' command (install all Python modules).""" -import os import importlib.util +import os import sys from ..core import Command from ..errors import DistutilsOptionError - # Extension for Python source files. PYTHON_SOURCE_EXTENSION = ".py" diff --git a/distutils/command/install_scripts.py b/distutils/command/install_scripts.py index 20f07aaa273..e66b13a16df 100644 --- a/distutils/command/install_scripts.py +++ b/distutils/command/install_scripts.py @@ -6,10 +6,11 @@ # contributed by Bastian Kleineidam import os -from ..core import Command from distutils._log import log from stat import ST_MODE +from ..core import Command + class install_scripts(Command): description = "install scripts (Python or otherwise)" diff --git a/distutils/command/register.py b/distutils/command/register.py index 5a24246ccba..e5e6b379ad4 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -10,10 +10,10 @@ import logging import urllib.parse import urllib.request +from distutils._log import log from warnings import warn from ..core import PyPIRCCommand -from distutils._log import log class register(PyPIRCCommand): diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index b76cb9bc73e..6414ef5c066 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -4,27 +4,25 @@ import os import sys +from distutils import archive_util, dir_util, file_util +from distutils._log import log from glob import glob -from warnings import warn from itertools import filterfalse +from warnings import warn from ..core import Command -from distutils import dir_util -from distutils import file_util -from distutils import archive_util -from ..text_file import TextFile +from ..errors import DistutilsOptionError, DistutilsTemplateError from ..filelist import FileList -from distutils._log import log +from ..text_file import TextFile from ..util import convert_path -from ..errors import DistutilsOptionError, DistutilsTemplateError def show_formats(): """Print all possible values for the 'formats' option (used by the "--help-formats" command-line option). """ - from ..fancy_getopt import FancyGetopt from ..archive_util import ARCHIVE_FORMATS + from ..fancy_getopt import FancyGetopt formats = [] for format in ARCHIVE_FORMATS.keys(): diff --git a/distutils/command/upload.py b/distutils/command/upload.py index a9124f2b718..e61a9ea8a54 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -5,18 +5,18 @@ index). """ -import os -import io import hashlib +import io import logging +import os from base64 import standard_b64encode -from urllib.request import urlopen, Request, HTTPError from urllib.parse import urlparse -from ..errors import DistutilsError, DistutilsOptionError +from urllib.request import HTTPError, Request, urlopen + from ..core import PyPIRCCommand +from ..errors import DistutilsError, DistutilsOptionError from ..spawn import spawn - # PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256) # https://bugs.python.org/issue40698 _FILE_CONTENT_DIGESTS = { diff --git a/distutils/config.py b/distutils/config.py index e0defd77e6f..83f96a9eec9 100644 --- a/distutils/config.py +++ b/distutils/config.py @@ -4,8 +4,8 @@ that uses .pypirc in the distutils.command package. """ -import os import email.message +import os from configparser import RawConfigParser from .cmd import Command diff --git a/distutils/core.py b/distutils/core.py index 799de9489ce..309ce696fa0 100644 --- a/distutils/core.py +++ b/distutils/core.py @@ -10,21 +10,20 @@ import sys import tokenize +from .cmd import Command +from .config import PyPIRCCommand from .debug import DEBUG + +# Mainly import these so setup scripts can "from distutils.core import" them. +from .dist import Distribution from .errors import ( - DistutilsSetupError, - DistutilsError, CCompilerError, DistutilsArgError, + DistutilsError, + DistutilsSetupError, ) - -# Mainly import these so setup scripts can "from distutils.core import" them. -from .dist import Distribution -from .cmd import Command -from .config import PyPIRCCommand from .extension import Extension - __all__ = ['Distribution', 'Command', 'PyPIRCCommand', 'Extension', 'setup'] # This is a barebones help message generated displayed when the user diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 20609504154..539f09d8f30 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -6,26 +6,25 @@ cygwin in no-cygwin mode). """ +import copy import os import pathlib import re -import sys -import copy import shlex +import sys import warnings from subprocess import check_output -from .unixccompiler import UnixCCompiler -from .file_util import write_file +from ._collections import RangeMap from .errors import ( - DistutilsExecError, - DistutilsPlatformError, CCompilerError, CompileError, + DistutilsExecError, + DistutilsPlatformError, ) +from .file_util import write_file +from .unixccompiler import UnixCCompiler from .version import LooseVersion, suppress_known_deprecation -from ._collections import RangeMap - _msvcr_lookup = RangeMap.left( { diff --git a/distutils/dir_util.py b/distutils/dir_util.py index 819fe56f6db..2021bed82e1 100644 --- a/distutils/dir_util.py +++ b/distutils/dir_util.py @@ -2,10 +2,11 @@ Utility functions for manipulating directories and directory trees.""" -import os import errno -from .errors import DistutilsInternalError, DistutilsFileError +import os + from ._log import log +from .errors import DistutilsFileError, DistutilsInternalError # cache for by mkpath() -- in addition to cheapening redundant calls, # eliminates redundant "creating /foo/bar/baz" messages in dry-run mode diff --git a/distutils/dist.py b/distutils/dist.py index bbea155556a..1759120c923 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -4,12 +4,12 @@ being built/installed/distributed. """ -import sys -import os -import re -import pathlib import contextlib import logging +import os +import pathlib +import re +import sys from email import message_from_file try: @@ -17,16 +17,16 @@ except ImportError: warnings = None +from ._log import log +from .debug import DEBUG from .errors import ( - DistutilsOptionError, - DistutilsModuleError, DistutilsArgError, DistutilsClassError, + DistutilsModuleError, + DistutilsOptionError, ) from .fancy_getopt import FancyGetopt, translate_longopt -from .util import check_environ, strtobool, rfc822_escape -from ._log import log -from .debug import DEBUG +from .util import check_environ, rfc822_escape, strtobool # 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 @@ -634,8 +634,8 @@ def _show_help(self, parser, global_options=1, display_options=1, commands=[]): in 'commands'. """ # late import because of mutual dependence between these modules - from distutils.core import gen_usage from distutils.cmd import Command + from distutils.core import gen_usage if global_options: if display_options: diff --git a/distutils/extension.py b/distutils/extension.py index 00ca61d569b..94e71635d93 100644 --- a/distutils/extension.py +++ b/distutils/extension.py @@ -139,8 +139,7 @@ def __repr__(self): def read_setup_file(filename): # noqa: C901 """Reads a Setup file and returns Extension instances.""" - from distutils.sysconfig import parse_makefile, expand_makefile_vars, _variable_rx - + from distutils.sysconfig import _variable_rx, expand_makefile_vars, parse_makefile from distutils.text_file import TextFile from distutils.util import split_quoted diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index e41b6064bd0..cb646c6d9ba 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -8,11 +8,12 @@ * options set attributes of a passed-in object """ -import sys -import string -import re import getopt -from .errors import DistutilsGetoptError, DistutilsArgError +import re +import string +import sys + +from .errors import DistutilsArgError, DistutilsGetoptError # Much like command_re in distutils.core, this is close to but not quite # the same as a Python NAME -- except, in the spirit of most GNU diff --git a/distutils/file_util.py b/distutils/file_util.py index 6c8193e9b7b..960def9cf97 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -4,8 +4,9 @@ """ import os -from .errors import DistutilsFileError + from ._log import log +from .errors import DistutilsFileError # for generating verbose output in 'copy_file()' _copy_action = {None: 'copying', 'hard': 'hard linking', 'sym': 'symbolically linking'} @@ -101,7 +102,7 @@ def copy_file( # noqa: C901 # (not update) and (src newer than dst). from distutils._modified import newer - from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE + from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME if not os.path.isfile(src): raise DistutilsFileError( @@ -175,8 +176,8 @@ def move_file(src, dst, verbose=1, dry_run=0): # noqa: C901 Handles cross-device moves on Unix using 'copy_file()'. What about other systems??? """ - from os.path import exists, isfile, isdir, basename, dirname import errno + from os.path import basename, dirname, exists, isdir, isfile if verbose >= 1: log.info("moving %s -> %s", src, dst) diff --git a/distutils/filelist.py b/distutils/filelist.py index 3205762654d..5ce47936a99 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -4,14 +4,14 @@ and building lists of files. """ -import os -import re import fnmatch import functools +import os +import re -from .util import convert_path -from .errors import DistutilsTemplateError, DistutilsInternalError from ._log import log +from .errors import DistutilsInternalError, DistutilsTemplateError +from .util import convert_path class FileList: diff --git a/distutils/log.py b/distutils/log.py index 239f3158506..8abb09cfa2b 100644 --- a/distutils/log.py +++ b/distutils/log.py @@ -9,7 +9,6 @@ from ._log import log as _global_log - DEBUG = logging.DEBUG INFO = logging.INFO WARN = logging.WARN diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index 402c0c0620e..6a0105e4847 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -13,24 +13,23 @@ # ported to VS2005 and VS 2008 by Christian Heimes import os +import re import subprocess import sys -import re import warnings +import winreg +from ._log import log +from .ccompiler import CCompiler, gen_lib_options from .errors import ( + CompileError, DistutilsExecError, DistutilsPlatformError, - CompileError, LibError, LinkError, ) -from .ccompiler import CCompiler, gen_lib_options -from ._log import log from .util import get_platform -import winreg - warnings.warn( "msvc9compiler is deprecated and slated to be removed " "in the future. Please discontinue use or file an issue " diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index b8694dd6d83..ac8b68c08c6 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -8,18 +8,19 @@ # hacked by Robin Becker and Thomas Heller to do a better job of # finding DevStudio (through the registry) -import sys import os +import sys import warnings + +from ._log import log +from .ccompiler import CCompiler, gen_lib_options from .errors import ( + CompileError, DistutilsExecError, DistutilsPlatformError, - CompileError, LibError, LinkError, ) -from .ccompiler import CCompiler, gen_lib_options -from ._log import log _can_read_reg = False try: @@ -681,7 +682,8 @@ def set_path_env_var(self, name): if get_build_version() >= 8.0: log.debug("Importing new compiler from distutils.msvc9compiler") OldMSVCCompiler = MSVCCompiler - from distutils.msvc9compiler import MSVCCompiler - # get_build_architecture not really relevant now we support cross-compile - from distutils.msvc9compiler import MacroExpander # noqa: F811 + from distutils.msvc9compiler import ( + MacroExpander, # noqa: F811 + MSVCCompiler, + ) diff --git a/distutils/spawn.py b/distutils/spawn.py index 48adceb1146..046b5bbb822 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -6,13 +6,13 @@ executable name. """ -import sys import os import subprocess +import sys -from .errors import DistutilsExecError -from .debug import DEBUG from ._log import log +from .debug import DEBUG +from .errors import DistutilsExecError def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901 diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 40215b83478..1a38e9fa797 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -9,16 +9,16 @@ Email: """ -import os import functools +import os +import pathlib import re import sys import sysconfig -import pathlib -from .errors import DistutilsPlatformError from . import py39compat from ._functools import pass_none +from .errors import DistutilsPlatformError IS_PYPY = '__pypy__' in sys.builtin_module_names diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 6d9b853215a..c475e5d0f25 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -17,8 +17,7 @@ def missing_compiler_executable(cmd_names=[]): # pragma: no cover missing. """ - from distutils import ccompiler, sysconfig, spawn - from distutils import errors + from distutils import ccompiler, errors, spawn, sysconfig compiler = ccompiler.new_compiler() sysconfig.customize_compiler(compiler) diff --git a/distutils/tests/py37compat.py b/distutils/tests/py37compat.py index e5d406a3b6e..76d3551c49e 100644 --- a/distutils/tests/py37compat.py +++ b/distutils/tests/py37compat.py @@ -1,6 +1,6 @@ import os -import sys import platform +import sys def subprocess_args_compat(*args): diff --git a/distutils/tests/support.py b/distutils/tests/support.py index ddf7bf1dba9..9cd2b8a9eed 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -1,18 +1,17 @@ """Support code for distutils test cases.""" +import itertools import os -import sys +import pathlib import shutil -import tempfile +import sys import sysconfig -import itertools -import pathlib +import tempfile +from distutils.core import Distribution import pytest from more_itertools import always_iterable -from distutils.core import Distribution - @pytest.mark.usefixtures('distutils_managed_tempdir') class TempdirManager: diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 2b5eafd27e6..145cce915db 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -1,31 +1,30 @@ """Tests for distutils.archive_util.""" +import functools +import operator import os +import pathlib import sys import tarfile -from os.path import splitdrive import warnings -import functools -import operator -import pathlib - -import pytest -import path - from distutils import archive_util from distutils.archive_util import ( + ARCHIVE_FORMATS, check_archive_formats, + make_archive, make_tarball, make_zipfile, - make_archive, - ARCHIVE_FORMATS, ) from distutils.spawn import spawn from distutils.tests import support +from os.path import splitdrive from test.support import patch -from .unix_compat import require_unix_id, require_uid_0, grp, pwd, UID_0_SUPPORT + +import path +import pytest from .py38compat import check_warnings +from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id def can_fs_encode(filename): diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index cfe7fa9e62b..78928fea24c 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -3,13 +3,12 @@ import os import sys import zipfile - -import pytest - -from distutils.core import Distribution from distutils.command.bdist_dumb import bdist_dumb +from distutils.core import Distribution from distutils.tests import support +import pytest + SETUP_PY = """\ from distutils.core import setup import foo diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index e6804088da5..769623cbb86 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -1,17 +1,15 @@ """Tests for distutils.command.bdist_rpm.""" -import sys import os - -import pytest - -from distutils.core import Distribution +import sys from distutils.command.bdist_rpm import bdist_rpm -from distutils.tests import support +from distutils.core import Distribution from distutils.spawn import find_executable # noqa: F401 +from distutils.tests import support -from .py38compat import requires_zlib +import pytest +from .py38compat import requires_zlib SETUP_PY = """\ from distutils.core import setup diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index 8617fa99197..25483ad76b1 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -2,7 +2,6 @@ import os import sys - from distutils.command.build import build from distutils.tests import support from sysconfig import get_platform diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index f8554542564..9c69b3e7fc5 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -1,12 +1,11 @@ """Tests for distutils.command.build_clib.""" import os - -import pytest - from distutils.command.build_clib import build_clib from distutils.errors import DistutilsSetupError -from distutils.tests import support, missing_compiler_executable +from distutils.tests import missing_compiler_executable, support + +import pytest class TestBuildCLib(support.TempdirManager): diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index ae66bc4eb84..ca5d9d57cd0 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -1,37 +1,36 @@ -import sys -import os -from io import StringIO -import textwrap -import site import contextlib -import platform -import tempfile import importlib -import shutil +import os +import platform import re - -import path -import pytest -import jaraco.path - -from distutils.core import Distribution -from distutils.command.build_ext import build_ext +import shutil +import site +import sys +import tempfile +import textwrap from distutils import sysconfig -from distutils.tests import missing_compiler_executable -from distutils.tests.support import ( - TempdirManager, - copy_xxmodule_c, - fixup_build_ext, -) -from distutils.extension import Extension +from distutils.command.build_ext import build_ext +from distutils.core import Distribution from distutils.errors import ( CompileError, DistutilsPlatformError, DistutilsSetupError, UnknownFileError, ) - +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 io import StringIO from test import support + +import jaraco.path +import path +import pytest + from . import py38compat as import_helper diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 6730878e962..8bc0e98a4f9 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -2,16 +2,14 @@ import os import sys - -import pytest -import jaraco.path - from distutils.command.build_py import build_py from distutils.core import Distribution from distutils.errors import DistutilsFileError - from distutils.tests import support +import jaraco.path +import pytest + @support.combine_markers class TestBuildPy(support.TempdirManager): diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index 7e05ec5f9a6..208b1f6e658 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -2,15 +2,13 @@ import os import textwrap - -import jaraco.path - +from distutils import sysconfig from distutils.command.build_scripts import build_scripts from distutils.core import Distribution -from distutils import sysconfig - from distutils.tests import support +import jaraco.path + class TestBuildScripts(support.TempdirManager): def test_default_settings(self): diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index b6512e6d778..d23b907cad8 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -1,13 +1,12 @@ import os -import sys import platform -import textwrap +import sys import sysconfig +import textwrap +from distutils import ccompiler import pytest -from distutils import ccompiler - def _make_strs(paths): """ diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index 8215300b976..580cb2a2677 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -2,12 +2,11 @@ import os import textwrap - -import pytest - from distutils.command.check import check -from distutils.tests import support from distutils.errors import DistutilsSetupError +from distutils.tests import support + +import pytest try: import pygments diff --git a/distutils/tests/test_clean.py b/distutils/tests/test_clean.py index e2459aa0c1e..9b11fa40f7b 100644 --- a/distutils/tests/test_clean.py +++ b/distutils/tests/test_clean.py @@ -1,7 +1,6 @@ """Tests for distutils.command.clean.""" import os - from distutils.command.clean import clean from distutils.tests import support diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py index 684662d32e2..f366aa6522e 100644 --- a/distutils/tests/test_cmd.py +++ b/distutils/tests/test_cmd.py @@ -1,11 +1,11 @@ """Tests for distutils.cmd.""" import os - +from distutils import debug from distutils.cmd import Command from distutils.dist import Distribution from distutils.errors import DistutilsOptionError -from distutils import debug + import pytest diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 11c23d837ed..be5ae0a6875 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -1,11 +1,10 @@ """Tests for distutils.pypirc.pypirc.""" import os +from distutils.tests import support import pytest -from distutils.tests import support - PYPIRC = """\ [distutils] diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 90c8f906791..fc0a7885cd1 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -2,15 +2,14 @@ import os import sys +from distutils._log import log +from distutils.command.config import config, dump_file +from distutils.tests import missing_compiler_executable, support import more_itertools import path import pytest -from distutils.command.config import dump_file, config -from distutils.tests import support, missing_compiler_executable -from distutils._log import log - @pytest.fixture(autouse=True) def info_log(request, monkeypatch): diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index 95aa299889a..5916718027e 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -1,14 +1,13 @@ """Tests for distutils.core.""" -import io import distutils.core +import io import os import sys +from distutils.dist import Distribution import pytest -from distutils.dist import Distribution - # setup script that uses __file__ setup_using___file__ = """\ diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index fc67d75f829..0a66193d354 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -1,19 +1,18 @@ """Tests for distutils.cygwinccompiler.""" -import sys import os - -import pytest - +import sys +from distutils import sysconfig from distutils.cygwinccompiler import ( - check_config_h, - CONFIG_H_OK, CONFIG_H_NOTOK, + CONFIG_H_OK, CONFIG_H_UNCERTAIN, + check_config_h, get_msvcr, ) from distutils.tests import support -from distutils import sysconfig + +import pytest @pytest.fixture(autouse=True) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 6fc9ed08835..84cda619ba4 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -3,22 +3,20 @@ import os import stat import unittest.mock as mock - -import jaraco.path -import path -import pytest - from distutils import dir_util, errors from distutils.dir_util import ( - mkpath, - remove_tree, - create_tree, copy_tree, + create_tree, ensure_relative, + mkpath, + remove_tree, ) - from distutils.tests import support +import jaraco.path +import path +import pytest + @pytest.fixture(autouse=True) def stuff(request, monkeypatch, distutils_managed_tempdir): diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 8e52873dced..9ed4d16dd8e 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -1,24 +1,21 @@ """Tests for distutils.dist.""" -import os -import io import email -import email.policy import email.generator +import email.policy +import functools +import io +import os import sys -import warnings import textwrap -import functools import unittest.mock as mock - -import pytest -import jaraco.path - -from distutils.dist import Distribution, fix_help_options +import warnings from distutils.cmd import Command - +from distutils.dist import Distribution, fix_help_options from distutils.tests import support +import jaraco.path +import pytest pydistutils_cfg = '.' * (os.name == 'posix') + 'pydistutils.cfg' diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 297ae44bfee..77bb147bfdc 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -2,11 +2,11 @@ import os import warnings +from distutils.extension import Extension, read_setup_file -from distutils.extension import read_setup_file, Extension +import pytest from .py38compat import check_warnings -import pytest class TestExtension: diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 6c7019140e6..4c2abd2453d 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -1,15 +1,14 @@ """Tests for distutils.file_util.""" -import os import errno +import os import unittest.mock as mock +from distutils.errors import DistutilsFileError +from distutils.file_util import copy_file, move_file import jaraco.path import pytest -from distutils.file_util import move_file, copy_file -from distutils.errors import DistutilsFileError - @pytest.fixture(autouse=True) def stuff(request, tmp_path): diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index bf1a9d9b45f..6a379a6323d 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -1,20 +1,17 @@ """Tests for distutils.filelist.""" +import logging import os import re -import logging - -from distutils import debug +from distutils import debug, filelist from distutils.errors import DistutilsTemplateError -from distutils.filelist import glob_to_re, translate_pattern, FileList -from distutils import filelist +from distutils.filelist import FileList, glob_to_re, translate_pattern -import pytest import jaraco.path +import pytest from . import py38compat as os_helper - MANIFEST_IN = """\ include ok include xo diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 08c72c1be00..08f0f83993e 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -1,23 +1,20 @@ """Tests for distutils.command.install.""" +import logging import os -import sys -import site import pathlib -import logging - -import pytest - +import site +import sys from distutils import sysconfig -from distutils.command.install import install from distutils.command import install as install_module from distutils.command.build_ext import build_ext -from distutils.command.install import INSTALL_SCHEMES +from distutils.command.install import INSTALL_SCHEMES, install from distutils.core import Distribution from distutils.errors import DistutilsOptionError from distutils.extension import Extension +from distutils.tests import missing_compiler_executable, support -from distutils.tests import support, missing_compiler_executable +import pytest def _make_ext_name(modname): diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py index 198c10da8d9..e453d01f1af 100644 --- a/distutils/tests/test_install_data.py +++ b/distutils/tests/test_install_data.py @@ -1,12 +1,11 @@ """Tests for distutils.command.install_data.""" import os - -import pytest - from distutils.command.install_data import install_data from distutils.tests import support +import pytest + @pytest.mark.usefixtures('save_env') class TestInstallData( diff --git a/distutils/tests/test_install_headers.py b/distutils/tests/test_install_headers.py index 8b86b6eaed6..2c74f06b976 100644 --- a/distutils/tests/test_install_headers.py +++ b/distutils/tests/test_install_headers.py @@ -1,12 +1,11 @@ """Tests for distutils.command.install_headers.""" import os - -import pytest - from distutils.command.install_headers import install_headers from distutils.tests import support +import pytest + @pytest.mark.usefixtures('save_env') class TestInstallHeaders( diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py index 0efe39fe86d..964106fa007 100644 --- a/distutils/tests/test_install_lib.py +++ b/distutils/tests/test_install_lib.py @@ -1,15 +1,14 @@ """Tests for distutils.command.install_data.""" -import sys -import os import importlib.util - -import pytest - +import os +import sys from distutils.command.install_lib import install_lib +from distutils.errors import DistutilsOptionError from distutils.extension import Extension from distutils.tests import support -from distutils.errors import DistutilsOptionError + +import pytest @support.combine_markers diff --git a/distutils/tests/test_install_scripts.py b/distutils/tests/test_install_scripts.py index 4da2acb6a87..5d9f13a4267 100644 --- a/distutils/tests/test_install_scripts.py +++ b/distutils/tests/test_install_scripts.py @@ -1,11 +1,10 @@ """Tests for distutils.command.install_scripts.""" import os - from distutils.command.install_scripts import install_scripts from distutils.core import Distribution - from distutils.tests import support + from . import test_build_scripts diff --git a/distutils/tests/test_log.py b/distutils/tests/test_log.py index ec6a0c80511..d67779fc9fa 100644 --- a/distutils/tests/test_log.py +++ b/distutils/tests/test_log.py @@ -1,7 +1,6 @@ """Tests for distutils.log""" import logging - from distutils._log import log diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index 5fde7a5971d..2bd82346cf0 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -2,13 +2,12 @@ import os import types - -import pytest - -from distutils._modified import newer, newer_pairwise, newer_group, newer_pairwise_group +from distutils._modified import newer, newer_group, newer_pairwise, newer_pairwise_group from distutils.errors import DistutilsFileError from distutils.tests import support +import pytest + class TestDepUtil(support.TempdirManager): def test_newer(self): diff --git a/distutils/tests/test_msvc9compiler.py b/distutils/tests/test_msvc9compiler.py index dfb34122bcb..58e24f017a5 100644 --- a/distutils/tests/test_msvc9compiler.py +++ b/distutils/tests/test_msvc9compiler.py @@ -1,10 +1,10 @@ """Tests for distutils.msvc9compiler.""" -import sys import os - +import sys from distutils.errors import DistutilsPlatformError from distutils.tests import support + import pytest # A manifest with the only assembly reference being the msvcrt assembly, so diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index f65a5a25a3c..23b6c732c3c 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -1,16 +1,14 @@ """Tests for distutils._msvccompiler.""" -import sys import os +import sys import threading import unittest.mock as mock - -import pytest - +from distutils import _msvccompiler from distutils.errors import DistutilsPlatformError from distutils.tests import support -from distutils import _msvccompiler +import pytest needs_winreg = pytest.mark.skipif('not hasattr(_msvccompiler, "winreg")') diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 591c5ce0add..d071bbe951e 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -4,12 +4,11 @@ import os import pathlib import urllib - from distutils.command import register as register_module from distutils.command.register import register from distutils.errors import DistutilsSetupError - from distutils.tests.test_config import BasePyPIRCCommandTestCase + import pytest try: diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 450f68c9936..66a4194706c 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -5,24 +5,23 @@ import tarfile import warnings import zipfile +from distutils.archive_util import ARCHIVE_FORMATS +from distutils.command.sdist import sdist, show_formats +from distutils.core import Distribution +from distutils.errors import DistutilsOptionError +from distutils.filelist import FileList +from distutils.spawn import find_executable # noqa: F401 +from distutils.tests.test_config import BasePyPIRCCommandTestCase from os.path import join from textwrap import dedent -from .unix_compat import require_unix_id, require_uid_0, pwd, grp -import pytest -import path import jaraco.path +import path +import pytest from more_itertools import ilen from .py38compat import check_warnings - -from distutils.command.sdist import sdist, show_formats -from distutils.core import Distribution -from distutils.tests.test_config import BasePyPIRCCommandTestCase -from distutils.errors import DistutilsOptionError -from distutils.spawn import find_executable # noqa: F401 -from distutils.filelist import FileList -from distutils.archive_util import ARCHIVE_FORMATS +from .unix_compat import grp, pwd, require_uid_0, require_unix_id SETUP_PY = """ from distutils.core import setup diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index ec4c9982ad1..abbac4c23f1 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -4,19 +4,16 @@ import stat import sys import unittest.mock as mock - +from distutils.errors import DistutilsExecError +from distutils.spawn import find_executable, spawn +from distutils.tests import support from test.support import unix_shell import path +import pytest from . import py38compat as os_helper -from distutils.spawn import find_executable -from distutils.spawn import spawn -from distutils.errors import DistutilsExecError -from distutils.tests import support -import pytest - class TestSpawn(support.TempdirManager): @pytest.mark.skipif("os.name not in ('nt', 'posix')") diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 131c1344bbf..ce13d6bdc34 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -1,22 +1,21 @@ """Tests for distutils.sysconfig.""" import contextlib +import distutils import os +import pathlib import subprocess import sys -import pathlib - -import pytest -import jaraco.envs -import path -from jaraco.text import trim - -import distutils from distutils import sysconfig from distutils.ccompiler import get_default_compiler # noqa: F401 from distutils.unixccompiler import UnixCCompiler from test.support import swap_item +import jaraco.envs +import path +import pytest +from jaraco.text import trim + from . import py37compat diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py index fe787f44c84..c5c910a820d 100644 --- a/distutils/tests/test_text_file.py +++ b/distutils/tests/test_text_file.py @@ -1,11 +1,11 @@ """Tests for distutils.text_file.""" +from distutils.tests import support +from distutils.text_file import TextFile + import jaraco.path import path -from distutils.text_file import TextFile -from distutils.tests import support - TEST_DATA = """# test file line 3 \\ diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index ca198873ade..f17edf2f6be 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -3,17 +3,16 @@ import os import sys import unittest.mock as mock - -from .py38compat import EnvironmentVarGuard - from distutils import sysconfig from distutils.errors import DistutilsPlatformError from distutils.unixccompiler import UnixCCompiler from distutils.util import _clear_cached_macosx_ver -from . import support import pytest +from . import support +from .py38compat import EnvironmentVarGuard + @pytest.fixture(autouse=True) def save_values(monkeypatch): diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index 5c5bc59a40e..0692f001604 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -2,15 +2,13 @@ import os import unittest.mock as mock -from urllib.request import HTTPError - - from distutils.command import upload as upload_mod from distutils.command.upload import upload from distutils.core import Distribution from distutils.errors import DistutilsError - from distutils.tests.test_config import PYPIRC, BasePyPIRCCommandTestCase +from urllib.request import HTTPError + import pytest PYPIRC_LONG_PASSWORD = """\ diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 53c131e9e5b..78d8b1e3b64 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -1,32 +1,30 @@ """Tests for distutils.util.""" import email -import email.policy import email.generator +import email.policy import io import os import sys import sysconfig as stdlib_sysconfig import unittest.mock as mock from copy import copy - -import pytest - +from distutils import sysconfig, util +from distutils.errors import DistutilsByteCompileError, DistutilsPlatformError from distutils.util import ( - get_platform, - convert_path, + byte_compile, change_root, check_environ, + convert_path, + get_host_platform, + get_platform, + grok_environment_error, + rfc822_escape, split_quoted, strtobool, - rfc822_escape, - byte_compile, - grok_environment_error, - get_host_platform, ) -from distutils import util -from distutils import sysconfig -from distutils.errors import DistutilsPlatformError, DistutilsByteCompileError + +import pytest @pytest.fixture(autouse=True) diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index f89d1b35805..ddf1789b44a 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -1,10 +1,9 @@ """Tests for distutils.version.""" -import pytest - import distutils -from distutils.version import LooseVersion -from distutils.version import StrictVersion +from distutils.version import LooseVersion, StrictVersion + +import pytest @pytest.fixture(autouse=True) diff --git a/distutils/tests/unix_compat.py b/distutils/tests/unix_compat.py index 95fc8eebe28..a5d9ee45ccc 100644 --- a/distutils/tests/unix_compat.py +++ b/distutils/tests/unix_compat.py @@ -8,7 +8,6 @@ import pytest - UNIX_ID_SUPPORT = grp and pwd UID_0_SUPPORT = UNIX_ID_SUPPORT and sys.platform != "cygwin" diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index d749fe25291..a1fe2b57a29 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -13,18 +13,18 @@ * link shared library handled by 'cc -shared' """ +import itertools import os -import sys import re import shlex -import itertools +import sys from . import sysconfig -from ._modified import newer -from .ccompiler import CCompiler, gen_preprocess_options, gen_lib_options -from .errors import DistutilsExecError, CompileError, LibError, LinkError from ._log import log from ._macos_compat import compiler_fixup +from ._modified import newer +from .ccompiler import CCompiler, gen_lib_options, gen_preprocess_options +from .errors import CompileError, DistutilsExecError, LibError, LinkError # XXX Things not currently handled: # * optimization/debug/warning flags; we just use whatever's in Python's diff --git a/distutils/util.py b/distutils/util.py index a24c9401027..9ee77721b38 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -4,6 +4,7 @@ one of the other *util.py modules. """ +import functools import importlib.util import os import re @@ -11,12 +12,11 @@ import subprocess import sys import sysconfig -import functools -from .errors import DistutilsPlatformError, DistutilsByteCompileError +from ._log import log from ._modified import newer +from .errors import DistutilsByteCompileError, DistutilsPlatformError from .spawn import spawn -from ._log import log def get_host_platform(): diff --git a/distutils/version.py b/distutils/version.py index 8ab76ddef4e..aa7c5385ae0 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -26,9 +26,9 @@ of the same class, thus must follow the same rules) """ +import contextlib import re import warnings -import contextlib @contextlib.contextmanager diff --git a/distutils/versionpredicate.py b/distutils/versionpredicate.py index c75e49486f3..31c420168cc 100644 --- a/distutils/versionpredicate.py +++ b/distutils/versionpredicate.py @@ -1,9 +1,9 @@ """Module for parsing and testing package version predicate strings.""" -import re -from . import version import operator +import re +from . import version re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)", re.ASCII) # (package) (rest) diff --git a/distutils/zosccompiler.py b/distutils/zosccompiler.py index 6d70b7f04f1..c7a7ca61cf8 100644 --- a/distutils/zosccompiler.py +++ b/distutils/zosccompiler.py @@ -12,9 +12,10 @@ """ import os -from .unixccompiler import UnixCCompiler + from . import sysconfig -from .errors import DistutilsExecError, CompileError +from .errors import CompileError, DistutilsExecError +from .unixccompiler import UnixCCompiler _cc_args = { 'ibm-openxl': [ From 1d11b1c3e21d82be2d7645f2aa4bd6115d335b75 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:11:14 -0400 Subject: [PATCH 108/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove now extraneous adjacent strings. --- distutils/_msvccompiler.py | 2 +- distutils/ccompiler.py | 4 ++-- distutils/command/bdist.py | 6 +++--- distutils/command/bdist_dumb.py | 8 ++++---- distutils/command/bdist_rpm.py | 6 +++--- distutils/command/build_ext.py | 4 +--- distutils/command/build_scripts.py | 2 +- distutils/command/install.py | 2 +- distutils/command/sdist.py | 4 ++-- distutils/fancy_getopt.py | 3 +-- distutils/filelist.py | 6 ++---- distutils/tests/test_version.py | 4 +--- distutils/text_file.py | 2 +- 13 files changed, 23 insertions(+), 30 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index d08910ecf97..a2159fef838 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -253,7 +253,7 @@ def initialize(self, plat_name=None): vc_env = _get_vc_env(plat_spec) if not vc_env: raise DistutilsPlatformError( - "Unable to find a compatible " "Visual Studio installation." + "Unable to find a compatible Visual Studio installation." ) self._configure(vc_env) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 03181cfb7cf..8876d730986 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -465,7 +465,7 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): ) else: raise TypeError( - "'runtime_library_dirs' (if supplied) " "must be a list of strings" + "'runtime_library_dirs' (if supplied) must be a list of strings" ) return (libraries, library_dirs, runtime_library_dirs) @@ -1245,7 +1245,7 @@ def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries): lib_opts.append(lib_file) else: compiler.warn( - "no library file corresponding to " "'%s' found (skipping)" % lib + "no library file corresponding to '%s' found (skipping)" % lib ) else: lib_opts.append(compiler.library_option(lib)) diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py index f681b5531d9..ade98445bac 100644 --- a/distutils/command/bdist.py +++ b/distutils/command/bdist.py @@ -47,18 +47,18 @@ class bdist(Command): ( 'dist-dir=', 'd', - "directory to put final built distributions in " "[default: dist]", + "directory to put final built distributions in [default: dist]", ), ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), ( 'owner=', 'u', - "Owner name used when creating a tar file" " [default: current user]", + "Owner name used when creating a tar file [default: current user]", ), ( 'group=', 'g', - "Group name used when creating a tar file" " [default: current group]", + "Group name used when creating a tar file [default: current group]", ), ] diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py index 41adf014187..06502d201e6 100644 --- a/distutils/command/bdist_dumb.py +++ b/distutils/command/bdist_dumb.py @@ -28,7 +28,7 @@ class bdist_dumb(Command): ( 'format=', 'f', - "archive format to create (tar, gztar, bztar, xztar, " "ztar, zip)", + "archive format to create (tar, gztar, bztar, xztar, ztar, zip)", ), ( 'keep-temp', @@ -41,17 +41,17 @@ class bdist_dumb(Command): ( 'relative', None, - "build the archive using relative paths " "(default: false)", + "build the archive using relative paths (default: false)", ), ( 'owner=', 'u', - "Owner name used when creating a tar file" " [default: current user]", + "Owner name used when creating a tar file [default: current user]", ), ( 'group=', 'g', - "Group name used when creating a tar file" " [default: current group]", + "Group name used when creating a tar file [default: current group]", ), ] diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 6a75e32fb12..649968a5eb5 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -34,7 +34,7 @@ class bdist_rpm(Command): ( 'dist-dir=', 'd', - "directory to put final RPM files in " "(and .spec files if --spec-only)", + "directory to put final RPM files in (and .spec files if --spec-only)", ), ( 'python=', @@ -75,7 +75,7 @@ class bdist_rpm(Command): ( 'packager=', None, - "RPM packager (eg. \"Jane Doe \") " "[default: vendor]", + "RPM packager (eg. \"Jane Doe \") [default: vendor]", ), ('doc-files=', None, "list of documentation files (space or comma-separated)"), ('changelog=', None, "RPM changelog"), @@ -214,7 +214,7 @@ def finalize_options(self): if os.name != 'posix': raise DistutilsPlatformError( - "don't know how to create RPM " "distributions on platform %s" % os.name + "don't know how to create RPM distributions on platform %s" % os.name ) if self.binary_only and self.source_only: raise DistutilsOptionError( diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index aa9ed578f86..82e1e020709 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -427,9 +427,7 @@ def check_extensions_list(self, extensions): # noqa: C901 # Medium-easy stuff: same syntax/semantics, different names. ext.runtime_library_dirs = build_info.get('rpath') if 'def_file' in build_info: - log.warning( - "'def_file' element of build info dict " "no longer supported" - ) + log.warning("'def_file' element of build info dict no longer supported") # Non-trivial stuff: 'macros' split into 'define_macros' # and 'undef_macros'. diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index 37bc5850383..5f3902a0275 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -156,7 +156,7 @@ def _validate_shebang(shebang, encoding): try: shebang.encode('utf-8') except UnicodeEncodeError: - raise ValueError(f"The shebang ({shebang!r}) is not encodable " "to utf-8") + raise ValueError(f"The shebang ({shebang!r}) is not encodable to utf-8") # If the script is encoded to a custom encoding (use a # #coding:xxx cookie), the shebang has to be encodable to diff --git a/distutils/command/install.py b/distutils/command/install.py index 575cebdbc8a..85165717a74 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -701,7 +701,7 @@ def run(self): # internally, and not to sys.path, so we don't check the platform # matches what we are running. if self.warn_dir and build_plat != get_platform(): - raise DistutilsPlatformError("Can't install when " "cross-compiling") + raise DistutilsPlatformError("Can't install when cross-compiling") # Run all sub-commands (at least those that need to be run) for cmd_name in self.get_sub_commands(): diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index 6414ef5c066..97bae8279d4 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -61,7 +61,7 @@ def checking_metadata(self): ( 'manifest-only', 'o', - "just regenerate the manifest and then stop " "(implies --force-manifest)", + "just regenerate the manifest and then stop (implies --force-manifest)", ), ( 'force-manifest', @@ -78,7 +78,7 @@ def checking_metadata(self): ( 'dist-dir=', 'd', - "directory to put the source distribution archive(s) in " "[default: dist]", + "directory to put the source distribution archive(s) in [default: dist]", ), ( 'metadata-check', diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index cb646c6d9ba..dccc54923fc 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -161,8 +161,7 @@ def _grok_option_table(self): # noqa: C901 # Type- and value-check the option names if not isinstance(long, str) or len(long) < 2: raise DistutilsGetoptError( - ("invalid long option '%s': " "must be a string of length >= 2") - % long + ("invalid long option '%s': must be a string of length >= 2") % long ) if not ((short is None) or (isinstance(short, str) and len(short) == 1)): diff --git a/distutils/filelist.py b/distutils/filelist.py index 5ce47936a99..71ffb2abe7e 100644 --- a/distutils/filelist.py +++ b/distutils/filelist.py @@ -162,9 +162,7 @@ def process_template_line(self, line): # noqa: C901 self.debug_print("recursive-include {} {}".format(dir, ' '.join(patterns))) for pattern in patterns: if not self.include_pattern(pattern, prefix=dir): - msg = ( - "warning: no files found matching '%s' " "under directory '%s'" - ) + msg = "warning: no files found matching '%s' under directory '%s'" log.warning(msg, pattern, dir) elif action == 'recursive-exclude': @@ -189,7 +187,7 @@ def process_template_line(self, line): # noqa: C901 self.debug_print("prune " + dir_pattern) if not self.exclude_pattern(None, prefix=dir_pattern): log.warning( - ("no previously-included directories found " "matching '%s'"), + ("no previously-included directories found matching '%s'"), dir_pattern, ) else: diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index ddf1789b44a..1508e1cc0a2 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -48,9 +48,7 @@ def test_cmp_strict(self): if wanted is ValueError: continue else: - raise AssertionError( - f"cmp({v1}, {v2}) " "shouldn't raise ValueError" - ) + raise AssertionError(f"cmp({v1}, {v2}) shouldn't raise ValueError") assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' res = StrictVersion(v1)._cmp(v2) assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}' diff --git a/distutils/text_file.py b/distutils/text_file.py index 6f90cfe21d8..0f846e3c526 100644 --- a/distutils/text_file.py +++ b/distutils/text_file.py @@ -220,7 +220,7 @@ def readline(self): # noqa: C901 if self.join_lines and buildup_line: # oops: end of file if line is None: - self.warn("continuation line immediately precedes " "end-of-file") + self.warn("continuation line immediately precedes end-of-file") return buildup_line if self.collapse_join: From 7c006d8f0902ad602556e58f7180320abf18da3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:19:17 -0400 Subject: [PATCH 109/232] Remove unreachable branch --- distutils/tests/test_clean.py | 2 +- distutils/version.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/distutils/tests/test_clean.py b/distutils/tests/test_clean.py index 9b11fa40f7b..bdbcd4fa46d 100644 --- a/distutils/tests/test_clean.py +++ b/distutils/tests/test_clean.py @@ -36,7 +36,7 @@ def test_simple_run(self): cmd.run() # make sure the files where removed - for name, path in dirs: + for _name, path in dirs: assert not os.path.exists(path), '%s was not removed' % path # let's run the command again (should spit warnings but succeed) diff --git a/distutils/version.py b/distutils/version.py index aa7c5385ae0..6e26e03007a 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -212,8 +212,6 @@ def _cmp(self, other): # noqa: C901 return -1 else: return 1 - else: - assert False, "never get here" # end class StrictVersion From 854780a8a9d5fd2038cc8826159d3639c81e6e15 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:21:29 -0400 Subject: [PATCH 110/232] Extract method for comparing prerelease. Satisfies complexity check. --- distutils/version.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/distutils/version.py b/distutils/version.py index 6e26e03007a..90adbc718ae 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -178,7 +178,7 @@ def __str__(self): return vstring - def _cmp(self, other): # noqa: C901 + def _cmp(self, other): if isinstance(other, str): with suppress_known_deprecation(): other = StrictVersion(other) @@ -193,25 +193,28 @@ def _cmp(self, other): # noqa: C901 else: return 1 - # have to compare prerelease - # case 1: neither has prerelease; they're equal - # case 2: self has prerelease, other doesn't; other is greater - # case 3: self doesn't have prerelease, other does: self is greater - # case 4: both have prerelease: must compare them! + return self._cmp_prerelease(other) + def _cmp_prerelease(self, other): + """ + case 1: neither has prerelease; they're equal + case 2: self has prerelease, other doesn't; other is greater + case 3: self doesn't have prerelease, other does: self is greater + case 4: both have prerelease: must compare them! + """ if not self.prerelease and not other.prerelease: return 0 elif self.prerelease and not other.prerelease: return -1 elif not self.prerelease and other.prerelease: return 1 - elif self.prerelease and other.prerelease: - if self.prerelease == other.prerelease: - return 0 - elif self.prerelease < other.prerelease: - return -1 - else: - return 1 + + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 # end class StrictVersion From cec4ce55bf5eb16d7d654ca845375381d08fcd51 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:25:00 -0400 Subject: [PATCH 111/232] Re-organize for brevity. --- distutils/version.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/distutils/version.py b/distutils/version.py index 90adbc718ae..30546a9dd61 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -185,15 +185,11 @@ def _cmp(self, other): elif not isinstance(other, StrictVersion): return NotImplemented - if self.version != other.version: - # numeric versions don't match - # prerelease stuff doesn't matter - if self.version < other.version: - return -1 - else: - return 1 - - return self._cmp_prerelease(other) + if self.version == other.version: + # versions match; pre-release drives the comparison + return self._cmp_prerelease(other) + + return -1 if self.version < other.version else 1 def _cmp_prerelease(self, other): """ From 47db63930c35143f3b0dd8dab305b0b8194ff82a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:30:38 -0400 Subject: [PATCH 112/232] Rely on None==None and handle two cases together. --- distutils/version.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/distutils/version.py b/distutils/version.py index 30546a9dd61..806d233ca59 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -193,14 +193,11 @@ def _cmp(self, other): def _cmp_prerelease(self, other): """ - case 1: neither has prerelease; they're equal - case 2: self has prerelease, other doesn't; other is greater - case 3: self doesn't have prerelease, other does: self is greater - case 4: both have prerelease: must compare them! + case 1: self has prerelease, other doesn't; other is greater + case 2: self doesn't have prerelease, other does: self is greater + case 3: both or neither have prerelease: compare them! """ - if not self.prerelease and not other.prerelease: - return 0 - elif self.prerelease and not other.prerelease: + if self.prerelease and not other.prerelease: return -1 elif not self.prerelease and other.prerelease: return 1 From 9390f46d67801364375653065922ab0d1b540c72 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:49:27 -0400 Subject: [PATCH 113/232] Refresh RangeMap from jaraco.collections 5.0.1. --- distutils/_collections.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/distutils/_collections.py b/distutils/_collections.py index 5ad21cc7c90..6810a5e24de 100644 --- a/distutils/_collections.py +++ b/distutils/_collections.py @@ -1,8 +1,13 @@ +from __future__ import annotations + import collections import functools import itertools import operator +from collections.abc import Mapping +from typing import Any + # from jaraco.collections 3.5.1 class DictStack(list, collections.abc.Mapping): @@ -58,7 +63,7 @@ def __len__(self): return len(list(iter(self))) -# from jaraco.collections 3.7 +# from jaraco.collections 5.0.1 class RangeMap(dict): """ A dictionary-like object that uses the keys as bounds for a range. @@ -70,7 +75,7 @@ class RangeMap(dict): One may supply keyword parameters to be passed to the sort function used to sort keys (i.e. key, reverse) as sort_params. - Let's create a map that maps 1-3 -> 'a', 4-6 -> 'b' + Create a map that maps 1-3 -> 'a', 4-6 -> 'b' >>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy >>> r[1], r[2], r[3], r[4], r[5], r[6] @@ -82,7 +87,7 @@ class RangeMap(dict): >>> r[4.5] 'b' - But you'll notice that the way rangemap is defined, it must be open-ended + Notice that the way rangemap is defined, it must be open-ended on one side. >>> r[0] @@ -140,7 +145,12 @@ class RangeMap(dict): """ - def __init__(self, source, sort_params={}, key_match_comparator=operator.le): + def __init__( + self, + source, + sort_params: Mapping[str, Any] = {}, + key_match_comparator=operator.le, + ): dict.__init__(self, source) self.sort_params = sort_params self.match = key_match_comparator From 7414bc5f5459ad67385cc3e2de6d6995fe90ed1e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:51:08 -0400 Subject: [PATCH 114/232] Ruff fixes B007. --- distutils/command/build_clib.py | 2 +- distutils/command/build_py.py | 4 ++-- distutils/command/install.py | 2 +- distutils/command/sdist.py | 2 +- distutils/dist.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py index 811e607e70a..360575d0cb4 100644 --- a/distutils/command/build_clib.py +++ b/distutils/command/build_clib.py @@ -155,7 +155,7 @@ def get_library_names(self): return None lib_names = [] - for lib_name, build_info in self.libraries: + for lib_name, _build_info in self.libraries: lib_names.append(lib_name) return lib_names diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py index a15d0af5190..56e6fa2e66f 100644 --- a/distutils/command/build_py.py +++ b/distutils/command/build_py.py @@ -136,7 +136,7 @@ def find_data_files(self, package, src_dir): def build_package_data(self): """Copy data files into build directory""" - for package, src_dir, build_dir, filenames in self.data_files: + for _package, src_dir, build_dir, filenames in self.data_files: for filename in filenames: target = os.path.join(build_dir, filename) self.mkpath(os.path.dirname(target)) @@ -309,7 +309,7 @@ def get_module_outfile(self, build_dir, package, module): def get_outputs(self, include_bytecode=1): modules = self.find_all_modules() outputs = [] - for package, module, module_file in modules: + for package, module, _module_file in modules: package = package.split('.') filename = self.get_module_outfile(self.build_lib, package, module) outputs.append(filename) diff --git a/distutils/command/install.py b/distutils/command/install.py index 85165717a74..8e920be4de6 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -683,7 +683,7 @@ def create_home_path(self): if not self.user: return home = convert_path(os.path.expanduser("~")) - for name, path in self.config_vars.items(): + for _name, path in self.config_vars.items(): if str(path).startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py index 97bae8279d4..387d27c90b3 100644 --- a/distutils/command/sdist.py +++ b/distutils/command/sdist.py @@ -308,7 +308,7 @@ def _add_defaults_python(self): # getting package_data files # (computed in build_py.data_files by build_py.finalize_options) - for pkg, src_dir, build_dir, filenames in build_py.data_files: + for _pkg, src_dir, _build_dir, filenames in build_py.data_files: for filename in filenames: self.filelist.append(os.path.join(src_dir, filename)) diff --git a/distutils/dist.py b/distutils/dist.py index 1759120c923..c32ffb6c0e3 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -414,7 +414,7 @@ def parse_config_files(self, filenames=None): # noqa: C901 # to set Distribution options. if 'global' in self.command_options: - for opt, (src, val) in self.command_options['global'].items(): + for opt, (_src, val) in self.command_options['global'].items(): alias = self.negative_opt.get(opt) try: if alias: @@ -585,7 +585,7 @@ def _parse_command_opts(self, parser, args): # noqa: C901 cmd_class.help_options, list ): help_option_found = 0 - for help_option, short, desc, func in cmd_class.help_options: + for help_option, _short, _desc, func in cmd_class.help_options: if hasattr(opts, parser.get_attr_name(help_option)): help_option_found = 1 if callable(func): From 448a2a12848ca7e99b83958f59db44bb68f6120b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 09:58:22 -0400 Subject: [PATCH 115/232] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add immutable type declarations to satisfy B006 checks. --- distutils/_collections.py | 1 - distutils/command/config.py | 7 +++++-- distutils/dist.py | 5 ++++- distutils/fancy_getopt.py | 3 ++- distutils/tests/__init__.py | 4 +++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/distutils/_collections.py b/distutils/_collections.py index 6810a5e24de..d11a83467c2 100644 --- a/distutils/_collections.py +++ b/distutils/_collections.py @@ -4,7 +4,6 @@ import functools import itertools import operator - from collections.abc import Mapping from typing import Any diff --git a/distutils/command/config.py b/distutils/command/config.py index 38a5ff5159b..d4b2b0a3620 100644 --- a/distutils/command/config.py +++ b/distutils/command/config.py @@ -9,9 +9,12 @@ this header file lives". """ +from __future__ import annotations + import os import pathlib import re +from collections.abc import Sequence from distutils._log import log from ..core import Command @@ -325,7 +328,7 @@ def check_lib( library_dirs=None, headers=None, include_dirs=None, - other_libraries=[], + other_libraries: Sequence[str] = [], ): """Determine if 'library' is available to be linked against, without actually checking that any particular symbols are provided @@ -340,7 +343,7 @@ def check_lib( "int main (void) { }", headers, include_dirs, - [library] + other_libraries, + [library] + list(other_libraries), library_dirs, ) diff --git a/distutils/dist.py b/distutils/dist.py index c32ffb6c0e3..f29a34faba4 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -10,6 +10,7 @@ import pathlib import re import sys +from collections.abc import Iterable from email import message_from_file try: @@ -620,7 +621,9 @@ def finalize_options(self): value = [elm.strip() for elm in value.split(',')] setattr(self.metadata, attr, value) - def _show_help(self, parser, global_options=1, display_options=1, commands=[]): + def _show_help( + self, parser, global_options=1, display_options=1, commands: Iterable = () + ): """Show help for the setup script command-line in the form of several lists of command-line options. 'parser' should be a FancyGetopt instance; do not expect it to be returned in the diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py index dccc54923fc..e905aede4d9 100644 --- a/distutils/fancy_getopt.py +++ b/distutils/fancy_getopt.py @@ -12,6 +12,7 @@ import re import string import sys +from typing import Any, Sequence from .errors import DistutilsArgError, DistutilsGetoptError @@ -448,7 +449,7 @@ class OptionDummy: """Dummy class just used as a place to hold command-line option values as instance attributes.""" - def __init__(self, options=[]): + def __init__(self, options: Sequence[Any] = []): """Create a new OptionDummy instance. The attributes listed in 'options' will be initialized to None.""" for opt in options: diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index c475e5d0f25..20dfe8f19bc 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -7,8 +7,10 @@ by import rather than matching pre-defined names. """ +from typing import Sequence -def missing_compiler_executable(cmd_names=[]): # pragma: no cover + +def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no cover """Check if the compiler components used to build the interpreter exist. Check for the existence of the compiler executables whose names are listed From a53e4258e144f03f1b48f1fced74aaf9d770f911 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 10:41:53 -0400 Subject: [PATCH 116/232] Fix B026 by moving star arg ahead of keyword arg. --- distutils/command/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/command/check.py b/distutils/command/check.py index 6b42a34f6d5..28599e109c8 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -33,7 +33,7 @@ def __init__( def system_message(self, level, message, *children, **kwargs): self.messages.append((level, message, children, kwargs)) return docutils.nodes.system_message( - message, level=level, type=self.levels[level], *children, **kwargs + message, *children, level=level, type=self.levels[level], **kwargs ) From db216f48ffc06eee7631f7060d3288b32e4d61f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 10:53:23 -0400 Subject: [PATCH 117/232] Extract 'make_iterable' for upload and register commands, avoiding masking loop input variable (B020). --- distutils/command/register.py | 15 +++++++++------ distutils/command/upload.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/distutils/command/register.py b/distutils/command/register.py index e5e6b379ad4..ee6c54dabad 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -13,6 +13,7 @@ from distutils._log import log from warnings import warn +from .._itertools import always_iterable from ..core import PyPIRCCommand @@ -273,12 +274,8 @@ def post_to_server(self, data, auth=None): # noqa: C901 sep_boundary = '\n--' + boundary end_boundary = sep_boundary + '--' body = io.StringIO() - for key, value in data.items(): - # handle multiple entries for the same name - if type(value) not in (type([]), type(())): - value = [value] - for value in value: - value = str(value) + for key, values in data.items(): + for value in map(str, make_iterable(values)): body.write(sep_boundary) body.write('\nContent-Disposition: form-data; name="%s"' % key) body.write("\n\n") @@ -318,3 +315,9 @@ def post_to_server(self, data, auth=None): # noqa: C901 msg = '\n'.join(('-' * 75, data, '-' * 75)) self.announce(msg, logging.INFO) return result + + +def make_iterable(values): + if values is None: + return [None] + return always_iterable(values) diff --git a/distutils/command/upload.py b/distutils/command/upload.py index e61a9ea8a54..cf541f8a821 100644 --- a/distutils/command/upload.py +++ b/distutils/command/upload.py @@ -13,6 +13,7 @@ from urllib.parse import urlparse from urllib.request import HTTPError, Request, urlopen +from .._itertools import always_iterable from ..core import PyPIRCCommand from ..errors import DistutilsError, DistutilsOptionError from ..spawn import spawn @@ -151,12 +152,9 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 sep_boundary = b'\r\n--' + boundary.encode('ascii') end_boundary = sep_boundary + b'--\r\n' body = io.BytesIO() - for key, value in data.items(): + for key, values in data.items(): title = '\r\nContent-Disposition: form-data; name="%s"' % key - # handle multiple entries for the same name - if not isinstance(value, list): - value = [value] - for value in value: + for value in make_iterable(values): if type(value) is tuple: title += '; filename="%s"' % value[0] value = value[1] @@ -202,3 +200,9 @@ def upload_file(self, command, pyversion, filename): # noqa: C901 msg = f'Upload failed ({status}): {reason}' self.announce(msg, logging.ERROR) raise DistutilsError(msg) + + +def make_iterable(values): + if values is None: + return [None] + return always_iterable(values, base_type=(bytes, str, tuple)) From 9f2922d9d035de477f7c97a2dd6a23004c024e4f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 10:54:38 -0400 Subject: [PATCH 118/232] Fix pointless comparison (B015). --- distutils/tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index 5916718027e..bad3fb7e831 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -123,7 +123,7 @@ def test_debug_mode(self, capsys, monkeypatch): # this covers the code called when DEBUG is set sys.argv = ['setup.py', '--name'] distutils.core.setup(name='bar') - capsys.readouterr().out == 'bar\n' + assert capsys.readouterr().out == 'bar\n' monkeypatch.setattr(distutils.core, 'DEBUG', True) distutils.core.setup(name='bar') wanted = "options (after parsing config files):\n" From 0543254d8bd57746429b9a6650689cc90429fc10 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 11:01:46 -0400 Subject: [PATCH 119/232] Remove Python 3.7 compatibility from build_ext --- distutils/command/build_ext.py | 3 +-- distutils/command/py37compat.py | 31 ------------------------------- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 distutils/command/py37compat.py diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index 82e1e020709..06d949aff16 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -24,7 +24,6 @@ from ..extension import Extension from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version from ..util import get_platform -from . import py37compat # An extension name is just a dot-separated list of Python NAMEs (ie. # the same as a fully-qualified module name). @@ -798,4 +797,4 @@ def get_libraries(self, ext): # noqa: C901 ldversion = get_config_var('LDVERSION') return ext.libraries + ['python' + ldversion] - return ext.libraries + py37compat.pythonlib() + return ext.libraries diff --git a/distutils/command/py37compat.py b/distutils/command/py37compat.py deleted file mode 100644 index aa0c0a7fcd1..00000000000 --- a/distutils/command/py37compat.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys - - -def _pythonlib_compat(): - """ - On Python 3.7 and earlier, distutils would include the Python - library. See pypa/distutils#9. - """ - from distutils import sysconfig - - if not sysconfig.get_config_var('Py_ENABLED_SHARED'): - return - - yield 'python{}.{}{}'.format( - sys.hexversion >> 24, - (sys.hexversion >> 16) & 0xFF, - sysconfig.get_config_var('ABIFLAGS'), - ) - - -def compose(f1, f2): - return lambda *args, **kwargs: f1(f2(*args, **kwargs)) - - -pythonlib = ( - compose(list, _pythonlib_compat) - if sys.version_info < (3, 8) - and sys.platform != 'darwin' - and sys.platform[:3] != 'aix' - else list -) From 6b6633af0e0c53243d9991fe9df3f29365c67db6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 11:03:45 -0400 Subject: [PATCH 120/232] Remove Python 3.7 compatibility from test_sysconfig. --- distutils/tests/py37compat.py | 18 ------------------ distutils/tests/test_sysconfig.py | 4 +--- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 distutils/tests/py37compat.py diff --git a/distutils/tests/py37compat.py b/distutils/tests/py37compat.py deleted file mode 100644 index 76d3551c49e..00000000000 --- a/distutils/tests/py37compat.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import platform -import sys - - -def subprocess_args_compat(*args): - return list(map(os.fspath, args)) - - -def subprocess_args_passthrough(*args): - return list(args) - - -subprocess_args = ( - subprocess_args_compat - if platform.system() == "Windows" and sys.version_info < (3, 8) - else subprocess_args_passthrough -) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index ce13d6bdc34..bc14d3c05aa 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -16,8 +16,6 @@ import pytest from jaraco.text import trim -from . import py37compat - def _gen_makefile(root, contents): jaraco.path.build({'Makefile': trim(contents)}, root) @@ -251,7 +249,7 @@ def test_customize_compiler_before_get_config_vars(self, tmp_path): tmp_path, ) p = subprocess.Popen( - py37compat.subprocess_args(sys.executable, tmp_path / 'file'), + [sys.executable, tmp_path / 'file'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, From 55982565e745262ae031a2001bd35a74867218aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 11:07:52 -0400 Subject: [PATCH 121/232] Move comment nearer the skip directive. Update wording. --- distutils/tests/test_sysconfig.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index bc14d3c05aa..c55896661f2 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -202,22 +202,21 @@ def test_sysconfig_module(self): 'LDFLAGS' ) + # On macOS, binary installers support extension module building on + # various levels of the operating system with differing Xcode + # configurations, requiring customization of some of the + # compiler configuration directives to suit the environment on + # the installed machine. Some of these customizations may require + # running external programs and are thus deferred until needed by + # the first extension module build. Only + # the Distutils version of sysconfig is used for extension module + # builds, which happens earlier in the Distutils tests. This may + # cause the following tests to fail since no tests have caused + # the global version of sysconfig to call the customization yet. + # The solution for now is to simply skip this test in this case. + # The longer-term solution is to only have one version of sysconfig. @pytest.mark.skipif("sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER')") def test_sysconfig_compiler_vars(self): - # On OS X, binary installers support extension module building on - # various levels of the operating system with differing Xcode - # configurations. This requires customization of some of the - # compiler configuration directives to suit the environment on - # the installed machine. Some of these customizations may require - # running external programs and, so, are deferred until needed by - # the first extension module build. With Python 3.3, only - # the Distutils version of sysconfig is used for extension module - # builds, which happens earlier in the Distutils tests. This may - # cause the following tests to fail since no tests have caused - # the global version of sysconfig to call the customization yet. - # The solution for now is to simply skip this test in this case. - # The longer-term solution is to only have one version of sysconfig. - import sysconfig as global_sysconfig if sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER'): From 48919ee0881caba6930ea8cdc79aaf834203a165 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 11:55:51 -0400 Subject: [PATCH 122/232] Add news fragment. --- newsfragments/4298.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4298.feature.rst diff --git a/newsfragments/4298.feature.rst b/newsfragments/4298.feature.rst new file mode 100644 index 00000000000..21d680d4864 --- /dev/null +++ b/newsfragments/4298.feature.rst @@ -0,0 +1 @@ +Merged with pypa/distutils@55982565e, including interoperability improvements for rfc822_escape (pypa/distutils#213), dynamic resolution of config_h_filename for Python 3.13 compatibility (pypa/distutils#219), added support for the z/OS compiler (pypa/distutils#216), modernized compiler options in unixcompiler (pypa/distutils#214), fixed accumulating flags bug after compile/link (pypa/distutils#207), fixed enconding warnings (pypa/distutils#236), and general quality improvements (pypa/distutils#234). From 6969162030244196b59fb561e0f316230e82db01 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 11:58:12 -0400 Subject: [PATCH 123/232] Omit distutils from coverage checks. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 14424a43ddf..1f214acf383 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + */setuptools/_distutils/* disable_warnings = couldnt-parse From e5087502969cd3ebf6aa2015b805142fbe1afc84 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 12:13:38 -0400 Subject: [PATCH 124/232] Fix EncodingWarnings in tools.finalize. --- tools/finalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/finalize.py b/tools/finalize.py index f79f5b3b458..3ba5d16ac7d 100644 --- a/tools/finalize.py +++ b/tools/finalize.py @@ -23,7 +23,7 @@ def get_version(): cmd = bump_version_command + ['--dry-run', '--verbose'] - out = subprocess.check_output(cmd, text=True) + out = subprocess.check_output(cmd, text=True, encoding='utf-8') return re.search('^new_version=(.*)', out, re.MULTILINE).group(1) From 92b45e9817ae829a5ca5a5962313a56b943cad91 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 12:13:44 -0400 Subject: [PATCH 125/232] =?UTF-8?q?Bump=20version:=2069.2.0=20=E2=86=92=20?= =?UTF-8?q?69.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- NEWS.rst | 9 +++++++++ newsfragments/3593.feature.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/3593.feature.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1236141a7cd..a76d5b66d77 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 69.2.0 +current_version = 69.3.0 commit = True tag = True diff --git a/NEWS.rst b/NEWS.rst index 2e849bdc5fb..7822ec63253 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v69.3.0 +======= + +Features +-------- + +- Support PEP 625 by canonicalizing package name and version in filenames. (#3593) + + v69.2.0 ======= diff --git a/newsfragments/3593.feature.rst b/newsfragments/3593.feature.rst deleted file mode 100644 index 2ec6f9714e4..00000000000 --- a/newsfragments/3593.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Support PEP 625 by canonicalizing package name and version in filenames. diff --git a/setup.cfg b/setup.cfg index aae14653750..bab3efa52ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 69.2.0 +version = 69.3.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages From fd5f55ea008b32d427d7059799302d65fd0cd0cd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 12:35:18 -0400 Subject: [PATCH 126/232] Refresh unpinned vendored dependencies. Closes #4253 --- newsfragments/4253.feature.rst | 1 + .../INSTALLER | 0 .../LICENSE | 2 - .../METADATA | 44 + .../backports.tarfile-1.0.0.dist-info/RECORD | 9 + .../REQUESTED | 0 .../WHEEL | 2 +- .../top_level.txt | 1 + pkg_resources/_vendor/backports/tarfile.py | 2900 +++++++++++++++++ .../RECORD | 58 +- .../jaraco.context-4.3.0.dist-info/METADATA | 68 - .../jaraco.context-4.3.0.dist-info/RECORD | 8 - .../INSTALLER | 0 .../LICENSE | 2 - .../jaraco.context-5.3.0.dist-info/METADATA | 75 + .../jaraco.context-5.3.0.dist-info/RECORD | 8 + .../jaraco.context-5.3.0.dist-info}/WHEEL | 2 +- .../top_level.txt | 0 .../jaraco.functools-3.6.0.dist-info/RECORD | 8 - .../INSTALLER | 0 .../jaraco.functools-4.0.0.dist-info}/LICENSE | 2 - .../METADATA | 37 +- .../jaraco.functools-4.0.0.dist-info/RECORD | 10 + .../jaraco.functools-4.0.0.dist-info}/WHEEL | 2 +- .../top_level.txt | 0 .../jaraco.text-3.7.0.dist-info/RECORD | 2 +- pkg_resources/_vendor/jaraco/context.py | 137 +- .../{functools.py => functools/__init__.py} | 205 +- .../_vendor/jaraco/functools/__init__.pyi | 128 + .../_vendor/jaraco/functools/py.typed | 0 .../INSTALLER | 0 .../LICENSE | 0 .../METADATA | 35 +- .../more_itertools-10.2.0.dist-info/RECORD | 15 + .../WHEEL | 0 .../more_itertools-9.1.0.dist-info/RECORD | 15 - .../_vendor/more_itertools/__init__.py | 2 +- pkg_resources/_vendor/more_itertools/more.py | 400 ++- pkg_resources/_vendor/more_itertools/more.pyi | 41 +- .../_vendor/more_itertools/recipes.py | 230 +- .../_vendor/more_itertools/recipes.pyi | 29 +- .../_vendor/packaging-23.1.dist-info/RECORD | 28 +- .../platformdirs-2.6.2.dist-info/RECORD | 16 +- .../typing_extensions-4.4.0.dist-info/RECORD | 2 +- pkg_resources/_vendor/vendored.txt | 2 + .../_vendor/zipp-3.7.0.dist-info/RECORD | 2 +- .../INSTALLER | 0 .../LICENSE | 2 - .../METADATA | 44 + .../backports.tarfile-1.0.0.dist-info/RECORD | 9 + .../REQUESTED | 0 .../backports.tarfile-1.0.0.dist-info}/WHEEL | 2 +- .../top_level.txt | 1 + setuptools/_vendor/backports/tarfile.py | 2900 +++++++++++++++++ .../importlib_metadata-6.0.0.dist-info/RECORD | 18 +- .../RECORD | 58 +- .../jaraco.context-4.3.0.dist-info/METADATA | 68 - .../jaraco.context-4.3.0.dist-info/RECORD | 8 - .../jaraco.context-5.3.0.dist-info/INSTALLER | 1 + .../jaraco.context-5.3.0.dist-info/LICENSE | 17 + .../jaraco.context-5.3.0.dist-info/METADATA | 75 + .../jaraco.context-5.3.0.dist-info/RECORD | 8 + .../jaraco.context-5.3.0.dist-info/WHEEL | 5 + .../top_level.txt | 0 .../jaraco.functools-3.6.0.dist-info/RECORD | 8 - .../INSTALLER | 1 + .../jaraco.functools-4.0.0.dist-info/LICENSE | 17 + .../METADATA | 37 +- .../jaraco.functools-4.0.0.dist-info/RECORD | 10 + .../jaraco.functools-4.0.0.dist-info/WHEEL | 5 + .../top_level.txt | 0 .../jaraco.text-3.7.0.dist-info/RECORD | 2 +- setuptools/_vendor/jaraco/context.py | 137 +- .../{functools.py => functools/__init__.py} | 205 +- .../_vendor/jaraco/functools/__init__.pyi | 128 + setuptools/_vendor/jaraco/functools/py.typed | 0 .../more_itertools-8.8.0.dist-info/RECORD | 6 +- .../ordered_set-3.1.1.dist-info/RECORD | 4 +- .../_vendor/ordered_set-3.1.1.dist-info/WHEEL | 2 +- .../_vendor/packaging-23.1.dist-info/RECORD | 28 +- .../_vendor/tomli-2.0.1.dist-info/RECORD | 8 +- .../typing_extensions-4.0.1.dist-info/RECORD | 2 +- setuptools/_vendor/vendored.txt | 2 + .../_vendor/zipp-3.7.0.dist-info/RECORD | 2 +- tools/vendored.py | 15 +- 85 files changed, 7642 insertions(+), 721 deletions(-) create mode 100644 newsfragments/4253.feature.rst rename pkg_resources/_vendor/{jaraco.context-4.3.0.dist-info => backports.tarfile-1.0.0.dist-info}/INSTALLER (100%) rename pkg_resources/_vendor/{jaraco.context-4.3.0.dist-info => backports.tarfile-1.0.0.dist-info}/LICENSE (97%) create mode 100644 pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED rename pkg_resources/_vendor/{jaraco.functools-3.6.0.dist-info => backports.tarfile-1.0.0.dist-info}/WHEEL (65%) create mode 100644 pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/backports/tarfile.py delete mode 100644 pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/METADATA delete mode 100644 pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/RECORD rename pkg_resources/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.context-5.3.0.dist-info}/INSTALLER (100%) rename pkg_resources/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.context-5.3.0.dist-info}/LICENSE (97%) create mode 100644 pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/RECORD rename {setuptools/_vendor/jaraco.context-4.3.0.dist-info => pkg_resources/_vendor/jaraco.context-5.3.0.dist-info}/WHEEL (65%) rename pkg_resources/_vendor/{jaraco.context-4.3.0.dist-info => jaraco.context-5.3.0.dist-info}/top_level.txt (100%) delete mode 100644 pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/RECORD rename pkg_resources/_vendor/{more_itertools-9.1.0.dist-info => jaraco.functools-4.0.0.dist-info}/INSTALLER (100%) rename {setuptools/_vendor/jaraco.functools-3.6.0.dist-info => pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info}/LICENSE (97%) rename pkg_resources/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.functools-4.0.0.dist-info}/METADATA (69%) create mode 100644 pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/RECORD rename {setuptools/_vendor/jaraco.functools-3.6.0.dist-info => pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info}/WHEEL (65%) rename pkg_resources/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.functools-4.0.0.dist-info}/top_level.txt (100%) rename pkg_resources/_vendor/jaraco/{functools.py => functools/__init__.py} (79%) create mode 100644 pkg_resources/_vendor/jaraco/functools/__init__.pyi create mode 100644 pkg_resources/_vendor/jaraco/functools/py.typed rename {setuptools/_vendor/jaraco.context-4.3.0.dist-info => pkg_resources/_vendor/more_itertools-10.2.0.dist-info}/INSTALLER (100%) rename pkg_resources/_vendor/{more_itertools-9.1.0.dist-info => more_itertools-10.2.0.dist-info}/LICENSE (100%) rename pkg_resources/_vendor/{more_itertools-9.1.0.dist-info => more_itertools-10.2.0.dist-info}/METADATA (90%) create mode 100644 pkg_resources/_vendor/more_itertools-10.2.0.dist-info/RECORD rename pkg_resources/_vendor/{more_itertools-9.1.0.dist-info => more_itertools-10.2.0.dist-info}/WHEEL (100%) delete mode 100644 pkg_resources/_vendor/more_itertools-9.1.0.dist-info/RECORD rename setuptools/_vendor/{jaraco.functools-3.6.0.dist-info => backports.tarfile-1.0.0.dist-info}/INSTALLER (100%) rename setuptools/_vendor/{jaraco.context-4.3.0.dist-info => backports.tarfile-1.0.0.dist-info}/LICENSE (97%) create mode 100644 setuptools/_vendor/backports.tarfile-1.0.0.dist-info/METADATA create mode 100644 setuptools/_vendor/backports.tarfile-1.0.0.dist-info/RECORD create mode 100644 setuptools/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED rename {pkg_resources/_vendor/jaraco.context-4.3.0.dist-info => setuptools/_vendor/backports.tarfile-1.0.0.dist-info}/WHEEL (65%) create mode 100644 setuptools/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt create mode 100644 setuptools/_vendor/backports/tarfile.py delete mode 100644 setuptools/_vendor/jaraco.context-4.3.0.dist-info/METADATA delete mode 100644 setuptools/_vendor/jaraco.context-4.3.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA create mode 100644 setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL rename setuptools/_vendor/{jaraco.context-4.3.0.dist-info => jaraco.context-5.3.0.dist-info}/top_level.txt (100%) delete mode 100644 setuptools/_vendor/jaraco.functools-3.6.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE rename setuptools/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.functools-4.0.0.dist-info}/METADATA (69%) create mode 100644 setuptools/_vendor/jaraco.functools-4.0.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL rename setuptools/_vendor/{jaraco.functools-3.6.0.dist-info => jaraco.functools-4.0.0.dist-info}/top_level.txt (100%) rename setuptools/_vendor/jaraco/{functools.py => functools/__init__.py} (79%) create mode 100644 setuptools/_vendor/jaraco/functools/__init__.pyi create mode 100644 setuptools/_vendor/jaraco/functools/py.typed diff --git a/newsfragments/4253.feature.rst b/newsfragments/4253.feature.rst new file mode 100644 index 00000000000..acc51ea4bd3 --- /dev/null +++ b/newsfragments/4253.feature.rst @@ -0,0 +1 @@ +Refresh unpinned vendored dependencies. \ No newline at end of file diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/INSTALLER b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/INSTALLER similarity index 100% rename from pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/INSTALLER rename to pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/INSTALLER diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/LICENSE b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE similarity index 97% rename from pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/LICENSE rename to pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE index 353924be0e5..1bb5a44356f 100644 --- a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/LICENSE +++ b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/METADATA b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/METADATA new file mode 100644 index 00000000000..e7b64c87f8e --- /dev/null +++ b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/METADATA @@ -0,0 +1,44 @@ +Metadata-Version: 2.1 +Name: backports.tarfile +Version: 1.0.0 +Summary: Backport of CPython tarfile module +Home-page: https://github.com/jaraco/backports.tarfile +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.8 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest !=8.1.1,>=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/backports.tarfile.svg + :target: https://pypi.org/project/backports.tarfile + +.. image:: https://img.shields.io/pypi/pyversions/backports.tarfile.svg + +.. image:: https://github.com/jaraco/backports.tarfile/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/backports.tarfile/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. .. image:: https://readthedocs.org/projects/backportstarfile/badge/?version=latest +.. :target: https://backportstarfile.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton diff --git a/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/RECORD b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/RECORD new file mode 100644 index 00000000000..a6a44d8fcc5 --- /dev/null +++ b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/RECORD @@ -0,0 +1,9 @@ +backports.tarfile-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +backports.tarfile-1.0.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +backports.tarfile-1.0.0.dist-info/METADATA,sha256=XlT7JAFR04zDMIjs-EFhqc0CkkVyeh-SiVUoKXONXJ0,1876 +backports.tarfile-1.0.0.dist-info/RECORD,, +backports.tarfile-1.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +backports.tarfile-1.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +backports.tarfile-1.0.0.dist-info/top_level.txt,sha256=cGjaLMOoBR1FK0ApojtzWVmViTtJ7JGIK_HwXiEsvtU,10 +backports/__pycache__/tarfile.cpython-312.pyc,, +backports/tarfile.py,sha256=IO3YX_ZYqn13VOi-3QLM0lnktn102U4d9wUrHc230LY,106920 diff --git a/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL similarity index 65% rename from pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL rename to pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL index 57e3d840d59..bab98d67588 100644 --- a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL +++ b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: bdist_wheel (0.38.4) +Generator: bdist_wheel (0.43.0) Root-Is-Purelib: true Tag: py3-none-any diff --git a/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt new file mode 100644 index 00000000000..99d2be5b64d --- /dev/null +++ b/pkg_resources/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +backports diff --git a/pkg_resources/_vendor/backports/tarfile.py b/pkg_resources/_vendor/backports/tarfile.py new file mode 100644 index 00000000000..a7a9a6e7b94 --- /dev/null +++ b/pkg_resources/_vendor/backports/tarfile.py @@ -0,0 +1,2900 @@ +#!/usr/bin/env python3 +#------------------------------------------------------------------- +# tarfile.py +#------------------------------------------------------------------- +# Copyright (C) 2002 Lars Gustaebel +# All rights reserved. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +"""Read from and write to tar format archives. +""" + +version = "0.9.0" +__author__ = "Lars Gust\u00e4bel (lars@gustaebel.de)" +__credits__ = "Gustavo Niemeyer, Niels Gust\u00e4bel, Richard Townsend." + +#--------- +# Imports +#--------- +from builtins import open as bltn_open +import sys +import os +import io +import shutil +import stat +import time +import struct +import copy +import re +import warnings + +try: + import pwd +except ImportError: + pwd = None +try: + import grp +except ImportError: + grp = None + +# os.symlink on Windows prior to 6.0 raises NotImplementedError +# OSError (winerror=1314) will be raised if the caller does not hold the +# SeCreateSymbolicLinkPrivilege privilege +symlink_exception = (AttributeError, NotImplementedError, OSError) + +# from tarfile import * +__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError", + "CompressionError", "StreamError", "ExtractError", "HeaderError", + "ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT", + "DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter", + "tar_filter", "FilterError", "AbsoluteLinkError", + "OutsideDestinationError", "SpecialFileError", "AbsolutePathError", + "LinkOutsideDestinationError"] + + +#--------------------------------------------------------- +# tar constants +#--------------------------------------------------------- +NUL = b"\0" # the null character +BLOCKSIZE = 512 # length of processing blocks +RECORDSIZE = BLOCKSIZE * 20 # length of records +GNU_MAGIC = b"ustar \0" # magic gnu tar string +POSIX_MAGIC = b"ustar\x0000" # magic posix tar string + +LENGTH_NAME = 100 # maximum length of a filename +LENGTH_LINK = 100 # maximum length of a linkname +LENGTH_PREFIX = 155 # maximum length of the prefix field + +REGTYPE = b"0" # regular file +AREGTYPE = b"\0" # regular file +LNKTYPE = b"1" # link (inside tarfile) +SYMTYPE = b"2" # symbolic link +CHRTYPE = b"3" # character special device +BLKTYPE = b"4" # block special device +DIRTYPE = b"5" # directory +FIFOTYPE = b"6" # fifo special device +CONTTYPE = b"7" # contiguous file + +GNUTYPE_LONGNAME = b"L" # GNU tar longname +GNUTYPE_LONGLINK = b"K" # GNU tar longlink +GNUTYPE_SPARSE = b"S" # GNU tar sparse file + +XHDTYPE = b"x" # POSIX.1-2001 extended header +XGLTYPE = b"g" # POSIX.1-2001 global header +SOLARIS_XHDTYPE = b"X" # Solaris extended header + +USTAR_FORMAT = 0 # POSIX.1-1988 (ustar) format +GNU_FORMAT = 1 # GNU tar format +PAX_FORMAT = 2 # POSIX.1-2001 (pax) format +DEFAULT_FORMAT = PAX_FORMAT + +#--------------------------------------------------------- +# tarfile constants +#--------------------------------------------------------- +# File types that tarfile supports: +SUPPORTED_TYPES = (REGTYPE, AREGTYPE, LNKTYPE, + SYMTYPE, DIRTYPE, FIFOTYPE, + CONTTYPE, CHRTYPE, BLKTYPE, + GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# File types that will be treated as a regular file. +REGULAR_TYPES = (REGTYPE, AREGTYPE, + CONTTYPE, GNUTYPE_SPARSE) + +# File types that are part of the GNU tar format. +GNU_TYPES = (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# Fields from a pax header that override a TarInfo attribute. +PAX_FIELDS = ("path", "linkpath", "size", "mtime", + "uid", "gid", "uname", "gname") + +# Fields from a pax header that are affected by hdrcharset. +PAX_NAME_FIELDS = {"path", "linkpath", "uname", "gname"} + +# Fields in a pax header that are numbers, all other fields +# are treated as strings. +PAX_NUMBER_FIELDS = { + "atime": float, + "ctime": float, + "mtime": float, + "uid": int, + "gid": int, + "size": int +} + +#--------------------------------------------------------- +# initialization +#--------------------------------------------------------- +if os.name == "nt": + ENCODING = "utf-8" +else: + ENCODING = sys.getfilesystemencoding() + +#--------------------------------------------------------- +# Some useful functions +#--------------------------------------------------------- + +def stn(s, length, encoding, errors): + """Convert a string to a null-terminated bytes object. + """ + if s is None: + raise ValueError("metadata cannot contain None") + s = s.encode(encoding, errors) + return s[:length] + (length - len(s)) * NUL + +def nts(s, encoding, errors): + """Convert a null-terminated bytes object to a string. + """ + p = s.find(b"\0") + if p != -1: + s = s[:p] + return s.decode(encoding, errors) + +def nti(s): + """Convert a number field to a python number. + """ + # There are two possible encodings for a number field, see + # itn() below. + if s[0] in (0o200, 0o377): + n = 0 + for i in range(len(s) - 1): + n <<= 8 + n += s[i + 1] + if s[0] == 0o377: + n = -(256 ** (len(s) - 1) - n) + else: + try: + s = nts(s, "ascii", "strict") + n = int(s.strip() or "0", 8) + except ValueError: + raise InvalidHeaderError("invalid header") + return n + +def itn(n, digits=8, format=DEFAULT_FORMAT): + """Convert a python number to a number field. + """ + # POSIX 1003.1-1988 requires numbers to be encoded as a string of + # octal digits followed by a null-byte, this allows values up to + # (8**(digits-1))-1. GNU tar allows storing numbers greater than + # that if necessary. A leading 0o200 or 0o377 byte indicate this + # particular encoding, the following digits-1 bytes are a big-endian + # base-256 representation. This allows values up to (256**(digits-1))-1. + # A 0o200 byte indicates a positive number, a 0o377 byte a negative + # number. + original_n = n + n = int(n) + if 0 <= n < 8 ** (digits - 1): + s = bytes("%0*o" % (digits - 1, n), "ascii") + NUL + elif format == GNU_FORMAT and -256 ** (digits - 1) <= n < 256 ** (digits - 1): + if n >= 0: + s = bytearray([0o200]) + else: + s = bytearray([0o377]) + n = 256 ** digits + n + + for i in range(digits - 1): + s.insert(1, n & 0o377) + n >>= 8 + else: + raise ValueError("overflow in number field") + + return s + +def calc_chksums(buf): + """Calculate the checksum for a member's header by summing up all + characters except for the chksum field which is treated as if + it was filled with spaces. According to the GNU tar sources, + some tars (Sun and NeXT) calculate chksum with signed char, + which will be different if there are chars in the buffer with + the high bit set. So we calculate two checksums, unsigned and + signed. + """ + unsigned_chksum = 256 + sum(struct.unpack_from("148B8x356B", buf)) + signed_chksum = 256 + sum(struct.unpack_from("148b8x356b", buf)) + return unsigned_chksum, signed_chksum + +def copyfileobj(src, dst, length=None, exception=OSError, bufsize=None): + """Copy length bytes from fileobj src to fileobj dst. + If length is None, copy the entire content. + """ + bufsize = bufsize or 16 * 1024 + if length == 0: + return + if length is None: + shutil.copyfileobj(src, dst, bufsize) + return + + blocks, remainder = divmod(length, bufsize) + for b in range(blocks): + buf = src.read(bufsize) + if len(buf) < bufsize: + raise exception("unexpected end of data") + dst.write(buf) + + if remainder != 0: + buf = src.read(remainder) + if len(buf) < remainder: + raise exception("unexpected end of data") + dst.write(buf) + return + +def _safe_print(s): + encoding = getattr(sys.stdout, 'encoding', None) + if encoding is not None: + s = s.encode(encoding, 'backslashreplace').decode(encoding) + print(s, end=' ') + + +class TarError(Exception): + """Base exception.""" + pass +class ExtractError(TarError): + """General exception for extract errors.""" + pass +class ReadError(TarError): + """Exception for unreadable tar archives.""" + pass +class CompressionError(TarError): + """Exception for unavailable compression methods.""" + pass +class StreamError(TarError): + """Exception for unsupported operations on stream-like TarFiles.""" + pass +class HeaderError(TarError): + """Base exception for header errors.""" + pass +class EmptyHeaderError(HeaderError): + """Exception for empty headers.""" + pass +class TruncatedHeaderError(HeaderError): + """Exception for truncated headers.""" + pass +class EOFHeaderError(HeaderError): + """Exception for end of file headers.""" + pass +class InvalidHeaderError(HeaderError): + """Exception for invalid headers.""" + pass +class SubsequentHeaderError(HeaderError): + """Exception for missing and invalid extended headers.""" + pass + +#--------------------------- +# internal stream interface +#--------------------------- +class _LowLevelFile: + """Low-level file object. Supports reading and writing. + It is used instead of a regular file object for streaming + access. + """ + + def __init__(self, name, mode): + mode = { + "r": os.O_RDONLY, + "w": os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + }[mode] + if hasattr(os, "O_BINARY"): + mode |= os.O_BINARY + self.fd = os.open(name, mode, 0o666) + + def close(self): + os.close(self.fd) + + def read(self, size): + return os.read(self.fd, size) + + def write(self, s): + os.write(self.fd, s) + +class _Stream: + """Class that serves as an adapter between TarFile and + a stream-like object. The stream-like object only + needs to have a read() or write() method that works with bytes, + and the method is accessed blockwise. + Use of gzip or bzip2 compression is possible. + A stream-like object could be for example: sys.stdin.buffer, + sys.stdout.buffer, a socket, a tape device etc. + + _Stream is intended to be used only internally. + """ + + def __init__(self, name, mode, comptype, fileobj, bufsize, + compresslevel): + """Construct a _Stream object. + """ + self._extfileobj = True + if fileobj is None: + fileobj = _LowLevelFile(name, mode) + self._extfileobj = False + + if comptype == '*': + # Enable transparent compression detection for the + # stream interface + fileobj = _StreamProxy(fileobj) + comptype = fileobj.getcomptype() + + self.name = name or "" + self.mode = mode + self.comptype = comptype + self.fileobj = fileobj + self.bufsize = bufsize + self.buf = b"" + self.pos = 0 + self.closed = False + + try: + if comptype == "gz": + try: + import zlib + except ImportError: + raise CompressionError("zlib module is not available") from None + self.zlib = zlib + self.crc = zlib.crc32(b"") + if mode == "r": + self.exception = zlib.error + self._init_read_gz() + else: + self._init_write_gz(compresslevel) + + elif comptype == "bz2": + try: + import bz2 + except ImportError: + raise CompressionError("bz2 module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = bz2.BZ2Decompressor() + self.exception = OSError + else: + self.cmp = bz2.BZ2Compressor(compresslevel) + + elif comptype == "xz": + try: + import lzma + except ImportError: + raise CompressionError("lzma module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = lzma.LZMADecompressor() + self.exception = lzma.LZMAError + else: + self.cmp = lzma.LZMACompressor() + + elif comptype != "tar": + raise CompressionError("unknown compression type %r" % comptype) + + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + def __del__(self): + if hasattr(self, "closed") and not self.closed: + self.close() + + def _init_write_gz(self, compresslevel): + """Initialize for writing with gzip compression. + """ + self.cmp = self.zlib.compressobj(compresslevel, + self.zlib.DEFLATED, + -self.zlib.MAX_WBITS, + self.zlib.DEF_MEM_LEVEL, + 0) + timestamp = struct.pack(" self.bufsize: + self.fileobj.write(self.buf[:self.bufsize]) + self.buf = self.buf[self.bufsize:] + + def close(self): + """Close the _Stream object. No operation should be + done on it afterwards. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode == "w" and self.comptype != "tar": + self.buf += self.cmp.flush() + + if self.mode == "w" and self.buf: + self.fileobj.write(self.buf) + self.buf = b"" + if self.comptype == "gz": + self.fileobj.write(struct.pack("= 0: + blocks, remainder = divmod(pos - self.pos, self.bufsize) + for i in range(blocks): + self.read(self.bufsize) + self.read(remainder) + else: + raise StreamError("seeking backwards is not allowed") + return self.pos + + def read(self, size): + """Return the next size number of bytes from the stream.""" + assert size is not None + buf = self._read(size) + self.pos += len(buf) + return buf + + def _read(self, size): + """Return size bytes from the stream. + """ + if self.comptype == "tar": + return self.__read(size) + + c = len(self.dbuf) + t = [self.dbuf] + while c < size: + # Skip underlying buffer to avoid unaligned double buffering. + if self.buf: + buf = self.buf + self.buf = b"" + else: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + try: + buf = self.cmp.decompress(buf) + except self.exception as e: + raise ReadError("invalid compressed data") from e + t.append(buf) + c += len(buf) + t = b"".join(t) + self.dbuf = t[size:] + return t[:size] + + def __read(self, size): + """Return size bytes from stream. If internal buffer is empty, + read another block from the stream. + """ + c = len(self.buf) + t = [self.buf] + while c < size: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + t.append(buf) + c += len(buf) + t = b"".join(t) + self.buf = t[size:] + return t[:size] +# class _Stream + +class _StreamProxy(object): + """Small proxy class that enables transparent compression + detection for the Stream interface (mode 'r|*'). + """ + + def __init__(self, fileobj): + self.fileobj = fileobj + self.buf = self.fileobj.read(BLOCKSIZE) + + def read(self, size): + self.read = self.fileobj.read + return self.buf + + def getcomptype(self): + if self.buf.startswith(b"\x1f\x8b\x08"): + return "gz" + elif self.buf[0:3] == b"BZh" and self.buf[4:10] == b"1AY&SY": + return "bz2" + elif self.buf.startswith((b"\x5d\x00\x00\x80", b"\xfd7zXZ")): + return "xz" + else: + return "tar" + + def close(self): + self.fileobj.close() +# class StreamProxy + +#------------------------ +# Extraction file object +#------------------------ +class _FileInFile(object): + """A thin wrapper around an existing file object that + provides a part of its data as an individual file + object. + """ + + def __init__(self, fileobj, offset, size, name, blockinfo=None): + self.fileobj = fileobj + self.offset = offset + self.size = size + self.position = 0 + self.name = name + self.closed = False + + if blockinfo is None: + blockinfo = [(0, size)] + + # Construct a map with data and zero blocks. + self.map_index = 0 + self.map = [] + lastpos = 0 + realpos = self.offset + for offset, size in blockinfo: + if offset > lastpos: + self.map.append((False, lastpos, offset, None)) + self.map.append((True, offset, offset + size, realpos)) + realpos += size + lastpos = offset + size + if lastpos < self.size: + self.map.append((False, lastpos, self.size, None)) + + def flush(self): + pass + + def readable(self): + return True + + def writable(self): + return False + + def seekable(self): + return self.fileobj.seekable() + + def tell(self): + """Return the current file position. + """ + return self.position + + def seek(self, position, whence=io.SEEK_SET): + """Seek to a position in the file. + """ + if whence == io.SEEK_SET: + self.position = min(max(position, 0), self.size) + elif whence == io.SEEK_CUR: + if position < 0: + self.position = max(self.position + position, 0) + else: + self.position = min(self.position + position, self.size) + elif whence == io.SEEK_END: + self.position = max(min(self.size + position, self.size), 0) + else: + raise ValueError("Invalid argument") + return self.position + + def read(self, size=None): + """Read data from the file. + """ + if size is None: + size = self.size - self.position + else: + size = min(size, self.size - self.position) + + buf = b"" + while size > 0: + while True: + data, start, stop, offset = self.map[self.map_index] + if start <= self.position < stop: + break + else: + self.map_index += 1 + if self.map_index == len(self.map): + self.map_index = 0 + length = min(size, stop - self.position) + if data: + self.fileobj.seek(offset + (self.position - start)) + b = self.fileobj.read(length) + if len(b) != length: + raise ReadError("unexpected end of data") + buf += b + else: + buf += NUL * length + size -= length + self.position += length + return buf + + def readinto(self, b): + buf = self.read(len(b)) + b[:len(buf)] = buf + return len(buf) + + def close(self): + self.closed = True +#class _FileInFile + +class ExFileObject(io.BufferedReader): + + def __init__(self, tarfile, tarinfo): + fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data, + tarinfo.size, tarinfo.name, tarinfo.sparse) + super().__init__(fileobj) +#class ExFileObject + + +#----------------------------- +# extraction filters (PEP 706) +#----------------------------- + +class FilterError(TarError): + pass + +class AbsolutePathError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'member {tarinfo.name!r} has an absolute path') + +class OutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would be extracted to {path!r}, ' + + 'which is outside the destination') + +class SpecialFileError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a special file') + +class AbsoluteLinkError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a link to an absolute path') + +class LinkOutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would link to {path!r}, ' + + 'which is outside the destination') + +def _get_filtered_attrs(member, dest_path, for_data=True): + new_attrs = {} + name = member.name + dest_path = os.path.realpath(dest_path) + # Strip leading / (tar's directory separator) from filenames. + # Include os.sep (target OS directory separator) as well. + if name.startswith(('/', os.sep)): + name = new_attrs['name'] = member.path.lstrip('/' + os.sep) + if os.path.isabs(name): + # Path is absolute even after stripping. + # For example, 'C:/foo' on Windows. + raise AbsolutePathError(member) + # Ensure we stay in the destination + target_path = os.path.realpath(os.path.join(dest_path, name)) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise OutsideDestinationError(member, target_path) + # Limit permissions (no high bits, and go-w) + mode = member.mode + if mode is not None: + # Strip high bits & group/other write bits + mode = mode & 0o755 + if for_data: + # For data, handle permissions & file types + if member.isreg() or member.islnk(): + if not mode & 0o100: + # Clear executable bits if not executable by user + mode &= ~0o111 + # Ensure owner can read & write + mode |= 0o600 + elif member.isdir() or member.issym(): + # Ignore mode for directories & symlinks + mode = None + else: + # Reject special files + raise SpecialFileError(member) + if mode != member.mode: + new_attrs['mode'] = mode + if for_data: + # Ignore ownership for 'data' + if member.uid is not None: + new_attrs['uid'] = None + if member.gid is not None: + new_attrs['gid'] = None + if member.uname is not None: + new_attrs['uname'] = None + if member.gname is not None: + new_attrs['gname'] = None + # Check link destination for 'data' + if member.islnk() or member.issym(): + if os.path.isabs(member.linkname): + raise AbsoluteLinkError(member) + if member.issym(): + target_path = os.path.join(dest_path, + os.path.dirname(name), + member.linkname) + else: + target_path = os.path.join(dest_path, + member.linkname) + target_path = os.path.realpath(target_path) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise LinkOutsideDestinationError(member, target_path) + return new_attrs + +def fully_trusted_filter(member, dest_path): + return member + +def tar_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, False) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +def data_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, True) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +_NAMED_FILTERS = { + "fully_trusted": fully_trusted_filter, + "tar": tar_filter, + "data": data_filter, +} + +#------------------ +# Exported Classes +#------------------ + +# Sentinel for replace() defaults, meaning "don't change the attribute" +_KEEP = object() + +class TarInfo(object): + """Informational class which holds the details about an + archive member given by a tar header block. + TarInfo objects are returned by TarFile.getmember(), + TarFile.getmembers() and TarFile.gettarinfo() and are + usually created internally. + """ + + __slots__ = dict( + name = 'Name of the archive member.', + mode = 'Permission bits.', + uid = 'User ID of the user who originally stored this member.', + gid = 'Group ID of the user who originally stored this member.', + size = 'Size in bytes.', + mtime = 'Time of last modification.', + chksum = 'Header checksum.', + type = ('File type. type is usually one of these constants: ' + 'REGTYPE, AREGTYPE, LNKTYPE, SYMTYPE, DIRTYPE, FIFOTYPE, ' + 'CONTTYPE, CHRTYPE, BLKTYPE, GNUTYPE_SPARSE.'), + linkname = ('Name of the target file name, which is only present ' + 'in TarInfo objects of type LNKTYPE and SYMTYPE.'), + uname = 'User name.', + gname = 'Group name.', + devmajor = 'Device major number.', + devminor = 'Device minor number.', + offset = 'The tar header starts here.', + offset_data = "The file's data starts here.", + pax_headers = ('A dictionary containing key-value pairs of an ' + 'associated pax extended header.'), + sparse = 'Sparse member information.', + tarfile = None, + _sparse_structs = None, + _link_target = None, + ) + + def __init__(self, name=""): + """Construct a TarInfo object. name is the optional name + of the member. + """ + self.name = name # member name + self.mode = 0o644 # file permissions + self.uid = 0 # user id + self.gid = 0 # group id + self.size = 0 # file size + self.mtime = 0 # modification time + self.chksum = 0 # header checksum + self.type = REGTYPE # member type + self.linkname = "" # link name + self.uname = "" # user name + self.gname = "" # group name + self.devmajor = 0 # device major number + self.devminor = 0 # device minor number + + self.offset = 0 # the tar header starts here + self.offset_data = 0 # the file's data starts here + + self.sparse = None # sparse member information + self.pax_headers = {} # pax header information + + @property + def path(self): + 'In pax headers, "name" is called "path".' + return self.name + + @path.setter + def path(self, name): + self.name = name + + @property + def linkpath(self): + 'In pax headers, "linkname" is called "linkpath".' + return self.linkname + + @linkpath.setter + def linkpath(self, linkname): + self.linkname = linkname + + def __repr__(self): + return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self)) + + def replace(self, *, + name=_KEEP, mtime=_KEEP, mode=_KEEP, linkname=_KEEP, + uid=_KEEP, gid=_KEEP, uname=_KEEP, gname=_KEEP, + deep=True, _KEEP=_KEEP): + """Return a deep copy of self with the given attributes replaced. + """ + if deep: + result = copy.deepcopy(self) + else: + result = copy.copy(self) + if name is not _KEEP: + result.name = name + if mtime is not _KEEP: + result.mtime = mtime + if mode is not _KEEP: + result.mode = mode + if linkname is not _KEEP: + result.linkname = linkname + if uid is not _KEEP: + result.uid = uid + if gid is not _KEEP: + result.gid = gid + if uname is not _KEEP: + result.uname = uname + if gname is not _KEEP: + result.gname = gname + return result + + def get_info(self): + """Return the TarInfo's attributes as a dictionary. + """ + if self.mode is None: + mode = None + else: + mode = self.mode & 0o7777 + info = { + "name": self.name, + "mode": mode, + "uid": self.uid, + "gid": self.gid, + "size": self.size, + "mtime": self.mtime, + "chksum": self.chksum, + "type": self.type, + "linkname": self.linkname, + "uname": self.uname, + "gname": self.gname, + "devmajor": self.devmajor, + "devminor": self.devminor + } + + if info["type"] == DIRTYPE and not info["name"].endswith("/"): + info["name"] += "/" + + return info + + def tobuf(self, format=DEFAULT_FORMAT, encoding=ENCODING, errors="surrogateescape"): + """Return a tar header as a string of 512 byte blocks. + """ + info = self.get_info() + for name, value in info.items(): + if value is None: + raise ValueError("%s may not be None" % name) + + if format == USTAR_FORMAT: + return self.create_ustar_header(info, encoding, errors) + elif format == GNU_FORMAT: + return self.create_gnu_header(info, encoding, errors) + elif format == PAX_FORMAT: + return self.create_pax_header(info, encoding) + else: + raise ValueError("invalid format") + + def create_ustar_header(self, info, encoding, errors): + """Return the object as a ustar header block. + """ + info["magic"] = POSIX_MAGIC + + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + raise ValueError("linkname is too long") + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + info["prefix"], info["name"] = self._posix_split_name(info["name"], encoding, errors) + + return self._create_header(info, USTAR_FORMAT, encoding, errors) + + def create_gnu_header(self, info, encoding, errors): + """Return the object as a GNU header block sequence. + """ + info["magic"] = GNU_MAGIC + + buf = b"" + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + buf += self._create_gnu_long_header(info["linkname"], GNUTYPE_LONGLINK, encoding, errors) + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + buf += self._create_gnu_long_header(info["name"], GNUTYPE_LONGNAME, encoding, errors) + + return buf + self._create_header(info, GNU_FORMAT, encoding, errors) + + def create_pax_header(self, info, encoding): + """Return the object as a ustar header block. If it cannot be + represented this way, prepend a pax extended header sequence + with supplement information. + """ + info["magic"] = POSIX_MAGIC + pax_headers = self.pax_headers.copy() + + # Test string fields for values that exceed the field length or cannot + # be represented in ASCII encoding. + for name, hname, length in ( + ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK), + ("uname", "uname", 32), ("gname", "gname", 32)): + + if hname in pax_headers: + # The pax header has priority. + continue + + # Try to encode the string as ASCII. + try: + info[name].encode("ascii", "strict") + except UnicodeEncodeError: + pax_headers[hname] = info[name] + continue + + if len(info[name]) > length: + pax_headers[hname] = info[name] + + # Test number fields for values that exceed the field limit or values + # that like to be stored as float. + for name, digits in (("uid", 8), ("gid", 8), ("size", 12), ("mtime", 12)): + needs_pax = False + + val = info[name] + val_is_float = isinstance(val, float) + val_int = round(val) if val_is_float else val + if not 0 <= val_int < 8 ** (digits - 1): + # Avoid overflow. + info[name] = 0 + needs_pax = True + elif val_is_float: + # Put rounded value in ustar header, and full + # precision value in pax header. + info[name] = val_int + needs_pax = True + + # The existing pax header has priority. + if needs_pax and name not in pax_headers: + pax_headers[name] = str(val) + + # Create a pax extended header if necessary. + if pax_headers: + buf = self._create_pax_generic_header(pax_headers, XHDTYPE, encoding) + else: + buf = b"" + + return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace") + + @classmethod + def create_pax_global_header(cls, pax_headers): + """Return the object as a pax global header block sequence. + """ + return cls._create_pax_generic_header(pax_headers, XGLTYPE, "utf-8") + + def _posix_split_name(self, name, encoding, errors): + """Split a name longer than 100 chars into a prefix + and a name part. + """ + components = name.split("/") + for i in range(1, len(components)): + prefix = "/".join(components[:i]) + name = "/".join(components[i:]) + if len(prefix.encode(encoding, errors)) <= LENGTH_PREFIX and \ + len(name.encode(encoding, errors)) <= LENGTH_NAME: + break + else: + raise ValueError("name is too long") + + return prefix, name + + @staticmethod + def _create_header(info, format, encoding, errors): + """Return a header block. info is a dictionary with file + information, format must be one of the *_FORMAT constants. + """ + has_device_fields = info.get("type") in (CHRTYPE, BLKTYPE) + if has_device_fields: + devmajor = itn(info.get("devmajor", 0), 8, format) + devminor = itn(info.get("devminor", 0), 8, format) + else: + devmajor = stn("", 8, encoding, errors) + devminor = stn("", 8, encoding, errors) + + # None values in metadata should cause ValueError. + # itn()/stn() do this for all fields except type. + filetype = info.get("type", REGTYPE) + if filetype is None: + raise ValueError("TarInfo.type must not be None") + + parts = [ + stn(info.get("name", ""), 100, encoding, errors), + itn(info.get("mode", 0) & 0o7777, 8, format), + itn(info.get("uid", 0), 8, format), + itn(info.get("gid", 0), 8, format), + itn(info.get("size", 0), 12, format), + itn(info.get("mtime", 0), 12, format), + b" ", # checksum field + filetype, + stn(info.get("linkname", ""), 100, encoding, errors), + info.get("magic", POSIX_MAGIC), + stn(info.get("uname", ""), 32, encoding, errors), + stn(info.get("gname", ""), 32, encoding, errors), + devmajor, + devminor, + stn(info.get("prefix", ""), 155, encoding, errors) + ] + + buf = struct.pack("%ds" % BLOCKSIZE, b"".join(parts)) + chksum = calc_chksums(buf[-BLOCKSIZE:])[0] + buf = buf[:-364] + bytes("%06o\0" % chksum, "ascii") + buf[-357:] + return buf + + @staticmethod + def _create_payload(payload): + """Return the string payload filled with zero bytes + up to the next 512 byte border. + """ + blocks, remainder = divmod(len(payload), BLOCKSIZE) + if remainder > 0: + payload += (BLOCKSIZE - remainder) * NUL + return payload + + @classmethod + def _create_gnu_long_header(cls, name, type, encoding, errors): + """Return a GNUTYPE_LONGNAME or GNUTYPE_LONGLINK sequence + for name. + """ + name = name.encode(encoding, errors) + NUL + + info = {} + info["name"] = "././@LongLink" + info["type"] = type + info["size"] = len(name) + info["magic"] = GNU_MAGIC + + # create extended header + name blocks. + return cls._create_header(info, USTAR_FORMAT, encoding, errors) + \ + cls._create_payload(name) + + @classmethod + def _create_pax_generic_header(cls, pax_headers, type, encoding): + """Return a POSIX.1-2008 extended or global header sequence + that contains a list of keyword, value pairs. The values + must be strings. + """ + # Check if one of the fields contains surrogate characters and thereby + # forces hdrcharset=BINARY, see _proc_pax() for more information. + binary = False + for keyword, value in pax_headers.items(): + try: + value.encode("utf-8", "strict") + except UnicodeEncodeError: + binary = True + break + + records = b"" + if binary: + # Put the hdrcharset field at the beginning of the header. + records += b"21 hdrcharset=BINARY\n" + + for keyword, value in pax_headers.items(): + keyword = keyword.encode("utf-8") + if binary: + # Try to restore the original byte representation of `value'. + # Needless to say, that the encoding must match the string. + value = value.encode(encoding, "surrogateescape") + else: + value = value.encode("utf-8") + + l = len(keyword) + len(value) + 3 # ' ' + '=' + '\n' + n = p = 0 + while True: + n = l + len(str(p)) + if n == p: + break + p = n + records += bytes(str(p), "ascii") + b" " + keyword + b"=" + value + b"\n" + + # We use a hardcoded "././@PaxHeader" name like star does + # instead of the one that POSIX recommends. + info = {} + info["name"] = "././@PaxHeader" + info["type"] = type + info["size"] = len(records) + info["magic"] = POSIX_MAGIC + + # Create pax header + record blocks. + return cls._create_header(info, USTAR_FORMAT, "ascii", "replace") + \ + cls._create_payload(records) + + @classmethod + def frombuf(cls, buf, encoding, errors): + """Construct a TarInfo object from a 512 byte bytes object. + """ + if len(buf) == 0: + raise EmptyHeaderError("empty header") + if len(buf) != BLOCKSIZE: + raise TruncatedHeaderError("truncated header") + if buf.count(NUL) == BLOCKSIZE: + raise EOFHeaderError("end of file header") + + chksum = nti(buf[148:156]) + if chksum not in calc_chksums(buf): + raise InvalidHeaderError("bad checksum") + + obj = cls() + obj.name = nts(buf[0:100], encoding, errors) + obj.mode = nti(buf[100:108]) + obj.uid = nti(buf[108:116]) + obj.gid = nti(buf[116:124]) + obj.size = nti(buf[124:136]) + obj.mtime = nti(buf[136:148]) + obj.chksum = chksum + obj.type = buf[156:157] + obj.linkname = nts(buf[157:257], encoding, errors) + obj.uname = nts(buf[265:297], encoding, errors) + obj.gname = nts(buf[297:329], encoding, errors) + obj.devmajor = nti(buf[329:337]) + obj.devminor = nti(buf[337:345]) + prefix = nts(buf[345:500], encoding, errors) + + # Old V7 tar format represents a directory as a regular + # file with a trailing slash. + if obj.type == AREGTYPE and obj.name.endswith("/"): + obj.type = DIRTYPE + + # The old GNU sparse format occupies some of the unused + # space in the buffer for up to 4 sparse structures. + # Save them for later processing in _proc_sparse(). + if obj.type == GNUTYPE_SPARSE: + pos = 386 + structs = [] + for i in range(4): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[482]) + origsize = nti(buf[483:495]) + obj._sparse_structs = (structs, isextended, origsize) + + # Remove redundant slashes from directories. + if obj.isdir(): + obj.name = obj.name.rstrip("/") + + # Reconstruct a ustar longname. + if prefix and obj.type not in GNU_TYPES: + obj.name = prefix + "/" + obj.name + return obj + + @classmethod + def fromtarfile(cls, tarfile): + """Return the next TarInfo object from TarFile object + tarfile. + """ + buf = tarfile.fileobj.read(BLOCKSIZE) + obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors) + obj.offset = tarfile.fileobj.tell() - BLOCKSIZE + return obj._proc_member(tarfile) + + #-------------------------------------------------------------------------- + # The following are methods that are called depending on the type of a + # member. The entry point is _proc_member() which can be overridden in a + # subclass to add custom _proc_*() methods. A _proc_*() method MUST + # implement the following + # operations: + # 1. Set self.offset_data to the position where the data blocks begin, + # if there is data that follows. + # 2. Set tarfile.offset to the position where the next member's header will + # begin. + # 3. Return self or another valid TarInfo object. + def _proc_member(self, tarfile): + """Choose the right processing method depending on + the type and call it. + """ + if self.type in (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK): + return self._proc_gnulong(tarfile) + elif self.type == GNUTYPE_SPARSE: + return self._proc_sparse(tarfile) + elif self.type in (XHDTYPE, XGLTYPE, SOLARIS_XHDTYPE): + return self._proc_pax(tarfile) + else: + return self._proc_builtin(tarfile) + + def _proc_builtin(self, tarfile): + """Process a builtin type or an unknown type which + will be treated as a regular file. + """ + self.offset_data = tarfile.fileobj.tell() + offset = self.offset_data + if self.isreg() or self.type not in SUPPORTED_TYPES: + # Skip the following data blocks. + offset += self._block(self.size) + tarfile.offset = offset + + # Patch the TarInfo object with saved global + # header information. + self._apply_pax_info(tarfile.pax_headers, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if self.isdir(): + self.name = self.name.rstrip("/") + + return self + + def _proc_gnulong(self, tarfile): + """Process the blocks that hold a GNU longname + or longlink member. + """ + buf = tarfile.fileobj.read(self._block(self.size)) + + # Fetch the next header and process it. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Patch the TarInfo object from the next header with + # the longname information. + next.offset = self.offset + if self.type == GNUTYPE_LONGNAME: + next.name = nts(buf, tarfile.encoding, tarfile.errors) + elif self.type == GNUTYPE_LONGLINK: + next.linkname = nts(buf, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if next.isdir(): + next.name = next.name.removesuffix("/") + + return next + + def _proc_sparse(self, tarfile): + """Process a GNU sparse header plus extra headers. + """ + # We already collected some sparse structures in frombuf(). + structs, isextended, origsize = self._sparse_structs + del self._sparse_structs + + # Collect sparse structures from extended header blocks. + while isextended: + buf = tarfile.fileobj.read(BLOCKSIZE) + pos = 0 + for i in range(21): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + if offset and numbytes: + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[504]) + self.sparse = structs + + self.offset_data = tarfile.fileobj.tell() + tarfile.offset = self.offset_data + self._block(self.size) + self.size = origsize + return self + + def _proc_pax(self, tarfile): + """Process an extended or global header as described in + POSIX.1-2008. + """ + # Read the header information. + buf = tarfile.fileobj.read(self._block(self.size)) + + # A pax header stores supplemental information for either + # the following file (extended) or all following files + # (global). + if self.type == XGLTYPE: + pax_headers = tarfile.pax_headers + else: + pax_headers = tarfile.pax_headers.copy() + + # Check if the pax header contains a hdrcharset field. This tells us + # the encoding of the path, linkpath, uname and gname fields. Normally, + # these fields are UTF-8 encoded but since POSIX.1-2008 tar + # implementations are allowed to store them as raw binary strings if + # the translation to UTF-8 fails. + match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf) + if match is not None: + pax_headers["hdrcharset"] = match.group(1).decode("utf-8") + + # For the time being, we don't care about anything other than "BINARY". + # The only other value that is currently allowed by the standard is + # "ISO-IR 10646 2000 UTF-8" in other words UTF-8. + hdrcharset = pax_headers.get("hdrcharset") + if hdrcharset == "BINARY": + encoding = tarfile.encoding + else: + encoding = "utf-8" + + # Parse pax header information. A record looks like that: + # "%d %s=%s\n" % (length, keyword, value). length is the size + # of the complete record including the length field itself and + # the newline. keyword and value are both UTF-8 encoded strings. + regex = re.compile(br"(\d+) ([^=]+)=") + pos = 0 + while match := regex.match(buf, pos): + length, keyword = match.groups() + length = int(length) + if length == 0: + raise InvalidHeaderError("invalid header") + value = buf[match.end(2) + 1:match.start(1) + length - 1] + + # Normally, we could just use "utf-8" as the encoding and "strict" + # as the error handler, but we better not take the risk. For + # example, GNU tar <= 1.23 is known to store filenames it cannot + # translate to UTF-8 as raw strings (unfortunately without a + # hdrcharset=BINARY header). + # We first try the strict standard encoding, and if that fails we + # fall back on the user's encoding and error handler. + keyword = self._decode_pax_field(keyword, "utf-8", "utf-8", + tarfile.errors) + if keyword in PAX_NAME_FIELDS: + value = self._decode_pax_field(value, encoding, tarfile.encoding, + tarfile.errors) + else: + value = self._decode_pax_field(value, "utf-8", "utf-8", + tarfile.errors) + + pax_headers[keyword] = value + pos += length + + # Fetch the next header. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Process GNU sparse information. + if "GNU.sparse.map" in pax_headers: + # GNU extended sparse format version 0.1. + self._proc_gnusparse_01(next, pax_headers) + + elif "GNU.sparse.size" in pax_headers: + # GNU extended sparse format version 0.0. + self._proc_gnusparse_00(next, pax_headers, buf) + + elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0": + # GNU extended sparse format version 1.0. + self._proc_gnusparse_10(next, pax_headers, tarfile) + + if self.type in (XHDTYPE, SOLARIS_XHDTYPE): + # Patch the TarInfo object with the extended header info. + next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors) + next.offset = self.offset + + if "size" in pax_headers: + # If the extended header replaces the size field, + # we need to recalculate the offset where the next + # header starts. + offset = next.offset_data + if next.isreg() or next.type not in SUPPORTED_TYPES: + offset += next._block(next.size) + tarfile.offset = offset + + return next + + def _proc_gnusparse_00(self, next, pax_headers, buf): + """Process a GNU tar extended sparse header, version 0.0. + """ + offsets = [] + for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf): + offsets.append(int(match.group(1))) + numbytes = [] + for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf): + numbytes.append(int(match.group(1))) + next.sparse = list(zip(offsets, numbytes)) + + def _proc_gnusparse_01(self, next, pax_headers): + """Process a GNU tar extended sparse header, version 0.1. + """ + sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")] + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _proc_gnusparse_10(self, next, pax_headers, tarfile): + """Process a GNU tar extended sparse header, version 1.0. + """ + fields = None + sparse = [] + buf = tarfile.fileobj.read(BLOCKSIZE) + fields, buf = buf.split(b"\n", 1) + fields = int(fields) + while len(sparse) < fields * 2: + if b"\n" not in buf: + buf += tarfile.fileobj.read(BLOCKSIZE) + number, buf = buf.split(b"\n", 1) + sparse.append(int(number)) + next.offset_data = tarfile.fileobj.tell() + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _apply_pax_info(self, pax_headers, encoding, errors): + """Replace fields with supplemental information from a previous + pax extended or global header. + """ + for keyword, value in pax_headers.items(): + if keyword == "GNU.sparse.name": + setattr(self, "path", value) + elif keyword == "GNU.sparse.size": + setattr(self, "size", int(value)) + elif keyword == "GNU.sparse.realsize": + setattr(self, "size", int(value)) + elif keyword in PAX_FIELDS: + if keyword in PAX_NUMBER_FIELDS: + try: + value = PAX_NUMBER_FIELDS[keyword](value) + except ValueError: + value = 0 + if keyword == "path": + value = value.rstrip("/") + setattr(self, keyword, value) + + self.pax_headers = pax_headers.copy() + + def _decode_pax_field(self, value, encoding, fallback_encoding, fallback_errors): + """Decode a single field from a pax record. + """ + try: + return value.decode(encoding, "strict") + except UnicodeDecodeError: + return value.decode(fallback_encoding, fallback_errors) + + def _block(self, count): + """Round up a byte count by BLOCKSIZE and return it, + e.g. _block(834) => 1024. + """ + blocks, remainder = divmod(count, BLOCKSIZE) + if remainder: + blocks += 1 + return blocks * BLOCKSIZE + + def isreg(self): + 'Return True if the Tarinfo object is a regular file.' + return self.type in REGULAR_TYPES + + def isfile(self): + 'Return True if the Tarinfo object is a regular file.' + return self.isreg() + + def isdir(self): + 'Return True if it is a directory.' + return self.type == DIRTYPE + + def issym(self): + 'Return True if it is a symbolic link.' + return self.type == SYMTYPE + + def islnk(self): + 'Return True if it is a hard link.' + return self.type == LNKTYPE + + def ischr(self): + 'Return True if it is a character device.' + return self.type == CHRTYPE + + def isblk(self): + 'Return True if it is a block device.' + return self.type == BLKTYPE + + def isfifo(self): + 'Return True if it is a FIFO.' + return self.type == FIFOTYPE + + def issparse(self): + return self.sparse is not None + + def isdev(self): + 'Return True if it is one of character device, block device or FIFO.' + return self.type in (CHRTYPE, BLKTYPE, FIFOTYPE) +# class TarInfo + +class TarFile(object): + """The TarFile Class provides an interface to tar archives. + """ + + debug = 0 # May be set from 0 (no msgs) to 3 (all msgs) + + dereference = False # If true, add content of linked file to the + # tar file, else the link. + + ignore_zeros = False # If true, skips empty or invalid blocks and + # continues processing. + + errorlevel = 1 # If 0, fatal errors only appear in debug + # messages (if debug >= 0). If > 0, errors + # are passed to the caller as exceptions. + + format = DEFAULT_FORMAT # The format to use when creating an archive. + + encoding = ENCODING # Encoding for 8-bit character strings. + + errors = None # Error handler for unicode conversion. + + tarinfo = TarInfo # The default TarInfo class to use. + + fileobject = ExFileObject # The file-object for extractfile(). + + extraction_filter = None # The default filter for extraction. + + def __init__(self, name=None, mode="r", fileobj=None, format=None, + tarinfo=None, dereference=None, ignore_zeros=None, encoding=None, + errors="surrogateescape", pax_headers=None, debug=None, + errorlevel=None, copybufsize=None): + """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to + read from an existing archive, 'a' to append data to an existing + file or 'w' to create a new file overwriting an existing one. `mode' + defaults to 'r'. + If `fileobj' is given, it is used for reading or writing data. If it + can be determined, `mode' is overridden by `fileobj's mode. + `fileobj' is not closed, when TarFile is closed. + """ + modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"} + if mode not in modes: + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + self.mode = mode + self._mode = modes[mode] + + if not fileobj: + if self.mode == "a" and not os.path.exists(name): + # Create nonexistent files in append mode. + self.mode = "w" + self._mode = "wb" + fileobj = bltn_open(name, self._mode) + self._extfileobj = False + else: + if (name is None and hasattr(fileobj, "name") and + isinstance(fileobj.name, (str, bytes))): + name = fileobj.name + if hasattr(fileobj, "mode"): + self._mode = fileobj.mode + self._extfileobj = True + self.name = os.path.abspath(name) if name else None + self.fileobj = fileobj + + # Init attributes. + if format is not None: + self.format = format + if tarinfo is not None: + self.tarinfo = tarinfo + if dereference is not None: + self.dereference = dereference + if ignore_zeros is not None: + self.ignore_zeros = ignore_zeros + if encoding is not None: + self.encoding = encoding + self.errors = errors + + if pax_headers is not None and self.format == PAX_FORMAT: + self.pax_headers = pax_headers + else: + self.pax_headers = {} + + if debug is not None: + self.debug = debug + if errorlevel is not None: + self.errorlevel = errorlevel + + # Init datastructures. + self.copybufsize = copybufsize + self.closed = False + self.members = [] # list of members as TarInfo objects + self._loaded = False # flag if all members have been read + self.offset = self.fileobj.tell() + # current position in the archive file + self.inodes = {} # dictionary caching the inodes of + # archive members already added + + try: + if self.mode == "r": + self.firstmember = None + self.firstmember = self.next() + + if self.mode == "a": + # Move to the end of the archive, + # before the first empty block. + while True: + self.fileobj.seek(self.offset) + try: + tarinfo = self.tarinfo.fromtarfile(self) + self.members.append(tarinfo) + except EOFHeaderError: + self.fileobj.seek(self.offset) + break + except HeaderError as e: + raise ReadError(str(e)) from None + + if self.mode in ("a", "w", "x"): + self._loaded = True + + if self.pax_headers: + buf = self.tarinfo.create_pax_global_header(self.pax_headers.copy()) + self.fileobj.write(buf) + self.offset += len(buf) + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + #-------------------------------------------------------------------------- + # Below are the classmethods which act as alternate constructors to the + # TarFile class. The open() method is the only one that is needed for + # public use; it is the "super"-constructor and is able to select an + # adequate "sub"-constructor for a particular compression using the mapping + # from OPEN_METH. + # + # This concept allows one to subclass TarFile without losing the comfort of + # the super-constructor. A sub-constructor is registered and made available + # by adding it to the mapping in OPEN_METH. + + @classmethod + def open(cls, name=None, mode="r", fileobj=None, bufsize=RECORDSIZE, **kwargs): + r"""Open a tar archive for reading, writing or appending. Return + an appropriate TarFile class. + + mode: + 'r' or 'r:\*' open for reading with transparent compression + 'r:' open for reading exclusively uncompressed + 'r:gz' open for reading with gzip compression + 'r:bz2' open for reading with bzip2 compression + 'r:xz' open for reading with lzma compression + 'a' or 'a:' open for appending, creating the file if necessary + 'w' or 'w:' open for writing without compression + 'w:gz' open for writing with gzip compression + 'w:bz2' open for writing with bzip2 compression + 'w:xz' open for writing with lzma compression + + 'x' or 'x:' create a tarfile exclusively without compression, raise + an exception if the file is already created + 'x:gz' create a gzip compressed tarfile, raise an exception + if the file is already created + 'x:bz2' create a bzip2 compressed tarfile, raise an exception + if the file is already created + 'x:xz' create an lzma compressed tarfile, raise an exception + if the file is already created + + 'r|\*' open a stream of tar blocks with transparent compression + 'r|' open an uncompressed stream of tar blocks for reading + 'r|gz' open a gzip compressed stream of tar blocks + 'r|bz2' open a bzip2 compressed stream of tar blocks + 'r|xz' open an lzma compressed stream of tar blocks + 'w|' open an uncompressed stream for writing + 'w|gz' open a gzip compressed stream for writing + 'w|bz2' open a bzip2 compressed stream for writing + 'w|xz' open an lzma compressed stream for writing + """ + + if not name and not fileobj: + raise ValueError("nothing to open") + + if mode in ("r", "r:*"): + # Find out which *open() is appropriate for opening the file. + def not_compressed(comptype): + return cls.OPEN_METH[comptype] == 'taropen' + error_msgs = [] + for comptype in sorted(cls.OPEN_METH, key=not_compressed): + func = getattr(cls, cls.OPEN_METH[comptype]) + if fileobj is not None: + saved_pos = fileobj.tell() + try: + return func(name, "r", fileobj, **kwargs) + except (ReadError, CompressionError) as e: + error_msgs.append(f'- method {comptype}: {e!r}') + if fileobj is not None: + fileobj.seek(saved_pos) + continue + error_msgs_summary = '\n'.join(error_msgs) + raise ReadError(f"file could not be opened successfully:\n{error_msgs_summary}") + + elif ":" in mode: + filemode, comptype = mode.split(":", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + # Select the *open() function according to + # given compression. + if comptype in cls.OPEN_METH: + func = getattr(cls, cls.OPEN_METH[comptype]) + else: + raise CompressionError("unknown compression type %r" % comptype) + return func(name, filemode, fileobj, **kwargs) + + elif "|" in mode: + filemode, comptype = mode.split("|", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + if filemode not in ("r", "w"): + raise ValueError("mode must be 'r' or 'w'") + + compresslevel = kwargs.pop("compresslevel", 9) + stream = _Stream(name, filemode, comptype, fileobj, bufsize, + compresslevel) + try: + t = cls(name, filemode, stream, **kwargs) + except: + stream.close() + raise + t._extfileobj = False + return t + + elif mode in ("a", "w", "x"): + return cls.taropen(name, mode, fileobj, **kwargs) + + raise ValueError("undiscernible mode") + + @classmethod + def taropen(cls, name, mode="r", fileobj=None, **kwargs): + """Open uncompressed tar archive name for reading or writing. + """ + if mode not in ("r", "a", "w", "x"): + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + return cls(name, mode, fileobj, **kwargs) + + @classmethod + def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open gzip compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from gzip import GzipFile + except ImportError: + raise CompressionError("gzip module is not available") from None + + try: + fileobj = GzipFile(name, mode + "b", compresslevel, fileobj) + except OSError as e: + if fileobj is not None and mode == 'r': + raise ReadError("not a gzip file") from e + raise + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except OSError as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a gzip file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open bzip2 compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from bz2 import BZ2File + except ImportError: + raise CompressionError("bz2 module is not available") from None + + fileobj = BZ2File(fileobj or name, mode, compresslevel=compresslevel) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (OSError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a bzip2 file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs): + """Open lzma compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from lzma import LZMAFile, LZMAError + except ImportError: + raise CompressionError("lzma module is not available") from None + + fileobj = LZMAFile(fileobj or name, mode, preset=preset) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (LZMAError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not an lzma file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + # All *open() methods are registered here. + OPEN_METH = { + "tar": "taropen", # uncompressed tar + "gz": "gzopen", # gzip compressed tar + "bz2": "bz2open", # bzip2 compressed tar + "xz": "xzopen" # lzma compressed tar + } + + #-------------------------------------------------------------------------- + # The public methods which TarFile provides: + + def close(self): + """Close the TarFile. In write-mode, two finishing zero blocks are + appended to the archive. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode in ("a", "w", "x"): + self.fileobj.write(NUL * (BLOCKSIZE * 2)) + self.offset += (BLOCKSIZE * 2) + # fill up the end with zero-blocks + # (like option -b20 for tar does) + blocks, remainder = divmod(self.offset, RECORDSIZE) + if remainder > 0: + self.fileobj.write(NUL * (RECORDSIZE - remainder)) + finally: + if not self._extfileobj: + self.fileobj.close() + + def getmember(self, name): + """Return a TarInfo object for member ``name``. If ``name`` can not be + found in the archive, KeyError is raised. If a member occurs more + than once in the archive, its last occurrence is assumed to be the + most up-to-date version. + """ + tarinfo = self._getmember(name.rstrip('/')) + if tarinfo is None: + raise KeyError("filename %r not found" % name) + return tarinfo + + def getmembers(self): + """Return the members of the archive as a list of TarInfo objects. The + list has the same order as the members in the archive. + """ + self._check() + if not self._loaded: # if we want to obtain a list of + self._load() # all members, we first have to + # scan the whole archive. + return self.members + + def getnames(self): + """Return the members of the archive as a list of their names. It has + the same order as the list returned by getmembers(). + """ + return [tarinfo.name for tarinfo in self.getmembers()] + + def gettarinfo(self, name=None, arcname=None, fileobj=None): + """Create a TarInfo object from the result of os.stat or equivalent + on an existing file. The file is either named by ``name``, or + specified as a file object ``fileobj`` with a file descriptor. If + given, ``arcname`` specifies an alternative name for the file in the + archive, otherwise, the name is taken from the 'name' attribute of + 'fileobj', or the 'name' argument. The name should be a text + string. + """ + self._check("awx") + + # When fileobj is given, replace name by + # fileobj's real name. + if fileobj is not None: + name = fileobj.name + + # Building the name of the member in the archive. + # Backward slashes are converted to forward slashes, + # Absolute paths are turned to relative paths. + if arcname is None: + arcname = name + drv, arcname = os.path.splitdrive(arcname) + arcname = arcname.replace(os.sep, "/") + arcname = arcname.lstrip("/") + + # Now, fill the TarInfo object with + # information specific for the file. + tarinfo = self.tarinfo() + tarinfo.tarfile = self # Not needed + + # Use os.stat or os.lstat, depending on if symlinks shall be resolved. + if fileobj is None: + if not self.dereference: + statres = os.lstat(name) + else: + statres = os.stat(name) + else: + statres = os.fstat(fileobj.fileno()) + linkname = "" + + stmd = statres.st_mode + if stat.S_ISREG(stmd): + inode = (statres.st_ino, statres.st_dev) + if not self.dereference and statres.st_nlink > 1 and \ + inode in self.inodes and arcname != self.inodes[inode]: + # Is it a hardlink to an already + # archived file? + type = LNKTYPE + linkname = self.inodes[inode] + else: + # The inode is added only if its valid. + # For win32 it is always 0. + type = REGTYPE + if inode[0]: + self.inodes[inode] = arcname + elif stat.S_ISDIR(stmd): + type = DIRTYPE + elif stat.S_ISFIFO(stmd): + type = FIFOTYPE + elif stat.S_ISLNK(stmd): + type = SYMTYPE + linkname = os.readlink(name) + elif stat.S_ISCHR(stmd): + type = CHRTYPE + elif stat.S_ISBLK(stmd): + type = BLKTYPE + else: + return None + + # Fill the TarInfo object with all + # information we can get. + tarinfo.name = arcname + tarinfo.mode = stmd + tarinfo.uid = statres.st_uid + tarinfo.gid = statres.st_gid + if type == REGTYPE: + tarinfo.size = statres.st_size + else: + tarinfo.size = 0 + tarinfo.mtime = statres.st_mtime + tarinfo.type = type + tarinfo.linkname = linkname + if pwd: + try: + tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0] + except KeyError: + pass + if grp: + try: + tarinfo.gname = grp.getgrgid(tarinfo.gid)[0] + except KeyError: + pass + + if type in (CHRTYPE, BLKTYPE): + if hasattr(os, "major") and hasattr(os, "minor"): + tarinfo.devmajor = os.major(statres.st_rdev) + tarinfo.devminor = os.minor(statres.st_rdev) + return tarinfo + + def list(self, verbose=True, *, members=None): + """Print a table of contents to sys.stdout. If ``verbose`` is False, only + the names of the members are printed. If it is True, an `ls -l'-like + output is produced. ``members`` is optional and must be a subset of the + list returned by getmembers(). + """ + self._check() + + if members is None: + members = self + for tarinfo in members: + if verbose: + if tarinfo.mode is None: + _safe_print("??????????") + else: + _safe_print(stat.filemode(tarinfo.mode)) + _safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid, + tarinfo.gname or tarinfo.gid)) + if tarinfo.ischr() or tarinfo.isblk(): + _safe_print("%10s" % + ("%d,%d" % (tarinfo.devmajor, tarinfo.devminor))) + else: + _safe_print("%10d" % tarinfo.size) + if tarinfo.mtime is None: + _safe_print("????-??-?? ??:??:??") + else: + _safe_print("%d-%02d-%02d %02d:%02d:%02d" \ + % time.localtime(tarinfo.mtime)[:6]) + + _safe_print(tarinfo.name + ("/" if tarinfo.isdir() else "")) + + if verbose: + if tarinfo.issym(): + _safe_print("-> " + tarinfo.linkname) + if tarinfo.islnk(): + _safe_print("link to " + tarinfo.linkname) + print() + + def add(self, name, arcname=None, recursive=True, *, filter=None): + """Add the file ``name`` to the archive. ``name`` may be any type of file + (directory, fifo, symbolic link, etc.). If given, ``arcname`` + specifies an alternative name for the file in the archive. + Directories are added recursively by default. This can be avoided by + setting ``recursive`` to False. ``filter`` is a function + that expects a TarInfo object argument and returns the changed + TarInfo object, if it returns None the TarInfo object will be + excluded from the archive. + """ + self._check("awx") + + if arcname is None: + arcname = name + + # Skip if somebody tries to archive the archive... + if self.name is not None and os.path.abspath(name) == self.name: + self._dbg(2, "tarfile: Skipped %r" % name) + return + + self._dbg(1, name) + + # Create a TarInfo object from the file. + tarinfo = self.gettarinfo(name, arcname) + + if tarinfo is None: + self._dbg(1, "tarfile: Unsupported type %r" % name) + return + + # Change or exclude the TarInfo object. + if filter is not None: + tarinfo = filter(tarinfo) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % name) + return + + # Append the tar header and data to the archive. + if tarinfo.isreg(): + with bltn_open(name, "rb") as f: + self.addfile(tarinfo, f) + + elif tarinfo.isdir(): + self.addfile(tarinfo) + if recursive: + for f in sorted(os.listdir(name)): + self.add(os.path.join(name, f), os.path.join(arcname, f), + recursive, filter=filter) + + else: + self.addfile(tarinfo) + + def addfile(self, tarinfo, fileobj=None): + """Add the TarInfo object ``tarinfo`` to the archive. If ``fileobj`` is + given, it should be a binary file, and tarinfo.size bytes are read + from it and added to the archive. You can create TarInfo objects + directly, or by using gettarinfo(). + """ + self._check("awx") + + tarinfo = copy.copy(tarinfo) + + buf = tarinfo.tobuf(self.format, self.encoding, self.errors) + self.fileobj.write(buf) + self.offset += len(buf) + bufsize=self.copybufsize + # If there's data to follow, append it. + if fileobj is not None: + copyfileobj(fileobj, self.fileobj, tarinfo.size, bufsize=bufsize) + blocks, remainder = divmod(tarinfo.size, BLOCKSIZE) + if remainder > 0: + self.fileobj.write(NUL * (BLOCKSIZE - remainder)) + blocks += 1 + self.offset += blocks * BLOCKSIZE + + self.members.append(tarinfo) + + def _get_filter_function(self, filter): + if filter is None: + filter = self.extraction_filter + if filter is None: + warnings.warn( + 'Python 3.14 will, by default, filter extracted tar ' + + 'archives and reject files or modify their metadata. ' + + 'Use the filter argument to control this behavior.', + DeprecationWarning) + return fully_trusted_filter + if isinstance(filter, str): + raise TypeError( + 'String names are not supported for ' + + 'TarFile.extraction_filter. Use a function such as ' + + 'tarfile.data_filter directly.') + return filter + if callable(filter): + return filter + try: + return _NAMED_FILTERS[filter] + except KeyError: + raise ValueError(f"filter {filter!r} not found") from None + + def extractall(self, path=".", members=None, *, numeric_owner=False, + filter=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). If `numeric_owner` is True, only + the numbers for user/group names are used and not the names. + + The `filter` function will be called on each member just + before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + directories = [] + + filter_function = self._get_filter_function(filter) + if members is None: + members = self + + for member in members: + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is None: + continue + if tarinfo.isdir(): + # For directories, delay setting attributes until later, + # since permissions can interfere with extraction and + # extracting contents can reset mtime. + directories.append(tarinfo) + self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(), + numeric_owner=numeric_owner) + + # Reverse sort directories. + directories.sort(key=lambda a: a.name, reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath, numeric_owner=numeric_owner) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, + filter=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a TarInfo object. You can + specify a different directory using `path'. File attributes (owner, + mtime, mode) are set unless `set_attrs' is False. If `numeric_owner` + is True, only the numbers for user/group names are used and not + the names. + + The `filter` function will be called before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + filter_function = self._get_filter_function(filter) + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is not None: + self._extract_one(tarinfo, path, set_attrs, numeric_owner) + + def _get_extract_tarinfo(self, member, filter_function, path): + """Get filtered TarInfo (or None) from member, which might be a str""" + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + unfiltered = tarinfo + try: + tarinfo = filter_function(tarinfo, path) + except (OSError, FilterError) as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % unfiltered.name) + return None + # Prepare the link target for makelink(). + if tarinfo.islnk(): + tarinfo = copy.copy(tarinfo) + tarinfo._link_target = os.path.join(path, tarinfo.linkname) + return tarinfo + + def _extract_one(self, tarinfo, path, set_attrs, numeric_owner): + """Extract from filtered tarinfo to disk""" + self._check("r") + + try: + self._extract_member(tarinfo, os.path.join(path, tarinfo.name), + set_attrs=set_attrs, + numeric_owner=numeric_owner) + except OSError as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def _handle_nonfatal_error(self, e): + """Handle non-fatal error (ExtractError) according to errorlevel""" + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + def _handle_fatal_error(self, e): + """Handle "fatal" error according to self.errorlevel""" + if self.errorlevel > 0: + raise + elif isinstance(e, OSError): + if e.filename is None: + self._dbg(1, "tarfile: %s" % e.strerror) + else: + self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename)) + else: + self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e)) + + def extractfile(self, member): + """Extract a member from the archive as a file object. ``member`` may be + a filename or a TarInfo object. If ``member`` is a regular file or + a link, an io.BufferedReader object is returned. For all other + existing members, None is returned. If ``member`` does not appear + in the archive, KeyError is raised. + """ + self._check("r") + + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + if tarinfo.isreg() or tarinfo.type not in SUPPORTED_TYPES: + # Members with unknown types are treated as regular files. + return self.fileobject(self, tarinfo) + + elif tarinfo.islnk() or tarinfo.issym(): + if isinstance(self.fileobj, _Stream): + # A small but ugly workaround for the case that someone tries + # to extract a (sym)link as a file-object from a non-seekable + # stream of tar blocks. + raise StreamError("cannot extract (sym)link as file object") + else: + # A (sym)link's file object is its target's file object. + return self.extractfile(self._find_link_target(tarinfo)) + else: + # If there's no data associated with the member (directory, chrdev, + # blkdev, etc.), return None instead of a file object. + return None + + def _extract_member(self, tarinfo, targetpath, set_attrs=True, + numeric_owner=False): + """Extract the TarInfo object tarinfo to a physical + file called targetpath. + """ + # Fetch the TarInfo object for the given name + # and build the destination pathname, replacing + # forward slashes to platform specific separators. + targetpath = targetpath.rstrip("/") + targetpath = targetpath.replace("/", os.sep) + + # Create all upper directories. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + # Create directories that are not part of the archive with + # default permissions. + os.makedirs(upperdirs) + + if tarinfo.islnk() or tarinfo.issym(): + self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname)) + else: + self._dbg(1, tarinfo.name) + + if tarinfo.isreg(): + self.makefile(tarinfo, targetpath) + elif tarinfo.isdir(): + self.makedir(tarinfo, targetpath) + elif tarinfo.isfifo(): + self.makefifo(tarinfo, targetpath) + elif tarinfo.ischr() or tarinfo.isblk(): + self.makedev(tarinfo, targetpath) + elif tarinfo.islnk() or tarinfo.issym(): + self.makelink(tarinfo, targetpath) + elif tarinfo.type not in SUPPORTED_TYPES: + self.makeunknown(tarinfo, targetpath) + else: + self.makefile(tarinfo, targetpath) + + if set_attrs: + self.chown(tarinfo, targetpath, numeric_owner) + if not tarinfo.issym(): + self.chmod(tarinfo, targetpath) + self.utime(tarinfo, targetpath) + + #-------------------------------------------------------------------------- + # Below are the different file methods. They are called via + # _extract_member() when extract() is called. They can be replaced in a + # subclass to implement other functionality. + + def makedir(self, tarinfo, targetpath): + """Make a directory called targetpath. + """ + try: + if tarinfo.mode is None: + # Use the system's default mode + os.mkdir(targetpath) + else: + # Use a safe mode for the directory, the real mode is set + # later in _extract_member(). + os.mkdir(targetpath, 0o700) + except FileExistsError: + if not os.path.isdir(targetpath): + raise + + def makefile(self, tarinfo, targetpath): + """Make a file called targetpath. + """ + source = self.fileobj + source.seek(tarinfo.offset_data) + bufsize = self.copybufsize + with bltn_open(targetpath, "wb") as target: + if tarinfo.sparse is not None: + for offset, size in tarinfo.sparse: + target.seek(offset) + copyfileobj(source, target, size, ReadError, bufsize) + target.seek(tarinfo.size) + target.truncate() + else: + copyfileobj(source, target, tarinfo.size, ReadError, bufsize) + + def makeunknown(self, tarinfo, targetpath): + """Make a file from a TarInfo object with an unknown type + at targetpath. + """ + self.makefile(tarinfo, targetpath) + self._dbg(1, "tarfile: Unknown file type %r, " \ + "extracted as regular file." % tarinfo.type) + + def makefifo(self, tarinfo, targetpath): + """Make a fifo called targetpath. + """ + if hasattr(os, "mkfifo"): + os.mkfifo(targetpath) + else: + raise ExtractError("fifo not supported by system") + + def makedev(self, tarinfo, targetpath): + """Make a character or block device called targetpath. + """ + if not hasattr(os, "mknod") or not hasattr(os, "makedev"): + raise ExtractError("special devices not supported by system") + + mode = tarinfo.mode + if mode is None: + # Use mknod's default + mode = 0o600 + if tarinfo.isblk(): + mode |= stat.S_IFBLK + else: + mode |= stat.S_IFCHR + + os.mknod(targetpath, mode, + os.makedev(tarinfo.devmajor, tarinfo.devminor)) + + def makelink(self, tarinfo, targetpath): + """Make a (symbolic) link called targetpath. If it cannot be created + (platform limitation), we try to make a copy of the referenced file + instead of a link. + """ + try: + # For systems that support symbolic and hard links. + if tarinfo.issym(): + if os.path.lexists(targetpath): + # Avoid FileExistsError on following os.symlink. + os.unlink(targetpath) + os.symlink(tarinfo.linkname, targetpath) + else: + if os.path.exists(tarinfo._link_target): + os.link(tarinfo._link_target, targetpath) + else: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except symlink_exception: + try: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except KeyError: + raise ExtractError("unable to resolve link inside archive") from None + + def chown(self, tarinfo, targetpath, numeric_owner): + """Set owner of targetpath according to tarinfo. If numeric_owner + is True, use .gid/.uid instead of .gname/.uname. If numeric_owner + is False, fall back to .gid/.uid when the search based on name + fails. + """ + if hasattr(os, "geteuid") and os.geteuid() == 0: + # We have to be root to do so. + g = tarinfo.gid + u = tarinfo.uid + if not numeric_owner: + try: + if grp and tarinfo.gname: + g = grp.getgrnam(tarinfo.gname)[2] + except KeyError: + pass + try: + if pwd and tarinfo.uname: + u = pwd.getpwnam(tarinfo.uname)[2] + except KeyError: + pass + if g is None: + g = -1 + if u is None: + u = -1 + try: + if tarinfo.issym() and hasattr(os, "lchown"): + os.lchown(targetpath, u, g) + else: + os.chown(targetpath, u, g) + except OSError as e: + raise ExtractError("could not change owner") from e + + def chmod(self, tarinfo, targetpath): + """Set file permissions of targetpath according to tarinfo. + """ + if tarinfo.mode is None: + return + try: + os.chmod(targetpath, tarinfo.mode) + except OSError as e: + raise ExtractError("could not change mode") from e + + def utime(self, tarinfo, targetpath): + """Set modification time of targetpath according to tarinfo. + """ + mtime = tarinfo.mtime + if mtime is None: + return + if not hasattr(os, 'utime'): + return + try: + os.utime(targetpath, (mtime, mtime)) + except OSError as e: + raise ExtractError("could not change modification time") from e + + #-------------------------------------------------------------------------- + def next(self): + """Return the next member of the archive as a TarInfo object, when + TarFile is opened for reading. Return None if there is no more + available. + """ + self._check("ra") + if self.firstmember is not None: + m = self.firstmember + self.firstmember = None + return m + + # Advance the file pointer. + if self.offset != self.fileobj.tell(): + if self.offset == 0: + return None + self.fileobj.seek(self.offset - 1) + if not self.fileobj.read(1): + raise ReadError("unexpected end of data") + + # Read the next block. + tarinfo = None + while True: + try: + tarinfo = self.tarinfo.fromtarfile(self) + except EOFHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + except InvalidHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + elif self.offset == 0: + raise ReadError(str(e)) from None + except EmptyHeaderError: + if self.offset == 0: + raise ReadError("empty file") from None + except TruncatedHeaderError as e: + if self.offset == 0: + raise ReadError(str(e)) from None + except SubsequentHeaderError as e: + raise ReadError(str(e)) from None + except Exception as e: + try: + import zlib + if isinstance(e, zlib.error): + raise ReadError(f'zlib error: {e}') from None + else: + raise e + except ImportError: + raise e + break + + if tarinfo is not None: + self.members.append(tarinfo) + else: + self._loaded = True + + return tarinfo + + #-------------------------------------------------------------------------- + # Little helper methods: + + def _getmember(self, name, tarinfo=None, normalize=False): + """Find an archive member by name from bottom to top. + If tarinfo is given, it is used as the starting point. + """ + # Ensure that all members have been loaded. + members = self.getmembers() + + # Limit the member search list up to tarinfo. + skipping = False + if tarinfo is not None: + try: + index = members.index(tarinfo) + except ValueError: + # The given starting point might be a (modified) copy. + # We'll later skip members until we find an equivalent. + skipping = True + else: + # Happy fast path + members = members[:index] + + if normalize: + name = os.path.normpath(name) + + for member in reversed(members): + if skipping: + if tarinfo.offset == member.offset: + skipping = False + continue + if normalize: + member_name = os.path.normpath(member.name) + else: + member_name = member.name + + if name == member_name: + return member + + if skipping: + # Starting point was not found + raise ValueError(tarinfo) + + def _load(self): + """Read through the entire archive file and look for readable + members. + """ + while self.next() is not None: + pass + self._loaded = True + + def _check(self, mode=None): + """Check if TarFile is still open, and if the operation's mode + corresponds to TarFile's mode. + """ + if self.closed: + raise OSError("%s is closed" % self.__class__.__name__) + if mode is not None and self.mode not in mode: + raise OSError("bad operation for mode %r" % self.mode) + + def _find_link_target(self, tarinfo): + """Find the target member of a symlink or hardlink member in the + archive. + """ + if tarinfo.issym(): + # Always search the entire archive. + linkname = "/".join(filter(None, (os.path.dirname(tarinfo.name), tarinfo.linkname))) + limit = None + else: + # Search the archive before the link, because a hard link is + # just a reference to an already archived file. + linkname = tarinfo.linkname + limit = tarinfo + + member = self._getmember(linkname, tarinfo=limit, normalize=True) + if member is None: + raise KeyError("linkname %r not found" % linkname) + return member + + def __iter__(self): + """Provide an iterator object. + """ + if self._loaded: + yield from self.members + return + + # Yield items using TarFile's next() method. + # When all members have been read, set TarFile as _loaded. + index = 0 + # Fix for SF #1100429: Under rare circumstances it can + # happen that getmembers() is called during iteration, + # which will have already exhausted the next() method. + if self.firstmember is not None: + tarinfo = self.next() + index += 1 + yield tarinfo + + while True: + if index < len(self.members): + tarinfo = self.members[index] + elif not self._loaded: + tarinfo = self.next() + if not tarinfo: + self._loaded = True + return + else: + return + index += 1 + yield tarinfo + + def _dbg(self, level, msg): + """Write debugging output to sys.stderr. + """ + if level <= self.debug: + print(msg, file=sys.stderr) + + def __enter__(self): + self._check() + return self + + def __exit__(self, type, value, traceback): + if type is None: + self.close() + else: + # An exception occurred. We must not call close() because + # it would try to write end-of-archive blocks and padding. + if not self._extfileobj: + self.fileobj.close() + self.closed = True + +#-------------------- +# exported functions +#-------------------- + +def is_tarfile(name): + """Return True if name points to a tar archive that we + are able to handle, else return False. + + 'name' should be a string, file, or file-like object. + """ + try: + if hasattr(name, "read"): + pos = name.tell() + t = open(fileobj=name) + name.seek(pos) + else: + t = open(name) + t.close() + return True + except TarError: + return False + +open = TarFile.open + + +def main(): + import argparse + + description = 'A simple command-line interface for tarfile module.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='Verbose output') + parser.add_argument('--filter', metavar='', + choices=_NAMED_FILTERS, + help='Filter for extraction') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-l', '--list', metavar='', + help='Show listing of a tarfile') + group.add_argument('-e', '--extract', nargs='+', + metavar=('', ''), + help='Extract tarfile into target dir') + group.add_argument('-c', '--create', nargs='+', + metavar=('', ''), + help='Create tarfile from sources') + group.add_argument('-t', '--test', metavar='', + help='Test if a tarfile is valid') + + args = parser.parse_args() + + if args.filter and args.extract is None: + parser.exit(1, '--filter is only valid for extraction\n') + + if args.test is not None: + src = args.test + if is_tarfile(src): + with open(src, 'r') as tar: + tar.getmembers() + print(tar.getmembers(), file=sys.stderr) + if args.verbose: + print('{!r} is a tar archive.'.format(src)) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.list is not None: + src = args.list + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.list(verbose=args.verbose) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.extract is not None: + if len(args.extract) == 1: + src = args.extract[0] + curdir = os.curdir + elif len(args.extract) == 2: + src, curdir = args.extract + else: + parser.exit(1, parser.format_help()) + + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.extractall(path=curdir, filter=args.filter) + if args.verbose: + if curdir == '.': + msg = '{!r} file is extracted.'.format(src) + else: + msg = ('{!r} file is extracted ' + 'into {!r} directory.').format(src, curdir) + print(msg) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.create is not None: + tar_name = args.create.pop(0) + _, ext = os.path.splitext(tar_name) + compressions = { + # gz + '.gz': 'gz', + '.tgz': 'gz', + # xz + '.xz': 'xz', + '.txz': 'xz', + # bz2 + '.bz2': 'bz2', + '.tbz': 'bz2', + '.tbz2': 'bz2', + '.tb2': 'bz2', + } + tar_mode = 'w:' + compressions[ext] if ext in compressions else 'w' + tar_files = args.create + + with TarFile.open(tar_name, tar_mode) as tf: + for file_name in tar_files: + tf.add(file_name) + + if args.verbose: + print('{!r} file created.'.format(tar_name)) + +if __name__ == '__main__': + main() diff --git a/pkg_resources/_vendor/importlib_resources-5.10.2.dist-info/RECORD b/pkg_resources/_vendor/importlib_resources-5.10.2.dist-info/RECORD index 7d19852d4a9..ba764991ee2 100644 --- a/pkg_resources/_vendor/importlib_resources-5.10.2.dist-info/RECORD +++ b/pkg_resources/_vendor/importlib_resources-5.10.2.dist-info/RECORD @@ -6,15 +6,15 @@ importlib_resources-5.10.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQe importlib_resources-5.10.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 importlib_resources-5.10.2.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 importlib_resources/__init__.py,sha256=evPm12kLgYqTm-pbzm60bOuumumT8IpBNWFp0uMyrzE,506 -importlib_resources/__pycache__/__init__.cpython-311.pyc,, -importlib_resources/__pycache__/_adapters.cpython-311.pyc,, -importlib_resources/__pycache__/_common.cpython-311.pyc,, -importlib_resources/__pycache__/_compat.cpython-311.pyc,, -importlib_resources/__pycache__/_itertools.cpython-311.pyc,, -importlib_resources/__pycache__/_legacy.cpython-311.pyc,, -importlib_resources/__pycache__/abc.cpython-311.pyc,, -importlib_resources/__pycache__/readers.cpython-311.pyc,, -importlib_resources/__pycache__/simple.cpython-311.pyc,, +importlib_resources/__pycache__/__init__.cpython-312.pyc,, +importlib_resources/__pycache__/_adapters.cpython-312.pyc,, +importlib_resources/__pycache__/_common.cpython-312.pyc,, +importlib_resources/__pycache__/_compat.cpython-312.pyc,, +importlib_resources/__pycache__/_itertools.cpython-312.pyc,, +importlib_resources/__pycache__/_legacy.cpython-312.pyc,, +importlib_resources/__pycache__/abc.cpython-312.pyc,, +importlib_resources/__pycache__/readers.cpython-312.pyc,, +importlib_resources/__pycache__/simple.cpython-312.pyc,, importlib_resources/_adapters.py,sha256=o51tP2hpVtohP33gSYyAkGNpLfYDBqxxYsadyiRZi1E,4504 importlib_resources/_common.py,sha256=jSC4xfLdcMNbtbWHtpzbFkNa0W7kvf__nsYn14C_AEU,5457 importlib_resources/_compat.py,sha256=dSadF6WPt8MwOqSm_NIOQPhw4x0iaMYTWxi-XS93p7M,2923 @@ -25,36 +25,36 @@ importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU, importlib_resources/readers.py,sha256=PZsi5qacr2Qn3KHw4qw3Gm1MzrBblPHoTdjqjH7EKWw,3581 importlib_resources/simple.py,sha256=0__2TQBTQoqkajYmNPt1HxERcReAT6boVKJA328pr04,2576 importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/__pycache__/__init__.cpython-311.pyc,, -importlib_resources/tests/__pycache__/_compat.cpython-311.pyc,, -importlib_resources/tests/__pycache__/_path.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_contents.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_files.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_open.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_path.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_read.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_reader.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_resource.cpython-311.pyc,, -importlib_resources/tests/__pycache__/update-zips.cpython-311.pyc,, -importlib_resources/tests/__pycache__/util.cpython-311.pyc,, +importlib_resources/tests/__pycache__/__init__.cpython-312.pyc,, +importlib_resources/tests/__pycache__/_compat.cpython-312.pyc,, +importlib_resources/tests/__pycache__/_path.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_contents.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_files.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_open.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_path.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_read.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_reader.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_resource.cpython-312.pyc,, +importlib_resources/tests/__pycache__/update-zips.cpython-312.pyc,, +importlib_resources/tests/__pycache__/util.cpython-312.pyc,, importlib_resources/tests/_compat.py,sha256=YTSB0U1R9oADnh6GrQcOCgojxcF_N6H1LklymEWf9SQ,708 importlib_resources/tests/_path.py,sha256=yZyWsQzJZQ1Z8ARAxWkjAdaVVsjlzyqxO0qjBUofJ8M,1039 importlib_resources/tests/data01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data01/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data01/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/data01/subdirectory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data01/subdirectory/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/data01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 importlib_resources/tests/data01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 importlib_resources/tests/data02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/one/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/one/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/one/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/one/resource1.txt,sha256=10flKac7c-XXFzJ3t-AB5MJjlBy__dSZvPE_dOm2q6U,13 importlib_resources/tests/data02/two/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/two/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/two/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/two/resource2.txt,sha256=lt2jbN3TMn9QiFKM832X39bU_62UptDdUkoYzkvEbl0,13 importlib_resources/tests/namespacedata01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/namespacedata01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 @@ -70,8 +70,8 @@ importlib_resources/tests/test_resource.py,sha256=EMoarxTEHcrq8R41LQDsndIG8Idtm4 importlib_resources/tests/update-zips.py,sha256=x-SrO5v87iLLUMXyefxDwAd3imAs_slI94sLWvJ6N40,1417 importlib_resources/tests/util.py,sha256=ARAlxZ47wC-lgR7PGlmgBoi4HnhzcykD5Is2-TAwY0I,4873 importlib_resources/tests/zipdata01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/zipdata01/ziptestdata.zip,sha256=z5Of4dsv3T0t-46B0MsVhxlhsPGMz28aUhJDWpj3_oY,876 importlib_resources/tests/zipdata02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/zipdata02/ziptestdata.zip,sha256=ydI-_j-xgQ7tDxqBp9cjOqXBGxUp6ZBbwVJu6Xj-nrY,698 diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/METADATA b/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/METADATA deleted file mode 100644 index 281137a035e..00000000000 --- a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/METADATA +++ /dev/null @@ -1,68 +0,0 @@ -Metadata-Version: 2.1 -Name: jaraco.context -Version: 4.3.0 -Summary: Context managers by jaraco -Home-page: https://github.com/jaraco/jaraco.context -Author: Jason R. Coombs -Author-email: jaraco@jaraco.com -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.7 -License-File: LICENSE -Provides-Extra: docs -Requires-Dist: sphinx (>=3.5) ; extra == 'docs' -Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' -Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' -Requires-Dist: furo ; extra == 'docs' -Requires-Dist: sphinx-lint ; extra == 'docs' -Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' -Provides-Extra: testing -Requires-Dist: pytest (>=6) ; extra == 'testing' -Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' -Requires-Dist: flake8 (<5) ; extra == 'testing' -Requires-Dist: pytest-cov ; extra == 'testing' -Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' -Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' - -.. image:: https://img.shields.io/pypi/v/jaraco.context.svg - :target: https://pypi.org/project/jaraco.context - -.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg - -.. image:: https://github.com/jaraco/jaraco.context/workflows/tests/badge.svg - :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 - :alt: tests - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - -.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest - :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest - -.. image:: https://img.shields.io/badge/skeleton-2023-informational - :target: https://blog.jaraco.com/skeleton - -.. image:: https://tidelift.com/badges/package/pypi/jaraco.context - :target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme - -For Enterprise -============== - -Available as part of the Tidelift Subscription. - -This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. - -`Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/RECORD deleted file mode 100644 index 03122364a21..00000000000 --- a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/RECORD +++ /dev/null @@ -1,8 +0,0 @@ -jaraco.context-4.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -jaraco.context-4.3.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 -jaraco.context-4.3.0.dist-info/METADATA,sha256=GqMykAm33E7Tt_t_MHc5O7GJN62Qwp6MEHX9WD-LPow,2958 -jaraco.context-4.3.0.dist-info/RECORD,, -jaraco.context-4.3.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 -jaraco.context-4.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 -jaraco/__pycache__/context.cpython-311.pyc,, -jaraco/context.py,sha256=vlyDzb_PvZ9H7R9bbTr_CMRnveW5Dc56eC7eyd_GfoA,7460 diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/INSTALLER b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER similarity index 100% rename from pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/INSTALLER rename to pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/LICENSE similarity index 97% rename from pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE rename to pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/LICENSE index 353924be0e5..1bb5a44356f 100644 --- a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE +++ b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/METADATA b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/METADATA new file mode 100644 index 00000000000..a36f7c5e82d --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/METADATA @@ -0,0 +1,75 @@ +Metadata-Version: 2.1 +Name: jaraco.context +Version: 5.3.0 +Summary: Useful decorators and context managers +Home-page: https://github.com/jaraco/jaraco.context +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.8 +License-File: LICENSE +Requires-Dist: backports.tarfile ; python_version < "3.12" +Provides-Extra: docs +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest !=8.1.1,>=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-mypy ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' +Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'testing' +Requires-Dist: portend ; extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.context.svg + :target: https://pypi.org/project/jaraco.context + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg + +.. image:: https://github.com/jaraco/jaraco.context/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest + :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/jaraco.context + :target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme + + +Highlights +========== + +See the docs linked from the badge above for the full details, but here are some features that may be of interest. + +- ``ExceptionTrap`` provides a general-purpose wrapper for trapping exceptions and then acting on the outcome. Includes ``passes`` and ``raises`` decorators to replace the result of a wrapped function by a boolean indicating the outcome of the exception trap. See `this keyring commit `_ for an example of it in production. +- ``suppress`` simply enables ``contextlib.suppress`` as a decorator. +- ``on_interrupt`` is a decorator used by CLI entry points to affect the handling of a ``KeyboardInterrupt``. Inspired by `Lucretiel/autocommand#18 `_. +- ``pushd`` is similar to pytest's ``monkeypatch.chdir`` or path's `default context `_, changes the current working directory for the duration of the context. +- ``tarball`` will download a tarball, extract it, change directory, yield, then clean up after. Convenient when working with web assets. +- ``null`` is there for those times when one code branch needs a context and the other doesn't; this null context provides symmetry across those branches. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/RECORD new file mode 100644 index 00000000000..09d191f214a --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.context-5.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.context-5.3.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +jaraco.context-5.3.0.dist-info/METADATA,sha256=xDtguJej0tN9iEXCUvxEJh2a7xceIRVBEakBLSr__tY,4020 +jaraco.context-5.3.0.dist-info/RECORD,, +jaraco.context-5.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +jaraco.context-5.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/context.cpython-312.pyc,, +jaraco/context.py,sha256=REoLIxDkO5MfEYowt_WoupNCRoxBS5v7YX2PbW8lIcs,9552 diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/WHEEL b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/WHEEL similarity index 65% rename from setuptools/_vendor/jaraco.context-4.3.0.dist-info/WHEEL rename to pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/WHEEL index 57e3d840d59..bab98d67588 100644 --- a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/WHEEL +++ b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: bdist_wheel (0.38.4) +Generator: bdist_wheel (0.43.0) Root-Is-Purelib: true Tag: py3-none-any diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/top_level.txt b/pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt similarity index 100% rename from pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/top_level.txt rename to pkg_resources/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/RECORD deleted file mode 100644 index 70a3521307a..00000000000 --- a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/RECORD +++ /dev/null @@ -1,8 +0,0 @@ -jaraco.functools-3.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -jaraco.functools-3.6.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 -jaraco.functools-3.6.0.dist-info/METADATA,sha256=ImGoa1WEbhsibIb288yWqkDAvqLwlPzayjravRvW_Bs,3136 -jaraco.functools-3.6.0.dist-info/RECORD,, -jaraco.functools-3.6.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 -jaraco.functools-3.6.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 -jaraco/__pycache__/functools.cpython-311.pyc,, -jaraco/functools.py,sha256=GhSJGMVMcb0U4-axXaY_au30hT-ceW-HM1EbV1_9NzI,15035 diff --git a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/INSTALLER b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER similarity index 100% rename from pkg_resources/_vendor/more_itertools-9.1.0.dist-info/INSTALLER rename to pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE similarity index 97% rename from setuptools/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE rename to pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE index 353924be0e5..1bb5a44356f 100644 --- a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/LICENSE +++ b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/METADATA b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/METADATA similarity index 69% rename from pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/METADATA rename to pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/METADATA index 23c6f5ef2b8..581b3083784 100644 --- a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/METADATA +++ b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jaraco.functools -Version: 3.6.0 +Version: 4.0.0 Summary: Functools like those found in stdlib Home-page: https://github.com/jaraco/jaraco.functools Author: Jason R. Coombs @@ -10,26 +10,26 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.7 +Requires-Python: >=3.8 License-File: LICENSE Requires-Dist: more-itertools Provides-Extra: docs -Requires-Dist: sphinx (>=3.5) ; extra == 'docs' -Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' -Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: sphinx <7.2.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' Requires-Dist: furo ; extra == 'docs' Requires-Dist: sphinx-lint ; extra == 'docs' -Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' +Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs' Provides-Extra: testing -Requires-Dist: pytest (>=6) ; extra == 'testing' -Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' -Requires-Dist: flake8 (<5) ; extra == 'testing' +Requires-Dist: pytest >=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' Requires-Dist: pytest-cov ; extra == 'testing' -Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' +Requires-Dist: pytest-ruff ; extra == 'testing' Requires-Dist: jaraco.classes ; extra == 'testing' -Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' +Requires-Dist: pytest-black >=0.3.7 ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy >=0.9.1 ; (platform_python_implementation != "PyPy") and extra == 'testing' .. image:: https://img.shields.io/pypi/v/jaraco.functools.svg :target: https://pypi.org/project/jaraco.functools @@ -40,6 +40,10 @@ Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' :target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22 :alt: tests +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black @@ -63,10 +67,3 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/RECORD new file mode 100644 index 00000000000..783aa7d2b9a --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/RECORD @@ -0,0 +1,10 @@ +jaraco.functools-4.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.functools-4.0.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +jaraco.functools-4.0.0.dist-info/METADATA,sha256=nVOe_vWvaN2iWJ2aBVkhKvmvH-gFksNCXHwCNvcj65I,3078 +jaraco.functools-4.0.0.dist-info/RECORD,, +jaraco.functools-4.0.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +jaraco.functools-4.0.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/functools/__init__.py,sha256=hEAJaS2uSZRuF_JY4CxCHIYh79ZpxaPp9OiHyr9EJ1w,16642 +jaraco/functools/__init__.pyi,sha256=N4lLbdhMtrmwiK3UuMGhYsiOLLZx69CUNOdmFPSVh6Q,3982 +jaraco/functools/__pycache__/__init__.cpython-312.pyc,, +jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL similarity index 65% rename from setuptools/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL rename to pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL index 57e3d840d59..ba48cbcf927 100644 --- a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/WHEEL +++ b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: bdist_wheel (0.38.4) +Generator: bdist_wheel (0.41.3) Root-Is-Purelib: true Tag: py3-none-any diff --git a/pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/top_level.txt b/pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/top_level.txt similarity index 100% rename from pkg_resources/_vendor/jaraco.functools-3.6.0.dist-info/top_level.txt rename to pkg_resources/_vendor/jaraco.functools-4.0.0.dist-info/top_level.txt diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD index dd471b07082..c698101cb4f 100644 --- a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD @@ -7,4 +7,4 @@ jaraco.text-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FG jaraco.text-3.7.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335 jaraco/text/__init__.py,sha256=I56MW2ZFwPrYXIxzqxMBe2A1t-T4uZBgEgAKe9-JoqM,15538 -jaraco/text/__pycache__/__init__.cpython-311.pyc,, +jaraco/text/__pycache__/__init__.cpython-312.pyc,, diff --git a/pkg_resources/_vendor/jaraco/context.py b/pkg_resources/_vendor/jaraco/context.py index b0d1ef37cbc..61b27135df1 100644 --- a/pkg_resources/_vendor/jaraco/context.py +++ b/pkg_resources/_vendor/jaraco/context.py @@ -1,15 +1,26 @@ -import os -import subprocess +from __future__ import annotations + import contextlib import functools -import tempfile -import shutil import operator +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.request import warnings +from typing import Iterator + + +if sys.version_info < (3, 12): + from backports import tarfile +else: + import tarfile @contextlib.contextmanager -def pushd(dir): +def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: """ >>> tmp_path = getfixture('tmp_path') >>> with pushd(tmp_path): @@ -26,33 +37,88 @@ def pushd(dir): @contextlib.contextmanager -def tarball_context(url, target_dir=None, runner=None, pushd=pushd): +def tarball( + url, target_dir: str | os.PathLike | None = None +) -> Iterator[str | os.PathLike]: """ - Get a tarball, extract it, change to that directory, yield, then - clean up. - `runner` is the function to invoke commands. - `pushd` is a context manager for changing the directory. + Get a tarball, extract it, yield, then clean up. + + >>> import urllib.request + >>> url = getfixture('tarfile_served') + >>> target = getfixture('tmp_path') / 'out' + >>> tb = tarball(url, target_dir=target) + >>> import pathlib + >>> with tb as extracted: + ... contents = pathlib.Path(extracted, 'contents.txt').read_text(encoding='utf-8') + >>> assert not os.path.exists(extracted) """ if target_dir is None: target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') - if runner is None: - runner = functools.partial(subprocess.check_call, shell=True) - else: - warnings.warn("runner parameter is deprecated", DeprecationWarning) # In the tar command, use --strip-components=1 to strip the first path and # then # use -C to cause the files to be extracted to {target_dir}. This ensures # that we always know where the files were extracted. - runner('mkdir {target_dir}'.format(**vars())) + os.mkdir(target_dir) try: - getter = 'wget {url} -O -' - extract = 'tar x{compression} --strip-components=1 -C {target_dir}' - cmd = ' | '.join((getter, extract)) - runner(cmd.format(compression=infer_compression(url), **vars())) - with pushd(target_dir): - yield target_dir + req = urllib.request.urlopen(url) + with tarfile.open(fileobj=req, mode='r|*') as tf: + tf.extractall(path=target_dir, filter=strip_first_component) + yield target_dir finally: - runner('rm -Rf {target_dir}'.format(**vars())) + shutil.rmtree(target_dir) + + +def strip_first_component( + member: tarfile.TarInfo, + path, +) -> tarfile.TarInfo: + _, member.name = member.name.split('/', 1) + return member + + +def _compose(*cmgrs): + """ + Compose any number of dependent context managers into a single one. + + The last, innermost context manager may take arbitrary arguments, but + each successive context manager should accept the result from the + previous as a single parameter. + + Like :func:`jaraco.functools.compose`, behavior works from right to + left, so the context manager should be indicated from outermost to + innermost. + + Example, to create a context manager to change to a temporary + directory: + + >>> temp_dir_as_cwd = _compose(pushd, temp_dir) + >>> with temp_dir_as_cwd() as dir: + ... assert os.path.samefile(os.getcwd(), dir) + """ + + def compose_two(inner, outer): + def composed(*args, **kwargs): + with inner(*args, **kwargs) as saved, outer(saved) as res: + yield res + + return contextlib.contextmanager(composed) + + return functools.reduce(compose_two, reversed(cmgrs)) + + +tarball_cwd = _compose(pushd, tarball) + + +@contextlib.contextmanager +def tarball_context(*args, **kwargs): + warnings.warn( + "tarball_context is deprecated. Use tarball or tarball_cwd instead.", + DeprecationWarning, + stacklevel=2, + ) + pushd_ctx = kwargs.pop('pushd', pushd) + with tarball(*args, **kwargs) as tball, pushd_ctx(tball) as dir: + yield dir def infer_compression(url): @@ -68,6 +134,11 @@ def infer_compression(url): >>> infer_compression('file.xz') 'J' """ + warnings.warn( + "infer_compression is deprecated with no replacement", + DeprecationWarning, + stacklevel=2, + ) # cheat and just assume it's the last two characters compression_indicator = url[-2:] mapping = dict(gz='z', bz='j', xz='J') @@ -84,7 +155,7 @@ def temp_dir(remover=shutil.rmtree): >>> import pathlib >>> with temp_dir() as the_dir: ... assert os.path.isdir(the_dir) - ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents') + ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents', encoding='utf-8') >>> assert not os.path.exists(the_dir) """ temp_dir = tempfile.mkdtemp() @@ -113,15 +184,23 @@ def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): yield repo_dir -@contextlib.contextmanager def null(): """ A null context suitable to stand in for a meaningful context. >>> with null() as value: ... assert value is None + + This context is most useful when dealing with two or more code + branches but only some need a context. Wrap the others in a null + context to provide symmetry across all options. """ - yield + warnings.warn( + "null is deprecated. Use contextlib.nullcontext", + DeprecationWarning, + stacklevel=2, + ) + return contextlib.nullcontext() class ExceptionTrap: @@ -267,13 +346,7 @@ class on_interrupt(contextlib.ContextDecorator): ... on_interrupt('ignore')(do_interrupt)() """ - def __init__( - self, - action='error', - # py3.7 compat - # /, - code=1, - ): + def __init__(self, action='error', /, code=1): self.action = action self.code = code diff --git a/pkg_resources/_vendor/jaraco/functools.py b/pkg_resources/_vendor/jaraco/functools/__init__.py similarity index 79% rename from pkg_resources/_vendor/jaraco/functools.py rename to pkg_resources/_vendor/jaraco/functools/__init__.py index 67aeadc353c..f523099c723 100644 --- a/pkg_resources/_vendor/jaraco/functools.py +++ b/pkg_resources/_vendor/jaraco/functools/__init__.py @@ -1,18 +1,14 @@ +import collections.abc import functools -import time import inspect -import collections -import types import itertools +import operator +import time +import types import warnings import pkg_resources.extern.more_itertools -from typing import Callable, TypeVar - - -CallableT = TypeVar("CallableT", bound=Callable[..., object]) - def compose(*funcs): """ @@ -38,24 +34,6 @@ def compose_two(f1, f2): return functools.reduce(compose_two, funcs) -def method_caller(method_name, *args, **kwargs): - """ - Return a function that will call a named method on the - target object with optional positional and keyword - arguments. - - >>> lower = method_caller('lower') - >>> lower('MyString') - 'mystring' - """ - - def call_method(target): - func = getattr(target, method_name) - return func(*args, **kwargs) - - return call_method - - def once(func): """ Decorate func so it's only ever called the first time. @@ -98,12 +76,7 @@ def wrapper(*args, **kwargs): return wrapper -def method_cache( - method: CallableT, - cache_wrapper: Callable[ - [CallableT], CallableT - ] = functools.lru_cache(), # type: ignore[assignment] -) -> CallableT: +def method_cache(method, cache_wrapper=functools.lru_cache()): """ Wrap lru_cache to support storing the cache data in the object instances. @@ -171,21 +144,17 @@ def method_cache( for another implementation and additional justification. """ - def wrapper(self: object, *args: object, **kwargs: object) -> object: + def wrapper(self, *args, **kwargs): # it's the first call, replace the method with a cached, bound method - bound_method: CallableT = types.MethodType( # type: ignore[assignment] - method, self - ) + bound_method = types.MethodType(method, self) cached_method = cache_wrapper(bound_method) setattr(self, method.__name__, cached_method) return cached_method(*args, **kwargs) # Support cache clear even before cache has been created. - wrapper.cache_clear = lambda: None # type: ignore[attr-defined] + wrapper.cache_clear = lambda: None - return ( # type: ignore[return-value] - _special_method_cache(method, cache_wrapper) or wrapper - ) + return _special_method_cache(method, cache_wrapper) or wrapper def _special_method_cache(method, cache_wrapper): @@ -201,12 +170,13 @@ def _special_method_cache(method, cache_wrapper): """ name = method.__name__ special_names = '__getattr__', '__getitem__' + if name not in special_names: - return + return None wrapper_name = '__cached' + name - def proxy(self, *args, **kwargs): + def proxy(self, /, *args, **kwargs): if wrapper_name not in vars(self): bound = types.MethodType(method, self) cache = cache_wrapper(bound) @@ -243,7 +213,7 @@ def result_invoke(action): r""" Decorate a function with an action function that is invoked on the results returned from the decorated - function (for its side-effect), then return the original + function (for its side effect), then return the original result. >>> @result_invoke(print) @@ -267,7 +237,7 @@ def wrapper(*args, **kwargs): return wrap -def invoke(f, *args, **kwargs): +def invoke(f, /, *args, **kwargs): """ Call a function for its side effect after initialization. @@ -302,25 +272,15 @@ def invoke(f, *args, **kwargs): Use functools.partial to pass parameters to the initial call >>> @functools.partial(invoke, name='bingo') - ... def func(name): print("called with", name) + ... def func(name): print('called with', name) called with bingo """ f(*args, **kwargs) return f -def call_aside(*args, **kwargs): - """ - Deprecated name for invoke. - """ - warnings.warn("call_aside is deprecated, use invoke", DeprecationWarning) - return invoke(*args, **kwargs) - - class Throttler: - """ - Rate-limit a function (or other callable) - """ + """Rate-limit a function (or other callable).""" def __init__(self, func, max_rate=float('Inf')): if isinstance(func, Throttler): @@ -337,20 +297,20 @@ def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) def _wait(self): - "ensure at least 1/max_rate seconds from last call" + """Ensure at least 1/max_rate seconds from last call.""" elapsed = time.time() - self.last_called must_wait = 1 / self.max_rate - elapsed time.sleep(max(0, must_wait)) self.last_called = time.time() - def __get__(self, obj, type=None): + def __get__(self, obj, owner=None): return first_invoke(self._wait, functools.partial(self.func, obj)) def first_invoke(func1, func2): """ Return a function that when invoked will invoke func1 without - any parameters (for its side-effect) and then invoke func2 + any parameters (for its side effect) and then invoke func2 with whatever parameters were passed, returning its result. """ @@ -361,6 +321,17 @@ def wrapper(*args, **kwargs): return wrapper +method_caller = first_invoke( + lambda: warnings.warn( + '`jaraco.functools.method_caller` is deprecated, ' + 'use `operator.methodcaller` instead', + DeprecationWarning, + stacklevel=3, + ), + operator.methodcaller, +) + + def retry_call(func, cleanup=lambda: None, retries=0, trap=()): """ Given a callable func, trap the indicated exceptions @@ -369,7 +340,7 @@ def retry_call(func, cleanup=lambda: None, retries=0, trap=()): to propagate. """ attempts = itertools.count() if retries == float('inf') else range(retries) - for attempt in attempts: + for _ in attempts: try: return func() except trap: @@ -406,7 +377,7 @@ def wrapper(*f_args, **f_kwargs): def print_yielded(func): """ - Convert a generator into a function that prints all yielded elements + Convert a generator into a function that prints all yielded elements. >>> @print_yielded ... def x(): @@ -422,7 +393,7 @@ def print_yielded(func): def pass_none(func): """ - Wrap func so it's not called if its first param is None + Wrap func so it's not called if its first param is None. >>> print_text = pass_none(print) >>> print_text('text') @@ -431,9 +402,10 @@ def pass_none(func): """ @functools.wraps(func) - def wrapper(param, *args, **kwargs): + def wrapper(param, /, *args, **kwargs): if param is not None: return func(param, *args, **kwargs) + return None return wrapper @@ -507,7 +479,7 @@ def save_method_args(method): args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') @functools.wraps(method) - def wrapper(self, *args, **kwargs): + def wrapper(self, /, *args, **kwargs): attr_name = '_saved_' + method.__name__ attr = args_and_kwargs(args, kwargs) setattr(self, attr_name, attr) @@ -554,3 +526,108 @@ def wrapper(*args, **kwargs): return wrapper return decorate + + +def identity(x): + """ + Return the argument. + + >>> o = object() + >>> identity(o) is o + True + """ + return x + + +def bypass_when(check, *, _op=identity): + """ + Decorate a function to return its parameter when ``check``. + + >>> bypassed = [] # False + + >>> @bypass_when(bypassed) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> bypassed[:] = [object()] # True + >>> double(2) + 2 + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(param, /): + return param if _op(check) else func(param) + + return wrapper + + return decorate + + +def bypass_unless(check): + """ + Decorate a function to return its parameter unless ``check``. + + >>> enabled = [object()] # True + + >>> @bypass_unless(enabled) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> del enabled[:] # False + >>> double(2) + 2 + """ + return bypass_when(check, _op=operator.not_) + + +@functools.singledispatch +def _splat_inner(args, func): + """Splat args to func.""" + return func(*args) + + +@_splat_inner.register +def _(args: collections.abc.Mapping, func): + """Splat kargs to func as kwargs.""" + return func(**args) + + +def splat(func): + """ + Wrap func to expect its parameters to be passed positionally in a tuple. + + Has a similar effect to that of ``itertools.starmap`` over + simple ``map``. + + >>> pairs = [(-1, 1), (0, 2)] + >>> pkg_resources.extern.more_itertools.consume(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> pkg_resources.extern.more_itertools.consume(map(splat(print), pairs)) + -1 1 + 0 2 + + The approach generalizes to other iterators that don't have a "star" + equivalent, such as a "starfilter". + + >>> list(filter(splat(operator.add), pairs)) + [(0, 2)] + + Splat also accepts a mapping argument. + + >>> def is_nice(msg, code): + ... return "smile" in msg or code == 0 + >>> msgs = [ + ... dict(msg='smile!', code=20), + ... dict(msg='error :(', code=1), + ... dict(msg='unknown', code=0), + ... ] + >>> for msg in filter(splat(is_nice), msgs): + ... print(msg) + {'msg': 'smile!', 'code': 20} + {'msg': 'unknown', 'code': 0} + """ + return functools.wraps(func)(functools.partial(_splat_inner, func=func)) diff --git a/pkg_resources/_vendor/jaraco/functools/__init__.pyi b/pkg_resources/_vendor/jaraco/functools/__init__.pyi new file mode 100644 index 00000000000..c2b9ab1757e --- /dev/null +++ b/pkg_resources/_vendor/jaraco/functools/__init__.pyi @@ -0,0 +1,128 @@ +from collections.abc import Callable, Hashable, Iterator +from functools import partial +from operator import methodcaller +import sys +from typing import ( + Any, + Generic, + Protocol, + TypeVar, + overload, +) + +if sys.version_info >= (3, 10): + from typing import Concatenate, ParamSpec +else: + from typing_extensions import Concatenate, ParamSpec + +_P = ParamSpec('_P') +_R = TypeVar('_R') +_T = TypeVar('_T') +_R1 = TypeVar('_R1') +_R2 = TypeVar('_R2') +_V = TypeVar('_V') +_S = TypeVar('_S') +_R_co = TypeVar('_R_co', covariant=True) + +class _OnceCallable(Protocol[_P, _R]): + saved_result: _R + reset: Callable[[], None] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _ProxyMethodCacheWrapper(Protocol[_R_co]): + cache_clear: Callable[[], None] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +class _MethodCacheWrapper(Protocol[_R_co]): + def cache_clear(self) -> None: ... + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +# `compose()` overloads below will cover most use cases. + +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[_P, _R], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R1], _R], + __func3: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R2], _R], + __func3: Callable[[_R1], _R2], + __func4: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ... +def method_cache( + method: Callable[..., _R], + cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ..., +) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ... +def apply( + transform: Callable[[_R], _T] +) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ... +def result_invoke( + action: Callable[[_R], Any] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +def invoke( + f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... +def call_aside( + f: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... + +class Throttler(Generic[_R]): + last_called: float + func: Callable[..., _R] + max_rate: float + def __init__( + self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ... + ) -> None: ... + def reset(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> _R: ... + def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ... + +def first_invoke( + func1: Callable[..., Any], func2: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +method_caller: Callable[..., methodcaller] + +def retry_call( + func: Callable[..., _R], + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> _R: ... +def retry( + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... +def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ... +def pass_none( + func: Callable[Concatenate[_T, _P], _R] +) -> Callable[Concatenate[_T, _P], _R]: ... +def assign_params( + func: Callable[..., _R], namespace: dict[str, Any] +) -> partial[_R]: ... +def save_method_args( + method: Callable[Concatenate[_S, _P], _R] +) -> Callable[Concatenate[_S, _P], _R]: ... +def except_( + *exceptions: type[BaseException], replace: Any = ..., use: Any = ... +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ... +def identity(x: _T) -> _T: ... +def bypass_when( + check: _V, *, _op: Callable[[_V], Any] = ... +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def bypass_unless( + check: Any, +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... diff --git a/pkg_resources/_vendor/jaraco/functools/py.typed b/pkg_resources/_vendor/jaraco/functools/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/INSTALLER b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/INSTALLER similarity index 100% rename from setuptools/_vendor/jaraco.context-4.3.0.dist-info/INSTALLER rename to pkg_resources/_vendor/more_itertools-10.2.0.dist-info/INSTALLER diff --git a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/LICENSE b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/LICENSE similarity index 100% rename from pkg_resources/_vendor/more_itertools-9.1.0.dist-info/LICENSE rename to pkg_resources/_vendor/more_itertools-10.2.0.dist-info/LICENSE diff --git a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/METADATA b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/METADATA similarity index 90% rename from pkg_resources/_vendor/more_itertools-9.1.0.dist-info/METADATA rename to pkg_resources/_vendor/more_itertools-10.2.0.dist-info/METADATA index bee87762394..f54f1ff2794 100644 --- a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/METADATA +++ b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/METADATA @@ -1,21 +1,21 @@ Metadata-Version: 2.1 Name: more-itertools -Version: 9.1.0 +Version: 10.2.0 Summary: More routines for operating on iterables, beyond itertools Keywords: itertools,iterator,iteration,filter,peek,peekable,chunk,chunked Author-email: Erik Rose -Requires-Python: >=3.7 +Requires-Python: >=3.8 Description-Content-Type: text/x-rst Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy @@ -51,7 +51,7 @@ Python iterables. | | `unzip `_, | | | `batched `_, | | | `grouper `_, | -| | `partition `_ | +| | `partition `_, | | | `transpose `_ | +------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Lookahead and lookback | `spy `_, | @@ -92,7 +92,8 @@ Python iterables. | | `flatten `_, | | | `roundrobin `_, | | | `prepend `_, | -| | `value_chain `_ | +| | `value_chain `_, | +| | `partial_product `_ | +------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Summarizing | `ilen `_, | | | `unique_to_each `_, | @@ -120,17 +121,21 @@ Python iterables. | | `rstrip `_, | | | `filter_except `_, | | | `map_except `_, | +| | `filter_map `_, | +| | `iter_suppress `_, | | | `nth_or_last `_, | | | `unique_in_window `_, | | | `before_and_after `_, | | | `nth `_, | | | `take `_, | | | `tail `_, | -| | `unique_everseen `_, | +| | `unique_everseen `_, | | | `unique_justseen `_, | | | `duplicates_everseen `_, | | | `duplicates_justseen `_, | -| | `longest_common_prefix `_ | +| | `classify_unique `_, | +| | `longest_common_prefix `_, | +| | `takewhile_inclusive `_ | +------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Combinatorics | `distinct_permutations `_, | | | `distinct_combinations `_, | @@ -140,7 +145,9 @@ Python iterables. | | `product_index `_, | | | `combination_index `_, | | | `permutation_index `_, | +| | `combination_with_replacement_index `_, | | | `gray_product `_, | +| | `outer_product `_, | | | `powerset `_, | | | `random_product `_, | | | `random_permutation `_, | @@ -148,7 +155,8 @@ Python iterables. | | `random_combination_with_replacement `_, | | | `nth_product `_, | | | `nth_permutation `_, | -| | `nth_combination `_ | +| | `nth_combination `_, | +| | `nth_combination_with_replacement `_ | +------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Wrapping | `always_iterable `_, | | | `always_reversible `_, | @@ -173,9 +181,14 @@ Python iterables. | | `tabulate `_, | | | `repeatfunc `_, | | | `polynomial_from_roots `_, | -| | `sieve `_ | -| | `factor `_ | -| | `matmul `_ | +| | `polynomial_eval `_, | +| | `polynomial_derivative `_, | +| | `sieve `_, | +| | `factor `_, | +| | `matmul `_, | +| | `sum_of_squares `_, | +| | `totient `_, | +| | `reshape `_ | +------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/RECORD b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/RECORD new file mode 100644 index 00000000000..2ce6e4a6f56 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/RECORD @@ -0,0 +1,15 @@ +more_itertools-10.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +more_itertools-10.2.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 +more_itertools-10.2.0.dist-info/METADATA,sha256=lTIPxfD4IiP6aHzPjP4dXmzRRUmiXicAB6qnY82T-Gs,34886 +more_itertools-10.2.0.dist-info/RECORD,, +more_itertools-10.2.0.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 +more_itertools/__init__.py,sha256=VodgFyRJvpnHbAMgseYRiP7r928FFOAakmQrl6J88os,149 +more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 +more_itertools/__pycache__/__init__.cpython-312.pyc,, +more_itertools/__pycache__/more.cpython-312.pyc,, +more_itertools/__pycache__/recipes.cpython-312.pyc,, +more_itertools/more.py,sha256=jYdpbgXHf8yZDByPrhluxpe0D_IXRk2tfQnyfOFMi74,143045 +more_itertools/more.pyi,sha256=KTHYeqr0rFbn1GWRnv0jY64JRNnKKT0kA3kmsah8DYQ,21044 +more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +more_itertools/recipes.py,sha256=Rb3OhzJTCn2biutDEUSImbuY-8NDS1lkHt0My-uCOf4,27548 +more_itertools/recipes.pyi,sha256=T1IuEVXCqw2NeJJNW036MtWi8BVfR8Ilpf7cBmvhBaQ,4436 diff --git a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/WHEEL b/pkg_resources/_vendor/more_itertools-10.2.0.dist-info/WHEEL similarity index 100% rename from pkg_resources/_vendor/more_itertools-9.1.0.dist-info/WHEEL rename to pkg_resources/_vendor/more_itertools-10.2.0.dist-info/WHEEL diff --git a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/RECORD b/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/RECORD deleted file mode 100644 index c2fd4da0aca..00000000000 --- a/pkg_resources/_vendor/more_itertools-9.1.0.dist-info/RECORD +++ /dev/null @@ -1,15 +0,0 @@ -more_itertools-9.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -more_itertools-9.1.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 -more_itertools-9.1.0.dist-info/METADATA,sha256=qP4FQl-r_CTDFj9wwQAf_KrRs4u_HZBIeyc2WCLW69c,32271 -more_itertools-9.1.0.dist-info/RECORD,, -more_itertools-9.1.0.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 -more_itertools/__init__.py,sha256=mTzXsWGDHiVW5x8zHzcRu1imUMzrEtJnUhfsN-dBrV4,148 -more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 -more_itertools/__pycache__/__init__.cpython-311.pyc,, -more_itertools/__pycache__/more.cpython-311.pyc,, -more_itertools/__pycache__/recipes.cpython-311.pyc,, -more_itertools/more.py,sha256=YlrEMtcLMdcmcwL-T9YIQvMKjrAomEDbvQxQd4i5LnA,134968 -more_itertools/more.pyi,sha256=tZNfrCeIQLfOYhRyp0Wq7no_ryJ5h3FDskNNUBD-zmU,20105 -more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -more_itertools/recipes.py,sha256=lgw5bP3UoNfvUPhRaz1VIAfRFkF9pKWN-8UB6H0W5Eo,25416 -more_itertools/recipes.pyi,sha256=Um3BGANEFi4papnQfKBJnlEEuSpXS8-nbxro8OyuOt8,4056 diff --git a/pkg_resources/_vendor/more_itertools/__init__.py b/pkg_resources/_vendor/more_itertools/__init__.py index 66443971dfd..aff94a9abd0 100644 --- a/pkg_resources/_vendor/more_itertools/__init__.py +++ b/pkg_resources/_vendor/more_itertools/__init__.py @@ -3,4 +3,4 @@ from .more import * # noqa from .recipes import * # noqa -__version__ = '9.1.0' +__version__ = '10.2.0' diff --git a/pkg_resources/_vendor/more_itertools/more.py b/pkg_resources/_vendor/more_itertools/more.py index e0e2d3de92d..d0957681f54 100755 --- a/pkg_resources/_vendor/more_itertools/more.py +++ b/pkg_resources/_vendor/more_itertools/more.py @@ -2,7 +2,7 @@ from collections import Counter, defaultdict, deque, abc from collections.abc import Sequence -from functools import partial, reduce, wraps +from functools import cached_property, partial, reduce, wraps from heapq import heapify, heapreplace, heappop from itertools import ( chain, @@ -17,8 +17,9 @@ takewhile, tee, zip_longest, + product, ) -from math import exp, factorial, floor, log +from math import exp, factorial, floor, log, perm, comb from queue import Empty, Queue from random import random, randrange, uniform from operator import itemgetter, mul, sub, gt, lt, ge, le @@ -36,6 +37,7 @@ take, unique_everseen, all_equal, + batched, ) __all__ = [ @@ -53,6 +55,7 @@ 'circular_shifts', 'collapse', 'combination_index', + 'combination_with_replacement_index', 'consecutive_groups', 'constrained_batches', 'consumer', @@ -65,8 +68,10 @@ 'divide', 'duplicates_everseen', 'duplicates_justseen', + 'classify_unique', 'exactly_n', 'filter_except', + 'filter_map', 'first', 'gray_product', 'groupby_transform', @@ -80,6 +85,7 @@ 'is_sorted', 'islice_extended', 'iterate', + 'iter_suppress', 'last', 'locate', 'longest_common_prefix', @@ -93,10 +99,13 @@ 'nth_or_last', 'nth_permutation', 'nth_product', + 'nth_combination_with_replacement', 'numeric_range', 'one', 'only', + 'outer_product', 'padded', + 'partial_product', 'partitions', 'peekable', 'permutation_index', @@ -125,6 +134,7 @@ 'strictly_n', 'substrings', 'substrings_indexes', + 'takewhile_inclusive', 'time_limited', 'unique_in_window', 'unique_to_each', @@ -191,15 +201,14 @@ def first(iterable, default=_marker): ``next(iter(iterable), default)``. """ - try: - return next(iter(iterable)) - except StopIteration as e: - if default is _marker: - raise ValueError( - 'first() was called on an empty iterable, and no ' - 'default value was provided.' - ) from e - return default + for item in iterable: + return item + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, and no ' + 'default value was provided.' + ) + return default def last(iterable, default=_marker): @@ -472,7 +481,10 @@ def iterate(func, start): """ while True: yield start - start = func(start) + try: + start = func(start) + except StopIteration: + break def with_iter(context_manager): @@ -572,6 +584,9 @@ def strictly_n(iterable, n, too_short=None, too_long=None): >>> list(strictly_n(iterable, n)) ['a', 'b', 'c', 'd'] + Note that the returned iterable must be consumed in order for the check to + be made. + By default, *too_short* and *too_long* are functions that raise ``ValueError``. @@ -909,7 +924,7 @@ def substrings_indexes(seq, reverse=False): class bucket: - """Wrap *iterable* and return an object that buckets it iterable into + """Wrap *iterable* and return an object that buckets the iterable into child iterables based on a *key* function. >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] @@ -2069,7 +2084,6 @@ def __init__(self, *args): if self._step == self._zero: raise ValueError('numeric_range() arg 3 must not be zero') self._growing = self._step > self._zero - self._init_len() def __bool__(self): if self._growing: @@ -2145,7 +2159,8 @@ def __iter__(self): def __len__(self): return self._len - def _init_len(self): + @cached_property + def _len(self): if self._growing: start = self._start stop = self._stop @@ -2156,10 +2171,10 @@ def _init_len(self): step = -self._step distance = stop - start if distance <= self._zero: - self._len = 0 + return 0 else: # distance > 0 and step > 0: regular euclidean division q, r = divmod(distance, step) - self._len = int(q) + int(r != self._zero) + return int(q) + int(r != self._zero) def __reduce__(self): return numeric_range, (self._start, self._stop, self._step) @@ -2699,6 +2714,9 @@ class seekable: >>> it.seek(10) >>> next(it) '10' + >>> it.relative_seek(-2) # Seeking relative to the current position + >>> next(it) + '9' >>> it.seek(20) # Seeking past the end of the source isn't a problem >>> list(it) [] @@ -2812,6 +2830,10 @@ def seek(self, index): if remainder > 0: consume(self, remainder) + def relative_seek(self, count): + index = len(self._cache) + self.seek(max(index + count, 0)) + class run_length: """ @@ -3205,6 +3227,8 @@ class time_limited: stops if the time elapsed is greater than *limit_seconds*. If your time limit is 1 second, but it takes 2 seconds to generate the first item from the iterable, the function will run for 2 seconds and not yield anything. + As a special case, when *limit_seconds* is zero, the iterator never + returns anything. """ @@ -3220,6 +3244,9 @@ def __iter__(self): return self def __next__(self): + if self.limit_seconds == 0: + self.timed_out = True + raise StopIteration item = next(self._iterable) if monotonic() - self._start_time > self.limit_seconds: self.timed_out = True @@ -3339,7 +3366,7 @@ def iequals(*iterables): >>> iequals("abc", "acb") False - Not to be confused with :func:`all_equals`, which checks whether all + Not to be confused with :func:`all_equal`, which checks whether all elements of iterable are equal to each other. """ @@ -3835,7 +3862,7 @@ def nth_permutation(iterable, r, index): elif not 0 <= r < n: raise ValueError else: - c = factorial(n) // factorial(n - r) + c = perm(n, r) if index < 0: index += c @@ -3858,6 +3885,52 @@ def nth_permutation(iterable, r, index): return tuple(map(pool.pop, result)) +def nth_combination_with_replacement(iterable, r, index): + """Equivalent to + ``list(combinations_with_replacement(iterable, r))[index]``. + + + The subsequences with repetition of *iterable* that are of length *r* can + be ordered lexicographically. :func:`nth_combination_with_replacement` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences with replacement. + + >>> nth_combination_with_replacement(range(5), 3, 5) + (0, 1, 1) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = comb(n + r - 1, r) + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + i = 0 + while r: + r -= 1 + while n >= 0: + num_combs = comb(n + r - 1, r) + if index < num_combs: + break + n -= 1 + i += 1 + index -= num_combs + result.append(pool[i]) + + return tuple(result) + + def value_chain(*args): """Yield all arguments passed to the function in the same order in which they were passed. If an argument itself is iterable then iterate over its @@ -3949,9 +4022,66 @@ def combination_index(element, iterable): for i, j in enumerate(reversed(indexes), start=1): j = n - j if i <= j: - index += factorial(j) // (factorial(i) * factorial(j - i)) + index += comb(j, i) + + return comb(n + 1, k + 1) - index + + +def combination_with_replacement_index(element, iterable): + """Equivalent to + ``list(combinations_with_replacement(iterable, r)).index(element)`` + + The subsequences with repetition of *iterable* that are of length *r* can + be ordered lexicographically. :func:`combination_with_replacement_index` + computes the index of the first *element*, without computing the previous + combinations with replacement. + + >>> combination_with_replacement_index('adf', 'abcdefg') + 20 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations with replacement of *iterable*. + """ + element = tuple(element) + l = len(element) + element = enumerate(element) + + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = tuple(iterable) + for n, x in enumerate(pool): + while x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + if y is None: + break + else: + raise ValueError( + 'element is not a combination with replacement of iterable' + ) + + n = len(pool) + occupations = [0] * n + for p in indexes: + occupations[p] += 1 + + index = 0 + cumulative_sum = 0 + for k in range(1, n): + cumulative_sum += occupations[k - 1] + j = l + n - 1 - k - cumulative_sum + i = n - k + if i <= j: + index += comb(j, i) - return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + return index def permutation_index(element, iterable): @@ -4056,26 +4186,20 @@ def _chunked_even_finite(iterable, N, n): num_full = N - partial_size * num_lists num_partial = num_lists - num_full - buffer = [] - iterator = iter(iterable) - # Yield num_full lists of full_size - for x in iterator: - buffer.append(x) - if len(buffer) == full_size: - yield buffer - buffer = [] - num_full -= 1 - if num_full <= 0: - break + partial_start_idx = num_full * full_size + if full_size > 0: + for i in range(0, partial_start_idx, full_size): + yield list(islice(iterable, i, i + full_size)) # Yield num_partial lists of partial_size - for x in iterator: - buffer.append(x) - if len(buffer) == partial_size: - yield buffer - buffer = [] - num_partial -= 1 + if partial_size > 0: + for i in range( + partial_start_idx, + partial_start_idx + (num_partial * partial_size), + partial_size, + ): + yield list(islice(iterable, i, i + partial_size)) def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): @@ -4114,30 +4238,23 @@ def is_scalar(obj): if not size: return + new_item = [None] * size iterables, iterable_positions = [], [] - scalars, scalar_positions = [], [] for i, obj in enumerate(objects): if is_scalar(obj): - scalars.append(obj) - scalar_positions.append(i) + new_item[i] = obj else: iterables.append(iter(obj)) iterable_positions.append(i) - if len(scalars) == size: + if not iterables: yield tuple(objects) return zipper = _zip_equal if strict else zip for item in zipper(*iterables): - new_item = [None] * size - - for i, elem in zip(iterable_positions, item): - new_item[i] = elem - - for i, elem in zip(scalar_positions, scalars): - new_item[i] = elem - + for i, new_item[i] in zip(iterable_positions, item): + pass yield tuple(new_item) @@ -4162,22 +4279,23 @@ def unique_in_window(iterable, n, key=None): raise ValueError('n must be greater than 0') window = deque(maxlen=n) - uniques = set() + counts = defaultdict(int) use_key = key is not None for item in iterable: - k = key(item) if use_key else item - if k in uniques: - continue - - if len(uniques) == n: - uniques.discard(window[0]) + if len(window) == n: + to_discard = window[0] + if counts[to_discard] == 1: + del counts[to_discard] + else: + counts[to_discard] -= 1 - uniques.add(k) + k = key(item) if use_key else item + if k not in counts: + yield item + counts[k] += 1 window.append(k) - yield item - def duplicates_everseen(iterable, key=None): """Yield duplicate elements after their first appearance. @@ -4187,7 +4305,7 @@ def duplicates_everseen(iterable, key=None): >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] - This function is analagous to :func:`unique_everseen` and is subject to + This function is analogous to :func:`unique_everseen` and is subject to the same performance considerations. """ @@ -4217,15 +4335,52 @@ def duplicates_justseen(iterable, key=None): >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] - This function is analagous to :func:`unique_justseen`. + This function is analogous to :func:`unique_justseen`. """ - return flatten( - map( - lambda group_tuple: islice_extended(group_tuple[1])[1:], - groupby(iterable, key), - ) - ) + return flatten(g for _, g in groupby(iterable, key) for _ in g) + + +def classify_unique(iterable, key=None): + """Classify each element in terms of its uniqueness. + + For each element in the input iterable, return a 3-tuple consisting of: + + 1. The element itself + 2. ``False`` if the element is equal to the one preceding it in the input, + ``True`` otherwise (i.e. the equivalent of :func:`unique_justseen`) + 3. ``False`` if this element has been seen anywhere in the input before, + ``True`` otherwise (i.e. the equivalent of :func:`unique_everseen`) + + >>> list(classify_unique('otto')) # doctest: +NORMALIZE_WHITESPACE + [('o', True, True), + ('t', True, True), + ('t', False, False), + ('o', True, False)] + + This function is analogous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + previous = None + + for i, element in enumerate(iterable): + k = key(element) if use_key else element + is_unique_justseen = not i or previous != k + previous = k + is_unique_everseen = False + try: + if k not in seen_set: + seen_set.add(k) + is_unique_everseen = True + except TypeError: + if k not in seen_list: + seen_list.append(k) + is_unique_everseen = True + yield element, is_unique_justseen, is_unique_everseen def minmax(iterable_or_value, *others, key=None, default=_marker): @@ -4389,3 +4544,112 @@ def gray_product(*iterables): o[j] = -o[j] f[j] = f[j + 1] f[j + 1] = j + 1 + + +def partial_product(*iterables): + """Yields tuples containing one item from each iterator, with subsequent + tuples changing a single item at a time by advancing each iterator until it + is exhausted. This sequence guarantees every value in each iterable is + output at least once without generating all possible combinations. + + This may be useful, for example, when testing an expensive function. + + >>> list(partial_product('AB', 'C', 'DEF')) + [('A', 'C', 'D'), ('B', 'C', 'D'), ('B', 'C', 'E'), ('B', 'C', 'F')] + """ + + iterators = list(map(iter, iterables)) + + try: + prod = [next(it) for it in iterators] + except StopIteration: + return + yield tuple(prod) + + for i, it in enumerate(iterators): + for prod[i] in it: + yield tuple(prod) + + +def takewhile_inclusive(predicate, iterable): + """A variant of :func:`takewhile` that yields one additional element. + + >>> list(takewhile_inclusive(lambda x: x < 5, [1, 4, 6, 4, 1])) + [1, 4, 6] + + :func:`takewhile` would return ``[1, 4]``. + """ + for x in iterable: + yield x + if not predicate(x): + break + + +def outer_product(func, xs, ys, *args, **kwargs): + """A generalized outer product that applies a binary function to all + pairs of items. Returns a 2D matrix with ``len(xs)`` rows and ``len(ys)`` + columns. + Also accepts ``*args`` and ``**kwargs`` that are passed to ``func``. + + Multiplication table: + + >>> list(outer_product(mul, range(1, 4), range(1, 6))) + [(1, 2, 3, 4, 5), (2, 4, 6, 8, 10), (3, 6, 9, 12, 15)] + + Cross tabulation: + + >>> xs = ['A', 'B', 'A', 'A', 'B', 'B', 'A', 'A', 'B', 'B'] + >>> ys = ['X', 'X', 'X', 'Y', 'Z', 'Z', 'Y', 'Y', 'Z', 'Z'] + >>> rows = list(zip(xs, ys)) + >>> count_rows = lambda x, y: rows.count((x, y)) + >>> list(outer_product(count_rows, sorted(set(xs)), sorted(set(ys)))) + [(2, 3, 0), (1, 0, 4)] + + Usage with ``*args`` and ``**kwargs``: + + >>> animals = ['cat', 'wolf', 'mouse'] + >>> list(outer_product(min, animals, animals, key=len)) + [('cat', 'cat', 'cat'), ('cat', 'wolf', 'wolf'), ('cat', 'wolf', 'mouse')] + """ + ys = tuple(ys) + return batched( + starmap(lambda x, y: func(x, y, *args, **kwargs), product(xs, ys)), + n=len(ys), + ) + + +def iter_suppress(iterable, *exceptions): + """Yield each of the items from *iterable*. If the iteration raises one of + the specified *exceptions*, that exception will be suppressed and iteration + will stop. + + >>> from itertools import chain + >>> def breaks_at_five(x): + ... while True: + ... if x >= 5: + ... raise RuntimeError + ... yield x + ... x += 1 + >>> it_1 = iter_suppress(breaks_at_five(1), RuntimeError) + >>> it_2 = iter_suppress(breaks_at_five(2), RuntimeError) + >>> list(chain(it_1, it_2)) + [1, 2, 3, 4, 2, 3, 4] + """ + try: + yield from iterable + except exceptions: + return + + +def filter_map(func, iterable): + """Apply *func* to every element of *iterable*, yielding only those which + are not ``None``. + + >>> elems = ['1', 'a', '2', 'b', '3'] + >>> list(filter_map(lambda s: int(s) if s.isnumeric() else None, elems)) + [1, 2, 3] + """ + for x in iterable: + y = func(x) + if y is not None: + yield y diff --git a/pkg_resources/_vendor/more_itertools/more.pyi b/pkg_resources/_vendor/more_itertools/more.pyi index 75c5232c1a0..9a5fc911a3e 100644 --- a/pkg_resources/_vendor/more_itertools/more.pyi +++ b/pkg_resources/_vendor/more_itertools/more.pyi @@ -29,7 +29,7 @@ _U = TypeVar('_U') _V = TypeVar('_V') _W = TypeVar('_W') _T_co = TypeVar('_T_co', covariant=True) -_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[Any]]) _Raisable = BaseException | Type[BaseException] @type_check_only @@ -74,7 +74,7 @@ class peekable(Generic[_T], Iterator[_T]): def __getitem__(self, index: slice) -> list[_T]: ... def consumer(func: _GenFn) -> _GenFn: ... -def ilen(iterable: Iterable[object]) -> int: ... +def ilen(iterable: Iterable[_T]) -> int: ... def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... def with_iter( context_manager: ContextManager[Iterable[_T]], @@ -116,7 +116,7 @@ class bucket(Generic[_T, _U], Container[_U]): self, iterable: Iterable[_T], key: Callable[[_T], _U], - validator: Callable[[object], object] | None = ..., + validator: Callable[[_U], object] | None = ..., ) -> None: ... def __contains__(self, value: object) -> bool: ... def __iter__(self) -> Iterator[_U]: ... @@ -383,7 +383,7 @@ def mark_ends( iterable: Iterable[_T], ) -> Iterable[tuple[bool, bool, _T]]: ... def locate( - iterable: Iterable[object], + iterable: Iterable[_T], pred: Callable[..., Any] = ..., window_size: int | None = ..., ) -> Iterator[int]: ... @@ -440,6 +440,7 @@ class seekable(Generic[_T], Iterator[_T]): def peek(self, default: _U) -> _T | _U: ... def elements(self) -> SequenceView[_T]: ... def seek(self, index: int) -> None: ... + def relative_seek(self, count: int) -> None: ... class run_length: @staticmethod @@ -578,6 +579,9 @@ def all_unique( iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... ) -> bool: ... def nth_product(index: int, *args: Iterable[_T]) -> tuple[_T, ...]: ... +def nth_combination_with_replacement( + iterable: Iterable[_T], r: int, index: int +) -> tuple[_T, ...]: ... def nth_permutation( iterable: Iterable[_T], r: int, index: int ) -> tuple[_T, ...]: ... @@ -586,6 +590,9 @@ def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... def combination_index( element: Iterable[_T], iterable: Iterable[_T] ) -> int: ... +def combination_with_replacement_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... def permutation_index( element: Iterable[_T], iterable: Iterable[_T] ) -> int: ... @@ -611,6 +618,9 @@ def duplicates_everseen( def duplicates_justseen( iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... ) -> Iterator[_T]: ... +def classify_unique( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[tuple[_T, bool, bool]]: ... class _SupportsLessThan(Protocol): def __lt__(self, __other: Any) -> bool: ... @@ -655,12 +665,31 @@ def minmax( def longest_common_prefix( iterables: Iterable[Iterable[_T]], ) -> Iterator[_T]: ... -def iequals(*iterables: Iterable[object]) -> bool: ... +def iequals(*iterables: Iterable[Any]) -> bool: ... def constrained_batches( - iterable: Iterable[object], + iterable: Iterable[_T], max_size: int, max_count: int | None = ..., get_len: Callable[[_T], object] = ..., strict: bool = ..., ) -> Iterator[tuple[_T]]: ... def gray_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def partial_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def takewhile_inclusive( + predicate: Callable[[_T], bool], iterable: Iterable[_T] +) -> Iterator[_T]: ... +def outer_product( + func: Callable[[_T, _U], _V], + xs: Iterable[_T], + ys: Iterable[_U], + *args: Any, + **kwargs: Any, +) -> Iterator[tuple[_V, ...]]: ... +def iter_suppress( + iterable: Iterable[_T], + *exceptions: Type[BaseException], +) -> Iterator[_T]: ... +def filter_map( + func: Callable[[_T], _V | None], + iterable: Iterable[_T], +) -> Iterator[_V]: ... diff --git a/pkg_resources/_vendor/more_itertools/recipes.py b/pkg_resources/_vendor/more_itertools/recipes.py index 3facc2e3a67..145e3cb5bd6 100644 --- a/pkg_resources/_vendor/more_itertools/recipes.py +++ b/pkg_resources/_vendor/more_itertools/recipes.py @@ -9,11 +9,10 @@ """ import math import operator -import warnings from collections import deque from collections.abc import Sized -from functools import reduce +from functools import partial, reduce from itertools import ( chain, combinations, @@ -52,10 +51,13 @@ 'pad_none', 'pairwise', 'partition', + 'polynomial_eval', 'polynomial_from_roots', + 'polynomial_derivative', 'powerset', 'prepend', 'quantify', + 'reshape', 'random_combination_with_replacement', 'random_combination', 'random_permutation', @@ -65,9 +67,11 @@ 'sieve', 'sliding_window', 'subslices', + 'sum_of_squares', 'tabulate', 'tail', 'take', + 'totient', 'transpose', 'triplewise', 'unique_everseen', @@ -77,6 +81,18 @@ _marker = object() +# zip with strict is available for Python 3.10+ +try: + zip(strict=True) +except TypeError: + _zip_strict = zip +else: + _zip_strict = partial(zip, strict=True) + +# math.sumprod is available for Python 3.12+ +_sumprod = getattr(math, 'sumprod', lambda x, y: dotproduct(x, y)) + + def take(n, iterable): """Return first *n* items of the iterable as a list. @@ -293,7 +309,7 @@ def _pairwise(iterable): """ a, b = tee(iterable) next(b, None) - yield from zip(a, b) + return zip(a, b) try: @@ -303,7 +319,7 @@ def _pairwise(iterable): else: def pairwise(iterable): - yield from itertools_pairwise(iterable) + return itertools_pairwise(iterable) pairwise.__doc__ = _pairwise.__doc__ @@ -334,13 +350,9 @@ def _zip_equal(*iterables): for i, it in enumerate(iterables[1:], 1): size = len(it) if size != first_size: - break - else: - # If we didn't break out, we can use the built-in zip. - return zip(*iterables) - - # If we did break out, there was a mismatch. - raise UnequalIterablesError(details=(first_size, i, size)) + raise UnequalIterablesError(details=(first_size, i, size)) + # All sizes are equal, we can use the built-in zip. + return zip(*iterables) # If any one of the iterables didn't have a length, start reading # them until one runs out. except TypeError: @@ -433,12 +445,9 @@ def partition(pred, iterable): if pred is None: pred = bool - evaluations = ((pred(x), x) for x in iterable) - t1, t2 = tee(evaluations) - return ( - (x for (cond, x) in t1 if not cond), - (x for (cond, x) in t2 if cond), - ) + t1, t2, p = tee(iterable, 3) + p1, p2 = tee(map(pred, p)) + return (compress(t1, map(operator.not_, p1)), compress(t2, p2)) def powerset(iterable): @@ -486,7 +495,7 @@ def unique_everseen(iterable, key=None): >>> list(unique_everseen(iterable, key=tuple)) # Faster [[1, 2], [2, 3]] - Similary, you may want to convert unhashable ``set`` objects with + Similarly, you may want to convert unhashable ``set`` objects with ``key=frozenset``. For ``dict`` objects, ``key=lambda x: frozenset(x.items())`` can be used. @@ -518,6 +527,9 @@ def unique_justseen(iterable, key=None): ['A', 'B', 'C', 'A', 'D'] """ + if key is None: + return map(operator.itemgetter(0), groupby(iterable)) + return map(next, map(operator.itemgetter(1), groupby(iterable, key))) @@ -712,12 +724,14 @@ def convolve(signal, kernel): is immediately consumed and stored. """ + # This implementation intentionally doesn't match the one in the itertools + # documentation. kernel = tuple(kernel)[::-1] n = len(kernel) window = deque([0], maxlen=n) * n for x in chain(signal, repeat(0, n - 1)): window.append(x) - yield sum(map(operator.mul, kernel, window)) + yield _sumprod(kernel, window) def before_and_after(predicate, it): @@ -778,9 +792,7 @@ def sliding_window(iterable, n): For a variant with more features, see :func:`windowed`. """ it = iter(iterable) - window = deque(islice(it, n), maxlen=n) - if len(window) == n: - yield tuple(window) + window = deque(islice(it, n - 1), maxlen=n) for x in it: window.append(x) yield tuple(window) @@ -807,39 +819,38 @@ def polynomial_from_roots(roots): >>> polynomial_from_roots(roots) # x^3 - 4 * x^2 - 17 * x + 60 [1, -4, -17, 60] """ - # Use math.prod for Python 3.8+, - prod = getattr(math, 'prod', lambda x: reduce(operator.mul, x, 1)) - roots = list(map(operator.neg, roots)) - return [ - sum(map(prod, combinations(roots, k))) for k in range(len(roots) + 1) - ] + factors = zip(repeat(1), map(operator.neg, roots)) + return list(reduce(convolve, factors, [1])) -def iter_index(iterable, value, start=0): +def iter_index(iterable, value, start=0, stop=None): """Yield the index of each place in *iterable* that *value* occurs, - beginning with index *start*. + beginning with index *start* and ending before index *stop*. See :func:`locate` for a more general means of finding the indexes associated with particular values. >>> list(iter_index('AABCADEAF', 'A')) [0, 1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1)) # start index is inclusive + [1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1, 7)) # stop index is not inclusive + [1, 4] """ - try: - seq_index = iterable.index - except AttributeError: + seq_index = getattr(iterable, 'index', None) + if seq_index is None: # Slow path for general iterables - it = islice(iterable, start, None) + it = islice(iterable, start, stop) for i, element in enumerate(it, start): if element is value or element == value: yield i else: # Fast path for sequences + stop = len(iterable) if stop is None else stop i = start - 1 try: while True: - i = seq_index(value, i + 1) - yield i + yield (i := seq_index(value, i + 1, stop)) except ValueError: pass @@ -850,81 +861,152 @@ def sieve(n): >>> list(sieve(30)) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] """ - isqrt = getattr(math, 'isqrt', lambda x: int(math.sqrt(x))) + if n > 2: + yield 2 + start = 3 data = bytearray((0, 1)) * (n // 2) - data[:3] = 0, 0, 0 - limit = isqrt(n) + 1 - for p in compress(range(limit), data): + limit = math.isqrt(n) + 1 + for p in iter_index(data, 1, start, limit): + yield from iter_index(data, 1, start, p * p) data[p * p : n : p + p] = bytes(len(range(p * p, n, p + p))) - data[2] = 1 - return iter_index(data, 1) if n > 2 else iter([]) + start = p * p + yield from iter_index(data, 1, start) -def batched(iterable, n): - """Batch data into lists of length *n*. The last batch may be shorter. +def _batched(iterable, n, *, strict=False): + """Batch data into tuples of length *n*. If the number of items in + *iterable* is not divisible by *n*: + * The last batch will be shorter if *strict* is ``False``. + * :exc:`ValueError` will be raised if *strict* is ``True``. >>> list(batched('ABCDEFG', 3)) - [['A', 'B', 'C'], ['D', 'E', 'F'], ['G']] + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)] - This recipe is from the ``itertools`` docs. This library also provides - :func:`chunked`, which has a different implementation. + On Python 3.13 and above, this is an alias for :func:`itertools.batched`. """ - if hexversion >= 0x30C00A0: # Python 3.12.0a0 - warnings.warn( - ( - 'batched will be removed in a future version of ' - 'more-itertools. Use the standard library ' - 'itertools.batched function instead' - ), - DeprecationWarning, - ) - + if n < 1: + raise ValueError('n must be at least one') it = iter(iterable) - while True: - batch = list(islice(it, n)) - if not batch: - break + while batch := tuple(islice(it, n)): + if strict and len(batch) != n: + raise ValueError('batched(): incomplete batch') yield batch +if hexversion >= 0x30D00A2: + from itertools import batched as itertools_batched + + def batched(iterable, n, *, strict=False): + return itertools_batched(iterable, n, strict=strict) + +else: + batched = _batched + + batched.__doc__ = _batched.__doc__ + + def transpose(it): - """Swap the rows and columns of the input. + """Swap the rows and columns of the input matrix. >>> list(transpose([(1, 2, 3), (11, 22, 33)])) [(1, 11), (2, 22), (3, 33)] The caller should ensure that the dimensions of the input are compatible. + If the input is empty, no output will be produced. + """ + return _zip_strict(*it) + + +def reshape(matrix, cols): + """Reshape the 2-D input *matrix* to have a column count given by *cols*. + + >>> matrix = [(0, 1), (2, 3), (4, 5)] + >>> cols = 3 + >>> list(reshape(matrix, cols)) + [(0, 1, 2), (3, 4, 5)] """ - # TODO: when 3.9 goes end-of-life, add stric=True to this. - return zip(*it) + return batched(chain.from_iterable(matrix), cols) def matmul(m1, m2): """Multiply two matrices. + >>> list(matmul([(7, 5), (3, 5)], [(2, 5), (7, 9)])) - [[49, 80], [41, 60]] + [(49, 80), (41, 60)] The caller should ensure that the dimensions of the input matrices are compatible with each other. """ n = len(m2[0]) - return batched(starmap(dotproduct, product(m1, transpose(m2))), n) + return batched(starmap(_sumprod, product(m1, transpose(m2))), n) def factor(n): """Yield the prime factors of n. + >>> list(factor(360)) [2, 2, 2, 3, 3, 5] """ - isqrt = getattr(math, 'isqrt', lambda x: int(math.sqrt(x))) - for prime in sieve(isqrt(n) + 1): - while True: - quotient, remainder = divmod(n, prime) - if remainder: - break + for prime in sieve(math.isqrt(n) + 1): + while not n % prime: yield prime - n = quotient + n //= prime if n == 1: return - if n >= 2: + if n > 1: yield n + + +def polynomial_eval(coefficients, x): + """Evaluate a polynomial at a specific value. + + Example: evaluating x^3 - 4 * x^2 - 17 * x + 60 at x = 2.5: + + >>> coefficients = [1, -4, -17, 60] + >>> x = 2.5 + >>> polynomial_eval(coefficients, x) + 8.125 + """ + n = len(coefficients) + if n == 0: + return x * 0 # coerce zero to the type of x + powers = map(pow, repeat(x), reversed(range(n))) + return _sumprod(coefficients, powers) + + +def sum_of_squares(it): + """Return the sum of the squares of the input values. + + >>> sum_of_squares([10, 20, 30]) + 1400 + """ + return _sumprod(*tee(it)) + + +def polynomial_derivative(coefficients): + """Compute the first derivative of a polynomial. + + Example: evaluating the derivative of x^3 - 4 * x^2 - 17 * x + 60 + + >>> coefficients = [1, -4, -17, 60] + >>> derivative_coefficients = polynomial_derivative(coefficients) + >>> derivative_coefficients + [3, -8, -17] + """ + n = len(coefficients) + powers = reversed(range(1, n)) + return list(map(operator.mul, coefficients, powers)) + + +def totient(n): + """Return the count of natural numbers up to *n* that are coprime with *n*. + + >>> totient(9) + 6 + >>> totient(12) + 4 + """ + for p in unique_justseen(factor(n)): + n = n // p * (p - 1) + + return n diff --git a/pkg_resources/_vendor/more_itertools/recipes.pyi b/pkg_resources/_vendor/more_itertools/recipes.pyi index 0267ed569e7..ed4c19db49b 100644 --- a/pkg_resources/_vendor/more_itertools/recipes.pyi +++ b/pkg_resources/_vendor/more_itertools/recipes.pyi @@ -14,6 +14,8 @@ from typing import ( # Type and type variable definitions _T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') _U = TypeVar('_U') def take(n: int, iterable: Iterable[_T]) -> list[_T]: ... @@ -21,19 +23,19 @@ def tabulate( function: Callable[[int], _T], start: int = ... ) -> Iterator[_T]: ... def tail(n: int, iterable: Iterable[_T]) -> Iterator[_T]: ... -def consume(iterator: Iterable[object], n: int | None = ...) -> None: ... +def consume(iterator: Iterable[_T], n: int | None = ...) -> None: ... @overload def nth(iterable: Iterable[_T], n: int) -> _T | None: ... @overload def nth(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ... -def all_equal(iterable: Iterable[object]) -> bool: ... +def all_equal(iterable: Iterable[_T]) -> bool: ... def quantify( iterable: Iterable[_T], pred: Callable[[_T], bool] = ... ) -> int: ... def pad_none(iterable: Iterable[_T]) -> Iterator[_T | None]: ... def padnone(iterable: Iterable[_T]) -> Iterator[_T | None]: ... def ncycles(iterable: Iterable[_T], n: int) -> Iterator[_T]: ... -def dotproduct(vec1: Iterable[object], vec2: Iterable[object]) -> object: ... +def dotproduct(vec1: Iterable[_T1], vec2: Iterable[_T2]) -> Any: ... def flatten(listOfLists: Iterable[Iterable[_T]]) -> Iterator[_T]: ... def repeatfunc( func: Callable[..., _U], times: int | None = ..., *args: Any @@ -101,19 +103,26 @@ def sliding_window( iterable: Iterable[_T], n: int ) -> Iterator[tuple[_T, ...]]: ... def subslices(iterable: Iterable[_T]) -> Iterator[list[_T]]: ... -def polynomial_from_roots(roots: Sequence[int]) -> list[int]: ... +def polynomial_from_roots(roots: Sequence[_T]) -> list[_T]: ... def iter_index( - iterable: Iterable[object], + iterable: Iterable[_T], value: Any, start: int | None = ..., + stop: int | None = ..., ) -> Iterator[int]: ... def sieve(n: int) -> Iterator[int]: ... def batched( - iterable: Iterable[_T], - n: int, -) -> Iterator[list[_T]]: ... + iterable: Iterable[_T], n: int, *, strict: bool = False +) -> Iterator[tuple[_T]]: ... def transpose( it: Iterable[Iterable[_T]], -) -> tuple[Iterator[_T], ...]: ... -def matmul(m1: Sequence[_T], m2: Sequence[_T]) -> Iterator[list[_T]]: ... +) -> Iterator[tuple[_T, ...]]: ... +def reshape( + matrix: Iterable[Iterable[_T]], cols: int +) -> Iterator[tuple[_T, ...]]: ... +def matmul(m1: Sequence[_T], m2: Sequence[_T]) -> Iterator[tuple[_T]]: ... def factor(n: int) -> Iterator[int]: ... +def polynomial_eval(coefficients: Sequence[_T], x: _U) -> _U: ... +def sum_of_squares(it: Iterable[_T]) -> _T: ... +def polynomial_derivative(coefficients: Sequence[_T]) -> list[_T]: ... +def totient(n: int) -> int: ... diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD b/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD index e240a8408d3..e041f20f6ad 100644 --- a/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD +++ b/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD @@ -7,20 +7,20 @@ packaging-23.1.dist-info/RECORD,, packaging-23.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 packaging-23.1.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 packaging/__init__.py,sha256=kYVZSmXT6CWInT4UJPDtrSQBAZu8fMuFBxpv5GsDTLk,501 -packaging/__pycache__/__init__.cpython-311.pyc,, -packaging/__pycache__/_elffile.cpython-311.pyc,, -packaging/__pycache__/_manylinux.cpython-311.pyc,, -packaging/__pycache__/_musllinux.cpython-311.pyc,, -packaging/__pycache__/_parser.cpython-311.pyc,, -packaging/__pycache__/_structures.cpython-311.pyc,, -packaging/__pycache__/_tokenizer.cpython-311.pyc,, -packaging/__pycache__/markers.cpython-311.pyc,, -packaging/__pycache__/metadata.cpython-311.pyc,, -packaging/__pycache__/requirements.cpython-311.pyc,, -packaging/__pycache__/specifiers.cpython-311.pyc,, -packaging/__pycache__/tags.cpython-311.pyc,, -packaging/__pycache__/utils.cpython-311.pyc,, -packaging/__pycache__/version.cpython-311.pyc,, +packaging/__pycache__/__init__.cpython-312.pyc,, +packaging/__pycache__/_elffile.cpython-312.pyc,, +packaging/__pycache__/_manylinux.cpython-312.pyc,, +packaging/__pycache__/_musllinux.cpython-312.pyc,, +packaging/__pycache__/_parser.cpython-312.pyc,, +packaging/__pycache__/_structures.cpython-312.pyc,, +packaging/__pycache__/_tokenizer.cpython-312.pyc,, +packaging/__pycache__/markers.cpython-312.pyc,, +packaging/__pycache__/metadata.cpython-312.pyc,, +packaging/__pycache__/requirements.cpython-312.pyc,, +packaging/__pycache__/specifiers.cpython-312.pyc,, +packaging/__pycache__/tags.cpython-312.pyc,, +packaging/__pycache__/utils.cpython-312.pyc,, +packaging/__pycache__/version.cpython-312.pyc,, packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 packaging/_manylinux.py,sha256=ESGrDEVmBc8jYTtdZRAWiLk72lOzAKWeezFgoJ_MuBc,8926 packaging/_musllinux.py,sha256=mvPk7FNjjILKRLIdMxR7IvJ1uggLgCszo-L9rjfpi0M,2524 diff --git a/pkg_resources/_vendor/platformdirs-2.6.2.dist-info/RECORD b/pkg_resources/_vendor/platformdirs-2.6.2.dist-info/RECORD index 843a5baf9d5..a7213226940 100644 --- a/pkg_resources/_vendor/platformdirs-2.6.2.dist-info/RECORD +++ b/pkg_resources/_vendor/platformdirs-2.6.2.dist-info/RECORD @@ -6,14 +6,14 @@ platformdirs-2.6.2.dist-info/WHEEL,sha256=NaLmgHHW_f9jTvv_wRh9vcK7c7EK9o5fwsIXMO platformdirs-2.6.2.dist-info/licenses/LICENSE,sha256=KeD9YukphQ6G6yjD_czwzv30-pSHkBHP-z0NS-1tTbY,1089 platformdirs/__init__.py,sha256=td0a-fHENmnG8ess2WRoysKv9ud5j6TQ-p_iUM_uE18,12864 platformdirs/__main__.py,sha256=VsC0t5m-6f0YVr96PVks93G3EDF8MSNY4KpUMvPahDA,1164 -platformdirs/__pycache__/__init__.cpython-311.pyc,, -platformdirs/__pycache__/__main__.cpython-311.pyc,, -platformdirs/__pycache__/android.cpython-311.pyc,, -platformdirs/__pycache__/api.cpython-311.pyc,, -platformdirs/__pycache__/macos.cpython-311.pyc,, -platformdirs/__pycache__/unix.cpython-311.pyc,, -platformdirs/__pycache__/version.cpython-311.pyc,, -platformdirs/__pycache__/windows.cpython-311.pyc,, +platformdirs/__pycache__/__init__.cpython-312.pyc,, +platformdirs/__pycache__/__main__.cpython-312.pyc,, +platformdirs/__pycache__/android.cpython-312.pyc,, +platformdirs/__pycache__/api.cpython-312.pyc,, +platformdirs/__pycache__/macos.cpython-312.pyc,, +platformdirs/__pycache__/unix.cpython-312.pyc,, +platformdirs/__pycache__/version.cpython-312.pyc,, +platformdirs/__pycache__/windows.cpython-312.pyc,, platformdirs/android.py,sha256=GKizhyS7ESRiU67u8UnBJLm46goau9937EchXWbPBlk,4068 platformdirs/api.py,sha256=MXKHXOL3eh_-trSok-JUTjAR_zjmmKF3rjREVABjP8s,4910 platformdirs/macos.py,sha256=-3UXQewbT0yMhMdkzRXfXGAntmLIH7Qt4a9Hlf8I5_Y,2655 diff --git a/pkg_resources/_vendor/typing_extensions-4.4.0.dist-info/RECORD b/pkg_resources/_vendor/typing_extensions-4.4.0.dist-info/RECORD index b9e1bb0391d..e1132566df7 100644 --- a/pkg_resources/_vendor/typing_extensions-4.4.0.dist-info/RECORD +++ b/pkg_resources/_vendor/typing_extensions-4.4.0.dist-info/RECORD @@ -1,4 +1,4 @@ -__pycache__/typing_extensions.cpython-311.pyc,, +__pycache__/typing_extensions.cpython-312.pyc,, typing_extensions-4.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 typing_extensions-4.4.0.dist-info/LICENSE,sha256=x6-2XnVXB7n7kEhziaF20-09ADHVExr95FwjcV_16JE,12787 typing_extensions-4.4.0.dist-info/METADATA,sha256=1zSh1eMLnLkLMMC6aZSGRKx3eRnivEGDFWGSVD1zqhA,7249 diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt index 4cd4ab8cb8b..11389159219 100644 --- a/pkg_resources/_vendor/vendored.txt +++ b/pkg_resources/_vendor/vendored.txt @@ -9,3 +9,5 @@ jaraco.text==3.7.0 importlib_resources==5.10.2 # required for importlib_resources on older Pythons zipp==3.7.0 +# required for jaraco.context on older Pythons +backports.tarfile diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD b/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD index 0a88551ce0d..adc797bc2ee 100644 --- a/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD @@ -1,4 +1,4 @@ -__pycache__/zipp.cpython-311.pyc,, +__pycache__/zipp.cpython-312.pyc,, zipp-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 zipp-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 zipp-3.7.0.dist-info/METADATA,sha256=ZLzgaXTyZX_MxTU0lcGfhdPY4CjFrT_3vyQ2Fo49pl8,2261 diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/INSTALLER b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/INSTALLER similarity index 100% rename from setuptools/_vendor/jaraco.functools-3.6.0.dist-info/INSTALLER rename to setuptools/_vendor/backports.tarfile-1.0.0.dist-info/INSTALLER diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/LICENSE b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE similarity index 97% rename from setuptools/_vendor/jaraco.context-4.3.0.dist-info/LICENSE rename to setuptools/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE index 353924be0e5..1bb5a44356f 100644 --- a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/LICENSE +++ b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/METADATA b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/METADATA new file mode 100644 index 00000000000..e7b64c87f8e --- /dev/null +++ b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/METADATA @@ -0,0 +1,44 @@ +Metadata-Version: 2.1 +Name: backports.tarfile +Version: 1.0.0 +Summary: Backport of CPython tarfile module +Home-page: https://github.com/jaraco/backports.tarfile +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.8 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest !=8.1.1,>=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/backports.tarfile.svg + :target: https://pypi.org/project/backports.tarfile + +.. image:: https://img.shields.io/pypi/pyversions/backports.tarfile.svg + +.. image:: https://github.com/jaraco/backports.tarfile/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/backports.tarfile/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. .. image:: https://readthedocs.org/projects/backportstarfile/badge/?version=latest +.. :target: https://backportstarfile.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton diff --git a/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/RECORD b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/RECORD new file mode 100644 index 00000000000..a6a44d8fcc5 --- /dev/null +++ b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/RECORD @@ -0,0 +1,9 @@ +backports.tarfile-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +backports.tarfile-1.0.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +backports.tarfile-1.0.0.dist-info/METADATA,sha256=XlT7JAFR04zDMIjs-EFhqc0CkkVyeh-SiVUoKXONXJ0,1876 +backports.tarfile-1.0.0.dist-info/RECORD,, +backports.tarfile-1.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +backports.tarfile-1.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +backports.tarfile-1.0.0.dist-info/top_level.txt,sha256=cGjaLMOoBR1FK0ApojtzWVmViTtJ7JGIK_HwXiEsvtU,10 +backports/__pycache__/tarfile.cpython-312.pyc,, +backports/tarfile.py,sha256=IO3YX_ZYqn13VOi-3QLM0lnktn102U4d9wUrHc230LY,106920 diff --git a/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/WHEEL b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL similarity index 65% rename from pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/WHEEL rename to setuptools/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL index 57e3d840d59..bab98d67588 100644 --- a/pkg_resources/_vendor/jaraco.context-4.3.0.dist-info/WHEEL +++ b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: bdist_wheel (0.38.4) +Generator: bdist_wheel (0.43.0) Root-Is-Purelib: true Tag: py3-none-any diff --git a/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt new file mode 100644 index 00000000000..99d2be5b64d --- /dev/null +++ b/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +backports diff --git a/setuptools/_vendor/backports/tarfile.py b/setuptools/_vendor/backports/tarfile.py new file mode 100644 index 00000000000..a7a9a6e7b94 --- /dev/null +++ b/setuptools/_vendor/backports/tarfile.py @@ -0,0 +1,2900 @@ +#!/usr/bin/env python3 +#------------------------------------------------------------------- +# tarfile.py +#------------------------------------------------------------------- +# Copyright (C) 2002 Lars Gustaebel +# All rights reserved. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +"""Read from and write to tar format archives. +""" + +version = "0.9.0" +__author__ = "Lars Gust\u00e4bel (lars@gustaebel.de)" +__credits__ = "Gustavo Niemeyer, Niels Gust\u00e4bel, Richard Townsend." + +#--------- +# Imports +#--------- +from builtins import open as bltn_open +import sys +import os +import io +import shutil +import stat +import time +import struct +import copy +import re +import warnings + +try: + import pwd +except ImportError: + pwd = None +try: + import grp +except ImportError: + grp = None + +# os.symlink on Windows prior to 6.0 raises NotImplementedError +# OSError (winerror=1314) will be raised if the caller does not hold the +# SeCreateSymbolicLinkPrivilege privilege +symlink_exception = (AttributeError, NotImplementedError, OSError) + +# from tarfile import * +__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError", + "CompressionError", "StreamError", "ExtractError", "HeaderError", + "ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT", + "DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter", + "tar_filter", "FilterError", "AbsoluteLinkError", + "OutsideDestinationError", "SpecialFileError", "AbsolutePathError", + "LinkOutsideDestinationError"] + + +#--------------------------------------------------------- +# tar constants +#--------------------------------------------------------- +NUL = b"\0" # the null character +BLOCKSIZE = 512 # length of processing blocks +RECORDSIZE = BLOCKSIZE * 20 # length of records +GNU_MAGIC = b"ustar \0" # magic gnu tar string +POSIX_MAGIC = b"ustar\x0000" # magic posix tar string + +LENGTH_NAME = 100 # maximum length of a filename +LENGTH_LINK = 100 # maximum length of a linkname +LENGTH_PREFIX = 155 # maximum length of the prefix field + +REGTYPE = b"0" # regular file +AREGTYPE = b"\0" # regular file +LNKTYPE = b"1" # link (inside tarfile) +SYMTYPE = b"2" # symbolic link +CHRTYPE = b"3" # character special device +BLKTYPE = b"4" # block special device +DIRTYPE = b"5" # directory +FIFOTYPE = b"6" # fifo special device +CONTTYPE = b"7" # contiguous file + +GNUTYPE_LONGNAME = b"L" # GNU tar longname +GNUTYPE_LONGLINK = b"K" # GNU tar longlink +GNUTYPE_SPARSE = b"S" # GNU tar sparse file + +XHDTYPE = b"x" # POSIX.1-2001 extended header +XGLTYPE = b"g" # POSIX.1-2001 global header +SOLARIS_XHDTYPE = b"X" # Solaris extended header + +USTAR_FORMAT = 0 # POSIX.1-1988 (ustar) format +GNU_FORMAT = 1 # GNU tar format +PAX_FORMAT = 2 # POSIX.1-2001 (pax) format +DEFAULT_FORMAT = PAX_FORMAT + +#--------------------------------------------------------- +# tarfile constants +#--------------------------------------------------------- +# File types that tarfile supports: +SUPPORTED_TYPES = (REGTYPE, AREGTYPE, LNKTYPE, + SYMTYPE, DIRTYPE, FIFOTYPE, + CONTTYPE, CHRTYPE, BLKTYPE, + GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# File types that will be treated as a regular file. +REGULAR_TYPES = (REGTYPE, AREGTYPE, + CONTTYPE, GNUTYPE_SPARSE) + +# File types that are part of the GNU tar format. +GNU_TYPES = (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK, + GNUTYPE_SPARSE) + +# Fields from a pax header that override a TarInfo attribute. +PAX_FIELDS = ("path", "linkpath", "size", "mtime", + "uid", "gid", "uname", "gname") + +# Fields from a pax header that are affected by hdrcharset. +PAX_NAME_FIELDS = {"path", "linkpath", "uname", "gname"} + +# Fields in a pax header that are numbers, all other fields +# are treated as strings. +PAX_NUMBER_FIELDS = { + "atime": float, + "ctime": float, + "mtime": float, + "uid": int, + "gid": int, + "size": int +} + +#--------------------------------------------------------- +# initialization +#--------------------------------------------------------- +if os.name == "nt": + ENCODING = "utf-8" +else: + ENCODING = sys.getfilesystemencoding() + +#--------------------------------------------------------- +# Some useful functions +#--------------------------------------------------------- + +def stn(s, length, encoding, errors): + """Convert a string to a null-terminated bytes object. + """ + if s is None: + raise ValueError("metadata cannot contain None") + s = s.encode(encoding, errors) + return s[:length] + (length - len(s)) * NUL + +def nts(s, encoding, errors): + """Convert a null-terminated bytes object to a string. + """ + p = s.find(b"\0") + if p != -1: + s = s[:p] + return s.decode(encoding, errors) + +def nti(s): + """Convert a number field to a python number. + """ + # There are two possible encodings for a number field, see + # itn() below. + if s[0] in (0o200, 0o377): + n = 0 + for i in range(len(s) - 1): + n <<= 8 + n += s[i + 1] + if s[0] == 0o377: + n = -(256 ** (len(s) - 1) - n) + else: + try: + s = nts(s, "ascii", "strict") + n = int(s.strip() or "0", 8) + except ValueError: + raise InvalidHeaderError("invalid header") + return n + +def itn(n, digits=8, format=DEFAULT_FORMAT): + """Convert a python number to a number field. + """ + # POSIX 1003.1-1988 requires numbers to be encoded as a string of + # octal digits followed by a null-byte, this allows values up to + # (8**(digits-1))-1. GNU tar allows storing numbers greater than + # that if necessary. A leading 0o200 or 0o377 byte indicate this + # particular encoding, the following digits-1 bytes are a big-endian + # base-256 representation. This allows values up to (256**(digits-1))-1. + # A 0o200 byte indicates a positive number, a 0o377 byte a negative + # number. + original_n = n + n = int(n) + if 0 <= n < 8 ** (digits - 1): + s = bytes("%0*o" % (digits - 1, n), "ascii") + NUL + elif format == GNU_FORMAT and -256 ** (digits - 1) <= n < 256 ** (digits - 1): + if n >= 0: + s = bytearray([0o200]) + else: + s = bytearray([0o377]) + n = 256 ** digits + n + + for i in range(digits - 1): + s.insert(1, n & 0o377) + n >>= 8 + else: + raise ValueError("overflow in number field") + + return s + +def calc_chksums(buf): + """Calculate the checksum for a member's header by summing up all + characters except for the chksum field which is treated as if + it was filled with spaces. According to the GNU tar sources, + some tars (Sun and NeXT) calculate chksum with signed char, + which will be different if there are chars in the buffer with + the high bit set. So we calculate two checksums, unsigned and + signed. + """ + unsigned_chksum = 256 + sum(struct.unpack_from("148B8x356B", buf)) + signed_chksum = 256 + sum(struct.unpack_from("148b8x356b", buf)) + return unsigned_chksum, signed_chksum + +def copyfileobj(src, dst, length=None, exception=OSError, bufsize=None): + """Copy length bytes from fileobj src to fileobj dst. + If length is None, copy the entire content. + """ + bufsize = bufsize or 16 * 1024 + if length == 0: + return + if length is None: + shutil.copyfileobj(src, dst, bufsize) + return + + blocks, remainder = divmod(length, bufsize) + for b in range(blocks): + buf = src.read(bufsize) + if len(buf) < bufsize: + raise exception("unexpected end of data") + dst.write(buf) + + if remainder != 0: + buf = src.read(remainder) + if len(buf) < remainder: + raise exception("unexpected end of data") + dst.write(buf) + return + +def _safe_print(s): + encoding = getattr(sys.stdout, 'encoding', None) + if encoding is not None: + s = s.encode(encoding, 'backslashreplace').decode(encoding) + print(s, end=' ') + + +class TarError(Exception): + """Base exception.""" + pass +class ExtractError(TarError): + """General exception for extract errors.""" + pass +class ReadError(TarError): + """Exception for unreadable tar archives.""" + pass +class CompressionError(TarError): + """Exception for unavailable compression methods.""" + pass +class StreamError(TarError): + """Exception for unsupported operations on stream-like TarFiles.""" + pass +class HeaderError(TarError): + """Base exception for header errors.""" + pass +class EmptyHeaderError(HeaderError): + """Exception for empty headers.""" + pass +class TruncatedHeaderError(HeaderError): + """Exception for truncated headers.""" + pass +class EOFHeaderError(HeaderError): + """Exception for end of file headers.""" + pass +class InvalidHeaderError(HeaderError): + """Exception for invalid headers.""" + pass +class SubsequentHeaderError(HeaderError): + """Exception for missing and invalid extended headers.""" + pass + +#--------------------------- +# internal stream interface +#--------------------------- +class _LowLevelFile: + """Low-level file object. Supports reading and writing. + It is used instead of a regular file object for streaming + access. + """ + + def __init__(self, name, mode): + mode = { + "r": os.O_RDONLY, + "w": os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + }[mode] + if hasattr(os, "O_BINARY"): + mode |= os.O_BINARY + self.fd = os.open(name, mode, 0o666) + + def close(self): + os.close(self.fd) + + def read(self, size): + return os.read(self.fd, size) + + def write(self, s): + os.write(self.fd, s) + +class _Stream: + """Class that serves as an adapter between TarFile and + a stream-like object. The stream-like object only + needs to have a read() or write() method that works with bytes, + and the method is accessed blockwise. + Use of gzip or bzip2 compression is possible. + A stream-like object could be for example: sys.stdin.buffer, + sys.stdout.buffer, a socket, a tape device etc. + + _Stream is intended to be used only internally. + """ + + def __init__(self, name, mode, comptype, fileobj, bufsize, + compresslevel): + """Construct a _Stream object. + """ + self._extfileobj = True + if fileobj is None: + fileobj = _LowLevelFile(name, mode) + self._extfileobj = False + + if comptype == '*': + # Enable transparent compression detection for the + # stream interface + fileobj = _StreamProxy(fileobj) + comptype = fileobj.getcomptype() + + self.name = name or "" + self.mode = mode + self.comptype = comptype + self.fileobj = fileobj + self.bufsize = bufsize + self.buf = b"" + self.pos = 0 + self.closed = False + + try: + if comptype == "gz": + try: + import zlib + except ImportError: + raise CompressionError("zlib module is not available") from None + self.zlib = zlib + self.crc = zlib.crc32(b"") + if mode == "r": + self.exception = zlib.error + self._init_read_gz() + else: + self._init_write_gz(compresslevel) + + elif comptype == "bz2": + try: + import bz2 + except ImportError: + raise CompressionError("bz2 module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = bz2.BZ2Decompressor() + self.exception = OSError + else: + self.cmp = bz2.BZ2Compressor(compresslevel) + + elif comptype == "xz": + try: + import lzma + except ImportError: + raise CompressionError("lzma module is not available") from None + if mode == "r": + self.dbuf = b"" + self.cmp = lzma.LZMADecompressor() + self.exception = lzma.LZMAError + else: + self.cmp = lzma.LZMACompressor() + + elif comptype != "tar": + raise CompressionError("unknown compression type %r" % comptype) + + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + def __del__(self): + if hasattr(self, "closed") and not self.closed: + self.close() + + def _init_write_gz(self, compresslevel): + """Initialize for writing with gzip compression. + """ + self.cmp = self.zlib.compressobj(compresslevel, + self.zlib.DEFLATED, + -self.zlib.MAX_WBITS, + self.zlib.DEF_MEM_LEVEL, + 0) + timestamp = struct.pack(" self.bufsize: + self.fileobj.write(self.buf[:self.bufsize]) + self.buf = self.buf[self.bufsize:] + + def close(self): + """Close the _Stream object. No operation should be + done on it afterwards. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode == "w" and self.comptype != "tar": + self.buf += self.cmp.flush() + + if self.mode == "w" and self.buf: + self.fileobj.write(self.buf) + self.buf = b"" + if self.comptype == "gz": + self.fileobj.write(struct.pack("= 0: + blocks, remainder = divmod(pos - self.pos, self.bufsize) + for i in range(blocks): + self.read(self.bufsize) + self.read(remainder) + else: + raise StreamError("seeking backwards is not allowed") + return self.pos + + def read(self, size): + """Return the next size number of bytes from the stream.""" + assert size is not None + buf = self._read(size) + self.pos += len(buf) + return buf + + def _read(self, size): + """Return size bytes from the stream. + """ + if self.comptype == "tar": + return self.__read(size) + + c = len(self.dbuf) + t = [self.dbuf] + while c < size: + # Skip underlying buffer to avoid unaligned double buffering. + if self.buf: + buf = self.buf + self.buf = b"" + else: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + try: + buf = self.cmp.decompress(buf) + except self.exception as e: + raise ReadError("invalid compressed data") from e + t.append(buf) + c += len(buf) + t = b"".join(t) + self.dbuf = t[size:] + return t[:size] + + def __read(self, size): + """Return size bytes from stream. If internal buffer is empty, + read another block from the stream. + """ + c = len(self.buf) + t = [self.buf] + while c < size: + buf = self.fileobj.read(self.bufsize) + if not buf: + break + t.append(buf) + c += len(buf) + t = b"".join(t) + self.buf = t[size:] + return t[:size] +# class _Stream + +class _StreamProxy(object): + """Small proxy class that enables transparent compression + detection for the Stream interface (mode 'r|*'). + """ + + def __init__(self, fileobj): + self.fileobj = fileobj + self.buf = self.fileobj.read(BLOCKSIZE) + + def read(self, size): + self.read = self.fileobj.read + return self.buf + + def getcomptype(self): + if self.buf.startswith(b"\x1f\x8b\x08"): + return "gz" + elif self.buf[0:3] == b"BZh" and self.buf[4:10] == b"1AY&SY": + return "bz2" + elif self.buf.startswith((b"\x5d\x00\x00\x80", b"\xfd7zXZ")): + return "xz" + else: + return "tar" + + def close(self): + self.fileobj.close() +# class StreamProxy + +#------------------------ +# Extraction file object +#------------------------ +class _FileInFile(object): + """A thin wrapper around an existing file object that + provides a part of its data as an individual file + object. + """ + + def __init__(self, fileobj, offset, size, name, blockinfo=None): + self.fileobj = fileobj + self.offset = offset + self.size = size + self.position = 0 + self.name = name + self.closed = False + + if blockinfo is None: + blockinfo = [(0, size)] + + # Construct a map with data and zero blocks. + self.map_index = 0 + self.map = [] + lastpos = 0 + realpos = self.offset + for offset, size in blockinfo: + if offset > lastpos: + self.map.append((False, lastpos, offset, None)) + self.map.append((True, offset, offset + size, realpos)) + realpos += size + lastpos = offset + size + if lastpos < self.size: + self.map.append((False, lastpos, self.size, None)) + + def flush(self): + pass + + def readable(self): + return True + + def writable(self): + return False + + def seekable(self): + return self.fileobj.seekable() + + def tell(self): + """Return the current file position. + """ + return self.position + + def seek(self, position, whence=io.SEEK_SET): + """Seek to a position in the file. + """ + if whence == io.SEEK_SET: + self.position = min(max(position, 0), self.size) + elif whence == io.SEEK_CUR: + if position < 0: + self.position = max(self.position + position, 0) + else: + self.position = min(self.position + position, self.size) + elif whence == io.SEEK_END: + self.position = max(min(self.size + position, self.size), 0) + else: + raise ValueError("Invalid argument") + return self.position + + def read(self, size=None): + """Read data from the file. + """ + if size is None: + size = self.size - self.position + else: + size = min(size, self.size - self.position) + + buf = b"" + while size > 0: + while True: + data, start, stop, offset = self.map[self.map_index] + if start <= self.position < stop: + break + else: + self.map_index += 1 + if self.map_index == len(self.map): + self.map_index = 0 + length = min(size, stop - self.position) + if data: + self.fileobj.seek(offset + (self.position - start)) + b = self.fileobj.read(length) + if len(b) != length: + raise ReadError("unexpected end of data") + buf += b + else: + buf += NUL * length + size -= length + self.position += length + return buf + + def readinto(self, b): + buf = self.read(len(b)) + b[:len(buf)] = buf + return len(buf) + + def close(self): + self.closed = True +#class _FileInFile + +class ExFileObject(io.BufferedReader): + + def __init__(self, tarfile, tarinfo): + fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data, + tarinfo.size, tarinfo.name, tarinfo.sparse) + super().__init__(fileobj) +#class ExFileObject + + +#----------------------------- +# extraction filters (PEP 706) +#----------------------------- + +class FilterError(TarError): + pass + +class AbsolutePathError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'member {tarinfo.name!r} has an absolute path') + +class OutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would be extracted to {path!r}, ' + + 'which is outside the destination') + +class SpecialFileError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a special file') + +class AbsoluteLinkError(FilterError): + def __init__(self, tarinfo): + self.tarinfo = tarinfo + super().__init__(f'{tarinfo.name!r} is a link to an absolute path') + +class LinkOutsideDestinationError(FilterError): + def __init__(self, tarinfo, path): + self.tarinfo = tarinfo + self._path = path + super().__init__(f'{tarinfo.name!r} would link to {path!r}, ' + + 'which is outside the destination') + +def _get_filtered_attrs(member, dest_path, for_data=True): + new_attrs = {} + name = member.name + dest_path = os.path.realpath(dest_path) + # Strip leading / (tar's directory separator) from filenames. + # Include os.sep (target OS directory separator) as well. + if name.startswith(('/', os.sep)): + name = new_attrs['name'] = member.path.lstrip('/' + os.sep) + if os.path.isabs(name): + # Path is absolute even after stripping. + # For example, 'C:/foo' on Windows. + raise AbsolutePathError(member) + # Ensure we stay in the destination + target_path = os.path.realpath(os.path.join(dest_path, name)) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise OutsideDestinationError(member, target_path) + # Limit permissions (no high bits, and go-w) + mode = member.mode + if mode is not None: + # Strip high bits & group/other write bits + mode = mode & 0o755 + if for_data: + # For data, handle permissions & file types + if member.isreg() or member.islnk(): + if not mode & 0o100: + # Clear executable bits if not executable by user + mode &= ~0o111 + # Ensure owner can read & write + mode |= 0o600 + elif member.isdir() or member.issym(): + # Ignore mode for directories & symlinks + mode = None + else: + # Reject special files + raise SpecialFileError(member) + if mode != member.mode: + new_attrs['mode'] = mode + if for_data: + # Ignore ownership for 'data' + if member.uid is not None: + new_attrs['uid'] = None + if member.gid is not None: + new_attrs['gid'] = None + if member.uname is not None: + new_attrs['uname'] = None + if member.gname is not None: + new_attrs['gname'] = None + # Check link destination for 'data' + if member.islnk() or member.issym(): + if os.path.isabs(member.linkname): + raise AbsoluteLinkError(member) + if member.issym(): + target_path = os.path.join(dest_path, + os.path.dirname(name), + member.linkname) + else: + target_path = os.path.join(dest_path, + member.linkname) + target_path = os.path.realpath(target_path) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise LinkOutsideDestinationError(member, target_path) + return new_attrs + +def fully_trusted_filter(member, dest_path): + return member + +def tar_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, False) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +def data_filter(member, dest_path): + new_attrs = _get_filtered_attrs(member, dest_path, True) + if new_attrs: + return member.replace(**new_attrs, deep=False) + return member + +_NAMED_FILTERS = { + "fully_trusted": fully_trusted_filter, + "tar": tar_filter, + "data": data_filter, +} + +#------------------ +# Exported Classes +#------------------ + +# Sentinel for replace() defaults, meaning "don't change the attribute" +_KEEP = object() + +class TarInfo(object): + """Informational class which holds the details about an + archive member given by a tar header block. + TarInfo objects are returned by TarFile.getmember(), + TarFile.getmembers() and TarFile.gettarinfo() and are + usually created internally. + """ + + __slots__ = dict( + name = 'Name of the archive member.', + mode = 'Permission bits.', + uid = 'User ID of the user who originally stored this member.', + gid = 'Group ID of the user who originally stored this member.', + size = 'Size in bytes.', + mtime = 'Time of last modification.', + chksum = 'Header checksum.', + type = ('File type. type is usually one of these constants: ' + 'REGTYPE, AREGTYPE, LNKTYPE, SYMTYPE, DIRTYPE, FIFOTYPE, ' + 'CONTTYPE, CHRTYPE, BLKTYPE, GNUTYPE_SPARSE.'), + linkname = ('Name of the target file name, which is only present ' + 'in TarInfo objects of type LNKTYPE and SYMTYPE.'), + uname = 'User name.', + gname = 'Group name.', + devmajor = 'Device major number.', + devminor = 'Device minor number.', + offset = 'The tar header starts here.', + offset_data = "The file's data starts here.", + pax_headers = ('A dictionary containing key-value pairs of an ' + 'associated pax extended header.'), + sparse = 'Sparse member information.', + tarfile = None, + _sparse_structs = None, + _link_target = None, + ) + + def __init__(self, name=""): + """Construct a TarInfo object. name is the optional name + of the member. + """ + self.name = name # member name + self.mode = 0o644 # file permissions + self.uid = 0 # user id + self.gid = 0 # group id + self.size = 0 # file size + self.mtime = 0 # modification time + self.chksum = 0 # header checksum + self.type = REGTYPE # member type + self.linkname = "" # link name + self.uname = "" # user name + self.gname = "" # group name + self.devmajor = 0 # device major number + self.devminor = 0 # device minor number + + self.offset = 0 # the tar header starts here + self.offset_data = 0 # the file's data starts here + + self.sparse = None # sparse member information + self.pax_headers = {} # pax header information + + @property + def path(self): + 'In pax headers, "name" is called "path".' + return self.name + + @path.setter + def path(self, name): + self.name = name + + @property + def linkpath(self): + 'In pax headers, "linkname" is called "linkpath".' + return self.linkname + + @linkpath.setter + def linkpath(self, linkname): + self.linkname = linkname + + def __repr__(self): + return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self)) + + def replace(self, *, + name=_KEEP, mtime=_KEEP, mode=_KEEP, linkname=_KEEP, + uid=_KEEP, gid=_KEEP, uname=_KEEP, gname=_KEEP, + deep=True, _KEEP=_KEEP): + """Return a deep copy of self with the given attributes replaced. + """ + if deep: + result = copy.deepcopy(self) + else: + result = copy.copy(self) + if name is not _KEEP: + result.name = name + if mtime is not _KEEP: + result.mtime = mtime + if mode is not _KEEP: + result.mode = mode + if linkname is not _KEEP: + result.linkname = linkname + if uid is not _KEEP: + result.uid = uid + if gid is not _KEEP: + result.gid = gid + if uname is not _KEEP: + result.uname = uname + if gname is not _KEEP: + result.gname = gname + return result + + def get_info(self): + """Return the TarInfo's attributes as a dictionary. + """ + if self.mode is None: + mode = None + else: + mode = self.mode & 0o7777 + info = { + "name": self.name, + "mode": mode, + "uid": self.uid, + "gid": self.gid, + "size": self.size, + "mtime": self.mtime, + "chksum": self.chksum, + "type": self.type, + "linkname": self.linkname, + "uname": self.uname, + "gname": self.gname, + "devmajor": self.devmajor, + "devminor": self.devminor + } + + if info["type"] == DIRTYPE and not info["name"].endswith("/"): + info["name"] += "/" + + return info + + def tobuf(self, format=DEFAULT_FORMAT, encoding=ENCODING, errors="surrogateescape"): + """Return a tar header as a string of 512 byte blocks. + """ + info = self.get_info() + for name, value in info.items(): + if value is None: + raise ValueError("%s may not be None" % name) + + if format == USTAR_FORMAT: + return self.create_ustar_header(info, encoding, errors) + elif format == GNU_FORMAT: + return self.create_gnu_header(info, encoding, errors) + elif format == PAX_FORMAT: + return self.create_pax_header(info, encoding) + else: + raise ValueError("invalid format") + + def create_ustar_header(self, info, encoding, errors): + """Return the object as a ustar header block. + """ + info["magic"] = POSIX_MAGIC + + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + raise ValueError("linkname is too long") + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + info["prefix"], info["name"] = self._posix_split_name(info["name"], encoding, errors) + + return self._create_header(info, USTAR_FORMAT, encoding, errors) + + def create_gnu_header(self, info, encoding, errors): + """Return the object as a GNU header block sequence. + """ + info["magic"] = GNU_MAGIC + + buf = b"" + if len(info["linkname"].encode(encoding, errors)) > LENGTH_LINK: + buf += self._create_gnu_long_header(info["linkname"], GNUTYPE_LONGLINK, encoding, errors) + + if len(info["name"].encode(encoding, errors)) > LENGTH_NAME: + buf += self._create_gnu_long_header(info["name"], GNUTYPE_LONGNAME, encoding, errors) + + return buf + self._create_header(info, GNU_FORMAT, encoding, errors) + + def create_pax_header(self, info, encoding): + """Return the object as a ustar header block. If it cannot be + represented this way, prepend a pax extended header sequence + with supplement information. + """ + info["magic"] = POSIX_MAGIC + pax_headers = self.pax_headers.copy() + + # Test string fields for values that exceed the field length or cannot + # be represented in ASCII encoding. + for name, hname, length in ( + ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK), + ("uname", "uname", 32), ("gname", "gname", 32)): + + if hname in pax_headers: + # The pax header has priority. + continue + + # Try to encode the string as ASCII. + try: + info[name].encode("ascii", "strict") + except UnicodeEncodeError: + pax_headers[hname] = info[name] + continue + + if len(info[name]) > length: + pax_headers[hname] = info[name] + + # Test number fields for values that exceed the field limit or values + # that like to be stored as float. + for name, digits in (("uid", 8), ("gid", 8), ("size", 12), ("mtime", 12)): + needs_pax = False + + val = info[name] + val_is_float = isinstance(val, float) + val_int = round(val) if val_is_float else val + if not 0 <= val_int < 8 ** (digits - 1): + # Avoid overflow. + info[name] = 0 + needs_pax = True + elif val_is_float: + # Put rounded value in ustar header, and full + # precision value in pax header. + info[name] = val_int + needs_pax = True + + # The existing pax header has priority. + if needs_pax and name not in pax_headers: + pax_headers[name] = str(val) + + # Create a pax extended header if necessary. + if pax_headers: + buf = self._create_pax_generic_header(pax_headers, XHDTYPE, encoding) + else: + buf = b"" + + return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace") + + @classmethod + def create_pax_global_header(cls, pax_headers): + """Return the object as a pax global header block sequence. + """ + return cls._create_pax_generic_header(pax_headers, XGLTYPE, "utf-8") + + def _posix_split_name(self, name, encoding, errors): + """Split a name longer than 100 chars into a prefix + and a name part. + """ + components = name.split("/") + for i in range(1, len(components)): + prefix = "/".join(components[:i]) + name = "/".join(components[i:]) + if len(prefix.encode(encoding, errors)) <= LENGTH_PREFIX and \ + len(name.encode(encoding, errors)) <= LENGTH_NAME: + break + else: + raise ValueError("name is too long") + + return prefix, name + + @staticmethod + def _create_header(info, format, encoding, errors): + """Return a header block. info is a dictionary with file + information, format must be one of the *_FORMAT constants. + """ + has_device_fields = info.get("type") in (CHRTYPE, BLKTYPE) + if has_device_fields: + devmajor = itn(info.get("devmajor", 0), 8, format) + devminor = itn(info.get("devminor", 0), 8, format) + else: + devmajor = stn("", 8, encoding, errors) + devminor = stn("", 8, encoding, errors) + + # None values in metadata should cause ValueError. + # itn()/stn() do this for all fields except type. + filetype = info.get("type", REGTYPE) + if filetype is None: + raise ValueError("TarInfo.type must not be None") + + parts = [ + stn(info.get("name", ""), 100, encoding, errors), + itn(info.get("mode", 0) & 0o7777, 8, format), + itn(info.get("uid", 0), 8, format), + itn(info.get("gid", 0), 8, format), + itn(info.get("size", 0), 12, format), + itn(info.get("mtime", 0), 12, format), + b" ", # checksum field + filetype, + stn(info.get("linkname", ""), 100, encoding, errors), + info.get("magic", POSIX_MAGIC), + stn(info.get("uname", ""), 32, encoding, errors), + stn(info.get("gname", ""), 32, encoding, errors), + devmajor, + devminor, + stn(info.get("prefix", ""), 155, encoding, errors) + ] + + buf = struct.pack("%ds" % BLOCKSIZE, b"".join(parts)) + chksum = calc_chksums(buf[-BLOCKSIZE:])[0] + buf = buf[:-364] + bytes("%06o\0" % chksum, "ascii") + buf[-357:] + return buf + + @staticmethod + def _create_payload(payload): + """Return the string payload filled with zero bytes + up to the next 512 byte border. + """ + blocks, remainder = divmod(len(payload), BLOCKSIZE) + if remainder > 0: + payload += (BLOCKSIZE - remainder) * NUL + return payload + + @classmethod + def _create_gnu_long_header(cls, name, type, encoding, errors): + """Return a GNUTYPE_LONGNAME or GNUTYPE_LONGLINK sequence + for name. + """ + name = name.encode(encoding, errors) + NUL + + info = {} + info["name"] = "././@LongLink" + info["type"] = type + info["size"] = len(name) + info["magic"] = GNU_MAGIC + + # create extended header + name blocks. + return cls._create_header(info, USTAR_FORMAT, encoding, errors) + \ + cls._create_payload(name) + + @classmethod + def _create_pax_generic_header(cls, pax_headers, type, encoding): + """Return a POSIX.1-2008 extended or global header sequence + that contains a list of keyword, value pairs. The values + must be strings. + """ + # Check if one of the fields contains surrogate characters and thereby + # forces hdrcharset=BINARY, see _proc_pax() for more information. + binary = False + for keyword, value in pax_headers.items(): + try: + value.encode("utf-8", "strict") + except UnicodeEncodeError: + binary = True + break + + records = b"" + if binary: + # Put the hdrcharset field at the beginning of the header. + records += b"21 hdrcharset=BINARY\n" + + for keyword, value in pax_headers.items(): + keyword = keyword.encode("utf-8") + if binary: + # Try to restore the original byte representation of `value'. + # Needless to say, that the encoding must match the string. + value = value.encode(encoding, "surrogateescape") + else: + value = value.encode("utf-8") + + l = len(keyword) + len(value) + 3 # ' ' + '=' + '\n' + n = p = 0 + while True: + n = l + len(str(p)) + if n == p: + break + p = n + records += bytes(str(p), "ascii") + b" " + keyword + b"=" + value + b"\n" + + # We use a hardcoded "././@PaxHeader" name like star does + # instead of the one that POSIX recommends. + info = {} + info["name"] = "././@PaxHeader" + info["type"] = type + info["size"] = len(records) + info["magic"] = POSIX_MAGIC + + # Create pax header + record blocks. + return cls._create_header(info, USTAR_FORMAT, "ascii", "replace") + \ + cls._create_payload(records) + + @classmethod + def frombuf(cls, buf, encoding, errors): + """Construct a TarInfo object from a 512 byte bytes object. + """ + if len(buf) == 0: + raise EmptyHeaderError("empty header") + if len(buf) != BLOCKSIZE: + raise TruncatedHeaderError("truncated header") + if buf.count(NUL) == BLOCKSIZE: + raise EOFHeaderError("end of file header") + + chksum = nti(buf[148:156]) + if chksum not in calc_chksums(buf): + raise InvalidHeaderError("bad checksum") + + obj = cls() + obj.name = nts(buf[0:100], encoding, errors) + obj.mode = nti(buf[100:108]) + obj.uid = nti(buf[108:116]) + obj.gid = nti(buf[116:124]) + obj.size = nti(buf[124:136]) + obj.mtime = nti(buf[136:148]) + obj.chksum = chksum + obj.type = buf[156:157] + obj.linkname = nts(buf[157:257], encoding, errors) + obj.uname = nts(buf[265:297], encoding, errors) + obj.gname = nts(buf[297:329], encoding, errors) + obj.devmajor = nti(buf[329:337]) + obj.devminor = nti(buf[337:345]) + prefix = nts(buf[345:500], encoding, errors) + + # Old V7 tar format represents a directory as a regular + # file with a trailing slash. + if obj.type == AREGTYPE and obj.name.endswith("/"): + obj.type = DIRTYPE + + # The old GNU sparse format occupies some of the unused + # space in the buffer for up to 4 sparse structures. + # Save them for later processing in _proc_sparse(). + if obj.type == GNUTYPE_SPARSE: + pos = 386 + structs = [] + for i in range(4): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[482]) + origsize = nti(buf[483:495]) + obj._sparse_structs = (structs, isextended, origsize) + + # Remove redundant slashes from directories. + if obj.isdir(): + obj.name = obj.name.rstrip("/") + + # Reconstruct a ustar longname. + if prefix and obj.type not in GNU_TYPES: + obj.name = prefix + "/" + obj.name + return obj + + @classmethod + def fromtarfile(cls, tarfile): + """Return the next TarInfo object from TarFile object + tarfile. + """ + buf = tarfile.fileobj.read(BLOCKSIZE) + obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors) + obj.offset = tarfile.fileobj.tell() - BLOCKSIZE + return obj._proc_member(tarfile) + + #-------------------------------------------------------------------------- + # The following are methods that are called depending on the type of a + # member. The entry point is _proc_member() which can be overridden in a + # subclass to add custom _proc_*() methods. A _proc_*() method MUST + # implement the following + # operations: + # 1. Set self.offset_data to the position where the data blocks begin, + # if there is data that follows. + # 2. Set tarfile.offset to the position where the next member's header will + # begin. + # 3. Return self or another valid TarInfo object. + def _proc_member(self, tarfile): + """Choose the right processing method depending on + the type and call it. + """ + if self.type in (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK): + return self._proc_gnulong(tarfile) + elif self.type == GNUTYPE_SPARSE: + return self._proc_sparse(tarfile) + elif self.type in (XHDTYPE, XGLTYPE, SOLARIS_XHDTYPE): + return self._proc_pax(tarfile) + else: + return self._proc_builtin(tarfile) + + def _proc_builtin(self, tarfile): + """Process a builtin type or an unknown type which + will be treated as a regular file. + """ + self.offset_data = tarfile.fileobj.tell() + offset = self.offset_data + if self.isreg() or self.type not in SUPPORTED_TYPES: + # Skip the following data blocks. + offset += self._block(self.size) + tarfile.offset = offset + + # Patch the TarInfo object with saved global + # header information. + self._apply_pax_info(tarfile.pax_headers, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if self.isdir(): + self.name = self.name.rstrip("/") + + return self + + def _proc_gnulong(self, tarfile): + """Process the blocks that hold a GNU longname + or longlink member. + """ + buf = tarfile.fileobj.read(self._block(self.size)) + + # Fetch the next header and process it. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Patch the TarInfo object from the next header with + # the longname information. + next.offset = self.offset + if self.type == GNUTYPE_LONGNAME: + next.name = nts(buf, tarfile.encoding, tarfile.errors) + elif self.type == GNUTYPE_LONGLINK: + next.linkname = nts(buf, tarfile.encoding, tarfile.errors) + + # Remove redundant slashes from directories. This is to be consistent + # with frombuf(). + if next.isdir(): + next.name = next.name.removesuffix("/") + + return next + + def _proc_sparse(self, tarfile): + """Process a GNU sparse header plus extra headers. + """ + # We already collected some sparse structures in frombuf(). + structs, isextended, origsize = self._sparse_structs + del self._sparse_structs + + # Collect sparse structures from extended header blocks. + while isextended: + buf = tarfile.fileobj.read(BLOCKSIZE) + pos = 0 + for i in range(21): + try: + offset = nti(buf[pos:pos + 12]) + numbytes = nti(buf[pos + 12:pos + 24]) + except ValueError: + break + if offset and numbytes: + structs.append((offset, numbytes)) + pos += 24 + isextended = bool(buf[504]) + self.sparse = structs + + self.offset_data = tarfile.fileobj.tell() + tarfile.offset = self.offset_data + self._block(self.size) + self.size = origsize + return self + + def _proc_pax(self, tarfile): + """Process an extended or global header as described in + POSIX.1-2008. + """ + # Read the header information. + buf = tarfile.fileobj.read(self._block(self.size)) + + # A pax header stores supplemental information for either + # the following file (extended) or all following files + # (global). + if self.type == XGLTYPE: + pax_headers = tarfile.pax_headers + else: + pax_headers = tarfile.pax_headers.copy() + + # Check if the pax header contains a hdrcharset field. This tells us + # the encoding of the path, linkpath, uname and gname fields. Normally, + # these fields are UTF-8 encoded but since POSIX.1-2008 tar + # implementations are allowed to store them as raw binary strings if + # the translation to UTF-8 fails. + match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf) + if match is not None: + pax_headers["hdrcharset"] = match.group(1).decode("utf-8") + + # For the time being, we don't care about anything other than "BINARY". + # The only other value that is currently allowed by the standard is + # "ISO-IR 10646 2000 UTF-8" in other words UTF-8. + hdrcharset = pax_headers.get("hdrcharset") + if hdrcharset == "BINARY": + encoding = tarfile.encoding + else: + encoding = "utf-8" + + # Parse pax header information. A record looks like that: + # "%d %s=%s\n" % (length, keyword, value). length is the size + # of the complete record including the length field itself and + # the newline. keyword and value are both UTF-8 encoded strings. + regex = re.compile(br"(\d+) ([^=]+)=") + pos = 0 + while match := regex.match(buf, pos): + length, keyword = match.groups() + length = int(length) + if length == 0: + raise InvalidHeaderError("invalid header") + value = buf[match.end(2) + 1:match.start(1) + length - 1] + + # Normally, we could just use "utf-8" as the encoding and "strict" + # as the error handler, but we better not take the risk. For + # example, GNU tar <= 1.23 is known to store filenames it cannot + # translate to UTF-8 as raw strings (unfortunately without a + # hdrcharset=BINARY header). + # We first try the strict standard encoding, and if that fails we + # fall back on the user's encoding and error handler. + keyword = self._decode_pax_field(keyword, "utf-8", "utf-8", + tarfile.errors) + if keyword in PAX_NAME_FIELDS: + value = self._decode_pax_field(value, encoding, tarfile.encoding, + tarfile.errors) + else: + value = self._decode_pax_field(value, "utf-8", "utf-8", + tarfile.errors) + + pax_headers[keyword] = value + pos += length + + # Fetch the next header. + try: + next = self.fromtarfile(tarfile) + except HeaderError as e: + raise SubsequentHeaderError(str(e)) from None + + # Process GNU sparse information. + if "GNU.sparse.map" in pax_headers: + # GNU extended sparse format version 0.1. + self._proc_gnusparse_01(next, pax_headers) + + elif "GNU.sparse.size" in pax_headers: + # GNU extended sparse format version 0.0. + self._proc_gnusparse_00(next, pax_headers, buf) + + elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0": + # GNU extended sparse format version 1.0. + self._proc_gnusparse_10(next, pax_headers, tarfile) + + if self.type in (XHDTYPE, SOLARIS_XHDTYPE): + # Patch the TarInfo object with the extended header info. + next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors) + next.offset = self.offset + + if "size" in pax_headers: + # If the extended header replaces the size field, + # we need to recalculate the offset where the next + # header starts. + offset = next.offset_data + if next.isreg() or next.type not in SUPPORTED_TYPES: + offset += next._block(next.size) + tarfile.offset = offset + + return next + + def _proc_gnusparse_00(self, next, pax_headers, buf): + """Process a GNU tar extended sparse header, version 0.0. + """ + offsets = [] + for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf): + offsets.append(int(match.group(1))) + numbytes = [] + for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf): + numbytes.append(int(match.group(1))) + next.sparse = list(zip(offsets, numbytes)) + + def _proc_gnusparse_01(self, next, pax_headers): + """Process a GNU tar extended sparse header, version 0.1. + """ + sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")] + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _proc_gnusparse_10(self, next, pax_headers, tarfile): + """Process a GNU tar extended sparse header, version 1.0. + """ + fields = None + sparse = [] + buf = tarfile.fileobj.read(BLOCKSIZE) + fields, buf = buf.split(b"\n", 1) + fields = int(fields) + while len(sparse) < fields * 2: + if b"\n" not in buf: + buf += tarfile.fileobj.read(BLOCKSIZE) + number, buf = buf.split(b"\n", 1) + sparse.append(int(number)) + next.offset_data = tarfile.fileobj.tell() + next.sparse = list(zip(sparse[::2], sparse[1::2])) + + def _apply_pax_info(self, pax_headers, encoding, errors): + """Replace fields with supplemental information from a previous + pax extended or global header. + """ + for keyword, value in pax_headers.items(): + if keyword == "GNU.sparse.name": + setattr(self, "path", value) + elif keyword == "GNU.sparse.size": + setattr(self, "size", int(value)) + elif keyword == "GNU.sparse.realsize": + setattr(self, "size", int(value)) + elif keyword in PAX_FIELDS: + if keyword in PAX_NUMBER_FIELDS: + try: + value = PAX_NUMBER_FIELDS[keyword](value) + except ValueError: + value = 0 + if keyword == "path": + value = value.rstrip("/") + setattr(self, keyword, value) + + self.pax_headers = pax_headers.copy() + + def _decode_pax_field(self, value, encoding, fallback_encoding, fallback_errors): + """Decode a single field from a pax record. + """ + try: + return value.decode(encoding, "strict") + except UnicodeDecodeError: + return value.decode(fallback_encoding, fallback_errors) + + def _block(self, count): + """Round up a byte count by BLOCKSIZE and return it, + e.g. _block(834) => 1024. + """ + blocks, remainder = divmod(count, BLOCKSIZE) + if remainder: + blocks += 1 + return blocks * BLOCKSIZE + + def isreg(self): + 'Return True if the Tarinfo object is a regular file.' + return self.type in REGULAR_TYPES + + def isfile(self): + 'Return True if the Tarinfo object is a regular file.' + return self.isreg() + + def isdir(self): + 'Return True if it is a directory.' + return self.type == DIRTYPE + + def issym(self): + 'Return True if it is a symbolic link.' + return self.type == SYMTYPE + + def islnk(self): + 'Return True if it is a hard link.' + return self.type == LNKTYPE + + def ischr(self): + 'Return True if it is a character device.' + return self.type == CHRTYPE + + def isblk(self): + 'Return True if it is a block device.' + return self.type == BLKTYPE + + def isfifo(self): + 'Return True if it is a FIFO.' + return self.type == FIFOTYPE + + def issparse(self): + return self.sparse is not None + + def isdev(self): + 'Return True if it is one of character device, block device or FIFO.' + return self.type in (CHRTYPE, BLKTYPE, FIFOTYPE) +# class TarInfo + +class TarFile(object): + """The TarFile Class provides an interface to tar archives. + """ + + debug = 0 # May be set from 0 (no msgs) to 3 (all msgs) + + dereference = False # If true, add content of linked file to the + # tar file, else the link. + + ignore_zeros = False # If true, skips empty or invalid blocks and + # continues processing. + + errorlevel = 1 # If 0, fatal errors only appear in debug + # messages (if debug >= 0). If > 0, errors + # are passed to the caller as exceptions. + + format = DEFAULT_FORMAT # The format to use when creating an archive. + + encoding = ENCODING # Encoding for 8-bit character strings. + + errors = None # Error handler for unicode conversion. + + tarinfo = TarInfo # The default TarInfo class to use. + + fileobject = ExFileObject # The file-object for extractfile(). + + extraction_filter = None # The default filter for extraction. + + def __init__(self, name=None, mode="r", fileobj=None, format=None, + tarinfo=None, dereference=None, ignore_zeros=None, encoding=None, + errors="surrogateescape", pax_headers=None, debug=None, + errorlevel=None, copybufsize=None): + """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to + read from an existing archive, 'a' to append data to an existing + file or 'w' to create a new file overwriting an existing one. `mode' + defaults to 'r'. + If `fileobj' is given, it is used for reading or writing data. If it + can be determined, `mode' is overridden by `fileobj's mode. + `fileobj' is not closed, when TarFile is closed. + """ + modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"} + if mode not in modes: + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + self.mode = mode + self._mode = modes[mode] + + if not fileobj: + if self.mode == "a" and not os.path.exists(name): + # Create nonexistent files in append mode. + self.mode = "w" + self._mode = "wb" + fileobj = bltn_open(name, self._mode) + self._extfileobj = False + else: + if (name is None and hasattr(fileobj, "name") and + isinstance(fileobj.name, (str, bytes))): + name = fileobj.name + if hasattr(fileobj, "mode"): + self._mode = fileobj.mode + self._extfileobj = True + self.name = os.path.abspath(name) if name else None + self.fileobj = fileobj + + # Init attributes. + if format is not None: + self.format = format + if tarinfo is not None: + self.tarinfo = tarinfo + if dereference is not None: + self.dereference = dereference + if ignore_zeros is not None: + self.ignore_zeros = ignore_zeros + if encoding is not None: + self.encoding = encoding + self.errors = errors + + if pax_headers is not None and self.format == PAX_FORMAT: + self.pax_headers = pax_headers + else: + self.pax_headers = {} + + if debug is not None: + self.debug = debug + if errorlevel is not None: + self.errorlevel = errorlevel + + # Init datastructures. + self.copybufsize = copybufsize + self.closed = False + self.members = [] # list of members as TarInfo objects + self._loaded = False # flag if all members have been read + self.offset = self.fileobj.tell() + # current position in the archive file + self.inodes = {} # dictionary caching the inodes of + # archive members already added + + try: + if self.mode == "r": + self.firstmember = None + self.firstmember = self.next() + + if self.mode == "a": + # Move to the end of the archive, + # before the first empty block. + while True: + self.fileobj.seek(self.offset) + try: + tarinfo = self.tarinfo.fromtarfile(self) + self.members.append(tarinfo) + except EOFHeaderError: + self.fileobj.seek(self.offset) + break + except HeaderError as e: + raise ReadError(str(e)) from None + + if self.mode in ("a", "w", "x"): + self._loaded = True + + if self.pax_headers: + buf = self.tarinfo.create_pax_global_header(self.pax_headers.copy()) + self.fileobj.write(buf) + self.offset += len(buf) + except: + if not self._extfileobj: + self.fileobj.close() + self.closed = True + raise + + #-------------------------------------------------------------------------- + # Below are the classmethods which act as alternate constructors to the + # TarFile class. The open() method is the only one that is needed for + # public use; it is the "super"-constructor and is able to select an + # adequate "sub"-constructor for a particular compression using the mapping + # from OPEN_METH. + # + # This concept allows one to subclass TarFile without losing the comfort of + # the super-constructor. A sub-constructor is registered and made available + # by adding it to the mapping in OPEN_METH. + + @classmethod + def open(cls, name=None, mode="r", fileobj=None, bufsize=RECORDSIZE, **kwargs): + r"""Open a tar archive for reading, writing or appending. Return + an appropriate TarFile class. + + mode: + 'r' or 'r:\*' open for reading with transparent compression + 'r:' open for reading exclusively uncompressed + 'r:gz' open for reading with gzip compression + 'r:bz2' open for reading with bzip2 compression + 'r:xz' open for reading with lzma compression + 'a' or 'a:' open for appending, creating the file if necessary + 'w' or 'w:' open for writing without compression + 'w:gz' open for writing with gzip compression + 'w:bz2' open for writing with bzip2 compression + 'w:xz' open for writing with lzma compression + + 'x' or 'x:' create a tarfile exclusively without compression, raise + an exception if the file is already created + 'x:gz' create a gzip compressed tarfile, raise an exception + if the file is already created + 'x:bz2' create a bzip2 compressed tarfile, raise an exception + if the file is already created + 'x:xz' create an lzma compressed tarfile, raise an exception + if the file is already created + + 'r|\*' open a stream of tar blocks with transparent compression + 'r|' open an uncompressed stream of tar blocks for reading + 'r|gz' open a gzip compressed stream of tar blocks + 'r|bz2' open a bzip2 compressed stream of tar blocks + 'r|xz' open an lzma compressed stream of tar blocks + 'w|' open an uncompressed stream for writing + 'w|gz' open a gzip compressed stream for writing + 'w|bz2' open a bzip2 compressed stream for writing + 'w|xz' open an lzma compressed stream for writing + """ + + if not name and not fileobj: + raise ValueError("nothing to open") + + if mode in ("r", "r:*"): + # Find out which *open() is appropriate for opening the file. + def not_compressed(comptype): + return cls.OPEN_METH[comptype] == 'taropen' + error_msgs = [] + for comptype in sorted(cls.OPEN_METH, key=not_compressed): + func = getattr(cls, cls.OPEN_METH[comptype]) + if fileobj is not None: + saved_pos = fileobj.tell() + try: + return func(name, "r", fileobj, **kwargs) + except (ReadError, CompressionError) as e: + error_msgs.append(f'- method {comptype}: {e!r}') + if fileobj is not None: + fileobj.seek(saved_pos) + continue + error_msgs_summary = '\n'.join(error_msgs) + raise ReadError(f"file could not be opened successfully:\n{error_msgs_summary}") + + elif ":" in mode: + filemode, comptype = mode.split(":", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + # Select the *open() function according to + # given compression. + if comptype in cls.OPEN_METH: + func = getattr(cls, cls.OPEN_METH[comptype]) + else: + raise CompressionError("unknown compression type %r" % comptype) + return func(name, filemode, fileobj, **kwargs) + + elif "|" in mode: + filemode, comptype = mode.split("|", 1) + filemode = filemode or "r" + comptype = comptype or "tar" + + if filemode not in ("r", "w"): + raise ValueError("mode must be 'r' or 'w'") + + compresslevel = kwargs.pop("compresslevel", 9) + stream = _Stream(name, filemode, comptype, fileobj, bufsize, + compresslevel) + try: + t = cls(name, filemode, stream, **kwargs) + except: + stream.close() + raise + t._extfileobj = False + return t + + elif mode in ("a", "w", "x"): + return cls.taropen(name, mode, fileobj, **kwargs) + + raise ValueError("undiscernible mode") + + @classmethod + def taropen(cls, name, mode="r", fileobj=None, **kwargs): + """Open uncompressed tar archive name for reading or writing. + """ + if mode not in ("r", "a", "w", "x"): + raise ValueError("mode must be 'r', 'a', 'w' or 'x'") + return cls(name, mode, fileobj, **kwargs) + + @classmethod + def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open gzip compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from gzip import GzipFile + except ImportError: + raise CompressionError("gzip module is not available") from None + + try: + fileobj = GzipFile(name, mode + "b", compresslevel, fileobj) + except OSError as e: + if fileobj is not None and mode == 'r': + raise ReadError("not a gzip file") from e + raise + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except OSError as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a gzip file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + """Open bzip2 compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from bz2 import BZ2File + except ImportError: + raise CompressionError("bz2 module is not available") from None + + fileobj = BZ2File(fileobj or name, mode, compresslevel=compresslevel) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (OSError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not a bzip2 file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + @classmethod + def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs): + """Open lzma compressed tar archive name for reading or writing. + Appending is not allowed. + """ + if mode not in ("r", "w", "x"): + raise ValueError("mode must be 'r', 'w' or 'x'") + + try: + from lzma import LZMAFile, LZMAError + except ImportError: + raise CompressionError("lzma module is not available") from None + + fileobj = LZMAFile(fileobj or name, mode, preset=preset) + + try: + t = cls.taropen(name, mode, fileobj, **kwargs) + except (LZMAError, EOFError) as e: + fileobj.close() + if mode == 'r': + raise ReadError("not an lzma file") from e + raise + except: + fileobj.close() + raise + t._extfileobj = False + return t + + # All *open() methods are registered here. + OPEN_METH = { + "tar": "taropen", # uncompressed tar + "gz": "gzopen", # gzip compressed tar + "bz2": "bz2open", # bzip2 compressed tar + "xz": "xzopen" # lzma compressed tar + } + + #-------------------------------------------------------------------------- + # The public methods which TarFile provides: + + def close(self): + """Close the TarFile. In write-mode, two finishing zero blocks are + appended to the archive. + """ + if self.closed: + return + + self.closed = True + try: + if self.mode in ("a", "w", "x"): + self.fileobj.write(NUL * (BLOCKSIZE * 2)) + self.offset += (BLOCKSIZE * 2) + # fill up the end with zero-blocks + # (like option -b20 for tar does) + blocks, remainder = divmod(self.offset, RECORDSIZE) + if remainder > 0: + self.fileobj.write(NUL * (RECORDSIZE - remainder)) + finally: + if not self._extfileobj: + self.fileobj.close() + + def getmember(self, name): + """Return a TarInfo object for member ``name``. If ``name`` can not be + found in the archive, KeyError is raised. If a member occurs more + than once in the archive, its last occurrence is assumed to be the + most up-to-date version. + """ + tarinfo = self._getmember(name.rstrip('/')) + if tarinfo is None: + raise KeyError("filename %r not found" % name) + return tarinfo + + def getmembers(self): + """Return the members of the archive as a list of TarInfo objects. The + list has the same order as the members in the archive. + """ + self._check() + if not self._loaded: # if we want to obtain a list of + self._load() # all members, we first have to + # scan the whole archive. + return self.members + + def getnames(self): + """Return the members of the archive as a list of their names. It has + the same order as the list returned by getmembers(). + """ + return [tarinfo.name for tarinfo in self.getmembers()] + + def gettarinfo(self, name=None, arcname=None, fileobj=None): + """Create a TarInfo object from the result of os.stat or equivalent + on an existing file. The file is either named by ``name``, or + specified as a file object ``fileobj`` with a file descriptor. If + given, ``arcname`` specifies an alternative name for the file in the + archive, otherwise, the name is taken from the 'name' attribute of + 'fileobj', or the 'name' argument. The name should be a text + string. + """ + self._check("awx") + + # When fileobj is given, replace name by + # fileobj's real name. + if fileobj is not None: + name = fileobj.name + + # Building the name of the member in the archive. + # Backward slashes are converted to forward slashes, + # Absolute paths are turned to relative paths. + if arcname is None: + arcname = name + drv, arcname = os.path.splitdrive(arcname) + arcname = arcname.replace(os.sep, "/") + arcname = arcname.lstrip("/") + + # Now, fill the TarInfo object with + # information specific for the file. + tarinfo = self.tarinfo() + tarinfo.tarfile = self # Not needed + + # Use os.stat or os.lstat, depending on if symlinks shall be resolved. + if fileobj is None: + if not self.dereference: + statres = os.lstat(name) + else: + statres = os.stat(name) + else: + statres = os.fstat(fileobj.fileno()) + linkname = "" + + stmd = statres.st_mode + if stat.S_ISREG(stmd): + inode = (statres.st_ino, statres.st_dev) + if not self.dereference and statres.st_nlink > 1 and \ + inode in self.inodes and arcname != self.inodes[inode]: + # Is it a hardlink to an already + # archived file? + type = LNKTYPE + linkname = self.inodes[inode] + else: + # The inode is added only if its valid. + # For win32 it is always 0. + type = REGTYPE + if inode[0]: + self.inodes[inode] = arcname + elif stat.S_ISDIR(stmd): + type = DIRTYPE + elif stat.S_ISFIFO(stmd): + type = FIFOTYPE + elif stat.S_ISLNK(stmd): + type = SYMTYPE + linkname = os.readlink(name) + elif stat.S_ISCHR(stmd): + type = CHRTYPE + elif stat.S_ISBLK(stmd): + type = BLKTYPE + else: + return None + + # Fill the TarInfo object with all + # information we can get. + tarinfo.name = arcname + tarinfo.mode = stmd + tarinfo.uid = statres.st_uid + tarinfo.gid = statres.st_gid + if type == REGTYPE: + tarinfo.size = statres.st_size + else: + tarinfo.size = 0 + tarinfo.mtime = statres.st_mtime + tarinfo.type = type + tarinfo.linkname = linkname + if pwd: + try: + tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0] + except KeyError: + pass + if grp: + try: + tarinfo.gname = grp.getgrgid(tarinfo.gid)[0] + except KeyError: + pass + + if type in (CHRTYPE, BLKTYPE): + if hasattr(os, "major") and hasattr(os, "minor"): + tarinfo.devmajor = os.major(statres.st_rdev) + tarinfo.devminor = os.minor(statres.st_rdev) + return tarinfo + + def list(self, verbose=True, *, members=None): + """Print a table of contents to sys.stdout. If ``verbose`` is False, only + the names of the members are printed. If it is True, an `ls -l'-like + output is produced. ``members`` is optional and must be a subset of the + list returned by getmembers(). + """ + self._check() + + if members is None: + members = self + for tarinfo in members: + if verbose: + if tarinfo.mode is None: + _safe_print("??????????") + else: + _safe_print(stat.filemode(tarinfo.mode)) + _safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid, + tarinfo.gname or tarinfo.gid)) + if tarinfo.ischr() or tarinfo.isblk(): + _safe_print("%10s" % + ("%d,%d" % (tarinfo.devmajor, tarinfo.devminor))) + else: + _safe_print("%10d" % tarinfo.size) + if tarinfo.mtime is None: + _safe_print("????-??-?? ??:??:??") + else: + _safe_print("%d-%02d-%02d %02d:%02d:%02d" \ + % time.localtime(tarinfo.mtime)[:6]) + + _safe_print(tarinfo.name + ("/" if tarinfo.isdir() else "")) + + if verbose: + if tarinfo.issym(): + _safe_print("-> " + tarinfo.linkname) + if tarinfo.islnk(): + _safe_print("link to " + tarinfo.linkname) + print() + + def add(self, name, arcname=None, recursive=True, *, filter=None): + """Add the file ``name`` to the archive. ``name`` may be any type of file + (directory, fifo, symbolic link, etc.). If given, ``arcname`` + specifies an alternative name for the file in the archive. + Directories are added recursively by default. This can be avoided by + setting ``recursive`` to False. ``filter`` is a function + that expects a TarInfo object argument and returns the changed + TarInfo object, if it returns None the TarInfo object will be + excluded from the archive. + """ + self._check("awx") + + if arcname is None: + arcname = name + + # Skip if somebody tries to archive the archive... + if self.name is not None and os.path.abspath(name) == self.name: + self._dbg(2, "tarfile: Skipped %r" % name) + return + + self._dbg(1, name) + + # Create a TarInfo object from the file. + tarinfo = self.gettarinfo(name, arcname) + + if tarinfo is None: + self._dbg(1, "tarfile: Unsupported type %r" % name) + return + + # Change or exclude the TarInfo object. + if filter is not None: + tarinfo = filter(tarinfo) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % name) + return + + # Append the tar header and data to the archive. + if tarinfo.isreg(): + with bltn_open(name, "rb") as f: + self.addfile(tarinfo, f) + + elif tarinfo.isdir(): + self.addfile(tarinfo) + if recursive: + for f in sorted(os.listdir(name)): + self.add(os.path.join(name, f), os.path.join(arcname, f), + recursive, filter=filter) + + else: + self.addfile(tarinfo) + + def addfile(self, tarinfo, fileobj=None): + """Add the TarInfo object ``tarinfo`` to the archive. If ``fileobj`` is + given, it should be a binary file, and tarinfo.size bytes are read + from it and added to the archive. You can create TarInfo objects + directly, or by using gettarinfo(). + """ + self._check("awx") + + tarinfo = copy.copy(tarinfo) + + buf = tarinfo.tobuf(self.format, self.encoding, self.errors) + self.fileobj.write(buf) + self.offset += len(buf) + bufsize=self.copybufsize + # If there's data to follow, append it. + if fileobj is not None: + copyfileobj(fileobj, self.fileobj, tarinfo.size, bufsize=bufsize) + blocks, remainder = divmod(tarinfo.size, BLOCKSIZE) + if remainder > 0: + self.fileobj.write(NUL * (BLOCKSIZE - remainder)) + blocks += 1 + self.offset += blocks * BLOCKSIZE + + self.members.append(tarinfo) + + def _get_filter_function(self, filter): + if filter is None: + filter = self.extraction_filter + if filter is None: + warnings.warn( + 'Python 3.14 will, by default, filter extracted tar ' + + 'archives and reject files or modify their metadata. ' + + 'Use the filter argument to control this behavior.', + DeprecationWarning) + return fully_trusted_filter + if isinstance(filter, str): + raise TypeError( + 'String names are not supported for ' + + 'TarFile.extraction_filter. Use a function such as ' + + 'tarfile.data_filter directly.') + return filter + if callable(filter): + return filter + try: + return _NAMED_FILTERS[filter] + except KeyError: + raise ValueError(f"filter {filter!r} not found") from None + + def extractall(self, path=".", members=None, *, numeric_owner=False, + filter=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). If `numeric_owner` is True, only + the numbers for user/group names are used and not the names. + + The `filter` function will be called on each member just + before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + directories = [] + + filter_function = self._get_filter_function(filter) + if members is None: + members = self + + for member in members: + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is None: + continue + if tarinfo.isdir(): + # For directories, delay setting attributes until later, + # since permissions can interfere with extraction and + # extracting contents can reset mtime. + directories.append(tarinfo) + self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(), + numeric_owner=numeric_owner) + + # Reverse sort directories. + directories.sort(key=lambda a: a.name, reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath, numeric_owner=numeric_owner) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, + filter=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a TarInfo object. You can + specify a different directory using `path'. File attributes (owner, + mtime, mode) are set unless `set_attrs' is False. If `numeric_owner` + is True, only the numbers for user/group names are used and not + the names. + + The `filter` function will be called before extraction. + It can return a changed TarInfo or None to skip the member. + String names of common filters are accepted. + """ + filter_function = self._get_filter_function(filter) + tarinfo = self._get_extract_tarinfo(member, filter_function, path) + if tarinfo is not None: + self._extract_one(tarinfo, path, set_attrs, numeric_owner) + + def _get_extract_tarinfo(self, member, filter_function, path): + """Get filtered TarInfo (or None) from member, which might be a str""" + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + unfiltered = tarinfo + try: + tarinfo = filter_function(tarinfo, path) + except (OSError, FilterError) as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + if tarinfo is None: + self._dbg(2, "tarfile: Excluded %r" % unfiltered.name) + return None + # Prepare the link target for makelink(). + if tarinfo.islnk(): + tarinfo = copy.copy(tarinfo) + tarinfo._link_target = os.path.join(path, tarinfo.linkname) + return tarinfo + + def _extract_one(self, tarinfo, path, set_attrs, numeric_owner): + """Extract from filtered tarinfo to disk""" + self._check("r") + + try: + self._extract_member(tarinfo, os.path.join(path, tarinfo.name), + set_attrs=set_attrs, + numeric_owner=numeric_owner) + except OSError as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) + + def _handle_nonfatal_error(self, e): + """Handle non-fatal error (ExtractError) according to errorlevel""" + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + def _handle_fatal_error(self, e): + """Handle "fatal" error according to self.errorlevel""" + if self.errorlevel > 0: + raise + elif isinstance(e, OSError): + if e.filename is None: + self._dbg(1, "tarfile: %s" % e.strerror) + else: + self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename)) + else: + self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e)) + + def extractfile(self, member): + """Extract a member from the archive as a file object. ``member`` may be + a filename or a TarInfo object. If ``member`` is a regular file or + a link, an io.BufferedReader object is returned. For all other + existing members, None is returned. If ``member`` does not appear + in the archive, KeyError is raised. + """ + self._check("r") + + if isinstance(member, str): + tarinfo = self.getmember(member) + else: + tarinfo = member + + if tarinfo.isreg() or tarinfo.type not in SUPPORTED_TYPES: + # Members with unknown types are treated as regular files. + return self.fileobject(self, tarinfo) + + elif tarinfo.islnk() or tarinfo.issym(): + if isinstance(self.fileobj, _Stream): + # A small but ugly workaround for the case that someone tries + # to extract a (sym)link as a file-object from a non-seekable + # stream of tar blocks. + raise StreamError("cannot extract (sym)link as file object") + else: + # A (sym)link's file object is its target's file object. + return self.extractfile(self._find_link_target(tarinfo)) + else: + # If there's no data associated with the member (directory, chrdev, + # blkdev, etc.), return None instead of a file object. + return None + + def _extract_member(self, tarinfo, targetpath, set_attrs=True, + numeric_owner=False): + """Extract the TarInfo object tarinfo to a physical + file called targetpath. + """ + # Fetch the TarInfo object for the given name + # and build the destination pathname, replacing + # forward slashes to platform specific separators. + targetpath = targetpath.rstrip("/") + targetpath = targetpath.replace("/", os.sep) + + # Create all upper directories. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + # Create directories that are not part of the archive with + # default permissions. + os.makedirs(upperdirs) + + if tarinfo.islnk() or tarinfo.issym(): + self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname)) + else: + self._dbg(1, tarinfo.name) + + if tarinfo.isreg(): + self.makefile(tarinfo, targetpath) + elif tarinfo.isdir(): + self.makedir(tarinfo, targetpath) + elif tarinfo.isfifo(): + self.makefifo(tarinfo, targetpath) + elif tarinfo.ischr() or tarinfo.isblk(): + self.makedev(tarinfo, targetpath) + elif tarinfo.islnk() or tarinfo.issym(): + self.makelink(tarinfo, targetpath) + elif tarinfo.type not in SUPPORTED_TYPES: + self.makeunknown(tarinfo, targetpath) + else: + self.makefile(tarinfo, targetpath) + + if set_attrs: + self.chown(tarinfo, targetpath, numeric_owner) + if not tarinfo.issym(): + self.chmod(tarinfo, targetpath) + self.utime(tarinfo, targetpath) + + #-------------------------------------------------------------------------- + # Below are the different file methods. They are called via + # _extract_member() when extract() is called. They can be replaced in a + # subclass to implement other functionality. + + def makedir(self, tarinfo, targetpath): + """Make a directory called targetpath. + """ + try: + if tarinfo.mode is None: + # Use the system's default mode + os.mkdir(targetpath) + else: + # Use a safe mode for the directory, the real mode is set + # later in _extract_member(). + os.mkdir(targetpath, 0o700) + except FileExistsError: + if not os.path.isdir(targetpath): + raise + + def makefile(self, tarinfo, targetpath): + """Make a file called targetpath. + """ + source = self.fileobj + source.seek(tarinfo.offset_data) + bufsize = self.copybufsize + with bltn_open(targetpath, "wb") as target: + if tarinfo.sparse is not None: + for offset, size in tarinfo.sparse: + target.seek(offset) + copyfileobj(source, target, size, ReadError, bufsize) + target.seek(tarinfo.size) + target.truncate() + else: + copyfileobj(source, target, tarinfo.size, ReadError, bufsize) + + def makeunknown(self, tarinfo, targetpath): + """Make a file from a TarInfo object with an unknown type + at targetpath. + """ + self.makefile(tarinfo, targetpath) + self._dbg(1, "tarfile: Unknown file type %r, " \ + "extracted as regular file." % tarinfo.type) + + def makefifo(self, tarinfo, targetpath): + """Make a fifo called targetpath. + """ + if hasattr(os, "mkfifo"): + os.mkfifo(targetpath) + else: + raise ExtractError("fifo not supported by system") + + def makedev(self, tarinfo, targetpath): + """Make a character or block device called targetpath. + """ + if not hasattr(os, "mknod") or not hasattr(os, "makedev"): + raise ExtractError("special devices not supported by system") + + mode = tarinfo.mode + if mode is None: + # Use mknod's default + mode = 0o600 + if tarinfo.isblk(): + mode |= stat.S_IFBLK + else: + mode |= stat.S_IFCHR + + os.mknod(targetpath, mode, + os.makedev(tarinfo.devmajor, tarinfo.devminor)) + + def makelink(self, tarinfo, targetpath): + """Make a (symbolic) link called targetpath. If it cannot be created + (platform limitation), we try to make a copy of the referenced file + instead of a link. + """ + try: + # For systems that support symbolic and hard links. + if tarinfo.issym(): + if os.path.lexists(targetpath): + # Avoid FileExistsError on following os.symlink. + os.unlink(targetpath) + os.symlink(tarinfo.linkname, targetpath) + else: + if os.path.exists(tarinfo._link_target): + os.link(tarinfo._link_target, targetpath) + else: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except symlink_exception: + try: + self._extract_member(self._find_link_target(tarinfo), + targetpath) + except KeyError: + raise ExtractError("unable to resolve link inside archive") from None + + def chown(self, tarinfo, targetpath, numeric_owner): + """Set owner of targetpath according to tarinfo. If numeric_owner + is True, use .gid/.uid instead of .gname/.uname. If numeric_owner + is False, fall back to .gid/.uid when the search based on name + fails. + """ + if hasattr(os, "geteuid") and os.geteuid() == 0: + # We have to be root to do so. + g = tarinfo.gid + u = tarinfo.uid + if not numeric_owner: + try: + if grp and tarinfo.gname: + g = grp.getgrnam(tarinfo.gname)[2] + except KeyError: + pass + try: + if pwd and tarinfo.uname: + u = pwd.getpwnam(tarinfo.uname)[2] + except KeyError: + pass + if g is None: + g = -1 + if u is None: + u = -1 + try: + if tarinfo.issym() and hasattr(os, "lchown"): + os.lchown(targetpath, u, g) + else: + os.chown(targetpath, u, g) + except OSError as e: + raise ExtractError("could not change owner") from e + + def chmod(self, tarinfo, targetpath): + """Set file permissions of targetpath according to tarinfo. + """ + if tarinfo.mode is None: + return + try: + os.chmod(targetpath, tarinfo.mode) + except OSError as e: + raise ExtractError("could not change mode") from e + + def utime(self, tarinfo, targetpath): + """Set modification time of targetpath according to tarinfo. + """ + mtime = tarinfo.mtime + if mtime is None: + return + if not hasattr(os, 'utime'): + return + try: + os.utime(targetpath, (mtime, mtime)) + except OSError as e: + raise ExtractError("could not change modification time") from e + + #-------------------------------------------------------------------------- + def next(self): + """Return the next member of the archive as a TarInfo object, when + TarFile is opened for reading. Return None if there is no more + available. + """ + self._check("ra") + if self.firstmember is not None: + m = self.firstmember + self.firstmember = None + return m + + # Advance the file pointer. + if self.offset != self.fileobj.tell(): + if self.offset == 0: + return None + self.fileobj.seek(self.offset - 1) + if not self.fileobj.read(1): + raise ReadError("unexpected end of data") + + # Read the next block. + tarinfo = None + while True: + try: + tarinfo = self.tarinfo.fromtarfile(self) + except EOFHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + except InvalidHeaderError as e: + if self.ignore_zeros: + self._dbg(2, "0x%X: %s" % (self.offset, e)) + self.offset += BLOCKSIZE + continue + elif self.offset == 0: + raise ReadError(str(e)) from None + except EmptyHeaderError: + if self.offset == 0: + raise ReadError("empty file") from None + except TruncatedHeaderError as e: + if self.offset == 0: + raise ReadError(str(e)) from None + except SubsequentHeaderError as e: + raise ReadError(str(e)) from None + except Exception as e: + try: + import zlib + if isinstance(e, zlib.error): + raise ReadError(f'zlib error: {e}') from None + else: + raise e + except ImportError: + raise e + break + + if tarinfo is not None: + self.members.append(tarinfo) + else: + self._loaded = True + + return tarinfo + + #-------------------------------------------------------------------------- + # Little helper methods: + + def _getmember(self, name, tarinfo=None, normalize=False): + """Find an archive member by name from bottom to top. + If tarinfo is given, it is used as the starting point. + """ + # Ensure that all members have been loaded. + members = self.getmembers() + + # Limit the member search list up to tarinfo. + skipping = False + if tarinfo is not None: + try: + index = members.index(tarinfo) + except ValueError: + # The given starting point might be a (modified) copy. + # We'll later skip members until we find an equivalent. + skipping = True + else: + # Happy fast path + members = members[:index] + + if normalize: + name = os.path.normpath(name) + + for member in reversed(members): + if skipping: + if tarinfo.offset == member.offset: + skipping = False + continue + if normalize: + member_name = os.path.normpath(member.name) + else: + member_name = member.name + + if name == member_name: + return member + + if skipping: + # Starting point was not found + raise ValueError(tarinfo) + + def _load(self): + """Read through the entire archive file and look for readable + members. + """ + while self.next() is not None: + pass + self._loaded = True + + def _check(self, mode=None): + """Check if TarFile is still open, and if the operation's mode + corresponds to TarFile's mode. + """ + if self.closed: + raise OSError("%s is closed" % self.__class__.__name__) + if mode is not None and self.mode not in mode: + raise OSError("bad operation for mode %r" % self.mode) + + def _find_link_target(self, tarinfo): + """Find the target member of a symlink or hardlink member in the + archive. + """ + if tarinfo.issym(): + # Always search the entire archive. + linkname = "/".join(filter(None, (os.path.dirname(tarinfo.name), tarinfo.linkname))) + limit = None + else: + # Search the archive before the link, because a hard link is + # just a reference to an already archived file. + linkname = tarinfo.linkname + limit = tarinfo + + member = self._getmember(linkname, tarinfo=limit, normalize=True) + if member is None: + raise KeyError("linkname %r not found" % linkname) + return member + + def __iter__(self): + """Provide an iterator object. + """ + if self._loaded: + yield from self.members + return + + # Yield items using TarFile's next() method. + # When all members have been read, set TarFile as _loaded. + index = 0 + # Fix for SF #1100429: Under rare circumstances it can + # happen that getmembers() is called during iteration, + # which will have already exhausted the next() method. + if self.firstmember is not None: + tarinfo = self.next() + index += 1 + yield tarinfo + + while True: + if index < len(self.members): + tarinfo = self.members[index] + elif not self._loaded: + tarinfo = self.next() + if not tarinfo: + self._loaded = True + return + else: + return + index += 1 + yield tarinfo + + def _dbg(self, level, msg): + """Write debugging output to sys.stderr. + """ + if level <= self.debug: + print(msg, file=sys.stderr) + + def __enter__(self): + self._check() + return self + + def __exit__(self, type, value, traceback): + if type is None: + self.close() + else: + # An exception occurred. We must not call close() because + # it would try to write end-of-archive blocks and padding. + if not self._extfileobj: + self.fileobj.close() + self.closed = True + +#-------------------- +# exported functions +#-------------------- + +def is_tarfile(name): + """Return True if name points to a tar archive that we + are able to handle, else return False. + + 'name' should be a string, file, or file-like object. + """ + try: + if hasattr(name, "read"): + pos = name.tell() + t = open(fileobj=name) + name.seek(pos) + else: + t = open(name) + t.close() + return True + except TarError: + return False + +open = TarFile.open + + +def main(): + import argparse + + description = 'A simple command-line interface for tarfile module.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='Verbose output') + parser.add_argument('--filter', metavar='', + choices=_NAMED_FILTERS, + help='Filter for extraction') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('-l', '--list', metavar='', + help='Show listing of a tarfile') + group.add_argument('-e', '--extract', nargs='+', + metavar=('', ''), + help='Extract tarfile into target dir') + group.add_argument('-c', '--create', nargs='+', + metavar=('', ''), + help='Create tarfile from sources') + group.add_argument('-t', '--test', metavar='', + help='Test if a tarfile is valid') + + args = parser.parse_args() + + if args.filter and args.extract is None: + parser.exit(1, '--filter is only valid for extraction\n') + + if args.test is not None: + src = args.test + if is_tarfile(src): + with open(src, 'r') as tar: + tar.getmembers() + print(tar.getmembers(), file=sys.stderr) + if args.verbose: + print('{!r} is a tar archive.'.format(src)) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.list is not None: + src = args.list + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.list(verbose=args.verbose) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.extract is not None: + if len(args.extract) == 1: + src = args.extract[0] + curdir = os.curdir + elif len(args.extract) == 2: + src, curdir = args.extract + else: + parser.exit(1, parser.format_help()) + + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.extractall(path=curdir, filter=args.filter) + if args.verbose: + if curdir == '.': + msg = '{!r} file is extracted.'.format(src) + else: + msg = ('{!r} file is extracted ' + 'into {!r} directory.').format(src, curdir) + print(msg) + else: + parser.exit(1, '{!r} is not a tar archive.\n'.format(src)) + + elif args.create is not None: + tar_name = args.create.pop(0) + _, ext = os.path.splitext(tar_name) + compressions = { + # gz + '.gz': 'gz', + '.tgz': 'gz', + # xz + '.xz': 'xz', + '.txz': 'xz', + # bz2 + '.bz2': 'bz2', + '.tbz': 'bz2', + '.tbz2': 'bz2', + '.tb2': 'bz2', + } + tar_mode = 'w:' + compressions[ext] if ext in compressions else 'w' + tar_files = args.create + + with TarFile.open(tar_name, tar_mode) as tf: + for file_name in tar_files: + tf.add(file_name) + + if args.verbose: + print('{!r} file created.'.format(tar_name)) + +if __name__ == '__main__': + main() diff --git a/setuptools/_vendor/importlib_metadata-6.0.0.dist-info/RECORD b/setuptools/_vendor/importlib_metadata-6.0.0.dist-info/RECORD index 01f235677f6..c5ed31bf55b 100644 --- a/setuptools/_vendor/importlib_metadata-6.0.0.dist-info/RECORD +++ b/setuptools/_vendor/importlib_metadata-6.0.0.dist-info/RECORD @@ -6,15 +6,15 @@ importlib_metadata-6.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk importlib_metadata-6.0.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 importlib_metadata-6.0.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 importlib_metadata/__init__.py,sha256=wiMJxNXXhPtRRHSX2N9gGLnTh0YszmE1rn3uKYRrNcs,26490 -importlib_metadata/__pycache__/__init__.cpython-311.pyc,, -importlib_metadata/__pycache__/_adapters.cpython-311.pyc,, -importlib_metadata/__pycache__/_collections.cpython-311.pyc,, -importlib_metadata/__pycache__/_compat.cpython-311.pyc,, -importlib_metadata/__pycache__/_functools.cpython-311.pyc,, -importlib_metadata/__pycache__/_itertools.cpython-311.pyc,, -importlib_metadata/__pycache__/_meta.cpython-311.pyc,, -importlib_metadata/__pycache__/_py39compat.cpython-311.pyc,, -importlib_metadata/__pycache__/_text.cpython-311.pyc,, +importlib_metadata/__pycache__/__init__.cpython-312.pyc,, +importlib_metadata/__pycache__/_adapters.cpython-312.pyc,, +importlib_metadata/__pycache__/_collections.cpython-312.pyc,, +importlib_metadata/__pycache__/_compat.cpython-312.pyc,, +importlib_metadata/__pycache__/_functools.cpython-312.pyc,, +importlib_metadata/__pycache__/_itertools.cpython-312.pyc,, +importlib_metadata/__pycache__/_meta.cpython-312.pyc,, +importlib_metadata/__pycache__/_py39compat.cpython-312.pyc,, +importlib_metadata/__pycache__/_text.cpython-312.pyc,, importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454 importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 importlib_metadata/_compat.py,sha256=9zOKf0eDgkCMnnaEhU5kQVxHd1P8BIYV7Stso7av5h8,1857 diff --git a/setuptools/_vendor/importlib_resources-5.10.2.dist-info/RECORD b/setuptools/_vendor/importlib_resources-5.10.2.dist-info/RECORD index 7d19852d4a9..ba764991ee2 100644 --- a/setuptools/_vendor/importlib_resources-5.10.2.dist-info/RECORD +++ b/setuptools/_vendor/importlib_resources-5.10.2.dist-info/RECORD @@ -6,15 +6,15 @@ importlib_resources-5.10.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQe importlib_resources-5.10.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 importlib_resources-5.10.2.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 importlib_resources/__init__.py,sha256=evPm12kLgYqTm-pbzm60bOuumumT8IpBNWFp0uMyrzE,506 -importlib_resources/__pycache__/__init__.cpython-311.pyc,, -importlib_resources/__pycache__/_adapters.cpython-311.pyc,, -importlib_resources/__pycache__/_common.cpython-311.pyc,, -importlib_resources/__pycache__/_compat.cpython-311.pyc,, -importlib_resources/__pycache__/_itertools.cpython-311.pyc,, -importlib_resources/__pycache__/_legacy.cpython-311.pyc,, -importlib_resources/__pycache__/abc.cpython-311.pyc,, -importlib_resources/__pycache__/readers.cpython-311.pyc,, -importlib_resources/__pycache__/simple.cpython-311.pyc,, +importlib_resources/__pycache__/__init__.cpython-312.pyc,, +importlib_resources/__pycache__/_adapters.cpython-312.pyc,, +importlib_resources/__pycache__/_common.cpython-312.pyc,, +importlib_resources/__pycache__/_compat.cpython-312.pyc,, +importlib_resources/__pycache__/_itertools.cpython-312.pyc,, +importlib_resources/__pycache__/_legacy.cpython-312.pyc,, +importlib_resources/__pycache__/abc.cpython-312.pyc,, +importlib_resources/__pycache__/readers.cpython-312.pyc,, +importlib_resources/__pycache__/simple.cpython-312.pyc,, importlib_resources/_adapters.py,sha256=o51tP2hpVtohP33gSYyAkGNpLfYDBqxxYsadyiRZi1E,4504 importlib_resources/_common.py,sha256=jSC4xfLdcMNbtbWHtpzbFkNa0W7kvf__nsYn14C_AEU,5457 importlib_resources/_compat.py,sha256=dSadF6WPt8MwOqSm_NIOQPhw4x0iaMYTWxi-XS93p7M,2923 @@ -25,36 +25,36 @@ importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU, importlib_resources/readers.py,sha256=PZsi5qacr2Qn3KHw4qw3Gm1MzrBblPHoTdjqjH7EKWw,3581 importlib_resources/simple.py,sha256=0__2TQBTQoqkajYmNPt1HxERcReAT6boVKJA328pr04,2576 importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/__pycache__/__init__.cpython-311.pyc,, -importlib_resources/tests/__pycache__/_compat.cpython-311.pyc,, -importlib_resources/tests/__pycache__/_path.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_contents.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_files.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_open.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_path.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_read.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_reader.cpython-311.pyc,, -importlib_resources/tests/__pycache__/test_resource.cpython-311.pyc,, -importlib_resources/tests/__pycache__/update-zips.cpython-311.pyc,, -importlib_resources/tests/__pycache__/util.cpython-311.pyc,, +importlib_resources/tests/__pycache__/__init__.cpython-312.pyc,, +importlib_resources/tests/__pycache__/_compat.cpython-312.pyc,, +importlib_resources/tests/__pycache__/_path.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_contents.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_files.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_open.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_path.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_read.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_reader.cpython-312.pyc,, +importlib_resources/tests/__pycache__/test_resource.cpython-312.pyc,, +importlib_resources/tests/__pycache__/update-zips.cpython-312.pyc,, +importlib_resources/tests/__pycache__/util.cpython-312.pyc,, importlib_resources/tests/_compat.py,sha256=YTSB0U1R9oADnh6GrQcOCgojxcF_N6H1LklymEWf9SQ,708 importlib_resources/tests/_path.py,sha256=yZyWsQzJZQ1Z8ARAxWkjAdaVVsjlzyqxO0qjBUofJ8M,1039 importlib_resources/tests/data01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data01/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data01/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/data01/subdirectory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data01/subdirectory/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/data01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 importlib_resources/tests/data01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 importlib_resources/tests/data02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/one/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/one/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/one/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/one/resource1.txt,sha256=10flKac7c-XXFzJ3t-AB5MJjlBy__dSZvPE_dOm2q6U,13 importlib_resources/tests/data02/two/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/data02/two/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/data02/two/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/data02/two/resource2.txt,sha256=lt2jbN3TMn9QiFKM832X39bU_62UptDdUkoYzkvEbl0,13 importlib_resources/tests/namespacedata01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 importlib_resources/tests/namespacedata01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 @@ -70,8 +70,8 @@ importlib_resources/tests/test_resource.py,sha256=EMoarxTEHcrq8R41LQDsndIG8Idtm4 importlib_resources/tests/update-zips.py,sha256=x-SrO5v87iLLUMXyefxDwAd3imAs_slI94sLWvJ6N40,1417 importlib_resources/tests/util.py,sha256=ARAlxZ47wC-lgR7PGlmgBoi4HnhzcykD5Is2-TAwY0I,4873 importlib_resources/tests/zipdata01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/zipdata01/ziptestdata.zip,sha256=z5Of4dsv3T0t-46B0MsVhxlhsPGMz28aUhJDWpj3_oY,876 importlib_resources/tests/zipdata02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-311.pyc,, +importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-312.pyc,, importlib_resources/tests/zipdata02/ziptestdata.zip,sha256=ydI-_j-xgQ7tDxqBp9cjOqXBGxUp6ZBbwVJu6Xj-nrY,698 diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/METADATA b/setuptools/_vendor/jaraco.context-4.3.0.dist-info/METADATA deleted file mode 100644 index 281137a035e..00000000000 --- a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/METADATA +++ /dev/null @@ -1,68 +0,0 @@ -Metadata-Version: 2.1 -Name: jaraco.context -Version: 4.3.0 -Summary: Context managers by jaraco -Home-page: https://github.com/jaraco/jaraco.context -Author: Jason R. Coombs -Author-email: jaraco@jaraco.com -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.7 -License-File: LICENSE -Provides-Extra: docs -Requires-Dist: sphinx (>=3.5) ; extra == 'docs' -Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' -Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' -Requires-Dist: furo ; extra == 'docs' -Requires-Dist: sphinx-lint ; extra == 'docs' -Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' -Provides-Extra: testing -Requires-Dist: pytest (>=6) ; extra == 'testing' -Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' -Requires-Dist: flake8 (<5) ; extra == 'testing' -Requires-Dist: pytest-cov ; extra == 'testing' -Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' -Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' - -.. image:: https://img.shields.io/pypi/v/jaraco.context.svg - :target: https://pypi.org/project/jaraco.context - -.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg - -.. image:: https://github.com/jaraco/jaraco.context/workflows/tests/badge.svg - :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 - :alt: tests - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - -.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest - :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest - -.. image:: https://img.shields.io/badge/skeleton-2023-informational - :target: https://blog.jaraco.com/skeleton - -.. image:: https://tidelift.com/badges/package/pypi/jaraco.context - :target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme - -For Enterprise -============== - -Available as part of the Tidelift Subscription. - -This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. - -`Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/RECORD b/setuptools/_vendor/jaraco.context-4.3.0.dist-info/RECORD deleted file mode 100644 index 03122364a21..00000000000 --- a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/RECORD +++ /dev/null @@ -1,8 +0,0 @@ -jaraco.context-4.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -jaraco.context-4.3.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 -jaraco.context-4.3.0.dist-info/METADATA,sha256=GqMykAm33E7Tt_t_MHc5O7GJN62Qwp6MEHX9WD-LPow,2958 -jaraco.context-4.3.0.dist-info/RECORD,, -jaraco.context-4.3.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 -jaraco.context-4.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 -jaraco/__pycache__/context.cpython-311.pyc,, -jaraco/context.py,sha256=vlyDzb_PvZ9H7R9bbTr_CMRnveW5Dc56eC7eyd_GfoA,7460 diff --git a/setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER new file mode 100644 index 00000000000..a1b589e38a3 --- /dev/null +++ b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE new file mode 100644 index 00000000000..1bb5a44356f --- /dev/null +++ b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA new file mode 100644 index 00000000000..a36f7c5e82d --- /dev/null +++ b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA @@ -0,0 +1,75 @@ +Metadata-Version: 2.1 +Name: jaraco.context +Version: 5.3.0 +Summary: Useful decorators and context managers +Home-page: https://github.com/jaraco/jaraco.context +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.8 +License-File: LICENSE +Requires-Dist: backports.tarfile ; python_version < "3.12" +Provides-Extra: docs +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest !=8.1.1,>=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-mypy ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' +Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'testing' +Requires-Dist: portend ; extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.context.svg + :target: https://pypi.org/project/jaraco.context + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg + +.. image:: https://github.com/jaraco/jaraco.context/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest + :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/jaraco.context + :target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme + + +Highlights +========== + +See the docs linked from the badge above for the full details, but here are some features that may be of interest. + +- ``ExceptionTrap`` provides a general-purpose wrapper for trapping exceptions and then acting on the outcome. Includes ``passes`` and ``raises`` decorators to replace the result of a wrapped function by a boolean indicating the outcome of the exception trap. See `this keyring commit `_ for an example of it in production. +- ``suppress`` simply enables ``contextlib.suppress`` as a decorator. +- ``on_interrupt`` is a decorator used by CLI entry points to affect the handling of a ``KeyboardInterrupt``. Inspired by `Lucretiel/autocommand#18 `_. +- ``pushd`` is similar to pytest's ``monkeypatch.chdir`` or path's `default context `_, changes the current working directory for the duration of the context. +- ``tarball`` will download a tarball, extract it, change directory, yield, then clean up after. Convenient when working with web assets. +- ``null`` is there for those times when one code branch needs a context and the other doesn't; this null context provides symmetry across those branches. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD new file mode 100644 index 00000000000..09d191f214a --- /dev/null +++ b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.context-5.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.context-5.3.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +jaraco.context-5.3.0.dist-info/METADATA,sha256=xDtguJej0tN9iEXCUvxEJh2a7xceIRVBEakBLSr__tY,4020 +jaraco.context-5.3.0.dist-info/RECORD,, +jaraco.context-5.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +jaraco.context-5.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/context.cpython-312.pyc,, +jaraco/context.py,sha256=REoLIxDkO5MfEYowt_WoupNCRoxBS5v7YX2PbW8lIcs,9552 diff --git a/setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL new file mode 100644 index 00000000000..bab98d67588 --- /dev/null +++ b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.43.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/jaraco.context-4.3.0.dist-info/top_level.txt b/setuptools/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt similarity index 100% rename from setuptools/_vendor/jaraco.context-4.3.0.dist-info/top_level.txt rename to setuptools/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/RECORD b/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/RECORD deleted file mode 100644 index 70a3521307a..00000000000 --- a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/RECORD +++ /dev/null @@ -1,8 +0,0 @@ -jaraco.functools-3.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -jaraco.functools-3.6.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 -jaraco.functools-3.6.0.dist-info/METADATA,sha256=ImGoa1WEbhsibIb288yWqkDAvqLwlPzayjravRvW_Bs,3136 -jaraco.functools-3.6.0.dist-info/RECORD,, -jaraco.functools-3.6.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92 -jaraco.functools-3.6.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 -jaraco/__pycache__/functools.cpython-311.pyc,, -jaraco/functools.py,sha256=GhSJGMVMcb0U4-axXaY_au30hT-ceW-HM1EbV1_9NzI,15035 diff --git a/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER new file mode 100644 index 00000000000..a1b589e38a3 --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE new file mode 100644 index 00000000000..1bb5a44356f --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/METADATA b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/METADATA similarity index 69% rename from setuptools/_vendor/jaraco.functools-3.6.0.dist-info/METADATA rename to setuptools/_vendor/jaraco.functools-4.0.0.dist-info/METADATA index 23c6f5ef2b8..581b3083784 100644 --- a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/METADATA +++ b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jaraco.functools -Version: 3.6.0 +Version: 4.0.0 Summary: Functools like those found in stdlib Home-page: https://github.com/jaraco/jaraco.functools Author: Jason R. Coombs @@ -10,26 +10,26 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.7 +Requires-Python: >=3.8 License-File: LICENSE Requires-Dist: more-itertools Provides-Extra: docs -Requires-Dist: sphinx (>=3.5) ; extra == 'docs' -Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' -Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: sphinx <7.2.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' Requires-Dist: furo ; extra == 'docs' Requires-Dist: sphinx-lint ; extra == 'docs' -Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' +Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs' Provides-Extra: testing -Requires-Dist: pytest (>=6) ; extra == 'testing' -Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' -Requires-Dist: flake8 (<5) ; extra == 'testing' +Requires-Dist: pytest >=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' Requires-Dist: pytest-cov ; extra == 'testing' -Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' +Requires-Dist: pytest-ruff ; extra == 'testing' Requires-Dist: jaraco.classes ; extra == 'testing' -Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' +Requires-Dist: pytest-black >=0.3.7 ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy >=0.9.1 ; (platform_python_implementation != "PyPy") and extra == 'testing' .. image:: https://img.shields.io/pypi/v/jaraco.functools.svg :target: https://pypi.org/project/jaraco.functools @@ -40,6 +40,10 @@ Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' :target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22 :alt: tests +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black @@ -63,10 +67,3 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/RECORD b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/RECORD new file mode 100644 index 00000000000..783aa7d2b9a --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/RECORD @@ -0,0 +1,10 @@ +jaraco.functools-4.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.functools-4.0.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +jaraco.functools-4.0.0.dist-info/METADATA,sha256=nVOe_vWvaN2iWJ2aBVkhKvmvH-gFksNCXHwCNvcj65I,3078 +jaraco.functools-4.0.0.dist-info/RECORD,, +jaraco.functools-4.0.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +jaraco.functools-4.0.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/functools/__init__.py,sha256=hEAJaS2uSZRuF_JY4CxCHIYh79ZpxaPp9OiHyr9EJ1w,16642 +jaraco/functools/__init__.pyi,sha256=N4lLbdhMtrmwiK3UuMGhYsiOLLZx69CUNOdmFPSVh6Q,3982 +jaraco/functools/__pycache__/__init__.cpython-312.pyc,, +jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL new file mode 100644 index 00000000000..ba48cbcf927 --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/jaraco.functools-3.6.0.dist-info/top_level.txt b/setuptools/_vendor/jaraco.functools-4.0.0.dist-info/top_level.txt similarity index 100% rename from setuptools/_vendor/jaraco.functools-3.6.0.dist-info/top_level.txt rename to setuptools/_vendor/jaraco.functools-4.0.0.dist-info/top_level.txt diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD index dd471b07082..c698101cb4f 100644 --- a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD @@ -7,4 +7,4 @@ jaraco.text-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FG jaraco.text-3.7.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335 jaraco/text/__init__.py,sha256=I56MW2ZFwPrYXIxzqxMBe2A1t-T4uZBgEgAKe9-JoqM,15538 -jaraco/text/__pycache__/__init__.cpython-311.pyc,, +jaraco/text/__pycache__/__init__.cpython-312.pyc,, diff --git a/setuptools/_vendor/jaraco/context.py b/setuptools/_vendor/jaraco/context.py index b0d1ef37cbc..61b27135df1 100644 --- a/setuptools/_vendor/jaraco/context.py +++ b/setuptools/_vendor/jaraco/context.py @@ -1,15 +1,26 @@ -import os -import subprocess +from __future__ import annotations + import contextlib import functools -import tempfile -import shutil import operator +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.request import warnings +from typing import Iterator + + +if sys.version_info < (3, 12): + from backports import tarfile +else: + import tarfile @contextlib.contextmanager -def pushd(dir): +def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: """ >>> tmp_path = getfixture('tmp_path') >>> with pushd(tmp_path): @@ -26,33 +37,88 @@ def pushd(dir): @contextlib.contextmanager -def tarball_context(url, target_dir=None, runner=None, pushd=pushd): +def tarball( + url, target_dir: str | os.PathLike | None = None +) -> Iterator[str | os.PathLike]: """ - Get a tarball, extract it, change to that directory, yield, then - clean up. - `runner` is the function to invoke commands. - `pushd` is a context manager for changing the directory. + Get a tarball, extract it, yield, then clean up. + + >>> import urllib.request + >>> url = getfixture('tarfile_served') + >>> target = getfixture('tmp_path') / 'out' + >>> tb = tarball(url, target_dir=target) + >>> import pathlib + >>> with tb as extracted: + ... contents = pathlib.Path(extracted, 'contents.txt').read_text(encoding='utf-8') + >>> assert not os.path.exists(extracted) """ if target_dir is None: target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') - if runner is None: - runner = functools.partial(subprocess.check_call, shell=True) - else: - warnings.warn("runner parameter is deprecated", DeprecationWarning) # In the tar command, use --strip-components=1 to strip the first path and # then # use -C to cause the files to be extracted to {target_dir}. This ensures # that we always know where the files were extracted. - runner('mkdir {target_dir}'.format(**vars())) + os.mkdir(target_dir) try: - getter = 'wget {url} -O -' - extract = 'tar x{compression} --strip-components=1 -C {target_dir}' - cmd = ' | '.join((getter, extract)) - runner(cmd.format(compression=infer_compression(url), **vars())) - with pushd(target_dir): - yield target_dir + req = urllib.request.urlopen(url) + with tarfile.open(fileobj=req, mode='r|*') as tf: + tf.extractall(path=target_dir, filter=strip_first_component) + yield target_dir finally: - runner('rm -Rf {target_dir}'.format(**vars())) + shutil.rmtree(target_dir) + + +def strip_first_component( + member: tarfile.TarInfo, + path, +) -> tarfile.TarInfo: + _, member.name = member.name.split('/', 1) + return member + + +def _compose(*cmgrs): + """ + Compose any number of dependent context managers into a single one. + + The last, innermost context manager may take arbitrary arguments, but + each successive context manager should accept the result from the + previous as a single parameter. + + Like :func:`jaraco.functools.compose`, behavior works from right to + left, so the context manager should be indicated from outermost to + innermost. + + Example, to create a context manager to change to a temporary + directory: + + >>> temp_dir_as_cwd = _compose(pushd, temp_dir) + >>> with temp_dir_as_cwd() as dir: + ... assert os.path.samefile(os.getcwd(), dir) + """ + + def compose_two(inner, outer): + def composed(*args, **kwargs): + with inner(*args, **kwargs) as saved, outer(saved) as res: + yield res + + return contextlib.contextmanager(composed) + + return functools.reduce(compose_two, reversed(cmgrs)) + + +tarball_cwd = _compose(pushd, tarball) + + +@contextlib.contextmanager +def tarball_context(*args, **kwargs): + warnings.warn( + "tarball_context is deprecated. Use tarball or tarball_cwd instead.", + DeprecationWarning, + stacklevel=2, + ) + pushd_ctx = kwargs.pop('pushd', pushd) + with tarball(*args, **kwargs) as tball, pushd_ctx(tball) as dir: + yield dir def infer_compression(url): @@ -68,6 +134,11 @@ def infer_compression(url): >>> infer_compression('file.xz') 'J' """ + warnings.warn( + "infer_compression is deprecated with no replacement", + DeprecationWarning, + stacklevel=2, + ) # cheat and just assume it's the last two characters compression_indicator = url[-2:] mapping = dict(gz='z', bz='j', xz='J') @@ -84,7 +155,7 @@ def temp_dir(remover=shutil.rmtree): >>> import pathlib >>> with temp_dir() as the_dir: ... assert os.path.isdir(the_dir) - ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents') + ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents', encoding='utf-8') >>> assert not os.path.exists(the_dir) """ temp_dir = tempfile.mkdtemp() @@ -113,15 +184,23 @@ def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): yield repo_dir -@contextlib.contextmanager def null(): """ A null context suitable to stand in for a meaningful context. >>> with null() as value: ... assert value is None + + This context is most useful when dealing with two or more code + branches but only some need a context. Wrap the others in a null + context to provide symmetry across all options. """ - yield + warnings.warn( + "null is deprecated. Use contextlib.nullcontext", + DeprecationWarning, + stacklevel=2, + ) + return contextlib.nullcontext() class ExceptionTrap: @@ -267,13 +346,7 @@ class on_interrupt(contextlib.ContextDecorator): ... on_interrupt('ignore')(do_interrupt)() """ - def __init__( - self, - action='error', - # py3.7 compat - # /, - code=1, - ): + def __init__(self, action='error', /, code=1): self.action = action self.code = code diff --git a/setuptools/_vendor/jaraco/functools.py b/setuptools/_vendor/jaraco/functools/__init__.py similarity index 79% rename from setuptools/_vendor/jaraco/functools.py rename to setuptools/_vendor/jaraco/functools/__init__.py index ebf7a36137f..130b87a4859 100644 --- a/setuptools/_vendor/jaraco/functools.py +++ b/setuptools/_vendor/jaraco/functools/__init__.py @@ -1,18 +1,14 @@ +import collections.abc import functools -import time import inspect -import collections -import types import itertools +import operator +import time +import types import warnings import setuptools.extern.more_itertools -from typing import Callable, TypeVar - - -CallableT = TypeVar("CallableT", bound=Callable[..., object]) - def compose(*funcs): """ @@ -38,24 +34,6 @@ def compose_two(f1, f2): return functools.reduce(compose_two, funcs) -def method_caller(method_name, *args, **kwargs): - """ - Return a function that will call a named method on the - target object with optional positional and keyword - arguments. - - >>> lower = method_caller('lower') - >>> lower('MyString') - 'mystring' - """ - - def call_method(target): - func = getattr(target, method_name) - return func(*args, **kwargs) - - return call_method - - def once(func): """ Decorate func so it's only ever called the first time. @@ -98,12 +76,7 @@ def wrapper(*args, **kwargs): return wrapper -def method_cache( - method: CallableT, - cache_wrapper: Callable[ - [CallableT], CallableT - ] = functools.lru_cache(), # type: ignore[assignment] -) -> CallableT: +def method_cache(method, cache_wrapper=functools.lru_cache()): """ Wrap lru_cache to support storing the cache data in the object instances. @@ -171,21 +144,17 @@ def method_cache( for another implementation and additional justification. """ - def wrapper(self: object, *args: object, **kwargs: object) -> object: + def wrapper(self, *args, **kwargs): # it's the first call, replace the method with a cached, bound method - bound_method: CallableT = types.MethodType( # type: ignore[assignment] - method, self - ) + bound_method = types.MethodType(method, self) cached_method = cache_wrapper(bound_method) setattr(self, method.__name__, cached_method) return cached_method(*args, **kwargs) # Support cache clear even before cache has been created. - wrapper.cache_clear = lambda: None # type: ignore[attr-defined] + wrapper.cache_clear = lambda: None - return ( # type: ignore[return-value] - _special_method_cache(method, cache_wrapper) or wrapper - ) + return _special_method_cache(method, cache_wrapper) or wrapper def _special_method_cache(method, cache_wrapper): @@ -201,12 +170,13 @@ def _special_method_cache(method, cache_wrapper): """ name = method.__name__ special_names = '__getattr__', '__getitem__' + if name not in special_names: - return + return None wrapper_name = '__cached' + name - def proxy(self, *args, **kwargs): + def proxy(self, /, *args, **kwargs): if wrapper_name not in vars(self): bound = types.MethodType(method, self) cache = cache_wrapper(bound) @@ -243,7 +213,7 @@ def result_invoke(action): r""" Decorate a function with an action function that is invoked on the results returned from the decorated - function (for its side-effect), then return the original + function (for its side effect), then return the original result. >>> @result_invoke(print) @@ -267,7 +237,7 @@ def wrapper(*args, **kwargs): return wrap -def invoke(f, *args, **kwargs): +def invoke(f, /, *args, **kwargs): """ Call a function for its side effect after initialization. @@ -302,25 +272,15 @@ def invoke(f, *args, **kwargs): Use functools.partial to pass parameters to the initial call >>> @functools.partial(invoke, name='bingo') - ... def func(name): print("called with", name) + ... def func(name): print('called with', name) called with bingo """ f(*args, **kwargs) return f -def call_aside(*args, **kwargs): - """ - Deprecated name for invoke. - """ - warnings.warn("call_aside is deprecated, use invoke", DeprecationWarning) - return invoke(*args, **kwargs) - - class Throttler: - """ - Rate-limit a function (or other callable) - """ + """Rate-limit a function (or other callable).""" def __init__(self, func, max_rate=float('Inf')): if isinstance(func, Throttler): @@ -337,20 +297,20 @@ def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) def _wait(self): - "ensure at least 1/max_rate seconds from last call" + """Ensure at least 1/max_rate seconds from last call.""" elapsed = time.time() - self.last_called must_wait = 1 / self.max_rate - elapsed time.sleep(max(0, must_wait)) self.last_called = time.time() - def __get__(self, obj, type=None): + def __get__(self, obj, owner=None): return first_invoke(self._wait, functools.partial(self.func, obj)) def first_invoke(func1, func2): """ Return a function that when invoked will invoke func1 without - any parameters (for its side-effect) and then invoke func2 + any parameters (for its side effect) and then invoke func2 with whatever parameters were passed, returning its result. """ @@ -361,6 +321,17 @@ def wrapper(*args, **kwargs): return wrapper +method_caller = first_invoke( + lambda: warnings.warn( + '`jaraco.functools.method_caller` is deprecated, ' + 'use `operator.methodcaller` instead', + DeprecationWarning, + stacklevel=3, + ), + operator.methodcaller, +) + + def retry_call(func, cleanup=lambda: None, retries=0, trap=()): """ Given a callable func, trap the indicated exceptions @@ -369,7 +340,7 @@ def retry_call(func, cleanup=lambda: None, retries=0, trap=()): to propagate. """ attempts = itertools.count() if retries == float('inf') else range(retries) - for attempt in attempts: + for _ in attempts: try: return func() except trap: @@ -406,7 +377,7 @@ def wrapper(*f_args, **f_kwargs): def print_yielded(func): """ - Convert a generator into a function that prints all yielded elements + Convert a generator into a function that prints all yielded elements. >>> @print_yielded ... def x(): @@ -422,7 +393,7 @@ def print_yielded(func): def pass_none(func): """ - Wrap func so it's not called if its first param is None + Wrap func so it's not called if its first param is None. >>> print_text = pass_none(print) >>> print_text('text') @@ -431,9 +402,10 @@ def pass_none(func): """ @functools.wraps(func) - def wrapper(param, *args, **kwargs): + def wrapper(param, /, *args, **kwargs): if param is not None: return func(param, *args, **kwargs) + return None return wrapper @@ -507,7 +479,7 @@ def save_method_args(method): args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') @functools.wraps(method) - def wrapper(self, *args, **kwargs): + def wrapper(self, /, *args, **kwargs): attr_name = '_saved_' + method.__name__ attr = args_and_kwargs(args, kwargs) setattr(self, attr_name, attr) @@ -554,3 +526,108 @@ def wrapper(*args, **kwargs): return wrapper return decorate + + +def identity(x): + """ + Return the argument. + + >>> o = object() + >>> identity(o) is o + True + """ + return x + + +def bypass_when(check, *, _op=identity): + """ + Decorate a function to return its parameter when ``check``. + + >>> bypassed = [] # False + + >>> @bypass_when(bypassed) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> bypassed[:] = [object()] # True + >>> double(2) + 2 + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(param, /): + return param if _op(check) else func(param) + + return wrapper + + return decorate + + +def bypass_unless(check): + """ + Decorate a function to return its parameter unless ``check``. + + >>> enabled = [object()] # True + + >>> @bypass_unless(enabled) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> del enabled[:] # False + >>> double(2) + 2 + """ + return bypass_when(check, _op=operator.not_) + + +@functools.singledispatch +def _splat_inner(args, func): + """Splat args to func.""" + return func(*args) + + +@_splat_inner.register +def _(args: collections.abc.Mapping, func): + """Splat kargs to func as kwargs.""" + return func(**args) + + +def splat(func): + """ + Wrap func to expect its parameters to be passed positionally in a tuple. + + Has a similar effect to that of ``itertools.starmap`` over + simple ``map``. + + >>> pairs = [(-1, 1), (0, 2)] + >>> setuptools.extern.more_itertools.consume(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> setuptools.extern.more_itertools.consume(map(splat(print), pairs)) + -1 1 + 0 2 + + The approach generalizes to other iterators that don't have a "star" + equivalent, such as a "starfilter". + + >>> list(filter(splat(operator.add), pairs)) + [(0, 2)] + + Splat also accepts a mapping argument. + + >>> def is_nice(msg, code): + ... return "smile" in msg or code == 0 + >>> msgs = [ + ... dict(msg='smile!', code=20), + ... dict(msg='error :(', code=1), + ... dict(msg='unknown', code=0), + ... ] + >>> for msg in filter(splat(is_nice), msgs): + ... print(msg) + {'msg': 'smile!', 'code': 20} + {'msg': 'unknown', 'code': 0} + """ + return functools.wraps(func)(functools.partial(_splat_inner, func=func)) diff --git a/setuptools/_vendor/jaraco/functools/__init__.pyi b/setuptools/_vendor/jaraco/functools/__init__.pyi new file mode 100644 index 00000000000..c2b9ab1757e --- /dev/null +++ b/setuptools/_vendor/jaraco/functools/__init__.pyi @@ -0,0 +1,128 @@ +from collections.abc import Callable, Hashable, Iterator +from functools import partial +from operator import methodcaller +import sys +from typing import ( + Any, + Generic, + Protocol, + TypeVar, + overload, +) + +if sys.version_info >= (3, 10): + from typing import Concatenate, ParamSpec +else: + from typing_extensions import Concatenate, ParamSpec + +_P = ParamSpec('_P') +_R = TypeVar('_R') +_T = TypeVar('_T') +_R1 = TypeVar('_R1') +_R2 = TypeVar('_R2') +_V = TypeVar('_V') +_S = TypeVar('_S') +_R_co = TypeVar('_R_co', covariant=True) + +class _OnceCallable(Protocol[_P, _R]): + saved_result: _R + reset: Callable[[], None] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _ProxyMethodCacheWrapper(Protocol[_R_co]): + cache_clear: Callable[[], None] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +class _MethodCacheWrapper(Protocol[_R_co]): + def cache_clear(self) -> None: ... + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +# `compose()` overloads below will cover most use cases. + +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[_P, _R], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R1], _R], + __func3: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R2], _R], + __func3: Callable[[_R1], _R2], + __func4: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ... +def method_cache( + method: Callable[..., _R], + cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ..., +) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ... +def apply( + transform: Callable[[_R], _T] +) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ... +def result_invoke( + action: Callable[[_R], Any] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +def invoke( + f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... +def call_aside( + f: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... + +class Throttler(Generic[_R]): + last_called: float + func: Callable[..., _R] + max_rate: float + def __init__( + self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ... + ) -> None: ... + def reset(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> _R: ... + def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ... + +def first_invoke( + func1: Callable[..., Any], func2: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +method_caller: Callable[..., methodcaller] + +def retry_call( + func: Callable[..., _R], + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> _R: ... +def retry( + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... +def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ... +def pass_none( + func: Callable[Concatenate[_T, _P], _R] +) -> Callable[Concatenate[_T, _P], _R]: ... +def assign_params( + func: Callable[..., _R], namespace: dict[str, Any] +) -> partial[_R]: ... +def save_method_args( + method: Callable[Concatenate[_S, _P], _R] +) -> Callable[Concatenate[_S, _P], _R]: ... +def except_( + *exceptions: type[BaseException], replace: Any = ..., use: Any = ... +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ... +def identity(x: _T) -> _T: ... +def bypass_when( + check: _V, *, _op: Callable[[_V], Any] = ... +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def bypass_unless( + check: Any, +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... diff --git a/setuptools/_vendor/jaraco/functools/py.typed b/setuptools/_vendor/jaraco/functools/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD b/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD index c3cbb833824..d1a6ea0d22d 100644 --- a/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD @@ -7,9 +7,9 @@ more_itertools-8.8.0.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQ more_itertools-8.8.0.dist-info/top_level.txt,sha256=fAuqRXu9LPhxdB9ujJowcFOu1rZ8wzSpOW9_jlKis6M,15 more_itertools/__init__.py,sha256=C7sXffHTXM3P-iaLPPfqfmDoxOflQMJLcM7ed9p3jak,82 more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 -more_itertools/__pycache__/__init__.cpython-311.pyc,, -more_itertools/__pycache__/more.cpython-311.pyc,, -more_itertools/__pycache__/recipes.cpython-311.pyc,, +more_itertools/__pycache__/__init__.cpython-312.pyc,, +more_itertools/__pycache__/more.cpython-312.pyc,, +more_itertools/__pycache__/recipes.cpython-312.pyc,, more_itertools/more.py,sha256=DlZa8v6JihVwfQ5zHidOA-xDE0orcQIUyxVnCaUoDKE,117968 more_itertools/more.pyi,sha256=r32pH2raBC1zih3evK4fyvAXvrUamJqc6dgV7QCRL_M,14977 more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD b/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD index 3c699595fb5..3267872d45e 100644 --- a/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD @@ -1,9 +1,9 @@ -__pycache__/ordered_set.cpython-311.pyc,, +__pycache__/ordered_set.cpython-312.pyc,, ordered_set-3.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 ordered_set-3.1.1.dist-info/METADATA,sha256=qEaJM9CbGNixB_jvfohisKbXTUjcef6nCCcBJju6f4U,5357 ordered_set-3.1.1.dist-info/MIT-LICENSE,sha256=TvRE7qUSUBcd0ols7wgNf3zDEEJWW7kv7WDRySrMBBE,1071 ordered_set-3.1.1.dist-info/RECORD,, ordered_set-3.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -ordered_set-3.1.1.dist-info/WHEEL,sha256=a-zpFRIJzOq5QfuhBzbhiA1eHTzNCJn8OdRvhdNX0Rk,110 +ordered_set-3.1.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110 ordered_set-3.1.1.dist-info/top_level.txt,sha256=NTY2_aDi1Do9fl3Z9EmWPxasFkUeW2dzO2D3RDx5CfM,12 ordered_set.py,sha256=dbaCcs27dyN9gnMWGF5nA_BrVn6Q-NrjKYJpV9_fgBs,15130 diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL b/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL index f771c29b873..832be111324 100644 --- a/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: bdist_wheel (0.40.0) +Generator: bdist_wheel (0.43.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any diff --git a/setuptools/_vendor/packaging-23.1.dist-info/RECORD b/setuptools/_vendor/packaging-23.1.dist-info/RECORD index e240a8408d3..e041f20f6ad 100644 --- a/setuptools/_vendor/packaging-23.1.dist-info/RECORD +++ b/setuptools/_vendor/packaging-23.1.dist-info/RECORD @@ -7,20 +7,20 @@ packaging-23.1.dist-info/RECORD,, packaging-23.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 packaging-23.1.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 packaging/__init__.py,sha256=kYVZSmXT6CWInT4UJPDtrSQBAZu8fMuFBxpv5GsDTLk,501 -packaging/__pycache__/__init__.cpython-311.pyc,, -packaging/__pycache__/_elffile.cpython-311.pyc,, -packaging/__pycache__/_manylinux.cpython-311.pyc,, -packaging/__pycache__/_musllinux.cpython-311.pyc,, -packaging/__pycache__/_parser.cpython-311.pyc,, -packaging/__pycache__/_structures.cpython-311.pyc,, -packaging/__pycache__/_tokenizer.cpython-311.pyc,, -packaging/__pycache__/markers.cpython-311.pyc,, -packaging/__pycache__/metadata.cpython-311.pyc,, -packaging/__pycache__/requirements.cpython-311.pyc,, -packaging/__pycache__/specifiers.cpython-311.pyc,, -packaging/__pycache__/tags.cpython-311.pyc,, -packaging/__pycache__/utils.cpython-311.pyc,, -packaging/__pycache__/version.cpython-311.pyc,, +packaging/__pycache__/__init__.cpython-312.pyc,, +packaging/__pycache__/_elffile.cpython-312.pyc,, +packaging/__pycache__/_manylinux.cpython-312.pyc,, +packaging/__pycache__/_musllinux.cpython-312.pyc,, +packaging/__pycache__/_parser.cpython-312.pyc,, +packaging/__pycache__/_structures.cpython-312.pyc,, +packaging/__pycache__/_tokenizer.cpython-312.pyc,, +packaging/__pycache__/markers.cpython-312.pyc,, +packaging/__pycache__/metadata.cpython-312.pyc,, +packaging/__pycache__/requirements.cpython-312.pyc,, +packaging/__pycache__/specifiers.cpython-312.pyc,, +packaging/__pycache__/tags.cpython-312.pyc,, +packaging/__pycache__/utils.cpython-312.pyc,, +packaging/__pycache__/version.cpython-312.pyc,, packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 packaging/_manylinux.py,sha256=ESGrDEVmBc8jYTtdZRAWiLk72lOzAKWeezFgoJ_MuBc,8926 packaging/_musllinux.py,sha256=mvPk7FNjjILKRLIdMxR7IvJ1uggLgCszo-L9rjfpi0M,2524 diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD b/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD index 5f7a6b06b3c..1db8063ec5c 100644 --- a/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD +++ b/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD @@ -5,10 +5,10 @@ tomli-2.0.1.dist-info/RECORD,, tomli-2.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 tomli-2.0.1.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81 tomli/__init__.py,sha256=JhUwV66DB1g4Hvt1UQCVMdfCu-IgAV8FXmvDU9onxd4,396 -tomli/__pycache__/__init__.cpython-311.pyc,, -tomli/__pycache__/_parser.cpython-311.pyc,, -tomli/__pycache__/_re.cpython-311.pyc,, -tomli/__pycache__/_types.cpython-311.pyc,, +tomli/__pycache__/__init__.cpython-312.pyc,, +tomli/__pycache__/_parser.cpython-312.pyc,, +tomli/__pycache__/_re.cpython-312.pyc,, +tomli/__pycache__/_types.cpython-312.pyc,, tomli/_parser.py,sha256=g9-ENaALS-B8dokYpCuzUFalWlog7T-SIYMjLZSWrtM,22633 tomli/_re.py,sha256=dbjg5ChZT23Ka9z9DHOXfdtSpPwUfdgMXnj8NOoly-w,2943 tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254 diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD index 786de8542d2..efc5f26cf32 100644 --- a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD @@ -1,4 +1,4 @@ -__pycache__/typing_extensions.cpython-311.pyc,, +__pycache__/typing_extensions.cpython-312.pyc,, typing_extensions-4.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 typing_extensions-4.0.1.dist-info/LICENSE,sha256=_xfOlOECAk3raHc-scx0ynbaTmWPNzUx8Kwi1oprsa0,12755 typing_extensions-4.0.1.dist-info/METADATA,sha256=iZ_5HONZZBXtF4kroz-IPZYIl9M8IE1B00R82dWcBqE,1736 diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 0fed8eeeaec..592fe491a1f 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -9,3 +9,5 @@ typing_extensions==4.0.1 # required for importlib_resources and _metadata on older Pythons zipp==3.7.0 tomli==2.0.1 +# required for jaraco.context on older Pythons +backports.tarfile diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD b/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD index 0a88551ce0d..adc797bc2ee 100644 --- a/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD @@ -1,4 +1,4 @@ -__pycache__/zipp.cpython-311.pyc,, +__pycache__/zipp.cpython-312.pyc,, zipp-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 zipp-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 zipp-3.7.0.dist-info/METADATA,sha256=ZLzgaXTyZX_MxTU0lcGfhdPY4CjFrT_3vyQ2Fo49pl8,2261 diff --git a/tools/vendored.py b/tools/vendored.py index f339497fa13..e33a44f291e 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -42,7 +42,12 @@ def rewrite_jaraco_text(pkg_files, new_root): file.write_text(text) -def rewrite_jaraco(pkg_files, new_root): +def repair_jaraco_namespace(pkg_files): + # required for zip-packaged setuptools #3084 + pkg_files.joinpath('__init__.py').write_text('') + + +def rewrite_jaraco_functools(pkg_files, new_root): """ Rewrite imports in jaraco.functools to redirect to vendored copies. """ @@ -50,8 +55,6 @@ def rewrite_jaraco(pkg_files, new_root): text = file.read_text() text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text) file.write_text(text) - # required for zip-packaged setuptools #3084 - pkg_files.joinpath('__init__.py').write_text('') def rewrite_importlib_resources(pkg_files, new_root): @@ -129,8 +132,9 @@ def update_pkg_resources(): vendor = Path('pkg_resources/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern') + repair_jaraco_namespace(vendor / 'jaraco') rewrite_jaraco_text(vendor / 'jaraco/text', 'pkg_resources.extern') - rewrite_jaraco(vendor / 'jaraco', 'pkg_resources.extern') + rewrite_jaraco_functools(vendor / 'jaraco/functools', 'pkg_resources.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'pkg_resources.extern') rewrite_more_itertools(vendor / "more_itertools") rewrite_platformdirs(vendor / "platformdirs") @@ -140,8 +144,9 @@ def update_setuptools(): vendor = Path('setuptools/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'setuptools.extern') + repair_jaraco_namespace(vendor / 'jaraco') rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern') - rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern') + rewrite_jaraco_functools(vendor / 'jaraco/functools', 'setuptools.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern') rewrite_more_itertools(vendor / "more_itertools") From 528fe53b78e11baeb70b9819845f09aa33cbecb6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 12:50:54 -0400 Subject: [PATCH 127/232] Ensure that 'backports' is included on older Pythons --- pkg_resources/_vendor/backports/__init__.py | 0 pkg_resources/_vendor/jaraco/context.py | 2 +- pkg_resources/extern/__init__.py | 1 + setuptools/_vendor/backports/__init__.py | 0 setuptools/_vendor/jaraco/context.py | 2 +- setuptools/extern/__init__.py | 1 + tools/vendored.py | 20 +++++++++++++++++--- 7 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 pkg_resources/_vendor/backports/__init__.py create mode 100644 setuptools/_vendor/backports/__init__.py diff --git a/pkg_resources/_vendor/backports/__init__.py b/pkg_resources/_vendor/backports/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg_resources/_vendor/jaraco/context.py b/pkg_resources/_vendor/jaraco/context.py index 61b27135df1..c42f6135d55 100644 --- a/pkg_resources/_vendor/jaraco/context.py +++ b/pkg_resources/_vendor/jaraco/context.py @@ -14,7 +14,7 @@ if sys.version_info < (3, 12): - from backports import tarfile + from pkg_resources.extern.backports import tarfile else: import tarfile diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py index 948bcc6094d..df96f7f26d1 100644 --- a/pkg_resources/extern/__init__.py +++ b/pkg_resources/extern/__init__.py @@ -76,5 +76,6 @@ def install(self): 'jaraco', 'importlib_resources', 'more_itertools', + 'backports', ) VendorImporter(__name__, names).install() diff --git a/setuptools/_vendor/backports/__init__.py b/setuptools/_vendor/backports/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setuptools/_vendor/jaraco/context.py b/setuptools/_vendor/jaraco/context.py index 61b27135df1..0322c45d4ab 100644 --- a/setuptools/_vendor/jaraco/context.py +++ b/setuptools/_vendor/jaraco/context.py @@ -14,7 +14,7 @@ if sys.version_info < (3, 12): - from backports import tarfile + from setuptools.extern.backports import tarfile else: import tarfile diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index 67c4a4552f8..427b27cb809 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -80,5 +80,6 @@ def install(self): 'jaraco', 'typing_extensions', 'tomli', + 'backports', ) VendorImporter(__name__, names, 'setuptools._vendor').install() diff --git a/tools/vendored.py b/tools/vendored.py index e33a44f291e..232e9625d27 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -42,7 +42,7 @@ def rewrite_jaraco_text(pkg_files, new_root): file.write_text(text) -def repair_jaraco_namespace(pkg_files): +def repair_namespace(pkg_files): # required for zip-packaged setuptools #3084 pkg_files.joinpath('__init__.py').write_text('') @@ -57,6 +57,16 @@ def rewrite_jaraco_functools(pkg_files, new_root): file.write_text(text) +def rewrite_jaraco_context(pkg_files, new_root): + """ + Rewrite imports in jaraco.context to redirect to vendored copies. + """ + for file in pkg_files.glob('context.py'): + text = file.read_text() + text = re.sub(r' (backports)', rf' {new_root}.\1', text) + file.write_text(text) + + def rewrite_importlib_resources(pkg_files, new_root): """ Rewrite imports in importlib_resources to redirect to vendored copies. @@ -132,9 +142,11 @@ def update_pkg_resources(): vendor = Path('pkg_resources/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern') - repair_jaraco_namespace(vendor / 'jaraco') + repair_namespace(vendor / 'jaraco') + repair_namespace(vendor / 'backports') rewrite_jaraco_text(vendor / 'jaraco/text', 'pkg_resources.extern') rewrite_jaraco_functools(vendor / 'jaraco/functools', 'pkg_resources.extern') + rewrite_jaraco_context(vendor / 'jaraco', 'pkg_resources.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'pkg_resources.extern') rewrite_more_itertools(vendor / "more_itertools") rewrite_platformdirs(vendor / "platformdirs") @@ -144,9 +156,11 @@ def update_setuptools(): vendor = Path('setuptools/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'setuptools.extern') - repair_jaraco_namespace(vendor / 'jaraco') + repair_namespace(vendor / 'jaraco') + repair_namespace(vendor / 'backports') rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern') rewrite_jaraco_functools(vendor / 'jaraco/functools', 'setuptools.extern') + rewrite_jaraco_context(vendor / 'jaraco', 'setuptools.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern') rewrite_more_itertools(vendor / "more_itertools") From c509c6cb4bbca6cf9ea189308ea7e1d6471055c2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 14:00:49 -0400 Subject: [PATCH 128/232] Exclude vendored packages and tools from coverage checks. --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1f214acf383..5b7fdefd2ae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,12 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + + # local + */_vendor/* + */tools/* */setuptools/_distutils/* + disable_warnings = couldnt-parse From 88a8caebc82a706da03c8002fc0f77ffb110fe64 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 14:19:41 -0400 Subject: [PATCH 129/232] =?UTF-8?q?Bump=20version:=2069.3.0=20=E2=86=92=20?= =?UTF-8?q?69.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- NEWS.rst | 9 +++++++++ newsfragments/4298.feature.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/4298.feature.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a76d5b66d77..007a8ec0f5f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 69.3.0 +current_version = 69.4.0 commit = True tag = True diff --git a/NEWS.rst b/NEWS.rst index 7822ec63253..0fcbdfc9a62 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v69.4.0 +======= + +Features +-------- + +- Merged with pypa/distutils@55982565e, including interoperability improvements for rfc822_escape (pypa/distutils#213), dynamic resolution of config_h_filename for Python 3.13 compatibility (pypa/distutils#219), added support for the z/OS compiler (pypa/distutils#216), modernized compiler options in unixcompiler (pypa/distutils#214), fixed accumulating flags bug after compile/link (pypa/distutils#207), fixed enconding warnings (pypa/distutils#236), and general quality improvements (pypa/distutils#234). (#4298) + + v69.3.0 ======= diff --git a/newsfragments/4298.feature.rst b/newsfragments/4298.feature.rst deleted file mode 100644 index 21d680d4864..00000000000 --- a/newsfragments/4298.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Merged with pypa/distutils@55982565e, including interoperability improvements for rfc822_escape (pypa/distutils#213), dynamic resolution of config_h_filename for Python 3.13 compatibility (pypa/distutils#219), added support for the z/OS compiler (pypa/distutils#216), modernized compiler options in unixcompiler (pypa/distutils#214), fixed accumulating flags bug after compile/link (pypa/distutils#207), fixed enconding warnings (pypa/distutils#236), and general quality improvements (pypa/distutils#234). diff --git a/setup.cfg b/setup.cfg index bab3efa52ca..02078f7466d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 69.3.0 +version = 69.4.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages From ab67b5e17158dcb208b81cec3c248b31228c5bb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2024 12:39:41 -0400 Subject: [PATCH 130/232] Update to packaging 24 --- newsfragments/4301.feature.rst | 1 + .../_vendor/packaging-23.1.dist-info/RECORD | 37 -- .../INSTALLER | 0 .../LICENSE | 0 .../LICENSE.APACHE | 0 .../LICENSE.BSD | 0 .../METADATA | 5 +- .../_vendor/packaging-24.0.dist-info/RECORD | 37 ++ .../REQUESTED | 0 .../_vendor/packaging-24.0.dist-info}/WHEEL | 2 +- pkg_resources/_vendor/packaging/__init__.py | 4 +- pkg_resources/_vendor/packaging/_manylinux.py | 74 +-- pkg_resources/_vendor/packaging/_musllinux.py | 19 +- pkg_resources/_vendor/packaging/_parser.py | 13 +- pkg_resources/_vendor/packaging/metadata.py | 441 +++++++++++++++++- .../_vendor/packaging/requirements.py | 45 +- pkg_resources/_vendor/packaging/specifiers.py | 63 +-- pkg_resources/_vendor/packaging/tags.py | 63 ++- pkg_resources/_vendor/packaging/utils.py | 39 +- pkg_resources/_vendor/packaging/version.py | 63 ++- pkg_resources/_vendor/vendored.txt | 2 +- .../_vendor/packaging-23.1.dist-info/RECORD | 37 -- .../INSTALLER | 0 .../LICENSE | 0 .../LICENSE.APACHE | 0 .../LICENSE.BSD | 0 .../METADATA | 5 +- .../_vendor/packaging-24.0.dist-info/RECORD | 37 ++ .../REQUESTED | 0 .../_vendor/packaging-24.0.dist-info}/WHEEL | 2 +- setuptools/_vendor/packaging/__init__.py | 4 +- setuptools/_vendor/packaging/_manylinux.py | 74 +-- setuptools/_vendor/packaging/_musllinux.py | 19 +- setuptools/_vendor/packaging/_parser.py | 13 +- setuptools/_vendor/packaging/metadata.py | 441 +++++++++++++++++- setuptools/_vendor/packaging/requirements.py | 45 +- setuptools/_vendor/packaging/specifiers.py | 63 +-- setuptools/_vendor/packaging/tags.py | 63 ++- setuptools/_vendor/packaging/utils.py | 39 +- setuptools/_vendor/packaging/version.py | 63 ++- setuptools/_vendor/vendored.txt | 2 +- 41 files changed, 1413 insertions(+), 402 deletions(-) create mode 100644 newsfragments/4301.feature.rst delete mode 100644 pkg_resources/_vendor/packaging-23.1.dist-info/RECORD rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/INSTALLER (100%) rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE (100%) rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE.APACHE (100%) rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE.BSD (100%) rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/METADATA (95%) create mode 100644 pkg_resources/_vendor/packaging-24.0.dist-info/RECORD rename pkg_resources/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/REQUESTED (100%) rename {setuptools/_vendor/packaging-23.1.dist-info => pkg_resources/_vendor/packaging-24.0.dist-info}/WHEEL (72%) delete mode 100644 setuptools/_vendor/packaging-23.1.dist-info/RECORD rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/INSTALLER (100%) rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE (100%) rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE.APACHE (100%) rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/LICENSE.BSD (100%) rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/METADATA (95%) create mode 100644 setuptools/_vendor/packaging-24.0.dist-info/RECORD rename setuptools/_vendor/{packaging-23.1.dist-info => packaging-24.0.dist-info}/REQUESTED (100%) rename {pkg_resources/_vendor/packaging-23.1.dist-info => setuptools/_vendor/packaging-24.0.dist-info}/WHEEL (72%) diff --git a/newsfragments/4301.feature.rst b/newsfragments/4301.feature.rst new file mode 100644 index 00000000000..28ceb2a689e --- /dev/null +++ b/newsfragments/4301.feature.rst @@ -0,0 +1 @@ +Updated vendored packaging to version 24.0. diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD b/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD deleted file mode 100644 index e041f20f6ad..00000000000 --- a/pkg_resources/_vendor/packaging-23.1.dist-info/RECORD +++ /dev/null @@ -1,37 +0,0 @@ -packaging-23.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -packaging-23.1.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 -packaging-23.1.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 -packaging-23.1.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 -packaging-23.1.dist-info/METADATA,sha256=JnduJDlxs2IVeB-nIqAC3-HyNcPhP_MADd9_k_MjmaI,3082 -packaging-23.1.dist-info/RECORD,, -packaging-23.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging-23.1.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 -packaging/__init__.py,sha256=kYVZSmXT6CWInT4UJPDtrSQBAZu8fMuFBxpv5GsDTLk,501 -packaging/__pycache__/__init__.cpython-312.pyc,, -packaging/__pycache__/_elffile.cpython-312.pyc,, -packaging/__pycache__/_manylinux.cpython-312.pyc,, -packaging/__pycache__/_musllinux.cpython-312.pyc,, -packaging/__pycache__/_parser.cpython-312.pyc,, -packaging/__pycache__/_structures.cpython-312.pyc,, -packaging/__pycache__/_tokenizer.cpython-312.pyc,, -packaging/__pycache__/markers.cpython-312.pyc,, -packaging/__pycache__/metadata.cpython-312.pyc,, -packaging/__pycache__/requirements.cpython-312.pyc,, -packaging/__pycache__/specifiers.cpython-312.pyc,, -packaging/__pycache__/tags.cpython-312.pyc,, -packaging/__pycache__/utils.cpython-312.pyc,, -packaging/__pycache__/version.cpython-312.pyc,, -packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 -packaging/_manylinux.py,sha256=ESGrDEVmBc8jYTtdZRAWiLk72lOzAKWeezFgoJ_MuBc,8926 -packaging/_musllinux.py,sha256=mvPk7FNjjILKRLIdMxR7IvJ1uggLgCszo-L9rjfpi0M,2524 -packaging/_parser.py,sha256=KJQkBh_Xbfb-qsB560YIEItrTpCZaOh4_YMfBtd5XIY,10194 -packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 -packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292 -packaging/markers.py,sha256=eH-txS2zq1HdNpTd9LcZUcVIwewAiNU0grmq5wjKnOk,8208 -packaging/metadata.py,sha256=PjELMLxKG_iu3HWjKAOdKhuNrHfWgpdTF2Q4nObsZeM,16397 -packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging/requirements.py,sha256=hJzvtJyAvENc_VfwfhnOZV1851-VW8JCGh-R96NE4Pc,3287 -packaging/specifiers.py,sha256=ZOpqL_w_Kj6ZF_OWdliQUzhEyHlDbi6989kr-sF5GHs,39206 -packaging/tags.py,sha256=_1gLX8h1SgpjAdYCP9XqU37zRjXtU5ZliGy3IM-WcSM,18106 -packaging/utils.py,sha256=es0cCezKspzriQ-3V88h3yJzxz028euV2sUwM61kE-o,4355 -packaging/version.py,sha256=2NH3E57hzRhn0BV9boUBvgPsxlTqLJeI0EpYQoNvGi0,16326 diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/INSTALLER b/pkg_resources/_vendor/packaging-24.0.dist-info/INSTALLER similarity index 100% rename from pkg_resources/_vendor/packaging-23.1.dist-info/INSTALLER rename to pkg_resources/_vendor/packaging-24.0.dist-info/INSTALLER diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE b/pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE similarity index 100% rename from pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE rename to pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE.APACHE b/pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE.APACHE similarity index 100% rename from pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE.APACHE rename to pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE.APACHE diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE.BSD b/pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE.BSD similarity index 100% rename from pkg_resources/_vendor/packaging-23.1.dist-info/LICENSE.BSD rename to pkg_resources/_vendor/packaging-24.0.dist-info/LICENSE.BSD diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/METADATA b/pkg_resources/_vendor/packaging-24.0.dist-info/METADATA similarity index 95% rename from pkg_resources/_vendor/packaging-23.1.dist-info/METADATA rename to pkg_resources/_vendor/packaging-24.0.dist-info/METADATA index c43882a826a..10ab4390a96 100644 --- a/pkg_resources/_vendor/packaging-23.1.dist-info/METADATA +++ b/pkg_resources/_vendor/packaging-24.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: packaging -Version: 23.1 +Version: 24.0 Summary: Core utilities for Python packages Author-email: Donald Stufft Requires-Python: >=3.7 @@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Typing :: Typed @@ -59,6 +60,8 @@ Use ``pip`` to install these utilities:: pip install packaging +The ``packaging`` library uses calendar-based versioning (``YY.N``). + Discussion ---------- diff --git a/pkg_resources/_vendor/packaging-24.0.dist-info/RECORD b/pkg_resources/_vendor/packaging-24.0.dist-info/RECORD new file mode 100644 index 00000000000..bcf796c2f49 --- /dev/null +++ b/pkg_resources/_vendor/packaging-24.0.dist-info/RECORD @@ -0,0 +1,37 @@ +packaging-24.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +packaging-24.0.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +packaging-24.0.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +packaging-24.0.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +packaging-24.0.dist-info/METADATA,sha256=0dESdhY_wHValuOrbgdebiEw04EbX4dkujlxPdEsFus,3203 +packaging-24.0.dist-info/RECORD,, +packaging-24.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging-24.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +packaging/__init__.py,sha256=UzotcV07p8vcJzd80S-W0srhgY8NMVD_XvJcZ7JN-tA,496 +packaging/__pycache__/__init__.cpython-312.pyc,, +packaging/__pycache__/_elffile.cpython-312.pyc,, +packaging/__pycache__/_manylinux.cpython-312.pyc,, +packaging/__pycache__/_musllinux.cpython-312.pyc,, +packaging/__pycache__/_parser.cpython-312.pyc,, +packaging/__pycache__/_structures.cpython-312.pyc,, +packaging/__pycache__/_tokenizer.cpython-312.pyc,, +packaging/__pycache__/markers.cpython-312.pyc,, +packaging/__pycache__/metadata.cpython-312.pyc,, +packaging/__pycache__/requirements.cpython-312.pyc,, +packaging/__pycache__/specifiers.cpython-312.pyc,, +packaging/__pycache__/tags.cpython-312.pyc,, +packaging/__pycache__/utils.cpython-312.pyc,, +packaging/__pycache__/version.cpython-312.pyc,, +packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 +packaging/_manylinux.py,sha256=1ng_TqyH49hY6s3W_zVHyoJIaogbJqbIF1jJ0fAehc4,9590 +packaging/_musllinux.py,sha256=kgmBGLFybpy8609-KTvzmt2zChCPWYvhp5BWP4JX7dE,2676 +packaging/_parser.py,sha256=zlsFB1FpMRjkUdQb6WLq7xON52ruQadxFpYsDXWhLb4,10347 +packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292 +packaging/markers.py,sha256=eH-txS2zq1HdNpTd9LcZUcVIwewAiNU0grmq5wjKnOk,8208 +packaging/metadata.py,sha256=w7jPEg6mDf1FTZMn79aFxFuk4SKtynUJtxr2InTxlV4,33036 +packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933 +packaging/specifiers.py,sha256=dB2DwbmvSbEuVilEyiIQ382YfW5JfwzXTfRRPVtaENY,39784 +packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950 +packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268 +packaging/version.py,sha256=XjRBLNK17UMDgLeP8UHnqwiY3TdSi03xFQURtec211A,16236 diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/REQUESTED b/pkg_resources/_vendor/packaging-24.0.dist-info/REQUESTED similarity index 100% rename from pkg_resources/_vendor/packaging-23.1.dist-info/REQUESTED rename to pkg_resources/_vendor/packaging-24.0.dist-info/REQUESTED diff --git a/setuptools/_vendor/packaging-23.1.dist-info/WHEEL b/pkg_resources/_vendor/packaging-24.0.dist-info/WHEEL similarity index 72% rename from setuptools/_vendor/packaging-23.1.dist-info/WHEEL rename to pkg_resources/_vendor/packaging-24.0.dist-info/WHEEL index db4a255f3a2..3b5e64b5e6c 100644 --- a/setuptools/_vendor/packaging-23.1.dist-info/WHEEL +++ b/pkg_resources/_vendor/packaging-24.0.dist-info/WHEEL @@ -1,4 +1,4 @@ Wheel-Version: 1.0 -Generator: flit 3.8.0 +Generator: flit 3.9.0 Root-Is-Purelib: true Tag: py3-none-any diff --git a/pkg_resources/_vendor/packaging/__init__.py b/pkg_resources/_vendor/packaging/__init__.py index 13cadc7f04d..e7c0aa12ca9 100644 --- a/pkg_resources/_vendor/packaging/__init__.py +++ b/pkg_resources/_vendor/packaging/__init__.py @@ -6,10 +6,10 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "23.1" +__version__ = "24.0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" __license__ = "BSD-2-Clause or Apache-2.0" -__copyright__ = "2014-2019 %s" % __author__ +__copyright__ = "2014 %s" % __author__ diff --git a/pkg_resources/_vendor/packaging/_manylinux.py b/pkg_resources/_vendor/packaging/_manylinux.py index 449c655be65..ad62505f3ff 100644 --- a/pkg_resources/_vendor/packaging/_manylinux.py +++ b/pkg_resources/_vendor/packaging/_manylinux.py @@ -5,7 +5,7 @@ import re import sys import warnings -from typing import Dict, Generator, Iterator, NamedTuple, Optional, Tuple +from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple from ._elffile import EIClass, EIData, ELFFile, EMachine @@ -50,12 +50,21 @@ def _is_linux_i686(executable: str) -> bool: ) -def _have_compatible_abi(executable: str, arch: str) -> bool: - if arch == "armv7l": +def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: + if "armv7l" in archs: return _is_linux_armhf(executable) - if arch == "i686": + if "i686" in archs: return _is_linux_i686(executable) - return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"} + allowed_archs = { + "x86_64", + "aarch64", + "ppc64", + "ppc64le", + "s390x", + "loongarch64", + "riscv64", + } + return any(arch in allowed_archs for arch in archs) # If glibc ever changes its major version, we need to know what the last @@ -81,7 +90,7 @@ def _glibc_version_string_confstr() -> Optional[str]: # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 try: # Should be a string like "glibc 2.17". - version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION") + version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION") assert version_string is not None _, version = version_string.rsplit() except (AssertionError, AttributeError, OSError, ValueError): @@ -167,13 +176,13 @@ def _get_glibc_version() -> Tuple[int, int]: # From PEP 513, PEP 600 -def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool: +def _is_compatible(arch: str, version: _GLibCVersion) -> bool: sys_glibc = _get_glibc_version() if sys_glibc < version: return False # Check for presence of _manylinux module. try: - import _manylinux # noqa + import _manylinux except ImportError: return True if hasattr(_manylinux, "manylinux_compatible"): @@ -203,12 +212,22 @@ def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool: } -def platform_tags(linux: str, arch: str) -> Iterator[str]: - if not _have_compatible_abi(sys.executable, arch): +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate manylinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be manylinux-compatible. + + :returns: An iterator of compatible manylinux tags. + """ + if not _have_compatible_abi(sys.executable, archs): return # Oldest glibc to be supported regardless of architecture is (2, 17). too_old_glibc2 = _GLibCVersion(2, 16) - if arch in {"x86_64", "i686"}: + if set(archs) & {"x86_64", "i686"}: # On x86/i686 also oldest glibc to be supported is (2, 5). too_old_glibc2 = _GLibCVersion(2, 4) current_glibc = _GLibCVersion(*_get_glibc_version()) @@ -222,19 +241,20 @@ def platform_tags(linux: str, arch: str) -> Iterator[str]: for glibc_major in range(current_glibc.major - 1, 1, -1): glibc_minor = _LAST_GLIBC_MINOR[glibc_major] glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) - for glibc_max in glibc_max_list: - if glibc_max.major == too_old_glibc2.major: - min_minor = too_old_glibc2.minor - else: - # For other glibc major versions oldest supported is (x, 0). - min_minor = -1 - for glibc_minor in range(glibc_max.minor, min_minor, -1): - glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) - tag = "manylinux_{}_{}".format(*glibc_version) - if _is_compatible(tag, arch, glibc_version): - yield linux.replace("linux", tag) - # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. - if glibc_version in _LEGACY_MANYLINUX_MAP: - legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] - if _is_compatible(legacy_tag, arch, glibc_version): - yield linux.replace("linux", legacy_tag) + for arch in archs: + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_compatible(arch, glibc_version): + yield f"{tag}_{arch}" + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_compatible(arch, glibc_version): + yield f"{legacy_tag}_{arch}" diff --git a/pkg_resources/_vendor/packaging/_musllinux.py b/pkg_resources/_vendor/packaging/_musllinux.py index 706ba600a93..86419df9d70 100644 --- a/pkg_resources/_vendor/packaging/_musllinux.py +++ b/pkg_resources/_vendor/packaging/_musllinux.py @@ -8,7 +8,7 @@ import re import subprocess import sys -from typing import Iterator, NamedTuple, Optional +from typing import Iterator, NamedTuple, Optional, Sequence from ._elffile import ELFFile @@ -47,24 +47,27 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]: return None if ld is None or "musl" not in ld: return None - proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True) + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) return _parse_musl_version(proc.stderr) -def platform_tags(arch: str) -> Iterator[str]: +def platform_tags(archs: Sequence[str]) -> Iterator[str]: """Generate musllinux tags compatible to the current platform. - :param arch: Should be the part of platform tag after the ``linux_`` - prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a - prerequisite for the current platform to be musllinux-compatible. + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be musllinux-compatible. :returns: An iterator of compatible musllinux tags. """ sys_musl = _get_musl_version(sys.executable) if sys_musl is None: # Python not dynamically linked against musl. return - for minor in range(sys_musl.minor, -1, -1): - yield f"musllinux_{sys_musl.major}_{minor}_{arch}" + for arch in archs: + for minor in range(sys_musl.minor, -1, -1): + yield f"musllinux_{sys_musl.major}_{minor}_{arch}" if __name__ == "__main__": # pragma: no cover diff --git a/pkg_resources/_vendor/packaging/_parser.py b/pkg_resources/_vendor/packaging/_parser.py index 5a18b758fe0..684df75457c 100644 --- a/pkg_resources/_vendor/packaging/_parser.py +++ b/pkg_resources/_vendor/packaging/_parser.py @@ -252,7 +252,13 @@ def _parse_version_many(tokenizer: Tokenizer) -> str: # Recursive descent parser for marker expression # -------------------------------------------------------------------------------------- def parse_marker(source: str) -> MarkerList: - return _parse_marker(Tokenizer(source, rules=DEFAULT_RULES)) + return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList: + retval = _parse_marker(tokenizer) + tokenizer.expect("END", expected="end of marker expression") + return retval def _parse_marker(tokenizer: Tokenizer) -> MarkerList: @@ -318,10 +324,7 @@ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: def process_env_var(env_var: str) -> Variable: - if ( - env_var == "platform_python_implementation" - or env_var == "python_implementation" - ): + if env_var in ("platform_python_implementation", "python_implementation"): return Variable("platform_python_implementation") else: return Variable(env_var) diff --git a/pkg_resources/_vendor/packaging/metadata.py b/pkg_resources/_vendor/packaging/metadata.py index e76a60c395e..fb274930799 100644 --- a/pkg_resources/_vendor/packaging/metadata.py +++ b/pkg_resources/_vendor/packaging/metadata.py @@ -5,23 +5,77 @@ import email.policy import sys import typing -from typing import Dict, List, Optional, Tuple, Union, cast - -if sys.version_info >= (3, 8): # pragma: no cover - from typing import TypedDict +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +from . import requirements, specifiers, utils, version as version_module + +T = typing.TypeVar("T") +if sys.version_info[:2] >= (3, 8): # pragma: no cover + from typing import Literal, TypedDict else: # pragma: no cover if typing.TYPE_CHECKING: - from typing_extensions import TypedDict + from typing_extensions import Literal, TypedDict else: try: - from typing_extensions import TypedDict + from typing_extensions import Literal, TypedDict except ImportError: + class Literal: + def __init_subclass__(*_args, **_kwargs): + pass + class TypedDict: def __init_subclass__(*_args, **_kwargs): pass +try: + ExceptionGroup +except NameError: # pragma: no cover + + class ExceptionGroup(Exception): # noqa: N818 + """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11. + + If :external:exc:`ExceptionGroup` is already defined by Python itself, + that version is used instead. + """ + + message: str + exceptions: List[Exception] + + def __init__(self, message: str, exceptions: List[Exception]) -> None: + self.message = message + self.exceptions = exceptions + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})" + +else: # pragma: no cover + ExceptionGroup = ExceptionGroup + + +class InvalidMetadata(ValueError): + """A metadata field contains invalid data.""" + + field: str + """The name of the field that contains invalid data.""" + + def __init__(self, field: str, message: str) -> None: + self.field = field + super().__init__(message) + + # The RawMetadata class attempts to make as few assumptions about the underlying # serialization formats as possible. The idea is that as long as a serialization # formats offer some very basic primitives in *some* way then we can support @@ -33,7 +87,8 @@ class RawMetadata(TypedDict, total=False): provided). The key is lower-case and underscores are used instead of dashes compared to the equivalent core metadata field. Any core metadata field that can be specified multiple times or can hold multiple values in a single - field have a key with a plural name. + field have a key with a plural name. See :class:`Metadata` whose attributes + match the keys of this dictionary. Core metadata fields that can be specified multiple times are stored as a list or dict depending on which is appropriate for the field. Any fields @@ -77,7 +132,7 @@ class RawMetadata(TypedDict, total=False): # but got stuck without ever being able to build consensus on # it and ultimately ended up withdrawn. # - # However, a number of tools had started emiting METADATA with + # However, a number of tools had started emitting METADATA with # `2.0` Metadata-Version, so for historical reasons, this version # was skipped. @@ -110,7 +165,7 @@ class RawMetadata(TypedDict, total=False): "version", } -_LIST_STRING_FIELDS = { +_LIST_FIELDS = { "classifiers", "dynamic", "obsoletes", @@ -125,6 +180,10 @@ class RawMetadata(TypedDict, total=False): "supported_platforms", } +_DICT_FIELDS = { + "project_urls", +} + def _parse_keywords(data: str) -> List[str]: """Split a string of comma-separate keyboards into a list of keywords.""" @@ -230,10 +289,11 @@ def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str: "supported-platform": "supported_platforms", "version": "version", } +_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()} def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]: - """Parse a distribution's metadata. + """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). This function returns a two-item tuple of dicts. The first dict is of recognized fields from the core metadata specification. Fields that can be @@ -267,7 +327,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # We use get_all() here, even for fields that aren't multiple use, # because otherwise someone could have e.g. two Name fields, and we # would just silently ignore it rather than doing something about it. - headers = parsed.get_all(name) + headers = parsed.get_all(name) or [] # The way the email module works when parsing bytes is that it # unconditionally decodes the bytes as ascii using the surrogateescape @@ -349,7 +409,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # If this is one of our list of string fields, then we can just assign # the value, since email *only* has strings, and our get_all() call # above ensures that this is a list. - elif raw_name in _LIST_STRING_FIELDS: + elif raw_name in _LIST_FIELDS: raw[raw_name] = value # Special Case: Keywords # The keywords field is implemented in the metadata spec as a str, @@ -406,3 +466,360 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st # way this function is implemented, our `TypedDict` can only have valid key # names. return cast(RawMetadata, raw), unparsed + + +_NOT_FOUND = object() + + +# Keep the two values in sync. +_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] +_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] + +_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) + + +class _Validator(Generic[T]): + """Validate a metadata field. + + All _process_*() methods correspond to a core metadata field. The method is + called with the field's raw value. If the raw value is valid it is returned + in its "enriched" form (e.g. ``version.Version`` for the ``Version`` field). + If the raw value is invalid, :exc:`InvalidMetadata` is raised (with a cause + as appropriate). + """ + + name: str + raw_name: str + added: _MetadataVersion + + def __init__( + self, + *, + added: _MetadataVersion = "1.0", + ) -> None: + self.added = added + + def __set_name__(self, _owner: "Metadata", name: str) -> None: + self.name = name + self.raw_name = _RAW_TO_EMAIL_MAPPING[name] + + def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T: + # With Python 3.8, the caching can be replaced with functools.cached_property(). + # No need to check the cache as attribute lookup will resolve into the + # instance's __dict__ before __get__ is called. + cache = instance.__dict__ + value = instance._raw.get(self.name) + + # To make the _process_* methods easier, we'll check if the value is None + # and if this field is NOT a required attribute, and if both of those + # things are true, we'll skip the the converter. This will mean that the + # converters never have to deal with the None union. + if self.name in _REQUIRED_ATTRS or value is not None: + try: + converter: Callable[[Any], T] = getattr(self, f"_process_{self.name}") + except AttributeError: + pass + else: + value = converter(value) + + cache[self.name] = value + try: + del instance._raw[self.name] # type: ignore[misc] + except KeyError: + pass + + return cast(T, value) + + def _invalid_metadata( + self, msg: str, cause: Optional[Exception] = None + ) -> InvalidMetadata: + exc = InvalidMetadata( + self.raw_name, msg.format_map({"field": repr(self.raw_name)}) + ) + exc.__cause__ = cause + return exc + + def _process_metadata_version(self, value: str) -> _MetadataVersion: + # Implicitly makes Metadata-Version required. + if value not in _VALID_METADATA_VERSIONS: + raise self._invalid_metadata(f"{value!r} is not a valid metadata version") + return cast(_MetadataVersion, value) + + def _process_name(self, value: str) -> str: + if not value: + raise self._invalid_metadata("{field} is a required field") + # Validate the name as a side-effect. + try: + utils.canonicalize_name(value, validate=True) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + else: + return value + + def _process_version(self, value: str) -> version_module.Version: + if not value: + raise self._invalid_metadata("{field} is a required field") + try: + return version_module.parse(value) + except version_module.InvalidVersion as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_summary(self, value: str) -> str: + """Check the field contains no newlines.""" + if "\n" in value: + raise self._invalid_metadata("{field} must be a single line") + return value + + def _process_description_content_type(self, value: str) -> str: + content_types = {"text/plain", "text/x-rst", "text/markdown"} + message = email.message.EmailMessage() + message["content-type"] = value + + content_type, parameters = ( + # Defaults to `text/plain` if parsing failed. + message.get_content_type().lower(), + message["content-type"].params, + ) + # Check if content-type is valid or defaulted to `text/plain` and thus was + # not parseable. + if content_type not in content_types or content_type not in value.lower(): + raise self._invalid_metadata( + f"{{field}} must be one of {list(content_types)}, not {value!r}" + ) + + charset = parameters.get("charset", "UTF-8") + if charset != "UTF-8": + raise self._invalid_metadata( + f"{{field}} can only specify the UTF-8 charset, not {list(charset)}" + ) + + markdown_variants = {"GFM", "CommonMark"} + variant = parameters.get("variant", "GFM") # Use an acceptable default. + if content_type == "text/markdown" and variant not in markdown_variants: + raise self._invalid_metadata( + f"valid Markdown variants for {{field}} are {list(markdown_variants)}, " + f"not {variant!r}", + ) + return value + + def _process_dynamic(self, value: List[str]) -> List[str]: + for dynamic_field in map(str.lower, value): + if dynamic_field in {"name", "version", "metadata-version"}: + raise self._invalid_metadata( + f"{value!r} is not allowed as a dynamic field" + ) + elif dynamic_field not in _EMAIL_TO_RAW_MAPPING: + raise self._invalid_metadata(f"{value!r} is not a valid dynamic field") + return list(map(str.lower, value)) + + def _process_provides_extra( + self, + value: List[str], + ) -> List[utils.NormalizedName]: + normalized_names = [] + try: + for name in value: + normalized_names.append(utils.canonicalize_name(name, validate=True)) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{name!r} is invalid for {{field}}", cause=exc + ) + else: + return normalized_names + + def _process_requires_python(self, value: str) -> specifiers.SpecifierSet: + try: + return specifiers.SpecifierSet(value) + except specifiers.InvalidSpecifier as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_requires_dist( + self, + value: List[str], + ) -> List[requirements.Requirement]: + reqs = [] + try: + for req in value: + reqs.append(requirements.Requirement(req)) + except requirements.InvalidRequirement as exc: + raise self._invalid_metadata(f"{req!r} is invalid for {{field}}", cause=exc) + else: + return reqs + + +class Metadata: + """Representation of distribution metadata. + + Compared to :class:`RawMetadata`, this class provides objects representing + metadata fields instead of only using built-in types. Any invalid metadata + will cause :exc:`InvalidMetadata` to be raised (with a + :py:attr:`~BaseException.__cause__` attribute as appropriate). + """ + + _raw: RawMetadata + + @classmethod + def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata": + """Create an instance from :class:`RawMetadata`. + + If *validate* is true, all metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + ins = cls() + ins._raw = data.copy() # Mutations occur due to caching enriched values. + + if validate: + exceptions: List[Exception] = [] + try: + metadata_version = ins.metadata_version + metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version) + except InvalidMetadata as metadata_version_exc: + exceptions.append(metadata_version_exc) + metadata_version = None + + # Make sure to check for the fields that are present, the required + # fields (so their absence can be reported). + fields_to_check = frozenset(ins._raw) | _REQUIRED_ATTRS + # Remove fields that have already been checked. + fields_to_check -= {"metadata_version"} + + for key in fields_to_check: + try: + if metadata_version: + # Can't use getattr() as that triggers descriptor protocol which + # will fail due to no value for the instance argument. + try: + field_metadata_version = cls.__dict__[key].added + except KeyError: + exc = InvalidMetadata(key, f"unrecognized field: {key!r}") + exceptions.append(exc) + continue + field_age = _VALID_METADATA_VERSIONS.index( + field_metadata_version + ) + if field_age > metadata_age: + field = _RAW_TO_EMAIL_MAPPING[key] + exc = InvalidMetadata( + field, + "{field} introduced in metadata version " + "{field_metadata_version}, not {metadata_version}", + ) + exceptions.append(exc) + continue + getattr(ins, key) + except InvalidMetadata as exc: + exceptions.append(exc) + + if exceptions: + raise ExceptionGroup("invalid metadata", exceptions) + + return ins + + @classmethod + def from_email( + cls, data: Union[bytes, str], *, validate: bool = True + ) -> "Metadata": + """Parse metadata from email headers. + + If *validate* is true, the metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + raw, unparsed = parse_email(data) + + if validate: + exceptions: list[Exception] = [] + for unparsed_key in unparsed: + if unparsed_key in _EMAIL_TO_RAW_MAPPING: + message = f"{unparsed_key!r} has invalid data" + else: + message = f"unrecognized field: {unparsed_key!r}" + exceptions.append(InvalidMetadata(unparsed_key, message)) + + if exceptions: + raise ExceptionGroup("unparsed", exceptions) + + try: + return cls.from_raw(raw, validate=validate) + except ExceptionGroup as exc_group: + raise ExceptionGroup( + "invalid or unparsed metadata", exc_group.exceptions + ) from None + + metadata_version: _Validator[_MetadataVersion] = _Validator() + """:external:ref:`core-metadata-metadata-version` + (required; validated to be a valid metadata version)""" + name: _Validator[str] = _Validator() + """:external:ref:`core-metadata-name` + (required; validated using :func:`~packaging.utils.canonicalize_name` and its + *validate* parameter)""" + version: _Validator[version_module.Version] = _Validator() + """:external:ref:`core-metadata-version` (required)""" + dynamic: _Validator[Optional[List[str]]] = _Validator( + added="2.2", + ) + """:external:ref:`core-metadata-dynamic` + (validated against core metadata field names and lowercased)""" + platforms: _Validator[Optional[List[str]]] = _Validator() + """:external:ref:`core-metadata-platform`""" + supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """:external:ref:`core-metadata-supported-platform`""" + summary: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-summary` (validated to contain no newlines)""" + description: _Validator[Optional[str]] = _Validator() # TODO 2.1: can be in body + """:external:ref:`core-metadata-description`""" + description_content_type: _Validator[Optional[str]] = _Validator(added="2.1") + """:external:ref:`core-metadata-description-content-type` (validated)""" + keywords: _Validator[Optional[List[str]]] = _Validator() + """:external:ref:`core-metadata-keywords`""" + home_page: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-home-page`""" + download_url: _Validator[Optional[str]] = _Validator(added="1.1") + """:external:ref:`core-metadata-download-url`""" + author: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-author`""" + author_email: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-author-email`""" + maintainer: _Validator[Optional[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer`""" + maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer-email`""" + license: _Validator[Optional[str]] = _Validator() + """:external:ref:`core-metadata-license`""" + classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """:external:ref:`core-metadata-classifier`""" + requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator( + added="1.2" + ) + """:external:ref:`core-metadata-requires-dist`""" + requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator( + added="1.2" + ) + """:external:ref:`core-metadata-requires-python`""" + # Because `Requires-External` allows for non-PEP 440 version specifiers, we + # don't do any processing on the values. + requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-requires-external`""" + project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-project-url`""" + # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation + # regardless of metadata version. + provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator( + added="2.1", + ) + """:external:ref:`core-metadata-provides-extra`""" + provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-provides-dist`""" + obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2") + """:external:ref:`core-metadata-obsoletes-dist`""" + requires: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Requires`` (deprecated)""" + provides: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Provides`` (deprecated)""" + obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1") + """``Obsoletes`` (deprecated)""" diff --git a/pkg_resources/_vendor/packaging/requirements.py b/pkg_resources/_vendor/packaging/requirements.py index f34bfa85c80..bdc43a7e98d 100644 --- a/pkg_resources/_vendor/packaging/requirements.py +++ b/pkg_resources/_vendor/packaging/requirements.py @@ -2,13 +2,13 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -import urllib.parse -from typing import Any, List, Optional, Set +from typing import Any, Iterator, Optional, Set from ._parser import parse_requirement as _parse_requirement from ._tokenizer import ParserSyntaxError from .markers import Marker, _normalize_extra_values from .specifiers import SpecifierSet +from .utils import canonicalize_name class InvalidRequirement(ValueError): @@ -37,57 +37,52 @@ def __init__(self, requirement_string: str) -> None: raise InvalidRequirement(str(e)) from e self.name: str = parsed.name - if parsed.url: - parsed_url = urllib.parse.urlparse(parsed.url) - if parsed_url.scheme == "file": - if urllib.parse.urlunparse(parsed_url) != parsed.url: - raise InvalidRequirement("Invalid URL given") - elif not (parsed_url.scheme and parsed_url.netloc) or ( - not parsed_url.scheme and not parsed_url.netloc - ): - raise InvalidRequirement(f"Invalid URL: {parsed.url}") - self.url: Optional[str] = parsed.url - else: - self.url = None - self.extras: Set[str] = set(parsed.extras if parsed.extras else []) + self.url: Optional[str] = parsed.url or None + self.extras: Set[str] = set(parsed.extras or []) self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) self.marker: Optional[Marker] = None if parsed.marker is not None: self.marker = Marker.__new__(Marker) self.marker._markers = _normalize_extra_values(parsed.marker) - def __str__(self) -> str: - parts: List[str] = [self.name] + def _iter_parts(self, name: str) -> Iterator[str]: + yield name if self.extras: formatted_extras = ",".join(sorted(self.extras)) - parts.append(f"[{formatted_extras}]") + yield f"[{formatted_extras}]" if self.specifier: - parts.append(str(self.specifier)) + yield str(self.specifier) if self.url: - parts.append(f"@ {self.url}") + yield f"@ {self.url}" if self.marker: - parts.append(" ") + yield " " if self.marker: - parts.append(f"; {self.marker}") + yield f"; {self.marker}" - return "".join(parts) + def __str__(self) -> str: + return "".join(self._iter_parts(self.name)) def __repr__(self) -> str: return f"" def __hash__(self) -> int: - return hash((self.__class__.__name__, str(self))) + return hash( + ( + self.__class__.__name__, + *self._iter_parts(canonicalize_name(self.name)), + ) + ) def __eq__(self, other: Any) -> bool: if not isinstance(other, Requirement): return NotImplemented return ( - self.name == other.name + canonicalize_name(self.name) == canonicalize_name(other.name) and self.extras == other.extras and self.specifier == other.specifier and self.url == other.url diff --git a/pkg_resources/_vendor/packaging/specifiers.py b/pkg_resources/_vendor/packaging/specifiers.py index ba8fe37b7f7..2d015bab595 100644 --- a/pkg_resources/_vendor/packaging/specifiers.py +++ b/pkg_resources/_vendor/packaging/specifiers.py @@ -11,17 +11,7 @@ import abc import itertools import re -from typing import ( - Callable, - Iterable, - Iterator, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union from .utils import canonicalize_version from .version import Version @@ -383,7 +373,7 @@ def _compare_compatible(self, prospective: Version, spec: str) -> bool: # We want everything but the last item in the version, but we want to # ignore suffix segments. - prefix = ".".join( + prefix = _version_join( list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] ) @@ -404,13 +394,13 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool: ) # Get the normalized version string ignoring the trailing .* normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) - # Split the spec out by dots, and pretend that there is an implicit - # dot in between a release segment and a pre-release segment. + # Split the spec out by bangs and dots, and pretend that there is + # an implicit dot in between a release segment and a pre-release segment. split_spec = _version_split(normalized_spec) - # Split the prospective version out by dots, and pretend that there - # is an implicit dot in between a release segment and a pre-release - # segment. + # Split the prospective version out by bangs and dots, and pretend + # that there is an implicit dot in between a release segment and + # a pre-release segment. split_prospective = _version_split(normalized_prospective) # 0-pad the prospective version before shortening it to get the correct @@ -644,8 +634,19 @@ def filter( def _version_split(version: str) -> List[str]: + """Split version into components. + + The split components are intended for version comparison. The logic does + not attempt to retain the original version string, so joining the + components back with :func:`_version_join` may not produce the original + version string. + """ result: List[str] = [] - for item in version.split("."): + + epoch, _, rest = version.rpartition("!") + result.append(epoch or "0") + + for item in rest.split("."): match = _prefix_regex.search(item) if match: result.extend(match.groups()) @@ -654,6 +655,17 @@ def _version_split(version: str) -> List[str]: return result +def _version_join(components: List[str]) -> str: + """Join split version components into a version string. + + This function assumes the input came from :func:`_version_split`, where the + first component must be the epoch (either empty or numeric), and all other + components numeric. + """ + epoch, *rest = components + return f"{epoch}!{'.'.join(rest)}" + + def _is_not_suffix(segment: str) -> bool: return not any( segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") @@ -675,7 +687,10 @@ def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) - return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) + return ( + list(itertools.chain.from_iterable(left_split)), + list(itertools.chain.from_iterable(right_split)), + ) class SpecifierSet(BaseSpecifier): @@ -707,14 +722,8 @@ def __init__( # strip each item to remove leading/trailing whitespace. split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] - # Parsed each individual specifier, attempting first to make it a - # Specifier. - parsed: Set[Specifier] = set() - for specifier in split_specifiers: - parsed.add(Specifier(specifier)) - - # Turn our parsed specifiers into a frozen set and save them for later. - self._specs = frozenset(parsed) + # Make each individual specifier a Specifier and save in a frozen set for later. + self._specs = frozenset(map(Specifier, split_specifiers)) # Store our prereleases value so we can use it later to determine if # we accept prereleases or not. diff --git a/pkg_resources/_vendor/packaging/tags.py b/pkg_resources/_vendor/packaging/tags.py index 76d243414d0..89f1926137d 100644 --- a/pkg_resources/_vendor/packaging/tags.py +++ b/pkg_resources/_vendor/packaging/tags.py @@ -4,6 +4,8 @@ import logging import platform +import re +import struct import subprocess import sys import sysconfig @@ -37,7 +39,7 @@ } -_32_BIT_INTERPRETER = sys.maxsize <= 2**32 +_32_BIT_INTERPRETER = struct.calcsize("P") == 4 class Tag: @@ -123,20 +125,37 @@ def _normalize_string(string: str) -> str: return string.replace(".", "_").replace("-", "_").replace(" ", "_") -def _abi3_applies(python_version: PythonVersion) -> bool: +def _is_threaded_cpython(abis: List[str]) -> bool: + """ + Determine if the ABI corresponds to a threaded (`--disable-gil`) build. + + The threaded builds are indicated by a "t" in the abiflags. + """ + if len(abis) == 0: + return False + # expect e.g., cp313 + m = re.match(r"cp\d+(.*)", abis[0]) + if not m: + return False + abiflags = m.group(1) + return "t" in abiflags + + +def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: """ Determine if the Python version supports abi3. - PEP 384 was first implemented in Python 3.2. + PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) + builds do not support abi3. """ - return len(python_version) > 1 and tuple(python_version) >= (3, 2) + return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] version = _version_nodot(py_version[:2]) - debug = pymalloc = ucs4 = "" + threading = debug = pymalloc = ucs4 = "" with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled @@ -145,6 +164,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: has_ext = "_d.pyd" in EXTENSION_SUFFIXES if with_debug or (with_debug is None and (has_refcount or has_ext)): debug = "d" + if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): + threading = "t" if py_version < (3, 8): with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) if with_pymalloc or with_pymalloc is None: @@ -158,13 +179,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: elif debug: # Debug builds can also load "normal" extension modules. # We can also assume no UCS-4 or pymalloc requirement. - abis.append(f"cp{version}") - abis.insert( - 0, - "cp{version}{debug}{pymalloc}{ucs4}".format( - version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 - ), - ) + abis.append(f"cp{version}{threading}") + abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") return abis @@ -212,11 +228,14 @@ def cpython_tags( for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - if _abi3_applies(python_version): + + threading = _is_threaded_cpython(abis) + use_abi3 = _abi3_applies(python_version, threading) + if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) - if _abi3_applies(python_version): + if use_abi3: for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: interpreter = "cp{version}".format( @@ -406,7 +425,7 @@ def mac_platforms( check=True, env={"SYSTEM_VERSION_COMPAT": "0"}, stdout=subprocess.PIPE, - universal_newlines=True, + text=True, ).stdout version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) else: @@ -469,15 +488,21 @@ def mac_platforms( def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: linux = _normalize_string(sysconfig.get_platform()) + if not linux.startswith("linux_"): + # we should never be here, just yield the sysconfig one and return + yield linux + return if is_32bit: if linux == "linux_x86_64": linux = "linux_i686" elif linux == "linux_aarch64": - linux = "linux_armv7l" + linux = "linux_armv8l" _, arch = linux.split("_", 1) - yield from _manylinux.platform_tags(linux, arch) - yield from _musllinux.platform_tags(arch) - yield linux + archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) + yield from _manylinux.platform_tags(archs) + yield from _musllinux.platform_tags(archs) + for arch in archs: + yield f"linux_{arch}" def _generic_platforms() -> Iterator[str]: diff --git a/pkg_resources/_vendor/packaging/utils.py b/pkg_resources/_vendor/packaging/utils.py index 33c613b749a..c2c2f75aa80 100644 --- a/pkg_resources/_vendor/packaging/utils.py +++ b/pkg_resources/_vendor/packaging/utils.py @@ -12,6 +12,12 @@ NormalizedName = NewType("NormalizedName", str) +class InvalidName(ValueError): + """ + An invalid distribution name; users should refer to the packaging user guide. + """ + + class InvalidWheelFilename(ValueError): """ An invalid wheel filename was found, users should refer to PEP 427. @@ -24,17 +30,28 @@ class InvalidSdistFilename(ValueError): """ +# Core metadata spec for `Name` +_validate_regex = re.compile( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE +) _canonicalize_regex = re.compile(r"[-_.]+") +_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") # PEP 427: The build number must start with a digit. _build_tag_regex = re.compile(r"(\d+)(.*)") -def canonicalize_name(name: str) -> NormalizedName: +def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: + if validate and not _validate_regex.match(name): + raise InvalidName(f"name is invalid: {name!r}") # This is taken from PEP 503. value = _canonicalize_regex.sub("-", name).lower() return cast(NormalizedName, value) +def is_normalized_name(name: str) -> bool: + return _normalized_regex.match(name) is not None + + def canonicalize_version( version: Union[Version, str], *, strip_trailing_zero: bool = True ) -> str: @@ -100,11 +117,18 @@ def parse_wheel_filename( parts = filename.split("-", dashes - 2) name_part = parts[0] - # See PEP 427 for the rules on escaping the project name + # See PEP 427 for the rules on escaping the project name. if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: raise InvalidWheelFilename(f"Invalid project name: {filename}") name = canonicalize_name(name_part) - version = Version(parts[1]) + + try: + version = Version(parts[1]) + except InvalidVersion as e: + raise InvalidWheelFilename( + f"Invalid wheel filename (invalid version): {filename}" + ) from e + if dashes == 5: build_part = parts[2] build_match = _build_tag_regex.match(build_part) @@ -137,5 +161,12 @@ def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") name = canonicalize_name(name_part) - version = Version(version_part) + + try: + version = Version(version_part) + except InvalidVersion as e: + raise InvalidSdistFilename( + f"Invalid sdist filename (invalid version): {filename}" + ) from e + return (name, version) diff --git a/pkg_resources/_vendor/packaging/version.py b/pkg_resources/_vendor/packaging/version.py index b30e8cbf84f..5faab9bd0dc 100644 --- a/pkg_resources/_vendor/packaging/version.py +++ b/pkg_resources/_vendor/packaging/version.py @@ -7,37 +7,39 @@ from packaging.version import parse, Version """ -import collections import itertools import re -from typing import Any, Callable, Optional, SupportsInt, Tuple, Union +from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType __all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] -InfiniteTypes = Union[InfinityType, NegativeInfinityType] -PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] -SubLocalType = Union[InfiniteTypes, int, str] -LocalType = Union[ +LocalType = Tuple[Union[int, str], ...] + +CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] +CmpLocalType = Union[ NegativeInfinityType, - Tuple[ - Union[ - SubLocalType, - Tuple[SubLocalType, str], - Tuple[NegativeInfinityType, SubLocalType], - ], - ..., - ], + Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], ] CmpKey = Tuple[ - int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType + int, + Tuple[int, ...], + CmpPrePostDevType, + CmpPrePostDevType, + CmpPrePostDevType, + CmpLocalType, ] VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] -_Version = collections.namedtuple( - "_Version", ["epoch", "release", "dev", "pre", "post", "local"] -) + +class _Version(NamedTuple): + epoch: int + release: Tuple[int, ...] + dev: Optional[Tuple[str, int]] + pre: Optional[Tuple[str, int]] + post: Optional[Tuple[str, int]] + local: Optional[LocalType] def parse(version: str) -> "Version": @@ -117,7 +119,7 @@ def __ne__(self, other: object) -> bool: (?P[0-9]+(?:\.[0-9]+)*) # release segment (?P
                                          # pre-release
             [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            (?Palpha|a|beta|b|preview|pre|c|rc)
             [-_\.]?
             (?P[0-9]+)?
         )?
@@ -269,8 +271,7 @@ def epoch(self) -> int:
         >>> Version("1!2.0.0").epoch
         1
         """
-        _epoch: int = self._version.epoch
-        return _epoch
+        return self._version.epoch
 
     @property
     def release(self) -> Tuple[int, ...]:
@@ -286,8 +287,7 @@ def release(self) -> Tuple[int, ...]:
         Includes trailing zeroes but not the epoch or any pre-release / development /
         post-release suffixes.
         """
-        _release: Tuple[int, ...] = self._version.release
-        return _release
+        return self._version.release
 
     @property
     def pre(self) -> Optional[Tuple[str, int]]:
@@ -302,8 +302,7 @@ def pre(self) -> Optional[Tuple[str, int]]:
         >>> Version("1.2.3rc1").pre
         ('rc', 1)
         """
-        _pre: Optional[Tuple[str, int]] = self._version.pre
-        return _pre
+        return self._version.pre
 
     @property
     def post(self) -> Optional[int]:
@@ -451,7 +450,7 @@ def micro(self) -> int:
 
 
 def _parse_letter_version(
-    letter: str, number: Union[str, bytes, SupportsInt]
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
 ) -> Optional[Tuple[str, int]]:
 
     if letter:
@@ -489,7 +488,7 @@ def _parse_letter_version(
 _local_version_separators = re.compile(r"[\._-]")
 
 
-def _parse_local_version(local: str) -> Optional[LocalType]:
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -507,7 +506,7 @@ def _cmpkey(
     pre: Optional[Tuple[str, int]],
     post: Optional[Tuple[str, int]],
     dev: Optional[Tuple[str, int]],
-    local: Optional[Tuple[SubLocalType]],
+    local: Optional[LocalType],
 ) -> CmpKey:
 
     # When we compare a release version, we want to compare it with all of the
@@ -524,7 +523,7 @@ def _cmpkey(
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        _pre: PrePostDevType = NegativeInfinity
+        _pre: CmpPrePostDevType = NegativeInfinity
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
@@ -534,21 +533,21 @@ def _cmpkey(
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        _post: PrePostDevType = NegativeInfinity
+        _post: CmpPrePostDevType = NegativeInfinity
 
     else:
         _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        _dev: PrePostDevType = Infinity
+        _dev: CmpPrePostDevType = Infinity
 
     else:
         _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        _local: LocalType = NegativeInfinity
+        _local: CmpLocalType = NegativeInfinity
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt
index 11389159219..c18a2cc0ebd 100644
--- a/pkg_resources/_vendor/vendored.txt
+++ b/pkg_resources/_vendor/vendored.txt
@@ -1,4 +1,4 @@
-packaging==23.1
+packaging==24
 
 platformdirs==2.6.2
 # required for platformdirs on Python < 3.8
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/RECORD b/setuptools/_vendor/packaging-23.1.dist-info/RECORD
deleted file mode 100644
index e041f20f6ad..00000000000
--- a/setuptools/_vendor/packaging-23.1.dist-info/RECORD
+++ /dev/null
@@ -1,37 +0,0 @@
-packaging-23.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-packaging-23.1.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
-packaging-23.1.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
-packaging-23.1.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
-packaging-23.1.dist-info/METADATA,sha256=JnduJDlxs2IVeB-nIqAC3-HyNcPhP_MADd9_k_MjmaI,3082
-packaging-23.1.dist-info/RECORD,,
-packaging-23.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-packaging-23.1.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81
-packaging/__init__.py,sha256=kYVZSmXT6CWInT4UJPDtrSQBAZu8fMuFBxpv5GsDTLk,501
-packaging/__pycache__/__init__.cpython-312.pyc,,
-packaging/__pycache__/_elffile.cpython-312.pyc,,
-packaging/__pycache__/_manylinux.cpython-312.pyc,,
-packaging/__pycache__/_musllinux.cpython-312.pyc,,
-packaging/__pycache__/_parser.cpython-312.pyc,,
-packaging/__pycache__/_structures.cpython-312.pyc,,
-packaging/__pycache__/_tokenizer.cpython-312.pyc,,
-packaging/__pycache__/markers.cpython-312.pyc,,
-packaging/__pycache__/metadata.cpython-312.pyc,,
-packaging/__pycache__/requirements.cpython-312.pyc,,
-packaging/__pycache__/specifiers.cpython-312.pyc,,
-packaging/__pycache__/tags.cpython-312.pyc,,
-packaging/__pycache__/utils.cpython-312.pyc,,
-packaging/__pycache__/version.cpython-312.pyc,,
-packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266
-packaging/_manylinux.py,sha256=ESGrDEVmBc8jYTtdZRAWiLk72lOzAKWeezFgoJ_MuBc,8926
-packaging/_musllinux.py,sha256=mvPk7FNjjILKRLIdMxR7IvJ1uggLgCszo-L9rjfpi0M,2524
-packaging/_parser.py,sha256=KJQkBh_Xbfb-qsB560YIEItrTpCZaOh4_YMfBtd5XIY,10194
-packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
-packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292
-packaging/markers.py,sha256=eH-txS2zq1HdNpTd9LcZUcVIwewAiNU0grmq5wjKnOk,8208
-packaging/metadata.py,sha256=PjELMLxKG_iu3HWjKAOdKhuNrHfWgpdTF2Q4nObsZeM,16397
-packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-packaging/requirements.py,sha256=hJzvtJyAvENc_VfwfhnOZV1851-VW8JCGh-R96NE4Pc,3287
-packaging/specifiers.py,sha256=ZOpqL_w_Kj6ZF_OWdliQUzhEyHlDbi6989kr-sF5GHs,39206
-packaging/tags.py,sha256=_1gLX8h1SgpjAdYCP9XqU37zRjXtU5ZliGy3IM-WcSM,18106
-packaging/utils.py,sha256=es0cCezKspzriQ-3V88h3yJzxz028euV2sUwM61kE-o,4355
-packaging/version.py,sha256=2NH3E57hzRhn0BV9boUBvgPsxlTqLJeI0EpYQoNvGi0,16326
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/INSTALLER b/setuptools/_vendor/packaging-24.0.dist-info/INSTALLER
similarity index 100%
rename from setuptools/_vendor/packaging-23.1.dist-info/INSTALLER
rename to setuptools/_vendor/packaging-24.0.dist-info/INSTALLER
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/LICENSE b/setuptools/_vendor/packaging-24.0.dist-info/LICENSE
similarity index 100%
rename from setuptools/_vendor/packaging-23.1.dist-info/LICENSE
rename to setuptools/_vendor/packaging-24.0.dist-info/LICENSE
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/LICENSE.APACHE b/setuptools/_vendor/packaging-24.0.dist-info/LICENSE.APACHE
similarity index 100%
rename from setuptools/_vendor/packaging-23.1.dist-info/LICENSE.APACHE
rename to setuptools/_vendor/packaging-24.0.dist-info/LICENSE.APACHE
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/LICENSE.BSD b/setuptools/_vendor/packaging-24.0.dist-info/LICENSE.BSD
similarity index 100%
rename from setuptools/_vendor/packaging-23.1.dist-info/LICENSE.BSD
rename to setuptools/_vendor/packaging-24.0.dist-info/LICENSE.BSD
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/METADATA b/setuptools/_vendor/packaging-24.0.dist-info/METADATA
similarity index 95%
rename from setuptools/_vendor/packaging-23.1.dist-info/METADATA
rename to setuptools/_vendor/packaging-24.0.dist-info/METADATA
index c43882a826a..10ab4390a96 100644
--- a/setuptools/_vendor/packaging-23.1.dist-info/METADATA
+++ b/setuptools/_vendor/packaging-24.0.dist-info/METADATA
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: packaging
-Version: 23.1
+Version: 24.0
 Summary: Core utilities for Python packages
 Author-email: Donald Stufft 
 Requires-Python: >=3.7
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Typing :: Typed
@@ -59,6 +60,8 @@ Use ``pip`` to install these utilities::
 
     pip install packaging
 
+The ``packaging`` library uses calendar-based versioning (``YY.N``).
+
 Discussion
 ----------
 
diff --git a/setuptools/_vendor/packaging-24.0.dist-info/RECORD b/setuptools/_vendor/packaging-24.0.dist-info/RECORD
new file mode 100644
index 00000000000..bcf796c2f49
--- /dev/null
+++ b/setuptools/_vendor/packaging-24.0.dist-info/RECORD
@@ -0,0 +1,37 @@
+packaging-24.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+packaging-24.0.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
+packaging-24.0.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
+packaging-24.0.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
+packaging-24.0.dist-info/METADATA,sha256=0dESdhY_wHValuOrbgdebiEw04EbX4dkujlxPdEsFus,3203
+packaging-24.0.dist-info/RECORD,,
+packaging-24.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+packaging-24.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
+packaging/__init__.py,sha256=UzotcV07p8vcJzd80S-W0srhgY8NMVD_XvJcZ7JN-tA,496
+packaging/__pycache__/__init__.cpython-312.pyc,,
+packaging/__pycache__/_elffile.cpython-312.pyc,,
+packaging/__pycache__/_manylinux.cpython-312.pyc,,
+packaging/__pycache__/_musllinux.cpython-312.pyc,,
+packaging/__pycache__/_parser.cpython-312.pyc,,
+packaging/__pycache__/_structures.cpython-312.pyc,,
+packaging/__pycache__/_tokenizer.cpython-312.pyc,,
+packaging/__pycache__/markers.cpython-312.pyc,,
+packaging/__pycache__/metadata.cpython-312.pyc,,
+packaging/__pycache__/requirements.cpython-312.pyc,,
+packaging/__pycache__/specifiers.cpython-312.pyc,,
+packaging/__pycache__/tags.cpython-312.pyc,,
+packaging/__pycache__/utils.cpython-312.pyc,,
+packaging/__pycache__/version.cpython-312.pyc,,
+packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266
+packaging/_manylinux.py,sha256=1ng_TqyH49hY6s3W_zVHyoJIaogbJqbIF1jJ0fAehc4,9590
+packaging/_musllinux.py,sha256=kgmBGLFybpy8609-KTvzmt2zChCPWYvhp5BWP4JX7dE,2676
+packaging/_parser.py,sha256=zlsFB1FpMRjkUdQb6WLq7xON52ruQadxFpYsDXWhLb4,10347
+packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
+packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292
+packaging/markers.py,sha256=eH-txS2zq1HdNpTd9LcZUcVIwewAiNU0grmq5wjKnOk,8208
+packaging/metadata.py,sha256=w7jPEg6mDf1FTZMn79aFxFuk4SKtynUJtxr2InTxlV4,33036
+packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933
+packaging/specifiers.py,sha256=dB2DwbmvSbEuVilEyiIQ382YfW5JfwzXTfRRPVtaENY,39784
+packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950
+packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268
+packaging/version.py,sha256=XjRBLNK17UMDgLeP8UHnqwiY3TdSi03xFQURtec211A,16236
diff --git a/setuptools/_vendor/packaging-23.1.dist-info/REQUESTED b/setuptools/_vendor/packaging-24.0.dist-info/REQUESTED
similarity index 100%
rename from setuptools/_vendor/packaging-23.1.dist-info/REQUESTED
rename to setuptools/_vendor/packaging-24.0.dist-info/REQUESTED
diff --git a/pkg_resources/_vendor/packaging-23.1.dist-info/WHEEL b/setuptools/_vendor/packaging-24.0.dist-info/WHEEL
similarity index 72%
rename from pkg_resources/_vendor/packaging-23.1.dist-info/WHEEL
rename to setuptools/_vendor/packaging-24.0.dist-info/WHEEL
index db4a255f3a2..3b5e64b5e6c 100644
--- a/pkg_resources/_vendor/packaging-23.1.dist-info/WHEEL
+++ b/setuptools/_vendor/packaging-24.0.dist-info/WHEEL
@@ -1,4 +1,4 @@
 Wheel-Version: 1.0
-Generator: flit 3.8.0
+Generator: flit 3.9.0
 Root-Is-Purelib: true
 Tag: py3-none-any
diff --git a/setuptools/_vendor/packaging/__init__.py b/setuptools/_vendor/packaging/__init__.py
index 13cadc7f04d..e7c0aa12ca9 100644
--- a/setuptools/_vendor/packaging/__init__.py
+++ b/setuptools/_vendor/packaging/__init__.py
@@ -6,10 +6,10 @@
 __summary__ = "Core utilities for Python packages"
 __uri__ = "https://github.com/pypa/packaging"
 
-__version__ = "23.1"
+__version__ = "24.0"
 
 __author__ = "Donald Stufft and individual contributors"
 __email__ = "donald@stufft.io"
 
 __license__ = "BSD-2-Clause or Apache-2.0"
-__copyright__ = "2014-2019 %s" % __author__
+__copyright__ = "2014 %s" % __author__
diff --git a/setuptools/_vendor/packaging/_manylinux.py b/setuptools/_vendor/packaging/_manylinux.py
index 449c655be65..ad62505f3ff 100644
--- a/setuptools/_vendor/packaging/_manylinux.py
+++ b/setuptools/_vendor/packaging/_manylinux.py
@@ -5,7 +5,7 @@
 import re
 import sys
 import warnings
-from typing import Dict, Generator, Iterator, NamedTuple, Optional, Tuple
+from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple
 
 from ._elffile import EIClass, EIData, ELFFile, EMachine
 
@@ -50,12 +50,21 @@ def _is_linux_i686(executable: str) -> bool:
         )
 
 
-def _have_compatible_abi(executable: str, arch: str) -> bool:
-    if arch == "armv7l":
+def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
+    if "armv7l" in archs:
         return _is_linux_armhf(executable)
-    if arch == "i686":
+    if "i686" in archs:
         return _is_linux_i686(executable)
-    return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"}
+    allowed_archs = {
+        "x86_64",
+        "aarch64",
+        "ppc64",
+        "ppc64le",
+        "s390x",
+        "loongarch64",
+        "riscv64",
+    }
+    return any(arch in allowed_archs for arch in archs)
 
 
 # If glibc ever changes its major version, we need to know what the last
@@ -81,7 +90,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
     # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
     try:
         # Should be a string like "glibc 2.17".
-        version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION")
+        version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION")
         assert version_string is not None
         _, version = version_string.rsplit()
     except (AssertionError, AttributeError, OSError, ValueError):
@@ -167,13 +176,13 @@ def _get_glibc_version() -> Tuple[int, int]:
 
 
 # From PEP 513, PEP 600
-def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool:
+def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
     sys_glibc = _get_glibc_version()
     if sys_glibc < version:
         return False
     # Check for presence of _manylinux module.
     try:
-        import _manylinux  # noqa
+        import _manylinux
     except ImportError:
         return True
     if hasattr(_manylinux, "manylinux_compatible"):
@@ -203,12 +212,22 @@ def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool:
 }
 
 
-def platform_tags(linux: str, arch: str) -> Iterator[str]:
-    if not _have_compatible_abi(sys.executable, arch):
+def platform_tags(archs: Sequence[str]) -> Iterator[str]:
+    """Generate manylinux tags compatible to the current platform.
+
+    :param archs: Sequence of compatible architectures.
+        The first one shall be the closest to the actual architecture and be the part of
+        platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
+        The ``linux_`` prefix is assumed as a prerequisite for the current platform to
+        be manylinux-compatible.
+
+    :returns: An iterator of compatible manylinux tags.
+    """
+    if not _have_compatible_abi(sys.executable, archs):
         return
     # Oldest glibc to be supported regardless of architecture is (2, 17).
     too_old_glibc2 = _GLibCVersion(2, 16)
-    if arch in {"x86_64", "i686"}:
+    if set(archs) & {"x86_64", "i686"}:
         # On x86/i686 also oldest glibc to be supported is (2, 5).
         too_old_glibc2 = _GLibCVersion(2, 4)
     current_glibc = _GLibCVersion(*_get_glibc_version())
@@ -222,19 +241,20 @@ def platform_tags(linux: str, arch: str) -> Iterator[str]:
     for glibc_major in range(current_glibc.major - 1, 1, -1):
         glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
         glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
-    for glibc_max in glibc_max_list:
-        if glibc_max.major == too_old_glibc2.major:
-            min_minor = too_old_glibc2.minor
-        else:
-            # For other glibc major versions oldest supported is (x, 0).
-            min_minor = -1
-        for glibc_minor in range(glibc_max.minor, min_minor, -1):
-            glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
-            tag = "manylinux_{}_{}".format(*glibc_version)
-            if _is_compatible(tag, arch, glibc_version):
-                yield linux.replace("linux", tag)
-            # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
-            if glibc_version in _LEGACY_MANYLINUX_MAP:
-                legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
-                if _is_compatible(legacy_tag, arch, glibc_version):
-                    yield linux.replace("linux", legacy_tag)
+    for arch in archs:
+        for glibc_max in glibc_max_list:
+            if glibc_max.major == too_old_glibc2.major:
+                min_minor = too_old_glibc2.minor
+            else:
+                # For other glibc major versions oldest supported is (x, 0).
+                min_minor = -1
+            for glibc_minor in range(glibc_max.minor, min_minor, -1):
+                glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
+                tag = "manylinux_{}_{}".format(*glibc_version)
+                if _is_compatible(arch, glibc_version):
+                    yield f"{tag}_{arch}"
+                # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
+                if glibc_version in _LEGACY_MANYLINUX_MAP:
+                    legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
+                    if _is_compatible(arch, glibc_version):
+                        yield f"{legacy_tag}_{arch}"
diff --git a/setuptools/_vendor/packaging/_musllinux.py b/setuptools/_vendor/packaging/_musllinux.py
index 706ba600a93..86419df9d70 100644
--- a/setuptools/_vendor/packaging/_musllinux.py
+++ b/setuptools/_vendor/packaging/_musllinux.py
@@ -8,7 +8,7 @@
 import re
 import subprocess
 import sys
-from typing import Iterator, NamedTuple, Optional
+from typing import Iterator, NamedTuple, Optional, Sequence
 
 from ._elffile import ELFFile
 
@@ -47,24 +47,27 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
         return None
     if ld is None or "musl" not in ld:
         return None
-    proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
+    proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
     return _parse_musl_version(proc.stderr)
 
 
-def platform_tags(arch: str) -> Iterator[str]:
+def platform_tags(archs: Sequence[str]) -> Iterator[str]:
     """Generate musllinux tags compatible to the current platform.
 
-    :param arch: Should be the part of platform tag after the ``linux_``
-        prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
-        prerequisite for the current platform to be musllinux-compatible.
+    :param archs: Sequence of compatible architectures.
+        The first one shall be the closest to the actual architecture and be the part of
+        platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
+        The ``linux_`` prefix is assumed as a prerequisite for the current platform to
+        be musllinux-compatible.
 
     :returns: An iterator of compatible musllinux tags.
     """
     sys_musl = _get_musl_version(sys.executable)
     if sys_musl is None:  # Python not dynamically linked against musl.
         return
-    for minor in range(sys_musl.minor, -1, -1):
-        yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
+    for arch in archs:
+        for minor in range(sys_musl.minor, -1, -1):
+            yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
 
 
 if __name__ == "__main__":  # pragma: no cover
diff --git a/setuptools/_vendor/packaging/_parser.py b/setuptools/_vendor/packaging/_parser.py
index 5a18b758fe0..684df75457c 100644
--- a/setuptools/_vendor/packaging/_parser.py
+++ b/setuptools/_vendor/packaging/_parser.py
@@ -252,7 +252,13 @@ def _parse_version_many(tokenizer: Tokenizer) -> str:
 # Recursive descent parser for marker expression
 # --------------------------------------------------------------------------------------
 def parse_marker(source: str) -> MarkerList:
-    return _parse_marker(Tokenizer(source, rules=DEFAULT_RULES))
+    return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
+
+
+def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
+    retval = _parse_marker(tokenizer)
+    tokenizer.expect("END", expected="end of marker expression")
+    return retval
 
 
 def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
@@ -318,10 +324,7 @@ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
 
 
 def process_env_var(env_var: str) -> Variable:
-    if (
-        env_var == "platform_python_implementation"
-        or env_var == "python_implementation"
-    ):
+    if env_var in ("platform_python_implementation", "python_implementation"):
         return Variable("platform_python_implementation")
     else:
         return Variable(env_var)
diff --git a/setuptools/_vendor/packaging/metadata.py b/setuptools/_vendor/packaging/metadata.py
index e76a60c395e..fb274930799 100644
--- a/setuptools/_vendor/packaging/metadata.py
+++ b/setuptools/_vendor/packaging/metadata.py
@@ -5,23 +5,77 @@
 import email.policy
 import sys
 import typing
-from typing import Dict, List, Optional, Tuple, Union, cast
-
-if sys.version_info >= (3, 8):  # pragma: no cover
-    from typing import TypedDict
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Generic,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    Union,
+    cast,
+)
+
+from . import requirements, specifiers, utils, version as version_module
+
+T = typing.TypeVar("T")
+if sys.version_info[:2] >= (3, 8):  # pragma: no cover
+    from typing import Literal, TypedDict
 else:  # pragma: no cover
     if typing.TYPE_CHECKING:
-        from typing_extensions import TypedDict
+        from typing_extensions import Literal, TypedDict
     else:
         try:
-            from typing_extensions import TypedDict
+            from typing_extensions import Literal, TypedDict
         except ImportError:
 
+            class Literal:
+                def __init_subclass__(*_args, **_kwargs):
+                    pass
+
             class TypedDict:
                 def __init_subclass__(*_args, **_kwargs):
                     pass
 
 
+try:
+    ExceptionGroup
+except NameError:  # pragma: no cover
+
+    class ExceptionGroup(Exception):  # noqa: N818
+        """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
+
+        If :external:exc:`ExceptionGroup` is already defined by Python itself,
+        that version is used instead.
+        """
+
+        message: str
+        exceptions: List[Exception]
+
+        def __init__(self, message: str, exceptions: List[Exception]) -> None:
+            self.message = message
+            self.exceptions = exceptions
+
+        def __repr__(self) -> str:
+            return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
+
+else:  # pragma: no cover
+    ExceptionGroup = ExceptionGroup
+
+
+class InvalidMetadata(ValueError):
+    """A metadata field contains invalid data."""
+
+    field: str
+    """The name of the field that contains invalid data."""
+
+    def __init__(self, field: str, message: str) -> None:
+        self.field = field
+        super().__init__(message)
+
+
 # The RawMetadata class attempts to make as few assumptions about the underlying
 # serialization formats as possible. The idea is that as long as a serialization
 # formats offer some very basic primitives in *some* way then we can support
@@ -33,7 +87,8 @@ class RawMetadata(TypedDict, total=False):
     provided). The key is lower-case and underscores are used instead of dashes
     compared to the equivalent core metadata field. Any core metadata field that
     can be specified multiple times or can hold multiple values in a single
-    field have a key with a plural name.
+    field have a key with a plural name. See :class:`Metadata` whose attributes
+    match the keys of this dictionary.
 
     Core metadata fields that can be specified multiple times are stored as a
     list or dict depending on which is appropriate for the field. Any fields
@@ -77,7 +132,7 @@ class RawMetadata(TypedDict, total=False):
     # but got stuck without ever being able to build consensus on
     # it and ultimately ended up withdrawn.
     #
-    # However, a number of tools had started emiting METADATA with
+    # However, a number of tools had started emitting METADATA with
     # `2.0` Metadata-Version, so for historical reasons, this version
     # was skipped.
 
@@ -110,7 +165,7 @@ class RawMetadata(TypedDict, total=False):
     "version",
 }
 
-_LIST_STRING_FIELDS = {
+_LIST_FIELDS = {
     "classifiers",
     "dynamic",
     "obsoletes",
@@ -125,6 +180,10 @@ class RawMetadata(TypedDict, total=False):
     "supported_platforms",
 }
 
+_DICT_FIELDS = {
+    "project_urls",
+}
+
 
 def _parse_keywords(data: str) -> List[str]:
     """Split a string of comma-separate keyboards into a list of keywords."""
@@ -230,10 +289,11 @@ def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str:
     "supported-platform": "supported_platforms",
     "version": "version",
 }
+_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
 
 
 def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]:
-    """Parse a distribution's metadata.
+    """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
 
     This function returns a two-item tuple of dicts. The first dict is of
     recognized fields from the core metadata specification. Fields that can be
@@ -267,7 +327,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
         # We use get_all() here, even for fields that aren't multiple use,
         # because otherwise someone could have e.g. two Name fields, and we
         # would just silently ignore it rather than doing something about it.
-        headers = parsed.get_all(name)
+        headers = parsed.get_all(name) or []
 
         # The way the email module works when parsing bytes is that it
         # unconditionally decodes the bytes as ascii using the surrogateescape
@@ -349,7 +409,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
         # If this is one of our list of string fields, then we can just assign
         # the value, since email *only* has strings, and our get_all() call
         # above ensures that this is a list.
-        elif raw_name in _LIST_STRING_FIELDS:
+        elif raw_name in _LIST_FIELDS:
             raw[raw_name] = value
         # Special Case: Keywords
         # The keywords field is implemented in the metadata spec as a str,
@@ -406,3 +466,360 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
     # way this function is implemented, our `TypedDict` can only have valid key
     # names.
     return cast(RawMetadata, raw), unparsed
+
+
+_NOT_FOUND = object()
+
+
+# Keep the two values in sync.
+_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"]
+_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"]
+
+_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
+
+
+class _Validator(Generic[T]):
+    """Validate a metadata field.
+
+    All _process_*() methods correspond to a core metadata field. The method is
+    called with the field's raw value. If the raw value is valid it is returned
+    in its "enriched" form (e.g. ``version.Version`` for the ``Version`` field).
+    If the raw value is invalid, :exc:`InvalidMetadata` is raised (with a cause
+    as appropriate).
+    """
+
+    name: str
+    raw_name: str
+    added: _MetadataVersion
+
+    def __init__(
+        self,
+        *,
+        added: _MetadataVersion = "1.0",
+    ) -> None:
+        self.added = added
+
+    def __set_name__(self, _owner: "Metadata", name: str) -> None:
+        self.name = name
+        self.raw_name = _RAW_TO_EMAIL_MAPPING[name]
+
+    def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T:
+        # With Python 3.8, the caching can be replaced with functools.cached_property().
+        # No need to check the cache as attribute lookup will resolve into the
+        # instance's __dict__ before __get__ is called.
+        cache = instance.__dict__
+        value = instance._raw.get(self.name)
+
+        # To make the _process_* methods easier, we'll check if the value is None
+        # and if this field is NOT a required attribute, and if both of those
+        # things are true, we'll skip the the converter. This will mean that the
+        # converters never have to deal with the None union.
+        if self.name in _REQUIRED_ATTRS or value is not None:
+            try:
+                converter: Callable[[Any], T] = getattr(self, f"_process_{self.name}")
+            except AttributeError:
+                pass
+            else:
+                value = converter(value)
+
+        cache[self.name] = value
+        try:
+            del instance._raw[self.name]  # type: ignore[misc]
+        except KeyError:
+            pass
+
+        return cast(T, value)
+
+    def _invalid_metadata(
+        self, msg: str, cause: Optional[Exception] = None
+    ) -> InvalidMetadata:
+        exc = InvalidMetadata(
+            self.raw_name, msg.format_map({"field": repr(self.raw_name)})
+        )
+        exc.__cause__ = cause
+        return exc
+
+    def _process_metadata_version(self, value: str) -> _MetadataVersion:
+        # Implicitly makes Metadata-Version required.
+        if value not in _VALID_METADATA_VERSIONS:
+            raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
+        return cast(_MetadataVersion, value)
+
+    def _process_name(self, value: str) -> str:
+        if not value:
+            raise self._invalid_metadata("{field} is a required field")
+        # Validate the name as a side-effect.
+        try:
+            utils.canonicalize_name(value, validate=True)
+        except utils.InvalidName as exc:
+            raise self._invalid_metadata(
+                f"{value!r} is invalid for {{field}}", cause=exc
+            )
+        else:
+            return value
+
+    def _process_version(self, value: str) -> version_module.Version:
+        if not value:
+            raise self._invalid_metadata("{field} is a required field")
+        try:
+            return version_module.parse(value)
+        except version_module.InvalidVersion as exc:
+            raise self._invalid_metadata(
+                f"{value!r} is invalid for {{field}}", cause=exc
+            )
+
+    def _process_summary(self, value: str) -> str:
+        """Check the field contains no newlines."""
+        if "\n" in value:
+            raise self._invalid_metadata("{field} must be a single line")
+        return value
+
+    def _process_description_content_type(self, value: str) -> str:
+        content_types = {"text/plain", "text/x-rst", "text/markdown"}
+        message = email.message.EmailMessage()
+        message["content-type"] = value
+
+        content_type, parameters = (
+            # Defaults to `text/plain` if parsing failed.
+            message.get_content_type().lower(),
+            message["content-type"].params,
+        )
+        # Check if content-type is valid or defaulted to `text/plain` and thus was
+        # not parseable.
+        if content_type not in content_types or content_type not in value.lower():
+            raise self._invalid_metadata(
+                f"{{field}} must be one of {list(content_types)}, not {value!r}"
+            )
+
+        charset = parameters.get("charset", "UTF-8")
+        if charset != "UTF-8":
+            raise self._invalid_metadata(
+                f"{{field}} can only specify the UTF-8 charset, not {list(charset)}"
+            )
+
+        markdown_variants = {"GFM", "CommonMark"}
+        variant = parameters.get("variant", "GFM")  # Use an acceptable default.
+        if content_type == "text/markdown" and variant not in markdown_variants:
+            raise self._invalid_metadata(
+                f"valid Markdown variants for {{field}} are {list(markdown_variants)}, "
+                f"not {variant!r}",
+            )
+        return value
+
+    def _process_dynamic(self, value: List[str]) -> List[str]:
+        for dynamic_field in map(str.lower, value):
+            if dynamic_field in {"name", "version", "metadata-version"}:
+                raise self._invalid_metadata(
+                    f"{value!r} is not allowed as a dynamic field"
+                )
+            elif dynamic_field not in _EMAIL_TO_RAW_MAPPING:
+                raise self._invalid_metadata(f"{value!r} is not a valid dynamic field")
+        return list(map(str.lower, value))
+
+    def _process_provides_extra(
+        self,
+        value: List[str],
+    ) -> List[utils.NormalizedName]:
+        normalized_names = []
+        try:
+            for name in value:
+                normalized_names.append(utils.canonicalize_name(name, validate=True))
+        except utils.InvalidName as exc:
+            raise self._invalid_metadata(
+                f"{name!r} is invalid for {{field}}", cause=exc
+            )
+        else:
+            return normalized_names
+
+    def _process_requires_python(self, value: str) -> specifiers.SpecifierSet:
+        try:
+            return specifiers.SpecifierSet(value)
+        except specifiers.InvalidSpecifier as exc:
+            raise self._invalid_metadata(
+                f"{value!r} is invalid for {{field}}", cause=exc
+            )
+
+    def _process_requires_dist(
+        self,
+        value: List[str],
+    ) -> List[requirements.Requirement]:
+        reqs = []
+        try:
+            for req in value:
+                reqs.append(requirements.Requirement(req))
+        except requirements.InvalidRequirement as exc:
+            raise self._invalid_metadata(f"{req!r} is invalid for {{field}}", cause=exc)
+        else:
+            return reqs
+
+
+class Metadata:
+    """Representation of distribution metadata.
+
+    Compared to :class:`RawMetadata`, this class provides objects representing
+    metadata fields instead of only using built-in types. Any invalid metadata
+    will cause :exc:`InvalidMetadata` to be raised (with a
+    :py:attr:`~BaseException.__cause__` attribute as appropriate).
+    """
+
+    _raw: RawMetadata
+
+    @classmethod
+    def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata":
+        """Create an instance from :class:`RawMetadata`.
+
+        If *validate* is true, all metadata will be validated. All exceptions
+        related to validation will be gathered and raised as an :class:`ExceptionGroup`.
+        """
+        ins = cls()
+        ins._raw = data.copy()  # Mutations occur due to caching enriched values.
+
+        if validate:
+            exceptions: List[Exception] = []
+            try:
+                metadata_version = ins.metadata_version
+                metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
+            except InvalidMetadata as metadata_version_exc:
+                exceptions.append(metadata_version_exc)
+                metadata_version = None
+
+            # Make sure to check for the fields that are present, the required
+            # fields (so their absence can be reported).
+            fields_to_check = frozenset(ins._raw) | _REQUIRED_ATTRS
+            # Remove fields that have already been checked.
+            fields_to_check -= {"metadata_version"}
+
+            for key in fields_to_check:
+                try:
+                    if metadata_version:
+                        # Can't use getattr() as that triggers descriptor protocol which
+                        # will fail due to no value for the instance argument.
+                        try:
+                            field_metadata_version = cls.__dict__[key].added
+                        except KeyError:
+                            exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
+                            exceptions.append(exc)
+                            continue
+                        field_age = _VALID_METADATA_VERSIONS.index(
+                            field_metadata_version
+                        )
+                        if field_age > metadata_age:
+                            field = _RAW_TO_EMAIL_MAPPING[key]
+                            exc = InvalidMetadata(
+                                field,
+                                "{field} introduced in metadata version "
+                                "{field_metadata_version}, not {metadata_version}",
+                            )
+                            exceptions.append(exc)
+                            continue
+                    getattr(ins, key)
+                except InvalidMetadata as exc:
+                    exceptions.append(exc)
+
+            if exceptions:
+                raise ExceptionGroup("invalid metadata", exceptions)
+
+        return ins
+
+    @classmethod
+    def from_email(
+        cls, data: Union[bytes, str], *, validate: bool = True
+    ) -> "Metadata":
+        """Parse metadata from email headers.
+
+        If *validate* is true, the metadata will be validated. All exceptions
+        related to validation will be gathered and raised as an :class:`ExceptionGroup`.
+        """
+        raw, unparsed = parse_email(data)
+
+        if validate:
+            exceptions: list[Exception] = []
+            for unparsed_key in unparsed:
+                if unparsed_key in _EMAIL_TO_RAW_MAPPING:
+                    message = f"{unparsed_key!r} has invalid data"
+                else:
+                    message = f"unrecognized field: {unparsed_key!r}"
+                exceptions.append(InvalidMetadata(unparsed_key, message))
+
+            if exceptions:
+                raise ExceptionGroup("unparsed", exceptions)
+
+        try:
+            return cls.from_raw(raw, validate=validate)
+        except ExceptionGroup as exc_group:
+            raise ExceptionGroup(
+                "invalid or unparsed metadata", exc_group.exceptions
+            ) from None
+
+    metadata_version: _Validator[_MetadataVersion] = _Validator()
+    """:external:ref:`core-metadata-metadata-version`
+    (required; validated to be a valid metadata version)"""
+    name: _Validator[str] = _Validator()
+    """:external:ref:`core-metadata-name`
+    (required; validated using :func:`~packaging.utils.canonicalize_name` and its
+    *validate* parameter)"""
+    version: _Validator[version_module.Version] = _Validator()
+    """:external:ref:`core-metadata-version` (required)"""
+    dynamic: _Validator[Optional[List[str]]] = _Validator(
+        added="2.2",
+    )
+    """:external:ref:`core-metadata-dynamic`
+    (validated against core metadata field names and lowercased)"""
+    platforms: _Validator[Optional[List[str]]] = _Validator()
+    """:external:ref:`core-metadata-platform`"""
+    supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    """:external:ref:`core-metadata-supported-platform`"""
+    summary: _Validator[Optional[str]] = _Validator()
+    """:external:ref:`core-metadata-summary` (validated to contain no newlines)"""
+    description: _Validator[Optional[str]] = _Validator()  # TODO 2.1: can be in body
+    """:external:ref:`core-metadata-description`"""
+    description_content_type: _Validator[Optional[str]] = _Validator(added="2.1")
+    """:external:ref:`core-metadata-description-content-type` (validated)"""
+    keywords: _Validator[Optional[List[str]]] = _Validator()
+    """:external:ref:`core-metadata-keywords`"""
+    home_page: _Validator[Optional[str]] = _Validator()
+    """:external:ref:`core-metadata-home-page`"""
+    download_url: _Validator[Optional[str]] = _Validator(added="1.1")
+    """:external:ref:`core-metadata-download-url`"""
+    author: _Validator[Optional[str]] = _Validator()
+    """:external:ref:`core-metadata-author`"""
+    author_email: _Validator[Optional[str]] = _Validator()
+    """:external:ref:`core-metadata-author-email`"""
+    maintainer: _Validator[Optional[str]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-maintainer`"""
+    maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-maintainer-email`"""
+    license: _Validator[Optional[str]] = _Validator()
+    """:external:ref:`core-metadata-license`"""
+    classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    """:external:ref:`core-metadata-classifier`"""
+    requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator(
+        added="1.2"
+    )
+    """:external:ref:`core-metadata-requires-dist`"""
+    requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator(
+        added="1.2"
+    )
+    """:external:ref:`core-metadata-requires-python`"""
+    # Because `Requires-External` allows for non-PEP 440 version specifiers, we
+    # don't do any processing on the values.
+    requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-requires-external`"""
+    project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-project-url`"""
+    # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation
+    # regardless of metadata version.
+    provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator(
+        added="2.1",
+    )
+    """:external:ref:`core-metadata-provides-extra`"""
+    provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-provides-dist`"""
+    obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    """:external:ref:`core-metadata-obsoletes-dist`"""
+    requires: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    """``Requires`` (deprecated)"""
+    provides: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    """``Provides`` (deprecated)"""
+    obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    """``Obsoletes`` (deprecated)"""
diff --git a/setuptools/_vendor/packaging/requirements.py b/setuptools/_vendor/packaging/requirements.py
index f34bfa85c80..bdc43a7e98d 100644
--- a/setuptools/_vendor/packaging/requirements.py
+++ b/setuptools/_vendor/packaging/requirements.py
@@ -2,13 +2,13 @@
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
 
-import urllib.parse
-from typing import Any, List, Optional, Set
+from typing import Any, Iterator, Optional, Set
 
 from ._parser import parse_requirement as _parse_requirement
 from ._tokenizer import ParserSyntaxError
 from .markers import Marker, _normalize_extra_values
 from .specifiers import SpecifierSet
+from .utils import canonicalize_name
 
 
 class InvalidRequirement(ValueError):
@@ -37,57 +37,52 @@ def __init__(self, requirement_string: str) -> None:
             raise InvalidRequirement(str(e)) from e
 
         self.name: str = parsed.name
-        if parsed.url:
-            parsed_url = urllib.parse.urlparse(parsed.url)
-            if parsed_url.scheme == "file":
-                if urllib.parse.urlunparse(parsed_url) != parsed.url:
-                    raise InvalidRequirement("Invalid URL given")
-            elif not (parsed_url.scheme and parsed_url.netloc) or (
-                not parsed_url.scheme and not parsed_url.netloc
-            ):
-                raise InvalidRequirement(f"Invalid URL: {parsed.url}")
-            self.url: Optional[str] = parsed.url
-        else:
-            self.url = None
-        self.extras: Set[str] = set(parsed.extras if parsed.extras else [])
+        self.url: Optional[str] = parsed.url or None
+        self.extras: Set[str] = set(parsed.extras or [])
         self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
         self.marker: Optional[Marker] = None
         if parsed.marker is not None:
             self.marker = Marker.__new__(Marker)
             self.marker._markers = _normalize_extra_values(parsed.marker)
 
-    def __str__(self) -> str:
-        parts: List[str] = [self.name]
+    def _iter_parts(self, name: str) -> Iterator[str]:
+        yield name
 
         if self.extras:
             formatted_extras = ",".join(sorted(self.extras))
-            parts.append(f"[{formatted_extras}]")
+            yield f"[{formatted_extras}]"
 
         if self.specifier:
-            parts.append(str(self.specifier))
+            yield str(self.specifier)
 
         if self.url:
-            parts.append(f"@ {self.url}")
+            yield f"@ {self.url}"
             if self.marker:
-                parts.append(" ")
+                yield " "
 
         if self.marker:
-            parts.append(f"; {self.marker}")
+            yield f"; {self.marker}"
 
-        return "".join(parts)
+    def __str__(self) -> str:
+        return "".join(self._iter_parts(self.name))
 
     def __repr__(self) -> str:
         return f""
 
     def __hash__(self) -> int:
-        return hash((self.__class__.__name__, str(self)))
+        return hash(
+            (
+                self.__class__.__name__,
+                *self._iter_parts(canonicalize_name(self.name)),
+            )
+        )
 
     def __eq__(self, other: Any) -> bool:
         if not isinstance(other, Requirement):
             return NotImplemented
 
         return (
-            self.name == other.name
+            canonicalize_name(self.name) == canonicalize_name(other.name)
             and self.extras == other.extras
             and self.specifier == other.specifier
             and self.url == other.url
diff --git a/setuptools/_vendor/packaging/specifiers.py b/setuptools/_vendor/packaging/specifiers.py
index ba8fe37b7f7..2d015bab595 100644
--- a/setuptools/_vendor/packaging/specifiers.py
+++ b/setuptools/_vendor/packaging/specifiers.py
@@ -11,17 +11,7 @@
 import abc
 import itertools
 import re
-from typing import (
-    Callable,
-    Iterable,
-    Iterator,
-    List,
-    Optional,
-    Set,
-    Tuple,
-    TypeVar,
-    Union,
-)
+from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union
 
 from .utils import canonicalize_version
 from .version import Version
@@ -383,7 +373,7 @@ def _compare_compatible(self, prospective: Version, spec: str) -> bool:
 
         # We want everything but the last item in the version, but we want to
         # ignore suffix segments.
-        prefix = ".".join(
+        prefix = _version_join(
             list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
         )
 
@@ -404,13 +394,13 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool:
             )
             # Get the normalized version string ignoring the trailing .*
             normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
-            # Split the spec out by dots, and pretend that there is an implicit
-            # dot in between a release segment and a pre-release segment.
+            # Split the spec out by bangs and dots, and pretend that there is
+            # an implicit dot in between a release segment and a pre-release segment.
             split_spec = _version_split(normalized_spec)
 
-            # Split the prospective version out by dots, and pretend that there
-            # is an implicit dot in between a release segment and a pre-release
-            # segment.
+            # Split the prospective version out by bangs and dots, and pretend
+            # that there is an implicit dot in between a release segment and
+            # a pre-release segment.
             split_prospective = _version_split(normalized_prospective)
 
             # 0-pad the prospective version before shortening it to get the correct
@@ -644,8 +634,19 @@ def filter(
 
 
 def _version_split(version: str) -> List[str]:
+    """Split version into components.
+
+    The split components are intended for version comparison. The logic does
+    not attempt to retain the original version string, so joining the
+    components back with :func:`_version_join` may not produce the original
+    version string.
+    """
     result: List[str] = []
-    for item in version.split("."):
+
+    epoch, _, rest = version.rpartition("!")
+    result.append(epoch or "0")
+
+    for item in rest.split("."):
         match = _prefix_regex.search(item)
         if match:
             result.extend(match.groups())
@@ -654,6 +655,17 @@ def _version_split(version: str) -> List[str]:
     return result
 
 
+def _version_join(components: List[str]) -> str:
+    """Join split version components into a version string.
+
+    This function assumes the input came from :func:`_version_split`, where the
+    first component must be the epoch (either empty or numeric), and all other
+    components numeric.
+    """
+    epoch, *rest = components
+    return f"{epoch}!{'.'.join(rest)}"
+
+
 def _is_not_suffix(segment: str) -> bool:
     return not any(
         segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
@@ -675,7 +687,10 @@ def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str
     left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
     right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
 
-    return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
+    return (
+        list(itertools.chain.from_iterable(left_split)),
+        list(itertools.chain.from_iterable(right_split)),
+    )
 
 
 class SpecifierSet(BaseSpecifier):
@@ -707,14 +722,8 @@ def __init__(
         # strip each item to remove leading/trailing whitespace.
         split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
 
-        # Parsed each individual specifier, attempting first to make it a
-        # Specifier.
-        parsed: Set[Specifier] = set()
-        for specifier in split_specifiers:
-            parsed.add(Specifier(specifier))
-
-        # Turn our parsed specifiers into a frozen set and save them for later.
-        self._specs = frozenset(parsed)
+        # Make each individual specifier a Specifier and save in a frozen set for later.
+        self._specs = frozenset(map(Specifier, split_specifiers))
 
         # Store our prereleases value so we can use it later to determine if
         # we accept prereleases or not.
diff --git a/setuptools/_vendor/packaging/tags.py b/setuptools/_vendor/packaging/tags.py
index 76d243414d0..89f1926137d 100644
--- a/setuptools/_vendor/packaging/tags.py
+++ b/setuptools/_vendor/packaging/tags.py
@@ -4,6 +4,8 @@
 
 import logging
 import platform
+import re
+import struct
 import subprocess
 import sys
 import sysconfig
@@ -37,7 +39,7 @@
 }
 
 
-_32_BIT_INTERPRETER = sys.maxsize <= 2**32
+_32_BIT_INTERPRETER = struct.calcsize("P") == 4
 
 
 class Tag:
@@ -123,20 +125,37 @@ def _normalize_string(string: str) -> str:
     return string.replace(".", "_").replace("-", "_").replace(" ", "_")
 
 
-def _abi3_applies(python_version: PythonVersion) -> bool:
+def _is_threaded_cpython(abis: List[str]) -> bool:
+    """
+    Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
+
+    The threaded builds are indicated by a "t" in the abiflags.
+    """
+    if len(abis) == 0:
+        return False
+    # expect e.g., cp313
+    m = re.match(r"cp\d+(.*)", abis[0])
+    if not m:
+        return False
+    abiflags = m.group(1)
+    return "t" in abiflags
+
+
+def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
     """
     Determine if the Python version supports abi3.
 
-    PEP 384 was first implemented in Python 3.2.
+    PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`)
+    builds do not support abi3.
     """
-    return len(python_version) > 1 and tuple(python_version) >= (3, 2)
+    return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
 
 
 def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
     py_version = tuple(py_version)  # To allow for version comparison.
     abis = []
     version = _version_nodot(py_version[:2])
-    debug = pymalloc = ucs4 = ""
+    threading = debug = pymalloc = ucs4 = ""
     with_debug = _get_config_var("Py_DEBUG", warn)
     has_refcount = hasattr(sys, "gettotalrefcount")
     # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
@@ -145,6 +164,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
     has_ext = "_d.pyd" in EXTENSION_SUFFIXES
     if with_debug or (with_debug is None and (has_refcount or has_ext)):
         debug = "d"
+    if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn):
+        threading = "t"
     if py_version < (3, 8):
         with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
         if with_pymalloc or with_pymalloc is None:
@@ -158,13 +179,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
     elif debug:
         # Debug builds can also load "normal" extension modules.
         # We can also assume no UCS-4 or pymalloc requirement.
-        abis.append(f"cp{version}")
-    abis.insert(
-        0,
-        "cp{version}{debug}{pymalloc}{ucs4}".format(
-            version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4
-        ),
-    )
+        abis.append(f"cp{version}{threading}")
+    abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}")
     return abis
 
 
@@ -212,11 +228,14 @@ def cpython_tags(
     for abi in abis:
         for platform_ in platforms:
             yield Tag(interpreter, abi, platform_)
-    if _abi3_applies(python_version):
+
+    threading = _is_threaded_cpython(abis)
+    use_abi3 = _abi3_applies(python_version, threading)
+    if use_abi3:
         yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
     yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
 
-    if _abi3_applies(python_version):
+    if use_abi3:
         for minor_version in range(python_version[1] - 1, 1, -1):
             for platform_ in platforms:
                 interpreter = "cp{version}".format(
@@ -406,7 +425,7 @@ def mac_platforms(
                 check=True,
                 env={"SYSTEM_VERSION_COMPAT": "0"},
                 stdout=subprocess.PIPE,
-                universal_newlines=True,
+                text=True,
             ).stdout
             version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
     else:
@@ -469,15 +488,21 @@ def mac_platforms(
 
 def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
     linux = _normalize_string(sysconfig.get_platform())
+    if not linux.startswith("linux_"):
+        # we should never be here, just yield the sysconfig one and return
+        yield linux
+        return
     if is_32bit:
         if linux == "linux_x86_64":
             linux = "linux_i686"
         elif linux == "linux_aarch64":
-            linux = "linux_armv7l"
+            linux = "linux_armv8l"
     _, arch = linux.split("_", 1)
-    yield from _manylinux.platform_tags(linux, arch)
-    yield from _musllinux.platform_tags(arch)
-    yield linux
+    archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
+    yield from _manylinux.platform_tags(archs)
+    yield from _musllinux.platform_tags(archs)
+    for arch in archs:
+        yield f"linux_{arch}"
 
 
 def _generic_platforms() -> Iterator[str]:
diff --git a/setuptools/_vendor/packaging/utils.py b/setuptools/_vendor/packaging/utils.py
index 33c613b749a..c2c2f75aa80 100644
--- a/setuptools/_vendor/packaging/utils.py
+++ b/setuptools/_vendor/packaging/utils.py
@@ -12,6 +12,12 @@
 NormalizedName = NewType("NormalizedName", str)
 
 
+class InvalidName(ValueError):
+    """
+    An invalid distribution name; users should refer to the packaging user guide.
+    """
+
+
 class InvalidWheelFilename(ValueError):
     """
     An invalid wheel filename was found, users should refer to PEP 427.
@@ -24,17 +30,28 @@ class InvalidSdistFilename(ValueError):
     """
 
 
+# Core metadata spec for `Name`
+_validate_regex = re.compile(
+    r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
+)
 _canonicalize_regex = re.compile(r"[-_.]+")
+_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$")
 # PEP 427: The build number must start with a digit.
 _build_tag_regex = re.compile(r"(\d+)(.*)")
 
 
-def canonicalize_name(name: str) -> NormalizedName:
+def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
+    if validate and not _validate_regex.match(name):
+        raise InvalidName(f"name is invalid: {name!r}")
     # This is taken from PEP 503.
     value = _canonicalize_regex.sub("-", name).lower()
     return cast(NormalizedName, value)
 
 
+def is_normalized_name(name: str) -> bool:
+    return _normalized_regex.match(name) is not None
+
+
 def canonicalize_version(
     version: Union[Version, str], *, strip_trailing_zero: bool = True
 ) -> str:
@@ -100,11 +117,18 @@ def parse_wheel_filename(
 
     parts = filename.split("-", dashes - 2)
     name_part = parts[0]
-    # See PEP 427 for the rules on escaping the project name
+    # See PEP 427 for the rules on escaping the project name.
     if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
         raise InvalidWheelFilename(f"Invalid project name: {filename}")
     name = canonicalize_name(name_part)
-    version = Version(parts[1])
+
+    try:
+        version = Version(parts[1])
+    except InvalidVersion as e:
+        raise InvalidWheelFilename(
+            f"Invalid wheel filename (invalid version): {filename}"
+        ) from e
+
     if dashes == 5:
         build_part = parts[2]
         build_match = _build_tag_regex.match(build_part)
@@ -137,5 +161,12 @@ def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
         raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
 
     name = canonicalize_name(name_part)
-    version = Version(version_part)
+
+    try:
+        version = Version(version_part)
+    except InvalidVersion as e:
+        raise InvalidSdistFilename(
+            f"Invalid sdist filename (invalid version): {filename}"
+        ) from e
+
     return (name, version)
diff --git a/setuptools/_vendor/packaging/version.py b/setuptools/_vendor/packaging/version.py
index b30e8cbf84f..5faab9bd0dc 100644
--- a/setuptools/_vendor/packaging/version.py
+++ b/setuptools/_vendor/packaging/version.py
@@ -7,37 +7,39 @@
     from packaging.version import parse, Version
 """
 
-import collections
 import itertools
 import re
-from typing import Any, Callable, Optional, SupportsInt, Tuple, Union
+from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union
 
 from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
 
 __all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]
 
-InfiniteTypes = Union[InfinityType, NegativeInfinityType]
-PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
-SubLocalType = Union[InfiniteTypes, int, str]
-LocalType = Union[
+LocalType = Tuple[Union[int, str], ...]
+
+CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]
+CmpLocalType = Union[
     NegativeInfinityType,
-    Tuple[
-        Union[
-            SubLocalType,
-            Tuple[SubLocalType, str],
-            Tuple[NegativeInfinityType, SubLocalType],
-        ],
-        ...,
-    ],
+    Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],
 ]
 CmpKey = Tuple[
-    int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+    int,
+    Tuple[int, ...],
+    CmpPrePostDevType,
+    CmpPrePostDevType,
+    CmpPrePostDevType,
+    CmpLocalType,
 ]
 VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
 
-_Version = collections.namedtuple(
-    "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
-)
+
+class _Version(NamedTuple):
+    epoch: int
+    release: Tuple[int, ...]
+    dev: Optional[Tuple[str, int]]
+    pre: Optional[Tuple[str, int]]
+    post: Optional[Tuple[str, int]]
+    local: Optional[LocalType]
 
 
 def parse(version: str) -> "Version":
@@ -117,7 +119,7 @@ def __ne__(self, other: object) -> bool:
         (?P[0-9]+(?:\.[0-9]+)*)                  # release segment
         (?P
                                          # pre-release
             [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            (?Palpha|a|beta|b|preview|pre|c|rc)
             [-_\.]?
             (?P[0-9]+)?
         )?
@@ -269,8 +271,7 @@ def epoch(self) -> int:
         >>> Version("1!2.0.0").epoch
         1
         """
-        _epoch: int = self._version.epoch
-        return _epoch
+        return self._version.epoch
 
     @property
     def release(self) -> Tuple[int, ...]:
@@ -286,8 +287,7 @@ def release(self) -> Tuple[int, ...]:
         Includes trailing zeroes but not the epoch or any pre-release / development /
         post-release suffixes.
         """
-        _release: Tuple[int, ...] = self._version.release
-        return _release
+        return self._version.release
 
     @property
     def pre(self) -> Optional[Tuple[str, int]]:
@@ -302,8 +302,7 @@ def pre(self) -> Optional[Tuple[str, int]]:
         >>> Version("1.2.3rc1").pre
         ('rc', 1)
         """
-        _pre: Optional[Tuple[str, int]] = self._version.pre
-        return _pre
+        return self._version.pre
 
     @property
     def post(self) -> Optional[int]:
@@ -451,7 +450,7 @@ def micro(self) -> int:
 
 
 def _parse_letter_version(
-    letter: str, number: Union[str, bytes, SupportsInt]
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
 ) -> Optional[Tuple[str, int]]:
 
     if letter:
@@ -489,7 +488,7 @@ def _parse_letter_version(
 _local_version_separators = re.compile(r"[\._-]")
 
 
-def _parse_local_version(local: str) -> Optional[LocalType]:
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -507,7 +506,7 @@ def _cmpkey(
     pre: Optional[Tuple[str, int]],
     post: Optional[Tuple[str, int]],
     dev: Optional[Tuple[str, int]],
-    local: Optional[Tuple[SubLocalType]],
+    local: Optional[LocalType],
 ) -> CmpKey:
 
     # When we compare a release version, we want to compare it with all of the
@@ -524,7 +523,7 @@ def _cmpkey(
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        _pre: PrePostDevType = NegativeInfinity
+        _pre: CmpPrePostDevType = NegativeInfinity
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
@@ -534,21 +533,21 @@ def _cmpkey(
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        _post: PrePostDevType = NegativeInfinity
+        _post: CmpPrePostDevType = NegativeInfinity
 
     else:
         _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        _dev: PrePostDevType = Infinity
+        _dev: CmpPrePostDevType = Infinity
 
     else:
         _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        _local: LocalType = NegativeInfinity
+        _local: CmpLocalType = NegativeInfinity
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 592fe491a1f..e67c7845c88 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -1,4 +1,4 @@
-packaging==23.1
+packaging==24
 ordered-set==3.1.1
 more_itertools==8.8.0
 jaraco.text==3.7.0

From ea55396cc4df42720b8557a13f4fd80283fc32e8 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 13 Apr 2024 11:46:42 +0200
Subject: [PATCH 131/232] Apply ruff/refurb rule (FURB105)

FURB105 Unnecessary empty string passed to `print`
---
 distutils/dist.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/distutils/dist.py b/distutils/dist.py
index f29a34faba4..668ce7eb0a1 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -647,7 +647,7 @@ def _show_help(
                 options = self.global_options
             parser.set_option_table(options)
             parser.print_help(self.common_usage + "\nGlobal options:")
-            print('')
+            print()
 
         if display_options:
             parser.set_option_table(self.display_options)
@@ -655,7 +655,7 @@ def _show_help(
                 "Information display options (just display "
                 + "information, ignore any commands)"
             )
-            print('')
+            print()
 
         for command in self.commands:
             if isinstance(command, type) and issubclass(command, Command):
@@ -669,7 +669,7 @@ def _show_help(
             else:
                 parser.set_option_table(klass.user_options)
             parser.print_help("Options for '%s' command:" % klass.__name__)
-            print('')
+            print()
 
         print(gen_usage(self.script_name))
 
@@ -686,7 +686,7 @@ def handle_display_options(self, option_order):
         # we ignore "foo bar").
         if self.help_commands:
             self.print_commands()
-            print('')
+            print()
             print(gen_usage(self.script_name))
             return 1
 

From 0d6794fdc2987703982f7d0e89123fffc9bbda79 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 13 Apr 2024 11:48:29 +0200
Subject: [PATCH 132/232] Apply ruff/refurb rule (FURB129)

FURB129 Instead of calling `readlines()`, iterate over file object directly
---
 distutils/tests/test_msvc9compiler.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/distutils/tests/test_msvc9compiler.py b/distutils/tests/test_msvc9compiler.py
index 58e24f017a5..6f6aabee4d5 100644
--- a/distutils/tests/test_msvc9compiler.py
+++ b/distutils/tests/test_msvc9compiler.py
@@ -161,7 +161,7 @@ def test_remove_visual_c_ref(self):
         f = open(manifest)
         try:
             # removing trailing spaces
-            content = '\n'.join([line.rstrip() for line in f.readlines()])
+            content = '\n'.join([line.rstrip() for line in f])
         finally:
             f.close()
 

From bfadc24bc9c120a6feae918cea5a9d80453cc8c6 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 13 Apr 2024 11:50:52 +0200
Subject: [PATCH 133/232] Apply ruff/refurb rule (FURB142)

FURB142 Use of `set.add()` in a for loop
---
 distutils/dir_util.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/distutils/dir_util.py b/distutils/dir_util.py
index 2021bed82e1..8a3aca65216 100644
--- a/distutils/dir_util.py
+++ b/distutils/dir_util.py
@@ -95,8 +95,7 @@ def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0):
     """
     # First get the list of directories to create
     need_dir = set()
-    for file in files:
-        need_dir.add(os.path.join(base_dir, os.path.dirname(file)))
+    need_dir.update(os.path.join(base_dir, os.path.dirname(file)) for file in files)
 
     # Now create them
     for dir in sorted(need_dir):

From ec303d5963920fb8e6fce5919615fcffb0c93fe5 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 13 Apr 2024 11:53:21 +0200
Subject: [PATCH 134/232] Apply ruff/refurb rule (FURB140)

FURB140 Use `itertools.starmap` instead of the generator
---
 distutils/unixccompiler.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index a1fe2b57a29..caf4cd338ed 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -389,10 +389,7 @@ def find_library_file(self, dirs, lib, debug=0):
 
         roots = map(self._library_root, dirs)
 
-        searched = (
-            os.path.join(root, lib_name)
-            for root, lib_name in itertools.product(roots, lib_names)
-        )
+        searched = itertools.starmap(os.path.join, itertools.product(roots, lib_names))
 
         found = filter(os.path.exists, searched)
 

From df45427cbb67c1149fcf5d2d1e2705e69b3baf0c Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:10:21 -0400
Subject: [PATCH 135/232] Remove attempt to canonicalize the version. It's
 already canonical enough.

Closes #4302
Closes #3593
---
 newsfragments/4302.bugfix.rst             | 1 +
 setuptools/_core_metadata.py              | 4 ++--
 setuptools/tests/test_config_discovery.py | 6 ++----
 3 files changed, 5 insertions(+), 6 deletions(-)
 create mode 100644 newsfragments/4302.bugfix.rst

diff --git a/newsfragments/4302.bugfix.rst b/newsfragments/4302.bugfix.rst
new file mode 100644
index 00000000000..666549bcab0
--- /dev/null
+++ b/newsfragments/4302.bugfix.rst
@@ -0,0 +1 @@
+Remove attempt to canonicalize the version. It's already canonical enough.
\ No newline at end of file
diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py
index d8732c49bb4..9b4f38ded23 100644
--- a/setuptools/_core_metadata.py
+++ b/setuptools/_core_metadata.py
@@ -17,7 +17,7 @@
 from . import _normalization, _reqs
 from .extern.packaging.markers import Marker
 from .extern.packaging.requirements import Requirement
-from .extern.packaging.utils import canonicalize_name, canonicalize_version
+from .extern.packaging.utils import canonicalize_name
 from .extern.packaging.version import Version
 from .warnings import SetuptoolsDeprecationWarning
 
@@ -264,5 +264,5 @@ def _write_provides_extra(file, processed_extras, safe, unsafe):
 def get_fullname(self):
     return "{}-{}".format(
         canonicalize_name(self.get_name()).replace('-', '_'),
-        canonicalize_version(self.get_version()),
+        self.get_version(),
     )
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index e1e67ffe111..ff9e672b689 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -255,7 +255,7 @@ def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path):
 
 
 class TestNoConfig:
-    CANONICAL_DEFAULT_VERSION = "0"  # Canonical default version given by setuptools
+    DEFAULT_VERSION = "0.0.0"  # Default version given by setuptools
 
     EXAMPLES = {
         "pkg1": ["src/pkg1.py"],
@@ -277,9 +277,7 @@ def test_build_with_discovered_name(self, tmp_path):
         _populate_project_dir(tmp_path, files, {})
         _run_build(tmp_path, "--sdist")
         # Expected distribution file
-        dist_file = (
-            tmp_path / f"dist/ns_nested_pkg-{self.CANONICAL_DEFAULT_VERSION}.tar.gz"
-        )
+        dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz"
         assert dist_file.is_file()
 
 

From 5fc21f6bda88648c021e45d6e7e5e5229293d561 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:13:00 -0400
Subject: [PATCH 136/232] =?UTF-8?q?Bump=20version:=2069.3.0=20=E2=86=92=20?=
 =?UTF-8?q?69.3.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg              | 2 +-
 NEWS.rst                      | 9 +++++++++
 newsfragments/4302.bugfix.rst | 1 -
 setup.cfg                     | 2 +-
 4 files changed, 11 insertions(+), 3 deletions(-)
 delete mode 100644 newsfragments/4302.bugfix.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index a76d5b66d77..d9cfd1ad7cd 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 69.3.0
+current_version = 69.3.1
 commit = True
 tag = True
 
diff --git a/NEWS.rst b/NEWS.rst
index 7822ec63253..8a45a961eb9 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,12 @@
+v69.3.1
+=======
+
+Bugfixes
+--------
+
+- Remove attempt to canonicalize the version. It's already canonical enough. (#4302)
+
+
 v69.3.0
 =======
 
diff --git a/newsfragments/4302.bugfix.rst b/newsfragments/4302.bugfix.rst
deleted file mode 100644
index 666549bcab0..00000000000
--- a/newsfragments/4302.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Remove attempt to canonicalize the version. It's already canonical enough.
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index bab3efa52ca..78b9166b85f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 69.3.0
+version = 69.3.1
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages

From d4affe01ceb1fa4ed4c51f21473dd4c77d764d70 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:17:42 -0400
Subject: [PATCH 137/232] =?UTF-8?q?Bump=20version:=2069.4.0=20=E2=86=92=20?=
 =?UTF-8?q?69.4.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg | 2 +-
 NEWS.rst         | 6 ++++++
 setup.cfg        | 2 +-
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 007a8ec0f5f..09a7b690f07 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 69.4.0
+current_version = 69.4.1
 commit = True
 tag = True
 
diff --git a/NEWS.rst b/NEWS.rst
index e01087fc2f3..fc213d160d6 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,9 @@
+v69.4.1
+=======
+
+No significant changes.
+
+
 v69.3.1
 =======
 
diff --git a/setup.cfg b/setup.cfg
index 02078f7466d..a579bf5ff7e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 69.4.0
+version = 69.4.1
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages

From 5d9e57fd3b529505d765f6806ef0c8dc1e239acd Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:18:48 -0400
Subject: [PATCH 138/232] =?UTF-8?q?Bump=20version:=2069.4.1=20=E2=86=92=20?=
 =?UTF-8?q?69.5.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg               |  2 +-
 NEWS.rst                       | 10 ++++++++++
 newsfragments/4253.feature.rst |  1 -
 newsfragments/4301.feature.rst |  1 -
 setup.cfg                      |  2 +-
 5 files changed, 12 insertions(+), 4 deletions(-)
 delete mode 100644 newsfragments/4253.feature.rst
 delete mode 100644 newsfragments/4301.feature.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 09a7b690f07..f12875d1862 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 69.4.1
+current_version = 69.5.0
 commit = True
 tag = True
 
diff --git a/NEWS.rst b/NEWS.rst
index fc213d160d6..b2eb9bb62a2 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,13 @@
+v69.5.0
+=======
+
+Features
+--------
+
+- Refresh unpinned vendored dependencies. (#4253)
+- Updated vendored packaging to version 24.0. (#4301)
+
+
 v69.4.1
 =======
 
diff --git a/newsfragments/4253.feature.rst b/newsfragments/4253.feature.rst
deleted file mode 100644
index acc51ea4bd3..00000000000
--- a/newsfragments/4253.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Refresh unpinned vendored dependencies.
\ No newline at end of file
diff --git a/newsfragments/4301.feature.rst b/newsfragments/4301.feature.rst
deleted file mode 100644
index 28ceb2a689e..00000000000
--- a/newsfragments/4301.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Updated vendored packaging to version 24.0.
diff --git a/setup.cfg b/setup.cfg
index a579bf5ff7e..62a759e54d1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 69.4.1
+version = 69.5.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages

From 8b9f35e00549615b43793efd3c90f75739b55abf Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:31:27 -0400
Subject: [PATCH 139/232] Construct the set in one expression.

---
 distutils/dir_util.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/distutils/dir_util.py b/distutils/dir_util.py
index 8a3aca65216..370c6ffd491 100644
--- a/distutils/dir_util.py
+++ b/distutils/dir_util.py
@@ -94,8 +94,7 @@ def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0):
     'dry_run' flags are as for 'mkpath()'.
     """
     # First get the list of directories to create
-    need_dir = set()
-    need_dir.update(os.path.join(base_dir, os.path.dirname(file)) for file in files)
+    need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files)
 
     # Now create them
     for dir in sorted(need_dir):

From a04913a51327c64f807e85119fd750485bbceb0a Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 13:33:48 -0400
Subject: [PATCH 140/232] Add type declaration for runtime_library_dir_option,
 making explicit the different return types one might expect.

---
 distutils/unixccompiler.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index caf4cd338ed..a54481c01b5 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -13,6 +13,8 @@
   * link shared library handled by 'cc -shared'
 """
 
+from __future__ import annotations
+
 import itertools
 import os
 import re
@@ -281,7 +283,7 @@ def _is_gcc(self):
         compiler = os.path.basename(shlex.split(cc_var)[0])
         return "gcc" in compiler or "g++" in compiler
 
-    def runtime_library_dir_option(self, dir):
+    def runtime_library_dir_option(self, dir: str) -> str | list[str]:
         # XXX Hackish, at the very least.  See Python bug #445902:
         # https://bugs.python.org/issue445902
         # Linkers on different platforms need different options to

From d2581bf30b6cfaa64f8b570b368a6f4ed5a710ff Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 13:47:03 -0400
Subject: [PATCH 141/232] Add 'consolidate_linker_args' wrapper to protect the
 old behavior for now.

Closes pypa/distutils#246.
---
 distutils/compat/__init__.py          | 15 +++++++++++++++
 distutils/compat/py38.py              | 23 +++++++++++++++++++++++
 distutils/tests/test_unixccompiler.py | 17 +++++++++--------
 distutils/unixccompiler.py            |  5 +++--
 4 files changed, 50 insertions(+), 10 deletions(-)
 create mode 100644 distutils/compat/__init__.py
 create mode 100644 distutils/compat/py38.py

diff --git a/distutils/compat/__init__.py b/distutils/compat/__init__.py
new file mode 100644
index 00000000000..b7be72678f2
--- /dev/null
+++ b/distutils/compat/__init__.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from .py38 import removeprefix
+
+
+def consolidate_linker_args(args: list[str]) -> str:
+    """
+    Ensure the return value is a string for backward compatibility.
+
+    Retain until at least 2024-10-31.
+    """
+
+    if not all(arg.startswith('-Wl,') for arg in args):
+        return args
+    return '-Wl,' + ','.join(removeprefix(arg, '-Wl,') for arg in args)
diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py
new file mode 100644
index 00000000000..0af38140174
--- /dev/null
+++ b/distutils/compat/py38.py
@@ -0,0 +1,23 @@
+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)
diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index f17edf2f6be..6f05fa69896 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -4,6 +4,7 @@
 import sys
 import unittest.mock as mock
 from distutils import sysconfig
+from distutils.compat import consolidate_linker_args
 from distutils.errors import DistutilsPlatformError
 from distutils.unixccompiler import UnixCCompiler
 from distutils.util import _clear_cached_macosx_ver
@@ -149,10 +150,10 @@ def gcv(v):
                 return 'yes'
 
         sysconfig.get_config_var = gcv
-        assert self.cc.rpath_foo() == [
+        assert self.cc.rpath_foo() == consolidate_linker_args([
             '-Wl,--enable-new-dtags',
             '-Wl,-rpath,/foo',
-        ]
+        ])
 
         def gcv(v):
             if v == 'CC':
@@ -161,10 +162,10 @@ def gcv(v):
                 return 'yes'
 
         sysconfig.get_config_var = gcv
-        assert self.cc.rpath_foo() == [
+        assert self.cc.rpath_foo() == consolidate_linker_args([
             '-Wl,--enable-new-dtags',
             '-Wl,-rpath,/foo',
-        ]
+        ])
 
         # GCC non-GNULD
         sys.platform = 'bar'
@@ -189,10 +190,10 @@ def gcv(v):
                 return 'yes'
 
         sysconfig.get_config_var = gcv
-        assert self.cc.rpath_foo() == [
+        assert self.cc.rpath_foo() == consolidate_linker_args([
             '-Wl,--enable-new-dtags',
             '-Wl,-rpath,/foo',
-        ]
+        ])
 
         # non-GCC GNULD
         sys.platform = 'bar'
@@ -204,10 +205,10 @@ def gcv(v):
                 return 'yes'
 
         sysconfig.get_config_var = gcv
-        assert self.cc.rpath_foo() == [
+        assert self.cc.rpath_foo() == consolidate_linker_args([
             '-Wl,--enable-new-dtags',
             '-Wl,-rpath,/foo',
-        ]
+        ])
 
         # non-GCC non-GNULD
         sys.platform = 'bar'
diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index a54481c01b5..0248bde87b0 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -22,6 +22,7 @@
 import sys
 
 from . import sysconfig
+from .compat import consolidate_linker_args
 from ._log import log
 from ._macos_compat import compiler_fixup
 from ._modified import newer
@@ -315,11 +316,11 @@ def runtime_library_dir_option(self, dir: str) -> str | list[str]:
         # For all compilers, `-Wl` is the presumed way to pass a
         # compiler option to the linker
         if sysconfig.get_config_var("GNULD") == "yes":
-            return [
+            return consolidate_linker_args([
                 # Force RUNPATH instead of RPATH
                 "-Wl,--enable-new-dtags",
                 "-Wl,-rpath," + dir,
-            ]
+            ])
         else:
             return "-Wl,-R" + dir
 

From 98eee7f74c93fb84226d18f370f883956e644619 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 14:03:03 -0400
Subject: [PATCH 142/232] Exclude compat package from coverage.

---
 .coveragerc | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.coveragerc b/.coveragerc
index 35b98b1df96..bcef31d957c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,6 +2,9 @@
 omit =
 	# leading `*/` for pytest-dev/pytest-cov#456
 	*/.tox/*
+
+	# local
+	*/compat/*
 disable_warnings =
 	couldnt-parse
 

From ef297f26182823d54acfe3719416aa2661706b29 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 16:40:21 -0400
Subject: [PATCH 143/232] Extend the retention of the compatibility.

---
 distutils/compat/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/distutils/compat/__init__.py b/distutils/compat/__init__.py
index b7be72678f2..b1ee3fe8b01 100644
--- a/distutils/compat/__init__.py
+++ b/distutils/compat/__init__.py
@@ -7,7 +7,7 @@ def consolidate_linker_args(args: list[str]) -> str:
     """
     Ensure the return value is a string for backward compatibility.
 
-    Retain until at least 2024-10-31.
+    Retain until at least 2024-04-31. See pypa/distutils#246
     """
 
     if not all(arg.startswith('-Wl,') for arg in args):

From f07b037161c9640e4518c5f71e78af49a478d5b2 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 16:47:30 -0400
Subject: [PATCH 144/232] Add news fragment.

---
 newsfragments/+27489545.bugfix.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 newsfragments/+27489545.bugfix.rst

diff --git a/newsfragments/+27489545.bugfix.rst b/newsfragments/+27489545.bugfix.rst
new file mode 100644
index 00000000000..83ed1520bed
--- /dev/null
+++ b/newsfragments/+27489545.bugfix.rst
@@ -0,0 +1 @@
+Merged bugfix for pypa/distutils#246
\ No newline at end of file

From 5de8e14572713629991f3097e3c3bc197a8d4890 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 16:47:55 -0400
Subject: [PATCH 145/232] =?UTF-8?q?Bump=20version:=2069.4.1=20=E2=86=92=20?=
 =?UTF-8?q?69.4.2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg                   | 2 +-
 NEWS.rst                           | 9 +++++++++
 newsfragments/+27489545.bugfix.rst | 1 -
 setup.cfg                          | 2 +-
 4 files changed, 11 insertions(+), 3 deletions(-)
 delete mode 100644 newsfragments/+27489545.bugfix.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 09a7b690f07..0570d58bb96 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 69.4.1
+current_version = 69.4.2
 commit = True
 tag = True
 
diff --git a/NEWS.rst b/NEWS.rst
index fc213d160d6..c4aa0392295 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,12 @@
+v69.4.2
+=======
+
+Bugfixes
+--------
+
+- Merged bugfix for pypa/distutils#246 (#27489545)
+
+
 v69.4.1
 =======
 
diff --git a/newsfragments/+27489545.bugfix.rst b/newsfragments/+27489545.bugfix.rst
deleted file mode 100644
index 83ed1520bed..00000000000
--- a/newsfragments/+27489545.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Merged bugfix for pypa/distutils#246
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index a579bf5ff7e..c51168c71be 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 69.4.1
+version = 69.4.2
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages

From ff58075cdf3459ecdf73486d2a83cecdd70c7e4a Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 16:49:55 -0400
Subject: [PATCH 146/232] =?UTF-8?q?Bump=20version:=2069.5.0=20=E2=86=92=20?=
 =?UTF-8?q?69.5.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg | 2 +-
 NEWS.rst         | 6 ++++++
 setup.cfg        | 2 +-
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index f12875d1862..557ae0ce341 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 69.5.0
+current_version = 69.5.1
 commit = True
 tag = True
 
diff --git a/NEWS.rst b/NEWS.rst
index 9fa3ade1fa2..08e28ecc28a 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,9 @@
+v69.5.1
+=======
+
+No significant changes.
+
+
 v69.4.2
 =======
 
diff --git a/setup.cfg b/setup.cfg
index 62a759e54d1..f7479e047fb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 69.5.0
+version = 69.5.1
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages

From 486a8afea286d4d67e5038b58bf4452ccfeebd69 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Sun, 14 Apr 2024 03:06:34 -0600
Subject: [PATCH 147/232] NEWS: Put releases in numerical order

---
 NEWS.rst | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/NEWS.rst b/NEWS.rst
index 08e28ecc28a..9bf82560cca 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -4,23 +4,23 @@ v69.5.1
 No significant changes.
 
 
-v69.4.2
+v69.5.0
 =======
 
-Bugfixes
+Features
 --------
 
-- Merged bugfix for pypa/distutils#246 (#27489545)
+- Refresh unpinned vendored dependencies. (#4253)
+- Updated vendored packaging to version 24.0. (#4301)
 
 
-v69.5.0
+v69.4.2
 =======
 
-Features
+Bugfixes
 --------
 
-- Refresh unpinned vendored dependencies. (#4253)
-- Updated vendored packaging to version 24.0. (#4301)
+- Merged bugfix for pypa/distutils#246 (#27489545)
 
 
 v69.4.1
@@ -29,22 +29,22 @@ v69.4.1
 No significant changes.
 
 
-v69.3.1
+v69.4.0
 =======
 
-Bugfixes
+Features
 --------
 
-- Remove attempt to canonicalize the version. It's already canonical enough. (#4302)
+- Merged with pypa/distutils@55982565e, including interoperability improvements for rfc822_escape (pypa/distutils#213), dynamic resolution of config_h_filename for Python 3.13 compatibility (pypa/distutils#219), added support for the z/OS compiler (pypa/distutils#216), modernized compiler options in unixcompiler (pypa/distutils#214), fixed accumulating flags bug after compile/link (pypa/distutils#207), fixed enconding warnings (pypa/distutils#236), and general quality improvements (pypa/distutils#234). (#4298)
 
 
-v69.4.0
+v69.3.1
 =======
 
-Features
+Bugfixes
 --------
 
-- Merged with pypa/distutils@55982565e, including interoperability improvements for rfc822_escape (pypa/distutils#213), dynamic resolution of config_h_filename for Python 3.13 compatibility (pypa/distutils#219), added support for the z/OS compiler (pypa/distutils#216), modernized compiler options in unixcompiler (pypa/distutils#214), fixed accumulating flags bug after compile/link (pypa/distutils#207), fixed enconding warnings (pypa/distutils#236), and general quality improvements (pypa/distutils#234). (#4298)
+- Remove attempt to canonicalize the version. It's already canonical enough. (#4302)
 
 
 v69.3.0

From 9698925c4f6d5cf59d182dcc73682a07cb16b924 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Sat, 16 Mar 2024 12:16:34 -0400
Subject: [PATCH 148/232] Deduplicate testing dependencies by dropping
 testing-integration

---
 newsfragments/4282.misc.rst |  1 +
 setup.cfg                   | 15 +--------------
 tox.ini                     |  2 +-
 3 files changed, 3 insertions(+), 15 deletions(-)
 create mode 100644 newsfragments/4282.misc.rst

diff --git a/newsfragments/4282.misc.rst b/newsfragments/4282.misc.rst
new file mode 100644
index 00000000000..841d1b292c0
--- /dev/null
+++ b/newsfragments/4282.misc.rst
@@ -0,0 +1 @@
+Removed the ``setuptools[testing-integration]`` in favor of ``setuptools[testing]`` -- by :user:`Avasam`
diff --git a/setup.cfg b/setup.cfg
index f7479e047fb..214964fa981 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -60,7 +60,7 @@ testing =
 	jaraco.envs>=2.2
 	pytest-xdist>=3 # Dropped dependency on pytest-fork and py
 	jaraco.path>=3.2.0
-	build[virtualenv]
+	build[virtualenv]>=1.0.3
 	filelock>=3.4.0
 	ini2toml[lite]>=0.9
 	tomli-w>=1.0.0
@@ -77,19 +77,6 @@ testing =
 	# No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly
 	importlib_metadata
 
-testing-integration =
-	pytest
-	pytest-xdist
-	pytest-enabler
-	virtualenv>=13.0.0
-	tomli
-	wheel
-	jaraco.path>=3.2.0
-	jaraco.envs>=2.2
-	build[virtualenv]>=1.0.3
-	filelock>=3.4.0
-	packaging>=23.2
-
 docs =
 	# upstream
 	sphinx >= 3.5
diff --git a/tox.ini b/tox.ini
index 8815a697abe..fa4864f27b4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -23,7 +23,7 @@ pass_env =
 
 [testenv:integration]
 deps = {[testenv]deps}
-extras = testing-integration
+extras = testing
 pass_env =
 	{[testenv]pass_env}
 	DOWNLOAD_PATH

From 3b6781d1d980d7ce16caacf3310c9f418b1feb56 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 20 Mar 2024 15:40:32 +0000
Subject: [PATCH 149/232] Update tox.ini

---
 tox.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tox.ini b/tox.ini
index fa4864f27b4..7412730008b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -23,7 +23,7 @@ pass_env =
 
 [testenv:integration]
 deps = {[testenv]deps}
-extras = testing
+extras = {[testenv]extras}
 pass_env =
 	{[testenv]pass_env}
 	DOWNLOAD_PATH

From 3ca45b7374ca8262b71e8197aba28f5580ab9550 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 20 Mar 2024 09:19:25 -0700
Subject: [PATCH 150/232] Ignore 'import-not-found' for _validate_pyproject

---
 mypy.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mypy.ini b/mypy.ini
index ee12ebb193b..45671826b19 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -32,5 +32,5 @@ ignore_missing_imports = True
 # - pkg_resources tests create modules that won't exists statically before the test is run.
 #   Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail.
 # - setuptools._vendor.packaging._manylinux: Mypy issue, this vendored module is already excluded!
-[mypy-pkg_resources.tests.*,setuptools._vendor.packaging._manylinux]
+[mypy-pkg_resources.tests.*,setuptools._vendor.packaging._manylinux,setuptools.config._validate_pyproject.*]
 disable_error_code = import-not-found

From 6c118beac827d233e0d5af76a1555092f631ce70 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 12:02:47 +0100
Subject: [PATCH 151/232] Disable plugins that already run on normal tests when
 running integration

---
 conftest.py | 29 +++++++++++++++++++++++------
 1 file changed, 23 insertions(+), 6 deletions(-)

diff --git a/conftest.py b/conftest.py
index 90b653146f2..328d45d351c 100644
--- a/conftest.py
+++ b/conftest.py
@@ -24,6 +24,7 @@ def pytest_addoption(parser):
 def pytest_configure(config):
     config.addinivalue_line("markers", "integration: integration tests")
     config.addinivalue_line("markers", "uses_network: tests may try to download files")
+    _IntegrationTestSpeedups.disable_plugins_already_run(config)
 
 
 collect_ignore = [
@@ -47,9 +48,25 @@ def pytest_configure(config):
 
 @pytest.fixture(autouse=True)
 def _skip_integration(request):
-    running_integration_tests = request.config.getoption("--integration")
-    is_integration_test = request.node.get_closest_marker("integration")
-    if running_integration_tests and not is_integration_test:
-        pytest.skip("running integration tests only")
-    if not running_integration_tests and is_integration_test:
-        pytest.skip("skipping integration tests")
+    _IntegrationTestSpeedups.conditional_skip(request)
+
+
+class _IntegrationTestSpeedups:
+    """Speed-up integration tests by only running what does not run in other tests."""
+
+    RUNS_ON_NORMAL_TESTS = ("checkdocks", "cov", "mypy", "perf", "ruff")
+
+    @classmethod
+    def disable_plugins_already_run(cls, config):
+        if config.getoption("--integration"):
+            for plugin in cls.RUNS_ON_NORMAL_TESTS:  # no need to run again
+                config.pluginmanager.set_blocked(plugin)
+
+    @staticmethod
+    def conditional_skip(request):
+        running_integration_tests = request.config.getoption("--integration")
+        is_integration_test = request.node.get_closest_marker("integration")
+        if running_integration_tests and not is_integration_test:
+            pytest.skip("running integration tests only")
+        if not running_integration_tests and is_integration_test:
+            pytest.skip("skipping integration tests")

From 0e98638760ca714fef90b2cb0f361024e9ec570c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?=
 =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?=
 =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= 
Date: Tue, 16 Apr 2024 17:44:37 +0200
Subject: [PATCH 152/232] Fix the 608de826 commit reference in changelog

Previously, the orphaned filename was used, making the current Sphinx setup link a non-existing issue.
---
 NEWS.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NEWS.rst b/NEWS.rst
index 9bf82560cca..a0714ab7d2a 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -20,7 +20,7 @@ v69.4.2
 Bugfixes
 --------
 
-- Merged bugfix for pypa/distutils#246 (#27489545)
+- Merged bugfix for pypa/distutils#246 (pypa/setuptools@608de826)
 
 
 v69.4.1

From 6067fa41a3d1dfa8255ce571c9789153bb03630a Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Tue, 16 Apr 2024 14:14:14 -0400
Subject: [PATCH 153/232] Removed meaningless reference from 69.4.2 release
 notes.

---
 NEWS.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NEWS.rst b/NEWS.rst
index a0714ab7d2a..20c6903a33a 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -20,7 +20,7 @@ v69.4.2
 Bugfixes
 --------
 
-- Merged bugfix for pypa/distutils#246 (pypa/setuptools@608de826)
+- Merged bugfix for pypa/distutils#246.
 
 
 v69.4.1

From 42d1b3ceaaeeaa01eef8ad1e3a883a5324c6275b Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Fri, 19 Apr 2024 06:33:06 -0400
Subject: [PATCH 154/232] Mark tests as xfail.

All are marked as xfail even though only two are failing. As far as I know, there's no easy way to mark some of the parameterized tests as xfail without splitting and selecting, so just get the whole bunch.

Ref #4315
---
 setuptools/tests/config/test_apply_pyprojecttoml.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 2ca35759bc8..27e57b27c73 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -37,6 +37,7 @@ def makedist(path, **attrs):
     return Distribution({"src_root": path, **attrs})
 
 
+@pytest.mark.xfail(reason="#4315")
 @pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))
 @pytest.mark.filterwarnings("ignore")
 @pytest.mark.uses_network

From 12ab7d85b74ce299d983e29cb4caff68818731dd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 20 Apr 2024 20:20:03 +0100
Subject: [PATCH 155/232] Make test_apply_pyprojecttoml more deterministic with
 new version of ini2toml

---
 setup.cfg                                           | 2 +-
 setuptools/tests/config/test_apply_pyprojecttoml.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 214964fa981..c8bb0ed41da 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -62,7 +62,7 @@ testing =
 	jaraco.path>=3.2.0
 	build[virtualenv]>=1.0.3
 	filelock>=3.4.0
-	ini2toml[lite]>=0.9
+	ini2toml[lite]>=0.14
 	tomli-w>=1.0.0
 	pytest-timeout
 	pytest-perf; \
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 27e57b27c73..1b9fd6b6835 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -14,7 +14,7 @@
 from zipfile import ZipFile
 
 import pytest
-from ini2toml.api import Translator
+from ini2toml.api import LiteTranslator
 
 from packaging.metadata import Metadata
 
@@ -46,7 +46,7 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
     setupcfg_example = retrieve_file(url)
     pyproject_example = Path(tmp_path, "pyproject.toml")
     setupcfg_text = setupcfg_example.read_text(encoding="utf-8")
-    toml_config = Translator().translate(setupcfg_text, "setup.cfg")
+    toml_config = LiteTranslator().translate(setupcfg_text, "setup.cfg")
     pyproject_example.write_text(toml_config, encoding="utf-8")
 
     dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example)

From fb6dc4de0ca47dc4356dc25ac63ea187f2ad8f5a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 20 Apr 2024 20:26:59 +0100
Subject: [PATCH 156/232] Remove solved xfail in
 test_apply_pyproject_equivalent_to_setupcfg

---
 setuptools/tests/config/test_apply_pyprojecttoml.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 1b9fd6b6835..bb78f643109 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -37,7 +37,6 @@ def makedist(path, **attrs):
     return Distribution({"src_root": path, **attrs})
 
 
-@pytest.mark.xfail(reason="#4315")
 @pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))
 @pytest.mark.filterwarnings("ignore")
 @pytest.mark.uses_network

From 8837459281c144cd5bbec7a43aaf5e0a727c0efa Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 19:52:26 +0100
Subject: [PATCH 157/232] Add sanity check for 'build/lib/build/lib' when
 creating distribution fixtures

---
 setuptools/tests/fixtures.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py
index 629daf93d4c..1b8f520e84b 100644
--- a/setuptools/tests/fixtures.py
+++ b/setuptools/tests/fixtures.py
@@ -77,6 +77,10 @@ def setuptools_sdist(tmp_path_factory, request):
         if dist:
             return dist
 
+        # Sanity check
+        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        assert not Path(request.config.rootdir, "build/lib/build").exists()
+
         subprocess.check_output([
             sys.executable,
             "-m",
@@ -86,6 +90,11 @@ def setuptools_sdist(tmp_path_factory, request):
             str(tmp),
             str(request.config.rootdir),
         ])
+
+        # Sanity check
+        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        assert not Path(request.config.rootdir, "build/lib/build").exists()
+
         return next(tmp.glob("*.tar.gz"))
 
 
@@ -102,6 +111,10 @@ def setuptools_wheel(tmp_path_factory, request):
         if dist:
             return dist
 
+        # Sanity check
+        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        assert not Path(request.config.rootdir, "build/lib/build").exists()
+
         subprocess.check_output([
             sys.executable,
             "-m",
@@ -111,6 +124,11 @@ def setuptools_wheel(tmp_path_factory, request):
             str(tmp),
             str(request.config.rootdir),
         ])
+
+        # Sanity check
+        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        assert not Path(request.config.rootdir, "build/lib/build").exists()
+
         return next(tmp.glob("*.whl"))
 
 

From f1ecea0486c8ea6fcf388b48ef0aae08e5fd8feb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 20:04:04 +0100
Subject: [PATCH 158/232] Avoid setuptools_wheel fixture to create recursively
 nested build/lib/build/lib/... directories

Based on the test introduced in b4d3e83f0, we can see that when none of
`PRE_BUILT_SETUPTOOLS_SDIST` or `PRE_BUILT_SETUPTOOLS_WHEEL` is set,
the `setuptools_wheel` fixture keeps recursively creating
`build/lib/build/lib/...` directories which slows down the tests and
creates a huge amount of unnecessary files.

This change tries to target that.
---
 setuptools/tests/fixtures.py | 63 +++++++++++++-----------------------
 1 file changed, 23 insertions(+), 40 deletions(-)

diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py
index 1b8f520e84b..a2870f11e12 100644
--- a/setuptools/tests/fixtures.py
+++ b/setuptools/tests/fixtures.py
@@ -63,73 +63,56 @@ def sample_project(tmp_path):
 # sdist and wheel artifacts should be stable across a round of tests
 # so we can build them once per session and use the files as "readonly"
 
+# In the case of setuptools, building the wheel without sdist may cause
+# it to contain the `build` directory, and therefore create situations with
+# `setuptools/build/lib/build/lib/...`. To avoid that, build both artifacts at once.
 
-@pytest.fixture(scope="session")
-def setuptools_sdist(tmp_path_factory, request):
-    prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_SDIST")
-    if prebuilt and os.path.exists(prebuilt):  # pragma: no cover
-        return Path(prebuilt).resolve()
 
+def _build_distributions(tmp_path_factory, request):
     with contexts.session_locked_tmp_dir(
-        request, tmp_path_factory, "sdist_build"
+        request, tmp_path_factory, "dist_build"
     ) as tmp:  # pragma: no cover
-        dist = next(tmp.glob("*.tar.gz"), None)
-        if dist:
-            return dist
+        sdist = next(tmp.glob("*.tar.gz"), None)
+        wheel = next(tmp.glob("*.whl"), None)
+        if sdist and wheel:
+            return (sdist, wheel)
 
-        # Sanity check
-        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
         assert not Path(request.config.rootdir, "build/lib/build").exists()
 
         subprocess.check_output([
             sys.executable,
             "-m",
             "build",
-            "--sdist",
             "--outdir",
             str(tmp),
             str(request.config.rootdir),
         ])
 
-        # Sanity check
-        # Building should not create recursive `setuptools/build/lib/build/lib/...`
+        # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
         assert not Path(request.config.rootdir, "build/lib/build").exists()
 
-        return next(tmp.glob("*.tar.gz"))
+        return next(tmp.glob("*.tar.gz")), next(tmp.glob("*.whl"))
 
 
 @pytest.fixture(scope="session")
-def setuptools_wheel(tmp_path_factory, request):
-    prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL")
+def setuptools_sdist(tmp_path_factory, request):
+    prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_SDIST")
     if prebuilt and os.path.exists(prebuilt):  # pragma: no cover
         return Path(prebuilt).resolve()
 
-    with contexts.session_locked_tmp_dir(
-        request, tmp_path_factory, "wheel_build"
-    ) as tmp:  # pragma: no cover
-        dist = next(tmp.glob("*.whl"), None)
-        if dist:
-            return dist
+    sdist, _ = _build_distributions(tmp_path_factory, request)
+    return sdist
 
-        # Sanity check
-        # Building should not create recursive `setuptools/build/lib/build/lib/...`
-        assert not Path(request.config.rootdir, "build/lib/build").exists()
 
-        subprocess.check_output([
-            sys.executable,
-            "-m",
-            "build",
-            "--wheel",
-            "--outdir",
-            str(tmp),
-            str(request.config.rootdir),
-        ])
-
-        # Sanity check
-        # Building should not create recursive `setuptools/build/lib/build/lib/...`
-        assert not Path(request.config.rootdir, "build/lib/build").exists()
+@pytest.fixture(scope="session")
+def setuptools_wheel(tmp_path_factory, request):
+    prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL")
+    if prebuilt and os.path.exists(prebuilt):  # pragma: no cover
+        return Path(prebuilt).resolve()
 
-        return next(tmp.glob("*.whl"))
+    _, wheel = _build_distributions(tmp_path_factory, request)
+    return wheel
 
 
 @pytest.fixture

From 14ce35000023749b41015c7b2884080be3803768 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 20:17:06 +0100
Subject: [PATCH 159/232] Add news fragment

---
 newsfragments/4308.misc.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 newsfragments/4308.misc.rst

diff --git a/newsfragments/4308.misc.rst b/newsfragments/4308.misc.rst
new file mode 100644
index 00000000000..6c43f6338ee
--- /dev/null
+++ b/newsfragments/4308.misc.rst
@@ -0,0 +1,2 @@
+Fix ``setuptools_wheel`` fixture and avoid the recursive creation of
+``build/lib/build/lib/build/...`` directories in the project root during tests.

From 153d75eaa3f2009d0a2e5d47a729428c24fc8913 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 19:25:12 +0100
Subject: [PATCH 160/232] Refactor _TopLevelFinder so it is easier to test

---
 setuptools/command/editable_wheel.py | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
index 4d21e2253fa..11673460699 100644
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -505,7 +505,7 @@ def __init__(self, dist: Distribution, name: str):
         self.dist = dist
         self.name = name
 
-    def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
+    def template_vars(self) -> Tuple[str, str, Dict[str, str], Dict[str, List[str]]]:
         src_root = self.dist.src_root or os.curdir
         top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
         package_dir = self.dist.package_dir or {}
@@ -519,7 +519,7 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
         )
 
         legacy_namespaces = {
-            pkg: find_package_path(pkg, roots, self.dist.src_root or "")
+            cast(str, pkg): find_package_path(pkg, roots, self.dist.src_root or "")
             for pkg in self.dist.namespace_packages or []
         }
 
@@ -530,11 +530,20 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
 
         name = f"__editable__.{self.name}.finder"
         finder = _normalization.safe_identifier(name)
+        return finder, name, mapping, namespaces_
+
+    def get_implementation(self) -> Iterator[Tuple[str, bytes]]:
+        finder, name, mapping, namespaces_ = self.template_vars()
+
         content = bytes(_finder_template(name, mapping, namespaces_), "utf-8")
-        wheel.writestr(f"{finder}.py", content)
+        yield (f"{finder}.py", content)
 
         content = _encode_pth(f"import {finder}; {finder}.install()")
-        wheel.writestr(f"__editable__.{self.name}.pth", content)
+        yield (f"__editable__.{self.name}.pth", content)
+
+    def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
+        for file, content in self.get_implementation():
+            wheel.writestr(file, content)
 
     def __enter__(self):
         msg = "Editable install will be performed using a meta path finder.\n"

From a39d3623f2d9088a6914cfa833838ceac182af57 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 19:25:48 +0100
Subject: [PATCH 161/232] Add test for issue 4248

---
 setuptools/tests/test_editable_install.py | 44 +++++++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 1df09fd256b..5da4fccefa0 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -24,6 +24,7 @@
 from setuptools.command.editable_wheel import (
     _DebuggingTips,
     _LinkTree,
+    _TopLevelFinder,
     _encode_pth,
     _find_virtual_namespaces,
     _find_namespaces,
@@ -530,6 +531,49 @@ def test_combine_namespaces(self, tmp_path):
             assert pkgA.a == 13
             assert mod2.b == 37
 
+    def test_combine_namespaces_nested(self, tmp_path):
+        """
+        Users may attempt to combine namespace packages in a nested way via
+        ``package_dir`` as shown in pypa/setuptools#4248.
+        """
+
+        files = {
+            "src": {"my_package": {"my_module.py": "a = 13"}},
+            "src2": {"my_package2": {"my_module2.py": "b = 37"}},
+        }
+
+        stack = jaraco.path.DirectoryStack()
+        with stack.context(tmp_path):
+            jaraco.path.build(files)
+            attrs = {
+                "script_name": "%PEP 517%",
+                "package_dir": {
+                    "different_name": "src/my_package",
+                    "different_name.subpkg": "src2/my_package2",
+                },
+                "packages": ["different_name", "different_name.subpkg"],
+            }
+            dist = Distribution(attrs)
+            finder = _TopLevelFinder(dist, str(uuid4()))
+            code = next(v for k, v in finder.get_implementation() if k.endswith(".py"))
+
+        with contexts.save_paths(), contexts.save_sys_modules():
+            for mod in attrs["packages"]:
+                sys.modules.pop(mod, None)
+
+            self.install_finder(code)
+            mod1 = import_module("different_name.my_module")
+            mod2 = import_module("different_name.subpkg.my_module2")
+
+            expected = str((tmp_path / "src/my_package/my_module.py").resolve())
+            assert str(Path(mod1.__file__).resolve()) == expected
+
+            expected = str((tmp_path / "src2/my_package2/my_module2.py").resolve())
+            assert str(Path(mod2.__file__).resolve()) == expected
+
+            assert mod1.a == 13
+            assert mod2.b == 37
+
     def test_dynamic_path_computation(self, tmp_path):
         # Follows the example in PEP 420
         files = {

From 19b63d1b81cd0544e2718f82c482efdc99742ef8 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 7 Mar 2024 02:46:09 +0000
Subject: [PATCH 162/232] Fix PathEntryFinder / MetaPathFinder in
 editable_wheel

It seems that the import machinery skips PathEntryFinder when trying to
locate nested namespaces, if the `sys.path_hook` item corresponding to
the finder cannot be located in the submodule locations of the parent
namespace.

This means that we should probably always add the PATH_PLACEHOLDER to
the namespace spec.

This PR also add some type hints to the template, because it helped to
debug some type errors.
---
 setuptools/command/editable_wheel.py | 32 +++++++++++++++++-----------
 1 file changed, 19 insertions(+), 13 deletions(-)

diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
index 11673460699..1722817f823 100644
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -793,6 +793,7 @@ def _get_root(self):
 
 
 _FINDER_TEMPLATE = """\
+from __future__ import annotations
 import sys
 from importlib.machinery import ModuleSpec, PathFinder
 from importlib.machinery import all_suffixes as module_suffixes
@@ -800,16 +801,14 @@ def _get_root(self):
 from itertools import chain
 from pathlib import Path
 
-MAPPING = {mapping!r}
-NAMESPACES = {namespaces!r}
+MAPPING: dict[str, str] = {mapping!r}
+NAMESPACES: dict[str, list[str]] = {namespaces!r}
 PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
 
 
 class _EditableFinder:  # MetaPathFinder
     @classmethod
-    def find_spec(cls, fullname, path=None, target=None):
-        extra_path = []
-
+    def find_spec(cls, fullname: str, _path=None, _target=None) -> ModuleSpec | None:
         # Top-level packages and modules (we know these exist in the FS)
         if fullname in MAPPING:
             pkg_path = MAPPING[fullname]
@@ -820,35 +819,42 @@ def find_spec(cls, fullname, path=None, target=None):
         # to the importlib.machinery implementation.
         parent, _, child = fullname.rpartition(".")
         if parent and parent in MAPPING:
-            return PathFinder.find_spec(fullname, path=[MAPPING[parent], *extra_path])
+            return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
 
         # Other levels of nesting should be handled automatically by importlib
         # using the parent path.
         return None
 
     @classmethod
-    def _find_spec(cls, fullname, candidate_path):
+    def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:
         init = candidate_path / "__init__.py"
         candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
         for candidate in chain([init], candidates):
             if candidate.exists():
                 return spec_from_file_location(fullname, candidate)
+        return None
 
 
 class _EditableNamespaceFinder:  # PathEntryFinder
     @classmethod
-    def _path_hook(cls, path):
+    def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:
         if path == PATH_PLACEHOLDER:
             return cls
         raise ImportError
 
     @classmethod
-    def _paths(cls, fullname):
-        # Ensure __path__ is not empty for the spec to be considered a namespace.
-        return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
+    def _paths(cls, fullname: str) -> list[str]:
+        paths = NAMESPACES[fullname]
+        if not paths and fullname in MAPPING:
+            paths = [MAPPING[fullname]]
+        # Always add placeholder, for 2 reasons:
+        # 1. __path__ cannot be empty for the spec to be considered namespace.
+        # 2. In the case of nested namespaces, we need to force
+        #    import machinery to query _EditableNamespaceFinder again.
+        return [*paths, PATH_PLACEHOLDER]
 
     @classmethod
-    def find_spec(cls, fullname, target=None):
+    def find_spec(cls, fullname: str, _target=None) -> ModuleSpec | None:
         if fullname in NAMESPACES:
             spec = ModuleSpec(fullname, None, is_package=True)
             spec.submodule_search_locations = cls._paths(fullname)
@@ -856,7 +862,7 @@ def find_spec(cls, fullname, target=None):
         return None
 
     @classmethod
-    def find_module(cls, fullname):
+    def find_module(cls, _fullname) -> None:
         return None
 
 

From 2b7ea603325bf8f2edbc7f12cc6b8cb2a7bd41e7 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 16 Apr 2024 19:35:57 +0100
Subject: [PATCH 163/232] Add news fragment

---
 newsfragments/4278.bugfix.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 newsfragments/4278.bugfix.rst

diff --git a/newsfragments/4278.bugfix.rst b/newsfragments/4278.bugfix.rst
new file mode 100644
index 00000000000..5e606cced8d
--- /dev/null
+++ b/newsfragments/4278.bugfix.rst
@@ -0,0 +1,2 @@
+Fix finder template for lenient editable installs of implicit nested namespaces
+constructed by using ``package_dir`` to reorganise directory structure.

From 3d539f30913fedb6bf1e3bb9f4ef52de09a9eb09 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:16:27 +0100
Subject: [PATCH 164/232] Convert safe txt files to UTF-8

---
 setuptools/command/bdist_egg.py | 5 ++---
 setuptools/dist.py              | 2 +-
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py
index 3687efdf9c6..b2897bfbb4d 100644
--- a/setuptools/command/bdist_egg.py
+++ b/setuptools/command/bdist_egg.py
@@ -350,9 +350,8 @@ def write_safety_flag(egg_dir, safe):
             if safe is None or bool(safe) != flag:
                 os.unlink(fn)
         elif safe is not None and bool(safe) == flag:
-            f = open(fn, 'wt')
-            f.write('\n')
-            f.close()
+            with open(fn, 'wt', encoding="utf-8") as f:
+                f.write('\n')
 
 
 safety_flags = {
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 6350e381006..076f9a2327a 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -685,7 +685,7 @@ def get_egg_cache_dir(self):
             os.mkdir(egg_cache_dir)
             windows_support.hide_file(egg_cache_dir)
             readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')
-            with open(readme_txt_filename, 'w') as f:
+            with open(readme_txt_filename, 'w', encoding="utf-8") as f:
                 f.write(
                     'This directory contains eggs that were downloaded '
                     'by setuptools to build, test, and run plug-ins.\n\n'

From d6651219d5284b2703b9849fe5fe2fcb47a54230 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:19:05 +0100
Subject: [PATCH 165/232] Try to read some files as UTF-8 before attempting
 locale

---
 setuptools/command/setopt.py | 11 +++++++++--
 setuptools/package_index.py  | 16 +++++++++++++---
 setuptools/wheel.py          | 11 +++++++++--
 3 files changed, 31 insertions(+), 7 deletions(-)

diff --git a/setuptools/command/setopt.py b/setuptools/command/setopt.py
index f9a60751285..aa800492f76 100644
--- a/setuptools/command/setopt.py
+++ b/setuptools/command/setopt.py
@@ -5,7 +5,8 @@
 import os
 import configparser
 
-from setuptools import Command
+from .. import Command
+from ..compat import py39
 
 __all__ = ['config_file', 'edit_config', 'option_base', 'setopt']
 
@@ -36,7 +37,13 @@ def edit_config(filename, settings, dry_run=False):
     log.debug("Reading configuration from %s", filename)
     opts = configparser.RawConfigParser()
     opts.optionxform = lambda x: x
-    opts.read([filename])
+
+    try:
+        opts.read([filename], encoding="utf-8")
+    except UnicodeDecodeError:  # pragma: no cover
+        opts.clear()
+        opts.read([filename], encoding=py39.LOCALE_ENCODING)
+
     for section, options in settings.items():
         if options is None:
             log.info("Deleting section [%s] from %s", section, filename)
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 271aa97f71a..f835bdcf14d 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -40,6 +40,8 @@
 from setuptools.wheel import Wheel
 from setuptools.extern.more_itertools import unique_everseen
 
+from .compat import py39
+
 
 EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
 HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I)
@@ -1011,7 +1013,11 @@ def __init__(self):
 
         rc = os.path.join(os.path.expanduser('~'), '.pypirc')
         if os.path.exists(rc):
-            self.read(rc)
+            try:
+                self.read(rc, encoding="utf-8")
+            except UnicodeDecodeError:  # pragma: no cover
+                self.clean()
+                self.read(rc, encoding=py39.LOCALE_ENCODING)
 
     @property
     def creds_by_repository(self):
@@ -1114,8 +1120,12 @@ def local_open(url):
         for f in os.listdir(filename):
             filepath = os.path.join(filename, f)
             if f == 'index.html':
-                with open(filepath, 'r') as fp:
-                    body = fp.read()
+                try:
+                    with open(filepath, 'r', encoding="utf-8") as fp:
+                        body = fp.read()
+                except UnicodeDecodeError:  # pragma: no cover
+                    with open(filepath, 'r', encoding=py39.LOCALE_ENCODING) as fp:
+                        body = fp.read()
                 break
             elif os.path.isdir(filepath):
                 f += '/'
diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index 9861b5cf1cf..4cf3f4ca0d0 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -18,6 +18,8 @@
 from setuptools.command.egg_info import write_requirements, _egg_basename
 from setuptools.archive_util import _unpack_zipfile_obj
 
+from .compat import py39
+
 
 WHEEL_NAME = re.compile(
     r"""^(?P.+?)-(?P\d.*?)
@@ -222,8 +224,13 @@ def _move_data_entries(destination_eggdir, dist_data):
     def _fix_namespace_packages(egg_info, destination_eggdir):
         namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')
         if os.path.exists(namespace_packages):
-            with open(namespace_packages) as fp:
-                namespace_packages = fp.read().split()
+            try:
+                with open(namespace_packages, encoding="utf-8") as fp:
+                    namespace_packages = fp.read().split()
+            except UnicodeDecodeError:  # pragma: no cover
+                with open(namespace_packages, encoding=py39.LOCALE_ENCODING) as fp:
+                    namespace_packages = fp.read().split()
+
             for mod in namespace_packages:
                 mod_dir = os.path.join(destination_eggdir, *mod.split('.'))
                 mod_init = os.path.join(mod_dir, '__init__.py')

From e8e59831978216b60712c1411d9f6bd6eabebe23 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:19:59 +0100
Subject: [PATCH 166/232] Attempt to use utf-8 with Popen

---
 setuptools/tests/environment.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py
index df2bd37ff65..6173936f7b7 100644
--- a/setuptools/tests/environment.py
+++ b/setuptools/tests/environment.py
@@ -76,6 +76,7 @@ def run_setup_py(cmd, pypath=None, path=None, data_stream=0, env=None):
             stderr=_PIPE,
             shell=shell,
             env=env,
+            encoding="utf-8",
         )
 
         if isinstance(data_stream, tuple):

From 13fdfaa2c9aa8a9decd8ec6624e536e2ed9cc7c6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:21:18 +0100
Subject: [PATCH 167/232] Use utf-8 for NAMESPACE_PACKAGE_INIT

This change should be relatively safe
---
 setuptools/wheel.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index 4cf3f4ca0d0..19f41574236 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -237,5 +237,5 @@ def _fix_namespace_packages(egg_info, destination_eggdir):
                 if not os.path.exists(mod_dir):
                     os.mkdir(mod_dir)
                 if not os.path.exists(mod_init):
-                    with open(mod_init, 'w') as fp:
+                    with open(mod_init, 'w', encoding="utf-8") as fp:
                         fp.write(NAMESPACE_PACKAGE_INIT)

From 851e972aae6efa2cf6ead909db7653bce4368c5f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:22:08 +0100
Subject: [PATCH 168/232] Attempt to use UTF-8 when rewriting 'setup.cfg'

---
 setuptools/command/setopt.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/command/setopt.py b/setuptools/command/setopt.py
index aa800492f76..89b1ac73078 100644
--- a/setuptools/command/setopt.py
+++ b/setuptools/command/setopt.py
@@ -69,7 +69,7 @@ def edit_config(filename, settings, dry_run=False):
 
     log.info("Writing %s", filename)
     if not dry_run:
-        with open(filename, 'w') as f:
+        with open(filename, 'w', encoding="utf-8") as f:
             opts.write(f)
 
 

From 20c0f82778a82d57c061f57d56c99192294e0acf Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:24:42 +0100
Subject: [PATCH 169/232] Attempt to use UTF-8 when writing str scripts with
 easy_install/install_scripts

---
 setuptools/command/easy_install.py    |  8 +++++++-
 setuptools/command/install_scripts.py | 10 +++++++---
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 87a68c292a1..3ad984f2127 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -873,7 +873,13 @@ def write_script(self, script_name, contents, mode="t", blockers=()):
         ensure_directory(target)
         if os.path.exists(target):
             os.unlink(target)
-        with open(target, "w" + mode) as f:  # TODO: is it safe to use utf-8?
+
+        if "b" not in mode and isinstance(contents, str):
+            kw = {"encoding": "utf-8"}
+        else:
+            kw = {}
+
+        with open(target, "w" + mode, **kw) as f:
             f.write(contents)
         chmod(target, 0o777 - mask)
 
diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py
index 72b2e45cbcf..758937b6143 100644
--- a/setuptools/command/install_scripts.py
+++ b/setuptools/command/install_scripts.py
@@ -57,10 +57,14 @@ def write_script(self, script_name, contents, mode="t", *ignored):
         target = os.path.join(self.install_dir, script_name)
         self.outfiles.append(target)
 
+        if "b" not in mode and isinstance(contents, str):
+            kw = {"encoding": "utf-8"}
+        else:
+            kw = {}
+
         mask = current_umask()
         if not self.dry_run:
             ensure_directory(target)
-            f = open(target, "w" + mode)
-            f.write(contents)
-            f.close()
+            with open(target, "w" + mode, **kw) as f:
+                f.write(contents)
             chmod(target, 0o777 - mask)

From e44a63cb16a19d31d4d0c080efb71aac63ff0948 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 10:28:43 +0100
Subject: [PATCH 170/232] Prevent missing UTF-8 warnings in setuptools._imp

This should be safe because we use `tokenize.open` for source codes.
---
 setuptools/_imp.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/setuptools/_imp.py b/setuptools/_imp.py
index 9d4ead0eb03..38b146fc4d5 100644
--- a/setuptools/_imp.py
+++ b/setuptools/_imp.py
@@ -6,6 +6,7 @@
 import os
 import importlib.util
 import importlib.machinery
+import tokenize
 
 from importlib.util import module_from_spec
 
@@ -60,13 +61,13 @@ def find_module(module, paths=None):
 
         if suffix in importlib.machinery.SOURCE_SUFFIXES:
             kind = PY_SOURCE
+            file = tokenize.open(path)
         elif suffix in importlib.machinery.BYTECODE_SUFFIXES:
             kind = PY_COMPILED
+            file = open(path, 'rb')
         elif suffix in importlib.machinery.EXTENSION_SUFFIXES:
             kind = C_EXTENSION
 
-        if kind in {PY_SOURCE, PY_COMPILED}:
-            file = open(path, mode)
     else:
         path = None
         suffix = mode = ''

From b540f93cdf7edd0a0ce3e06a23ff4714bf13420e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 16:01:56 +0100
Subject: [PATCH 171/232] Use UTF-8 with venv.run and avoid encoding warnings

---
 setuptools/tests/environment.py           |  2 +-
 setuptools/tests/test_build_meta.py       |  4 +-
 setuptools/tests/test_editable_install.py | 66 +++++++++++------------
 3 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py
index 6173936f7b7..b9de4fda6b2 100644
--- a/setuptools/tests/environment.py
+++ b/setuptools/tests/environment.py
@@ -17,7 +17,7 @@ class VirtualEnv(jaraco.envs.VirtualEnv):
 
     def run(self, cmd, *args, **kwargs):
         cmd = [self.exe(cmd[0])] + cmd[1:]
-        kwargs = {"cwd": self.root, **kwargs}  # Allow overriding
+        kwargs = {"cwd": self.root, "encoding": "utf-8", **kwargs}  # Allow overriding
         # In some environments (eg. downstream distro packaging), where:
         # - tox isn't used to run tests and
         # - PYTHONPATH is set to point to a specific setuptools codebase and
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index c2a1e6dc751..43830feb770 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -941,14 +941,14 @@ def test_legacy_editable_install(venv, tmpdir, tmpdir_cwd):
 
     # First: sanity check
     cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
-    output = str(venv.run(cmd, cwd=tmpdir), "utf-8").lower()
+    output = venv.run(cmd, cwd=tmpdir).lower()
     assert "running setup.py develop for myproj" not in output
     assert "created wheel for myproj" in output
 
     # Then: real test
     env = {**os.environ, "SETUPTOOLS_ENABLE_FEATURES": "legacy-editable"}
     cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
-    output = str(venv.run(cmd, cwd=tmpdir, env=env), "utf-8").lower()
+    output = venv.run(cmd, cwd=tmpdir, env=env).lower()
     assert "running setup.py develop for myproj" in output
 
 
diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 5da4fccefa0..119b1286942 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -131,7 +131,7 @@ def test_editable_with_pyproject(tmp_path, venv, files, editable_opts):
     jaraco.path.build(files, prefix=project)
 
     cmd = [
-        venv.exe(),
+        "python",
         "-m",
         "pip",
         "install",
@@ -140,14 +140,14 @@ def test_editable_with_pyproject(tmp_path, venv, files, editable_opts):
         str(project),
         *editable_opts,
     ]
-    print(str(subprocess.check_output(cmd), "utf-8"))
+    print(venv.run(cmd))
 
-    cmd = [venv.exe(), "-m", "mypkg"]
-    assert subprocess.check_output(cmd).strip() == b"3.14159.post0 Hello World"
+    cmd = ["python", "-m", "mypkg"]
+    assert venv.run(cmd).strip() == "3.14159.post0 Hello World"
 
     (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8")
     (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8")
-    assert subprocess.check_output(cmd).strip() == b"3.14159.post0 foobar 42"
+    assert venv.run(cmd).strip() == "3.14159.post0 foobar 42"
 
 
 def test_editable_with_flat_layout(tmp_path, venv, editable_opts):
@@ -176,7 +176,7 @@ def test_editable_with_flat_layout(tmp_path, venv, editable_opts):
     project = tmp_path / "mypkg"
 
     cmd = [
-        venv.exe(),
+        "python",
         "-m",
         "pip",
         "install",
@@ -185,9 +185,9 @@ def test_editable_with_flat_layout(tmp_path, venv, editable_opts):
         str(project),
         *editable_opts,
     ]
-    print(str(subprocess.check_output(cmd), "utf-8"))
-    cmd = [venv.exe(), "-c", "import pkg, mod; print(pkg.a, mod.b)"]
-    assert subprocess.check_output(cmd).strip() == b"4 2"
+    print(venv.run(cmd))
+    cmd = ["python", "-c", "import pkg, mod; print(pkg.a, mod.b)"]
+    assert venv.run(cmd).strip() == "4 2"
 
 
 def test_editable_with_single_module(tmp_path, venv, editable_opts):
@@ -214,7 +214,7 @@ def test_editable_with_single_module(tmp_path, venv, editable_opts):
     project = tmp_path / "mypkg"
 
     cmd = [
-        venv.exe(),
+        "python",
         "-m",
         "pip",
         "install",
@@ -223,9 +223,9 @@ def test_editable_with_single_module(tmp_path, venv, editable_opts):
         str(project),
         *editable_opts,
     ]
-    print(str(subprocess.check_output(cmd), "utf-8"))
-    cmd = [venv.exe(), "-c", "import mod; print(mod.b)"]
-    assert subprocess.check_output(cmd).strip() == b"2"
+    print(venv.run(cmd))
+    cmd = ["python", "-c", "import mod; print(mod.b)"]
+    assert venv.run(cmd).strip() == "2"
 
 
 class TestLegacyNamespaces:
@@ -384,7 +384,7 @@ def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):
         opts = ["--no-build-isolation"]  # force current version of setuptools
         venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts])
         out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"])
-        assert str(out, "utf-8").strip() == "1"
+        assert out.strip() == "1"
         cmd = """\
         try:
             import mypkg.other
@@ -392,7 +392,7 @@ def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):
             print("mypkg.other not defined")
         """
         out = venv.run(["python", "-c", dedent(cmd)])
-        assert "mypkg.other not defined" in str(out, "utf-8")
+        assert "mypkg.other not defined" in out
 
 
 # Moved here from test_develop:
@@ -911,7 +911,7 @@ def test_editable_install(self, tmp_path, venv, layout, editable_opts):
             print(ex)
         """
         out = venv.run(["python", "-c", dedent(cmd_import_error)])
-        assert b"No module named 'otherfile'" in out
+        assert "No module named 'otherfile'" in out
 
         # Ensure the modules are importable
         cmd_get_vars = """\
@@ -919,7 +919,7 @@ def test_editable_install(self, tmp_path, venv, layout, editable_opts):
         print(mypkg.mod1.var, mypkg.subpackage.mod2.var)
         """
         out = venv.run(["python", "-c", dedent(cmd_get_vars)])
-        assert b"42 13" in out
+        assert "42 13" in out
 
         # Ensure resources are reachable
         cmd_get_resource = """\
@@ -929,7 +929,7 @@ def test_editable_install(self, tmp_path, venv, layout, editable_opts):
         print(text.read_text(encoding="utf-8"))
         """
         out = venv.run(["python", "-c", dedent(cmd_get_resource)])
-        assert b"resource 39" in out
+        assert "resource 39" in out
 
         # Ensure files are editable
         mod1 = next(project.glob("**/mod1.py"))
@@ -941,12 +941,12 @@ def test_editable_install(self, tmp_path, venv, layout, editable_opts):
         resource_file.write_text("resource 374", encoding="utf-8")
 
         out = venv.run(["python", "-c", dedent(cmd_get_vars)])
-        assert b"42 13" not in out
-        assert b"17 781" in out
+        assert "42 13" not in out
+        assert "17 781" in out
 
         out = venv.run(["python", "-c", dedent(cmd_get_resource)])
-        assert b"resource 39" not in out
-        assert b"resource 374" in out
+        assert "resource 39" not in out
+        assert "resource 374" in out
 
 
 class TestLinkTree:
@@ -1005,7 +1005,7 @@ def test_strict_install(self, tmp_path, venv):
         install_project("mypkg", venv, tmp_path, self.FILES, *opts)
 
         out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
-        assert b"42" in out
+        assert "42" in out
 
         # Ensure packages excluded from distribution are not importable
         cmd_import_error = """\
@@ -1015,7 +1015,7 @@ def test_strict_install(self, tmp_path, venv):
             print(ex)
         """
         out = venv.run(["python", "-c", dedent(cmd_import_error)])
-        assert b"cannot import name 'subpackage'" in out
+        assert "cannot import name 'subpackage'" in out
 
         # Ensure resource files excluded from distribution are not reachable
         cmd_get_resource = """\
@@ -1028,8 +1028,8 @@ def test_strict_install(self, tmp_path, venv):
             print(ex)
         """
         out = venv.run(["python", "-c", dedent(cmd_get_resource)])
-        assert b"No such file or directory" in out
-        assert b"resource.not_in_manifest" in out
+        assert "No such file or directory" in out
+        assert "resource.not_in_manifest" in out
 
 
 @pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning")
@@ -1040,7 +1040,7 @@ def test_compat_install(tmp_path, venv):
     install_project("mypkg", venv, tmp_path, files, *opts)
 
     out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
-    assert b"42" in out
+    assert "42" in out
 
     expected_path = comparable_path(str(tmp_path))
 
@@ -1051,7 +1051,7 @@ def test_compat_install(tmp_path, venv):
         "import other; print(other)",
         "import mypkg; print(mypkg)",
     ):
-        out = comparable_path(str(venv.run(["python", "-c", cmd]), "utf-8"))
+        out = comparable_path(venv.run(["python", "-c", cmd]))
         assert expected_path in out
 
     # Compatible behaviour will not consider custom mappings
@@ -1061,7 +1061,7 @@ def test_compat_install(tmp_path, venv):
     except ImportError as ex:
         print(ex)
     """
-    out = str(venv.run(["python", "-c", dedent(cmd)]), "utf-8")
+    out = venv.run(["python", "-c", dedent(cmd)])
     assert "cannot import name 'subpackage'" in out
 
 
@@ -1105,7 +1105,7 @@ def test_pbr_integration(tmp_path, venv, editable_opts):
         install_project("mypkg", venv, tmp_path, files, *editable_opts)
 
     out = venv.run(["python", "-c", "import mypkg.hello"])
-    assert b"Hello world!" in out
+    assert "Hello world!" in out
 
 
 class TestCustomBuildPy:
@@ -1143,11 +1143,11 @@ def test_safeguarded_from_errors(self, tmp_path, venv):
         """Ensure that errors in custom build_py are reported as warnings"""
         # Warnings should show up
         _, out = install_project("mypkg", venv, tmp_path, self.FILES)
-        assert b"SetuptoolsDeprecationWarning" in out
-        assert b"ValueError: TEST_RAISE" in out
+        assert "SetuptoolsDeprecationWarning" in out
+        assert "ValueError: TEST_RAISE" in out
         # but installation should be successful
         out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
-        assert b"42" in out
+        assert "42" in out
 
 
 class TestCustomBuildWheel:

From eae4e26b0a1fb9ff8ca31ca2be4b58220b59a32f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 17:56:36 +0100
Subject: [PATCH 172/232] Ignore warning caused by 3rd-party setup.py

---
 setuptools/tests/test_integration.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py
index 1aa16172b57..77ef7336989 100644
--- a/setuptools/tests/test_integration.py
+++ b/setuptools/tests/test_integration.py
@@ -99,6 +99,10 @@ def test_pbr(install_context):
 
 
 @pytest.mark.xfail
+@pytest.mark.filterwarnings("ignore::EncodingWarning")
+# ^-- Dependency chain: `python-novaclient` < `oslo-utils` < `netifaces==0.11.0`
+#     netifaces' setup.py uses `open` without `encoding="utf-8"` which is hijacked by
+#     `setuptools.sandbox._open` and triggers the EncodingWarning.
 def test_python_novaclient(install_context):
     _install_one('python-novaclient', install_context, 'novaclient', 'base.py')
 

From 8a75f99ca60a8a78f1dbba94b2969d292ea5557c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 17:57:26 +0100
Subject: [PATCH 173/232] Use UTF-8 in setuptools/tests/test_easy_install.py

---
 setuptools/tests/test_easy_install.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 950cb23d211..ada4c322858 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -361,7 +361,7 @@ def test_many_pth_distributions_merge_together(self, tmpdir):
 
 @pytest.fixture
 def setup_context(tmpdir):
-    with (tmpdir / 'setup.py').open('w') as f:
+    with (tmpdir / 'setup.py').open('w', encoding="utf-8") as f:
         f.write(SETUP_PY)
     with tmpdir.as_cwd():
         yield tmpdir

From 2b82912b4b4a4576cd6edfd307f14ff615f21ed4 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 17:57:49 +0100
Subject: [PATCH 174/232] Attempt to use UTF-8 with develop command.

This change tries to use UTF-8 when writing `.egg-link` files.
When reading other files, we first attempt to use UTF-8 and then
fallback for the locale encoding.
---
 setuptools/command/develop.py | 27 +++++++++++++++++++++------
 1 file changed, 21 insertions(+), 6 deletions(-)

diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
index d8c1b49b3dc..aeb491fe2c3 100644
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -10,6 +10,8 @@
 from setuptools import namespaces
 import setuptools
 
+from ..compat import py39
+
 
 class develop(namespaces.DevelopInstaller, easy_install):
     """Set up package for development"""
@@ -119,7 +121,7 @@ def install_for_development(self):
         # create an .egg-link in the installation dir, pointing to our egg
         log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
         if not self.dry_run:
-            with open(self.egg_link, "w") as f:
+            with open(self.egg_link, "w", encoding="utf-8") as f:
                 f.write(self.egg_path + "\n" + self.setup_path)
         # postprocess the installed distro, fixing up .pth, installing scripts,
         # and handling requirements
@@ -128,9 +130,16 @@ def install_for_development(self):
     def uninstall_link(self):
         if os.path.exists(self.egg_link):
             log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
-            egg_link_file = open(self.egg_link)
-            contents = [line.rstrip() for line in egg_link_file]
-            egg_link_file.close()
+
+            try:
+                with open(self.egg_link, encoding="utf-8") as egg_link_file:
+                    contents = [line.rstrip() for line in egg_link_file]
+            except UnicodeDecodeError:  # pragma: no cover
+                with open(
+                    self.egg_link, encoding=py39.LOCALE_ENCODING
+                ) as egg_link_file:
+                    contents = [line.rstrip() for line in egg_link_file]
+
             if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
                 log.warn("Link points to %s: uninstall aborted", contents)
                 return
@@ -156,8 +165,14 @@ def install_egg_scripts(self, dist):
         for script_name in self.distribution.scripts or []:
             script_path = os.path.abspath(convert_path(script_name))
             script_name = os.path.basename(script_path)
-            with open(script_path) as strm:
-                script_text = strm.read()
+
+            try:
+                with open(script_path, encoding="utf-8") as strm:
+                    script_text = strm.read()
+            except UnicodeDecodeError:  # pragma: no cover
+                with open(script_path, encoding=py39.LOCALE_ENCODING) as strm:
+                    script_text = strm.read()
+
             self.install_script(dist, script_name, script_text, script_path)
 
         return None

From 74a622833716b9fbbf2b1a94c58b59d5defe0dd3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 18:32:08 +0100
Subject: [PATCH 175/232] Refactor some try..excepts into
 read_utf8_with_fallback

Extract common pattern for reading a file with UTF-8 into the
unicode_utils module.
---
 setuptools/command/develop.py | 23 ++++++-----------------
 setuptools/package_index.py   |  8 ++------
 setuptools/unicode_utils.py   | 17 +++++++++++++++++
 setuptools/wheel.py           |  9 ++-------
 4 files changed, 27 insertions(+), 30 deletions(-)

diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
index aeb491fe2c3..9966681bad8 100644
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -10,7 +10,7 @@
 from setuptools import namespaces
 import setuptools
 
-from ..compat import py39
+from ..unicode_utils import read_utf8_with_fallback
 
 
 class develop(namespaces.DevelopInstaller, easy_install):
@@ -131,14 +131,10 @@ def uninstall_link(self):
         if os.path.exists(self.egg_link):
             log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
 
-            try:
-                with open(self.egg_link, encoding="utf-8") as egg_link_file:
-                    contents = [line.rstrip() for line in egg_link_file]
-            except UnicodeDecodeError:  # pragma: no cover
-                with open(
-                    self.egg_link, encoding=py39.LOCALE_ENCODING
-                ) as egg_link_file:
-                    contents = [line.rstrip() for line in egg_link_file]
+            contents = [
+                line.rstrip()
+                for line in read_utf8_with_fallback(self.egg_link).splitlines()
+            ]
 
             if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
                 log.warn("Link points to %s: uninstall aborted", contents)
@@ -165,14 +161,7 @@ def install_egg_scripts(self, dist):
         for script_name in self.distribution.scripts or []:
             script_path = os.path.abspath(convert_path(script_name))
             script_name = os.path.basename(script_path)
-
-            try:
-                with open(script_path, encoding="utf-8") as strm:
-                    script_text = strm.read()
-            except UnicodeDecodeError:  # pragma: no cover
-                with open(script_path, encoding=py39.LOCALE_ENCODING) as strm:
-                    script_text = strm.read()
-
+            script_text = read_utf8_with_fallback(script_path)
             self.install_script(dist, script_name, script_text, script_path)
 
         return None
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index f835bdcf14d..2aa84641622 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -41,6 +41,7 @@
 from setuptools.extern.more_itertools import unique_everseen
 
 from .compat import py39
+from .unicode_utils import read_utf8_with_fallback
 
 
 EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
@@ -1120,12 +1121,7 @@ def local_open(url):
         for f in os.listdir(filename):
             filepath = os.path.join(filename, f)
             if f == 'index.html':
-                try:
-                    with open(filepath, 'r', encoding="utf-8") as fp:
-                        body = fp.read()
-                except UnicodeDecodeError:  # pragma: no cover
-                    with open(filepath, 'r', encoding=py39.LOCALE_ENCODING) as fp:
-                        body = fp.read()
+                body = read_utf8_with_fallback(filepath)
                 break
             elif os.path.isdir(filepath):
                 f += '/'
diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
index d43dcc11f91..4bc67feba07 100644
--- a/setuptools/unicode_utils.py
+++ b/setuptools/unicode_utils.py
@@ -1,6 +1,8 @@
 import unicodedata
 import sys
 
+from .compat import py39
+
 
 # HFS Plus uses decomposed UTF-8
 def decompose(path):
@@ -42,3 +44,18 @@ def try_encode(string, enc):
         return string.encode(enc)
     except UnicodeEncodeError:
         return None
+
+
+def read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING) -> str:
+    """
+    First try to read the file with UTF-8, if there is an error fallback to a
+    different encoding ("locale" by default). Returns the content of the file.
+    Also useful when reading files that might have been produced by an older version of
+    setuptools.
+    """
+    try:
+        with open(file, "r", encoding="utf-8") as f:
+            return f.read()
+    except UnicodeDecodeError:  # pragma: no cover
+        with open(file, "r", encoding=fallback_encoding) as f:
+            return f.read()
diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index 19f41574236..babd45940fe 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -18,7 +18,7 @@
 from setuptools.command.egg_info import write_requirements, _egg_basename
 from setuptools.archive_util import _unpack_zipfile_obj
 
-from .compat import py39
+from .unicode_utils import read_utf8_with_fallback
 
 
 WHEEL_NAME = re.compile(
@@ -224,12 +224,7 @@ def _move_data_entries(destination_eggdir, dist_data):
     def _fix_namespace_packages(egg_info, destination_eggdir):
         namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')
         if os.path.exists(namespace_packages):
-            try:
-                with open(namespace_packages, encoding="utf-8") as fp:
-                    namespace_packages = fp.read().split()
-            except UnicodeDecodeError:  # pragma: no cover
-                with open(namespace_packages, encoding=py39.LOCALE_ENCODING) as fp:
-                    namespace_packages = fp.read().split()
+            namespace_packages = read_utf8_with_fallback(namespace_packages).split()
 
             for mod in namespace_packages:
                 mod_dir = os.path.join(destination_eggdir, *mod.split('.'))

From aeac45b14d910044e2c1f0d2faec231b8c41fbeb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 17 Apr 2024 19:05:14 +0100
Subject: [PATCH 176/232] Avoid using EncodingWarning because it is not defined
 for Python < 3.10

---
 setuptools/tests/test_integration.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py
index 77ef7336989..71f10d9a6e0 100644
--- a/setuptools/tests/test_integration.py
+++ b/setuptools/tests/test_integration.py
@@ -99,10 +99,11 @@ def test_pbr(install_context):
 
 
 @pytest.mark.xfail
-@pytest.mark.filterwarnings("ignore::EncodingWarning")
+@pytest.mark.filterwarnings("ignore:'encoding' argument not specified")
 # ^-- Dependency chain: `python-novaclient` < `oslo-utils` < `netifaces==0.11.0`
 #     netifaces' setup.py uses `open` without `encoding="utf-8"` which is hijacked by
 #     `setuptools.sandbox._open` and triggers the EncodingWarning.
+#     Can't use EncodingWarning in the filter, as it does not exist on Python < 3.10.
 def test_python_novaclient(install_context):
     _install_one('python-novaclient', install_context, 'novaclient', 'base.py')
 

From 9fd598172bc4af6e90f95ff9916faf3e8717e497 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 10:59:52 +0100
Subject: [PATCH 177/232] Mark read_utf8_with_fallback as private

---
 setuptools/command/develop.py | 6 +++---
 setuptools/package_index.py   | 4 ++--
 setuptools/unicode_utils.py   | 2 +-
 setuptools/wheel.py           | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
index 9966681bad8..d07736a005f 100644
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -10,7 +10,7 @@
 from setuptools import namespaces
 import setuptools
 
-from ..unicode_utils import read_utf8_with_fallback
+from ..unicode_utils import _read_utf8_with_fallback
 
 
 class develop(namespaces.DevelopInstaller, easy_install):
@@ -133,7 +133,7 @@ def uninstall_link(self):
 
             contents = [
                 line.rstrip()
-                for line in read_utf8_with_fallback(self.egg_link).splitlines()
+                for line in _read_utf8_with_fallback(self.egg_link).splitlines()
             ]
 
             if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
@@ -161,7 +161,7 @@ def install_egg_scripts(self, dist):
         for script_name in self.distribution.scripts or []:
             script_path = os.path.abspath(convert_path(script_name))
             script_name = os.path.basename(script_path)
-            script_text = read_utf8_with_fallback(script_path)
+            script_text = _read_utf8_with_fallback(script_path)
             self.install_script(dist, script_name, script_text, script_path)
 
         return None
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 2aa84641622..0ca87df357d 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -41,7 +41,7 @@
 from setuptools.extern.more_itertools import unique_everseen
 
 from .compat import py39
-from .unicode_utils import read_utf8_with_fallback
+from .unicode_utils import _read_utf8_with_fallback
 
 
 EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
@@ -1121,7 +1121,7 @@ def local_open(url):
         for f in os.listdir(filename):
             filepath = os.path.join(filename, f)
             if f == 'index.html':
-                body = read_utf8_with_fallback(filepath)
+                body = _read_utf8_with_fallback(filepath)
                 break
             elif os.path.isdir(filepath):
                 f += '/'
diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
index 4bc67feba07..6b60417a917 100644
--- a/setuptools/unicode_utils.py
+++ b/setuptools/unicode_utils.py
@@ -46,7 +46,7 @@ def try_encode(string, enc):
         return None
 
 
-def read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING) -> str:
+def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING) -> str:
     """
     First try to read the file with UTF-8, if there is an error fallback to a
     different encoding ("locale" by default). Returns the content of the file.
diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index babd45940fe..e06daec4d0e 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -18,7 +18,7 @@
 from setuptools.command.egg_info import write_requirements, _egg_basename
 from setuptools.archive_util import _unpack_zipfile_obj
 
-from .unicode_utils import read_utf8_with_fallback
+from .unicode_utils import _read_utf8_with_fallback
 
 
 WHEEL_NAME = re.compile(
@@ -224,7 +224,7 @@ def _move_data_entries(destination_eggdir, dist_data):
     def _fix_namespace_packages(egg_info, destination_eggdir):
         namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')
         if os.path.exists(namespace_packages):
-            namespace_packages = read_utf8_with_fallback(namespace_packages).split()
+            namespace_packages = _read_utf8_with_fallback(namespace_packages).split()
 
             for mod in namespace_packages:
                 mod_dir = os.path.join(destination_eggdir, *mod.split('.'))

From 9aa9f22e04b473af110461fea591560678bc1284 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:18:45 +0100
Subject: [PATCH 178/232] Add newsfragment

---
 newsfragments/4309.removal.rst | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 newsfragments/4309.removal.rst

diff --git a/newsfragments/4309.removal.rst b/newsfragments/4309.removal.rst
new file mode 100644
index 00000000000..08818104f94
--- /dev/null
+++ b/newsfragments/4309.removal.rst
@@ -0,0 +1,5 @@
+Further adoption of UTF-8 in ``setuptools``.
+This change regards mostly files produced and consumed during the build process
+(e.g. metadata files, script wrappers, automatically updated config files, etc..)
+Although precautions were taken to minimize disruptions, some edge cases might
+be subject to backwards incompatibility.

From 8b2009176279f977f6b6b50e8b8c4e4ba5b9f99e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:25:18 +0100
Subject: [PATCH 179/232] Read files using UTF-8 in pkg_resources, with
 fallback to locale

---
 pkg_resources/__init__.py | 25 ++++++++++++++++++-------
 1 file changed, 18 insertions(+), 7 deletions(-)

diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index c2ba0476e51..5d773da5412 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -1524,8 +1524,7 @@ def run_script(self, script_name, namespace):
         script_filename = self._fn(self.egg_info, script)
         namespace['__file__'] = script_filename
         if os.path.exists(script_filename):
-            with open(script_filename) as fid:
-                source = fid.read()
+            source = _read_utf8_with_fallback(script_filename)
             code = compile(source, script_filename, 'exec')
             exec(code, namespace, namespace)
         else:
@@ -2175,11 +2174,10 @@ def non_empty_lines(path):
     """
     Yield non-empty lines from file at path
     """
-    with open(path) as f:
-        for line in f:
-            line = line.strip()
-            if line:
-                yield line
+    for line in _read_utf8_with_fallback(path).splitlines():
+        line = line.strip()
+        if line:
+            yield line
 
 
 def resolve_egg_link(path):
@@ -3323,3 +3321,16 @@ def _initialize_master_working_set():
     # match order
     list(map(working_set.add_entry, sys.path))
     globals().update(locals())
+
+
+#  ---- Ported from ``setuptools`` to avoid introducing dependencies ----
+LOCALE_ENCODING = "locale" if sys.version_info >= (3, 10) else None
+
+
+def _read_utf8_with_fallback(file: str, fallback_encoding=LOCALE_ENCODING) -> str:
+    try:
+        with open(file, "r", encoding="utf-8") as f:
+            return f.read()
+    except UnicodeDecodeError:  # pragma: no cover
+        with open(file, "r", encoding=fallback_encoding) as f:
+            return f.read()

From 69f580687de2af6760fe171c81d84e6bfcc665ea Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:39:50 +0100
Subject: [PATCH 180/232] Use UTF-8 for writing python stubs

Since Python3 is "UTF-8 first", this change should not cause problems.
---
 setuptools/command/bdist_egg.py | 2 +-
 setuptools/command/build_ext.py | 6 +++---
 setuptools/package_index.py     | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py
index b2897bfbb4d..5581b1d2e0d 100644
--- a/setuptools/command/bdist_egg.py
+++ b/setuptools/command/bdist_egg.py
@@ -54,7 +54,7 @@ def __bootstrap__():
         __bootstrap__()
         """
     ).lstrip()
-    with open(pyfile, 'w') as f:
+    with open(pyfile, 'w', encoding="utf-8") as f:
         f.write(_stub_template % resource)
 
 
diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py
index b5c98c86dcd..49699d30ecc 100644
--- a/setuptools/command/build_ext.py
+++ b/setuptools/command/build_ext.py
@@ -342,8 +342,7 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
         if compile and os.path.exists(stub_file):
             raise BaseError(stub_file + " already exists! Please delete.")
         if not self.dry_run:
-            f = open(stub_file, 'w')
-            f.write(
+            content = (
                 '\n'.join([
                     "def __bootstrap__():",
                     "   global __bootstrap__, __file__, __loader__",
@@ -369,7 +368,8 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
                     "",  # terminal \n
                 ])
             )
-            f.close()
+            with open(stub_file, 'w', encoding="utf-8") as f:
+                f.write(content)
         if compile:
             self._compile_and_remove_stub(stub_file)
 
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 0ca87df357d..42a98b919a4 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -717,7 +717,7 @@ def gen_setup(self, filename, fragment, tmpdir):
                     shutil.copy2(filename, dst)
                     filename = dst
 
-            with open(os.path.join(tmpdir, 'setup.py'), 'w') as file:
+            with open(os.path.join(tmpdir, 'setup.py'), 'w', encoding="utf-8") as file:
                 file.write(
                     "from setuptools import setup\n"
                     "setup(name=%r, version=%r, py_modules=[%r])\n"

From 2675e85a20cff489b9bdce0d958968eca24d542d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:41:45 +0100
Subject: [PATCH 181/232] Use UTF-8 to write metadata files

---
 setuptools/command/bdist_egg.py    |  7 +++----
 setuptools/command/easy_install.py | 16 +++++++---------
 2 files changed, 10 insertions(+), 13 deletions(-)

diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py
index 5581b1d2e0d..adcb0a1ba1c 100644
--- a/setuptools/command/bdist_egg.py
+++ b/setuptools/command/bdist_egg.py
@@ -200,10 +200,9 @@ def run(self):  # noqa: C901  # is too complex (14)  # FIXME
             log.info("writing %s", native_libs)
             if not self.dry_run:
                 ensure_directory(native_libs)
-                libs_file = open(native_libs, 'wt')
-                libs_file.write('\n'.join(all_outputs))
-                libs_file.write('\n')
-                libs_file.close()
+                with open(native_libs, 'wt', encoding="utf-8") as libs_file:
+                    libs_file.write('\n'.join(all_outputs))
+                    libs_file.write('\n')
         elif os.path.isfile(native_libs):
             log.info("removing %s", native_libs)
             if not self.dry_run:
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 3ad984f2127..bfacf1e46fa 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -1023,12 +1023,11 @@ def install_exe(self, dist_filename, tmpdir):
 
         # Write EGG-INFO/PKG-INFO
         if not os.path.exists(pkg_inf):
-            f = open(pkg_inf, 'w')  # TODO: probably it is safe to use utf-8
-            f.write('Metadata-Version: 1.0\n')
-            for k, v in cfg.items('metadata'):
-                if k != 'target_version':
-                    f.write('%s: %s\n' % (k.replace('_', '-').title(), v))
-            f.close()
+            with open(pkg_inf, 'w', encoding="utf-8") as f:
+                f.write('Metadata-Version: 1.0\n')
+                for k, v in cfg.items('metadata'):
+                    if k != 'target_version':
+                        f.write('%s: %s\n' % (k.replace('_', '-').title(), v))
         script_dir = os.path.join(_egg_info, 'scripts')
         # delete entry-point scripts to avoid duping
         self.delete_blockers([
@@ -1094,9 +1093,8 @@ def process(src, dst):
             if locals()[name]:
                 txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt')
                 if not os.path.exists(txt):
-                    f = open(txt, 'w')  # TODO: probably it is safe to use utf-8
-                    f.write('\n'.join(locals()[name]) + '\n')
-                    f.close()
+                    with open(txt, 'w', encoding="utf-8") as f:
+                        f.write('\n'.join(locals()[name]) + '\n')
 
     def install_wheel(self, wheel_path, tmpdir):
         wheel = Wheel(wheel_path)

From 5305908063aaea823dfc337cb1e5667e7bf2220a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:42:20 +0100
Subject: [PATCH 182/232] Attempt to use UTF-8 to read egg-link files in
 package_index

---
 setuptools/package_index.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 42a98b919a4..918a34e102d 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -422,9 +422,9 @@ def scan_egg_links(self, search_path):
         list(itertools.starmap(self.scan_egg_link, egg_links))
 
     def scan_egg_link(self, path, entry):
-        with open(os.path.join(path, entry)) as raw_lines:
-            # filter non-empty lines
-            lines = list(filter(None, map(str.strip, raw_lines)))
+        content = _read_utf8_with_fallback(os.path.join(path, entry))
+        # filter non-empty lines
+        lines = list(filter(None, map(str.strip, content.splitlines())))
 
         if len(lines) != 2:
             # format is not recognized; punt

From f35f9122376186da349ad040dfb1fc59328e52d4 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 11:52:36 +0100
Subject: [PATCH 183/232] Apply ruff formatting

---
 setuptools/command/build_ext.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py
index 49699d30ecc..6056fe9b24b 100644
--- a/setuptools/command/build_ext.py
+++ b/setuptools/command/build_ext.py
@@ -342,8 +342,8 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
         if compile and os.path.exists(stub_file):
             raise BaseError(stub_file + " already exists! Please delete.")
         if not self.dry_run:
-            content = (
-                '\n'.join([
+            with open(stub_file, 'w', encoding="utf-8") as f:
+                content = '\n'.join([
                     "def __bootstrap__():",
                     "   global __bootstrap__, __file__, __loader__",
                     "   import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
@@ -367,8 +367,6 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
                     "__bootstrap__()",
                     "",  # terminal \n
                 ])
-            )
-            with open(stub_file, 'w', encoding="utf-8") as f:
                 f.write(content)
         if compile:
             self._compile_and_remove_stub(stub_file)

From 39a8ef47dbaa5f569cfc327175aa8b74fd572eeb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 12:11:16 +0100
Subject: [PATCH 184/232] Simplify conditional encoding in install_scripts and
 easy_install

---
 setuptools/command/easy_install.py    | 8 ++------
 setuptools/command/install_scripts.py | 8 ++------
 2 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index bfacf1e46fa..41ff382fe4d 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -874,12 +874,8 @@ def write_script(self, script_name, contents, mode="t", blockers=()):
         if os.path.exists(target):
             os.unlink(target)
 
-        if "b" not in mode and isinstance(contents, str):
-            kw = {"encoding": "utf-8"}
-        else:
-            kw = {}
-
-        with open(target, "w" + mode, **kw) as f:
+        encoding = None if "b" in mode else "utf-8"
+        with open(target, "w" + mode, encoding=encoding) as f:
             f.write(contents)
         chmod(target, 0o777 - mask)
 
diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py
index 758937b6143..d79a4ab7b03 100644
--- a/setuptools/command/install_scripts.py
+++ b/setuptools/command/install_scripts.py
@@ -57,14 +57,10 @@ def write_script(self, script_name, contents, mode="t", *ignored):
         target = os.path.join(self.install_dir, script_name)
         self.outfiles.append(target)
 
-        if "b" not in mode and isinstance(contents, str):
-            kw = {"encoding": "utf-8"}
-        else:
-            kw = {}
-
+        encoding = None if "b" in mode else "utf-8"
         mask = current_umask()
         if not self.dry_run:
             ensure_directory(target)
-            with open(target, "w" + mode, **kw) as f:
+            with open(target, "w" + mode, encoding=encoding) as f:
                 f.write(contents)
             chmod(target, 0o777 - mask)

From b8da410f4121c527996ed63affa686f13215a216 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Sun, 21 Apr 2024 16:32:00 -0400
Subject: [PATCH 185/232] Fix missing backtick in changelog

---
 NEWS.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NEWS.rst b/NEWS.rst
index 20c6903a33a..73a8148d9ca 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -119,7 +119,7 @@ Improved Documentation
 ----------------------
 
 - Updated documentation referencing obsolete Python 3.7 code. -- by :user:`Avasam` (#4096)
-- Changed ``versionadded`` for "Type information included by default" feature from ``v68.3.0`` to ``v69.0.0`` -- by :user:Avasam` (#4182)
+- Changed ``versionadded`` for "Type information included by default" feature from ``v68.3.0`` to ``v69.0.0`` -- by :user:`Avasam` (#4182)
 - Described the auto-generated files -- by :user:`VladimirFokow` (#4198)
 - Updated "Quickstart" to describe the current status of ``setup.cfg`` and ``pyproject.toml`` -- by :user:`VladimirFokow` (#4200)
 

From 3fbaa4c6d5af1f7846fc21c7fb54952c5cc23621 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 10:31:20 +0100
Subject: [PATCH 186/232] Add deprecation warning for non utf-8

---
 pkg_resources/__init__.py    | 21 +++++++++++++++++-
 setuptools/command/setopt.py |  9 ++------
 setuptools/package_index.py  |  9 ++------
 setuptools/unicode_utils.py  | 41 ++++++++++++++++++++++++++++++++++++
 4 files changed, 65 insertions(+), 15 deletions(-)

diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index 5d773da5412..675b728f9dd 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -3323,14 +3323,33 @@ def _initialize_master_working_set():
     globals().update(locals())
 
 
-#  ---- Ported from ``setuptools`` to avoid introducing dependencies ----
+#  ---- Ported from ``setuptools`` to avoid introducing an import inter-dependency ----
 LOCALE_ENCODING = "locale" if sys.version_info >= (3, 10) else None
 
 
 def _read_utf8_with_fallback(file: str, fallback_encoding=LOCALE_ENCODING) -> str:
+    """See setuptools.unicode_utils._read_utf8_with_fallback"""
     try:
         with open(file, "r", encoding="utf-8") as f:
             return f.read()
     except UnicodeDecodeError:  # pragma: no cover
+        msg = f"""\
+        ********************************************************************************
+        `encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`.
+
+        This fallback behaviour is considered **deprecated** and future versions of
+        `setuptools/pkg_resources` may not implement it.
+
+        Please encode {file!r} with "utf-8" to ensure future builds will succeed.
+
+        If this file was produced by `setuptools` itself, cleaning up the cached files
+        and re-building/re-installing the package with a newer version of `setuptools`
+        (e.g. by updating `build-system.requires` in its `pyproject.toml`)
+        might solve the problem.
+        ********************************************************************************
+        """
+        # TODO: Add a deadline?
+        #       See comment in setuptools.unicode_utils._Utf8EncodingNeeded
+        warnings.warns(msg, PkgResourcesDeprecationWarning, stacklevel=2)
         with open(file, "r", encoding=fallback_encoding) as f:
             return f.read()
diff --git a/setuptools/command/setopt.py b/setuptools/command/setopt.py
index 89b1ac73078..b78d845e60a 100644
--- a/setuptools/command/setopt.py
+++ b/setuptools/command/setopt.py
@@ -6,7 +6,7 @@
 import configparser
 
 from .. import Command
-from ..compat import py39
+from ..unicode_utils import _cfg_read_utf8_with_fallback
 
 __all__ = ['config_file', 'edit_config', 'option_base', 'setopt']
 
@@ -37,12 +37,7 @@ def edit_config(filename, settings, dry_run=False):
     log.debug("Reading configuration from %s", filename)
     opts = configparser.RawConfigParser()
     opts.optionxform = lambda x: x
-
-    try:
-        opts.read([filename], encoding="utf-8")
-    except UnicodeDecodeError:  # pragma: no cover
-        opts.clear()
-        opts.read([filename], encoding=py39.LOCALE_ENCODING)
+    _cfg_read_utf8_with_fallback(opts, filename)
 
     for section, options in settings.items():
         if options is None:
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 918a34e102d..f5a7d77eed8 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -40,8 +40,7 @@
 from setuptools.wheel import Wheel
 from setuptools.extern.more_itertools import unique_everseen
 
-from .compat import py39
-from .unicode_utils import _read_utf8_with_fallback
+from .unicode_utils import _read_utf8_with_fallback, _cfg_read_utf8_with_fallback
 
 
 EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
@@ -1014,11 +1013,7 @@ def __init__(self):
 
         rc = os.path.join(os.path.expanduser('~'), '.pypirc')
         if os.path.exists(rc):
-            try:
-                self.read(rc, encoding="utf-8")
-            except UnicodeDecodeError:  # pragma: no cover
-                self.clean()
-                self.read(rc, encoding=py39.LOCALE_ENCODING)
+            _cfg_read_utf8_with_fallback(self, rc)
 
     @property
     def creds_by_repository(self):
diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
index 6b60417a917..9934330da93 100644
--- a/setuptools/unicode_utils.py
+++ b/setuptools/unicode_utils.py
@@ -1,7 +1,9 @@
 import unicodedata
 import sys
+from configparser import ConfigParser
 
 from .compat import py39
+from .warnings import SetuptoolsDeprecationWarning
 
 
 # HFS Plus uses decomposed UTF-8
@@ -57,5 +59,44 @@ def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING)
         with open(file, "r", encoding="utf-8") as f:
             return f.read()
     except UnicodeDecodeError:  # pragma: no cover
+        _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding)
         with open(file, "r", encoding=fallback_encoding) as f:
             return f.read()
+
+
+def _cfg_read_utf8_with_fallback(
+    cfg: ConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
+) -> str:
+    """Same idea as :func:`_read_utf8_with_fallback`, but for the
+    :meth:`ConfigParser.read` method.
+
+    This method may call ``cfg.clear()``.
+    """
+    try:
+        cfg.read(file, encoding="utf-8")
+    except UnicodeDecodeError:  # pragma: no cover
+        _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding)
+        cfg.clear()
+        cfg.read(file, encoding=fallback_encoding)
+
+
+class _Utf8EncodingNeeded(SetuptoolsDeprecationWarning):
+    _SUMMARY = """
+    `encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`.
+    """
+
+    _DETAILS = """
+    Fallback behaviour for UTF-8 is considered **deprecated** and future versions of
+    `setuptools` may not implement it.
+
+    Please encode {file!r} with "utf-8" to ensure future builds will succeed.
+
+    If this file was produced by `setuptools` itself, cleaning up the cached files
+    and re-building/re-installing the package with a newer version of `setuptools`
+    (e.g. by updating `build-system.requires` in its `pyproject.toml`)
+    might solve the problem.
+    """
+    # TODO: Add a deadline?
+    #       Will we be able to remove this?
+    #       The question comes to mind mainly because of sdists that have been produced
+    #       by old versions of setuptools and published to PyPI...

From 463b60c8801e7ae1f902bf2f53fa32889b867866 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 10:34:52 +0100
Subject: [PATCH 187/232] Update news fragment to mention deprecation

---
 newsfragments/4309.removal.rst | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/newsfragments/4309.removal.rst b/newsfragments/4309.removal.rst
index 08818104f94..b69b17d45f7 100644
--- a/newsfragments/4309.removal.rst
+++ b/newsfragments/4309.removal.rst
@@ -3,3 +3,5 @@ This change regards mostly files produced and consumed during the build process
 (e.g. metadata files, script wrappers, automatically updated config files, etc..)
 Although precautions were taken to minimize disruptions, some edge cases might
 be subject to backwards incompatibility.
+
+Support for ``"locale"`` encoding is now **deprecated**.

From 1c91ac81823a32d2d7ff55cde8abef7e4ebfc3e3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 10:39:41 +0100
Subject: [PATCH 188/232] Fix type hint in setuptools/unicode_utils.py

---
 setuptools/unicode_utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
index 9934330da93..696b34c46a1 100644
--- a/setuptools/unicode_utils.py
+++ b/setuptools/unicode_utils.py
@@ -66,7 +66,7 @@ def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING)
 
 def _cfg_read_utf8_with_fallback(
     cfg: ConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
-) -> str:
+) -> None:
     """Same idea as :func:`_read_utf8_with_fallback`, but for the
     :meth:`ConfigParser.read` method.
 

From 57a29feea3917cad0fc45e5b6148a70b7aab0f5b Mon Sep 17 00:00:00 2001
From: shenxianpeng 
Date: Fri, 19 Apr 2024 04:58:32 +0000
Subject: [PATCH 189/232] Uses RST substitution to put badges in 1 line

---
 README.rst | 26 ++++++++++++++------------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/README.rst b/README.rst
index eec6e35531d..181c3b2af6a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,32 +1,34 @@
-.. image:: https://img.shields.io/pypi/v/setuptools.svg
+.. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg
    :target: https://pypi.org/project/setuptools
 
-.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg
+.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg
 
-.. image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg
+.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg
    :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22
    :alt: tests
 
-.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
-    :target: https://github.com/astral-sh/ruff
-    :alt: Ruff
+.. |ruff-badge| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+   :target: https://github.com/astral-sh/ruff
+   :alt: Ruff
 
-.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg
-    :target: https://setuptools.pypa.io
+.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg
+   :target: https://setuptools.pypa.io
 
-.. image:: https://img.shields.io/badge/skeleton-2024-informational
+.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2024-informational
    :target: https://blog.jaraco.com/skeleton
 
-.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white
+.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white
    :target: https://codecov.io/gh/pypa/setuptools
 
-.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat
+.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat
    :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme
 
-.. image:: https://img.shields.io/discord/803025117553754132
+.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132
    :target: https://discord.com/channels/803025117553754132/815945031150993468
    :alt: Discord
 
+|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge|
+
 See the `Quickstart `_
 and the `User's Guide `_ for
 instructions on how to use Setuptools.

From d16f1921723de09bcb61e814c63c78f23631f4ef Mon Sep 17 00:00:00 2001
From: shenxianpeng 
Date: Fri, 19 Apr 2024 05:04:39 +0000
Subject: [PATCH 190/232] Added a news fragment

---
 newsfragments/4312.doc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 newsfragments/4312.doc.rst

diff --git a/newsfragments/4312.doc.rst b/newsfragments/4312.doc.rst
new file mode 100644
index 00000000000..7ada9548767
--- /dev/null
+++ b/newsfragments/4312.doc.rst
@@ -0,0 +1 @@
+Uses RST substitution to put badges in 1 line.

From a14e36a670ec76fc631eba04f3bbde3b57ef5547 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:01:07 +0100
Subject: [PATCH 191/232] Add cog annotations to extern/__init__.py for future
 checks

---
 pkg_resources/extern/__init__.py | 7 +++++++
 setuptools/extern/__init__.py    | 7 +++++++
 2 files changed, 14 insertions(+)

diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py
index df96f7f26d1..12a8fccda16 100644
--- a/pkg_resources/extern/__init__.py
+++ b/pkg_resources/extern/__init__.py
@@ -70,6 +70,12 @@ def install(self):
             sys.meta_path.append(self)
 
 
+# [[[cog
+# import cog
+# from tools.vendored import yield_root_package
+# names = "\n".join(f"    {x!r}," for x in yield_root_package('pkg_resources'))
+# cog.outl(f"names = (\n{names}\n)")
+# ]]]
 names = (
     'packaging',
     'platformdirs',
@@ -78,4 +84,5 @@ def install(self):
     'more_itertools',
     'backports',
 )
+# [[[end]]]
 VendorImporter(__name__, names).install()
diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py
index 427b27cb809..66e216f9b4e 100644
--- a/setuptools/extern/__init__.py
+++ b/setuptools/extern/__init__.py
@@ -70,6 +70,12 @@ def install(self):
             sys.meta_path.append(self)
 
 
+# [[[cog
+# import cog
+# from tools.vendored import yield_root_package
+# names = "\n".join(f"    {x!r}," for x in yield_root_package('setuptools'))
+# cog.outl(f"names = (\n{names}\n)")
+# ]]]
 names = (
     'packaging',
     'ordered_set',
@@ -82,4 +88,5 @@ def install(self):
     'tomli',
     'backports',
 )
+# [[[end]]]
 VendorImporter(__name__, names, 'setuptools._vendor').install()

From a19973b25e46219865310402ee17494ff02a0809 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:25:43 +0100
Subject: [PATCH 192/232] Add a function to tools/vendored to list root
 packages

---
 tools/vendored.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/tools/vendored.py b/tools/vendored.py
index 232e9625d27..685b0841341 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -166,4 +166,17 @@ def update_setuptools():
     rewrite_more_itertools(vendor / "more_itertools")
 
 
+def yield_root_package(name):
+    """Useful when defining the MetaPathFinder
+    >>> set(yield_root_package("setuptools")) & {"jaraco", "backports"}
+    {'jaraco', 'backports'}
+    """
+    vendored = Path(f"{name}/_vendor/vendored.txt")
+    yield from (
+        line.partition("=")[0].partition(".")[0].replace("-", "_")
+        for line in vendored.read_text(encoding="utf-8").splitlines()
+        if line and not line.startswith("#")
+    )
+
+
 __name__ == '__main__' and update_vendored()

From bf573220152f47d0b90ef6f6e2d90890fb41f564 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:28:47 +0100
Subject: [PATCH 193/232] Update tox testenv 'vendor' to use cog to
 automatically update/check *.extern

---
 tox.ini | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/tox.ini b/tox.ini
index 7412730008b..22dd7af8da4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -69,12 +69,16 @@ pass_env = *
 commands =
 	python tools/finalize.py
 
-[testenv:vendor]
+[testenv:{vendor,check-extern}]
 skip_install = True
+allowlist_externals = sh
 deps =
 	path
+	cogapp
 commands =
-	python -m tools.vendored
+	vendor: python -m tools.vendored
+	vendor: sh -c "git grep -l -F '\[\[\[cog' | xargs cog -I {toxinidir} -r"  # update `*.extern`
+	check-extern: sh -c "git grep -l -F '\[\[\[cog' | xargs cog -I {toxinidir} --check"
 
 [testenv:generate-validation-code]
 skip_install = True

From 536ff950a05f7adfd5626d1cf3cdf7bffb3f887e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:39:38 +0100
Subject: [PATCH 194/232] Sync */vendored.txt and *.extern.py

---
 pkg_resources/_vendor/vendored.txt | 2 ++
 pkg_resources/extern/__init__.py   | 2 ++
 setuptools/extern/__init__.py      | 6 +++---
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt
index c18a2cc0ebd..967a2841f3b 100644
--- a/pkg_resources/_vendor/vendored.txt
+++ b/pkg_resources/_vendor/vendored.txt
@@ -9,5 +9,7 @@ jaraco.text==3.7.0
 importlib_resources==5.10.2
 # required for importlib_resources on older Pythons
 zipp==3.7.0
+# required for jaraco.functools
+more_itertools==8.8.0
 # required for jaraco.context on older Pythons
 backports.tarfile
diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py
index 12a8fccda16..7f80b04164f 100644
--- a/pkg_resources/extern/__init__.py
+++ b/pkg_resources/extern/__init__.py
@@ -79,8 +79,10 @@ def install(self):
 names = (
     'packaging',
     'platformdirs',
+    'typing_extensions',
     'jaraco',
     'importlib_resources',
+    'zipp',
     'more_itertools',
     'backports',
 )
diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py
index 66e216f9b4e..16e2c9ea9e6 100644
--- a/setuptools/extern/__init__.py
+++ b/setuptools/extern/__init__.py
@@ -80,11 +80,11 @@ def install(self):
     'packaging',
     'ordered_set',
     'more_itertools',
-    'importlib_metadata',
-    'zipp',
-    'importlib_resources',
     'jaraco',
+    'importlib_resources',
+    'importlib_metadata',
     'typing_extensions',
+    'zipp',
     'tomli',
     'backports',
 )

From 35bb574c99b1730d4284b37bd7770c2bf9832e82 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:49:20 +0100
Subject: [PATCH 195/232] Add 'check-extern' as collateral to github actions
 workflow

---
 .github/workflows/main.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3f3b53aa076..6ec4f83be59 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -122,6 +122,7 @@ jobs:
         job:
         - diffcov
         - docs
+        - check-extern
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4

From 86770badfb4290cfbdce19880f5590fb33390896 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 13:58:58 +0100
Subject: [PATCH 196/232] Match version of more_itertools that is already
 installed in pkg_resources with vendored.txt

---
 pkg_resources/_vendor/vendored.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt
index 967a2841f3b..f8cbfd967ed 100644
--- a/pkg_resources/_vendor/vendored.txt
+++ b/pkg_resources/_vendor/vendored.txt
@@ -10,6 +10,6 @@ importlib_resources==5.10.2
 # required for importlib_resources on older Pythons
 zipp==3.7.0
 # required for jaraco.functools
-more_itertools==8.8.0
+more_itertools==10.2.0
 # required for jaraco.context on older Pythons
 backports.tarfile

From 175787e1066c865dcaf18e66563a00233eab2c9d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 14:25:11 +0100
Subject: [PATCH 197/232] Improve determinism in doctest for tools/vendored

---
 tools/vendored.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/tools/vendored.py b/tools/vendored.py
index 685b0841341..63797ea24af 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -168,8 +168,9 @@ def update_setuptools():
 
 def yield_root_package(name):
     """Useful when defining the MetaPathFinder
-    >>> set(yield_root_package("setuptools")) & {"jaraco", "backports"}
-    {'jaraco', 'backports'}
+    >>> examples = set(yield_root_package("setuptools")) & {"jaraco", "backports"}
+    >>> list(sorted(examples))
+    ['backports', 'jaraco']
     """
     vendored = Path(f"{name}/_vendor/vendored.txt")
     yield from (

From a30589bf3ce8b94b1b8abbb14e8a470778680950 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 12:48:42 +0100
Subject: [PATCH 198/232] Avoid errors on Python 3.8 macos-latest as GitHub CI
 has dropped support

---
 .github/workflows/main.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3f3b53aa076..d08b857eca1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -61,6 +61,12 @@ jobs:
         - platform: ubuntu-latest
           python: "3.10"
           distutils: stdlib
+        # Python 3.9 is on macos-13 but not macos-latest (macos-14-arm64)
+        # https://github.com/actions/setup-python/issues/850
+        # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760
+        - {python: "3.8", platform: "macos-13"}
+        exclude:
+        - {python: "3.8", platform: "macos-latest"}
     runs-on: ${{ matrix.platform }}
     continue-on-error: ${{ matrix.python == '3.13' }}
     env:

From 37f20201aec28a61f011a6248f042d174292976d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 13:02:57 +0100
Subject: [PATCH 199/232] Add review suggestions.

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d08b857eca1..82757f478cb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -61,7 +61,7 @@ jobs:
         - platform: ubuntu-latest
           python: "3.10"
           distutils: stdlib
-        # Python 3.9 is on macos-13 but not macos-latest (macos-14-arm64)
+        # Python 3.8, 3.9 are on macos-13 but not macos-latest (macos-14-arm64)
         # https://github.com/actions/setup-python/issues/850
         # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760
         - {python: "3.8", platform: "macos-13"}

From b4cfab303aa1f3c2b52ff62da479f321f0681a08 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 13:34:38 +0100
Subject: [PATCH 200/232] Mark unstable tests on macOS

---
 setuptools/tests/test_editable_install.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 119b1286942..91b65e5a389 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -118,6 +118,7 @@ def editable_opts(request):
 SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
 
 
+@pytest.mark.xfail(sys.platform == "darwin", reason="Test is unstable on macOS?")
 @pytest.mark.parametrize(
     "files",
     [
@@ -897,6 +898,7 @@ class TestOverallBehaviour:
         },
     }
 
+    @pytest.mark.xfail(sys.platform == "darwin", reason="Test is unstable on macOS?")
     @pytest.mark.parametrize("layout", EXAMPLES.keys())
     def test_editable_install(self, tmp_path, venv, layout, editable_opts):
         project, _ = install_project(

From f3f5bf7869e87d80eb9457c2e0537f82af255500 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 13:44:51 +0100
Subject: [PATCH 201/232] Add proper xfail mark

---
 setuptools/tests/test_editable_install.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 91b65e5a389..300a02cfb9e 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -118,7 +118,7 @@ def editable_opts(request):
 SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
 
 
-@pytest.mark.xfail(sys.platform == "darwin", reason="Test is unstable on macOS?")
+@pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
 @pytest.mark.parametrize(
     "files",
     [
@@ -898,7 +898,7 @@ class TestOverallBehaviour:
         },
     }
 
-    @pytest.mark.xfail(sys.platform == "darwin", reason="Test is unstable on macOS?")
+    @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
     @pytest.mark.parametrize("layout", EXAMPLES.keys())
     def test_editable_install(self, tmp_path, venv, layout, editable_opts):
         project, _ = install_project(

From 80d101eea16a6fc72759e193882ef60451f51d98 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Fri, 8 Mar 2024 17:21:56 -0500
Subject: [PATCH 202/232] Update `pytest.ini` for `EncodingWarning` from
 external libraries + avoid getpreferredencoding when possible

---
 pytest.ini                           | 27 ++++++++++++++-------------
 setuptools/command/editable_wheel.py |  3 ++-
 setuptools/tests/__init__.py         |  9 +++++++--
 3 files changed, 23 insertions(+), 16 deletions(-)

diff --git a/pytest.ini b/pytest.ini
index e7c96274a3b..40a64b5cd4c 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -10,6 +10,18 @@ filterwarnings=
 	# Fail on warnings
 	error
 
+	# Workarounds for pypa/setuptools#3810
+	# Can't use EncodingWarning as it doesn't exist on Python 3.9.
+	# These warnings only appear on Python 3.10+
+	default:'encoding' argument not specified
+
+	# pypa/distutils#236
+	ignore:'encoding' argument not specified::distutils
+	ignore:'encoding' argument not specified::setuptools._distutils
+
+	# subprocess.check_output still warns with EncodingWarning even with encoding set
+	ignore:'encoding' argument not specified::setuptools.tests.environment
+
 	## upstream
 
 	# Ensure ResourceWarnings are emitted
@@ -18,14 +30,8 @@ filterwarnings=
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy
 
-	# python/cpython#100750
-	ignore:'encoding' argument not specified::platform
-
-	# pypa/build#615
-	ignore:'encoding' argument not specified::build.env
-
-	# dateutil/dateutil#1284
-	ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz
+	# pytest-dev/pytest # TODO: Raise issue upstream 
+	ignore:'encoding' argument not specified::_pytest
 
 	## end upstream
 
@@ -69,11 +75,6 @@ filterwarnings=
 	# https://github.com/pypa/setuptools/issues/3655
 	ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning
 
-	# Workarounds for pypa/setuptools#3810
-	# Can't use EncodingWarning as it doesn't exist on Python 3.9
-	default:'encoding' argument not specified
-	default:UTF-8 Mode affects locale.getpreferredencoding().
-
 	# Avoid errors when testing pkg_resources.declare_namespace
 	ignore:.*pkg_resources\.declare_namespace.*:DeprecationWarning
 
diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
index 1722817f823..b8ed84750a6 100644
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -565,7 +565,8 @@ def _encode_pth(content: str) -> bytes:
     This function tries to simulate this behaviour without having to create an
     actual file, in a way that supports a range of active Python versions.
     (There seems to be some variety in the way different version of Python handle
-    ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``).
+    ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``
+    or ``locale.getencoding()``).
     """
     with io.BytesIO() as buffer:
         wrapper = io.TextIOWrapper(buffer, encoding=py39.LOCALE_ENCODING)
diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py
index 564adf2b0ae..738ebf43bef 100644
--- a/setuptools/tests/__init__.py
+++ b/setuptools/tests/__init__.py
@@ -1,10 +1,15 @@
 import locale
+import sys
 
 import pytest
 
 
 __all__ = ['fail_on_ascii']
 
-
-is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968'
+locale_encoding = (
+    locale.getencoding()
+    if sys.version_info >= (3, 11)
+    else locale.getpreferredencoding(False)
+)
+is_ascii = locale_encoding == 'ANSI_X3.4-1968'
 fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")

From 9acea31d0e8c3f94db7dca3fedaa256d0f8a2250 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Sun, 21 Apr 2024 16:18:38 -0400
Subject: [PATCH 203/232] Remove distutils EncodingWarning exclusion in
 pytest.ini Vendored distutils was updated with fixes

---
 pytest.ini | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/pytest.ini b/pytest.ini
index 40a64b5cd4c..2ce6e3e1e7c 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -15,10 +15,6 @@ filterwarnings=
 	# These warnings only appear on Python 3.10+
 	default:'encoding' argument not specified
 
-	# pypa/distutils#236
-	ignore:'encoding' argument not specified::distutils
-	ignore:'encoding' argument not specified::setuptools._distutils
-
 	# subprocess.check_output still warns with EncodingWarning even with encoding set
 	ignore:'encoding' argument not specified::setuptools.tests.environment
 

From 27f5e0ae4ba6fae8a30bc7b7aa674f8be2afc22f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 16:34:43 +0100
Subject: [PATCH 204/232] Ignore encoding warnings bu only in in stdlib's
 distutils

---
 pytest.ini | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pytest.ini b/pytest.ini
index 2ce6e3e1e7c..648b145b691 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -29,6 +29,9 @@ filterwarnings=
 	# pytest-dev/pytest # TODO: Raise issue upstream 
 	ignore:'encoding' argument not specified::_pytest
 
+	# Already fixed in pypa/distutils, but present in stdlib
+	ignore:'encoding' argument not specified::distutils
+
 	## end upstream
 
 	# https://github.com/pypa/setuptools/issues/1823

From ef7d2590ec6b4e4a410b7e4f983386ae07f13f64 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 14:36:08 +0100
Subject: [PATCH 205/232] Remove EncodingWarning workarounds for setuptools
 from pytest.ini

---
 pytest.ini | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/pytest.ini b/pytest.ini
index 648b145b691..1b565222e20 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -10,14 +10,6 @@ filterwarnings=
 	# Fail on warnings
 	error
 
-	# Workarounds for pypa/setuptools#3810
-	# Can't use EncodingWarning as it doesn't exist on Python 3.9.
-	# These warnings only appear on Python 3.10+
-	default:'encoding' argument not specified
-
-	# subprocess.check_output still warns with EncodingWarning even with encoding set
-	ignore:'encoding' argument not specified::setuptools.tests.environment
-
 	## upstream
 
 	# Ensure ResourceWarnings are emitted

From 4fc0b15d424ae97663d48a34ee7bd586b3aade69 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 15:56:28 +0100
Subject: [PATCH 206/232] Fix EncodingWarning in test_build_meta

---
 setuptools/tests/test_build_meta.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index 43830feb770..cc996b42556 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -160,7 +160,7 @@ def run():
             # to obtain a distribution object first, and then run the distutils
             # commands later, because these files will be removed in the meantime.
 
-            with open('world.py', 'w') as f:
+            with open('world.py', 'w', encoding="utf-8") as f:
                 f.write('x = 42')
 
             try:

From d7ac06f0d5892183c9ac9ce5501d785348b4bbde Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 21:48:32 +0100
Subject: [PATCH 207/232] Return comment to pytest.ini that got lost in changes

---
 pytest.ini | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pytest.ini b/pytest.ini
index 1b565222e20..2aceea2d580 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -10,6 +10,9 @@ filterwarnings=
 	# Fail on warnings
 	error
 
+	# Workarounds for pypa/setuptools#3810
+	# Can't use EncodingWarning as it doesn't exist on Python 3.9.
+	# These warnings only appear on Python 3.10+
 	## upstream
 
 	# Ensure ResourceWarnings are emitted

From 969f00b16d190567ce27d9788e90ab4806a32443 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 22:54:45 +0100
Subject: [PATCH 208/232] Re-enable warning filter for distutils.text_file
 inside test_excluded_subpackages

---
 setuptools/tests/test_build_py.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py
index 4aa1fe68faa..db2052a5863 100644
--- a/setuptools/tests/test_build_py.py
+++ b/setuptools/tests/test_build_py.py
@@ -1,6 +1,7 @@
 import os
 import stat
 import shutil
+import warnings
 from pathlib import Path
 from unittest.mock import Mock
 
@@ -162,11 +163,23 @@ def test_excluded_subpackages(tmpdir_cwd):
     dist.parse_config_files()
 
     build_py = dist.get_command_obj("build_py")
+
     msg = r"Python recognizes 'mypkg\.tests' as an importable package"
     with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
         # TODO: To fix #3260 we need some transition period to deprecate the
         # existing behavior of `include_package_data`. After the transition, we
         # should remove the warning and fix the behaviour.
+
+        if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib":
+            # pytest.warns reset the warning filter temporarily
+            # https://github.com/pytest-dev/pytest/issues/4011#issuecomment-423494810
+            warnings.filterwarnings(
+                "ignore",
+                "'encoding' argument not specified",
+                module="distutils.text_file",
+                # This warning is already fixed in pypa/distutils but not in stdlib
+            )
+
         build_py.finalize_options()
         build_py.run()
 

From 919e3934c9e7a8085bd4ee72d3259be76fe0186b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 23 Apr 2024 00:02:06 +0100
Subject: [PATCH 209/232] Attempt to fix errors in mypy for PyPy (test of
 hypothesis)

---
 pytest.ini | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/pytest.ini b/pytest.ini
index 2aceea2d580..4bebae0fd94 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -18,6 +18,12 @@ filterwarnings=
 	# Ensure ResourceWarnings are emitted
 	default::ResourceWarning
 
+	# python/mypy#17057
+	ignore:'encoding' argument not specified::mypy.config_parser
+	ignore:'encoding' argument not specified::mypy.build
+	ignore:'encoding' argument not specified::mypy.modulefinder
+	ignore:'encoding' argument not specified::mypy.metastore
+
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy
 

From 57ea91b448ca3852c33a2b164b5d6bdc3551a3a6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 16:34:19 +0100
Subject: [PATCH 210/232] Attempt to solve the problem in PyPy

---
 pytest.ini | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pytest.ini b/pytest.ini
index 4bebae0fd94..8ab6d5ebf18 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -19,10 +19,10 @@ filterwarnings=
 	default::ResourceWarning
 
 	# python/mypy#17057
-	ignore:'encoding' argument not specified::mypy.config_parser
-	ignore:'encoding' argument not specified::mypy.build
-	ignore:'encoding' argument not specified::mypy.modulefinder
-	ignore:'encoding' argument not specified::mypy.metastore
+	ignore:'encoding' argument not specified::mypy
+	ignore:'encoding' argument not specified::configparser
+	# ^-- ConfigParser is called by mypy,
+	#     but ignoring the warning in `mypy` is not enough on PyPy
 
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy

From 1316a611f2faf5828f3d10a1bb4df7a2475ebeef Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 16:35:03 +0100
Subject: [PATCH 211/232] Better wording for comment in pytest.ini

---
 pytest.ini | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/pytest.ini b/pytest.ini
index 8ab6d5ebf18..000e6634714 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -22,7 +22,8 @@ filterwarnings=
 	ignore:'encoding' argument not specified::mypy
 	ignore:'encoding' argument not specified::configparser
 	# ^-- ConfigParser is called by mypy,
-	#     but ignoring the warning in `mypy` is not enough on PyPy
+	#     but ignoring the warning in `mypy` is not enough
+	#     to make it work on PyPy
 
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy

From 8dc50ccc2fe55a3c6b2c99f5c95597242c61572d Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Wed, 24 Apr 2024 14:02:04 -0400
Subject: [PATCH 212/232] Add newsfragment

---
 newsfragments/4255.misc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 newsfragments/4255.misc.rst

diff --git a/newsfragments/4255.misc.rst b/newsfragments/4255.misc.rst
new file mode 100644
index 00000000000..1f9fde768b7
--- /dev/null
+++ b/newsfragments/4255.misc.rst
@@ -0,0 +1 @@
+Treat `EncodingWarning`s as an errors in tests. -- by :user:`Avasam`

From b69c0de234a9b648828e5f3a180639f5ea0e24fa Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Wed, 24 Apr 2024 14:05:12 -0400
Subject: [PATCH 213/232] Update comment

---
 pytest.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pytest.ini b/pytest.ini
index 000e6634714..87e3d9aae37 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -28,7 +28,7 @@ filterwarnings=
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy
 
-	# pytest-dev/pytest # TODO: Raise issue upstream 
+	# TODO: Set encoding when openning tmpdir files with pytest's LocalPath.open
 	ignore:'encoding' argument not specified::_pytest
 
 	# Already fixed in pypa/distutils, but present in stdlib

From b341011f3b8be7e10d933831750aed48b381f382 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Wed, 24 Apr 2024 14:38:00 -0400
Subject: [PATCH 214/232] Fix Windows issue

---
 setuptools/tests/test_windows_wrappers.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/test_windows_wrappers.py b/setuptools/tests/test_windows_wrappers.py
index 3f321386f10..b2726893514 100644
--- a/setuptools/tests/test_windows_wrappers.py
+++ b/setuptools/tests/test_windows_wrappers.py
@@ -110,7 +110,11 @@ def test_basic(self, tmpdir):
             'arg5 a\\\\b',
         ]
         proc = subprocess.Popen(
-            cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, text=True
+            cmd,
+            stdout=subprocess.PIPE,
+            stdin=subprocess.PIPE,
+            text=True,
+            encoding="utf-8",
         )
         stdout, stderr = proc.communicate('hello\nworld\n')
         actual = stdout.replace('\r\n', '\n')
@@ -143,7 +147,11 @@ def test_symlink(self, tmpdir):
             'arg5 a\\\\b',
         ]
         proc = subprocess.Popen(
-            cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, text=True
+            cmd,
+            stdout=subprocess.PIPE,
+            stdin=subprocess.PIPE,
+            text=True,
+            encoding="utf-8",
         )
         stdout, stderr = proc.communicate('hello\nworld\n')
         actual = stdout.replace('\r\n', '\n')
@@ -191,6 +199,7 @@ def test_with_options(self, tmpdir):
             stdin=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             text=True,
+            encoding="utf-8",
         )
         stdout, stderr = proc.communicate()
         actual = stdout.replace('\r\n', '\n')
@@ -240,6 +249,7 @@ def test_basic(self, tmpdir):
             stdin=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             text=True,
+            encoding="utf-8",
         )
         stdout, stderr = proc.communicate()
         assert not stdout

From 22ca7e5ba90cce639e241428226279b0c7be2242 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 12:29:09 +0100
Subject: [PATCH 215/232] Update comment in pytest.ini

---
 pytest.ini | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/pytest.ini b/pytest.ini
index 87e3d9aae37..0c9651d96f9 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -13,6 +13,7 @@ filterwarnings=
 	# Workarounds for pypa/setuptools#3810
 	# Can't use EncodingWarning as it doesn't exist on Python 3.9.
 	# These warnings only appear on Python 3.10+
+
 	## upstream
 
 	# Ensure ResourceWarnings are emitted
@@ -28,7 +29,8 @@ filterwarnings=
 	# realpython/pytest-mypy#152
 	ignore:'encoding' argument not specified::pytest_mypy
 
-	# TODO: Set encoding when openning tmpdir files with pytest's LocalPath.open
+	# TODO: Set encoding when openning/writing tmpdir files with pytest's LocalPath.open
+	# see pypa/setuptools#4326
 	ignore:'encoding' argument not specified::_pytest
 
 	# Already fixed in pypa/distutils, but present in stdlib

From 3ea4aa933ba140cb1c19ce44dfef4564563a79ac Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 25 Apr 2024 14:40:49 +0100
Subject: [PATCH 216/232] Improve RST syntax on news fragment.

---
 newsfragments/4255.misc.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/newsfragments/4255.misc.rst b/newsfragments/4255.misc.rst
index 1f9fde768b7..e5e5728d702 100644
--- a/newsfragments/4255.misc.rst
+++ b/newsfragments/4255.misc.rst
@@ -1 +1 @@
-Treat `EncodingWarning`s as an errors in tests. -- by :user:`Avasam`
+Treat ``EncodingWarning``s as an errors in tests. -- by :user:`Avasam`

From 0faba5086564eab2feb2fa94f2e4592c50589952 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Thu, 25 Apr 2024 10:05:44 -0400
Subject: [PATCH 217/232] Fix typo

---
 newsfragments/4255.misc.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/newsfragments/4255.misc.rst b/newsfragments/4255.misc.rst
index e5e5728d702..50a0a3d195c 100644
--- a/newsfragments/4255.misc.rst
+++ b/newsfragments/4255.misc.rst
@@ -1 +1 @@
-Treat ``EncodingWarning``s as an errors in tests. -- by :user:`Avasam`
+Treat ``EncodingWarning``s as errors in tests. -- by :user:`Avasam`

From 7b17049aabb7b493ad106fcefe00856607b6f181 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 15:24:49 -0400
Subject: [PATCH 218/232] Pin against pyproject-hooks==1.1. Closes #4333.

---
 .github/workflows/main.yml | 3 ++-
 setup.cfg                  | 3 +++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c5aed1d1a07..ec2e567a1ef 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -95,7 +95,8 @@ jobs:
         shell: bash
         run: |
           rm -rf dist
-          pipx run build
+          # workaround for pypa/setuptools#4333
+          pipx run --pip-args 'pyproject-hooks!=1.1' build
           echo "PRE_BUILT_SETUPTOOLS_SDIST=$(ls dist/*.tar.gz)" >> $GITHUB_ENV
           echo "PRE_BUILT_SETUPTOOLS_WHEEL=$(ls dist/*.whl)" >> $GITHUB_ENV
           rm -rf setuptools.egg-info  # Avoid interfering with the other tests
diff --git a/setup.cfg b/setup.cfg
index c8bb0ed41da..68be6c8e7cb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -77,6 +77,9 @@ testing =
 	# No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly
 	importlib_metadata
 
+	# workaround for pypa/setuptools#4333
+	pyproject-hooks!=1.1
+
 docs =
 	# upstream
 	sphinx >= 3.5

From 4a0a9ce587515edce83ab97aa5c7943c045ac180 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 16:13:58 -0400
Subject: [PATCH 219/232] Make the test less fragile and search simply for the
 presence of a ValueError in the traceback. Closes #4334.

---
 setuptools/tests/test_egg_info.py | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index a4b0ecf3989..f6b2302d975 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -213,13 +213,9 @@ def test_license_is_a_string(self, tmpdir_cwd, env):
         with pytest.raises(AssertionError) as exc:
             self._run_egg_info_command(tmpdir_cwd, env)
 
-        # Hopefully this is not too fragile: the only argument to the
-        # assertion error should be a traceback, ending with:
-        #     ValueError: ....
-        #
-        #     assert not 1
-        tb = exc.value.args[0].split('\n')
-        assert tb[-3].lstrip().startswith('ValueError')
+        # The only argument to the assertion error should be a traceback
+        # containing a ValueError
+        assert 'ValueError' in exc.value.args[0]
 
     def test_rebuilt(self, tmpdir_cwd, env):
         """Ensure timestamps are updated when the command is re-run."""

From fe8980b4505cea1982979fdca20c4078ed8fb8c6 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 09:38:31 -0400
Subject: [PATCH 220/232] Remove pop_prefix parameter, unused.

---
 setuptools/package_index.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index f5a7d77eed8..345344c2c22 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -863,7 +863,7 @@ def _download_svn(self, url, _filename):
         raise DistutilsError(f"Invalid config, SVN download is not supported: {url}")
 
     @staticmethod
-    def _vcs_split_rev_from_url(url, pop_prefix=False):
+    def _vcs_split_rev_from_url(url):
         scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
 
         scheme = scheme.split('+', 1)[-1]
@@ -882,7 +882,7 @@ def _vcs_split_rev_from_url(url, pop_prefix=False):
 
     def _download_git(self, url, filename):
         filename = filename.split('#', 1)[0]
-        url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)
+        url, rev = self._vcs_split_rev_from_url(url)
 
         self.info("Doing git clone from %s to %s", url, filename)
         os.system("git clone --quiet %s %s" % (url, filename))
@@ -901,7 +901,7 @@ def _download_git(self, url, filename):
 
     def _download_hg(self, url, filename):
         filename = filename.split('#', 1)[0]
-        url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)
+        url, rev = self._vcs_split_rev_from_url(url)
 
         self.info("Doing hg clone from %s to %s", url, filename)
         os.system("hg clone --quiet %s %s" % (url, filename))

From 35ee2b4abd8bc745766c809d31fc6bf19e6979dc Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 09:49:56 -0400
Subject: [PATCH 221/232] Add a test capturing the basic expectation.

---
 setuptools/package_index.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 345344c2c22..d2985cc1f95 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -864,6 +864,15 @@ def _download_svn(self, url, _filename):
 
     @staticmethod
     def _vcs_split_rev_from_url(url):
+        """
+        >>> vsrfu = PackageIndex._vcs_split_rev_from_url
+        >>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools')
+        ('https://github.com/pypa/setuptools', 'v69.0.0')
+        >>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools')
+        ('https://github.com/pypa/setuptools', None)
+        >>> vsrfu('http://foo/bar')
+        ('http://foo/bar', None)
+        """
         scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
 
         scheme = scheme.split('+', 1)[-1]

From eb42e5c45b5888863aa1877517f6dbf6f7b080cc Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 10:15:31 -0400
Subject: [PATCH 222/232] Update _vcs_split_rev_from_url to use modern
 constructs.

---
 setuptools/package_index.py | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index d2985cc1f95..9da138d87cf 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -865,6 +865,8 @@ def _download_svn(self, url, _filename):
     @staticmethod
     def _vcs_split_rev_from_url(url):
         """
+        Given a possible VCS URL, return a clean URL and resolved revision if any.
+
         >>> vsrfu = PackageIndex._vcs_split_rev_from_url
         >>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools')
         ('https://github.com/pypa/setuptools', 'v69.0.0')
@@ -873,21 +875,24 @@ def _vcs_split_rev_from_url(url):
         >>> vsrfu('http://foo/bar')
         ('http://foo/bar', None)
         """
-        scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
+        parts = urllib.parse.urlsplit(url)
 
-        scheme = scheme.split('+', 1)[-1]
+        clean_scheme = parts.scheme.split('+', 1)[-1]
 
         # Some fragment identification fails
-        path = path.split('#', 1)[0]
+        no_fragment_path, _, _ = parts.path.partition('#')
 
-        rev = None
-        if '@' in path:
-            path, rev = path.rsplit('@', 1)
+        pre, sep, post = no_fragment_path.rpartition('@')
+        clean_path, rev = (pre, post) if sep else (post, None)
 
-        # Also, discard fragment
-        url = urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
+        resolved = parts._replace(
+            scheme=clean_scheme,
+            path=clean_path,
+            # discard the fragment
+            fragment='',
+        ).geturl()
 
-        return url, rev
+        return resolved, rev
 
     def _download_git(self, url, filename):
         filename = filename.split('#', 1)[0]

From 7c1c29b56dcff03bb637eeabba139c31600a55d1 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 10:25:24 -0400
Subject: [PATCH 223/232] package-index: Extract fall-through methods
 _download_vcs and _download_other.

---
 setuptools/package_index.py | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 9da138d87cf..5a3e9db2a25 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -831,19 +831,24 @@ def _download_url(self, scheme, url, tmpdir):
 
         filename = os.path.join(tmpdir, name)
 
-        # Download the file
-        #
+        return self._download_vcs(url, filename) or self._download_other(url, filename)
+
+    def _download_vcs(self, url, filename):
+        scheme = urllib.parse.urlsplit(url).scheme
         if scheme == 'svn' or scheme.startswith('svn+'):
             return self._download_svn(url, filename)
         elif scheme == 'git' or scheme.startswith('git+'):
             return self._download_git(url, filename)
         elif scheme.startswith('hg+'):
             return self._download_hg(url, filename)
-        elif scheme == 'file':
-            return urllib.request.url2pathname(urllib.parse.urlparse(url)[2])
-        else:
-            self.url_ok(url, True)  # raises error if not allowed
-            return self._attempt_download(url, filename)
+
+    def _download_other(self, url, filename):
+        scheme = urllib.parse.urlsplit(url).scheme
+        if scheme == 'file':
+            return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
+        # raise error if not allowed
+        self.url_ok(url, True)
+        return self._attempt_download(url, filename)
 
     def scan_url(self, url):
         self.process_url(url, True)

From 4d54fa77943c78f393217b2931665d3ab64cd3f2 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 10:46:34 -0400
Subject: [PATCH 224/232] Extract _resolve_vcs for resolving a VCS from a URL.

---
 setuptools/package_index.py | 34 +++++++++++++++++++++++++---------
 1 file changed, 25 insertions(+), 9 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 5a3e9db2a25..8d46a70c471 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -1,5 +1,6 @@
 """PyPI and direct package downloading."""
 
+import contextlib
 import sys
 import os
 import re
@@ -587,7 +588,7 @@ def download(self, spec, tmpdir):
             scheme = URL_SCHEME(spec)
             if scheme:
                 # It's a url, download it to tmpdir
-                found = self._download_url(scheme.group(1), spec, tmpdir)
+                found = self._download_url(spec, tmpdir)
                 base, fragment = egg_info_for_url(spec)
                 if base.endswith('.py'):
                     found = self.gen_setup(found, fragment, tmpdir)
@@ -816,7 +817,7 @@ def open_url(self, url, warning=None):  # noqa: C901  # is too complex (12)
             else:
                 raise DistutilsError("Download error for %s: %s" % (url, v)) from v
 
-    def _download_url(self, scheme, url, tmpdir):
+    def _download_url(self, url, tmpdir):
         # Determine download filename
         #
         name, fragment = egg_info_for_url(url)
@@ -833,14 +834,29 @@ def _download_url(self, scheme, url, tmpdir):
 
         return self._download_vcs(url, filename) or self._download_other(url, filename)
 
-    def _download_vcs(self, url, filename):
+    @staticmethod
+    def _resolve_vcs(url):
+        """
+        >>> rvcs = PackageIndex._resolve_vcs
+        >>> rvcs('git+http://foo/bar')
+        'git'
+        >>> rvcs('hg+https://foo/bar')
+        'hg'
+        >>> rvcs('git:myhost')
+        'git'
+        >>> rvcs('hg:myhost')
+        >>> rvcs('http://foo/bar')
+        """
         scheme = urllib.parse.urlsplit(url).scheme
-        if scheme == 'svn' or scheme.startswith('svn+'):
-            return self._download_svn(url, filename)
-        elif scheme == 'git' or scheme.startswith('git+'):
-            return self._download_git(url, filename)
-        elif scheme.startswith('hg+'):
-            return self._download_hg(url, filename)
+        pre, sep, post = scheme.partition('+')
+        # svn and git have their own protocol; hg does not
+        allowed = set(['svn', 'git'] + ['hg'] * bool(sep))
+        return next(iter({pre} & allowed), None)
+
+    def _download_vcs(self, url, filename):
+        vcs = self._resolve_vcs(url)
+        with contextlib.suppress(AttributeError):
+            return getattr(self, f'_download_{vcs}')(url, filename)
 
     def _download_other(self, url, filename):
         scheme = urllib.parse.urlsplit(url).scheme

From cf18f716c1fe638d812d487f61aa987101a763a8 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 10:55:48 -0400
Subject: [PATCH 225/232] Consolidated all _download_vcs methods into one.

---
 setuptools/package_index.py | 68 +++++++++++++------------------------
 1 file changed, 23 insertions(+), 45 deletions(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 8d46a70c471..ba7819304fa 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -1,6 +1,5 @@
 """PyPI and direct package downloading."""
 
-import contextlib
 import sys
 import os
 import re
@@ -853,10 +852,30 @@ def _resolve_vcs(url):
         allowed = set(['svn', 'git'] + ['hg'] * bool(sep))
         return next(iter({pre} & allowed), None)
 
-    def _download_vcs(self, url, filename):
+    def _download_vcs(self, url, spec_filename):
         vcs = self._resolve_vcs(url)
-        with contextlib.suppress(AttributeError):
-            return getattr(self, f'_download_{vcs}')(url, filename)
+        if not vcs:
+            return
+        if vcs == 'svn':
+            raise DistutilsError(
+                f"Invalid config, SVN download is not supported: {url}"
+            )
+
+        filename, _, _ = spec_filename.partition('#')
+        url, rev = self._vcs_split_rev_from_url(url)
+
+        self.info(f"Doing {vcs} clone from {url} to {filename}")
+        os.system(f"{vcs} clone --quiet {url} {filename}")
+
+        co_commands = dict(
+            git=f"git -C {filename} checkout --quiet {rev}",
+            hg=f"hg --cwd {filename} up -C -r {rev} -q",
+        )
+        if rev is not None:
+            self.info(f"Checking out {rev}")
+            os.system(co_commands[vcs])
+
+        return filename
 
     def _download_other(self, url, filename):
         scheme = urllib.parse.urlsplit(url).scheme
@@ -880,9 +899,6 @@ def _invalid_download_html(self, url, headers, filename):
         os.unlink(filename)
         raise DistutilsError(f"Unexpected HTML page found at {url}")
 
-    def _download_svn(self, url, _filename):
-        raise DistutilsError(f"Invalid config, SVN download is not supported: {url}")
-
     @staticmethod
     def _vcs_split_rev_from_url(url):
         """
@@ -915,44 +931,6 @@ def _vcs_split_rev_from_url(url):
 
         return resolved, rev
 
-    def _download_git(self, url, filename):
-        filename = filename.split('#', 1)[0]
-        url, rev = self._vcs_split_rev_from_url(url)
-
-        self.info("Doing git clone from %s to %s", url, filename)
-        os.system("git clone --quiet %s %s" % (url, filename))
-
-        if rev is not None:
-            self.info("Checking out %s", rev)
-            os.system(
-                "git -C %s checkout --quiet %s"
-                % (
-                    filename,
-                    rev,
-                )
-            )
-
-        return filename
-
-    def _download_hg(self, url, filename):
-        filename = filename.split('#', 1)[0]
-        url, rev = self._vcs_split_rev_from_url(url)
-
-        self.info("Doing hg clone from %s to %s", url, filename)
-        os.system("hg clone --quiet %s %s" % (url, filename))
-
-        if rev is not None:
-            self.info("Updating to %s", rev)
-            os.system(
-                "hg --cwd %s up -C -r %s -q"
-                % (
-                    filename,
-                    rev,
-                )
-            )
-
-        return filename
-
     def debug(self, msg, *args):
         log.debug(msg, *args)
 

From f0cda0b9a3cf9d81844738ba96b1df95e2abb799 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 11:40:10 -0400
Subject: [PATCH 226/232] Replace os.system calls with subprocess calls.

---
 setup.cfg                             |  1 +
 setuptools/package_index.py           |  9 +++---
 setuptools/tests/test_packageindex.py | 46 ++++++++++++---------------
 3 files changed, 27 insertions(+), 29 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 68be6c8e7cb..1226c940fcb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -76,6 +76,7 @@ testing =
 	tomli
 	# No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly
 	importlib_metadata
+	pytest-subprocess
 
 	# workaround for pypa/setuptools#4333
 	pyproject-hooks!=1.1
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index ba7819304fa..bbc5846ed9e 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -1,6 +1,7 @@
 """PyPI and direct package downloading."""
 
 import sys
+import subprocess
 import os
 import re
 import io
@@ -865,15 +866,15 @@ def _download_vcs(self, url, spec_filename):
         url, rev = self._vcs_split_rev_from_url(url)
 
         self.info(f"Doing {vcs} clone from {url} to {filename}")
-        os.system(f"{vcs} clone --quiet {url} {filename}")
+        subprocess.check_call([vcs, 'clone', '--quiet', url, filename])
 
         co_commands = dict(
-            git=f"git -C {filename} checkout --quiet {rev}",
-            hg=f"hg --cwd {filename} up -C -r {rev} -q",
+            git=[vcs, '-C', filename, 'checkout', '--quiet', rev],
+            hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'],
         )
         if rev is not None:
             self.info(f"Checking out {rev}")
-            os.system(co_commands[vcs])
+            subprocess.check_call(co_commands[vcs])
 
         return filename
 
diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py
index 93474ae5af3..2776ba9f63a 100644
--- a/setuptools/tests/test_packageindex.py
+++ b/setuptools/tests/test_packageindex.py
@@ -3,7 +3,6 @@
 import urllib.error
 import http.client
 from inspect import cleandoc
-from unittest import mock
 
 import pytest
 
@@ -171,41 +170,38 @@ def test_egg_fragment(self):
             assert dists[0].version == ''
             assert dists[1].version == vc
 
-    def test_download_git_with_rev(self, tmpdir):
+    def test_download_git_with_rev(self, tmpdir, fp):
         url = 'git+https://github.example/group/project@master#egg=foo'
         index = setuptools.package_index.PackageIndex()
 
-        with mock.patch("os.system") as os_system_mock:
-            result = index.download(url, str(tmpdir))
+        expected_dir = str(tmpdir / 'project@master')
+        fp.register([
+            'git',
+            'clone',
+            '--quiet',
+            'https://github.example/group/project',
+            expected_dir,
+        ])
+        fp.register(['git', '-C', expected_dir, 'checkout', '--quiet', 'master'])
 
-        os_system_mock.assert_called()
+        result = index.download(url, str(tmpdir))
 
-        expected_dir = str(tmpdir / 'project@master')
-        expected = (
-            'git clone --quiet ' 'https://github.example/group/project {expected_dir}'
-        ).format(**locals())
-        first_call_args = os_system_mock.call_args_list[0][0]
-        assert first_call_args == (expected,)
-
-        tmpl = 'git -C {expected_dir} checkout --quiet master'
-        expected = tmpl.format(**locals())
-        assert os_system_mock.call_args_list[1][0] == (expected,)
         assert result == expected_dir
+        assert len(fp.calls) == 2
 
-    def test_download_git_no_rev(self, tmpdir):
+    def test_download_git_no_rev(self, tmpdir, fp):
         url = 'git+https://github.example/group/project#egg=foo'
         index = setuptools.package_index.PackageIndex()
 
-        with mock.patch("os.system") as os_system_mock:
-            result = index.download(url, str(tmpdir))
-
-        os_system_mock.assert_called()
-
         expected_dir = str(tmpdir / 'project')
-        expected = (
-            'git clone --quiet ' 'https://github.example/group/project {expected_dir}'
-        ).format(**locals())
-        os_system_mock.assert_called_once_with(expected)
+        fp.register([
+            'git',
+            'clone',
+            '--quiet',
+            'https://github.example/group/project',
+            expected_dir,
+        ])
+        index.download(url, str(tmpdir))
 
     def test_download_svn(self, tmpdir):
         url = 'svn+https://svn.example/project#egg=foo'

From a36b1121d817ef82aef0971aaa37989941019c8f Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 11:55:20 -0400
Subject: [PATCH 227/232] Prefer tmp_path fixture.

---
 setuptools/tests/test_packageindex.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py
index 2776ba9f63a..f5f37e0563a 100644
--- a/setuptools/tests/test_packageindex.py
+++ b/setuptools/tests/test_packageindex.py
@@ -170,11 +170,11 @@ def test_egg_fragment(self):
             assert dists[0].version == ''
             assert dists[1].version == vc
 
-    def test_download_git_with_rev(self, tmpdir, fp):
+    def test_download_git_with_rev(self, tmp_path, fp):
         url = 'git+https://github.example/group/project@master#egg=foo'
         index = setuptools.package_index.PackageIndex()
 
-        expected_dir = str(tmpdir / 'project@master')
+        expected_dir = tmp_path / 'project@master'
         fp.register([
             'git',
             'clone',
@@ -184,16 +184,16 @@ def test_download_git_with_rev(self, tmpdir, fp):
         ])
         fp.register(['git', '-C', expected_dir, 'checkout', '--quiet', 'master'])
 
-        result = index.download(url, str(tmpdir))
+        result = index.download(url, tmp_path)
 
-        assert result == expected_dir
+        assert result == str(expected_dir)
         assert len(fp.calls) == 2
 
-    def test_download_git_no_rev(self, tmpdir, fp):
+    def test_download_git_no_rev(self, tmp_path, fp):
         url = 'git+https://github.example/group/project#egg=foo'
         index = setuptools.package_index.PackageIndex()
 
-        expected_dir = str(tmpdir / 'project')
+        expected_dir = tmp_path / 'project'
         fp.register([
             'git',
             'clone',
@@ -201,15 +201,15 @@ def test_download_git_no_rev(self, tmpdir, fp):
             'https://github.example/group/project',
             expected_dir,
         ])
-        index.download(url, str(tmpdir))
+        index.download(url, tmp_path)
 
-    def test_download_svn(self, tmpdir):
+    def test_download_svn(self, tmp_path):
         url = 'svn+https://svn.example/project#egg=foo'
         index = setuptools.package_index.PackageIndex()
 
         msg = r".*SVN download is not supported.*"
         with pytest.raises(distutils.errors.DistutilsError, match=msg):
-            index.download(url, str(tmpdir))
+            index.download(url, tmp_path)
 
 
 class TestContentCheckers:

From 1a0cbf5f59b5fa1debb4b46fd5e1c41ebc2344dd Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 17:07:40 -0400
Subject: [PATCH 228/232] Add news fragment.

---
 newsfragments/4332.feature.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 newsfragments/4332.feature.rst

diff --git a/newsfragments/4332.feature.rst b/newsfragments/4332.feature.rst
new file mode 100644
index 00000000000..9f46298adc4
--- /dev/null
+++ b/newsfragments/4332.feature.rst
@@ -0,0 +1 @@
+Modernized and refactored VCS handling in package_index.
\ No newline at end of file

From 9bc2e87fc74e726927d208438385956591c96aa5 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 17:29:45 -0400
Subject: [PATCH 229/232] Ignore coverage for file urls.

---
 setuptools/package_index.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index bbc5846ed9e..c3ffee41a72 100644
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -880,7 +880,7 @@ def _download_vcs(self, url, spec_filename):
 
     def _download_other(self, url, filename):
         scheme = urllib.parse.urlsplit(url).scheme
-        if scheme == 'file':
+        if scheme == 'file':  # pragma: no cover
             return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
         # raise error if not allowed
         self.url_ok(url, True)

From 225d15808ba140799673b730e90b25e4117cc365 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 29 Apr 2024 20:19:04 -0400
Subject: [PATCH 230/232] Pin against pyproject-hooks==1.1 (docs). Closes
 #4333.

---
 setup.cfg | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/setup.cfg b/setup.cfg
index 1226c940fcb..0756fa92eaf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -100,6 +100,9 @@ docs =
 	sphinxcontrib-towncrier
 	sphinx-notfound-page >=1,<2
 
+	# workaround for pypa/setuptools#4333
+	pyproject-hooks!=1.1
+
 ssl =
 
 certs =

From a84b262a8d50ae8a3ed74bca58e6cadd1ac46495 Mon Sep 17 00:00:00 2001
From: wim glenn 
Date: Tue, 30 Apr 2024 16:09:19 -0500
Subject: [PATCH 231/232] Typo fix in build_meta.py docstring

---
 setuptools/build_meta.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
index 2decd2d2140..be2742d73db 100644
--- a/setuptools/build_meta.py
+++ b/setuptools/build_meta.py
@@ -2,7 +2,7 @@
 
 Previously, when a user or a command line tool (let's call it a "frontend")
 needed to make a request of setuptools to take a certain action, for
-example, generating a list of installation requirements, the frontend would
+example, generating a list of installation requirements, the frontend
 would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
 
 PEP 517 defines a different method of interfacing with setuptools. Rather

From 9cf334d45e32d767d394fa6cc9ffa8829b150af0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 1 May 2024 17:27:36 +0100
Subject: [PATCH 232/232] Avoid newer importlib-metadata APIs for backwards
 compatibility

---
 setuptools/dist.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index 076f9a2327a..03f6c0398ba 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -535,7 +535,8 @@ def warn_dash_deprecation(self, opt, section):
 
     def _setuptools_commands(self):
         try:
-            return metadata.distribution('setuptools').entry_points.names
+            entry_points = metadata.distribution('setuptools').entry_points
+            return {ep.name for ep in entry_points}  # Avoid newer API for compatibility
         except metadata.PackageNotFoundError:
             # during bootstrapping, distribution doesn't exist
             return []