From e649e936e79fb5cbbf45f63475934faa3cb0f4bc Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Tue, 28 Feb 2023 08:55:11 +0300 Subject: [PATCH 001/184] 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 1818fce901..ae60578ac2 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/184] 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 60801acecd..dbba53e2b7 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/184] 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 06657e4eaa..fd858d182e 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/184] 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 070a277069..22a003d8ca 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/184] 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 30a6f9ff2e..694bf02a60 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/184] 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 7ef47176e2..4f94e587e2 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/184] 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 a018442459..23b4eb5a4c 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 6ca2332ae1..d5c245969d 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/184] =?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 3ed608b479..696f26751f 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 f9f9f2d844..724986d89d 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 4a702fb913..3fd2c7e2ac 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 1a5753c772..28cc5632a3 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 fdb768e73f..a3fa290275 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 bd8db9ac3f..294a16b7f4 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 83b8ef5d52..beb17bc3fc 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 28582bab36..d758a8105e 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 3c8e1ab1b3..71d2439f7e 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/184] 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 27e73393a0..fdec5a9650 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 b5a392a85f..98ab0b171f 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 cb61ad7455..3c83cca4d2 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 e72a7c5ff8..ecb8510246 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 3f525db42a..082ee1d349 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/184] 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 574ffc28e6..68c38ac901 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/184] 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 fdec5a9650..85293cbb5b 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/184] 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 7b9cc6927b..213558aac4 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/184] 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 ff2aade085..68c38ac901 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/184] 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 fe99eaf6e5..400a72a5ed 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/184] 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 a40a7231b3..c89fff4be1 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/184] 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 c89fff4be1..fac3259f88 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/184] =?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 b01b313085..ca808a6ab7 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 3c2ba15410..14d51472f2 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 c1c7d5476e..6935e2c37f 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 cffa27cb08..b4228299f4 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 6329039ce4..237b14656f 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 696f26751f..e96db22bed 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 d9df95922f..e16011d46a 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 575e49fb4b..b59cc23731 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 a7ac4e6077..927c3ed3a2 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 c19aabb91f..cf1afc8c1f 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 9a4044adaf..a55951ed7c 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 47efa377c5..b3dbc3be15 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 6b8575de29..8f186b72ff 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 85293cbb5b..aad8edb242 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 fd4b11bf75..2080604982 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 89c415d761..2b5eafd27e 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 af330a06e7..1804807752 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 6fb50c4b8e..95532e83b9 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 3fd2c7e2ac..e6804088da 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 66d8af50ac..c2cff44523 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 98ab0b171f..f855454256 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 3c83cca4d2..537959fed6 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 3bef9d79ec..77c9ad7573 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 6d240b8b2b..8215300b97 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 157b60a1e9..e2459aa0c1 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 cc740d1a8b..684662d32e 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 1ae615db95..11c23d837e 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 ecb8510246..2519ed6a10 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 6fb449a6c2..fc67d75f82 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 72aca4ee55..0738b7c877 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 694bf02a60..fe979efed5 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 f86af07376..297ae44bfe 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 9f44f91dfa..3b9f82b71e 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 2cee42cddd..bfffbb1da0 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 9badbc264f..198c10da8d 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 1e8ccf7991..8b86b6eaed 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 0bd67cd04d..0efe39fe86 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 ca07c7e853..5fde7a5971 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 fe5693e1d8..dfb34122bc 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 f63537b8e5..f65a5a25a3 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 34e593244e..5d3826a1b7 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 a3fa290275..00718a37bd 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 08a34ee2b8..57cf1a525c 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 bfeaf9a6b9..6cbf51681b 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 7c8dc5be54..4a721b691c 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 a018442459..c1e57a016f 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 af113b8b6e..5c5bc59a40 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 22a003d8ca..c632b3910f 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 ff52ea4683..900edafa7c 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 74c40d7bfd..18385cfef2 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 d6c0c007aa..c75e49486f 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/184] 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 6935e2c37f..d5ca761f5a 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 b48f462626..98938babd0 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 0000000000..6d70b7f04f --- /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/184] 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 98938babd0..ba6580c71e 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/184] =?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 e8c34ce63e..62efce436f 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 b676a6a8af..d749fe2529 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/184] 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 62efce436f..a313da3e75 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/184] 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 0000000000..85b2951186 --- /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 d5ca761f5a..28d2da5c58 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/184] 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 45c66794f0..473c2e0fcc 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/184] 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 331eeed93f..4c39a5b139 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 97aae46711b6a7e0045dec3a17ebec6ec60062ee Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 21 Feb 2024 16:03:38 +0000 Subject: [PATCH 025/184] Further clarify include_package_data The fact that the `Subdirectory for Data Files` section in `userguide/datafiles` repeat examples for `include_package_data` without mentioning `MANIFEST.in` may be confusing for users that skip previous parts of the documen]t. --- docs/userguide/datafiles.rst | 100 +++++++++++++++++++------------ docs/userguide/miscellaneous.rst | 18 ++++++ 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 2e37289d5f..4eca7e4303 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -10,12 +10,19 @@ by including the data files **inside the package directory**. Setuptools focuses on this most common type of data files and offers three ways of specifying which files should be included in your packages, as described in -the following sections. +the following section. + + +Configuration Options +===================== + + +.. _include-package-data: include_package_data -==================== +-------------------- -First, you can simply use the ``include_package_data`` keyword. +First, you can use the ``include_package_data`` keyword. For example, if the package tree looks like this:: project_root_directory @@ -92,8 +99,10 @@ your package, provided: (where ``include_package_data=False`` by default), which was not changed to ensure backwards compatibility with existing projects. +.. _package-data: + package_data -============ +------------ By default, ``include_package_data`` considers **all** non ``.py`` files found inside the package directory (``src/mypkg`` in this case) as data files, and includes those that @@ -260,8 +269,10 @@ we specify that ``data1.rst`` from ``mypkg1`` alone should be captured as well. Please check :ref:`section subdirectories ` below. +.. _exclude-package-data: + exclude_package_data -==================== +-------------------- Sometimes, the ``include_package_data`` or ``package_data`` options alone aren't sufficient to precisely define what files you want included. For example, @@ -327,6 +338,38 @@ even if they were listed in ``package_data`` or were included as a result of usi ``include_package_data``. +Summary +------- + +In summary, the three options allow you to: + +``include_package_data`` + Accept all data files and directories matched by + :ref:`MANIFEST.in ` or added by + a :ref:`plugin `. + +``package_data`` + Specify additional patterns to match files that may or may + not be matched by :ref:`MANIFEST.in ` + or added by a :ref:`plugin `. + +``exclude_package_data`` + Specify patterns for data files and directories that should *not* be + included when a package is installed, even if they would otherwise have + been included due to the use of the preceding options. + +.. note:: + Due to the way the build process works, a data file that you + include in your project and then stop including may be "orphaned" in your + project's build directories, requiring you to manually deleting them. + This may also be important for your users and contributors + if they track intermediate revisions of your project using Subversion; be sure + to let them know when you make changes that remove files from inclusion so they + can also manually delete them. + + See also troubleshooting information in :ref:`Caching and Troubleshooting`. + + .. _subdir-data-files: Subdirectory for Data Files @@ -350,8 +393,13 @@ Here, the ``.rst`` files are placed under a ``data`` subdirectory inside ``mypkg while the ``.txt`` files are directly under ``mypkg``. In this case, the recommended approach is to treat ``data`` as a namespace package -(refer :pep:`420`). With ``package_data``, -the configuration might look like this: +(refer :pep:`420`). This way, you can rely on the same methods described above, +using either :ref:`package-data` or :ref:`include-package-data`. +For the sake of completeness, we include below configuration examples +for the subdirectory structure, but please refer to the detailed +information in the previous sections of this document. + +With :ref:`package-data`, the configuration might look like this: .. tab:: pyproject.toml @@ -407,8 +455,9 @@ which enables the ``data`` directory to be identified, and then, we separately s files for the root package ``mypkg``, and the namespace package ``data`` under the package ``mypkg``. -With ``include_package_data`` the configuration is simpler: you simply need to enable -scanning of namespace packages in the ``src`` directory and the rest is handled by Setuptools. +Alternatively, you can also rely on :ref:`include-package-data`. +Note that this is the default behaviour in ``pyproject.toml``, but you need to +manually enable scanning of namespace packages in ``setup.cfg`` or ``setup.py``: .. tab:: pyproject.toml @@ -422,7 +471,7 @@ scanning of namespace packages in the ``src`` directory and the rest is handled [tool.setuptools.packages.find] # scanning for namespace packages is true by default in pyproject.toml, so - # you need NOT include the following line. + # you need NOT include this configuration. namespaces = true where = ["src"] @@ -451,34 +500,9 @@ scanning of namespace packages in the ``src`` directory and the rest is handled include_package_data=True, ) -Summary -======= - -In summary, the three options allow you to: - -``include_package_data`` - Accept all data files and directories matched by - :ref:`MANIFEST.in ` or added by - a :ref:`plugin `. - -``package_data`` - Specify additional patterns to match files that may or may - not be matched by :ref:`MANIFEST.in ` - or added by a :ref:`plugin `. - -``exclude_package_data`` - Specify patterns for data files and directories that should *not* be - included when a package is installed, even if they would otherwise have - been included due to the use of the preceding options. - -.. note:: - Due to the way the build process works, a data file that you - include in your project and then stop including may be "orphaned" in your - project's build directories, requiring you to manually deleting them. - This may also be important for your users and contributors - if they track intermediate revisions of your project using Subversion; be sure - to let them know when you make changes that remove files from inclusion so they - can also manually delete them. +To avoid common mistakes with :ref:`include-package-data`, +please ensure :ref:`MANIFEST.in ` is properly set +or use a revision control system plugin (see :doc:`/userguide/miscellaneous`). .. _Accessing Data Files at Runtime: diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index 5f15ff6053..7d841f6661 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -168,6 +168,20 @@ binary extensions during the build process, or included in the final See :doc:`/userguide/datafiles` for more information. + +.. _Caching and Troubleshooting: + +Caching and Troubleshooting +=========================== + +Setuptools automatically creates a few directories to host build artefacts and +cache files, such as ``build``, ``dist``, ``*.egg-info``. While cache is +useful to speed up incremental builds, in some edge cases it might become +stale. If you feel that caching is causing problems to your build, specially +after changes in configuration or in the directory/file structure., consider +removing ``build``, ``dist``, ``*.egg-info`` [#PKG-INFO]_ before rebuilding or +reinstalling your project. + ---- .. [#build-process] @@ -183,5 +197,9 @@ binary extensions during the build process, or included in the final :term:`Virtual Environment`. Therefore it only contains items that are required during runtime. +.. [#PKG-INFO] + When working from an extracted sdist (e.g. for patching), you might also consider removing + the ``PKG-INFO`` file to force its recreation. + .. _git: https://git-scm.com .. _mercurial: https://www.mercurial-scm.org From 520d5efb7a6ee4cb27477b2ab4a62064808b0bd5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 22 Feb 2024 10:45:16 -0500 Subject: [PATCH 026/184] Remoe .idea/ from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 90ae80505e..311c9b269e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,5 @@ setuptools.egg-info *~ .hg* .cache -.idea/ .pytest_cache/ .mypy_cache/ From e8e7b1b4fae50db7508183a2cd3c7e0eaffe01d5 Mon Sep 17 00:00:00 2001 From: Tommy Date: Sun, 25 Feb 2024 15:20:26 +0700 Subject: [PATCH 027/184] small grammar fix --- docs/userguide/dependency_management.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 0feb346dc5..a2c0c890f3 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -19,7 +19,7 @@ Build system requirement After organizing all the scripts and files and getting ready for packaging, there needs to be a way to specify what programs and libraries are actually needed -do the packaging (in our case, ``setuptools`` of course). +to do the packaging (in our case, ``setuptools`` of course). This needs to be specified in your ``pyproject.toml`` file (if you have forgot what this is, go to :doc:`/userguide/quickstart` or :doc:`/build_meta`): From c6ebe3d95f3f8720c39e01b6db6764743308ce26 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 25 Feb 2024 20:58:49 -0500 Subject: [PATCH 028/184] Drop dependency on `py` --- newsfragments/4237.misc.rst | 1 + .../tests/test_find_distributions.py | 21 ++++++++++++++----- setup.cfg | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4237.misc.rst diff --git a/newsfragments/4237.misc.rst b/newsfragments/4237.misc.rst new file mode 100644 index 0000000000..995bee20e1 --- /dev/null +++ b/newsfragments/4237.misc.rst @@ -0,0 +1 @@ +Drop dependency on `py`. Bump ``pytest-xdist`` to ``>=3`` and use `pathlib` instead in tests -- by :user:`Avasam` diff --git a/pkg_resources/tests/test_find_distributions.py b/pkg_resources/tests/test_find_distributions.py index 4ffcdf3b58..8263ca6c41 100644 --- a/pkg_resources/tests/test_find_distributions.py +++ b/pkg_resources/tests/test_find_distributions.py @@ -1,9 +1,10 @@ -import py +from pathlib import Path +import shutil import pytest import pkg_resources -TESTS_DATA_DIR = py.path.local(__file__).dirpath('data') +TESTS_DATA_DIR = Path(__file__).parent / 'data' class TestFindDistributions: @@ -19,21 +20,31 @@ def test_non_egg_dir_named_egg(self, target_dir): assert not list(dists) def test_standalone_egg_directory(self, target_dir): - (TESTS_DATA_DIR / 'my-test-package_unpacked-egg').copy(target_dir) + shutil.copytree( + TESTS_DATA_DIR / 'my-test-package_unpacked-egg', + target_dir, + dirs_exist_ok=True, + ) dists = pkg_resources.find_distributions(str(target_dir)) assert [dist.project_name for dist in dists] == ['my-test-package'] dists = pkg_resources.find_distributions(str(target_dir), only=True) assert not list(dists) def test_zipped_egg(self, target_dir): - (TESTS_DATA_DIR / 'my-test-package_zipped-egg').copy(target_dir) + shutil.copytree( + TESTS_DATA_DIR / 'my-test-package_zipped-egg', + target_dir, + dirs_exist_ok=True, + ) dists = pkg_resources.find_distributions(str(target_dir)) assert [dist.project_name for dist in dists] == ['my-test-package'] dists = pkg_resources.find_distributions(str(target_dir), only=True) assert not list(dists) def test_zipped_sdist_one_level_removed(self, target_dir): - (TESTS_DATA_DIR / 'my-test-package-zip').copy(target_dir) + shutil.copytree( + TESTS_DATA_DIR / 'my-test-package-zip', target_dir, dirs_exist_ok=True + ) dists = pkg_resources.find_distributions( str(target_dir / "my-test-package.zip") ) diff --git a/setup.cfg b/setup.cfg index 0699bc72e7..f40fcd8265 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,7 +61,7 @@ testing = pip>=19.1 # For proper file:// URLs support. packaging>=23.2 jaraco.envs>=2.2 - pytest-xdist + pytest-xdist>=3 # Dropped dependency on pytest-fork and py jaraco.path>=3.2.0 build[virtualenv] filelock>=3.4.0 From 3d7e0bf126397ddb1ada6aa893fa27ac61d9d2da Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 25 Feb 2024 22:19:18 -0500 Subject: [PATCH 029/184] Replace Flake8-2020 by Ruff --- newsfragments/4238.misc.rst | 1 + ruff.toml | 1 + setup.cfg | 1 - setuptools/tests/test_core_metadata.py | 8 ++++---- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4238.misc.rst diff --git a/newsfragments/4238.misc.rst b/newsfragments/4238.misc.rst new file mode 100644 index 0000000000..a7ccfc911e --- /dev/null +++ b/newsfragments/4238.misc.rst @@ -0,0 +1 @@ +Drop dependency on Flake8 by using Ruff's YTT rules instead of flake8-2020 -- by :user:`Avasam` diff --git a/ruff.toml b/ruff.toml index 597d516bc4..e520746944 100644 --- a/ruff.toml +++ b/ruff.toml @@ -18,6 +18,7 @@ ignore = [ ] extend-select = [ "UP", # pyupgrade + "YTT", # flake8-2020 ] extend-ignore = [ "UP015", # redundant-open-modes, explicit is preferred diff --git a/setup.cfg b/setup.cfg index 0699bc72e7..b544ac72f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,6 @@ testing = pytest-ruff >= 0.2.1; sys_platform != "cygwin" # local - flake8-2020 virtualenv>=13.0.0 wheel pip>=19.1 # For proper file:// URLs support. diff --git a/setuptools/tests/test_core_metadata.py b/setuptools/tests/test_core_metadata.py index 68002cc51b..eaabf20a9d 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -323,9 +323,9 @@ def test_parity_with_metadata_from_pypa_wheel(tmp_path): "testing": """ pytest >= 6 pytest-checkdocs >= 2.4 - pytest-flake8 ; \\ - # workaround for tholo/pytest-flake8#87 - python_version < "3.12" + tomli ; \\ + # Using stdlib when possible + python_version < "3.11" ini2toml[lite]>=0.9 """, "other": [], @@ -345,7 +345,7 @@ def test_parity_with_metadata_from_pypa_wheel(tmp_path): 'Requires-Python: >=3.8', 'Provides-Extra: other', 'Provides-Extra: testing', - 'Requires-Dist: pytest-flake8; python_version < "3.12" and extra == "testing"', + 'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"', 'Requires-Dist: more-itertools==8.8.0; extra == "other"', 'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"', ] From 2d8ab8600477d788cb68187118d73154d9746963 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 17:31:05 -0500 Subject: [PATCH 030/184] Standardize and centralize StrPath TypeAlias --- newsfragments/4241.misc.rst | 1 + setuptools/_normalization.py | 4 --- setuptools/_path.py | 9 ++++-- setuptools/command/editable_wheel.py | 25 ++++++++------- setuptools/config/_apply_pyprojecttoml.py | 19 ++++++------ setuptools/config/expand.py | 31 +++++++++---------- setuptools/config/pyprojecttoml.py | 16 +++++----- setuptools/config/setupcfg.py | 16 +++++----- setuptools/discovery.py | 29 +++++++++-------- setuptools/tests/config/test_pyprojecttoml.py | 4 +-- setuptools/tests/test_config_discovery.py | 10 +++--- tools/generate_validation_code.py | 4 ++- 12 files changed, 85 insertions(+), 83 deletions(-) create mode 100644 newsfragments/4241.misc.rst diff --git a/newsfragments/4241.misc.rst b/newsfragments/4241.misc.rst new file mode 100644 index 0000000000..ef6da2c323 --- /dev/null +++ b/newsfragments/4241.misc.rst @@ -0,0 +1 @@ +Improvements to `Path`-related type annotations when it could be ``str | PathLike`` -- by :user:`Avasam` diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 8f211b8bfb..e858052ccd 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -4,13 +4,9 @@ """ import re -from pathlib import Path -from typing import Union from .extern import packaging -_Path = Union[str, Path] - # https://packaging.python.org/en/latest/specifications/core-metadata/#name _VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I) _UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I) diff --git a/setuptools/_path.py b/setuptools/_path.py index b99d9dadcf..fb8ef0e198 100644 --- a/setuptools/_path.py +++ b/setuptools/_path.py @@ -2,7 +2,10 @@ import sys from typing import Union -_Path = Union[str, os.PathLike] +if sys.version_info >= (3, 9): + StrPath = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath +else: + StrPath = Union[str, os.PathLike] def ensure_directory(path): @@ -11,7 +14,7 @@ def ensure_directory(path): os.makedirs(dirname, exist_ok=True) -def same_path(p1: _Path, p2: _Path) -> bool: +def same_path(p1: StrPath, p2: StrPath) -> bool: """Differs from os.path.samefile because it does not require paths to exist. Purely string based (no comparison between i-nodes). >>> same_path("a/b", "./a/b") @@ -30,7 +33,7 @@ def same_path(p1: _Path, p2: _Path) -> bool: return normpath(p1) == normpath(p2) -def normpath(filename: _Path) -> str: +def normpath(filename: StrPath) -> str: """Normalize a file/dir name for comparison purposes.""" # See pkg_resources.normalize_path for notes about cygwin file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 8a4ae7928f..a81fcd5bf9 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -33,7 +33,6 @@ Protocol, Tuple, TypeVar, - Union, ) from .. import ( @@ -43,6 +42,7 @@ errors, namespaces, ) +from .._path import StrPath from ..discovery import find_package_path from ..dist import Distribution from ..warnings import ( @@ -55,8 +55,7 @@ if TYPE_CHECKING: from wheel.wheelfile import WheelFile # noqa -_Path = Union[str, Path] -_P = TypeVar("_P", bound=_Path) +_P = TypeVar("_P", bound=StrPath) _logger = logging.getLogger(__name__) @@ -181,7 +180,7 @@ def _find_egg_info_dir(self) -> Optional[str]: return next(candidates, None) def _configure_build( - self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path + self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath ): """Configure commands to behave in the following ways: @@ -256,7 +255,11 @@ def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]: return files, mapping def _run_build_commands( - self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path + self, + dist_name: str, + unpacked_wheel: StrPath, + build_lib: StrPath, + tmp_dir: StrPath, ) -> Tuple[List[str], Dict[str, str]]: self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir) self._run_build_subcommands() @@ -354,7 +357,7 @@ def _select_strategy( self, name: str, tag: str, - build_lib: _Path, + build_lib: StrPath, ) -> "EditableStrategy": """Decides which strategy to use to implement an editable installation.""" build_name = f"__editable__.{name}-{tag}" @@ -424,8 +427,8 @@ def __init__( self, dist: Distribution, name: str, - auxiliary_dir: _Path, - build_lib: _Path, + auxiliary_dir: StrPath, + build_lib: StrPath, ): self.auxiliary_dir = Path(auxiliary_dir) self.build_lib = Path(build_lib).resolve() @@ -567,7 +570,7 @@ def _can_symlink_files(base_dir: Path) -> bool: def _simple_layout( - packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path + packages: Iterable[str], package_dir: Dict[str, str], project_dir: StrPath ) -> bool: """Return ``True`` if: - all packages are contained by the same parent directory, **and** @@ -649,7 +652,7 @@ def _find_top_level_modules(dist: Distribution) -> Iterator[str]: def _find_package_roots( packages: Iterable[str], package_dir: Mapping[str, str], - src_root: _Path, + src_root: StrPath, ) -> Dict[str, str]: pkg_roots: Dict[str, str] = { pkg: _absolute_root(find_package_path(pkg, package_dir, src_root)) @@ -659,7 +662,7 @@ def _find_package_roots( return _remove_nested(pkg_roots) -def _absolute_root(path: _Path) -> str: +def _absolute_root(path: StrPath) -> str: """Works for packages and top-level modules""" path_ = Path(path) parent = path_.parent diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 32fb00131e..7301bc65c1 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -29,7 +29,7 @@ Union, cast, ) - +from .._path import StrPath from ..errors import RemovedConfigError from ..warnings import SetuptoolsWarning @@ -38,15 +38,14 @@ from setuptools.dist import Distribution # noqa EMPTY: Mapping = MappingProxyType({}) # Immutable dict-like -_Path = Union[os.PathLike, str] _DictOrStr = Union[dict, str] -_CorrespFn = Callable[["Distribution", Any, _Path], None] +_CorrespFn = Callable[["Distribution", Any, StrPath], None] _Correspondence = Union[str, _CorrespFn] _logger = logging.getLogger(__name__) -def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution": +def apply(dist: "Distribution", config: dict, filename: StrPath) -> "Distribution": """Apply configuration dict read with :func:`read_configuration`""" if not config: @@ -68,7 +67,7 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution" return dist -def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path): +def _apply_project_table(dist: "Distribution", config: dict, root_dir: StrPath): project_table = config.get("project", {}).copy() if not project_table: return # short-circuit @@ -85,7 +84,7 @@ def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path): _set_config(dist, corresp, value) -def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path): +def _apply_tool_table(dist: "Distribution", config: dict, filename: StrPath): tool_table = config.get("tool", {}).get("setuptools", {}) if not tool_table: return # short-circuit @@ -153,7 +152,7 @@ def _guess_content_type(file: str) -> Optional[str]: raise ValueError(f"Undefined content type for {file}, {msg}") -def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path): +def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: StrPath): from setuptools.config import expand if isinstance(val, str): @@ -174,7 +173,7 @@ def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path): dist._referenced_files.add(cast(str, file)) -def _license(dist: "Distribution", val: dict, root_dir: _Path): +def _license(dist: "Distribution", val: dict, root_dir: StrPath): from setuptools.config import expand if "file" in val: @@ -184,7 +183,7 @@ def _license(dist: "Distribution", val: dict, root_dir: _Path): _set_config(dist, "license", val["text"]) -def _people(dist: "Distribution", val: List[dict], _root_dir: _Path, kind: str): +def _people(dist: "Distribution", val: List[dict], _root_dir: StrPath, kind: str): field = [] email_field = [] for person in val: @@ -244,7 +243,7 @@ def _unify_entry_points(project_table: dict): # intentional (for resetting configurations that are missing `dynamic`). -def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path): +def _copy_command_options(pyproject: dict, dist: "Distribution", filename: StrPath): tool_table = pyproject.get("tool", {}) cmdclass = tool_table.get("setuptools", {}).get("cmdclass", {}) valid_options = _valid_command_options(cmdclass) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index b48fc1187e..e23a762cf5 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -46,7 +46,7 @@ from distutils.errors import DistutilsOptionError -from .._path import same_path as _same_path +from .._path import same_path as _same_path, StrPath from ..warnings import SetuptoolsWarning if TYPE_CHECKING: @@ -55,7 +55,6 @@ from distutils.dist import DistributionMetadata # noqa chain_iter = chain.from_iterable -_Path = Union[str, os.PathLike] _K = TypeVar("_K") _V = TypeVar("_V", covariant=True) @@ -88,7 +87,7 @@ def __getattr__(self, attr): def glob_relative( - patterns: Iterable[str], root_dir: Optional[_Path] = None + patterns: Iterable[str], root_dir: Optional[StrPath] = None ) -> List[str]: """Expand the list of glob patterns, but preserving relative paths. @@ -120,7 +119,7 @@ def glob_relative( return expanded_values -def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) -> str: +def read_files(filepaths: Union[str, bytes, Iterable[StrPath]], root_dir=None) -> str: """Return the content of the files concatenated using ``\n`` as str This function is sandboxed and won't reach anything outside ``root_dir`` @@ -138,7 +137,7 @@ def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) -> ) -def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]: +def _filter_existing_files(filepaths: Iterable[StrPath]) -> Iterator[StrPath]: for path in filepaths: if os.path.isfile(path): yield path @@ -146,12 +145,12 @@ def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]: SetuptoolsWarning.emit(f"File {path!r} cannot be found") -def _read_file(filepath: Union[bytes, _Path]) -> str: +def _read_file(filepath: Union[bytes, StrPath]) -> str: with open(filepath, encoding='utf-8') as f: return f.read() -def _assert_local(filepath: _Path, root_dir: str): +def _assert_local(filepath: StrPath, root_dir: str): if Path(os.path.abspath(root_dir)) not in Path(os.path.abspath(filepath)).parents: msg = f"Cannot access {filepath!r} (or anything outside {root_dir!r})" raise DistutilsOptionError(msg) @@ -162,7 +161,7 @@ def _assert_local(filepath: _Path, root_dir: str): def read_attr( attr_desc: str, package_dir: Optional[Mapping[str, str]] = None, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, ): """Reads the value of an attribute from a module. @@ -197,7 +196,7 @@ def read_attr( return getattr(module, attr_name) -def _find_spec(module_name: str, module_path: Optional[_Path]) -> ModuleSpec: +def _find_spec(module_name: str, module_path: Optional[StrPath]) -> ModuleSpec: spec = importlib.util.spec_from_file_location(module_name, module_path) spec = spec or importlib.util.find_spec(module_name) @@ -218,8 +217,8 @@ def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType: def _find_module( - module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: _Path -) -> Tuple[_Path, Optional[str], str]: + module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: StrPath +) -> Tuple[StrPath, Optional[str], str]: """Given a module (that could normally be imported by ``module_name`` after the build is complete), find the path to the parent directory where it is contained and the canonical name that could be used to import it @@ -254,7 +253,7 @@ def _find_module( def resolve_class( qualified_class_name: str, package_dir: Optional[Mapping[str, str]] = None, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, ) -> Callable: """Given a qualified class name, return the associated class object""" root_dir = root_dir or os.getcwd() @@ -270,7 +269,7 @@ def resolve_class( def cmdclass( values: Dict[str, str], package_dir: Optional[Mapping[str, str]] = None, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, ) -> Dict[str, Callable]: """Given a dictionary mapping command names to strings for qualified class names, apply :func:`resolve_class` to the dict values. @@ -282,7 +281,7 @@ def find_packages( *, namespaces=True, fill_package_dir: Optional[Dict[str, str]] = None, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, **kwargs, ) -> List[str]: """Works similarly to :func:`setuptools.find_packages`, but with all @@ -331,7 +330,7 @@ def find_packages( return packages -def _nest_path(parent: _Path, path: _Path) -> str: +def _nest_path(parent: StrPath, path: StrPath) -> str: path = parent if path in {".", ""} else os.path.join(parent, path) return os.path.normpath(path) @@ -361,7 +360,7 @@ def canonic_package_data(package_data: dict) -> dict: def canonic_data_files( - data_files: Union[list, dict], root_dir: Optional[_Path] = None + data_files: Union[list, dict], root_dir: Optional[StrPath] = None ) -> List[Tuple[str, List[str]]]: """For compatibility with ``setup.py``, ``data_files`` should be a list of pairs instead of a dict. diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 9b9788eff4..5eb9421f1f 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -13,8 +13,9 @@ import os from contextlib import contextmanager from functools import partial -from typing import TYPE_CHECKING, Callable, Dict, Mapping, Optional, Set, Union +from typing import TYPE_CHECKING, Callable, Dict, Mapping, Optional, Set +from .._path import StrPath from ..errors import FileError, InvalidConfigError from ..warnings import SetuptoolsWarning from . import expand as _expand @@ -24,18 +25,17 @@ if TYPE_CHECKING: from setuptools.dist import Distribution # noqa -_Path = Union[str, os.PathLike] _logger = logging.getLogger(__name__) -def load_file(filepath: _Path) -> dict: +def load_file(filepath: StrPath) -> dict: from ..compat.py310 import tomllib with open(filepath, "rb") as file: return tomllib.load(file) -def validate(config: dict, filepath: _Path) -> bool: +def validate(config: dict, filepath: StrPath) -> bool: from . import _validate_pyproject as validator trove_classifier = validator.FORMAT_FUNCTIONS.get("trove-classifier") @@ -58,7 +58,7 @@ def validate(config: dict, filepath: _Path) -> bool: def apply_configuration( dist: "Distribution", - filepath: _Path, + filepath: StrPath, ignore_option_errors=False, ) -> "Distribution": """Apply the configuration from a ``pyproject.toml`` file into an existing @@ -69,7 +69,7 @@ def apply_configuration( def read_configuration( - filepath: _Path, + filepath: StrPath, expand=True, ignore_option_errors=False, dist: Optional["Distribution"] = None, @@ -136,7 +136,7 @@ def read_configuration( def expand_configuration( config: dict, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, ignore_option_errors: bool = False, dist: Optional["Distribution"] = None, ) -> dict: @@ -161,7 +161,7 @@ class _ConfigExpander: def __init__( self, config: dict, - root_dir: Optional[_Path] = None, + root_dir: Optional[StrPath] = None, ignore_option_errors: bool = False, dist: Optional["Distribution"] = None, ): diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index a7f02714cb..cfa43a57b5 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -30,6 +30,7 @@ Union, ) +from .._path import StrPath from ..errors import FileError, OptionError from ..extern.packaging.markers import default_environment as marker_env from ..extern.packaging.requirements import InvalidRequirement, Requirement @@ -43,7 +44,6 @@ from setuptools.dist import Distribution # noqa -_Path = Union[str, os.PathLike] SingleCommandOptions = Dict["str", Tuple["str", Any]] """Dict that associate the name of the options of a particular command to a tuple. The first element of the tuple indicates the origin of the option value @@ -55,7 +55,7 @@ def read_configuration( - filepath: _Path, find_others=False, ignore_option_errors=False + filepath: StrPath, find_others=False, ignore_option_errors=False ) -> dict: """Read given configuration file and returns options from it as a dict. @@ -80,7 +80,7 @@ def read_configuration( return configuration_to_dict(handlers) -def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution": +def apply_configuration(dist: "Distribution", filepath: StrPath) -> "Distribution": """Apply the configuration from a ``setup.cfg`` file into an existing distribution object. """ @@ -91,8 +91,8 @@ def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution" def _apply( dist: "Distribution", - filepath: _Path, - other_files: Iterable[_Path] = (), + filepath: StrPath, + other_files: Iterable[StrPath] = (), ignore_option_errors: bool = False, ) -> Tuple["ConfigHandler", ...]: """Read configuration from ``filepath`` and applies to the ``dist`` object.""" @@ -371,7 +371,7 @@ def parser(value): return parser - def _parse_file(self, value, root_dir: _Path): + def _parse_file(self, value, root_dir: StrPath): """Represents value as a string, allowing including text from nearest files using `file:` directive. @@ -397,7 +397,7 @@ def _parse_file(self, value, root_dir: _Path): self._referenced_files.update(filepaths) return expand.read_files(filepaths, root_dir) - def _parse_attr(self, value, package_dir, root_dir: _Path): + def _parse_attr(self, value, package_dir, root_dir: StrPath): """Represents value as a module attribute. Examples: @@ -539,7 +539,7 @@ def __init__( ignore_option_errors: bool, ensure_discovered: expand.EnsurePackagesDiscovered, package_dir: Optional[dict] = None, - root_dir: _Path = os.curdir, + root_dir: StrPath = os.curdir, ): super().__init__(target_obj, options, ignore_option_errors, ensure_discovered) self.package_dir = package_dir diff --git a/setuptools/discovery.py b/setuptools/discovery.py index 50a948750f..571be12bf4 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -51,15 +51,14 @@ Mapping, Optional, Tuple, - Union, ) import _distutils_hack.override # noqa: F401 +from ._path import StrPath from distutils import log from distutils.util import convert_path -_Path = Union[str, os.PathLike] StrIter = Iterator[str] chain_iter = itertools.chain.from_iterable @@ -68,7 +67,7 @@ from setuptools import Distribution # noqa -def _valid_name(path: _Path) -> bool: +def _valid_name(path: StrPath) -> bool: # Ignore invalid names that cannot be imported directly return os.path.basename(path).isidentifier() @@ -98,7 +97,7 @@ class _Finder: @classmethod def find( cls, - where: _Path = '.', + where: StrPath = '.', exclude: Iterable[str] = (), include: Iterable[str] = ('*',), ) -> List[str]: @@ -131,7 +130,7 @@ def find( ) @classmethod - def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter: + def _find_iter(cls, where: StrPath, exclude: _Filter, include: _Filter) -> StrIter: raise NotImplementedError @@ -143,7 +142,7 @@ class PackageFinder(_Finder): ALWAYS_EXCLUDE = ("ez_setup", "*__pycache__") @classmethod - def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter: + def _find_iter(cls, where: StrPath, exclude: _Filter, include: _Filter) -> StrIter: """ All the packages found in 'where' that pass the 'include' filter, but not the 'exclude' filter. @@ -175,14 +174,14 @@ def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter dirs.append(dir) @staticmethod - def _looks_like_package(path: _Path, _package_name: str) -> bool: + def _looks_like_package(path: StrPath, _package_name: str) -> bool: """Does a directory look like a package?""" return os.path.isfile(os.path.join(path, '__init__.py')) class PEP420PackageFinder(PackageFinder): @staticmethod - def _looks_like_package(_path: _Path, _package_name: str) -> bool: + def _looks_like_package(_path: StrPath, _package_name: str) -> bool: return True @@ -192,7 +191,7 @@ class ModuleFinder(_Finder): """ @classmethod - def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter: + def _find_iter(cls, where: StrPath, exclude: _Filter, include: _Filter) -> StrIter: for file in glob(os.path.join(where, "*.py")): module, _ext = os.path.splitext(os.path.basename(file)) @@ -255,7 +254,7 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): """Reserved package names""" @staticmethod - def _looks_like_package(_path: _Path, package_name: str) -> bool: + def _looks_like_package(_path: StrPath, package_name: str) -> bool: names = package_name.split('.') # Consider PEP 561 root_pkg_is_valid = names[0].isidentifier() or names[0].endswith("-stubs") @@ -292,7 +291,7 @@ class FlatLayoutModuleFinder(ModuleFinder): """Reserved top-level module names""" -def _find_packages_within(root_pkg: str, pkg_dir: _Path) -> List[str]: +def _find_packages_within(root_pkg: str, pkg_dir: StrPath) -> List[str]: nested = PEP420PackageFinder.find(pkg_dir) return [root_pkg] + [".".join((root_pkg, n)) for n in nested] @@ -325,7 +324,7 @@ def _ignore_ext_modules(self): self._skip_ext_modules = True @property - def _root_dir(self) -> _Path: + def _root_dir(self) -> StrPath: # The best is to wait until `src_root` is set in dist, before using _root_dir. return self.dist.src_root or os.curdir @@ -551,7 +550,7 @@ def remove_stubs(packages: List[str]) -> List[str]: def find_parent_package( - packages: List[str], package_dir: Mapping[str, str], root_dir: _Path + packages: List[str], package_dir: Mapping[str, str], root_dir: StrPath ) -> Optional[str]: """Find the parent package that is not a namespace.""" packages = sorted(packages, key=len) @@ -575,7 +574,7 @@ def find_parent_package( def find_package_path( - name: str, package_dir: Mapping[str, str], root_dir: _Path + name: str, package_dir: Mapping[str, str], root_dir: StrPath ) -> str: """Given a package name, return the path where it should be found on disk, considering the ``package_dir`` option. @@ -608,7 +607,7 @@ def find_package_path( return os.path.join(root_dir, *parent.split("/"), *parts) -def construct_package_dir(packages: List[str], package_path: _Path) -> Dict[str, str]: +def construct_package_dir(packages: List[str], package_path: StrPath) -> Dict[str, str]: parent_pkgs = remove_nested_packages(packages) prefix = Path(package_path).parts return {pkg: "/".join([*prefix, *pkg.split(".")]) for pkg in parent_pkgs} diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 318885a6bd..6a40f3bfd7 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -4,7 +4,7 @@ import pytest import tomli_w -from path import Path as _Path +from path import Path from setuptools.config.pyprojecttoml import ( read_configuration, @@ -352,7 +352,7 @@ def test_include_package_data_in_setuppy(tmp_path): setuppy = tmp_path / "setup.py" setuppy.write_text("__import__('setuptools').setup(include_package_data=False)") - with _Path(tmp_path): + with Path(tmp_path): dist = distutils.core.run_setup("setup.py", {}, stop_after="config") assert dist.get_name() == "myproj" diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 85cb09730c..ef2979d4f5 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -13,7 +13,7 @@ import pytest import jaraco.path -from path import Path as _Path +from path import Path from .contexts import quiet from .integration.helpers import get_sdist_members, get_wheel_members, run @@ -304,7 +304,7 @@ def test_setupcfg_metadata(self, tmp_path, folder, opts): assert dist.package_dir package_path = find_package_path("pkg", dist.package_dir, tmp_path) assert os.path.exists(package_path) - assert folder in _Path(package_path).parts() + assert folder in Path(package_path).parts() _run_build(tmp_path, "--sdist") dist_file = tmp_path / "dist/pkg-42.tar.gz" @@ -607,14 +607,14 @@ def _get_dist(dist_path, attrs): script = dist_path / 'setup.py' if script.exists(): - with _Path(dist_path): + with Path(dist_path): dist = distutils.core.run_setup("setup.py", {}, stop_after="init") else: dist = Distribution(attrs) dist.src_root = root dist.script_name = "setup.py" - with _Path(dist_path): + with Path(dist_path): dist.parse_config_files() dist.set_defaults() @@ -627,7 +627,7 @@ def _run_sdist_programatically(dist_path, attrs): cmd.ensure_finalized() assert cmd.distribution.packages or cmd.distribution.py_modules - with quiet(), _Path(dist_path): + with quiet(), Path(dist_path): cmd.run() return dist, cmd diff --git a/tools/generate_validation_code.py b/tools/generate_validation_code.py index 53bc8ad650..b575fb1e1c 100644 --- a/tools/generate_validation_code.py +++ b/tools/generate_validation_code.py @@ -1,10 +1,12 @@ +from os import PathLike import subprocess import sys from pathlib import Path +from typing import Union -def generate_pyproject_validation(dest: Path): +def generate_pyproject_validation(dest: Union[str, PathLike]): """ Generates validation code for ``pyproject.toml`` based on JSON schemas and the ``validate-pyproject`` library. From 2118a5521581feb79538ac9253d21302208b53c9 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:07:09 -0500 Subject: [PATCH 031/184] pkg_resources: Clarify some methods return `bytes`, not `str` --- pkg_resources/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 10c6a9cd06..aa77d4a3f0 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -566,8 +566,8 @@ def get_resource_stream(self, manager, resource_name): `manager` must be an ``IResourceManager``""" - def get_resource_string(self, manager, resource_name): - """Return a string containing the contents of `resource_name` + def get_resource_string(self, manager, resource_name) -> bytes: + """Return a bytes string containing the contents of `resource_name` `manager` must be an ``IResourceManager``""" @@ -1203,8 +1203,8 @@ def resource_stream(self, package_or_requirement, resource_name): self, resource_name ) - def resource_string(self, package_or_requirement, resource_name): - """Return specified resource as a string""" + def resource_string(self, package_or_requirement, resource_name) -> bytes: + """Return specified resource as a bytes string""" return get_provider(package_or_requirement).get_resource_string( self, resource_name ) @@ -1479,7 +1479,7 @@ def get_resource_filename(self, manager, resource_name): def get_resource_stream(self, manager, resource_name): return io.BytesIO(self.get_resource_string(manager, resource_name)) - def get_resource_string(self, manager, resource_name): + def get_resource_string(self, manager, resource_name) -> bytes: return self._get(self._fn(self.module_path, resource_name)) def has_resource(self, resource_name): @@ -1649,7 +1649,7 @@ def _validate_resource_path(path): DeprecationWarning, ) - def _get(self, path): + def _get(self, path) -> bytes: if hasattr(self.loader, 'get_data'): return self.loader.get_data(path) raise NotImplementedError( @@ -1706,7 +1706,7 @@ def _listdir(self, path): def get_resource_stream(self, manager, resource_name): return open(self._fn(self.module_path, resource_name), 'rb') - def _get(self, path): + def _get(self, path) -> bytes: with open(path, 'rb') as stream: return stream.read() @@ -1731,8 +1731,8 @@ class EmptyProvider(NullProvider): _isdir = _has = lambda self, path: False - def _get(self, path): - return '' + def _get(self, path) -> bytes: + return b'' def _listdir(self, path): return [] From 5f00f07838c35a35db0c898507de3bbde4176c0d Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:11:27 -0500 Subject: [PATCH 032/184] Create 4243.bugfix.rst --- newsfragments/4243.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4243.bugfix.rst diff --git a/newsfragments/4243.bugfix.rst b/newsfragments/4243.bugfix.rst new file mode 100644 index 0000000000..40dbdc3730 --- /dev/null +++ b/newsfragments/4243.bugfix.rst @@ -0,0 +1 @@ +Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in `EmptyProvider._get` -- by :user:`Avasam` From f93a094256d4c2946768a7ed38302a35944a4f88 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:19:18 -0500 Subject: [PATCH 033/184] Return an empty list by default in `ResourceManager.cleanup_resources` Subclasses are expected to return a list. There's no mention of this method potentially returning `None` in the docstring. --- pkg_resources/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 10c6a9cd06..f2e5bb3645 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1339,7 +1339,7 @@ def set_extraction_path(self, path): self.extraction_path = path - def cleanup_resources(self, force=False): + def cleanup_resources(self, force=False) -> list[str]: """ Delete all extracted resource files and directories, returning a list of the file and directory names that could not be successfully removed. @@ -1351,6 +1351,7 @@ def cleanup_resources(self, force=False): directory used for extractions. """ # XXX + return [] def get_default_cache(): From 19f57d0652c1bed18558c8d552e10a497d07cc01 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:21:27 -0500 Subject: [PATCH 034/184] Update newsfragments/4243.bugfix.rst --- newsfragments/4243.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/4243.bugfix.rst b/newsfragments/4243.bugfix.rst index 40dbdc3730..e8212721f3 100644 --- a/newsfragments/4243.bugfix.rst +++ b/newsfragments/4243.bugfix.rst @@ -1 +1 @@ -Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in `EmptyProvider._get` -- by :user:`Avasam` +Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in ``EmptyProvider._get`` -- by :user:`Avasam` From 29b36fa20c0350c3c998511890e6f578d91f4a87 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:24:08 -0500 Subject: [PATCH 035/184] Create 4244.bugfix.rst --- newsfragments/4244.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4244.bugfix.rst diff --git a/newsfragments/4244.bugfix.rst b/newsfragments/4244.bugfix.rst new file mode 100644 index 0000000000..5d606de718 --- /dev/null +++ b/newsfragments/4244.bugfix.rst @@ -0,0 +1 @@ +Return an empty `list` by default in ``pkg_resources.ResourceManager.cleanup_resources`` -- by :user:`Avasam` From 5b538e1879edaa574a3c4cb7119bbddf3ee6cc9d Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:38:06 -0500 Subject: [PATCH 036/184] Update __init__.py --- pkg_resources/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index f2e5bb3645..67883b11f1 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -27,7 +27,7 @@ import time import re import types -from typing import Protocol +from typing import List, Protocol import zipfile import zipimport import warnings @@ -1339,7 +1339,7 @@ def set_extraction_path(self, path): self.extraction_path = path - def cleanup_resources(self, force=False) -> list[str]: + def cleanup_resources(self, force=False) -> List[str]: """ Delete all extracted resource files and directories, returning a list of the file and directory names that could not be successfully removed. From 219a000b299f0d610dbb1b766286a379e4d2ba06 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Feb 2024 23:53:17 -0500 Subject: [PATCH 037/184] Prevent `pkg_resources._find_adapter` from ever returning `None` --- pkg_resources/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 10c6a9cd06..5e9036564a 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3207,7 +3207,9 @@ def _find_adapter(registry, ob): for t in types: if t in registry: return registry[t] - return None + # _find_adapter would previously return None, and immediatly 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}") def ensure_directory(path): From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 038/184] 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 e61ca8b0d6..6c5b00092e 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 039/184] 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 fac3259f88..a88fd021df 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 040/184] 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 a88fd021df..b1d8e7c7ae 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 041/184] 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 ca808a6ab7..06ce3bc6c8 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 6cbf51681b..f656be6089 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 042/184] 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 a313da3e75..2763db9c02 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 043/184] 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 b1d8e7c7ae..5fb811c406 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 044/184] 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 6c5b00092e..70612985a7 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 045/184] 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 537959fed6..51e5cd00cc 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 046/184] 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 2080604982..ddf7bf1dba 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 047/184] 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 77c9ad7573..6730878e96 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 048/184] 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 5408b16032..aa0c90cfcd 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 049/184] 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 28cc5632a3..8005b81c64 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 050/184] 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 49691d4b9b..b6512e6d77 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 2519ed6a10..90c8f90679 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 ba2d659984..d1c9855450 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 051/184] 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 2c11ff769e..95aa299889 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 052/184] 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 14d51472f2..d496d5d452 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 67feb16486..6faf546cfe 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 8fdcbc0ea2..8849474cd7 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 b4228299f4..397ebf823e 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 e96db22bed..675bcebdad 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 cc9b367ef9..d8704e3583 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 ba6580c71e..a15781f28a 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 b59cc23731..28f55fb914 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 cf1afc8c1f..5a24246ccb 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 caf15f04a6..a9124f2b71 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 05d2971994..799de9489c 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 b3dbc3be15..84151b7eb9 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 23dc3392a2..819fe56f6d 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 7c0f0e5b78..659583943b 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 3b887dc5a4..c025f12062 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 3f3e21b567..8ebd2a790f 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 6dadf923d7..3205762654 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 724986d89d..402c0c0620 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 c3823e257e..1a07746bc7 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 59224e71e5..ab12119fa5 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 afefe525ef..48adceb114 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 5fb811c406..40215b8347 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 95532e83b9..cb4db4e192 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 c2cff44523..8617fa9919 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 51e5cd00cc..e24dea3603 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 0738b7c877..e7d69bb6ef 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 3b9f82b71e..e441186e3a 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 900edafa7c..0aaf0a534c 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 aa0c90cfcd..c26e61ab4a 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 18385cfef2..8ab76ddef4 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 053/184] 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 e7d69bb6ef..6fc9ed0883 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 054/184] 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 e441186e3a..888e27b5b5 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 055/184] 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 888e27b5b5..27796d9fd5 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 056/184] 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 27796d9fd5..08f9e19fac 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 057/184] =?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 e24dea3603..4ae81a22e4 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 08f9e19fac..6c7019140e 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 058/184] 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 bfffbb1da0..bf1a9d9b45 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 059/184] 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 082ee1d349..16ac5ca746 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 060/184] 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 8005b81c64..7e05ec5f9a 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 58313f2864..4da2acb6a8 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 061/184] 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 5d3826a1b7..591c5ce0ad 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 062/184] 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 00718a37bd..450f68c993 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 063/184] 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 57cf1a525c..ec4c9982ad 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 064/184] 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 f656be6089..131c1344bb 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 065/184] 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 4a721b691c..fe787f44c8 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 066/184] 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 14d51472f2..d496d5d452 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 67feb16486..6faf546cfe 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 8fdcbc0ea2..8849474cd7 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 b4228299f4..397ebf823e 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 e96db22bed..675bcebdad 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 cc9b367ef9..d8704e3583 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 ba6580c71e..a15781f28a 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 b59cc23731..28f55fb914 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 cf1afc8c1f..5a24246ccb 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 caf15f04a6..a9124f2b71 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 05d2971994..799de9489c 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 b3dbc3be15..84151b7eb9 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 23dc3392a2..819fe56f6d 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 7c0f0e5b78..659583943b 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 3b887dc5a4..c025f12062 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 3f3e21b567..8ebd2a790f 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 6dadf923d7..3205762654 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 724986d89d..402c0c0620 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 c3823e257e..1a07746bc7 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 59224e71e5..ab12119fa5 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 afefe525ef..48adceb114 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 5fb811c406..40215b8347 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 95532e83b9..cb4db4e192 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 c2cff44523..8617fa9919 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 537959fed6..da4663076b 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 0738b7c877..e7d69bb6ef 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 3b9f82b71e..e441186e3a 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 900edafa7c..0aaf0a534c 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 5408b16032..a2ba1fc961 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 18385cfef2..8ab76ddef4 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 067/184] 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 c26e61ab4a..bfd30700fa 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 068/184] 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 4ae81a22e4..ae66bc4eb8 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 069/184] 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 6faf546cfe..bcf9580c7a 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 070/184] 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 494d97d16f..573741d772 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 071/184] 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 a55951ed7c..f92ecb9638 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 072/184] 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 ac489726ca..b76cb9bc73 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 073/184] 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 36f947e51c..6f90cfe21d 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 074/184] 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 659583943b..c4d2a45dc2 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 075/184] 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 84151b7eb9..2060950415 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 076/184] 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 8ebd2a790f..0eb9b86107 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 077/184] 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 3ee2f886ba..42820fc7ed 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 078/184] 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 f92ecb9638..e0defd77e6 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 079/184] 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 aad8edb242..6d9b853215 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 080/184] 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 fe979efed5..8e52873dce 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 42820fc7ed..fa31fb33dc 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 dbc6471dbacb5a43ed60625cda8b9fb4d8f0aa34 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Mar 2024 12:19:58 -0500 Subject: [PATCH 081/184] Avoid removing ruff.toml during vendoring. Closes #4252. --- tools/vendored.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/vendored.py b/tools/vendored.py index 9b7cf729ba..f339497fa1 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -105,7 +105,8 @@ def clean(vendor): Remove all files out of the vendor directory except the meta data (as pip uninstall doesn't support -t). """ - remove_all(path for path in vendor.glob('*') if path.basename() != 'vendored.txt') + ignored = ['vendored.txt', 'ruff.toml'] + remove_all(path for path in vendor.glob('*') if path.basename() not in ignored) def install(vendor): From 56bd481f5e515d70f825d397372f6ba020ebcccb Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 3 Mar 2024 19:58:07 -0500 Subject: [PATCH 082/184] Made `pkg_resoursces.NullProvider`'s `has_metadata` and `metadata_isdir` methods return actual booleans like all other Providers --- newsfragments/4254.bugfix.rst | 1 + pkg_resources/__init__.py | 26 +++++++++++++------------- pkg_resources/tests/test_resources.py | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 newsfragments/4254.bugfix.rst diff --git a/newsfragments/4254.bugfix.rst b/newsfragments/4254.bugfix.rst new file mode 100644 index 0000000000..e944fcfb49 --- /dev/null +++ b/newsfragments/4254.bugfix.rst @@ -0,0 +1 @@ +Made ``pkg_resoursces.NullProvider``'s ``has_metadata`` and ``metadata_isdir`` methods return actual booleans like all other Providers. -- by :user:`Avasam` diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 10c6a9cd06..a810bf0082 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -531,7 +531,7 @@ def get_entry_info(dist, group, name): class IMetadataProvider(Protocol): - def has_metadata(self, name): + def has_metadata(self, name) -> bool: """Does the package's distribution contain the named metadata?""" def get_metadata(self, name): @@ -543,7 +543,7 @@ def get_metadata_lines(self, name): Leading and trailing whitespace is stripped from each line, and lines with ``#`` as the first non-blank character are omitted.""" - def metadata_isdir(self, name): + def metadata_isdir(self, name) -> bool: """Is the named metadata a directory? (like ``os.path.isdir()``)""" def metadata_listdir(self, name): @@ -1488,9 +1488,9 @@ def has_resource(self, resource_name): def _get_metadata_path(self, name): return self._fn(self.egg_info, name) - def has_metadata(self, name): + def has_metadata(self, name) -> bool: if not self.egg_info: - return self.egg_info + return False path = self._get_metadata_path(name) return self._has(path) @@ -1514,8 +1514,8 @@ def get_metadata_lines(self, name): def resource_isdir(self, resource_name): return self._isdir(self._fn(self.module_path, resource_name)) - def metadata_isdir(self, name): - return self.egg_info and self._isdir(self._fn(self.egg_info, name)) + def metadata_isdir(self, name) -> bool: + return bool(self.egg_info and self._isdir(self._fn(self.egg_info, name))) def resource_listdir(self, resource_name): return self._listdir(self._fn(self.module_path, resource_name)) @@ -1554,12 +1554,12 @@ def run_script(self, script_name, namespace): script_code = compile(script_text, script_filename, 'exec') exec(script_code, namespace, namespace) - def _has(self, path): + def _has(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) - def _isdir(self, path): + def _isdir(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) @@ -1694,10 +1694,10 @@ def _set_egg(self, path): class DefaultProvider(EggProvider): """Provides access to package resources in the filesystem""" - def _has(self, path): + def _has(self, path) -> bool: return os.path.exists(path) - def _isdir(self, path): + def _isdir(self, path) -> bool: return os.path.isdir(path) def _listdir(self, path): @@ -1939,11 +1939,11 @@ def _index(self): self._dirindex = ind return ind - def _has(self, fspath): + def _has(self, fspath) -> bool: zip_path = self._zipinfo_name(fspath) return zip_path in self.zipinfo or zip_path in self._index() - def _isdir(self, fspath): + def _isdir(self, fspath) -> bool: return self._zipinfo_name(fspath) in self._index() def _listdir(self, fspath): @@ -1977,7 +1977,7 @@ def __init__(self, path): def _get_metadata_path(self, name): return self.path - def has_metadata(self, name): + def has_metadata(self, name) -> bool: return name == 'PKG-INFO' and os.path.isfile(self.path) def get_metadata(self, name): diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 5b2308aea7..b0a319e60f 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -35,7 +35,7 @@ class Metadata(pkg_resources.EmptyProvider): def __init__(self, *pairs): self.metadata = dict(pairs) - def has_metadata(self, name): + def has_metadata(self, name) -> bool: return name in self.metadata def get_metadata(self, name): From 226e1a284dde19b09530a27c66dd18e72f2971b1 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 5 Mar 2024 06:07:38 -0500 Subject: [PATCH 083/184] Fix mypy issues (#3979) * Fix all mypy issues * Ran black * Exclude tox from mypy check * Fix all mypy issues again * Address PR comments * Fix accidental line ending changes * Update .gitignore * No unused type: ignore * TypeError: 'ABCMeta' object is not subscriptable * Fix RuffError * Fix post-merge mypy issues * RUff format * Ignore more generated files * Disable more mypy errors * Globally ignore attr-defined for now * Update more comments * Address PR comments and fix new exposed typing issues * Comments updates and don't touch vendored * Accidentally removed noqa * Update setuptools/tests/integration/test_pip_install_sdist.py Co-authored-by: Anderson Bravalheri * Post merge comments Update setuptools/tests/integration/test_pip_install_sdist.py Co-authored-by: Anderson Bravalheri * Document that usage of _config_vars is very purposeful Closes #4228 + try to resolve doc issue * sort nitpick_ignore * Make only comment on newline like others * Forgot to re-ignore --------- Co-authored-by: Anderson Bravalheri --- docs/conf.py | 10 ++--- mypy.ini | 43 +++++++++++++++++-- pkg_resources/tests/test_pkg_resources.py | 3 +- setup.py | 3 +- setuptools/__init__.py | 11 +++-- setuptools/command/_requirestxt.py | 2 +- setuptools/command/build_ext.py | 11 +++-- setuptools/command/dist_info.py | 4 +- setuptools/command/easy_install.py | 10 ++--- setuptools/command/editable_wheel.py | 41 +++++++++++++----- setuptools/command/install.py | 5 ++- setuptools/command/rotate.py | 3 +- setuptools/command/upload_docs.py | 2 +- setuptools/config/_apply_pyprojecttoml.py | 3 +- setuptools/config/expand.py | 2 +- setuptools/config/pyprojecttoml.py | 5 ++- setuptools/config/setupcfg.py | 4 +- setuptools/dist.py | 18 +++++--- setuptools/extension.py | 8 +++- setuptools/monkey.py | 7 ++- setuptools/msvc.py | 6 ++- setuptools/sandbox.py | 13 +++--- .../tests/config/test_apply_pyprojecttoml.py | 3 +- .../integration/test_pip_install_sdist.py | 6 ++- setuptools/tests/test_bdist_egg.py | 2 +- setuptools/tests/test_editable_install.py | 2 +- setuptools/tests/test_egg_info.py | 3 +- setuptools/tests/test_manifest.py | 3 +- 28 files changed, 164 insertions(+), 69 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0a82ff2fe2..be8856849b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -161,22 +161,23 @@ # Ref: https://stackoverflow.com/a/30624034/595220 nitpick_ignore = [ ('c:func', 'SHGetSpecialFolderPath'), # ref to MS docs + ('envvar', 'DIST_EXTRA_CONFIG'), # undocumented ('envvar', 'DISTUTILS_DEBUG'), # undocumented ('envvar', 'HOME'), # undocumented ('envvar', 'PLAT'), # undocumented - ('envvar', 'DIST_EXTRA_CONFIG'), # undocumented ('py:attr', 'CCompiler.language_map'), # undocumented ('py:attr', 'CCompiler.language_order'), # undocumented - ('py:class', 'distutils.dist.Distribution'), # undocumented - ('py:class', 'distutils.extension.Extension'), # undocumented ('py:class', 'BorlandCCompiler'), # undocumented ('py:class', 'CCompiler'), # undocumented ('py:class', 'CygwinCCompiler'), # undocumented + ('py:class', 'distutils.dist.Distribution'), # undocumented ('py:class', 'distutils.dist.DistributionMetadata'), # undocumented + ('py:class', 'distutils.extension.Extension'), # undocumented ('py:class', 'FileList'), # undocumented ('py:class', 'IShellLink'), # ref to MS docs ('py:class', 'MSVCCompiler'), # undocumented ('py:class', 'OptionDummy'), # undocumented + ('py:class', 'setuptools.dist.Distribution'), # undocumented ('py:class', 'UnixCCompiler'), # undocumented ('py:exc', 'CompileError'), # undocumented ('py:exc', 'DistutilsExecError'), # undocumented @@ -186,8 +187,7 @@ ('py:exc', 'PreprocessError'), # undocumented ('py:exc', 'setuptools.errors.PlatformError'), # sphinx cannot find it ('py:func', 'distutils.CCompiler.new_compiler'), # undocumented - # undocumented: - ('py:func', 'distutils.dist.DistributionMetadata.read_pkg_file'), + ('py:func', 'distutils.dist.DistributionMetadata.read_pkg_file'), # undocumented ('py:func', 'distutils.file_util._copy_file_contents'), # undocumented ('py:func', 'distutils.log.debug'), # undocumented ('py:func', 'distutils.spawn.find_executable'), # undocumented diff --git a/mypy.ini b/mypy.ini index b6f972769e..e0fa8e5c47 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,42 @@ [mypy] -ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# CI should test for all versions, local development gets hints for oldest supported +python_version = 3.8 +strict = False +warn_unused_ignores = True +# required to support namespace packages: https://github.com/python/mypy/issues/14057 explicit_package_bases = True +exclude = (?x)( + ^build/ + | ^.tox/ + | ^pkg_resources/tests/data/my-test-package-source/setup.py$ # Duplicate module name + | ^.+?/(_vendor|extern)/ # Vendored + | ^setuptools/_distutils/ # Vendored + | ^setuptools/config/_validate_pyproject/ # Auto-generated + ) +disable_error_code = + # TODO: Test environment is not yet properly configured to install all imported packages + # import-not-found, # This can be left commented out for local runs until we enforce running mypy in the CI + # TODO: Not all dependencies are typed. Namely: distutils._modified, wheel.wheelfile, and jaraco.* + import-untyped, + # Ignoring attr-defined because setuptools wraps a lot of distutils classes, adding new attributes, + # w/o updating all the attributes and return types from the base classes for type-checkers to understand + # Especially with setuptools.dist.command vs distutils.dist.command vs setuptools._distutils.dist.command + # *.extern modules that actually live in *._vendor will also cause attr-defined issues on import + attr-defined, + +# Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. +# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 +[mypy-pkg_resources.extern.*,setuptools.extern.*] +ignore_missing_imports = True + +[mypy-pkg_resources.tests.*,setuptools.tests.*] +disable_error_code = + # Tests include creating dynamic modules that won't exists statically before the test is run. + # Let's ignore all "import-not-found", as if an import really wasn't found, then the test would fail. + import-not-found, + # mmany untyped "jaraco" modules + import-untyped, + +# Mypy issue, this vendored module is already excluded! +[mypy-setuptools._vendor.packaging._manylinux] +disable_error_code = import-not-found diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 0883642080..bfbf619c85 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -9,6 +9,7 @@ import stat import distutils.dist import distutils.command.install_egg_info +from typing import List from unittest import mock @@ -32,7 +33,7 @@ def __call__(self): class TestZipProvider: - finalizers = [] + finalizers: List[EggRemover] = [] ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0) "A reference time for a file modification" diff --git a/setup.py b/setup.py index 075d7c405f..1a6074766a 100755 --- a/setup.py +++ b/setup.py @@ -88,5 +88,6 @@ def _restore_install_lib(self): if __name__ == '__main__': # allow setup.py to run from another directory - here and os.chdir(here) + # TODO: Use a proper conditonal statement here + here and os.chdir(here) # type: ignore[func-returns-value] dist = setuptools.setup(**setup_params) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 563ca1c4ba..7c88c7e19b 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -3,6 +3,7 @@ import functools import os import re +from typing import TYPE_CHECKING import _distutils_hack.override # noqa: F401 import distutils.core @@ -105,8 +106,11 @@ def setup(**attrs): setup.__doc__ = distutils.core.setup.__doc__ - -_Command = monkey.get_unpatched(distutils.core.Command) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Command = distutils.core.Command +else: + _Command = monkey.get_unpatched(distutils.core.Command) class Command(_Command): @@ -165,8 +169,9 @@ class Command(_Command): """ command_consumes_arguments = False + distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution - def __init__(self, dist, **kw): + def __init__(self, dist: Distribution, **kw): """ Construct the command for dist, updating vars(self) with any keyword parameters. diff --git a/setuptools/command/_requirestxt.py b/setuptools/command/_requirestxt.py index 7b732b11ab..b0c2d7059a 100644 --- a/setuptools/command/_requirestxt.py +++ b/setuptools/command/_requirestxt.py @@ -35,7 +35,7 @@ def _prepare( def _convert_extras_requirements( - extras_require: _StrOrIter, + extras_require: Mapping[str, _StrOrIter], ) -> Mapping[str, _Ordered[Requirement]]: """ Convert requirements in `extras_require` of the form diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 780afe3aec..1301433a32 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -26,7 +26,9 @@ # make sure _config_vars is initialized get_config_var("LDSHARED") -from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa +# Not publicly exposed in typeshed distutils stubs, but this is done on purpose +# See https://github.com/pypa/setuptools/pull/4228#issuecomment-1959856400 +from distutils.sysconfig import _config_vars as _CONFIG_VARS # type: ignore # noqa def _customize_compiler_for_shlib(compiler): @@ -58,7 +60,7 @@ def _customize_compiler_for_shlib(compiler): use_stubs = True elif os.name != 'nt': try: - import dl + import dl # type: ignore[import-not-found] # https://github.com/python/mypy/issues/13002 use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW') except ImportError: @@ -378,7 +380,10 @@ def _compile_and_remove_stub(self, stub_file: str): optimize = self.get_finalized_command('install_lib').optimize if optimize > 0: byte_compile( - [stub_file], optimize=optimize, force=True, dry_run=self.dry_run + [stub_file], + optimize=optimize, + force=True, + dry_run=self.dry_run, ) if os.path.exists(stub_file) and not self.dry_run: os.unlink(stub_file) diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index f5061afaaf..52c0721903 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -9,8 +9,10 @@ from distutils import log from distutils.core import Command from pathlib import Path +from typing import cast from .. import _normalization +from .egg_info import egg_info as egg_info_cls class dist_info(Command): @@ -50,7 +52,7 @@ def finalize_options(self): project_dir = dist.src_root or os.curdir self.output_dir = Path(self.output_dir or project_dir) - egg_info = self.reinitialize_command("egg_info") + egg_info = cast(egg_info_cls, self.reinitialize_command("egg_info")) egg_info.egg_base = str(self.output_dir) if self.tag_date: diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index cc0c409123..402355bd81 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -25,6 +25,7 @@ from distutils.command import install import sys import os +from typing import Dict, List import zipimport import shutil import tempfile @@ -43,7 +44,6 @@ import configparser import sysconfig - from sysconfig import get_path from setuptools import Command @@ -1765,7 +1765,7 @@ def _wrap_lines(cls, lines): if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite': - PthDistributions = RewritePthDistributions + PthDistributions = RewritePthDistributions # type: ignore[misc] # Overwriting type def _first_line_re(): @@ -2015,7 +2015,7 @@ def is_python_script(script_text, filename): from os import chmod as _chmod except ImportError: # Jython compatibility - def _chmod(*args): + def _chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy re-uses the imported definition anyway pass @@ -2033,8 +2033,8 @@ class CommandSpec(list): those passed to Popen. """ - options = [] - split_args = dict() + options: List[str] = [] + split_args: Dict[str, bool] = dict() @classmethod def best(cls): diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index a81fcd5bf9..24980edcf4 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -33,6 +33,7 @@ Protocol, Tuple, TypeVar, + cast, ) from .. import ( @@ -50,7 +51,12 @@ SetuptoolsDeprecationWarning, SetuptoolsWarning, ) +from .build import build as build_cls from .build_py import build_py as build_py_cls +from .dist_info import dist_info as dist_info_cls +from .egg_info import egg_info as egg_info_cls +from .install import install as install_cls +from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: from wheel.wheelfile import WheelFile # noqa @@ -155,7 +161,7 @@ def run(self): def _ensure_dist_info(self): if self.dist_info_dir is None: - dist_info = self.reinitialize_command("dist_info") + dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info")) dist_info.output_dir = self.dist_dir dist_info.ensure_finalized() dist_info.run() @@ -202,12 +208,18 @@ def _configure_build( scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts")) # egg-info may be generated again to create a manifest (used for package data) - egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True) + egg_info = cast( + egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True) + ) egg_info.egg_base = str(tmp_dir) egg_info.ignore_egg_info_in_manifest = True - build = dist.reinitialize_command("build", reinit_subcommands=True) - install = dist.reinitialize_command("install", reinit_subcommands=True) + build = cast( + build_cls, dist.reinitialize_command("build", reinit_subcommands=True) + ) + install = cast( + install_cls, dist.reinitialize_command("install", reinit_subcommands=True) + ) build.build_platlib = build.build_purelib = build.build_lib = build_lib install.install_purelib = install.install_platlib = install.install_lib = wheel @@ -215,12 +227,14 @@ def _configure_build( install.install_headers = headers install.install_data = data - install_scripts = dist.get_command_obj("install_scripts") + install_scripts = cast( + install_scripts_cls, dist.get_command_obj("install_scripts") + ) install_scripts.no_ep = True build.build_temp = str(tmp_dir) - build_py = dist.get_command_obj("build_py") + build_py = cast(build_py_cls, dist.get_command_obj("build_py")) build_py.compile = False build_py.existing_egg_info_dir = self._find_egg_info_dir() @@ -233,6 +247,7 @@ def _set_editable_mode(self): """Set the ``editable_mode`` flag in the build sub-commands""" dist = self.distribution build = dist.get_command_obj("build") + # TODO: Update typeshed distutils stubs to overload non-None return type by default for cmd_name in build.get_sub_commands(): cmd = dist.get_command_obj(cmd_name) if hasattr(cmd, "editable_mode"): @@ -269,7 +284,7 @@ def _run_build_commands( self._run_install("data") return files, mapping - def _run_build_subcommands(self): + def _run_build_subcommands(self) -> None: """ Issue #3501 indicates that some plugins/customizations might rely on: @@ -283,7 +298,7 @@ def _run_build_subcommands(self): # TODO: Once plugins/customisations had the chance to catch up, replace # `self._run_build_subcommands()` with `self.run_command("build")`. # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023. - build: Command = self.get_finalized_command("build") + build = self.get_finalized_command("build") for name in build.get_sub_commands(): cmd = self.get_finalized_command(name) if name == "build_py" and type(cmd) != build_py_cls: @@ -432,7 +447,8 @@ def __init__( ): self.auxiliary_dir = Path(auxiliary_dir) self.build_lib = Path(build_lib).resolve() - self._file = dist.get_command_obj("build_py").copy_file + # TODO: Update typeshed distutils stubs to overload non-None return type by default + self._file = dist.get_command_obj("build_py").copy_file # type: ignore[union-attr] super().__init__(dist, name, [self.auxiliary_dir]) def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]): @@ -450,7 +466,9 @@ def _create_file(self, relative_output: str, src_file: str, link=None): dest = self.auxiliary_dir / relative_output if not dest.parent.is_dir(): dest.parent.mkdir(parents=True) - self._file(src_file, dest, link=link) + # TODO: Update typeshed distutils stubs so distutils.cmd.Command.copy_file, accepts PathLike + # same with methods used by copy_file + self._file(src_file, dest, link=link) # type: ignore[arg-type] def _create_links(self, outputs, output_mapping): self.auxiliary_dir.mkdir(parents=True, exist_ok=True) @@ -603,7 +621,8 @@ 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 ({}, {""}) - parent = os.path.commonpath(starmap(_parent_path, layout.items())) + # 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] return all( _path.same_path(Path(parent, *key.split('.')), value) for key, value in layout.items() diff --git a/setuptools/command/install.py b/setuptools/command/install.py index b97a9b4713..56c1155b50 100644 --- a/setuptools/command/install.py +++ b/setuptools/command/install.py @@ -3,9 +3,11 @@ import glob import platform import distutils.command.install as orig +from typing import cast import setuptools from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning +from .bdist_egg import bdist_egg as bdist_egg_cls # Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for # now. See https://github.com/pypa/setuptools/issues/199/ @@ -135,7 +137,8 @@ def do_egg_install(self): cmd.package_index.scan(glob.glob('*.egg')) self.run_command('bdist_egg') - args = [self.distribution.get_command_obj('bdist_egg').egg_output] + bdist_egg = cast(bdist_egg_cls, self.distribution.get_command_obj('bdist_egg')) + args = [bdist_egg.egg_output] if setuptools.bootstrap_install_from: # Bootstrap self-installation of setuptools diff --git a/setuptools/command/rotate.py b/setuptools/command/rotate.py index cfb78ce52d..6f73721c70 100644 --- a/setuptools/command/rotate.py +++ b/setuptools/command/rotate.py @@ -3,6 +3,7 @@ from distutils.errors import DistutilsOptionError import os import shutil +from typing import List from setuptools import Command @@ -17,7 +18,7 @@ class rotate(Command): ('keep=', 'k', "number of matching distributions to keep"), ] - boolean_options = [] + boolean_options: List[str] = [] def initialize_options(self): self.match = None diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 32c9abd796..3fbbb62553 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -50,7 +50,7 @@ def has_sphinx(self): and metadata.entry_points(group='distutils.commands', name='build_sphinx') ) - sub_commands = [('build_sphinx', has_sphinx)] + sub_commands = [('build_sphinx', has_sphinx)] # type: ignore[list-item] # TODO: Fix in typeshed distutils stubs def initialize_options(self): upload.initialize_options(self) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 7301bc65c1..3626282a79 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -34,6 +34,7 @@ from ..warnings import SetuptoolsWarning if TYPE_CHECKING: + from distutils.dist import _OptionsList from setuptools._importlib import metadata # noqa from setuptools.dist import Distribution # noqa @@ -293,7 +294,7 @@ def _normalise_cmd_option_key(name: str) -> str: return json_compatible_key(name).strip("_=") -def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[str]: +def _normalise_cmd_options(desc: "_OptionsList") -> Set[str]: return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc} diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index e23a762cf5..0d8d58add8 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -63,7 +63,7 @@ class StaticModule: """Proxy to a module object that avoids executing arbitrary code.""" def __init__(self, name: str, spec: ModuleSpec): - module = ast.parse(pathlib.Path(spec.origin).read_bytes()) + module = ast.parse(pathlib.Path(spec.origin).read_bytes()) # type: ignore[arg-type] # Let it raise an error on None vars(self).update(locals()) del self.self diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 5eb9421f1f..ff97679895 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from setuptools.dist import Distribution # noqa + from typing_extensions import Self _logger = logging.getLogger(__name__) @@ -271,7 +272,7 @@ def _ensure_previously_set(self, dist: "Distribution", field: str): def _expand_directive( self, specifier: str, directive, package_dir: Mapping[str, str] ): - from setuptools.extern.more_itertools import always_iterable # type: ignore + from setuptools.extern.more_itertools import always_iterable with _ignore_errors(self.ignore_option_errors): root_dir = self.root_dir @@ -401,7 +402,7 @@ def __init__( self._project_cfg = project_cfg self._setuptools_cfg = setuptools_cfg - def __enter__(self): + def __enter__(self) -> "Self": """When entering the context, the values of ``packages``, ``py_modules`` and ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``. """ diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index cfa43a57b5..2912d3e143 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -108,7 +108,7 @@ def _apply( filenames = [*other_files, filepath] try: - _Distribution.parse_config_files(dist, filenames=filenames) + _Distribution.parse_config_files(dist, filenames=filenames) # type: ignore[arg-type] # TODO: fix in disutils stubs handlers = parse_configuration( dist, dist.command_options, ignore_option_errors=ignore_option_errors ) @@ -475,7 +475,7 @@ def parse_section(self, section_options): # Keep silent for a new option may appear anytime. self[name] = value - def parse(self): + def parse(self) -> None: """Parses configuration file items from one or more related sections. diff --git a/setuptools/dist.py b/setuptools/dist.py index d5787ed474..6350e38100 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -10,7 +10,7 @@ from contextlib import suppress from glob import iglob from pathlib import Path -from typing import List, Optional, Set +from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Set, Tuple import distutils.cmd import distutils.command @@ -202,7 +202,11 @@ def check_packages(dist, attr, value): ) -_Distribution = get_unpatched(distutils.core.Distribution) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Distribution = distutils.core.Distribution +else: + _Distribution = get_unpatched(distutils.core.Distribution) class Distribution(_Distribution): @@ -283,12 +287,12 @@ def patch_missing_pkg_info(self, attrs): dist._version = _normalization.safe_version(str(attrs['version'])) self._patched_dist = dist - def __init__(self, attrs=None): + def __init__(self, attrs: Optional[MutableMapping] = None) -> None: have_package_data = hasattr(self, "package_data") if not have_package_data: - self.package_data = {} + self.package_data: Dict[str, List[str]] = {} attrs = attrs or {} - self.dist_files = [] + self.dist_files: List[Tuple[str, str, str]] = [] # Filter-out setuptools' specific options. self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) @@ -381,7 +385,7 @@ def _normalize_requires(self): k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() } - def _finalize_license_files(self): + def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" license_files: Optional[List[str]] = self.metadata.license_files patterns: List[str] = license_files if license_files else [] @@ -394,7 +398,7 @@ def _finalize_license_files(self): # Default patterns match the ones wheel uses # See https://wheel.readthedocs.io/en/stable/user_guide.html # -> 'Including license files in the generated wheel file' - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] self.metadata.license_files = list( unique_everseen(self._expand_patterns(patterns)) diff --git a/setuptools/extension.py b/setuptools/extension.py index 58c023f6b4..8caad78d4b 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -3,6 +3,7 @@ import distutils.core import distutils.errors import distutils.extension +from typing import TYPE_CHECKING from .monkey import get_unpatched @@ -23,8 +24,11 @@ def _have_cython(): # for compatibility have_pyrex = _have_cython - -_Extension = get_unpatched(distutils.core.Extension) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Extension = distutils.core.Extension +else: + _Extension = get_unpatched(distutils.core.Extension) class Extension(_Extension): diff --git a/setuptools/monkey.py b/setuptools/monkey.py index da0993506c..fd07d91dec 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -8,11 +8,14 @@ import sys import types from importlib import import_module +from typing import List, TypeVar import distutils.filelist -__all__ = [] +_T = TypeVar("_T") + +__all__: List[str] = [] """ Everything is private. Contact the project team if you think you need this functionality. @@ -33,7 +36,7 @@ def _get_mro(cls): return inspect.getmro(cls) -def get_unpatched(item): +def get_unpatched(item: _T) -> _T: lookup = ( get_unpatched_class if isinstance(item, type) diff --git a/setuptools/msvc.py b/setuptools/msvc.py index 53fe7b0de1..b2a0f2bebb 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -20,9 +20,11 @@ import itertools import subprocess import distutils.errors +from typing import Dict, TYPE_CHECKING from setuptools.extern.more_itertools import unique_everseen -if platform.system() == 'Windows': +# https://github.com/python/mypy/issues/8166 +if not TYPE_CHECKING and platform.system() == 'Windows': import winreg from os import environ else: @@ -34,7 +36,7 @@ class winreg: HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None - environ = dict() + environ: Dict[str, str] = dict() def _msvc14_find_vc2015(): diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 7634b1320b..6c095e029e 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -9,6 +9,7 @@ import pickle import textwrap import builtins +from typing import Union, List import pkg_resources from distutils.errors import DistutilsError @@ -19,7 +20,7 @@ else: _os = sys.modules[os.name] try: - _file = file + _file = file # type: ignore[name-defined] # Check for global variable except NameError: _file = None _open = open @@ -298,7 +299,7 @@ def run(self, func): with self: return func() - def _mk_dual_path_wrapper(name): + def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, src, dst, *args, **kw): @@ -312,7 +313,7 @@ def wrap(self, src, dst, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_dual_path_wrapper(name) - def _mk_single_path_wrapper(name, original=None): + 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) def wrap(self, path, *args, **kw): @@ -349,7 +350,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_path_wrapper(name) - def _mk_single_with_return(name): + def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, path, *args, **kw): @@ -364,7 +365,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_with_return(name) - def _mk_query(name): + def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, *args, **kw): @@ -424,7 +425,7 @@ class DirectorySandbox(AbstractSandbox): "tempnam", ]) - _exception_patterns = [] + _exception_patterns: List[Union[str, re.Pattern]] = [] "exempt writing to paths that match the pattern" def __init__(self, sandbox, exceptions=_EXCEPTIONS): diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index e63a89b0b4..555489b140 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,6 +9,7 @@ import tarfile from inspect import cleandoc from pathlib import Path +from typing import Tuple from unittest.mock import Mock from zipfile import ZipFile @@ -455,7 +456,7 @@ def core_metadata(dist) -> str: # Make sure core metadata is valid Metadata.from_email(pkg_file_txt, validate=True) # can raise exceptions - skip_prefixes = () + skip_prefixes: Tuple[str, ...] = () skip_lines = set() # ---- DIFF NORMALISATION ---- # PEP 621 is very particular about author/maintainer metadata conversion, so skip diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 3467a5ec07..17bf2af9d2 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -1,3 +1,5 @@ +# https://github.com/python/mypy/issues/16936 +# mypy: disable-error-code="has-type" """Integration tests for setuptools that focus on building packages via pip. The idea behind these tests is not to exhaustively check all the possible @@ -25,10 +27,10 @@ from .helpers import Archive, run - pytestmark = pytest.mark.integration -(LATEST,) = Enum("v", "LATEST") + +(LATEST,) = Enum("v", "LATEST") # type: ignore[misc] # https://github.com/python/mypy/issues/16936 """Default version to be checked""" # There are positive and negative aspects of checking the latest version of the # packages. diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index 0e473d168b..12ed4d328c 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -47,7 +47,7 @@ def test_bdist_egg(self, setup_context, user_override): assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content) @pytest.mark.xfail( - os.environ.get('PYTHONDONTWRITEBYTECODE'), + os.environ.get('PYTHONDONTWRITEBYTECODE', False), reason="Byte code disabled", ) def test_exclude_source_files(self, setup_context, user_override): diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 862f8172cd..df85699586 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -120,7 +120,7 @@ def editable_opts(request): @pytest.mark.parametrize( "files", [ - {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, # type: ignore + {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, EXAMPLE, # No setup.py script ], ) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index af7d2f8295..ba019dc79d 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -79,7 +79,8 @@ def run(): @staticmethod def _extract_mv_version(pkg_info_lines: List[str]) -> Tuple[int, int]: version_str = pkg_info_lines[0].split(' ')[1] - return tuple(map(int, version_str.split('.')[:2])) + major, minor = map(int, version_str.split('.')[:2]) + return major, minor def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): """ diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index fbd21b1976..16fa2c2460 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -10,6 +10,7 @@ import logging from distutils import log from distutils.errors import DistutilsTemplateError +from typing import List, Tuple from setuptools.command.egg_info import FileList, egg_info, translate_pattern from setuptools.dist import Distribution @@ -75,7 +76,7 @@ def touch(filename): ) -translate_specs = [ +translate_specs: List[Tuple[str, List[str], List[str]]] = [ ('foo', ['foo'], ['bar', 'foobar']), ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), # Glob matching From 66dfd2822665a60d88935933816de9e5261b012d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 5 Mar 2024 12:02:15 +0000 Subject: [PATCH 084/184] Add link to `.gitignore` guidelines --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 311c9b269e..f25d073e24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # syntax: glob +# See https://blog.jaraco.com/skeleton/#ignoring-artifacts before modifying. bin build dist From 15f7ef76fdfc6e9993fe10b1a6e9b77f7676bc3a Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 5 Mar 2024 18:43:37 -0500 Subject: [PATCH 085/184] Apply suggestions from code review Co-authored-by: Anderson Bravalheri --- pkg_resources/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index bff48db73a..907a16da30 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -567,7 +567,7 @@ def get_resource_stream(self, manager, resource_name): `manager` must be an ``IResourceManager``""" def get_resource_string(self, manager, resource_name) -> bytes: - """Return a bytes string containing the contents of `resource_name` + """Return the contents of `resource_name` as :obj:`bytes` `manager` must be an ``IResourceManager``""" @@ -1204,7 +1204,7 @@ def resource_stream(self, package_or_requirement, resource_name): ) def resource_string(self, package_or_requirement, resource_name) -> bytes: - """Return specified resource as a bytes string""" + """Return specified resource as :obj:`bytes`""" return get_provider(package_or_requirement).get_resource_string( self, resource_name ) From a33c387d2c475ad2481a30e58b759c34297c4b84 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 6 Mar 2024 11:28:43 -0500 Subject: [PATCH 086/184] Enable mypy on CI --- mypy.ini | 37 +++++++------------ pyproject.toml | 3 -- setup.cfg | 4 ++ setuptools/command/build_ext.py | 2 +- setuptools/command/editable_wheel.py | 2 +- .../fastjsonschema_validations.py | 1 - 6 files changed, 20 insertions(+), 29 deletions(-) diff --git a/mypy.ini b/mypy.ini index e0fa8e5c47..42ade6537e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -13,30 +13,21 @@ exclude = (?x)( | ^setuptools/_distutils/ # Vendored | ^setuptools/config/_validate_pyproject/ # Auto-generated ) -disable_error_code = - # TODO: Test environment is not yet properly configured to install all imported packages - # import-not-found, # This can be left commented out for local runs until we enforce running mypy in the CI - # TODO: Not all dependencies are typed. Namely: distutils._modified, wheel.wheelfile, and jaraco.* - import-untyped, - # Ignoring attr-defined because setuptools wraps a lot of distutils classes, adding new attributes, - # w/o updating all the attributes and return types from the base classes for type-checkers to understand - # Especially with setuptools.dist.command vs distutils.dist.command vs setuptools._distutils.dist.command - # *.extern modules that actually live in *._vendor will also cause attr-defined issues on import - attr-defined, +# Ignoring attr-defined because setuptools wraps a lot of distutils classes, adding new attributes, +# w/o updating all the attributes and return types from the base classes for type-checkers to understand +# Especially with setuptools.dist.command vs distutils.dist.command vs setuptools._distutils.dist.command +# *.extern modules that actually live in *._vendor will also cause attr-defined issues on import +disable_error_code = attr-defined -# Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. -# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 -[mypy-pkg_resources.extern.*,setuptools.extern.*] +# - Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. +# 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.*] ignore_missing_imports = True -[mypy-pkg_resources.tests.*,setuptools.tests.*] -disable_error_code = - # Tests include creating dynamic modules that won't exists statically before the test is run. - # Let's ignore all "import-not-found", as if an import really wasn't found, then the test would fail. - import-not-found, - # mmany untyped "jaraco" modules - import-untyped, - -# Mypy issue, this vendored module is already excluded! -[mypy-setuptools._vendor.packaging._manylinux] +# - 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] disable_error_code = import-not-found diff --git a/pyproject.toml b/pyproject.toml index cd95aad07f..58aacd9fe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,3 @@ build-backend = "setuptools.build_meta" backend-path = ["."] [tool.setuptools_scm] - -[tool.pytest-enabler.mypy] -# disabled diff --git a/setup.cfg b/setup.cfg index 91fb67724c..4d1155e884 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,10 @@ testing = # for tools/finalize.py jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin" pytest-home >= 0.5 + # 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 + importlib_metadata testing-integration = pytest diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 1301433a32..b5c98c86dc 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -16,7 +16,7 @@ try: # Attempt to use Cython for building extensions, if available - from Cython.Distutils.build_ext import build_ext as _build_ext + from Cython.Distutils.build_ext import build_ext as _build_ext # type: ignore[import-not-found] # Cython not installed on CI tests # Additionally, assert that the compiler module will load # also. Ref #1229. diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 24980edcf4..9d319398c9 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -59,7 +59,7 @@ from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: - from wheel.wheelfile import WheelFile # noqa + from wheel.wheelfile import WheelFile # type:ignore[import-untyped] # noqa _P = TypeVar("_P", bound=StrPath) _logger = logging.getLogger(__name__) diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py index b81d13c119..8b852bbfd4 100644 --- a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py +++ b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py @@ -1,5 +1,4 @@ # noqa -# type: ignore # flake8: noqa # pylint: skip-file # mypy: ignore-errors From ba0917602dacc86fd725cecd2182abc4837a30e8 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 6 Mar 2024 12:31:31 -0500 Subject: [PATCH 087/184] `pkg_resources`: Remove unused and shadowed variables --- newsfragments/4260.misc.rst | 1 + pkg_resources/__init__.py | 15 --------------- 2 files changed, 1 insertion(+), 15 deletions(-) create mode 100644 newsfragments/4260.misc.rst diff --git a/newsfragments/4260.misc.rst b/newsfragments/4260.misc.rst new file mode 100644 index 0000000000..9dfde3498d --- /dev/null +++ b/newsfragments/4260.misc.rst @@ -0,0 +1 @@ +Remove unused ``resources_stream`` ``resource_dir`` and shadowed functions from `pkg_resources` -- by :user:`Avasam` diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index d0b9b5469a..163a5521d6 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -85,9 +85,7 @@ require = None working_set = None add_activation_listener = None -resources_stream = None cleanup_resources = None -resource_dir = None resource_stream = None set_extraction_path = None resource_isdir = None @@ -491,19 +489,6 @@ def compatible_platforms(provided, required): return False -def run_script(dist_spec, script_name): - """Locate distribution `dist_spec` and run its `script_name` script""" - ns = sys._getframe(1).f_globals - name = ns['__name__'] - ns.clear() - ns['__name__'] = name - require(dist_spec)[0].run_script(script_name, ns) - - -# backward compatibility -run_main = run_script - - def get_distribution(dist): """Return a current distribution object for a Requirement or string""" if isinstance(dist, str): From 50221262fe31326c8fa6458e15316a69f3ce40ac Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 15:27:28 +0000 Subject: [PATCH 088/184] Add missing ``encoding`` to setuptools tests --- setuptools/tests/config/test_pyprojecttoml.py | 2 +- setuptools/tests/test_build_meta.py | 8 ++-- setuptools/tests/test_build_py.py | 10 ++--- setuptools/tests/test_config_discovery.py | 2 +- setuptools/tests/test_easy_install.py | 16 ++++---- setuptools/tests/test_egg_info.py | 38 +++++++++---------- setuptools/tests/test_find_packages.py | 3 +- setuptools/tests/test_install_scripts.py | 8 ++-- setuptools/tests/test_manifest.py | 8 ++-- setuptools/tests/test_sandbox.py | 4 +- setuptools/tests/test_sdist.py | 22 +++++------ 11 files changed, 59 insertions(+), 62 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 6a40f3bfd7..dee2ecc7ab 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -174,7 +174,7 @@ class TestEntryPoints: def write_entry_points(self, tmp_path): entry_points = ConfigParser() entry_points.read_dict(ENTRY_POINTS) - with open(tmp_path / "entry-points.txt", "w") as f: + with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f: entry_points.write(f) def pyproject(self, dynamic=None): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 9332781764..32676aee79 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -296,7 +296,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): first_result = build_method(dist_dir) # Change version. - with open("VERSION", "wt") as version_file: + with open("VERSION", "wt", encoding="utf-8") as version_file: version_file.write("0.0.2") # Build a *second* sdist/wheel. @@ -306,7 +306,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): assert first_result != second_result # And if rebuilding the exact same sdist/wheel? - open(os.path.join(dist_dir, second_result), 'w').close() + open(os.path.join(dist_dir, second_result), 'w', encoding="utf-8").close() third_result = build_method(dist_dir) assert third_result == second_result assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0 @@ -568,9 +568,9 @@ def test_build_sdist_version_change(self, build_backend): if not os.path.exists(setup_loc): setup_loc = os.path.abspath("setup.cfg") - with open(setup_loc, 'rt') as file_handler: + with open(setup_loc, 'rt', encoding="utf-8") as file_handler: content = file_handler.read() - with open(setup_loc, 'wt') as file_handler: + with open(setup_loc, 'wt', encoding="utf-8") as file_handler: file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'")) shutil.rmtree(sdist_into_directory) diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index 500a9ab6f3..d79cbbdf0c 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -49,7 +49,7 @@ def test_recursive_in_package_data_glob(tmpdir_cwd): ) ) os.makedirs('path/subpath/subsubpath') - open('path/subpath/subsubpath/data', 'w').close() + open('path/subpath/subsubpath/data', 'w', encoding="utf-8").close() dist.parse_command_line() dist.run_commands() @@ -77,8 +77,8 @@ def test_read_only(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w').close() - open('pkg/data.dat', 'w').close() + open('pkg/__init__.py', 'w', encoding="utf-8").close() + open('pkg/data.dat', 'w', encoding="utf-8").close() os.chmod('pkg/__init__.py', stat.S_IREAD) os.chmod('pkg/data.dat', stat.S_IREAD) dist.parse_command_line() @@ -108,8 +108,8 @@ def test_executable_data(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w').close() - open('pkg/run-me', 'w').close() + open('pkg/__init__.py', 'w', encoding="utf-8").close() + open('pkg/run-me', 'w', encoding="utf-8").close() os.chmod('pkg/run-me', 0o700) dist.parse_command_line() diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index ef2979d4f5..409c219ed5 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -591,7 +591,7 @@ def _write_setupcfg(root, options): setupcfg["options"][key] = "\n" + str_value else: setupcfg["options"][key] = str(value) - with open(root / "setup.cfg", "w") as f: + with open(root / "setup.cfg", "w", encoding="utf-8") as f: setupcfg.write(f) print("~~~~~ setup.cfg ~~~~~") print((root / "setup.cfg").read_text()) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index a001ae27c5..26778e65ec 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -467,7 +467,7 @@ def distutils_package(): 'from distutils.core import setup', ) with contexts.tempdir(cd=os.chdir): - with open('setup.py', 'w') as f: + with open('setup.py', 'w', encoding="utf-8") as f: f.write(distutils_setup_py) yield @@ -784,7 +784,7 @@ def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): setup_attrs=dict(dependency_links=[]), ) test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') - with open(test_setup_cfg, 'w') as fp: + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -918,7 +918,7 @@ def test_setup_requires_with_find_links_in_setup_cfg( test_setup_py = os.path.join(test_pkg, 'setup.py') test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: if with_dependency_links_in_setup_py: dependency_links = [os.path.join(temp_dir, 'links')] else: @@ -932,7 +932,7 @@ def test_setup_requires_with_find_links_in_setup_cfg( """ ).format(dependency_links=dependency_links) ) - with open(test_setup_cfg, 'w') as fp: + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -984,7 +984,7 @@ def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch): test_pkg = os.path.join(temp_dir, 'test_pkg') test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -1068,7 +1068,7 @@ class epcmd(build_py): test_pkg = os.path.join(temp_dir, 'test_pkg') test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -1244,7 +1244,7 @@ def create_setup_requires_package( ) else: test_setup_cfg_contents = '' - with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f: + with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f: f.write(test_setup_cfg_contents) # setup.py @@ -1255,7 +1255,7 @@ def create_setup_requires_package( setuptools.setup(**%r) """ ) - with open(os.path.join(test_pkg, 'setup.py'), 'w') as f: + with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f: f.write(setup_py_template % test_setup_attrs) foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version)) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index ba019dc79d..215cb096fc 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -94,7 +94,7 @@ def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): ei.initialize_options() ei.save_version_info(setup_cfg) - with open(setup_cfg, 'r') as f: + with open(setup_cfg, 'r', encoding="utf-8") as f: content = f.read() assert '[egg_info]' in content @@ -139,7 +139,7 @@ def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): ei.initialize_options() ei.save_version_info(setup_cfg) - with open(setup_cfg, 'r') as f: + with open(setup_cfg, 'r', encoding="utf-8") as f: content = f.read() assert '[egg_info]' in content @@ -251,7 +251,7 @@ def test_manifest_template_is_read(self, tmpdir_cwd, env): self._run_egg_info_command(tmpdir_cwd, env) egg_info_dir = os.path.join('.', 'foo.egg-info') sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') - with open(sources_txt) as f: + with open(sources_txt, encoding="utf-8") as f: assert 'docs/usage.rst' in f.read().split('\n') def _setup_script_with_requires(self, requires, use_setup_cfg=False): @@ -492,7 +492,7 @@ def test_requires( egg_info_dir = os.path.join('.', 'foo.egg-info') requires_txt = os.path.join(egg_info_dir, 'requires.txt') if os.path.exists(requires_txt): - with open(requires_txt) as fp: + with open(requires_txt, encoding="utf-8") as fp: install_requires = fp.read() else: install_requires = '' @@ -538,7 +538,7 @@ def test_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'Provides-Extra: foobar' in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines @@ -557,7 +557,7 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_text = pkginfo_file.read() assert 'Provides-Extra:' not in pkg_info_text @@ -636,7 +636,7 @@ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: sources_text = sources_file.read() if license_in_sources: @@ -849,7 +849,7 @@ def test_setup_cfg_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: sources_lines = list(line.strip() for line in sources_file) for lf in incl_licenses: @@ -1033,7 +1033,7 @@ def test_setup_cfg_license_file_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: sources_lines = list(line.strip() for line in sources_file) for lf in incl_licenses: @@ -1065,7 +1065,7 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') license_file_lines = [ line for line in pkg_info_lines if line.startswith('License-File:') @@ -1086,7 +1086,7 @@ def test_metadata_version(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') # Update metadata version if changed assert self._extract_mv_version(pkg_info_lines) == (2, 1) @@ -1112,7 +1112,7 @@ def test_long_description_content_type(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') expected_line = 'Description-Content-Type: text/markdown' assert expected_line in pkg_info_lines @@ -1133,7 +1133,7 @@ def test_long_description(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'Metadata-Version: 2.1' in pkg_info_lines assert '' == pkg_info_lines[-1] # last line should be empty @@ -1165,7 +1165,7 @@ def test_project_urls(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') expected_line = 'Project-URL: Link One, https://example.com/one/' assert expected_line in pkg_info_lines @@ -1182,7 +1182,7 @@ def test_license(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'License: MIT' in pkg_info_lines @@ -1197,7 +1197,7 @@ def test_license_escape(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'License: This is a long license text ' in pkg_info_lines @@ -1216,7 +1216,7 @@ def test_python_requires_egg_info(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'Requires-Python: >=2.7.12' in pkg_info_lines assert self._extract_mv_version(pkg_info_lines) >= (1, 2) @@ -1240,7 +1240,7 @@ def test_egg_info_includes_setup_py(self, tmpdir_cwd): assert 'setup.py' in egg_info_instance.filelist.files - with open(egg_info_instance.egg_info + "/SOURCES.txt") as f: + with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f: sources = f.read().split('\n') assert 'setup.py' in sources @@ -1277,7 +1277,7 @@ def test_egg_info_tag_only_once(self, tmpdir_cwd, env): }) self._run_egg_info_command(tmpdir_cwd, env) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: pkg_info_lines = pkginfo_file.read().split('\n') assert 'Version: 0.0.0.dev0' in pkg_info_lines diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index cb1900df3d..4fefd3dccf 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -72,8 +72,7 @@ def _mkdir(self, path, parent_dir=None): def _touch(self, path, dir_=None): if dir_: path = os.path.join(dir_, path) - fp = open(path, 'w') - fp.close() + open(path, 'wb').close() return path def test_regular_package(self): diff --git a/setuptools/tests/test_install_scripts.py b/setuptools/tests/test_install_scripts.py index a783459730..595b7ade67 100644 --- a/setuptools/tests/test_install_scripts.py +++ b/setuptools/tests/test_install_scripts.py @@ -41,7 +41,7 @@ def test_sys_executable_escaping_unix(self, tmpdir, monkeypatch): monkeypatch.setattr('sys.executable', self.unix_exe) with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir)) - with open(str(tmpdir.join('foo')), 'r') as f: + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -55,7 +55,7 @@ def test_sys_executable_escaping_win32(self, tmpdir, monkeypatch): monkeypatch.setattr('sys.executable', self.win32_exe) with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir)) - with open(str(tmpdir.join('foo-script.py')), 'r') as f: + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -69,7 +69,7 @@ def test_executable_with_spaces_escaping_unix(self, tmpdir): expected = '#!%s\n' % self.unix_spaces_exe with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir), self.unix_spaces_exe) - with open(str(tmpdir.join('foo')), 'r') as f: + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -83,6 +83,6 @@ def test_executable_arg_escaping_win32(self, tmpdir): expected = '#!"%s"\n' % self.win32_exe with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir), '"' + self.win32_exe + '"') - with open(str(tmpdir.join('foo-script.py')), 'r') as f: + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 16fa2c2460..6fb824f534 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -54,7 +54,7 @@ def quiet(): def touch(filename): - open(filename, 'w').close() + open(filename, 'wb').close() # The set of files always in the manifest, including all files in the @@ -174,7 +174,7 @@ class TestManifestTest(TempDirTestCase): def setup_method(self, method): super().setup_method(method) - f = open(os.path.join(self.temp_dir, 'setup.py'), 'w') + f = open(os.path.join(self.temp_dir, 'setup.py'), 'w', encoding="utf-8") f.write(SETUP_PY) f.close() """ @@ -212,7 +212,7 @@ def setup_method(self, method): def make_manifest(self, contents): """Write a MANIFEST.in.""" - with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w') as f: + with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w', encoding="utf-8") as f: f.write(DALS(contents)) def get_files(self): @@ -369,7 +369,7 @@ def make_files(self, files): file = os.path.join(self.temp_dir, file) dirname, basename = os.path.split(file) os.makedirs(dirname, exist_ok=True) - open(file, 'w').close() + touch(file) def test_process_template_line(self): # testing all MANIFEST.in template patterns diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index 9b4937e213..898661acf0 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -17,7 +17,7 @@ def test_devnull(self, tmpdir): @staticmethod def _file_writer(path): def do_write(): - with open(path, 'w') as f: + with open(path, 'w', encoding="utf-8") as f: f.write('xxx') return do_write @@ -114,7 +114,7 @@ def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir): def write_file(): "Trigger a SandboxViolation by writing outside the sandbox" - with open('/etc/foo', 'w'): + with open('/etc/foo', 'w', encoding="utf-8"): pass with pytest.raises(setuptools.sandbox.SandboxViolation) as caught: diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 5d597709ed..6eccb35826 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -116,10 +116,8 @@ def latin1_fail(): def touch(path): - if isinstance(path, str): - path = Path(path) - path.write_text('', encoding='utf-8') - return path + open(path, 'wb').close() + return Path(path) def symlink_or_skip_test(src, dst): @@ -386,7 +384,7 @@ def test_setup_py_missing(self): assert 'setup.py' not in manifest def test_setup_py_excluded(self): - with open("MANIFEST.in", "w") as manifest_file: + with open("MANIFEST.in", "w", encoding="utf-8") as manifest_file: manifest_file.write("exclude setup.py") dist = Distribution(SETUP_ATTRS) @@ -441,7 +439,7 @@ def test_manifest_is_written_with_utf8_encoding(self): filename = os.path.join('sdist_test', 'smörbröd.py') # Must create the file or it will get stripped. - open(filename, 'w').close() + touch(filename) # Add UTF-8 filename and write manifest with quiet(): @@ -469,7 +467,7 @@ def test_write_manifest_allows_utf8_filenames(self): filename = os.path.join(b'sdist_test', Filenames.utf_8) # Must touch the file or risk removal - open(filename, "w").close() + touch(filename) # Add filename and write manifest with quiet(): @@ -546,7 +544,7 @@ def test_manifest_is_read_with_utf8_encoding(self): manifest.close() # The file must exist to be included in the filelist - open(filename, 'w').close() + touch(filename) # Re-read manifest cmd.filelist.files = [] @@ -577,7 +575,7 @@ def test_read_manifest_skips_non_utf8_filenames(self): manifest.close() # The file must exist to be included in the filelist - open(filename, 'w').close() + touch(filename) # Re-read manifest cmd.filelist.files = [] @@ -598,7 +596,7 @@ def test_sdist_with_utf8_encoded_filename(self): cmd.ensure_finalized() filename = os.path.join(b'sdist_test', Filenames.utf_8) - open(filename, 'w').close() + touch(filename) with quiet(): cmd.run() @@ -639,7 +637,7 @@ def test_sdist_with_latin1_encoded_filename(self): # Latin-1 filename filename = os.path.join(b'sdist_test', Filenames.latin_1) - open(filename, 'w').close() + touch(filename) assert os.path.isfile(filename) with quiet(): @@ -736,7 +734,7 @@ def test_pyproject_toml_excluded(self, source_dir): Check that pyproject.toml can excluded even if present """ touch(source_dir / 'pyproject.toml') - with open('MANIFEST.in', 'w') as mts: + with open('MANIFEST.in', 'w', encoding="utf-8") as mts: print('exclude pyproject.toml', file=mts) dist = Distribution(SETUP_ATTRS) dist.script_name = 'setup.py' From dea08d77d908e5e8ad54ecf531dd78fc7f667fb2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 15:39:54 +0000 Subject: [PATCH 089/184] Fix formatting errors --- setuptools/tests/test_egg_info.py | 55 +++++++++++++++---------------- setuptools/tests/test_manifest.py | 3 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 215cb096fc..a4b0ecf398 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -538,8 +538,8 @@ def test_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Provides-Extra: foobar' in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines @@ -557,8 +557,8 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_text = pkginfo_file.read() + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_text = fp.read() assert 'Provides-Extra:' not in pkg_info_text @pytest.mark.parametrize( @@ -636,8 +636,7 @@ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: - sources_text = sources_file.read() + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") if license_in_sources: assert 'LICENSE' in sources_text @@ -849,8 +848,8 @@ def test_setup_cfg_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: - sources_lines = list(line.strip() for line in sources_file) + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] for lf in incl_licenses: assert sources_lines.count(lf) == 1 @@ -1033,8 +1032,8 @@ def test_setup_cfg_license_file_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt'), encoding="utf-8") as sources_file: - sources_lines = list(line.strip() for line in sources_file) + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] for lf in incl_licenses: assert sources_lines.count(lf) == 1 @@ -1065,8 +1064,8 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') license_file_lines = [ line for line in pkg_info_lines if line.startswith('License-File:') ] @@ -1086,8 +1085,8 @@ def test_metadata_version(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') # Update metadata version if changed assert self._extract_mv_version(pkg_info_lines) == (2, 1) @@ -1112,8 +1111,8 @@ def test_long_description_content_type(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') expected_line = 'Description-Content-Type: text/markdown' assert expected_line in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines @@ -1133,8 +1132,8 @@ def test_long_description(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Metadata-Version: 2.1' in pkg_info_lines assert '' == pkg_info_lines[-1] # last line should be empty long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :] @@ -1165,8 +1164,8 @@ def test_project_urls(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') expected_line = 'Project-URL: Link One, https://example.com/one/' assert expected_line in pkg_info_lines expected_line = 'Project-URL: Link Two, https://example.com/two/' @@ -1182,8 +1181,8 @@ def test_license(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'License: MIT' in pkg_info_lines def test_license_escape(self, tmpdir_cwd, env): @@ -1197,8 +1196,8 @@ def test_license_escape(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'License: This is a long license text ' in pkg_info_lines assert ' over multiple lines' in pkg_info_lines @@ -1216,8 +1215,8 @@ def test_python_requires_egg_info(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Requires-Python: >=2.7.12' in pkg_info_lines assert self._extract_mv_version(pkg_info_lines) >= (1, 2) @@ -1277,8 +1276,8 @@ def test_egg_info_tag_only_once(self, tmpdir_cwd, env): }) self._run_egg_info_command(tmpdir_cwd, env) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Version: 0.0.0.dev0' in pkg_info_lines diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 6fb824f534..6911b0224c 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -212,7 +212,8 @@ def setup_method(self, method): def make_manifest(self, contents): """Write a MANIFEST.in.""" - with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w', encoding="utf-8") as f: + manifest = os.path.join(self.temp_dir, 'MANIFEST.in') + with open(manifest, 'w', encoding="utf-8") as f: f.write(DALS(contents)) def get_files(self): From 96e1074df24400a11008ebe12cb575ebdec5c552 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 16:12:19 +0000 Subject: [PATCH 090/184] Adjust test_sandbox to encoding change --- setuptools/tests/test_sandbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index 898661acf0..f666615d99 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -126,8 +126,9 @@ def write_file(): cmd, args, kwargs = caught.value.args assert cmd == 'open' assert args == ('/etc/foo', 'w') - assert kwargs == {} + assert kwargs == {"encoding": "utf-8"} msg = str(caught.value) assert 'open' in msg assert "('/etc/foo', 'w')" in msg + assert "{'encoding': 'utf-8'}" in msg From f51351e10400970454901328f87b8d41aac1bd80 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 17:26:31 +0000 Subject: [PATCH 091/184] Fix `pathlib.Path` error --- setuptools/tests/test_sdist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 6eccb35826..387ec3bebf 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -10,7 +10,6 @@ import logging import distutils from inspect import cleandoc -from pathlib import Path from unittest import mock import pytest @@ -117,7 +116,7 @@ def latin1_fail(): def touch(path): open(path, 'wb').close() - return Path(path) + return path def symlink_or_skip_test(src, dst): From e1cd88c18900656030ac65bcfa5cb7214e4c03a5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:21:24 +0000 Subject: [PATCH 092/184] Rework test_pyprojecttoml.create_example --- setuptools/tests/config/test_pyprojecttoml.py | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index dee2ecc7ab..a661b7fc2f 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -5,6 +5,7 @@ import pytest import tomli_w from path import Path +from jaraco.path import build as path_build from setuptools.config.pyprojecttoml import ( read_configuration, @@ -82,25 +83,32 @@ def create_example(path, pkg_root): - pyproject = path / "pyproject.toml" + files = { + "pyproject.toml": EXAMPLE, + "README.md": "hello world", + "_files": { + "file.txt": "", + }, + } + packages = { + "pkg": { + "__init__.py": "", + "mod.py": "class CustomSdist: pass", + "__version__.py": "VERSION = (3, 10)", + "__main__.py": "def exec(): print('hello')", + }, + } + + assert pkg_root # Meta-test: cannot be empty string. - files = [ - f"{pkg_root}/pkg/__init__.py", - "_files/file.txt", - ] - if pkg_root != ".": # flat-layout will raise error for multi-package dist - # Ensure namespaces are discovered - files.append(f"{pkg_root}/other/nested/__init__.py") - - for file in files: - (path / file).parent.mkdir(exist_ok=True, parents=True) - (path / file).touch() - - pyproject.write_text(EXAMPLE) - (path / "README.md").write_text("hello world") - (path / f"{pkg_root}/pkg/mod.py").write_text("class CustomSdist: pass") - (path / f"{pkg_root}/pkg/__version__.py").write_text("VERSION = (3, 10)") - (path / f"{pkg_root}/pkg/__main__.py").write_text("def exec(): print('hello')") + if pkg_root == ".": + files = {**files, **packages} + # skip other files: flat-layout will raise error for multi-package dist + else: + # Use this opportunity to ensure namespaces are discovered + files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}} + + path_build(files, prefix=path) def verify_example(config, path, pkg_root): From 390f06a1629891aa1b4a495ba33f5300cf810f71 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:30:32 +0000 Subject: [PATCH 093/184] Add more explict encodings in test_pyprojecttoml --- setuptools/tests/config/test_pyprojecttoml.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index a661b7fc2f..b7a38f0a88 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -216,11 +216,13 @@ def test_dynamic(self, tmp_path): # Let's create a project example that has dynamic classifiers # coming from a txt file. create_example(tmp_path, "src") - classifiers = """\ - Framework :: Flask - Programming Language :: Haskell - """ - (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers)) + classifiers = cleandoc( + """ + Framework :: Flask + Programming Language :: Haskell + """ + ) + (tmp_path / "classifiers.txt").write_text(classifiers, encoding="utf-8") pyproject = tmp_path / "pyproject.toml" config = read_configuration(pyproject, expand=False) @@ -248,7 +250,7 @@ def test_dynamic_without_config(self, tmp_path): """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") with pytest.raises(OptionError, match="No configuration .* .classifiers."): read_configuration(pyproject) @@ -260,7 +262,7 @@ def test_dynamic_readme_from_setup_script_args(self, tmp_path): dynamic = ["readme"] """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") dist = Distribution(attrs={"long_description": "42"}) # No error should occur because of missing `readme` dist = apply_configuration(dist, pyproject) @@ -278,7 +280,7 @@ def test_dynamic_without_file(self, tmp_path): """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"): expanded = read_configuration(pyproject) assert "classifiers" not in expanded["project"] @@ -299,7 +301,7 @@ def test_dynamic_without_file(self, tmp_path): ) def test_ignore_unrelated_config(tmp_path, example): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(example)) + pyproject.write_text(cleandoc(example), encoding="utf-8") # Make sure no error is raised due to 3rd party configs in pyproject.toml assert read_configuration(pyproject) is not None @@ -321,7 +323,7 @@ def test_ignore_unrelated_config(tmp_path, example): ) def test_invalid_example(tmp_path, example, error_msg): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(example)) + pyproject.write_text(cleandoc(example), encoding="utf-8") pattern = re.compile(f"invalid pyproject.toml.*{error_msg}.*", re.M | re.S) with pytest.raises(ValueError, match=pattern): @@ -331,7 +333,7 @@ def test_invalid_example(tmp_path, example, error_msg): @pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42")) def test_empty(tmp_path, config): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(config) + pyproject.write_text(config, encoding="utf-8") # Make sure no error is raised assert read_configuration(pyproject) == {} @@ -343,7 +345,7 @@ def test_include_package_data_by_default(tmp_path, config): default. """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(config) + pyproject.write_text(config, encoding="utf-8") config = read_configuration(pyproject) assert config["tool"]["setuptools"]["include-package-data"] is True From d1019e1affec66ac105d7cecc14a4795e3365377 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:33:22 +0000 Subject: [PATCH 094/184] Refactor test_pyproject.toml to use more jaraco.path.build --- setuptools/tests/config/test_pyprojecttoml.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index b7a38f0a88..7279ae2e2a 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -357,10 +357,11 @@ def test_include_package_data_in_setuppy(tmp_path): See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889 """ - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text("[project]\nname = 'myproj'\nversion='42'\n") - setuppy = tmp_path / "setup.py" - setuppy.write_text("__import__('setuptools').setup(include_package_data=False)") + files = { + "pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n", + "setup.py": "__import__('setuptools').setup(include_package_data=False)", + } + path_build(files, prefix=tmp_path) with Path(tmp_path): dist = distutils.core.run_setup("setup.py", {}, stop_after="config") From be016801add784dbd7a4a63a6412080f312e3b29 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:35:11 +0000 Subject: [PATCH 095/184] Add utf-8 encoding in test_setupcfg, test_expand --- setuptools/tests/config/test_expand.py | 2 +- setuptools/tests/config/test_setupcfg.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index cdcbffc14c..fe80890678 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -12,7 +12,7 @@ def write_files(files, root_dir): for file, content in files.items(): path = root_dir / file path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(content) + path.write_text(content, encoding="utf-8") def test_glob_relative(tmp_path, monkeypatch): diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index 7f93858bd4..706e2d0ebe 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -904,7 +904,8 @@ def test_cmdclass(self, tmpdir): module_path = Path(tmpdir, "src/custom_build.py") # auto discovery for src module_path.parent.mkdir(parents=True, exist_ok=True) module_path.write_text( - "from distutils.core import Command\n" "class CustomCmd(Command): pass\n" + "from distutils.core import Command\n" "class CustomCmd(Command): pass\n", + encoding="utf-8", ) setup_cfg = """ From 5b57ac96e7be909d8c02b1b6233d395ff3ec199d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:44:25 +0000 Subject: [PATCH 096/184] Use jaraco.path.build in test_pyprojecttoml_dynamic_deps --- .../config/test_pyprojecttoml_dynamic_deps.py | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py index b6516227c0..37e5234a45 100644 --- a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py +++ b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py @@ -1,57 +1,59 @@ +from inspect import cleandoc + import pytest +from jaraco import path from setuptools.config.pyprojecttoml import apply_configuration from setuptools.dist import Distribution -from setuptools.tests.textwrap import DALS def test_dynamic_dependencies(tmp_path): - (tmp_path / "requirements.txt").write_text("six\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements.txt": "six\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["dependencies"] - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" - [tool.setuptools.dynamic.dependencies] - file = ["requirements.txt"] - """ - ) - ) + [tool.setuptools.dynamic.dependencies] + file = ["requirements.txt"] + """ + ), + } + path.build(files, prefix=tmp_path) dist = Distribution() - dist = apply_configuration(dist, pyproject) + dist = apply_configuration(dist, tmp_path / "pyproject.toml") assert dist.install_requires == ["six"] def test_dynamic_optional_dependencies(tmp_path): - (tmp_path / "requirements-docs.txt").write_text("sphinx\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements-docs.txt": "sphinx\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["optional-dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] - [tool.setuptools.dynamic.optional-dependencies.docs] - file = ["requirements-docs.txt"] + [tool.setuptools.dynamic.optional-dependencies.docs] + file = ["requirements-docs.txt"] - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" - """ - ) - ) + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + path.build(files, prefix=tmp_path) dist = Distribution() - dist = apply_configuration(dist, pyproject) + dist = apply_configuration(dist, tmp_path / "pyproject.toml") assert dist.extras_require == {"docs": ["sphinx"]} @@ -61,29 +63,32 @@ def test_mixed_dynamic_optional_dependencies(tmp_path): configurations in the case of fields containing sub-fields (groups), things would work out. """ - (tmp_path / "requirements-images.txt").write_text("pillow~=42.0\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements-images.txt": "pillow~=42.0\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["optional-dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] - [project.optional-dependencies] - docs = ["sphinx"] + [project.optional-dependencies] + docs = ["sphinx"] - [tool.setuptools.dynamic.optional-dependencies.images] - file = ["requirements-images.txt"] + [tool.setuptools.dynamic.optional-dependencies.images] + file = ["requirements-images.txt"] + + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + + path.build(files, prefix=tmp_path) - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" - """ - ) - ) # Test that the mix-and-match doesn't currently validate. + pyproject = tmp_path / "pyproject.toml" with pytest.raises(ValueError, match="project.optional-dependencies"): apply_configuration(Distribution(), pyproject) From bff5e77db9c4c0ac84f59a1d067039021170aa3c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:47:48 +0000 Subject: [PATCH 097/184] Add utf-8 encoding in test_apply_pyproject --- setuptools/tests/config/test_apply_pyprojecttoml.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 555489b140..2ca35759bc 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -44,8 +44,9 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path): monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1")) setupcfg_example = retrieve_file(url) pyproject_example = Path(tmp_path, "pyproject.toml") - toml_config = Translator().translate(setupcfg_example.read_text(), "setup.cfg") - pyproject_example.write_text(toml_config) + setupcfg_text = setupcfg_example.read_text(encoding="utf-8") + toml_config = Translator().translate(setupcfg_text, "setup.cfg") + pyproject_example.write_text(toml_config, encoding="utf-8") dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example) dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example) @@ -177,9 +178,9 @@ def _pep621_example_project( text = text.replace(orig, subst) pyproject.write_text(text, encoding="utf-8") - (tmp_path / readme).write_text("hello world") - (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---") - (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT) + (tmp_path / readme).write_text("hello world", encoding="utf-8") + (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---", encoding="utf-8") + (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT, encoding="utf-8") return pyproject From e6585999ce2de34e1dec82116bd17a592120f2c7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:51:53 +0000 Subject: [PATCH 098/184] Add utf-8 encoding in test_packageindex --- setuptools/tests/test_packageindex.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index 41b96614f8..93474ae5af 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -2,12 +2,12 @@ import urllib.request import urllib.error import http.client +from inspect import cleandoc from unittest import mock import pytest import setuptools.package_index -from .textwrap import DALS class TestPackageIndex: @@ -257,14 +257,15 @@ class TestPyPIConfig: def test_percent_in_password(self, tmp_home_dir): pypirc = tmp_home_dir / '.pypirc' pypirc.write_text( - DALS( + cleandoc( """ - [pypi] - repository=https://pypi.org - username=jaraco - password=pity% - """ - ) + [pypi] + repository=https://pypi.org + username=jaraco + password=pity% + """ + ), + encoding="utf-8", ) cfg = setuptools.package_index.PyPIConfig() cred = cfg.creds_by_repository['https://pypi.org'] From 9e0a88814d6b95e41f92f75ae3da0a3da4ce5561 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 18:54:46 +0000 Subject: [PATCH 099/184] Add utf-8 encoding in test_editable_install test_logging --- setuptools/tests/test_editable_install.py | 4 ++-- setuptools/tests/test_logging.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index df85699586..1df09fd256 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -144,8 +144,8 @@ def test_editable_with_pyproject(tmp_path, venv, files, editable_opts): cmd = [venv.exe(), "-m", "mypkg"] assert subprocess.check_output(cmd).strip() == b"3.14159.post0 Hello World" - (project / "src/mypkg/data.txt").write_text("foobar") - (project / "src/mypkg/mod.py").write_text("x = 42") + (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" diff --git a/setuptools/tests/test_logging.py b/setuptools/tests/test_logging.py index 7a9a33f1ea..cf89b3bd00 100644 --- a/setuptools/tests/test_logging.py +++ b/setuptools/tests/test_logging.py @@ -33,7 +33,7 @@ def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level): assert logging.getLevelName(unset_log_level) == "NOTSET" setup_script = tmp_path / "setup.py" - setup_script.write_text(setup_py) + setup_script.write_text(setup_py, encoding="utf-8") dist = distutils.core.run_setup(setup_script, stop_after="init") dist.script_args = [flag, "sdist"] dist.parse_command_line() # <- where the log level is set From 27ec7fa50029b32e7bf8459602e850baf554fa22 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 19:06:15 +0000 Subject: [PATCH 100/184] Improve utf-8 in test_config_discovery --- setuptools/tests/test_config_discovery.py | 52 ++++++++++++++--------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 409c219ed5..72772caebf 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -176,11 +176,11 @@ def test_purposefully_empty(self, tmp_path, config_file, param, circumstance): else: # Make sure build works with or without setup.cfg pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"] - (tmp_path / "pyproject.toml").write_text(pyproject) + (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8") template_param = param config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param) - (tmp_path / config_file).write_text(config) + (tmp_path / config_file).write_text(config, encoding="utf-8") dist = _get_dist(tmp_path, {}) # When either parameter package or py_modules is an empty list, @@ -292,11 +292,13 @@ class TestWithAttrDirective: def test_setupcfg_metadata(self, tmp_path, folder, opts): files = [f"{folder}/pkg/__init__.py", "setup.cfg"] _populate_project_dir(tmp_path, files, opts) - (tmp_path / folder / "pkg/__init__.py").write_text("version = 42") - (tmp_path / "setup.cfg").write_text( - "[metadata]\nversion = attr: pkg.version\n" - + (tmp_path / "setup.cfg").read_text() - ) + + config = (tmp_path / "setup.cfg").read_text(encoding="utf-8") + overwrite = { + folder: {"pkg": {"__init__.py": "version = 42"}}, + "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config, + } + jaraco.path.build(overwrite, prefix=tmp_path) dist = _get_dist(tmp_path, {}) assert dist.get_name() == "pkg" @@ -312,11 +314,16 @@ def test_setupcfg_metadata(self, tmp_path, folder, opts): def test_pyproject_metadata(self, tmp_path): _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {}) - (tmp_path / "src/pkg/__init__.py").write_text("version = 42") - (tmp_path / "pyproject.toml").write_text( - "[project]\nname = 'pkg'\ndynamic = ['version']\n" - "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" - ) + + overwrite = { + "src": {"pkg": {"__init__.py": "version = 42"}}, + "pyproject.toml": ( + "[project]\nname = 'pkg'\ndynamic = ['version']\n" + "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" + ), + } + jaraco.path.build(overwrite, prefix=tmp_path) + dist = _get_dist(tmp_path, {}) assert dist.get_version() == "42" assert dist.package_dir == {"": "src"} @@ -354,7 +361,7 @@ def _simulate_package_with_extension(self, tmp_path): ] setup(ext_modules=ext_modules) """ - (tmp_path / "setup.py").write_text(DALS(setup_script)) + (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8") def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): """Ensure that auto-discovery is not triggered when the project is based on @@ -367,14 +374,14 @@ def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): requires = [] build-backend = 'setuptools.build_meta' """ - (tmp_path / "pyproject.toml").write_text(DALS(pyproject)) + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") setupcfg = """ [metadata] name = proj version = 42 """ - (tmp_path / "setup.cfg").write_text(DALS(setupcfg)) + (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8") dist = _get_dist(tmp_path, {}) assert dist.get_name() == "proj" @@ -399,7 +406,7 @@ def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path): name = 'proj' version = '42' """ - (tmp_path / "pyproject.toml").write_text(DALS(pyproject)) + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): _get_dist(tmp_path, {}) @@ -416,7 +423,7 @@ def _simulate_package_with_data_files(self, tmp_path, src_root): manifest = """ global-include *.py *.txt """ - (tmp_path / "MANIFEST.in").write_text(DALS(manifest)) + (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8") EXAMPLE_SETUPCFG = """ [metadata] @@ -564,9 +571,12 @@ def _populate_project_dir(root, files, options): # NOTE: Currently pypa/build will refuse to build the project if no # `pyproject.toml` or `setup.py` is found. So it is impossible to do # completely "config-less" projects. - (root / "setup.py").write_text("import setuptools\nsetuptools.setup()") - (root / "README.md").write_text("# Example Package") - (root / "LICENSE").write_text("Copyright (c) 2018") + basic = { + "setup.py": "import setuptools\nsetuptools.setup()", + "README.md": "# Example Package", + "LICENSE": "Copyright (c) 2018", + } + jaraco.path.build(basic, prefix=root) _write_setupcfg(root, options) paths = (root / f for f in files) for path in paths: @@ -594,7 +604,7 @@ def _write_setupcfg(root, options): with open(root / "setup.cfg", "w", encoding="utf-8") as f: setupcfg.write(f) print("~~~~~ setup.cfg ~~~~~") - print((root / "setup.cfg").read_text()) + print((root / "setup.cfg").read_text(encoding="utf-8")) def _run_build(path, *flags): From 337e17532a6c08caf10cd721b576d7c6458d9246 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 19:50:58 +0000 Subject: [PATCH 101/184] Avoid aliasing jaraco.path.build in tests_pyprojecttoml --- setuptools/tests/config/test_pyprojecttoml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 7279ae2e2a..abec68ab30 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -2,10 +2,10 @@ from configparser import ConfigParser from inspect import cleandoc +import jaraco.path import pytest import tomli_w from path import Path -from jaraco.path import build as path_build from setuptools.config.pyprojecttoml import ( read_configuration, @@ -108,7 +108,7 @@ def create_example(path, pkg_root): # Use this opportunity to ensure namespaces are discovered files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}} - path_build(files, prefix=path) + jaraco.path.build(files, prefix=path) def verify_example(config, path, pkg_root): @@ -361,7 +361,7 @@ def test_include_package_data_in_setuppy(tmp_path): "pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n", "setup.py": "__import__('setuptools').setup(include_package_data=False)", } - path_build(files, prefix=tmp_path) + jaraco.path.build(files, prefix=tmp_path) with Path(tmp_path): dist = distutils.core.run_setup("setup.py", {}, stop_after="config") From 52e1b7047e2577c6e0620c381c5beadaddde800e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 00:02:49 +0000 Subject: [PATCH 102/184] Add utf-8 encoding to test_pkg_resources --- pkg_resources/tests/test_pkg_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index bfbf619c85..4724c82860 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -111,13 +111,13 @@ def test_resource_filename_rewrites_on_change(self): filename = zp.get_resource_filename(manager, 'data.dat') actual = datetime.datetime.fromtimestamp(os.stat(filename).st_mtime) assert actual == self.ref_time - f = open(filename, 'w') + f = open(filename, 'w', encoding="utf-8") f.write('hello, world?') f.close() ts = self.ref_time.timestamp() os.utime(filename, (ts, ts)) filename = zp.get_resource_filename(manager, 'data.dat') - with open(filename) as f: + with open(filename, encoding="utf-8") as f: assert f.read() == 'hello, world!' manager.cleanup_resources() From 7a0a29d628f7bc19287a9f253649299ade422832 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 00:14:48 +0000 Subject: [PATCH 103/184] Add newsfragment --- newsfragments/4263.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4263.misc.rst diff --git a/newsfragments/4263.misc.rst b/newsfragments/4263.misc.rst new file mode 100644 index 0000000000..f84eb8dd42 --- /dev/null +++ b/newsfragments/4263.misc.rst @@ -0,0 +1 @@ +Avoid implicit ``encoding`` parameter in ``pkg_resources/tests``. From 31ff4dd517442bfb06d8a3973766ebab0008166d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 00:17:19 +0000 Subject: [PATCH 104/184] Add newsfragment --- newsfragments/4261.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4261.misc.rst diff --git a/newsfragments/4261.misc.rst b/newsfragments/4261.misc.rst new file mode 100644 index 0000000000..83c10f0f66 --- /dev/null +++ b/newsfragments/4261.misc.rst @@ -0,0 +1 @@ +Avoid implicit ``encoding`` parameter in ``setuptools/tests``. From c646680450fcb91e0e238fcef97dd788c15c2978 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 00:23:56 +0000 Subject: [PATCH 105/184] Use binary mode when 'touch'-ing files in test_build_py and test_build_meta --- setuptools/tests/test_build_meta.py | 2 +- setuptools/tests/test_build_py.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 32676aee79..99c0c0543a 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -306,7 +306,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): assert first_result != second_result # And if rebuilding the exact same sdist/wheel? - open(os.path.join(dist_dir, second_result), 'w', encoding="utf-8").close() + open(os.path.join(dist_dir, second_result), 'wb').close() third_result = build_method(dist_dir) assert third_result == second_result assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0 diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index d79cbbdf0c..4aa1fe68fa 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -49,7 +49,7 @@ def test_recursive_in_package_data_glob(tmpdir_cwd): ) ) os.makedirs('path/subpath/subsubpath') - open('path/subpath/subsubpath/data', 'w', encoding="utf-8").close() + open('path/subpath/subsubpath/data', 'wb').close() dist.parse_command_line() dist.run_commands() @@ -77,8 +77,8 @@ def test_read_only(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w', encoding="utf-8").close() - open('pkg/data.dat', 'w', encoding="utf-8").close() + open('pkg/__init__.py', 'wb').close() + open('pkg/data.dat', 'wb').close() os.chmod('pkg/__init__.py', stat.S_IREAD) os.chmod('pkg/data.dat', stat.S_IREAD) dist.parse_command_line() @@ -108,8 +108,8 @@ def test_executable_data(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w', encoding="utf-8").close() - open('pkg/run-me', 'w', encoding="utf-8").close() + open('pkg/__init__.py', 'wb').close() + open('pkg/run-me', 'wb').close() os.chmod('pkg/run-me', 0o700) dist.parse_command_line() From 3115855126402d2678afc9867e6b3f2449c529ac Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 00:28:34 +0000 Subject: [PATCH 106/184] Minor formatting improvement --- setuptools/tests/test_build_meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 99c0c0543a..c2a1e6dc75 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -272,14 +272,14 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): [metadata] name = foo version = file: VERSION - """ + """ ), 'pyproject.toml': DALS( """ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" - """ + """ ), } From 0eceb495e0b5ccd1b9204d1e773b734a1a4d1e05 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 7 Mar 2024 08:40:46 +0000 Subject: [PATCH 107/184] Add `encoding` to subprocess.run inside setuptools/tests --- setuptools/tests/integration/helpers.py | 1 + setuptools/tests/test_dist_info.py | 3 ++- setuptools/tests/test_distutils_adoption.py | 12 +++++++----- setuptools/tests/test_easy_install.py | 1 + 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/setuptools/tests/integration/helpers.py b/setuptools/tests/integration/helpers.py index 82cb36a2e4..615c43b2e0 100644 --- a/setuptools/tests/integration/helpers.py +++ b/setuptools/tests/integration/helpers.py @@ -17,6 +17,7 @@ def run(cmd, env=None): cmd, capture_output=True, text=True, + encoding="utf-8", env={**os.environ, **(env or {})}, # ^-- allow overwriting instead of discarding the current env ) diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py index ad6cebad0b..c6fe97e2ba 100644 --- a/setuptools/tests/test_dist_info.py +++ b/setuptools/tests/test_dist_info.py @@ -198,7 +198,8 @@ def run_command_inner(*cmd, **kwargs): "stderr": subprocess.STDOUT, "stdout": subprocess.PIPE, "text": True, - 'check': True, + "encoding": "utf-8", + "check": True, **kwargs, } cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *map(str, cmd)] diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index eb7feba637..74883d2199 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -8,6 +8,8 @@ IS_PYPY = '__pypy__' in sys.builtin_module_names +_TEXT_KWARGS = {"text": True, "encoding": "utf-8"} # For subprocess.run + def win_sr(env): """ @@ -24,7 +26,7 @@ def win_sr(env): def find_distutils(venv, imports='distutils', env=None, **kwargs): py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals()) cmd = ['python', '-c', py_cmd] - return venv.run(cmd, env=win_sr(env), text=True, **kwargs) + return venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS, **kwargs) def count_meta_path(venv, env=None): @@ -36,7 +38,7 @@ def count_meta_path(venv, env=None): """ ) cmd = ['python', '-c', py_cmd] - return int(venv.run(cmd, env=win_sr(env), text=True)) + return int(venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS)) skip_without_stdlib_distutils = pytest.mark.skipif( @@ -82,7 +84,7 @@ def test_pip_import(venv): Regression test for #3002. """ cmd = ['python', '-c', 'import pip'] - venv.run(cmd, text=True) + venv.run(cmd, **_TEXT_KWARGS) def test_distutils_has_origin(): @@ -130,7 +132,7 @@ def test_modules_are_not_duplicated_on_import( env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module) cmd = ['python', '-c', script] - output = venv.run(cmd, env=win_sr(env), text=True).strip() + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() assert output == "success" @@ -154,5 +156,5 @@ def test_modules_are_not_duplicated_on_import( def test_log_module_is_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] - output = venv.run(cmd, env=win_sr(env), text=True).strip() + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() assert output == "success" diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 26778e65ec..950cb23d21 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -530,6 +530,7 @@ def test_setup_install_includes_dependencies(self, tmp_path, mock_index): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding="utf-8", ) assert cp.returncode != 0 try: From 03166bcd5d86426ef055d147697dea1c9a9215e9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 6 Mar 2024 15:05:39 +0000 Subject: [PATCH 108/184] 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 0000000000..04a4abe5a9 --- /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 109/184] 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 9d319398c9..5f08ab53fc 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 110/184] 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 402355bd81..c256770239 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 111/184] 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 c256770239..858fb20f83 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 112/184] 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 e8f2941d45..0185d55f94 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 113/184] 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 be8856849b..534da15a37 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 0000000000..bcb0467205 --- /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 114/184] 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 42ade6537e..90c8ff13e7 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 4d1155e884..5231358289 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 4bf3c7c947..5dd97c7719 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 5f08ab53fc..4d21e2253f 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 115/184] 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 e841bde57c..d2beaa0c48 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 116/184] 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 e841bde57c..715eb1db7c 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 117/184] 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 e841bde57c..4d65b0dad4 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 118/184] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5231358289..c41b226e0c 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 119/184] 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 163a5521d6..c2ba0476e5 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 1a6074766a..542edaea68 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 120/184] 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 bc0b67003f..87b7317f13 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 121/184] 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 88cc75cabb..a37f30294d 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 122/184] 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 a37f30294d..3da7141573 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 123/184] =?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 8d101ab5af..1236141a7c 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 abc4bb3f04..2e849bdc5f 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 995bee20e1..0000000000 --- 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 a7ccfc911e..0000000000 --- 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 ef6da2c323..0000000000 --- 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 e8212721f3..0000000000 --- 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 5d606de718..0000000000 --- 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 e944fcfb49..0000000000 --- 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 9dfde3498d..0000000000 --- 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 83c10f0f66..0000000000 --- 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 f84eb8dd42..0000000000 --- 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 bcb0467205..0000000000 --- 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 c41b226e0c..9b504dd39b 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 b0135f5097f32a27b7a14e2c6296ba14bcb4e10b Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Mar 2024 20:46:50 +0000 Subject: [PATCH 124/184] 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 7c0f0e5b78..d20ce33e00 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 72772caebf..7d51a47012 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 125/184] 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 0000000000..2ec6f9714e --- /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 126/184] Add mypy.ini to MANIFEST.in --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 116840bfa2..c4f12dc68a 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 127/184] 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 400a72a5ed..6fa73b6a09 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 128/184] 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 d20ce33e00..7c0f0e5b78 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 129/184] 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 6350e38100..c7a3e5175d 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 130/184] Linting --- setuptools/dist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/dist.py b/setuptools/dist.py index c7a3e5175d..c62187ec25 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 131/184] 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 cf94f7d816..143b0984b0 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 132/184] 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 6848906374..85dfea9d42 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 133/184] 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 143b0984b0..a15c74a618 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 134/184] 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 022a723e7e..9a0f3bce13 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 6fa73b6a09..f46b6cbff4 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 135/184] 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 fa31fb33dc..3ee2f886ba 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 136/184] 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 a15c74a618..ac0ff69e22 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 137/184] 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 c62187ec25..202430fb69 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 7d51a47012..e1e67ffe11 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 138/184] =?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 90c8ff13e7..ee12ebb193 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 139/184] 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 bd1a86ff17..6f620cb890 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 858fb20f83..87a68c292a 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 140/184] 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 5dd97c7719..d8732c49bb 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 202430fb69..6350e38100 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 fd07d91dec..1f8d8ffe0f 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 141/184] 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 822809de2b..aa3b65f15e 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 142/184] 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 01dd79079b..4beb123630 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 675bcebdad..bb3bee7eb9 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 1a4d67f492..68caf5a65b 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 8f186b72ff..00ca61d569 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 1a07746bc7..8b4f7046c7 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 2763db9c02..ca198873ad 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 c632b3910f..53c131e9e5 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 0aaf0a534c..7e42227e19 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 143/184] 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 bcf9580c7a..cdfe9d74ef 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 4beb123630..5880ad2ba4 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 bb3bee7eb9..64af0db0cf 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 68caf5a65b..6a5e6ed081 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 7ba35eef82..31ae4350dc 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 c4d2a45dc2..bbea155556 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 c025f12062..e41b6064bd 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 0eb9b86107..6c8193e9b7 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 8b4f7046c7..b8694dd6d8 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 cb4db4e192..cfe7fa9e62 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 16ac5ca746..08c72c1be0 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 7e42227e19..f89d1b3580 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 bfd30700fa..ce5bc55f36 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 144/184] =?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 6a5e6ed081..29d9c27829 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 ce5bc55f36..a24c940102 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 145/184] =?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 06ce3bc6c8..3ce3411535 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 1a188c35cb..e374d5c560 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 4a2ae0acb8..0148f157ff 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 17769e9154..76ecb96abe 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 fbb95a8f27..78485dc25e 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 4f081c7e92..d08910ecf9 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 7f9e1e00cc..052f6e4646 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 d496d5d452..c1341e43cb 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 cdfe9d74ef..03181cfb7c 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 8849474cd7..02dbf165f5 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 397ebf823e..00d34bc7d8 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 237b14656f..f681b5531d 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 5880ad2ba4..41adf01418 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 64af0db0cf..6a75e32fb1 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 d8704e3583..d18ed503e3 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 b3f679b67d..811e607e70 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 a15781f28a..aa9ed578f8 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 e16011d46a..a15d0af519 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 29d9c27829..37bc585038 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 28f55fb914..6b42a34f6d 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 9413f7cfcb..4167a83fb3 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 573741d772..38a5ff5159 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 927c3ed3a2..575cebdbc8 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 31ae4350dc..b63a1af25e 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 f3e8f3447d..4fbb3440ab 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 be4c243321..b1f346f018 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 20f07aaa27..e66b13a16d 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 5a24246ccb..e5e6b379ad 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 b76cb9bc73..6414ef5c06 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 a9124f2b71..e61a9ea8a5 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 e0defd77e6..83f96a9eec 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 799de9489c..309ce696fa 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 2060950415..539f09d8f3 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 819fe56f6d..2021bed82e 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 bbea155556..1759120c92 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 00ca61d569..94e71635d9 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 e41b6064bd..cb646c6d9b 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 6c8193e9b7..960def9cf9 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 3205762654..5ce47936a9 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 239f315850..8abb09cfa2 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 402c0c0620..6a0105e484 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 b8694dd6d8..ac8b68c08c 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 48adceb114..046b5bbb82 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 40215b8347..1a38e9fa79 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 6d9b853215..c475e5d0f2 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 e5d406a3b6..76d3551c49 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 ddf7bf1dba..9cd2b8a9ee 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 2b5eafd27e..145cce915d 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 cfe7fa9e62..78928fea24 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 e6804088da..769623cbb8 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 8617fa9919..25483ad76b 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 f855454256..9c69b3e7fc 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 ae66bc4eb8..ca5d9d57cd 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 6730878e96..8bc0e98a4f 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 7e05ec5f9a..208b1f6e65 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 b6512e6d77..d23b907cad 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 8215300b97..580cb2a267 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 e2459aa0c1..9b11fa40f7 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 684662d32e..f366aa6522 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 11c23d837e..be5ae0a687 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 90c8f90679..fc0a7885cd 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 95aa299889..5916718027 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 fc67d75f82..0a66193d35 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 6fc9ed0883..84cda619ba 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 8e52873dce..9ed4d16dd8 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 297ae44bfe..77bb147bfd 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 6c7019140e..4c2abd2453 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 bf1a9d9b45..6a379a6323 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 08c72c1be0..08f0f83993 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 198c10da8d..e453d01f1a 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 8b86b6eaed..2c74f06b97 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 0efe39fe86..964106fa00 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 4da2acb6a8..5d9f13a426 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 ec6a0c8051..d67779fc9f 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 5fde7a5971..2bd82346cf 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 dfb34122bc..58e24f017a 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 f65a5a25a3..23b6c732c3 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 591c5ce0ad..d071bbe951 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 450f68c993..66a4194706 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 ec4c9982ad..abbac4c23f 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 131c1344bb..ce13d6bdc3 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 fe787f44c8..c5c910a820 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 ca198873ad..f17edf2f6b 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 5c5bc59a40..0692f00160 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 53c131e9e5..78d8b1e3b6 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 f89d1b3580..ddf1789b44 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 95fc8eebe2..a5d9ee45cc 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 d749fe2529..a1fe2b57a2 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 a24c940102..9ee77721b3 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 8ab76ddef4..aa7c5385ae 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 c75e49486f..31c420168c 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 6d70b7f04f..c7a7ca61cf 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 146/184] =?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 d08910ecf9..a2159fef83 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 03181cfb7c..8876d73098 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 f681b5531d..ade98445ba 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 41adf01418..06502d201e 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 6a75e32fb1..649968a5eb 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 aa9ed578f8..82e1e02070 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 37bc585038..5f3902a027 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 575cebdbc8..85165717a7 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 6414ef5c06..97bae8279d 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 cb646c6d9b..dccc54923f 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 5ce47936a9..71ffb2abe7 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 ddf1789b44..1508e1cc0a 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 6f90cfe21d..0f846e3c52 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 147/184] 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 9b11fa40f7..bdbcd4fa46 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 aa7c5385ae..6e26e03007 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 148/184] 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 6e26e03007..90adbc718a 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 149/184] 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 90adbc718a..30546a9dd6 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 150/184] 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 30546a9dd6..806d233ca5 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 151/184] 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 5ad21cc7c9..6810a5e24d 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 152/184] 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 811e607e70..360575d0cb 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 a15d0af519..56e6fa2e66 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 85165717a7..8e920be4de 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 97bae8279d..387d27c90b 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 1759120c92..c32ffb6c0e 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 153/184] =?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 6810a5e24d..d11a83467c 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 38a5ff5159..d4b2b0a362 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 c32ffb6c0e..f29a34faba 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 dccc54923f..e905aede4d 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 c475e5d0f2..20dfe8f19b 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 154/184] 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 6b42a34f6d..28599e109c 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 155/184] 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 e5e6b379ad..ee6c54daba 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 e61a9ea8a5..cf541f8a82 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 156/184] 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 5916718027..bad3fb7e83 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 157/184] 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 82e1e02070..06d949aff1 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 aa0c0a7fcd..0000000000 --- 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 158/184] 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 76d3551c49..0000000000 --- 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 ce13d6bdc3..bc14d3c05a 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 159/184] 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 bc14d3c05a..c55896661f 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 160/184] 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 0000000000..21d680d486 --- /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 161/184] Omit distutils from coverage checks. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 14424a43dd..1f214acf38 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 162/184] 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 f79f5b3b45..3ba5d16ac7 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 163/184] =?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 1236141a7c..a76d5b66d7 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 2e849bdc5f..7822ec6325 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 2ec6f9714e..0000000000 --- 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 aae1465375..bab3efa52c 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 164/184] 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 0000000000..acc51ea4bd --- /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 353924be0e..1bb5a44356 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 0000000000..e7b64c87f8 --- /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 0000000000..a6a44d8fcc --- /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 0000000000..e69de29bb2 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 57e3d840d5..bab98d6758 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 0000000000..99d2be5b64 --- /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 0000000000..a7a9a6e7b9 --- /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 7d19852d4a..ba764991ee 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 281137a035..0000000000 --- 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 03122364a2..0000000000 --- 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 353924be0e..1bb5a44356 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 0000000000..a36f7c5e82 --- /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 0000000000..09d191f214 --- /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 57e3d840d5..bab98d6758 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 70a3521307..0000000000 --- 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 353924be0e..1bb5a44356 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 23c6f5ef2b..581b308378 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 0000000000..783aa7d2b9 --- /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 57e3d840d5..ba48cbcf92 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 dd471b0708..c698101cb4 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 b0d1ef37cb..61b27135df 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 67aeadc353..f523099c72 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 0000000000..c2b9ab1757 --- /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 0000000000..e69de29bb2 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 bee8776239..f54f1ff279 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 0000000000..2ce6e4a6f5 --- /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 c2fd4da0ac..0000000000 --- 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 66443971df..aff94a9abd 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 e0e2d3de92..d0957681f5 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 75c5232c1a..9a5fc911a3 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 3facc2e3a6..145e3cb5bd 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 0267ed569e..ed4c19db49 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 e240a8408d..e041f20f6a 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 843a5baf9d..a721322694 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 b9e1bb0391..e1132566df 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 4cd4ab8cb8..1138915921 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 0a88551ce0..adc797bc2e 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 353924be0e..1bb5a44356 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 0000000000..e7b64c87f8 --- /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 0000000000..a6a44d8fcc --- /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 0000000000..e69de29bb2 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 57e3d840d5..bab98d6758 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 0000000000..99d2be5b64 --- /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 0000000000..a7a9a6e7b9 --- /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 01f235677f..c5ed31bf55 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 7d19852d4a..ba764991ee 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 281137a035..0000000000 --- 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 03122364a2..0000000000 --- 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 0000000000..a1b589e38a --- /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 0000000000..1bb5a44356 --- /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 0000000000..a36f7c5e82 --- /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 0000000000..09d191f214 --- /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 0000000000..bab98d6758 --- /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 70a3521307..0000000000 --- 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 0000000000..a1b589e38a --- /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 0000000000..1bb5a44356 --- /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 23c6f5ef2b..581b308378 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 0000000000..783aa7d2b9 --- /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 0000000000..ba48cbcf92 --- /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 dd471b0708..c698101cb4 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 b0d1ef37cb..61b27135df 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 ebf7a36137..130b87a485 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 0000000000..c2b9ab1757 --- /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 0000000000..e69de29bb2 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 c3cbb83382..d1a6ea0d22 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 3c699595fb..3267872d45 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 f771c29b87..832be11132 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 e240a8408d..e041f20f6a 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 5f7a6b06b3..1db8063ec5 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 786de8542d..efc5f26cf3 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 0fed8eeeae..592fe491a1 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 0a88551ce0..adc797bc2e 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 f339497fa1..e33a44f291 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 165/184] 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 0000000000..e69de29bb2 diff --git a/pkg_resources/_vendor/jaraco/context.py b/pkg_resources/_vendor/jaraco/context.py index 61b27135df..c42f6135d5 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 948bcc6094..df96f7f26d 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 0000000000..e69de29bb2 diff --git a/setuptools/_vendor/jaraco/context.py b/setuptools/_vendor/jaraco/context.py index 61b27135df..0322c45d4a 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 67c4a4552f..427b27cb80 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 e33a44f291..232e9625d2 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 166/184] Exclude vendored packages and tools from coverage checks. --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1f214acf38..5b7fdefd2a 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 167/184] =?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 a76d5b66d7..007a8ec0f5 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 7822ec6325..0fcbdfc9a6 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 21d680d486..0000000000 --- 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 bab3efa52c..02078f7466 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 168/184] 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 0000000000..28ceb2a689 --- /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 e041f20f6a..0000000000 --- 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 c43882a826..10ab4390a9 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 0000000000..bcf796c2f4 --- /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 db4a255f3a..3b5e64b5e6 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 13cadc7f04..e7c0aa12ca 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 449c655be6..ad62505f3f 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 706ba600a9..86419df9d7 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 5a18b758fe..684df75457 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 e76a60c395..fb27493079 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 f34bfa85c8..bdc43a7e98 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 ba8fe37b7f..2d015bab59 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 76d243414d..89f1926137 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 33c613b749..c2c2f75aa8 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 b30e8cbf84..5faab9bd0d 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 1138915921..c18a2cc0eb 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 e041f20f6a..0000000000
--- 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 c43882a826..10ab4390a9 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 0000000000..bcf796c2f4
--- /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 db4a255f3a..3b5e64b5e6 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 13cadc7f04..e7c0aa12ca 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 449c655be6..ad62505f3f 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 706ba600a9..86419df9d7 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 5a18b758fe..684df75457 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 e76a60c395..fb27493079 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 f34bfa85c8..bdc43a7e98 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 ba8fe37b7f..2d015bab59 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 76d243414d..89f1926137 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 33c613b749..c2c2f75aa8 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 b30e8cbf84..5faab9bd0d 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 592fe491a1..e67c7845c8 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 169/184] 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 f29a34faba..668ce7eb0a 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 170/184] 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 58e24f017a..6f6aabee4d 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 171/184] 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 2021bed82e..8a3aca6521 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 172/184] 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 a1fe2b57a2..caf4cd338e 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 173/184] 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 0000000000..666549bcab
--- /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 d8732c49bb..9b4f38ded2 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 e1e67ffe11..ff9e672b68 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 174/184] =?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 a76d5b66d7..d9cfd1ad7c 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 7822ec6325..8a45a961eb 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 666549bcab..0000000000
--- 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 bab3efa52c..78b9166b85 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 175/184] =?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 007a8ec0f5..09a7b690f0 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 e01087fc2f..fc213d160d 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 02078f7466..a579bf5ff7 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 176/184] =?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 09a7b690f0..f12875d186 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 fc213d160d..b2eb9bb62a 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 acc51ea4bd..0000000000
--- 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 28ceb2a689..0000000000
--- 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 a579bf5ff7..62a759e54d 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 177/184] 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 8a3aca6521..370c6ffd49 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 178/184] 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 caf4cd338e..a54481c01b 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 179/184] 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 0000000000..b7be72678f
--- /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 0000000000..0af3814017
--- /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 f17edf2f6b..6f05fa6989 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 a54481c01b..0248bde87b 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 180/184] Exclude compat package from coverage.

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

diff --git a/.coveragerc b/.coveragerc
index 35b98b1df9..bcef31d957 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 181/184] 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 b7be72678f..b1ee3fe8b0 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 182/184] 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 0000000000..83ed1520be
--- /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 183/184] =?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 09a7b690f0..0570d58bb9 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 fc213d160d..c4aa039229 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 83ed1520be..0000000000
--- 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 a579bf5ff7..c51168c71b 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 184/184] =?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 f12875d186..557ae0ce34 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 9fa3ade1fa..08e28ecc28 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 62a759e54d..f7479e047f 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