From e649e936e79fb5cbbf45f63475934faa3cb0f4bc Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Tue, 28 Feb 2023 08:55:11 +0300 Subject: [PATCH 001/221] 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 1818fce9..ae60578a 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 5a8ca1b0f362968a29f2fb9c107cc0d4d79c3263 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Jul 2023 18:20:05 -0400 Subject: [PATCH 002/221] Rely on pytest as found in pytest-dev/pytest#11155. Fixes pypa/distutils#186. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index c4200483..06657e4e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,8 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = - # < 7.2 due to pypa/distutils#186 - pytest < 7.2 + # pypa/distutils#186; workaround for pytest-dev/pytest#10447 + pytest @ git+https://github.com/RonnyPfannschmidt/pytest@fix-10447-maker-mro-order-needs-reverse pytest-flake8 # workaround for tholo/pytest-flake8#87 From c29955f9be8e44b2ea5fea12f86b7bd46a0b3958 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Jul 2023 11:53:15 -0400 Subject: [PATCH 003/221] Collapse skeleton history. Workaround for jaraco/skeleton#87. --- .coveragerc | 9 ++ .editorconfig | 19 ++++ .github/dependabot.yml | 8 ++ .github/workflows/main.yml | 124 +++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 ++ .readthedocs.yaml | 12 +++ LICENSE | 17 ++++ NEWS.rst | 0 README.rst | 22 +++++ docs/conf.py | 42 +++++++++ docs/history.rst | 8 ++ docs/index.rst | 22 +++++ mypy.ini | 5 ++ newsfragments/+drop-py37.feature.rst | 1 + pyproject.toml | 8 ++ pytest.ini | 27 ++++++ setup.cfg | 55 ++++++++++++ towncrier.toml | 2 + tox.ini | 49 +++++++++++ 19 files changed, 435 insertions(+) create mode 100644 .coveragerc create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 LICENSE create mode 100644 NEWS.rst create mode 100644 README.rst create mode 100644 docs/conf.py create mode 100644 docs/history.rst create mode 100644 docs/index.rst create mode 100644 mypy.ini create mode 100644 newsfragments/+drop-py37.feature.rst create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 towncrier.toml create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..02879483 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* +disable_warnings = + couldnt-parse + +[report] +show_missing = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..304196f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space +max_line_length = 88 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rst] +indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..f54dfbc6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,124 @@ +name: tests + +on: [push, pull_request] + +permissions: + contents: read + +env: + # Environment variables to support color support (jaraco/skeleton#66): + # Request colored output from CLI tools supporting it. Different tools + # interpret the value differently. For some, just being set is sufficient. + # For others, it must be a non-zero integer. For yet others, being set + # to a non-empty value is sufficient. For tox, it must be one of + # , 0, 1, false, no, off, on, true, yes. The only enabling value + # in common is "1". + FORCE_COLOR: 1 + # MyPy's color enforcement (must be a non-zero number) + MYPY_FORCE_COLOR: -42 + # Recognized by the `py` package, dependency of `pytest` (must be "1") + PY_COLORS: 1 + # Make tox-wrapped tools see color requests + TOX_TESTENV_PASSENV: >- + FORCE_COLOR + MYPY_FORCE_COLOR + NO_COLOR + PY_COLORS + PYTEST_THEME + PYTEST_THEME_MODE + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_PYTHON_VERSION_WARNING: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream + # Must be "1". + TOX_PARALLEL_NO_SPINNER: 1 + + +jobs: + test: + strategy: + matrix: + python: + - "3.8" + - "3.11" + - "3.12" + platform: + - ubuntu-latest + - macos-latest + - windows-latest + include: + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest + - python: pypy3.9 + platform: ubuntu-latest + runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.python == '3.12' }} + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + docs: + runs-on: ubuntu-latest + env: + TOXENV: docs + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + - docs + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + release: + permissions: + contents: write + needs: + - check + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..af502010 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..053c7287 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs + +# required boilerplate readthedocs/readthedocs.org#10401 +build: + os: ubuntu-22.04 + tools: + python: "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1bb5a443 --- /dev/null +++ b/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/NEWS.rst b/NEWS.rst new file mode 100644 index 00000000..e69de29b diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..b703d490 --- /dev/null +++ b/README.rst @@ -0,0 +1,22 @@ +.. image:: https://img.shields.io/pypi/v/PROJECT.svg + :target: https://pypi.org/project/PROJECT + +.. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg + +.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg + :target: https://github.com/PROJECT_PATH/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 + +.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest +.. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2023-informational + :target: https://blog.jaraco.com/skeleton diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..32150488 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,42 @@ +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] + +master_doc = "index" +html_theme = "furo" + +# Link dates and other references in the changelog +extensions += ['rst.linker'] +link_files = { + '../NEWS.rst': dict( + using=dict(GH='https://github.com'), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://peps.python.org/pep-{pep_number:0>4}/', + ), + ], + ) +} + +# Be strict about any broken references +nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..5bdc2320 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../NEWS (links).rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..53117d16 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to |project| documentation! +=================================== + +.. toctree:: + :maxdepth: 1 + + history + + +.. automodule:: PROJECT + :members: + :undoc-members: + :show-inheritance: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b6f97276 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True +# required to support namespace packages +# https://github.com/python/mypy/issues/14057 +explicit_package_bases = True diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst new file mode 100644 index 00000000..ccabdaa3 --- /dev/null +++ b/newsfragments/+drop-py37.feature.rst @@ -0,0 +1 @@ +Require Python 3.8 or later. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dce944df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" + +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..d9a15ed1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,27 @@ +[pytest] +norecursedirs=dist build .tox .eggs +addopts=--doctest-modules +filterwarnings= + ## upstream + + # Ensure ResourceWarnings are emitted + default::ResourceWarning + + # shopkeep/pytest-black#55 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning + ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning + + # shopkeep/pytest-black#67 + ignore:'encoding' argument not specified::pytest_black + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore:'encoding' argument not specified::platform + + # pypa/build#615 + ignore:'encoding' argument not specified::build.env + + ## end upstream diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..a9ca2a88 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,55 @@ +[metadata] +name = PROJECT +author = Jason R. Coombs +author_email = jaraco@jaraco.com +description = PROJECT_DESCRIPTION +long_description = file:README.rst +url = https://github.com/PROJECT_PATH +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + +[options] +packages = find_namespace: +include_package_data = true +python_requires = >=3.8 +install_requires = + +[options.packages.find] +exclude = + build* + dist* + docs* + tests* + +[options.extras_require] +testing = + # upstream + pytest >= 6 + pytest-checkdocs >= 2.4 + pytest-black >= 0.3.7; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-cov + pytest-mypy >= 0.9.1; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-enabler >= 2.2 + pytest-ruff + + # local + +docs = + # upstream + sphinx >= 3.5 + jaraco.packaging >= 9 + rst.linker >= 1.9 + furo + sphinx-lint + + # local + +[options.entry_points] diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..1093e028 --- /dev/null +++ b/tox.ini @@ -0,0 +1,49 @@ +[tox] +toxworkdir={env:TOX_WORK_DIR:.tox} + + +[testenv] +deps = +setenv = + PYTHONWARNDEFAULTENCODING = 1 +commands = + pytest {posargs} +usedevelop = True +extras = + testing + +[testenv:docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint + +[testenv:finalize] +skip_install = True +deps = + towncrier + jaraco.develop >= 7.23 +passenv = * +commands = + python -m jaraco.develop.finalize + + +[testenv:release] +skip_install = True +deps = + build + twine>=3 + jaraco.develop>=7.1 +passenv = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + python -m twine upload dist/* + python -m jaraco.develop.create-github-release From 972d1b3033afba89ffa20e6c492c4d02742e8a9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jul 2023 22:20:28 -0400 Subject: [PATCH 004/221] Add links to project home page and pypi. Fixes jaraco/skeleton#77. --- docs/index.rst | 4 ++++ setup.cfg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 53117d16..5a3c6770 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,10 @@ Welcome to |project| documentation! =================================== +.. sidebar-links:: + :home: + :pypi: + .. toctree:: :maxdepth: 1 diff --git a/setup.cfg b/setup.cfg index a9ca2a88..46f7bdf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ testing = docs = # upstream sphinx >= 3.5 - jaraco.packaging >= 9 + jaraco.packaging >= 9.3 rst.linker >= 1.9 furo sphinx-lint From 747c2a36524f83b84a3d9497121313bb5751b877 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 08:59:49 -0400 Subject: [PATCH 005/221] Replace redundant step names with simple 'Run'. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f54dfbc6..b8224099 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox docs: @@ -82,7 +82,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox check: # This job does nothing and is only used for the branch protection @@ -117,7 +117,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Release + - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} From c43962adf34c28c22573093419e5e98b2e57cc07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jul 2023 23:34:53 -0400 Subject: [PATCH 006/221] Remove TOX_WORK_DIR workaround, no longer necessary with tox 4. Ref tox-dev/tox#3050. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 1093e028..e51d652d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,3 @@ -[tox] -toxworkdir={env:TOX_WORK_DIR:.tox} - - [testenv] deps = setenv = 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 007/221] 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 60801ace..dbba53e2 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 008/221] 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 06657e4e..fd858d18 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 009/221] 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 070a2770..22a003d8 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 010/221] 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 30a6f9ff..694bf02a 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 011/221] 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 7ef47176..4f94e587 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 0e2032c4754c598ba75e467c64009ba4490ddea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Aug 2023 18:42:14 -0400 Subject: [PATCH 012/221] Pin against sphinx 7.2.5 as workaround for sphinx/sphinx-doc#11662. Closes jaraco/skeleton#88. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 46f7bdf7..4f184c7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,8 @@ 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 92d2d8e1aff997f3877239230c9490ed9cdd1222 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:46:27 -0400 Subject: [PATCH 013/221] Allow GITHUB_* settings to pass through to tests. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8224099..67d9d3bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,10 @@ env: # Must be "1". TOX_PARALLEL_NO_SPINNER: 1 + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_* + jobs: test: From f3dc1f4776c94a9a4a7c0e8c5b49c532b0a7d411 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:49:13 -0400 Subject: [PATCH 014/221] Remove spinner disablement. If it's not already fixed upstream, that's where it should be fixed. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 67d9d3bc..30c9615d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,10 +32,6 @@ env: PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' - # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream - # Must be "1". - TOX_PARALLEL_NO_SPINNER: 1 - # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- testenv.pass_env+=GITHUB_* From 0484daa8a6f72c9ad4e1784f9181c2488a191d8e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:53:55 -0400 Subject: [PATCH 015/221] Clean up 'color' environment variables. The TOX_TESTENV_PASSENV hasn't been useful for some time and by its mere presence wasted a lot of time today under the assumption that it's doing something. Instead, just rely on one variable FORCE_COLOR. If it's not honored, then that should be the fix upstream. --- .github/workflows/main.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30c9615d..f3028549 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,26 +6,8 @@ permissions: contents: read env: - # Environment variables to support color support (jaraco/skeleton#66): - # Request colored output from CLI tools supporting it. Different tools - # interpret the value differently. For some, just being set is sufficient. - # For others, it must be a non-zero integer. For yet others, being set - # to a non-empty value is sufficient. For tox, it must be one of - # , 0, 1, false, no, off, on, true, yes. The only enabling value - # in common is "1". + # Environment variable to support color support (jaraco/skeleton#66) FORCE_COLOR: 1 - # MyPy's color enforcement (must be a non-zero number) - MYPY_FORCE_COLOR: -42 - # Recognized by the `py` package, dependency of `pytest` (must be "1") - PY_COLORS: 1 - # Make tox-wrapped tools see color requests - TOX_TESTENV_PASSENV: >- - FORCE_COLOR - MYPY_FORCE_COLOR - NO_COLOR - PY_COLORS - PYTEST_THEME - PYTEST_THEME_MODE # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' From b02bf32bae729d53bdb7c9649d6ec36afdb793ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:27:03 -0400 Subject: [PATCH 016/221] Add diff-cover check to Github Actions CI. Closes jaraco/skeleton#90. --- .github/workflows/main.yml | 18 ++++++++++++++++++ tox.ini | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3028549..fa326a26 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,6 +53,24 @@ jobs: - name: Run run: tox + diffcov: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Evaluate coverage + run: tox + env: + TOXENV: diffcov + docs: runs-on: ubuntu-latest env: diff --git a/tox.ini b/tox.ini index e51d652d..3b4414b4 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,14 @@ usedevelop = True extras = testing +[testenv:diffcov] +deps = + diff-cover +commands = + pytest {posargs} --cov-report xml + diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 + [testenv:docs] extras = docs From a6256e2935468b72a61aa7fda1e036faef3bfb3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:59:47 -0400 Subject: [PATCH 017/221] Add descriptions to the tox environments. Closes jaraco/skeleton#91. --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 3b4414b4..1950b4ef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [testenv] +description = perform primary checks (tests, style, types, coverage) deps = setenv = PYTHONWARNDEFAULTENCODING = 1 @@ -9,6 +10,7 @@ extras = testing [testenv:diffcov] +description = run tests and check that diff from main is covered deps = diff-cover commands = @@ -17,6 +19,7 @@ commands = diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] +description = build the documentation extras = docs testing @@ -26,6 +29,7 @@ commands = python -m sphinxlint [testenv:finalize] +description = assemble changelog and tag a release skip_install = True deps = towncrier @@ -36,6 +40,7 @@ commands = [testenv:release] +description = publish the package to PyPI and GitHub skip_install = True deps = build From 928e9a86d61d3a660948bcba7689f90216cc8243 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 14:10:31 -0400 Subject: [PATCH 018/221] Add FORCE_COLOR to the TOX_OVERRIDE for GHA. Requires tox 4.11.1. Closes jaraco/skeleton#89. --- .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 fa326a26..28e36786 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ env: # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- - testenv.pass_env+=GITHUB_* + testenv.pass_env+=GITHUB_*,FORCE_COLOR jobs: From ca1831c2148fe5ddbffd001de76ff5f6005f812c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2023 11:05:36 -0400 Subject: [PATCH 019/221] Prefer ``pass_env`` in tox config. Preferred failure mode for tox-dev/tox#3127 and closes jaraco/skeleton#92. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1950b4ef..33da3deb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ skip_install = True deps = towncrier jaraco.develop >= 7.23 -passenv = * +pass_env = * commands = python -m jaraco.develop.finalize @@ -46,7 +46,7 @@ deps = build twine>=3 jaraco.develop>=7.1 -passenv = +pass_env = TWINE_PASSWORD GITHUB_TOKEN setenv = From a131f83e2967514e2973fb36f2ca64e3ac8efc3c Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Tue, 26 Sep 2023 11:23:08 +0200 Subject: [PATCH 020/221] 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 a0184424..23b4eb5a 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 6ca2332a..d5c24596 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 4a7033164d5bd4fe7ee4d96dae1c0cbfb122df9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 11:45:37 -0400 Subject: [PATCH 021/221] Clean up docstrings and remove crufty comments. Replace integer literals with booleans. --- distutils/dep_util.py | 91 ++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index 48da8641..3b3c830c 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -1,45 +1,45 @@ -"""distutils.dep_util +"""Timestamp comparison of files and groups of files.""" -Utility functions for simple, timestamp-based dependency of files -and groups of files; also, function based entirely on such -timestamp dependency analysis.""" +import os.path +import stat -import os from .errors import DistutilsFileError def newer(source, target): - """Return true if 'source' exists and is more recently modified than - 'target', or if 'source' exists and 'target' doesn't. Return false if - both exist and 'target' is the same age or younger than 'source'. - Raise DistutilsFileError if 'source' does not exist. + """ + Is source modified more recently than target. + + Returns True if 'source' is modified more recently than + 'target' or if 'target' does not exist. + + Raises DistutilsFileError if 'source' does not exist. """ if not os.path.exists(source): raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) - if not os.path.exists(target): - return 1 - from stat import ST_MTIME + if not os.path.exists(target): + return True - mtime1 = os.stat(source)[ST_MTIME] - mtime2 = os.stat(target)[ST_MTIME] + mtime1 = os.stat(source)[stat.ST_MTIME] + mtime2 = os.stat(target)[stat.ST_MTIME] return mtime1 > mtime2 -# newer () - - def newer_pairwise(sources, targets): - """Walk two filename lists in parallel, testing if each source is newer - than its corresponding target. Return a pair of lists (sources, + """ + Filter filenames where sources are newer than targets. + + Walk two filename lists in parallel, testing if each source is newer + than its corresponding target. Returns a pair of lists (sources, targets) where source is newer than target, according to the semantics of 'newer()'. """ if len(sources) != len(targets): raise ValueError("'sources' and 'targets' must be same length") - # build a pair of lists (sources, targets) where source is newer + # build a pair of lists (sources, targets) where source is newer n_sources = [] n_targets = [] for i in range(len(sources)): @@ -50,33 +50,31 @@ def newer_pairwise(sources, targets): return (n_sources, n_targets) -# newer_pairwise () - - def newer_group(sources, target, missing='error'): - """Return true if 'target' is out-of-date with respect to any file - listed in 'sources'. In other words, if 'target' exists and is newer - than every file in 'sources', return false; otherwise return true. - 'missing' controls what we do when a source file is missing; the - default ("error") is to blow up with an OSError from inside 'stat()'; - if it is "ignore", we silently drop any missing source files; if it is - "newer", any missing source files make us assume that 'target' is - out-of-date (this is handy in "dry-run" mode: it'll make you pretend to - carry out commands that wouldn't work because inputs are missing, but - that doesn't matter because you're not actually going to run the - commands). + """ + Is target out-of-date with respect to any file in sources. + + Return True if 'target' is out-of-date with respect to any file + listed in 'sources'. In other words, if 'target' exists and is newer + than every file in 'sources', return False; otherwise return True. + ``missing`` controls how to handle a missing source file: + + - error (default): allow the ``stat()`` call to fail. + - ignore: silently disregard any missing source files. + - newer: treat missing source files as "target out of date". This + mode is handy in "dry-run" mode: it will pretend to carry out + commands that wouldn't work because inputs are missing, but + that doesn't matter because dry-run won't run the commands. """ # If the target doesn't even exist, then it's definitely out-of-date. if not os.path.exists(target): - return 1 + return True - # Otherwise we have to find out the hard way: if *any* source file + # If *any* source file # is more recent than 'target', then 'target' is out-of-date and - # we can immediately return true. If we fall through to the end - # of the loop, then 'target' is up-to-date and we return false. - from stat import ST_MTIME - - target_mtime = os.stat(target)[ST_MTIME] + # we can immediately return True. If the loop completes, then + # 'target' is up-to-date. + target_mtime = os.stat(target)[stat.ST_MTIME] for source in sources: if not os.path.exists(source): if missing == 'error': # blow up when we stat() the file @@ -84,13 +82,10 @@ def newer_group(sources, target, missing='error'): elif missing == 'ignore': # missing source dropped from continue # target's dependency list elif missing == 'newer': # missing source means target is - return 1 # out-of-date + return True # out-of-date - source_mtime = os.stat(source)[ST_MTIME] + source_mtime = os.stat(source)[stat.ST_MTIME] if source_mtime > target_mtime: - return 1 + return True else: - return 0 - - -# newer_group () + return False From c4e27db944fc8ef08b215e593bbd328ce17bfff5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 12:35:52 -0400 Subject: [PATCH 022/221] "Refactor to newer_group to utilize higher level constructs ("any"), re-use _newer logic, and avoid complexity in branching." --- distutils/dep_util.py | 48 +++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index 3b3c830c..9250e937 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -6,6 +6,16 @@ from .errors import DistutilsFileError +def _newer(source, target): + if not os.path.exists(target): + return True + + mtime1 = os.stat(source)[stat.ST_MTIME] + mtime2 = os.stat(target)[stat.ST_MTIME] + + return mtime1 > mtime2 + + def newer(source, target): """ Is source modified more recently than target. @@ -18,13 +28,7 @@ def newer(source, target): if not os.path.exists(source): raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) - if not os.path.exists(target): - return True - - mtime1 = os.stat(source)[stat.ST_MTIME] - mtime2 = os.stat(target)[stat.ST_MTIME] - - return mtime1 > mtime2 + return _newer(source, target) def newer_pairwise(sources, targets): @@ -66,26 +70,12 @@ def newer_group(sources, target, missing='error'): commands that wouldn't work because inputs are missing, but that doesn't matter because dry-run won't run the commands. """ - # If the target doesn't even exist, then it's definitely out-of-date. - if not os.path.exists(target): - return True - # If *any* source file - # is more recent than 'target', then 'target' is out-of-date and - # we can immediately return True. If the loop completes, then - # 'target' is up-to-date. - target_mtime = os.stat(target)[stat.ST_MTIME] - for source in sources: - if not os.path.exists(source): - if missing == 'error': # blow up when we stat() the file - pass - elif missing == 'ignore': # missing source dropped from - continue # target's dependency list - elif missing == 'newer': # missing source means target is - return True # out-of-date - - source_mtime = os.stat(source)[stat.ST_MTIME] - if source_mtime > target_mtime: - return True - else: - return False + def missing_as_newer(source): + return missing == 'newer' and not os.path.exists(source) + + ignored = os.path.exists if missing == 'ignore' else None + return any( + missing_as_newer(source) or _newer(source, target) + for source in filter(ignored, sources) + ) From d7aa1884989cb8e57382553d4c39b7e2a48b12f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 12:38:06 -0400 Subject: [PATCH 023/221] Prefer os.path.getmtime --- distutils/dep_util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index 9250e937..f4f006c7 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -1,7 +1,6 @@ """Timestamp comparison of files and groups of files.""" import os.path -import stat from .errors import DistutilsFileError @@ -10,10 +9,7 @@ def _newer(source, target): if not os.path.exists(target): return True - mtime1 = os.stat(source)[stat.ST_MTIME] - mtime2 = os.stat(target)[stat.ST_MTIME] - - return mtime1 > mtime2 + return os.path.getmtime(source) > os.path.getmtime(target) def newer(source, target): From dfc8e609c9ca359d2c73815af511c2f286d3a92c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 12:38:57 -0400 Subject: [PATCH 024/221] Inline check for target presence. --- distutils/dep_util.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index f4f006c7..eec76c3c 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -6,10 +6,9 @@ def _newer(source, target): - if not os.path.exists(target): - return True - - return os.path.getmtime(source) > os.path.getmtime(target) + return not os.path.exists(target) or ( + os.path.getmtime(source) > os.path.getmtime(target) + ) def newer(source, target): From bdffb48680406e6a8033f35cc68b061f7765d2be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 12:59:15 -0400 Subject: [PATCH 025/221] Add test for newer_pairwise, bringing coverage in dep_util to 100%. --- distutils/tests/test_dep_util.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_dep_util.py b/distutils/tests/test_dep_util.py index e5dcad94..759772d2 100644 --- a/distutils/tests/test_dep_util.py +++ b/distutils/tests/test_dep_util.py @@ -27,7 +27,7 @@ def test_newer(self): # than 'new_file'. assert not newer(old_file, new_file) - def test_newer_pairwise(self): + def _setup_1234(self): tmpdir = self.mkdtemp() sources = os.path.join(tmpdir, 'sources') targets = os.path.join(tmpdir, 'targets') @@ -40,9 +40,22 @@ def test_newer_pairwise(self): self.write_file(one) self.write_file(two) self.write_file(four) + return one, two, three, four + + def test_newer_pairwise(self): + one, two, three, four = self._setup_1234() assert newer_pairwise([one, two], [three, four]) == ([one], [three]) + def test_newer_pairwise_mismatch(self): + one, two, three, four = self._setup_1234() + + with pytest.raises(ValueError): + newer_pairwise([one], [three, four]) + + with pytest.raises(ValueError): + newer_pairwise([one, two], [three]) + def test_newer_group(self): tmpdir = self.mkdtemp() sources = os.path.join(tmpdir, 'sources') From 0720b98908e0a6143c4fe260f3b154cf4426c8bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 13:07:05 -0400 Subject: [PATCH 026/221] Replace for/append loop with a filter function (newer_pair). --- distutils/dep_util.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index eec76c3c..18aeae46 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -38,15 +38,11 @@ def newer_pairwise(sources, targets): if len(sources) != len(targets): raise ValueError("'sources' and 'targets' must be same length") - # build a pair of lists (sources, targets) where source is newer - n_sources = [] - n_targets = [] - for i in range(len(sources)): - if newer(sources[i], targets[i]): - n_sources.append(sources[i]) - n_targets.append(targets[i]) - - return (n_sources, n_targets) + def newer_pair(pair): + return newer(*pair) + + newer_pairs = filter(newer_pair, zip(sources, targets)) + return tuple(map(list, zip(*newer_pairs))) def newer_group(sources, target, missing='error'): From 131eff757c51fa8781404a8f1d46c358804a0ce7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 13:14:24 -0400 Subject: [PATCH 027/221] Replace explicit list check with zip(strict=True). Allows inputs to be iterables. --- distutils/dep_util.py | 7 +++---- distutils/py39compat.py | 46 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index 18aeae46..d8538b50 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -3,6 +3,7 @@ import os.path from .errors import DistutilsFileError +from .py39compat import zip_strict def _newer(source, target): @@ -30,18 +31,16 @@ def newer_pairwise(sources, targets): """ Filter filenames where sources are newer than targets. - Walk two filename lists in parallel, testing if each source is newer + Walk two filename iterables in parallel, testing if each source is newer than its corresponding target. Returns a pair of lists (sources, targets) where source is newer than target, according to the semantics of 'newer()'. """ - if len(sources) != len(targets): - raise ValueError("'sources' and 'targets' must be same length") def newer_pair(pair): return newer(*pair) - newer_pairs = filter(newer_pair, zip(sources, targets)) + newer_pairs = filter(newer_pair, zip_strict(sources, targets)) return tuple(map(list, zip(*newer_pairs))) diff --git a/distutils/py39compat.py b/distutils/py39compat.py index c43e5f10..1b436d76 100644 --- a/distutils/py39compat.py +++ b/distutils/py39compat.py @@ -1,5 +1,7 @@ -import sys +import functools +import itertools import platform +import sys def add_ext_suffix_39(vars): @@ -20,3 +22,45 @@ def add_ext_suffix_39(vars): needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows' add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None + + +# from more_itertools +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format(*details) + + super().__init__(msg) + + +# from more_itertools +def _zip_equal_generator(iterables): + _marker = object() + for combo in itertools.zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +# from more_itertools +def _zip_equal(*iterables): + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_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: + return _zip_equal_generator(iterables) + + +zip_strict = ( + _zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True) +) From 4d82dc4a053c7e8b7a5720b5a4db7da2ca2ea912 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 13:24:00 -0400 Subject: [PATCH 028/221] Extract a 'starfilter', similar to itertools.starmap, to generalize the concept of filtering results over a sequence of tuples. --- distutils/dep_util.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/distutils/dep_util.py b/distutils/dep_util.py index d8538b50..c1ae3297 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -27,6 +27,13 @@ def newer(source, target): return _newer(source, target) +def _starfilter(pred, iterables): + """ + Like itertools.starmap but for filter. + """ + return filter(lambda x: pred(*x), iterables) + + def newer_pairwise(sources, targets): """ Filter filenames where sources are newer than targets. @@ -36,11 +43,7 @@ def newer_pairwise(sources, targets): targets) where source is newer than target, according to the semantics of 'newer()'. """ - - def newer_pair(pair): - return newer(*pair) - - newer_pairs = filter(newer_pair, zip_strict(sources, targets)) + newer_pairs = _starfilter(newer, zip_strict(sources, targets)) return tuple(map(list, zip(*newer_pairs))) From 5deb5ac17329a44b720c55b9f006858607cfbb3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 21:47:23 -0400 Subject: [PATCH 029/221] Replace '_starfilter' with 'jaraco.functools.splat'. --- distutils/_functools.py | 53 +++++++++++++++++++++++++++++++++++++++++ distutils/dep_util.py | 10 ++------ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/distutils/_functools.py b/distutils/_functools.py index e7053bac..e03365ea 100644 --- a/distutils/_functools.py +++ b/distutils/_functools.py @@ -1,3 +1,4 @@ +import collections.abc import functools @@ -18,3 +19,55 @@ def wrapper(param, *args, **kwargs): return func(param, *args, **kwargs) return wrapper + + +# from jaraco.functools 4.0 +@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``. + + >>> import itertools, operator + >>> pairs = [(-1, 1), (0, 2)] + >>> _ = tuple(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> _ = tuple(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/distutils/dep_util.py b/distutils/dep_util.py index c1ae3297..18a4f2b2 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -4,6 +4,7 @@ from .errors import DistutilsFileError from .py39compat import zip_strict +from ._functools import splat def _newer(source, target): @@ -27,13 +28,6 @@ def newer(source, target): return _newer(source, target) -def _starfilter(pred, iterables): - """ - Like itertools.starmap but for filter. - """ - return filter(lambda x: pred(*x), iterables) - - def newer_pairwise(sources, targets): """ Filter filenames where sources are newer than targets. @@ -43,7 +37,7 @@ def newer_pairwise(sources, targets): targets) where source is newer than target, according to the semantics of 'newer()'. """ - newer_pairs = _starfilter(newer, zip_strict(sources, targets)) + newer_pairs = filter(splat(newer), zip_strict(sources, targets)) return tuple(map(list, zip(*newer_pairs))) From 94942032878d431cee55adaab12a8bd83549a833 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 05:59:02 -0500 Subject: [PATCH 030/221] Move dep_util to _modified and mark dep_util as deprecated. --- distutils/_modified.py | 68 +++++++++++++++++ distutils/bcppcompiler.py | 2 +- distutils/ccompiler.py | 2 +- distutils/cmd.py | 4 +- distutils/command/build_ext.py | 2 +- distutils/command/build_scripts.py | 2 +- distutils/dep_util.py | 74 +++---------------- distutils/file_util.py | 2 +- .../{test_dep_util.py => test_modified.py} | 4 +- distutils/unixccompiler.py | 2 +- distutils/util.py | 2 +- 11 files changed, 89 insertions(+), 75 deletions(-) create mode 100644 distutils/_modified.py rename distutils/tests/{test_dep_util.py => test_modified.py} (96%) diff --git a/distutils/_modified.py b/distutils/_modified.py new file mode 100644 index 00000000..18a4f2b2 --- /dev/null +++ b/distutils/_modified.py @@ -0,0 +1,68 @@ +"""Timestamp comparison of files and groups of files.""" + +import os.path + +from .errors import DistutilsFileError +from .py39compat import zip_strict +from ._functools import splat + + +def _newer(source, target): + return not os.path.exists(target) or ( + os.path.getmtime(source) > os.path.getmtime(target) + ) + + +def newer(source, target): + """ + Is source modified more recently than target. + + Returns True if 'source' is modified more recently than + 'target' or if 'target' does not exist. + + Raises DistutilsFileError if 'source' does not exist. + """ + if not os.path.exists(source): + raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) + + return _newer(source, target) + + +def newer_pairwise(sources, targets): + """ + Filter filenames where sources are newer than targets. + + Walk two filename iterables in parallel, testing if each source is newer + than its corresponding target. Returns a pair of lists (sources, + targets) where source is newer than target, according to the semantics + of 'newer()'. + """ + newer_pairs = filter(splat(newer), zip_strict(sources, targets)) + return tuple(map(list, zip(*newer_pairs))) + + +def newer_group(sources, target, missing='error'): + """ + Is target out-of-date with respect to any file in sources. + + Return True if 'target' is out-of-date with respect to any file + listed in 'sources'. In other words, if 'target' exists and is newer + than every file in 'sources', return False; otherwise return True. + ``missing`` controls how to handle a missing source file: + + - error (default): allow the ``stat()`` call to fail. + - ignore: silently disregard any missing source files. + - newer: treat missing source files as "target out of date". This + mode is handy in "dry-run" mode: it will pretend to carry out + commands that wouldn't work because inputs are missing, but + that doesn't matter because dry-run won't run the commands. + """ + + def missing_as_newer(source): + return missing == 'newer' and not os.path.exists(source) + + ignored = os.path.exists if missing == 'ignore' else None + return any( + missing_as_newer(source) or _newer(source, target) + for source in filter(ignored, sources) + ) diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index ba45ea2b..3c2ba154 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -24,7 +24,7 @@ ) from .ccompiler import CCompiler, gen_preprocess_options from .file_util import write_file -from .dep_util import newer +from ._modified import newer from ._log import log diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 1818fce9..c1c7d547 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -18,7 +18,7 @@ from .spawn import spawn from .file_util import move_file from .dir_util import mkpath -from .dep_util import newer_group +from ._modified import newer_group from .util import split_quoted, execute from ._log import log diff --git a/distutils/cmd.py b/distutils/cmd.py index 3860c3ff..8fdcbc0e 100644 --- a/distutils/cmd.py +++ b/distutils/cmd.py @@ -10,7 +10,7 @@ import logging from .errors import DistutilsOptionError -from . import util, dir_util, file_util, archive_util, dep_util +from . import util, dir_util, file_util, archive_util, _modified from ._log import log @@ -428,7 +428,7 @@ def make_file( # If 'outfile' must be regenerated (either because it doesn't # exist, is out-of-date, or the 'force' flag is true) then # perform the action that presumably regenerates it - if self.force or dep_util.newer_group(infiles, outfile): + if self.force or _modified.newer_group(infiles, outfile): self.execute(func, args, exec_msg, level) # Otherwise, print the "skip" message else: diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index fbeec342..b48f4626 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -19,7 +19,7 @@ ) from ..sysconfig import customize_compiler, get_python_version from ..sysconfig import get_config_h_filename -from ..dep_util import newer_group +from .._modified import newer_group from ..extension import Extension from ..util import get_platform from distutils._log import log diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py index ce222f1e..1a4d67f4 100644 --- a/distutils/command/build_scripts.py +++ b/distutils/command/build_scripts.py @@ -7,7 +7,7 @@ from stat import ST_MODE from distutils import sysconfig from ..core import Command -from ..dep_util import newer +from .._modified import newer from ..util import convert_path from distutils._log import log import tokenize diff --git a/distutils/dep_util.py b/distutils/dep_util.py index 18a4f2b2..09a8a2e1 100644 --- a/distutils/dep_util.py +++ b/distutils/dep_util.py @@ -1,68 +1,14 @@ -"""Timestamp comparison of files and groups of files.""" +import warnings -import os.path +from . import _modified -from .errors import DistutilsFileError -from .py39compat import zip_strict -from ._functools import splat - -def _newer(source, target): - return not os.path.exists(target) or ( - os.path.getmtime(source) > os.path.getmtime(target) - ) - - -def newer(source, target): - """ - Is source modified more recently than target. - - Returns True if 'source' is modified more recently than - 'target' or if 'target' does not exist. - - Raises DistutilsFileError if 'source' does not exist. - """ - if not os.path.exists(source): - raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source)) - - return _newer(source, target) - - -def newer_pairwise(sources, targets): - """ - Filter filenames where sources are newer than targets. - - Walk two filename iterables in parallel, testing if each source is newer - than its corresponding target. Returns a pair of lists (sources, - targets) where source is newer than target, according to the semantics - of 'newer()'. - """ - newer_pairs = filter(splat(newer), zip_strict(sources, targets)) - return tuple(map(list, zip(*newer_pairs))) - - -def newer_group(sources, target, missing='error'): - """ - Is target out-of-date with respect to any file in sources. - - Return True if 'target' is out-of-date with respect to any file - listed in 'sources'. In other words, if 'target' exists and is newer - than every file in 'sources', return False; otherwise return True. - ``missing`` controls how to handle a missing source file: - - - error (default): allow the ``stat()`` call to fail. - - ignore: silently disregard any missing source files. - - newer: treat missing source files as "target out of date". This - mode is handy in "dry-run" mode: it will pretend to carry out - commands that wouldn't work because inputs are missing, but - that doesn't matter because dry-run won't run the commands. - """ - - def missing_as_newer(source): - return missing == 'newer' and not os.path.exists(source) - - ignored = os.path.exists if missing == 'ignore' else None - return any( - missing_as_newer(source) or _newer(source, target) - for source in filter(ignored, sources) +def __getattr__(name): + if name not in ['newer', 'newer_group', 'newer_pairwise']: + raise AttributeError(name) + warnings.warn( + "dep_util is Deprecated. Use functions from setuptools instead.", + DeprecationWarning, + stacklevel=2, ) + return getattr(_modified, name) diff --git a/distutils/file_util.py b/distutils/file_util.py index 7c699066..3f3e21b5 100644 --- a/distutils/file_util.py +++ b/distutils/file_util.py @@ -108,7 +108,7 @@ def copy_file( # noqa: C901 # changing it (ie. it's not already a hard/soft link to src OR # (not update) and (src newer than dst). - from distutils.dep_util import newer + from distutils._modified import newer from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE if not os.path.isfile(src): diff --git a/distutils/tests/test_dep_util.py b/distutils/tests/test_modified.py similarity index 96% rename from distutils/tests/test_dep_util.py rename to distutils/tests/test_modified.py index 759772d2..eae7a7fa 100644 --- a/distutils/tests/test_dep_util.py +++ b/distutils/tests/test_modified.py @@ -1,7 +1,7 @@ -"""Tests for distutils.dep_util.""" +"""Tests for distutils._modified.""" import os -from distutils.dep_util import newer, newer_pairwise, newer_group +from distutils._modified import newer, newer_pairwise, newer_group from distutils.errors import DistutilsFileError from distutils.tests import support import pytest diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index 6ca2332a..bd8db9ac 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -20,7 +20,7 @@ import itertools from . import sysconfig -from .dep_util import newer +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 diff --git a/distutils/util.py b/distutils/util.py index 7ef47176..7ae914f7 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -14,7 +14,7 @@ import functools from .errors import DistutilsPlatformError, DistutilsByteCompileError -from .dep_util import newer +from ._modified import newer from .spawn import spawn from ._log import log From ce9efc41ec587d2f111fe09a4d855ffad15f95fc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 06:19:43 -0500 Subject: [PATCH 031/221] Extend tests for newer_pairwise and fix failed expectation when no files are newer. --- distutils/_modified.py | 2 +- distutils/tests/test_modified.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/distutils/_modified.py b/distutils/_modified.py index 18a4f2b2..41ab1df3 100644 --- a/distutils/_modified.py +++ b/distutils/_modified.py @@ -38,7 +38,7 @@ def newer_pairwise(sources, targets): of 'newer()'. """ newer_pairs = filter(splat(newer), zip_strict(sources, targets)) - return tuple(map(list, zip(*newer_pairs))) + return tuple(map(list, zip(*newer_pairs))) or ([], []) def newer_group(sources, target, missing='error'): diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index eae7a7fa..34ced956 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -56,6 +56,14 @@ def test_newer_pairwise_mismatch(self): with pytest.raises(ValueError): newer_pairwise([one, two], [three]) + def test_newer_pairwise_empty(self): + assert newer_pairwise([], []) == ([], []) + + def test_newer_pairwise_fresh(self): + one, two, three, four = self._setup_1234() + + assert newer_pairwise([one, three], [two, four]) == ([], []) + def test_newer_group(self): tmpdir = self.mkdtemp() sources = os.path.join(tmpdir, 'sources') From 2972e29ad43eb08241fd8ebebff1437b8d8dafb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 06:05:59 -0500 Subject: [PATCH 032/221] Add newer_pairwise_group (inspired by setuptools.dep_util). --- distutils/_modified.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/distutils/_modified.py b/distutils/_modified.py index 41ab1df3..fbb95a8f 100644 --- a/distutils/_modified.py +++ b/distutils/_modified.py @@ -1,5 +1,6 @@ """Timestamp comparison of files and groups of files.""" +import functools import os.path from .errors import DistutilsFileError @@ -28,7 +29,7 @@ def newer(source, target): return _newer(source, target) -def newer_pairwise(sources, targets): +def newer_pairwise(sources, targets, newer=newer): """ Filter filenames where sources are newer than targets. @@ -66,3 +67,6 @@ def missing_as_newer(source): missing_as_newer(source) or _newer(source, target) for source in filter(ignored, sources) ) + + +newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group) From 378d0d5ab16baa75acc6bb91ce7eb64f5f6ea91a Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 15 Jan 2017 01:36:02 +0000 Subject: [PATCH 033/221] Added tests for newer_pairwise_group(). Cherry-picked from pypa/setuptools@a40114a442e18cd29271bd3c37dbfcaf6a2ec817. --- distutils/tests/test_modified.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index 34ced956..87b3ecde 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -1,7 +1,7 @@ """Tests for distutils._modified.""" import os -from distutils._modified import newer, newer_pairwise, newer_group +from distutils._modified import newer, newer_pairwise, newer_group, newer_pairwise_group from distutils.errors import DistutilsFileError from distutils.tests import support import pytest @@ -89,3 +89,30 @@ def test_newer_group(self): assert not newer_group([one, two, old_file], three, missing='ignore') assert newer_group([one, two, old_file], three, missing='newer') + + +@pytest.fixture +def groups_target(tmpdir): + """Sets up some older sources, a target and newer sources. + Returns a 3-tuple in this order. + """ + creation_order = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] + mtime = 0 + + for i in range(len(creation_order)): + creation_order[i] = os.path.join(str(tmpdir), creation_order[i]) + with open(creation_order[i], 'w'): + pass + + # make sure modification times are sequential + os.utime(creation_order[i], (mtime, mtime)) + mtime += 1 + + return creation_order[:2], creation_order[2], creation_order[3:] + + +def test_newer_pairwise_group(groups_target): + older = newer_pairwise_group([groups_target[0]], [groups_target[1]]) + newer = newer_pairwise_group([groups_target[2]], [groups_target[1]]) + assert older == ([], []) + assert newer == ([groups_target[2]], [groups_target[1]]) From 501b753d153d5e6ca51a55d7f9b256bc3518c98a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 06:35:18 -0500 Subject: [PATCH 034/221] Modernize test_newer_pairwise_group by using tmp_path and a SimpleNamespace. --- distutils/tests/test_modified.py | 33 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index 87b3ecde..ca07c7e8 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -1,10 +1,12 @@ """Tests for distutils._modified.""" import os +import types + +import pytest from distutils._modified import newer, newer_pairwise, newer_group, newer_pairwise_group from distutils.errors import DistutilsFileError from distutils.tests import support -import pytest class TestDepUtil(support.TempdirManager): @@ -92,27 +94,26 @@ def test_newer_group(self): @pytest.fixture -def groups_target(tmpdir): - """Sets up some older sources, a target and newer sources. - Returns a 3-tuple in this order. +def groups_target(tmp_path): + """ + Set up some older sources, a target, and newer sources. + + Returns a simple namespace with these values. """ - creation_order = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] - mtime = 0 + filenames = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] + paths = [tmp_path / name for name in filenames] - for i in range(len(creation_order)): - creation_order[i] = os.path.join(str(tmpdir), creation_order[i]) - with open(creation_order[i], 'w'): - pass + for mtime, path in enumerate(paths): + path.write_text('', encoding='utf-8') # make sure modification times are sequential - os.utime(creation_order[i], (mtime, mtime)) - mtime += 1 + os.utime(path, (mtime, mtime)) - return creation_order[:2], creation_order[2], creation_order[3:] + return types.SimpleNamespace(older=paths[:2], target=paths[2], newer=paths[3:]) def test_newer_pairwise_group(groups_target): - older = newer_pairwise_group([groups_target[0]], [groups_target[1]]) - newer = newer_pairwise_group([groups_target[2]], [groups_target[1]]) + older = newer_pairwise_group([groups_target.older], [groups_target.target]) + newer = newer_pairwise_group([groups_target.newer], [groups_target.target]) assert older == ([], []) - assert newer == ([groups_target[2]], [groups_target[1]]) + assert newer == ([groups_target.newer], [groups_target.target]) From 603932219176de7449af496b724dd8e58d4589d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:01:38 -0500 Subject: [PATCH 035/221] Remove latent references in docs. --- docs/distutils/configfile.rst | 7 ------- docs/distutils/packageindex.rst | 13 ++++++------- docs/distutils/uploading.rst | 5 +++-- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/distutils/configfile.rst b/docs/distutils/configfile.rst index bdd7c455..30cccd71 100644 --- a/docs/distutils/configfile.rst +++ b/docs/distutils/configfile.rst @@ -131,13 +131,6 @@ Note that the ``doc_files`` option is simply a whitespace-separated string split across multiple lines for readability. -.. seealso:: - - :ref:`inst-config-syntax` in "Installing Python Modules" - More information on the configuration files is available in the manual for - system administrators. - - .. rubric:: Footnotes .. [#] This ideal probably won't be achieved until auto-configuration is fully diff --git a/docs/distutils/packageindex.rst b/docs/distutils/packageindex.rst index ccb9a598..27ea717a 100644 --- a/docs/distutils/packageindex.rst +++ b/docs/distutils/packageindex.rst @@ -6,11 +6,10 @@ The Python Package Index (PyPI) ******************************* -The `Python Package Index (PyPI)`_ stores metadata describing distributions -packaged with distutils and other publishing tools, as well the distribution -archives themselves. +The `Python Package Index (PyPI) `_ stores +metadata describing distributions packaged with distutils and +other publishing tools, as well the distribution archives +themselves. -References to up to date PyPI documentation can be found at -:ref:`publishing-python-packages`. - -.. _Python Package Index (PyPI): https://pypi.org +The best resource for working with PyPI is the +`Python Packaging User Guide `_. diff --git a/docs/distutils/uploading.rst b/docs/distutils/uploading.rst index 4c391cab..f5c4c619 100644 --- a/docs/distutils/uploading.rst +++ b/docs/distutils/uploading.rst @@ -4,5 +4,6 @@ Uploading Packages to the Package Index *************************************** -References to up to date PyPI documentation can be found at -:ref:`publishing-python-packages`. +See the +`Python Packaging User Guide `_ +for the best guidance on uploading packages. From 03f03e7802b0842b41f70b2b1c17ab26551a7533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:43:46 -0500 Subject: [PATCH 036/221] Limit sphinxlint jobs to 1. Workaround for sphinx-contrib/sphinx-lint#83. --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 33da3deb..331eeed9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,9 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint + python -m sphinxlint \ + # workaround for sphinx-contrib/sphinx-lint#83 + --jobs 1 [testenv:finalize] description = assemble changelog and tag a release From 41b9cce3d3ee81e929610ab95b928dfd08bbba22 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:58:16 -0500 Subject: [PATCH 037/221] Replace git version with released version. Ref #186. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 06657e4e..6a224b52 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,9 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = - # pypa/distutils#186; workaround for pytest-dev/pytest#10447 - pytest @ git+https://github.com/RonnyPfannschmidt/pytest@fix-10447-maker-mro-order-needs-reverse + pytest \ + # required for #186 + >= 7.4.3 pytest-flake8 # workaround for tholo/pytest-flake8#87 From d3e5de05f6afe958d0fde20945ed0f7a2dfef270 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 May 2023 21:38:33 -0400 Subject: [PATCH 038/221] Disable cygwin tests for now. Ref pypa/setuptools#3921 --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60801ace..c420a976 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,6 +37,8 @@ jobs: run: tox test_cygwin: + # disabled due to lack of Rust support pypa/setuptools#3921 + if: ${{ false }} strategy: matrix: python: From d23e28a03a2c120e204c4c788ecd316e0dfe8fbb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 10:54:57 -0500 Subject: [PATCH 039/221] Disable integration test due to known breakage from deprecation warnings. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c420a976..cb85ffe6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,6 +66,7 @@ jobs: ci_setuptools: # Integration testing with setuptools + if: ${{ false }} # disabled for deprecation warnings strategy: matrix: python: From 03c6392c21800f51010d805b98aee7eb406f9c79 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 13:08:21 -0500 Subject: [PATCH 040/221] Allow diffcov to fail also, as it requires the tests to pass on the latest Python to succeed. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 158814e5..4d9b8a3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,6 +58,7 @@ jobs: diffcov: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.python == '3.12' }} steps: - uses: actions/checkout@v3 with: From 6e6ee9759da3e71c9e90920c2bb91b2a27df3dfc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 13:10:07 -0500 Subject: [PATCH 041/221] Remove newsfragment --- newsfragments/+drop-py37.feature.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. From 7a04cbda0fc71487af84e1d35055b736e339a6d6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 15:57:33 -0500 Subject: [PATCH 042/221] Copy concurrency setting from setuptools --- .github/workflows/main.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d9b8a3e..e36084a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,13 @@ name: tests on: [push, pull_request] +concurrency: + group: >- + ${{ github.workflow }}- + ${{ github.ref_type }}- + ${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + permissions: contents: read @@ -18,10 +25,6 @@ env: TOX_OVERRIDE: >- testenv.pass_env+=GITHUB_*,FORCE_COLOR -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true - jobs: test: strategy: From 75d9cc1b7cb6f84e7a16a83ec3abb9a478fdb130 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 19:57:45 +0600 Subject: [PATCH 043/221] Upgrade GitHub Actions checkout (jaraco/skeleton#94) Also, upgrade from `pypy3.9` to `pypy3.10` and remove the `continue-on-error` for Python 3.12. As recommended at jaraco/cssutils#41 --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28e36786..10828667 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,12 +36,12 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - - python: pypy3.9 + - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.12' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: @@ -56,7 +56,7 @@ jobs: diffcov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python @@ -76,7 +76,7 @@ jobs: env: TOXENV: docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 - name: Install tox @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: From 5732ebeeaa9480f8cd80c96a3183d7b247f27214 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 20:08:10 +0600 Subject: [PATCH 044/221] GitHub Actions: Combine tox jobs diffcov and docs (jaraco/skeleton#95) Code reuse Co-authored-by: Jason R. Coombs --- .github/workflows/main.yml | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10828667..9682985c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,12 +48,15 @@ jobs: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox - diffcov: + collateral: + strategy: + fail-fast: false + matrix: + job: [diffcov, docs] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -64,33 +67,16 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox - - name: Evaluate coverage - run: tox - env: - TOXENV: diffcov - - docs: - runs-on: ubuntu-latest - env: - TOXENV: docs - steps: - - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v4 - - name: Install tox - run: | - python -m pip install tox - - name: Run - run: tox + run: python -m pip install tox + - name: Eval ${{ matrix.job }} + run: tox -e ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() needs: - test - - docs + - collateral runs-on: ubuntu-latest @@ -115,8 +101,7 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox -e release env: 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 045/221] =?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 3ed608b4..696f2675 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 f9f9f2d8..724986d8 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 4a702fb9..3fd2c7e2 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 1a5753c7..28cc5632 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 fdb768e7..a3fa2902 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 bd8db9ac..294a16b7 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 83b8ef5d..beb17bc3 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 28582bab..d758a810 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 3c8e1ab1..71d2439f 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 26f420a97e73a2ab695023f6cc21f5c786d2b289 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Nov 2023 11:43:20 -0500 Subject: [PATCH 046/221] Remove news fragment after allowing time to be processed downstream. --- newsfragments/+drop-py37.feature.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. From 33dd01267b6a886217bae3ebd5df5b689e2ab722 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Nov 2023 13:21:17 -0500 Subject: [PATCH 047/221] Suppress deprecation warning in dateutil. Workaround for dateutil/dateutil#1284. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index d9a15ed1..f9533b57 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,4 +24,7 @@ filterwarnings= # pypa/build#615 ignore:'encoding' argument not specified::build.env + # dateutil/dateutil#1284 + ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz + ## end upstream From 97a5f44787ac5a928534cdf724210c429621435c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:53:37 -0500 Subject: [PATCH 048/221] Update Github Actions badge per actions/starter-workflows#1525. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b703d490..41bcfbe8 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ .. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg -.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg +.. image:: https://github.com/PROJECT_PATH/actions/workflows/main.yml/badge.svg :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests From 8bff8b034a0bbf0273a38f0a0cc41e3a52b26864 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 5 Dec 2023 15:48:52 +0100 Subject: [PATCH 049/221] Enable testing merge queues @ GitHub Actions CI/CD (jaraco/skeleton#93) This allows org-hosted projects to start enabling merge queues in the repository settings. With that, GitHub would trigger a separate event against a merge commit derived from merging several pull requests with the target branch. --- .github/workflows/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9682985c..387d01aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,11 @@ name: tests -on: [push, pull_request] +on: + merge_group: + push: + branches-ignore: + - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + pull_request: permissions: contents: read From e4bd6091a1fbe26fe113051f0f47875d627c7ed2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Dec 2023 10:46:32 -0500 Subject: [PATCH 050/221] Separate collateral jobs on different lines for easier override/extension. --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 387d01aa..a079bbfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,9 @@ jobs: strategy: fail-fast: false matrix: - job: [diffcov, docs] + job: + - diffcov + - docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 596e6834c8a037c935338afe92e0b9c5ffa1768f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Dec 2023 18:29:16 -0500 Subject: [PATCH 051/221] Drop minimum requirement on pytest-mypy as most environments are already running much later. Closes jaraco/skeleton#96. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4f184c7e..20c5dd76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov - pytest-mypy >= 0.9.1; \ + pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 From b8c6c1530ef937521b60aabb0ecd98a8b5dca761 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:25:02 +0100 Subject: [PATCH 052/221] Use the ruff formatter (jaraco/skeleton#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use the ruff formatter, instead of black Based on: - ruff-pre-commit README.md | Using Ruff with pre-commit https://github.com/astral-sh/ruff-pre-commit/blob/main/README.md - The Ruff Formatter | Conflicting lint rules https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules Support for the ruff formatter was added to pytest-ruff by commits from October 2023, released the same day as versions 0.2 and 0.2.1. Hence, it makes sense to require pytest-ruff ≥ 0.2.1 now. Support for `quote-style = "preserve"` was added to ruff in the last couple of weeks, therefore require the latest version, ruff ≥ 0.1.8. This option is equivalent to `skip-string-normalization` in black. Closes jaraco/skeleton#101. --------- Co-authored-by: Jason R. Coombs --- .pre-commit-config.yaml | 7 ++++--- README.rst | 4 ---- pyproject.toml | 3 --- pytest.ini | 8 -------- ruff.toml | 22 ++++++++++++++++++++++ setup.cfg | 5 +---- 6 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af502010..5a4a7e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ repos: -- repo: https://github.com/psf/black - rev: 22.6.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 hooks: - - id: black + - id: ruff + - id: ruff-format diff --git a/README.rst b/README.rst index 41bcfbe8..2fabcf33 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,6 @@ :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 - .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest diff --git a/pyproject.toml b/pyproject.toml index dce944df..a853c578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,4 @@ requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" -[tool.black] -skip-string-normalization = true - [tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index f9533b57..022a723e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,14 +7,6 @@ filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning - # shopkeep/pytest-black#55 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning - ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - - # shopkeep/pytest-black#67 - ignore:'encoding' argument not specified::pytest_black - # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..7ed133b7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,22 @@ +[lint] +extend-ignore = [ + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] + +[format] +# https://docs.astral.sh/ruff/settings/#format-quote-style +quote-style = "preserve" diff --git a/setup.cfg b/setup.cfg index 20c5dd76..1d2729be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,15 +30,12 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-black >= 0.3.7; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" pytest-cov pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 - pytest-ruff + pytest-ruff >= 0.2.1 # local From a9c5dd5a4eab9f4132d62344cdbad24e077c650e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 Dec 2023 12:08:46 -0500 Subject: [PATCH 053/221] Remove sole entry for branches-ignore. Workaround for and closes jaraco/skeleton#103. --- .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 a079bbfb..cf94f7d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,8 @@ on: merge_group: push: branches-ignore: - - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # disabled for jaraco/skeleton#103 + # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches pull_request: permissions: From db0d581685d4fc2a16d392d4dedffe622e9a355c Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:58:23 +0100 Subject: [PATCH 054/221] =?UTF-8?q?ruff:=20extended-ignore=20=E2=86=92=20i?= =?UTF-8?q?gnore=20(jaraco/skeleton#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies Repo-Review suggestion: RF201: Avoid using deprecated config settings extend-ignore deprecated, use ignore instead (identical) --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 7ed133b7..795cca16 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -extend-ignore = [ +ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", From f6d9e107365ca270ec843898c05bb8e43dc6987a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Jan 2024 17:56:53 -0500 Subject: [PATCH 055/221] Bump year on badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2fabcf33..efabeee4 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2023-informational +.. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton From ff32ae0b43340341719b6b1b0ff15b7598a8644f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 16:57:08 -0500 Subject: [PATCH 056/221] 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 27e73393..fdec5a96 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 b5a392a8..98ab0b17 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 cb61ad74..3c83cca4 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 e72a7c5f..ecb85102 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 3f525db4..082ee1d3 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 057/221] 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 574ffc28..68c38ac9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,6 @@ install_requires = [options.packages.find] exclude = - build* - dist* docs* tests* From dbcb0747110d074112f27e2699856acfc4ba8ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:09:59 -0500 Subject: [PATCH 058/221] 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 1d2729be..c2e82875 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 059/221] 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 fdec5a96..85293cbb 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 060/221] 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 7b9cc692..213558aa 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 061/221] 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 ff2aade0..68c38ac9 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 d27890573088a6a0292139c5e30466debd7dc1dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:26:16 -0500 Subject: [PATCH 062/221] Exclude docs and tests directories properly per Setuptools behavior. --- setup.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index c2e82875..c5aa1af9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,11 @@ install_requires = [options.packages.find] exclude = - docs* - tests* + # duplicate exclusions for pypa/setuptools#2688 + docs + docs.* + tests + tests.* [options.extras_require] testing = From 63535c6efd3516a7ef35c862c24ef5b6d43c8494 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:49:05 -0500 Subject: [PATCH 063/221] Rely on default discovery for good heuristics for finding packages. --- setup.cfg | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index c5aa1af9..fe99eaf6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,19 +13,10 @@ classifiers = Programming Language :: Python :: 3 :: Only [options] -packages = find_namespace: include_package_data = true python_requires = >=3.8 install_requires = -[options.packages.find] -exclude = - # duplicate exclusions for pypa/setuptools#2688 - docs - docs.* - tests - tests.* - [options.extras_require] testing = # upstream From 2ad8784dfeb816829995613fb5fd9818f3e88922 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Tue, 4 Oct 2022 13:49:49 +0530 Subject: [PATCH 064/221] Add support for building extensions using MinGW compilers --- distutils/ccompiler.py | 6 +++++- distutils/command/build_ext.py | 8 ++++---- distutils/cygwinccompiler.py | 4 ++-- distutils/sysconfig.py | 9 ++++++--- distutils/util.py | 9 +++++++++ 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index c1c7d547..dba2e615 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -19,7 +19,7 @@ from .file_util import move_file from .dir_util import mkpath from ._modified import newer_group -from .util import split_quoted, execute +from .util import split_quoted, execute, is_mingw from ._log import log @@ -1076,6 +1076,10 @@ def get_default_compiler(osname=None, platform=None): osname = os.name if platform is None: platform = sys.platform + # Mingw is a special case where sys.platform is 'win32' but we + # want to use the 'mingw32' compiler, so check it first + if is_mingw(): + return 'mingw32' for pattern, compiler in _default_compilers: if ( re.match(pattern, platform) is not None diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index b48f4626..4a69e9c1 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -21,7 +21,7 @@ from ..sysconfig import get_config_h_filename from .._modified import newer_group from ..extension import Extension -from ..util import get_platform +from ..util import get_platform, is_mingw from distutils._log import log from . import py37compat @@ -189,7 +189,7 @@ def finalize_options(self): # noqa: C901 # for extensions under windows use different directories # for Release and Debug builds. # also Python's library directory must be appended to library_dirs - if os.name == 'nt': + if os.name == 'nt' and not is_mingw(): # the 'libs' directory is for binary installs - we assume that # must be the *native* platform. But we don't really support # cross-compiling via a binary install anyway, so we let it go. @@ -742,7 +742,7 @@ def get_libraries(self, ext): # noqa: C901 # pyconfig.h that MSVC groks. The other Windows compilers all seem # to need it mentioned explicitly, though, so that's what we do. # Append '_d' to the python import library on debug builds. - if sys.platform == "win32": + if sys.platform == "win32" and not is_mingw(): from .._msvccompiler import MSVCCompiler if not isinstance(self.compiler, MSVCCompiler): @@ -772,7 +772,7 @@ def get_libraries(self, ext): # noqa: C901 # A native build on an Android device or on Cygwin if hasattr(sys, 'getandroidapilevel'): link_libpython = True - elif sys.platform == 'cygwin': + elif sys.platform == 'cygwin' or is_mingw(): link_libpython = True elif '_PYTHON_HOST_PLATFORM' in os.environ: # We are cross-compiling for one of the relevant platforms diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 47efa377..7ed169f3 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -57,7 +57,7 @@ def get_msvcr(): try: msc_ver = int(match.group(1)) except AttributeError: - return + return [] try: return _msvcr_lookup[msc_ver] except KeyError: @@ -277,7 +277,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): self.set_executables( compiler='%s -O -Wall' % self.cc, - compiler_so='%s -mdll -O -Wall' % self.cc, + compiler_so='%s -shared -O -Wall' % self.cc, compiler_cxx='%s -O -Wall' % self.cxx, linker_exe='%s' % self.cc, linker_so='{} {}'.format(self.linker_dll, shared_option), diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index a40a7231..166d8543 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -18,6 +18,7 @@ from .errors import DistutilsPlatformError from . import py39compat from ._functools import pass_none +from .util import is_mingw IS_PYPY = '__pypy__' in sys.builtin_module_names @@ -120,8 +121,10 @@ def get_python_inc(plat_specific=0, prefix=None): """ default_prefix = BASE_EXEC_PREFIX if plat_specific else BASE_PREFIX resolved_prefix = prefix if prefix is not None else default_prefix + # MinGW imitates posix like layout, but os.name != posix + os_name = "posix" if is_mingw() else os.name try: - getter = globals()[f'_get_python_inc_{os.name}'] + getter = globals()[f'_get_python_inc_{os_name}'] except KeyError: raise DistutilsPlatformError( "I don't know where Python installs its C header files " @@ -244,7 +247,7 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None): else: prefix = plat_specific and EXEC_PREFIX or PREFIX - if os.name == "posix": + if os.name == "posix" or is_mingw(): if plat_specific or standard_lib: # Platform-specific modules (any module from a non-pure-Python # module distribution) or standard Python library modules. @@ -273,7 +276,7 @@ def customize_compiler(compiler): # noqa: C901 Mainly needed on Unix, so we can plug in the information that varies across Unices and is stored in Python's Makefile. """ - if compiler.compiler_type == "unix": + if compiler.compiler_type in ["unix", "cygwin", "mingw32"]: if sys.platform == "darwin": # Perform first-time customization of compiler-related # config vars on OS X now that we know we need a compiler. diff --git a/distutils/util.py b/distutils/util.py index 5408b160..bec979d7 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -517,3 +517,12 @@ def rfc822_escape(header): suffix = indent if ends_in_newline else "" return indent.join(lines) + suffix + + +def is_mingw(): + """Returns True if the current platform is mingw. + + Python compiled with Mingw-w64 has sys.platform == 'win32' and + get_platform() starts with 'mingw'. + """ + return sys.platform == 'win32' and get_platform().startswith('mingw') From e69a19e59768b08a4a29bb8be09c09974d625ff5 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sat, 8 Oct 2022 11:26:48 +0530 Subject: [PATCH 065/221] Fix tests for `get_msvcr` function --- distutils/tests/test_cygwinccompiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 6fb449a6..3cb95e12 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -71,12 +71,12 @@ def test_check_config_h(self): assert check_config_h()[0] == CONFIG_H_OK def test_get_msvcr(self): - # none + # [] sys.version = ( '2.6.1 (r261:67515, Dec 6 2008, 16:42:21) ' '\n[GCC 4.0.1 (Apple Computer, Inc. build 5370)]' ) - assert get_msvcr() is None + assert get_msvcr() == [] # MSVC 7.0 sys.version = ( From 571f761ab313aa0e8171da5956c090cb7d417764 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Thu, 17 Nov 2022 13:25:32 +0530 Subject: [PATCH 066/221] Make `test_customize_compiler` run on mingw Simply, run it for the subclasses for `UnixCCompiler` --- distutils/tests/test_sysconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index bfeaf9a6..c7af690b 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -12,7 +12,7 @@ import distutils from distutils import sysconfig -from distutils.ccompiler import get_default_compiler # noqa: F401 +from distutils.ccompiler import new_compiler # noqa: F401 from distutils.unixccompiler import UnixCCompiler from test.support import swap_item @@ -109,7 +109,7 @@ def set_executables(self, **kw): return comp - @pytest.mark.skipif("get_default_compiler() != 'unix'") + @pytest.mark.skipif("not isinstance(new_compiler(), UnixCCompiler)") def test_customize_compiler(self): # Make sure that sysconfig._config_vars is initialized sysconfig.get_config_vars() From 9e707f07e9794a209ee49f23c3a8890b57c5e9e7 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Thu, 17 Nov 2022 13:54:18 +0530 Subject: [PATCH 067/221] CI: add msys2 mingw test --- .github/workflows/main.yml | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45c66794..402363d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,6 +115,45 @@ jobs: shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} run: tox + test_msys2_mingw: + strategy: + matrix: + include: + - { sys: mingw64, env: x86_64 } + - { sys: mingw32, env: i686 } + - { sys: ucrt64, env: ucrt-x86_64 } + - { sys: clang64, env: clang-x86_64 } + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: ${{matrix.sys}} + install: | + mingw-w64-${{matrix.env}}-toolchain + mingw-w64-${{matrix.env}}-python + mingw-w64-${{matrix.env}}-python-pip + mingw-w64-${{matrix.env}}-python-virtualenv + mingw-w64-${{matrix.env}}-cc + git + - name: Install Dependencies + shell: msys2 {0} + run: | + export VIRTUALENV_NO_SETUPTOOLS=1 + + python -m virtualenv venv + source venv/bin/activate + + # python-ruff doesn't work without rust + sed -i '/pytest-ruff/d' setup.cfg + + pip install -e .[testing] + - name: Run tests + shell: msys2 {0} + run: | + source venv/bin/activate + pytest distutils/tests + ci_setuptools: # Integration testing with setuptools if: ${{ false }} # disabled for deprecation warnings From 36a2941e558126364900a6fd4ad0ab3c0d21a95b Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Wed, 8 Nov 2023 20:30:29 +0530 Subject: [PATCH 068/221] Fix path separator issue in change_root function use `os.sep` instead of hardcoding `\\` also, fix appropriate tests --- distutils/tests/test_util.py | 1 + distutils/util.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 22a003d8..2b340a3b 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -109,6 +109,7 @@ def _join(*path): # windows os.name = 'nt' + os.sep = '\\' def _isabs(path): return path.startswith('c:\\') diff --git a/distutils/util.py b/distutils/util.py index bec979d7..b291ba3e 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -165,7 +165,7 @@ def change_root(new_root, pathname): elif os.name == 'nt': (drive, path) = os.path.splitdrive(pathname) - if path[0] == '\\': + if path[0] == os.sep: path = path[1:] return os.path.join(new_root, path) From c9770264f7af6f08d98bbd6928a921d6b14eb198 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Wed, 8 Nov 2023 20:39:37 +0530 Subject: [PATCH 069/221] test_install: fix an issue specific to mingw --- distutils/tests/test_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 082ee1d3..c74bf8cb 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -16,6 +16,7 @@ from distutils.core import Distribution from distutils.errors import DistutilsOptionError from distutils.extension import Extension +from distutils.util import is_mingw from distutils.tests import support, missing_compiler_executable @@ -120,7 +121,7 @@ def _expanduser(path): assert 'usersite' in cmd.config_vars actual_headers = os.path.relpath(cmd.install_headers, site.USER_BASE) - if os.name == 'nt': + if os.name == 'nt' and not is_mingw(): site_path = os.path.relpath(os.path.dirname(orig_site), orig_base) include = os.path.join(site_path, 'Include') else: From 79830a81664feee1a9ae4d031909d261e363320e Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Wed, 8 Nov 2023 23:23:42 +0530 Subject: [PATCH 070/221] Remove testing dependency on jaraco.text it depends on pydantic-core which requires rust to work also, takes a few minutes to build. --- distutils/tests/test_sysconfig.py | 6 +++++- setup.cfg | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index c7af690b..c4e1648f 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -8,7 +8,7 @@ import pytest import jaraco.envs import path -from jaraco.text import trim +from textwrap import dedent import distutils from distutils import sysconfig @@ -19,6 +19,10 @@ from . import py37compat +def trim(s): + return dedent(s).strip() + + @pytest.mark.usefixtures('save_env') class TestSysconfig: def test_get_config_h_filename(self): diff --git a/setup.cfg b/setup.cfg index 68c38ac9..7a748f49 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,6 @@ testing = pytest >= 7.4.3 #186 jaraco.envs>=2.4 jaraco.path - jaraco.text path docutils pyfakefs From e4e16870510edbe59f834693ff62857ddd14481d Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Thu, 9 Nov 2023 18:48:16 +0530 Subject: [PATCH 071/221] Add test for dll_libraries attribute in CygwinCCompiler class --- distutils/tests/test_cygwinccompiler.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 3cb95e12..ffbaf1ea 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -114,3 +114,10 @@ def test_get_msvcr(self): ) with pytest.raises(ValueError): get_msvcr() + + @pytest.mark.skipif('sys.platform != "cygwin"') + def test_dll_libraries_not_none(self): + from distutils.cygwinccompiler import CygwinCCompiler + + compiler = CygwinCCompiler() + assert compiler.dll_libraries is not None From bbe7b64f4eedbeee9d71e197dd4b5d3175f4d04a Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Thu, 9 Nov 2023 18:48:50 +0530 Subject: [PATCH 072/221] Add some tests for Mingw32CCompiler class --- distutils/tests/test_mingwccompiler.py | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 distutils/tests/test_mingwccompiler.py diff --git a/distutils/tests/test_mingwccompiler.py b/distutils/tests/test_mingwccompiler.py new file mode 100644 index 00000000..d81360e7 --- /dev/null +++ b/distutils/tests/test_mingwccompiler.py @@ -0,0 +1,45 @@ +import pytest + +from distutils.util import split_quoted, is_mingw +from distutils.errors import DistutilsPlatformError, CCompilerError + + +class TestMingw32CCompiler: + @pytest.mark.skipif(not is_mingw(), reason='not on mingw') + def test_compiler_type(self): + from distutils.cygwinccompiler import Mingw32CCompiler + + compiler = Mingw32CCompiler() + assert compiler.compiler_type == 'mingw32' + + @pytest.mark.skipif(not is_mingw(), reason='not on mingw') + def test_set_executables(self, monkeypatch): + from distutils.cygwinccompiler import Mingw32CCompiler + + monkeypatch.setenv('CC', 'cc') + monkeypatch.setenv('CXX', 'c++') + + compiler = Mingw32CCompiler() + + assert compiler.compiler == split_quoted('cc -O -Wall') + assert compiler.compiler_so == split_quoted('cc -shared -O -Wall') + assert compiler.compiler_cxx == split_quoted('c++ -O -Wall') + assert compiler.linker_exe == split_quoted('cc') + assert compiler.linker_so == split_quoted('cc -shared') + + @pytest.mark.skipif(not is_mingw(), reason='not on mingw') + def test_runtime_library_dir_option(self): + from distutils.cygwinccompiler import Mingw32CCompiler + + compiler = Mingw32CCompiler() + with pytest.raises(DistutilsPlatformError): + compiler.runtime_library_dir_option('/usr/lib') + + @pytest.mark.skipif(not is_mingw(), reason='not on mingw') + def test_cygwincc_error(self, monkeypatch): + import distutils.cygwinccompiler + + monkeypatch.setattr(distutils.cygwinccompiler, 'is_cygwincc', lambda _: True) + + with pytest.raises(CCompilerError): + distutils.cygwinccompiler.Mingw32CCompiler() From 29e5d34af962e59e92c501ebb988dcaf192b114e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Feb 2024 10:15:04 -0500 Subject: [PATCH 073/221] Enable preview to enable preserving quotes. --- ruff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ruff.toml b/ruff.toml index 795cca16..e61ca8b0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -18,5 +18,7 @@ ignore = [ ] [format] +# Enable preview, required for quote-style = "preserve" +preview = true # https://docs.astral.sh/ruff/settings/#format-quote-style quote-style = "preserve" From 2a402a39f154d9a6cf4621e8c5d22bace749b55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 6 Feb 2024 23:01:32 +0100 Subject: [PATCH 074/221] Tweak coverage configuration for type checking (jaraco/skeleton#97) * Tweak coverage configuration for type checking * Use `exclude_also` instead of `exclude_lines` Co-authored-by: Sviatoslav Sydorenko * Add reference to the issue. --------- Co-authored-by: Sviatoslav Sydorenko Co-authored-by: Jason R. Coombs --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 02879483..35b98b1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,3 +7,7 @@ disable_warnings = [report] show_missing = True +exclude_also = + # jaraco/skeleton#97 + @overload + if TYPE_CHECKING: From 68ac292eb37ce92e992e6fab05a44ad86f32e8f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Feb 2024 16:53:46 -0500 Subject: [PATCH 075/221] Use latest versions in RTD boilerplate. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 053c7287..68489063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,6 @@ python: # required boilerplate readthedocs/readthedocs.org#10401 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3" + python: latest From 178d254379ed260eb537f48722703f819eaa8235 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Feb 2024 16:02:29 -0500 Subject: [PATCH 076/221] 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 fe99eaf6..400a72a5 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 077/221] 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 a40a7231..c89fff4b 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 078/221] 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 c89fff4b..fac3259f 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 079/221] =?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 b01b3130..ca808a6a 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 3c2ba154..14d51472 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 c1c7d547..6935e2c3 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 cffa27cb..b4228299 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 6329039c..237b1465 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 696f2675..e96db22b 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 d9df9592..e16011d4 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 575e49fb..b59cc237 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 a7ac4e60..927c3ed3 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 c19aabb9..cf1afc8c 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 9a4044ad..a55951ed 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 47efa377..b3dbc3be 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 6b8575de..8f186b72 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 85293cbb..aad8edb2 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 fd4b11bf..20806049 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 89c415d7..2b5eafd2 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 af330a06..18048077 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 6fb50c4b..95532e83 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 3fd2c7e2..e6804088 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 66d8af50..c2cff445 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 98ab0b17..f8554542 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 3c83cca4..537959fe 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 3bef9d79..77c9ad75 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 6d240b8b..8215300b 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 157b60a1..e2459aa0 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 cc740d1a..684662d3 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 1ae615db..11c23d83 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 ecb85102..2519ed6a 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 6fb449a6..fc67d75f 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 72aca4ee..0738b7c8 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 694bf02a..fe979efe 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 f86af073..297ae44b 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 9f44f91d..3b9f82b7 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 2cee42cd..bfffbb1d 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 9badbc26..198c10da 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 1e8ccf79..8b86b6ea 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 0bd67cd0..0efe39fe 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 ca07c7e8..5fde7a59 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 fe5693e1..dfb34122 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 f63537b8..f65a5a25 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 34e59324..5d3826a1 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 a3fa2902..00718a37 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 08a34ee2..57cf1a52 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 bfeaf9a6..6cbf5168 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 7c8dc5be..4a721b69 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 a0184424..c1e57a01 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 af113b8b..5c5bc59a 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 22a003d8..c632b391 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 ff52ea46..900edafa 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 74c40d7b..18385cfe 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 d6c0c007..c75e4948 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 080/221] 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 6935e2c3..d5ca761f 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 b48f4626..98938bab 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 00000000..6d70b7f0 --- /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 081/221] 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 98938bab..ba6580c7 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 082/221] =?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 e8c34ce6..62efce43 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 b676a6a8..d749fe25 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 083/221] 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 62efce43..a313da3e 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 084/221] 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 00000000..85b29511 --- /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 d5ca761f..28d2da5c 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 085/221] 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 45c66794..473c2e0f 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 086/221] 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 331eeed9..4c39a5b1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ extras = [testenv:diffcov] description = run tests and check that diff from main is covered deps = + {[testenv]deps} diff-cover commands = pytest {posargs} --cov-report xml From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 087/221] 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 e61ca8b0..6c5b0009 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 088/221] 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 fac3259f..a88fd021 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 089/221] 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 a88fd021..b1d8e7c7 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 090/221] 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 ca808a6a..06ce3bc6 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 6cbf5168..f656be60 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 091/221] 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 a313da3e..2763db9c 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 092/221] 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 b1d8e7c7..5fb811c4 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 093/221] 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 6c5b0009..70612985 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 094/221] 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 537959fe..51e5cd00 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 095/221] 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 20806049..ddf7bf1d 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 096/221] 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 77c9ad75..6730878e 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 097/221] 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 5408b160..aa0c90cf 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 098/221] 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 28cc5632..8005b81c 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 099/221] 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 49691d4b..b6512e6d 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 2519ed6a..90c8f906 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 ba2d6599..d1c98554 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 100/221] 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 2c11ff76..95aa2998 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 101/221] 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 14d51472..d496d5d4 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 67feb164..6faf546c 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 8fdcbc0e..8849474c 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 b4228299..397ebf82 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 e96db22b..675bcebd 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 cc9b367e..d8704e35 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 ba6580c7..a15781f2 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 b59cc237..28f55fb9 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 cf1afc8c..5a24246c 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 caf15f04..a9124f2b 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 05d29719..799de948 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 b3dbc3be..84151b7e 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 23dc3392..819fe56f 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 7c0f0e5b..65958394 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 3b887dc5..c025f120 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 3f3e21b5..8ebd2a79 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 6dadf923..32057626 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 724986d8..402c0c06 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 c3823e25..1a07746b 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 59224e71..ab12119f 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 afefe525..48adceb1 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 5fb811c4..40215b83 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 95532e83..cb4db4e1 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 c2cff445..8617fa99 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 51e5cd00..e24dea36 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 0738b7c8..e7d69bb6 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 3b9f82b7..e441186e 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 900edafa..0aaf0a53 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 aa0c90cf..c26e61ab 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 18385cfe..8ab76dde 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 102/221] 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 e7d69bb6..6fc9ed08 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 103/221] 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 e441186e..888e27b5 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 104/221] 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 888e27b5..27796d9f 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 105/221] 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 27796d9f..08f9e19f 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 106/221] =?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 e24dea36..4ae81a22 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 08f9e19f..6c701914 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 107/221] 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 bfffbb1d..bf1a9d9b 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 108/221] 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 082ee1d3..16ac5ca7 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 109/221] 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 8005b81c..7e05ec5f 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 58313f28..4da2acb6 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 110/221] 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 5d3826a1..591c5ce0 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 111/221] 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 00718a37..450f68c9 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 112/221] 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 57cf1a52..ec4c9982 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 113/221] 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 f656be60..131c1344 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 114/221] 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 4a721b69..fe787f44 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 115/221] 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 14d51472..d496d5d4 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 67feb164..6faf546c 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 8fdcbc0e..8849474c 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 b4228299..397ebf82 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 e96db22b..675bcebd 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 cc9b367e..d8704e35 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 ba6580c7..a15781f2 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 b59cc237..28f55fb9 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 cf1afc8c..5a24246c 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 caf15f04..a9124f2b 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 05d29719..799de948 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 b3dbc3be..84151b7e 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 23dc3392..819fe56f 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 7c0f0e5b..65958394 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 3b887dc5..c025f120 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 3f3e21b5..8ebd2a79 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 6dadf923..32057626 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 724986d8..402c0c06 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 c3823e25..1a07746b 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 59224e71..ab12119f 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 afefe525..48adceb1 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 5fb811c4..40215b83 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 95532e83..cb4db4e1 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 c2cff445..8617fa99 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 537959fe..da466307 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 0738b7c8..e7d69bb6 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 3b9f82b7..e441186e 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 900edafa..0aaf0a53 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 5408b160..a2ba1fc9 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 18385cfe..8ab76dde 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 116/221] 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 c26e61ab..bfd30700 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 117/221] 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 4ae81a22..ae66bc4e 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 118/221] 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 6faf546c..bcf9580c 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 119/221] 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 494d97d1..573741d7 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 120/221] 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 a55951ed..f92ecb96 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 121/221] 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 ac489726..b76cb9bc 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 122/221] 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 36f947e5..6f90cfe2 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 123/221] 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 65958394..c4d2a45d 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 124/221] 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 84151b7e..20609504 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 125/221] 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 8ebd2a79..0eb9b861 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 126/221] 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 3ee2f886..42820fc7 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 127/221] 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 f92ecb96..e0defd77 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 128/221] 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 aad8edb2..6d9b8532 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 129/221] 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 fe979efe..8e52873d 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 42820fc7..fa31fb33 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 a0d0c4b7e87fbfd04cee2546ba452858587516fd Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 21 Mar 2024 15:34:23 -0400 Subject: [PATCH 130/221] 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 400a72a5..6fa73b6a 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 c9a7f97ba83be124e173713f5c24564c2b6dd49e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 15:49:52 -0400 Subject: [PATCH 131/221] 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 cf94f7d8..143b0984 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/221] 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 68489063..85dfea9d 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/221] 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 143b0984..a15c74a6 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/221] 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 022a723e..9a0f3bce 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 6fa73b6a..f46b6cbf 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/221] 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 fa31fb33..3ee2f886 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/221] 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 a15c74a6..ac0ff69e 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 21482ac4b2ef50e41218beafba261061daf23d9b Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Mar 2024 20:46:50 +0000 Subject: [PATCH 137/221] Support PEP 625 --- distutils/dist.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/distutils/dist.py b/distutils/dist.py index c4d2a45d..9f1e23e6 100644 --- a/distutils/dist.py +++ b/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 f"{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 From 005ba4b6d47d96c3a93e21a8bcae32c10c1f202a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 19:10:53 -0400 Subject: [PATCH 138/221] Expect to find canonicalize_* functions in packaging. --- distutils/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/dist.py b/distutils/dist.py index 9f1e23e6..b0cb87d5 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -12,6 +12,8 @@ import logging from email import message_from_file +from packaging.utils import canonicalize_name, canonicalize_version + try: import warnings except ImportError: @@ -23,7 +25,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 From be32fecc787c3de8c292638c5004a8bcf92dc540 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 19:14:18 -0400 Subject: [PATCH 139/221] Update tests to match new expectation. --- distutils/tests/test_sdist.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 450f68c9..e708edc4 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -118,9 +118,9 @@ def test_prune_file_list(self): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - assert files == ['fake-1.0.zip'] + assert files == ['fake-1.zip'] - zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) + zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.zip')) try: content = zip_file.namelist() finally: @@ -135,7 +135,7 @@ def test_prune_file_list(self): 'somecode/', 'somecode/__init__.py', ] - assert sorted(content) == ['fake-1.0/' + x for x in expected] + assert sorted(content) == ['fake-1/' + x for x in expected] @pytest.mark.usefixtures('needs_zlib') @pytest.mark.skipif("not find_executable('tar')") @@ -153,10 +153,10 @@ def test_make_distribution(self): dist_folder = join(self.tmp_dir, 'dist') result = os.listdir(dist_folder) result.sort() - assert result == ['fake-1.0.tar', 'fake-1.0.tar.gz'] + assert result == ['fake-1.tar', 'fake-1.tar.gz'] - os.remove(join(dist_folder, 'fake-1.0.tar')) - os.remove(join(dist_folder, 'fake-1.0.tar.gz')) + os.remove(join(dist_folder, 'fake-1.tar')) + os.remove(join(dist_folder, 'fake-1.tar.gz')) # now trying a tar then a gztar cmd.formats = ['tar', 'gztar'] @@ -166,7 +166,7 @@ def test_make_distribution(self): result = os.listdir(dist_folder) result.sort() - assert result == ['fake-1.0.tar', 'fake-1.0.tar.gz'] + assert result == ['fake-1.tar', 'fake-1.tar.gz'] @pytest.mark.usefixtures('needs_zlib') def test_add_defaults(self): @@ -219,9 +219,9 @@ def test_add_defaults(self): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - assert files == ['fake-1.0.zip'] + assert files == ['fake-1.zip'] - zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) + zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.zip')) try: content = zip_file.namelist() finally: @@ -247,7 +247,7 @@ def test_add_defaults(self): 'somecode/doc.dat', 'somecode/doc.txt', ] - assert sorted(content) == ['fake-1.0/' + x for x in expected] + assert sorted(content) == ['fake-1/' + x for x in expected] # checking the MANIFEST manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8') @@ -420,16 +420,16 @@ def test_manual_manifest(self): assert list(clean_lines(cmd.manifest)) == ['README.manual'] - archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') archive = tarfile.open(archive_name) try: filenames = [tarinfo.name for tarinfo in archive] finally: archive.close() assert sorted(filenames) == [ - 'fake-1.0', - 'fake-1.0/PKG-INFO', - 'fake-1.0/README.manual', + 'fake-1', + 'fake-1/PKG-INFO', + 'fake-1/README.manual', ] @pytest.mark.usefixtures('needs_zlib') @@ -449,7 +449,7 @@ def test_make_distribution_owner_group(self): cmd.run() # making sure we have the good rights - archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') archive = tarfile.open(archive_name) try: for member in archive.getmembers(): @@ -467,7 +467,7 @@ def test_make_distribution_owner_group(self): cmd.run() # making sure we have the good rights - archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') archive = tarfile.open(archive_name) # note that we are not testing the group ownership here From 2316432d6e62a3db3ae701d7cf839258c02a702d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 11 Apr 2024 15:43:12 +0000 Subject: [PATCH 140/221] Fix canonicalization --- distutils/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/dist.py b/distutils/dist.py index b0cb87d5..4b3824df 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -1192,7 +1192,8 @@ def get_version(self): def get_fullname(self): return "{}-{}".format( - canonicalize_name(self.get_name()), canonicalize_version(self.get_version()) + canonicalize_name(self.get_name()).replace('-', '_'), + canonicalize_version(self.get_version()), ) def get_author(self): From d2de2195f48ddc07999cbb173df0adc3185013ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 19:29:06 -0400 Subject: [PATCH 141/221] In test_sdist, provide a more complex name to capture canonicalization behavior. --- distutils/tests/test_sdist.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index e708edc4..359325d5 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -74,7 +74,7 @@ def get_cmd(self, metadata=None): """Returns a cmd""" if metadata is None: metadata = { - 'name': 'fake', + 'name': 'ns.fake--pkg', 'version': '1.0', 'url': 'xxx', 'author': 'xxx', @@ -118,9 +118,9 @@ def test_prune_file_list(self): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - assert files == ['fake-1.zip'] + assert files == ['ns_fake_pkg-1.zip'] - zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.zip')) + zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.zip')) try: content = zip_file.namelist() finally: @@ -135,7 +135,7 @@ def test_prune_file_list(self): 'somecode/', 'somecode/__init__.py', ] - assert sorted(content) == ['fake-1/' + x for x in expected] + assert sorted(content) == ['ns_fake_pkg-1/' + x for x in expected] @pytest.mark.usefixtures('needs_zlib') @pytest.mark.skipif("not find_executable('tar')") @@ -153,10 +153,10 @@ def test_make_distribution(self): dist_folder = join(self.tmp_dir, 'dist') result = os.listdir(dist_folder) result.sort() - assert result == ['fake-1.tar', 'fake-1.tar.gz'] + assert result == ['ns_fake_pkg-1.tar', 'ns_fake_pkg-1.tar.gz'] - os.remove(join(dist_folder, 'fake-1.tar')) - os.remove(join(dist_folder, 'fake-1.tar.gz')) + os.remove(join(dist_folder, 'ns_fake_pkg-1.tar')) + os.remove(join(dist_folder, 'ns_fake_pkg-1.tar.gz')) # now trying a tar then a gztar cmd.formats = ['tar', 'gztar'] @@ -166,7 +166,7 @@ def test_make_distribution(self): result = os.listdir(dist_folder) result.sort() - assert result == ['fake-1.tar', 'fake-1.tar.gz'] + assert result == ['ns_fake_pkg-1.tar', 'ns_fake_pkg-1.tar.gz'] @pytest.mark.usefixtures('needs_zlib') def test_add_defaults(self): @@ -219,9 +219,9 @@ def test_add_defaults(self): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - assert files == ['fake-1.zip'] + assert files == ['ns_fake_pkg-1.zip'] - zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.zip')) + zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.zip')) try: content = zip_file.namelist() finally: @@ -247,7 +247,7 @@ def test_add_defaults(self): 'somecode/doc.dat', 'somecode/doc.txt', ] - assert sorted(content) == ['fake-1/' + x for x in expected] + assert sorted(content) == ['ns_fake_pkg-1/' + x for x in expected] # checking the MANIFEST manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8') @@ -420,16 +420,16 @@ def test_manual_manifest(self): assert list(clean_lines(cmd.manifest)) == ['README.manual'] - archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz') archive = tarfile.open(archive_name) try: filenames = [tarinfo.name for tarinfo in archive] finally: archive.close() assert sorted(filenames) == [ - 'fake-1', - 'fake-1/PKG-INFO', - 'fake-1/README.manual', + 'ns_fake_pkg-1', + 'ns_fake_pkg-1/PKG-INFO', + 'ns_fake_pkg-1/README.manual', ] @pytest.mark.usefixtures('needs_zlib') @@ -449,7 +449,7 @@ def test_make_distribution_owner_group(self): cmd.run() # making sure we have the good rights - archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz') archive = tarfile.open(archive_name) try: for member in archive.getmembers(): @@ -467,7 +467,7 @@ def test_make_distribution_owner_group(self): cmd.run() # making sure we have the good rights - archive_name = join(self.tmp_dir, 'dist', 'fake-1.tar.gz') + archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz') archive = tarfile.open(archive_name) # note that we are not testing the group ownership here From 11e8f76e422a8d21170a6aae65bde409fc8e0901 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 11 Apr 2024 19:06:14 -0400 Subject: [PATCH 142/221] Add packaging as a vendored package. --- .coveragerc | 1 + conftest.py | 5 + distutils/_vendor/__init__.py | 0 .../packaging-24.0.dist-info/INSTALLER | 1 + .../_vendor/packaging-24.0.dist-info/LICENSE | 3 + .../packaging-24.0.dist-info/LICENSE.APACHE | 177 +++ .../packaging-24.0.dist-info/LICENSE.BSD | 23 + .../_vendor/packaging-24.0.dist-info/METADATA | 102 ++ .../_vendor/packaging-24.0.dist-info/RECORD | 37 + .../packaging-24.0.dist-info/REQUESTED | 0 .../_vendor/packaging-24.0.dist-info/WHEEL | 4 + distutils/_vendor/packaging/__init__.py | 15 + distutils/_vendor/packaging/_elffile.py | 108 ++ distutils/_vendor/packaging/_manylinux.py | 260 +++++ distutils/_vendor/packaging/_musllinux.py | 83 ++ distutils/_vendor/packaging/_parser.py | 356 ++++++ distutils/_vendor/packaging/_structures.py | 61 + distutils/_vendor/packaging/_tokenizer.py | 192 ++++ distutils/_vendor/packaging/markers.py | 252 ++++ distutils/_vendor/packaging/metadata.py | 825 +++++++++++++ distutils/_vendor/packaging/py.typed | 0 distutils/_vendor/packaging/requirements.py | 90 ++ distutils/_vendor/packaging/specifiers.py | 1017 +++++++++++++++++ distutils/_vendor/packaging/tags.py | 571 +++++++++ distutils/_vendor/packaging/utils.py | 172 +++ distutils/_vendor/packaging/version.py | 563 +++++++++ distutils/_vendor/ruff.toml | 1 + 27 files changed, 4919 insertions(+) create mode 100644 distutils/_vendor/__init__.py create mode 100644 distutils/_vendor/packaging-24.0.dist-info/INSTALLER create mode 100644 distutils/_vendor/packaging-24.0.dist-info/LICENSE create mode 100644 distutils/_vendor/packaging-24.0.dist-info/LICENSE.APACHE create mode 100644 distutils/_vendor/packaging-24.0.dist-info/LICENSE.BSD create mode 100644 distutils/_vendor/packaging-24.0.dist-info/METADATA create mode 100644 distutils/_vendor/packaging-24.0.dist-info/RECORD create mode 100644 distutils/_vendor/packaging-24.0.dist-info/REQUESTED create mode 100644 distutils/_vendor/packaging-24.0.dist-info/WHEEL create mode 100644 distutils/_vendor/packaging/__init__.py create mode 100644 distutils/_vendor/packaging/_elffile.py create mode 100644 distutils/_vendor/packaging/_manylinux.py create mode 100644 distutils/_vendor/packaging/_musllinux.py create mode 100644 distutils/_vendor/packaging/_parser.py create mode 100644 distutils/_vendor/packaging/_structures.py create mode 100644 distutils/_vendor/packaging/_tokenizer.py create mode 100644 distutils/_vendor/packaging/markers.py create mode 100644 distutils/_vendor/packaging/metadata.py create mode 100644 distutils/_vendor/packaging/py.typed create mode 100644 distutils/_vendor/packaging/requirements.py create mode 100644 distutils/_vendor/packaging/specifiers.py create mode 100644 distutils/_vendor/packaging/tags.py create mode 100644 distutils/_vendor/packaging/utils.py create mode 100644 distutils/_vendor/packaging/version.py create mode 100644 distutils/_vendor/ruff.toml diff --git a/.coveragerc b/.coveragerc index 35b98b1d..654e4c4f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + */distutils/_vendor/* disable_warnings = couldnt-parse diff --git a/conftest.py b/conftest.py index 06ce3bc6..8e05649f 100644 --- a/conftest.py +++ b/conftest.py @@ -17,6 +17,11 @@ ]) +collect_ignore_glob = [ + 'distutils/_vendor/**/*', +] + + @pytest.fixture def save_env(): orig = os.environ.copy() diff --git a/distutils/_vendor/__init__.py b/distutils/_vendor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/distutils/_vendor/packaging-24.0.dist-info/INSTALLER b/distutils/_vendor/packaging-24.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/distutils/_vendor/packaging-24.0.dist-info/LICENSE b/distutils/_vendor/packaging-24.0.dist-info/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/distutils/_vendor/packaging-24.0.dist-info/LICENSE.APACHE b/distutils/_vendor/packaging-24.0.dist-info/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/distutils/_vendor/packaging-24.0.dist-info/LICENSE.BSD b/distutils/_vendor/packaging-24.0.dist-info/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/distutils/_vendor/packaging-24.0.dist-info/METADATA b/distutils/_vendor/packaging-24.0.dist-info/METADATA new file mode 100644 index 00000000..10ab4390 --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/METADATA @@ -0,0 +1,102 @@ +Metadata-Version: 2.1 +Name: packaging +Version: 24.0 +Summary: Core utilities for Python packages +Author-email: Donald Stufft +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +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 :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Typing :: Typed +Project-URL: Documentation, https://packaging.pypa.io/ +Project-URL: Source, https://github.com/pypa/packaging + +packaging +========= + +.. start-intro + +Reusable core utilities for various Python Packaging +`interoperability specifications `_. + +This library provides utilities that implement the interoperability +specifications which have clearly one correct behaviour (eg: :pep:`440`) +or benefit greatly from having a single shared implementation (eg: :pep:`425`). + +.. end-intro + +The ``packaging`` project includes the following: version handling, specifiers, +markers, requirements, tags, utilities. + +Documentation +------------- + +The `documentation`_ provides information and the API for the following: + +- Version Handling +- Specifiers +- Markers +- Requirements +- Tags +- Utilities + +Installation +------------ + +Use ``pip`` to install these utilities:: + + pip install packaging + +The ``packaging`` library uses calendar-based versioning (``YY.N``). + +Discussion +---------- + +If you run into bugs, you can file them in our `issue tracker`_. + +You can also join ``#pypa`` on Freenode to ask questions or get involved. + + +.. _`documentation`: https://packaging.pypa.io/ +.. _`issue tracker`: https://github.com/pypa/packaging/issues + + +Code of Conduct +--------------- + +Everyone interacting in the packaging project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + +Contributing +------------ + +The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as +well as how to report a potential security issue. The documentation for this +project also covers information about `project development`_ and `security`_. + +.. _`project development`: https://packaging.pypa.io/en/latest/development/ +.. _`security`: https://packaging.pypa.io/en/latest/security/ + +Project History +--------------- + +Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for +recent changes and project history. + +.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ + diff --git a/distutils/_vendor/packaging-24.0.dist-info/RECORD b/distutils/_vendor/packaging-24.0.dist-info/RECORD new file mode 100644 index 00000000..bcf796c2 --- /dev/null +++ b/distutils/_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/distutils/_vendor/packaging-24.0.dist-info/REQUESTED b/distutils/_vendor/packaging-24.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/distutils/_vendor/packaging-24.0.dist-info/WHEEL b/distutils/_vendor/packaging-24.0.dist-info/WHEEL new file mode 100644 index 00000000..3b5e64b5 --- /dev/null +++ b/distutils/_vendor/packaging-24.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/distutils/_vendor/packaging/__init__.py b/distutils/_vendor/packaging/__init__.py new file mode 100644 index 00000000..e7c0aa12 --- /dev/null +++ b/distutils/_vendor/packaging/__init__.py @@ -0,0 +1,15 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +__title__ = "packaging" +__summary__ = "Core utilities for Python packages" +__uri__ = "https://github.com/pypa/packaging" + +__version__ = "24.0" + +__author__ = "Donald Stufft and individual contributors" +__email__ = "donald@stufft.io" + +__license__ = "BSD-2-Clause or Apache-2.0" +__copyright__ = "2014 %s" % __author__ diff --git a/distutils/_vendor/packaging/_elffile.py b/distutils/_vendor/packaging/_elffile.py new file mode 100644 index 00000000..6fb19b30 --- /dev/null +++ b/distutils/_vendor/packaging/_elffile.py @@ -0,0 +1,108 @@ +""" +ELF file parser. + +This provides a class ``ELFFile`` that parses an ELF executable in a similar +interface to ``ZipFile``. Only the read interface is implemented. + +Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca +ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html +""" + +import enum +import os +import struct +from typing import IO, Optional, Tuple + + +class ELFInvalid(ValueError): + pass + + +class EIClass(enum.IntEnum): + C32 = 1 + C64 = 2 + + +class EIData(enum.IntEnum): + Lsb = 1 + Msb = 2 + + +class EMachine(enum.IntEnum): + I386 = 3 + S390 = 22 + Arm = 40 + X8664 = 62 + AArc64 = 183 + + +class ELFFile: + """ + Representation of an ELF executable. + """ + + def __init__(self, f: IO[bytes]) -> None: + self._f = f + + try: + ident = self._read("16B") + except struct.error: + raise ELFInvalid("unable to parse identification") + magic = bytes(ident[:4]) + if magic != b"\x7fELF": + raise ELFInvalid(f"invalid magic: {magic!r}") + + self.capacity = ident[4] # Format for program header (bitness). + self.encoding = ident[5] # Data structure encoding (endianness). + + try: + # e_fmt: Format for program header. + # p_fmt: Format for section header. + # p_idx: Indexes to find p_type, p_offset, and p_filesz. + e_fmt, self._p_fmt, self._p_idx = { + (1, 1): ("HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. + (2, 1): ("HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. + }[(self.capacity, self.encoding)] + except KeyError: + raise ELFInvalid( + f"unrecognized capacity ({self.capacity}) or " + f"encoding ({self.encoding})" + ) + + try: + ( + _, + self.machine, # Architecture type. + _, + _, + self._e_phoff, # Offset of program header. + _, + self.flags, # Processor-specific flags. + _, + self._e_phentsize, # Size of section. + self._e_phnum, # Number of sections. + ) = self._read(e_fmt) + except struct.error as e: + raise ELFInvalid("unable to parse machine and section information") from e + + def _read(self, fmt: str) -> Tuple[int, ...]: + return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) + + @property + def interpreter(self) -> Optional[str]: + """ + The path recorded in the ``PT_INTERP`` section header. + """ + for index in range(self._e_phnum): + self._f.seek(self._e_phoff + self._e_phentsize * index) + try: + data = self._read(self._p_fmt) + except struct.error: + continue + if data[self._p_idx[0]] != 3: # Not PT_INTERP. + continue + self._f.seek(data[self._p_idx[1]]) + return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") + return None diff --git a/distutils/_vendor/packaging/_manylinux.py b/distutils/_vendor/packaging/_manylinux.py new file mode 100644 index 00000000..ad62505f --- /dev/null +++ b/distutils/_vendor/packaging/_manylinux.py @@ -0,0 +1,260 @@ +import collections +import contextlib +import functools +import os +import re +import sys +import warnings +from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple + +from ._elffile import EIClass, EIData, ELFFile, EMachine + +EF_ARM_ABIMASK = 0xFF000000 +EF_ARM_ABI_VER5 = 0x05000000 +EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + +# `os.PathLike` not a generic type until Python 3.9, so sticking with `str` +# as the type for `path` until then. +@contextlib.contextmanager +def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: + try: + with open(path, "rb") as f: + yield ELFFile(f) + except (OSError, TypeError, ValueError): + yield None + + +def _is_linux_armhf(executable: str) -> bool: + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.Arm + and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 + and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD + ) + + +def _is_linux_i686(executable: str) -> bool: + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.I386 + ) + + +def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: + if "armv7l" in archs: + return _is_linux_armhf(executable) + if "i686" in archs: + return _is_linux_i686(executable) + 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 +# minor version was, so we can build the complete list of all versions. +# For now, guess what the highest minor version might be, assume it will +# be 50 for testing. Once this actually happens, update the dictionary +# with the actual value. +_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) + + +class _GLibCVersion(NamedTuple): + major: int + minor: int + + +def _glibc_version_string_confstr() -> Optional[str]: + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 + try: + # Should be a string like "glibc 2.17". + 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): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes() -> Optional[str]: + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + # + # We must also handle the special case where the executable is not a + # dynamically linked executable. This can occur when using musl libc, + # for example. In this situation, dlopen() will error, leading to an + # OSError. Interestingly, at least in the case of musl, there is no + # errno set on the OSError. The single string argument used to construct + # OSError comes from libc itself and is therefore not portable to + # hard code here. In any case, failure to call dlopen() means we + # can proceed, so we bail on our attempt. + try: + process_namespace = ctypes.CDLL(None) + except OSError: + return None + + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str: str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +def _glibc_version_string() -> Optional[str]: + """Returns glibc version string, or None if not using glibc.""" + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _parse_glibc_version(version_str: str) -> Tuple[int, int]: + """Parse glibc version. + + We use a regexp instead of str.split because we want to discard any + random junk that might come after the minor version -- this might happen + in patched/forked versions of glibc (e.g. Linaro's version of glibc + uses version strings like "2.20-2014.11"). See gh-3588. + """ + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + f"Expected glibc version with 2 components major.minor," + f" got: {version_str}", + RuntimeWarning, + ) + return -1, -1 + return int(m.group("major")), int(m.group("minor")) + + +@functools.lru_cache() +def _get_glibc_version() -> Tuple[int, int]: + version_str = _glibc_version_string() + if version_str is None: + return (-1, -1) + return _parse_glibc_version(version_str) + + +# From PEP 513, PEP 600 +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 + except ImportError: + return True + if hasattr(_manylinux, "manylinux_compatible"): + result = _manylinux.manylinux_compatible(version[0], version[1], arch) + if result is not None: + return bool(result) + return True + if version == _GLibCVersion(2, 5): + if hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if version == _GLibCVersion(2, 12): + if hasattr(_manylinux, "manylinux2010_compatible"): + return bool(_manylinux.manylinux2010_compatible) + if version == _GLibCVersion(2, 17): + if hasattr(_manylinux, "manylinux2014_compatible"): + return bool(_manylinux.manylinux2014_compatible) + return True + + +_LEGACY_MANYLINUX_MAP = { + # CentOS 7 w/ glibc 2.17 (PEP 599) + (2, 17): "manylinux2014", + # CentOS 6 w/ glibc 2.12 (PEP 571) + (2, 12): "manylinux2010", + # CentOS 5 w/ glibc 2.5 (PEP 513) + (2, 5): "manylinux1", +} + + +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 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()) + glibc_max_list = [current_glibc] + # We can assume compatibility across glibc major versions. + # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 + # + # Build a list of maximum glibc versions so that we can + # output the canonical list of all glibc from current_glibc + # down to too_old_glibc2, including all intermediary versions. + 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 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/distutils/_vendor/packaging/_musllinux.py b/distutils/_vendor/packaging/_musllinux.py new file mode 100644 index 00000000..86419df9 --- /dev/null +++ b/distutils/_vendor/packaging/_musllinux.py @@ -0,0 +1,83 @@ +"""PEP 656 support. + +This module implements logic to detect if the currently running Python is +linked against musl, and what musl version is used. +""" + +import functools +import re +import subprocess +import sys +from typing import Iterator, NamedTuple, Optional, Sequence + +from ._elffile import ELFFile + + +class _MuslVersion(NamedTuple): + major: int + minor: int + + +def _parse_musl_version(output: str) -> Optional[_MuslVersion]: + lines = [n for n in (n.strip() for n in output.splitlines()) if n] + if len(lines) < 2 or lines[0][:4] != "musl": + return None + m = re.match(r"Version (\d+)\.(\d+)", lines[1]) + if not m: + return None + return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) + + +@functools.lru_cache() +def _get_musl_version(executable: str) -> Optional[_MuslVersion]: + """Detect currently-running musl runtime version. + + This is done by checking the specified executable's dynamic linking + information, and invoking the loader to parse its output for a version + string. If the loader is musl, the output would be something like:: + + musl libc (x86_64) + Version 1.2.2 + Dynamic Program Loader + """ + try: + with open(executable, "rb") as f: + ld = ELFFile(f).interpreter + except (OSError, TypeError, ValueError): + return None + if ld is None or "musl" not in ld: + return None + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) + return _parse_musl_version(proc.stderr) + + +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate musllinux 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 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 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 + import sysconfig + + plat = sysconfig.get_platform() + assert plat.startswith("linux-"), "not linux" + + print("plat:", plat) + print("musl:", _get_musl_version(sys.executable)) + print("tags:", end=" ") + for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): + print(t, end="\n ") diff --git a/distutils/_vendor/packaging/_parser.py b/distutils/_vendor/packaging/_parser.py new file mode 100644 index 00000000..684df754 --- /dev/null +++ b/distutils/_vendor/packaging/_parser.py @@ -0,0 +1,356 @@ +"""Handwritten parser of dependency specifiers. + +The docstring for each __parse_* function contains ENBF-inspired grammar representing +the implementation. +""" + +import ast +from typing import Any, List, NamedTuple, Optional, Tuple, Union + +from ._tokenizer import DEFAULT_RULES, Tokenizer + + +class Node: + def __init__(self, value: str) -> None: + self.value = value + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}('{self}')>" + + def serialize(self) -> str: + raise NotImplementedError + + +class Variable(Node): + def serialize(self) -> str: + return str(self) + + +class Value(Node): + def serialize(self) -> str: + return f'"{self}"' + + +class Op(Node): + def serialize(self) -> str: + return str(self) + + +MarkerVar = Union[Variable, Value] +MarkerItem = Tuple[MarkerVar, Op, MarkerVar] +# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]] +# MarkerList = List[Union["MarkerList", MarkerAtom, str]] +# mypy does not support recursive type definition +# https://github.com/python/mypy/issues/731 +MarkerAtom = Any +MarkerList = List[Any] + + +class ParsedRequirement(NamedTuple): + name: str + url: str + extras: List[str] + specifier: str + marker: Optional[MarkerList] + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for dependency specifier +# -------------------------------------------------------------------------------------- +def parse_requirement(source: str) -> ParsedRequirement: + return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement: + """ + requirement = WS? IDENTIFIER WS? extras WS? requirement_details + """ + tokenizer.consume("WS") + + name_token = tokenizer.expect( + "IDENTIFIER", expected="package name at the start of dependency specifier" + ) + name = name_token.text + tokenizer.consume("WS") + + extras = _parse_extras(tokenizer) + tokenizer.consume("WS") + + url, specifier, marker = _parse_requirement_details(tokenizer) + tokenizer.expect("END", expected="end of dependency specifier") + + return ParsedRequirement(name, url, extras, specifier, marker) + + +def _parse_requirement_details( + tokenizer: Tokenizer, +) -> Tuple[str, str, Optional[MarkerList]]: + """ + requirement_details = AT URL (WS requirement_marker?)? + | specifier WS? (requirement_marker)? + """ + + specifier = "" + url = "" + marker = None + + if tokenizer.check("AT"): + tokenizer.read() + tokenizer.consume("WS") + + url_start = tokenizer.position + url = tokenizer.expect("URL", expected="URL after @").text + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + tokenizer.expect("WS", expected="whitespace after URL") + + # The input might end after whitespace. + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, span_start=url_start, after="URL and whitespace" + ) + else: + specifier_start = tokenizer.position + specifier = _parse_specifier(tokenizer) + tokenizer.consume("WS") + + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, + span_start=specifier_start, + after=( + "version specifier" + if specifier + else "name and no valid version specifier" + ), + ) + + return (url, specifier, marker) + + +def _parse_requirement_marker( + tokenizer: Tokenizer, *, span_start: int, after: str +) -> MarkerList: + """ + requirement_marker = SEMICOLON marker WS? + """ + + if not tokenizer.check("SEMICOLON"): + tokenizer.raise_syntax_error( + f"Expected end or semicolon (after {after})", + span_start=span_start, + ) + tokenizer.read() + + marker = _parse_marker(tokenizer) + tokenizer.consume("WS") + + return marker + + +def _parse_extras(tokenizer: Tokenizer) -> List[str]: + """ + extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)? + """ + if not tokenizer.check("LEFT_BRACKET", peek=True): + return [] + + with tokenizer.enclosing_tokens( + "LEFT_BRACKET", + "RIGHT_BRACKET", + around="extras", + ): + tokenizer.consume("WS") + extras = _parse_extras_list(tokenizer) + tokenizer.consume("WS") + + return extras + + +def _parse_extras_list(tokenizer: Tokenizer) -> List[str]: + """ + extras_list = identifier (wsp* ',' wsp* identifier)* + """ + extras: List[str] = [] + + if not tokenizer.check("IDENTIFIER"): + return extras + + extras.append(tokenizer.read().text) + + while True: + tokenizer.consume("WS") + if tokenizer.check("IDENTIFIER", peek=True): + tokenizer.raise_syntax_error("Expected comma between extra names") + elif not tokenizer.check("COMMA"): + break + + tokenizer.read() + tokenizer.consume("WS") + + extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma") + extras.append(extra_token.text) + + return extras + + +def _parse_specifier(tokenizer: Tokenizer) -> str: + """ + specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS + | WS? version_many WS? + """ + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="version specifier", + ): + tokenizer.consume("WS") + parsed_specifiers = _parse_version_many(tokenizer) + tokenizer.consume("WS") + + return parsed_specifiers + + +def _parse_version_many(tokenizer: Tokenizer) -> str: + """ + version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)? + """ + parsed_specifiers = "" + while tokenizer.check("SPECIFIER"): + span_start = tokenizer.position + parsed_specifiers += tokenizer.read().text + if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True): + tokenizer.raise_syntax_error( + ".* suffix can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position + 1, + ) + if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True): + tokenizer.raise_syntax_error( + "Local version label can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position, + ) + tokenizer.consume("WS") + if not tokenizer.check("COMMA"): + break + parsed_specifiers += tokenizer.read().text + tokenizer.consume("WS") + + return parsed_specifiers + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for marker expression +# -------------------------------------------------------------------------------------- +def parse_marker(source: str) -> MarkerList: + 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: + """ + marker = marker_atom (BOOLOP marker_atom)+ + """ + expression = [_parse_marker_atom(tokenizer)] + while tokenizer.check("BOOLOP"): + token = tokenizer.read() + expr_right = _parse_marker_atom(tokenizer) + expression.extend((token.text, expr_right)) + return expression + + +def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom: + """ + marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS? + | WS? marker_item WS? + """ + + tokenizer.consume("WS") + if tokenizer.check("LEFT_PARENTHESIS", peek=True): + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="marker expression", + ): + tokenizer.consume("WS") + marker: MarkerAtom = _parse_marker(tokenizer) + tokenizer.consume("WS") + else: + marker = _parse_marker_item(tokenizer) + tokenizer.consume("WS") + return marker + + +def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem: + """ + marker_item = WS? marker_var WS? marker_op WS? marker_var WS? + """ + tokenizer.consume("WS") + marker_var_left = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + marker_op = _parse_marker_op(tokenizer) + tokenizer.consume("WS") + marker_var_right = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + return (marker_var_left, marker_op, marker_var_right) + + +def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: + """ + marker_var = VARIABLE | QUOTED_STRING + """ + if tokenizer.check("VARIABLE"): + return process_env_var(tokenizer.read().text.replace(".", "_")) + elif tokenizer.check("QUOTED_STRING"): + return process_python_str(tokenizer.read().text) + else: + tokenizer.raise_syntax_error( + message="Expected a marker variable or quoted string" + ) + + +def process_env_var(env_var: str) -> Variable: + if env_var in ("platform_python_implementation", "python_implementation"): + return Variable("platform_python_implementation") + else: + return Variable(env_var) + + +def process_python_str(python_str: str) -> Value: + value = ast.literal_eval(python_str) + return Value(str(value)) + + +def _parse_marker_op(tokenizer: Tokenizer) -> Op: + """ + marker_op = IN | NOT IN | OP + """ + if tokenizer.check("IN"): + tokenizer.read() + return Op("in") + elif tokenizer.check("NOT"): + tokenizer.read() + tokenizer.expect("WS", expected="whitespace after 'not'") + tokenizer.expect("IN", expected="'in' after 'not'") + return Op("not in") + elif tokenizer.check("OP"): + return Op(tokenizer.read().text) + else: + return tokenizer.raise_syntax_error( + "Expected marker operator, one of " + "<=, <, !=, ==, >=, >, ~=, ===, in, not in" + ) diff --git a/distutils/_vendor/packaging/_structures.py b/distutils/_vendor/packaging/_structures.py new file mode 100644 index 00000000..90a6465f --- /dev/null +++ b/distutils/_vendor/packaging/_structures.py @@ -0,0 +1,61 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + + +class InfinityType: + def __repr__(self) -> str: + return "Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return False + + def __le__(self, other: object) -> bool: + return False + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return True + + def __ge__(self, other: object) -> bool: + return True + + def __neg__(self: object) -> "NegativeInfinityType": + return NegativeInfinity + + +Infinity = InfinityType() + + +class NegativeInfinityType: + def __repr__(self) -> str: + return "-Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return True + + def __le__(self, other: object) -> bool: + return True + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return False + + def __ge__(self, other: object) -> bool: + return False + + def __neg__(self: object) -> InfinityType: + return Infinity + + +NegativeInfinity = NegativeInfinityType() diff --git a/distutils/_vendor/packaging/_tokenizer.py b/distutils/_vendor/packaging/_tokenizer.py new file mode 100644 index 00000000..dd0d648d --- /dev/null +++ b/distutils/_vendor/packaging/_tokenizer.py @@ -0,0 +1,192 @@ +import contextlib +import re +from dataclasses import dataclass +from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union + +from .specifiers import Specifier + + +@dataclass +class Token: + name: str + text: str + position: int + + +class ParserSyntaxError(Exception): + """The provided source text could not be parsed correctly.""" + + def __init__( + self, + message: str, + *, + source: str, + span: Tuple[int, int], + ) -> None: + self.span = span + self.message = message + self.source = source + + super().__init__() + + def __str__(self) -> str: + marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" + return "\n ".join([self.message, self.source, marker]) + + +DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { + "LEFT_PARENTHESIS": r"\(", + "RIGHT_PARENTHESIS": r"\)", + "LEFT_BRACKET": r"\[", + "RIGHT_BRACKET": r"\]", + "SEMICOLON": r";", + "COMMA": r",", + "QUOTED_STRING": re.compile( + r""" + ( + ('[^']*') + | + ("[^"]*") + ) + """, + re.VERBOSE, + ), + "OP": r"(===|==|~=|!=|<=|>=|<|>)", + "BOOLOP": r"\b(or|and)\b", + "IN": r"\bin\b", + "NOT": r"\bnot\b", + "VARIABLE": re.compile( + r""" + \b( + python_version + |python_full_version + |os[._]name + |sys[._]platform + |platform_(release|system) + |platform[._](version|machine|python_implementation) + |python_implementation + |implementation_(name|version) + |extra + )\b + """, + re.VERBOSE, + ), + "SPECIFIER": re.compile( + Specifier._operator_regex_str + Specifier._version_regex_str, + re.VERBOSE | re.IGNORECASE, + ), + "AT": r"\@", + "URL": r"[^ \t]+", + "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", + "VERSION_PREFIX_TRAIL": r"\.\*", + "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", + "WS": r"[ \t]+", + "END": r"$", +} + + +class Tokenizer: + """Context-sensitive token parsing. + + Provides methods to examine the input stream to check whether the next token + matches. + """ + + def __init__( + self, + source: str, + *, + rules: "Dict[str, Union[str, re.Pattern[str]]]", + ) -> None: + self.source = source + self.rules: Dict[str, re.Pattern[str]] = { + name: re.compile(pattern) for name, pattern in rules.items() + } + self.next_token: Optional[Token] = None + self.position = 0 + + def consume(self, name: str) -> None: + """Move beyond provided token name, if at current position.""" + if self.check(name): + self.read() + + def check(self, name: str, *, peek: bool = False) -> bool: + """Check whether the next token has the provided name. + + By default, if the check succeeds, the token *must* be read before + another check. If `peek` is set to `True`, the token is not loaded and + would need to be checked again. + """ + assert ( + self.next_token is None + ), f"Cannot check for {name!r}, already have {self.next_token!r}" + assert name in self.rules, f"Unknown token name: {name!r}" + + expression = self.rules[name] + + match = expression.match(self.source, self.position) + if match is None: + return False + if not peek: + self.next_token = Token(name, match[0], self.position) + return True + + def expect(self, name: str, *, expected: str) -> Token: + """Expect a certain token name next, failing with a syntax error otherwise. + + The token is *not* read. + """ + if not self.check(name): + raise self.raise_syntax_error(f"Expected {expected}") + return self.read() + + def read(self) -> Token: + """Consume the next token and return it.""" + token = self.next_token + assert token is not None + + self.position += len(token.text) + self.next_token = None + + return token + + def raise_syntax_error( + self, + message: str, + *, + span_start: Optional[int] = None, + span_end: Optional[int] = None, + ) -> NoReturn: + """Raise ParserSyntaxError at the given position.""" + span = ( + self.position if span_start is None else span_start, + self.position if span_end is None else span_end, + ) + raise ParserSyntaxError( + message, + source=self.source, + span=span, + ) + + @contextlib.contextmanager + def enclosing_tokens( + self, open_token: str, close_token: str, *, around: str + ) -> Iterator[None]: + if self.check(open_token): + open_position = self.position + self.read() + else: + open_position = None + + yield + + if open_position is None: + return + + if not self.check(close_token): + self.raise_syntax_error( + f"Expected matching {close_token} for {open_token}, after {around}", + span_start=open_position, + ) + + self.read() diff --git a/distutils/_vendor/packaging/markers.py b/distutils/_vendor/packaging/markers.py new file mode 100644 index 00000000..8b98fca7 --- /dev/null +++ b/distutils/_vendor/packaging/markers.py @@ -0,0 +1,252 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import operator +import os +import platform +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from ._parser import ( + MarkerAtom, + MarkerList, + Op, + Value, + Variable, + parse_marker as _parse_marker, +) +from ._tokenizer import ParserSyntaxError +from .specifiers import InvalidSpecifier, Specifier +from .utils import canonicalize_name + +__all__ = [ + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", +] + +Operator = Callable[[str, str], bool] + + +class InvalidMarker(ValueError): + """ + An invalid marker was found, users should refer to PEP 508. + """ + + +class UndefinedComparison(ValueError): + """ + An invalid operation was attempted on a value that doesn't support it. + """ + + +class UndefinedEnvironmentName(ValueError): + """ + A name was attempted to be used that does not exist inside of the + environment. + """ + + +def _normalize_extra_values(results: Any) -> Any: + """ + Normalize extra values. + """ + if isinstance(results[0], tuple): + lhs, op, rhs = results[0] + if isinstance(lhs, Variable) and lhs.value == "extra": + normalized_extra = canonicalize_name(rhs.value) + rhs = Value(normalized_extra) + elif isinstance(rhs, Variable) and rhs.value == "extra": + normalized_extra = canonicalize_name(lhs.value) + lhs = Value(normalized_extra) + results[0] = lhs, op, rhs + return results + + +def _format_marker( + marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True +) -> str: + + assert isinstance(marker, (list, tuple, str)) + + # Sometimes we have a structure like [[...]] which is a single item list + # where the single item is itself it's own list. In that case we want skip + # the rest of this function so that we don't get extraneous () on the + # outside. + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): + return _format_marker(marker[0]) + + if isinstance(marker, list): + inner = (_format_marker(m, first=False) for m in marker) + if first: + return " ".join(inner) + else: + return "(" + " ".join(inner) + ")" + elif isinstance(marker, tuple): + return " ".join([m.serialize() for m in marker]) + else: + return marker + + +_operators: Dict[str, Operator] = { + "in": lambda lhs, rhs: lhs in rhs, + "not in": lambda lhs, rhs: lhs not in rhs, + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">=": operator.ge, + ">": operator.gt, +} + + +def _eval_op(lhs: str, op: Op, rhs: str) -> bool: + try: + spec = Specifier("".join([op.serialize(), rhs])) + except InvalidSpecifier: + pass + else: + return spec.contains(lhs, prereleases=True) + + oper: Optional[Operator] = _operators.get(op.serialize()) + if oper is None: + raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") + + return oper(lhs, rhs) + + +def _normalize(*values: str, key: str) -> Tuple[str, ...]: + # PEP 685 – Comparison of extra names for optional distribution dependencies + # https://peps.python.org/pep-0685/ + # > When comparing extra names, tools MUST normalize the names being + # > compared using the semantics outlined in PEP 503 for names + if key == "extra": + return tuple(canonicalize_name(v) for v in values) + + # other environment markers don't have such standards + return values + + +def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: + groups: List[List[bool]] = [[]] + + for marker in markers: + assert isinstance(marker, (list, tuple, str)) + + if isinstance(marker, list): + groups[-1].append(_evaluate_markers(marker, environment)) + elif isinstance(marker, tuple): + lhs, op, rhs = marker + + if isinstance(lhs, Variable): + environment_key = lhs.value + lhs_value = environment[environment_key] + rhs_value = rhs.value + else: + lhs_value = lhs.value + environment_key = rhs.value + rhs_value = environment[environment_key] + + lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) + groups[-1].append(_eval_op(lhs_value, op, rhs_value)) + else: + assert marker in ["and", "or"] + if marker == "or": + groups.append([]) + + return any(all(item) for item in groups) + + +def format_full_version(info: "sys._version_info") -> str: + version = "{0.major}.{0.minor}.{0.micro}".format(info) + kind = info.releaselevel + if kind != "final": + version += kind[0] + str(info.serial) + return version + + +def default_environment() -> Dict[str, str]: + iver = format_full_version(sys.implementation.version) + implementation_name = sys.implementation.name + return { + "implementation_name": implementation_name, + "implementation_version": iver, + "os_name": os.name, + "platform_machine": platform.machine(), + "platform_release": platform.release(), + "platform_system": platform.system(), + "platform_version": platform.version(), + "python_full_version": platform.python_version(), + "platform_python_implementation": platform.python_implementation(), + "python_version": ".".join(platform.python_version_tuple()[:2]), + "sys_platform": sys.platform, + } + + +class Marker: + def __init__(self, marker: str) -> None: + # Note: We create a Marker object without calling this constructor in + # packaging.requirements.Requirement. If any additional logic is + # added here, make sure to mirror/adapt Requirement. + try: + self._markers = _normalize_extra_values(_parse_marker(marker)) + # The attribute `_markers` can be described in terms of a recursive type: + # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] + # + # For example, the following expression: + # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") + # + # is parsed into: + # [ + # (, ')>, ), + # 'and', + # [ + # (, , ), + # 'or', + # (, , ) + # ] + # ] + except ParserSyntaxError as e: + raise InvalidMarker(str(e)) from e + + def __str__(self) -> str: + return _format_marker(self._markers) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash((self.__class__.__name__, str(self))) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Marker): + return NotImplemented + + return str(self) == str(other) + + def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: + """Evaluate a marker. + + Return the boolean from evaluating the given marker against the + environment. environment is an optional argument to override all or + part of the determined environment. + + The environment is determined from the current Python process. + """ + current_environment = default_environment() + current_environment["extra"] = "" + if environment is not None: + current_environment.update(environment) + # The API used to allow setting extra to None. We need to handle this + # case for backwards compatibility. + if current_environment["extra"] is None: + current_environment["extra"] = "" + + return _evaluate_markers(self._markers, current_environment) diff --git a/distutils/_vendor/packaging/metadata.py b/distutils/_vendor/packaging/metadata.py new file mode 100644 index 00000000..fb274930 --- /dev/null +++ b/distutils/_vendor/packaging/metadata.py @@ -0,0 +1,825 @@ +import email.feedparser +import email.header +import email.message +import email.parser +import email.policy +import sys +import typing +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 Literal, TypedDict + else: + try: + 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 +# serializing to and from that format. +class RawMetadata(TypedDict, total=False): + """A dictionary of raw core metadata. + + Each field in core metadata maps to a key of this dictionary (when data is + 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. 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 + which hold multiple values in a single field are stored as a list. + + """ + + # Metadata 1.0 - PEP 241 + metadata_version: str + name: str + version: str + platforms: List[str] + summary: str + description: str + keywords: List[str] + home_page: str + author: str + author_email: str + license: str + + # Metadata 1.1 - PEP 314 + supported_platforms: List[str] + download_url: str + classifiers: List[str] + requires: List[str] + provides: List[str] + obsoletes: List[str] + + # Metadata 1.2 - PEP 345 + maintainer: str + maintainer_email: str + requires_dist: List[str] + provides_dist: List[str] + obsoletes_dist: List[str] + requires_python: str + requires_external: List[str] + project_urls: Dict[str, str] + + # Metadata 2.0 + # PEP 426 attempted to completely revamp the metadata format + # but got stuck without ever being able to build consensus on + # it and ultimately ended up withdrawn. + # + # However, a number of tools had started emitting METADATA with + # `2.0` Metadata-Version, so for historical reasons, this version + # was skipped. + + # Metadata 2.1 - PEP 566 + description_content_type: str + provides_extra: List[str] + + # Metadata 2.2 - PEP 643 + dynamic: List[str] + + # Metadata 2.3 - PEP 685 + # No new fields were added in PEP 685, just some edge case were + # tightened up to provide better interoptability. + + +_STRING_FIELDS = { + "author", + "author_email", + "description", + "description_content_type", + "download_url", + "home_page", + "license", + "maintainer", + "maintainer_email", + "metadata_version", + "name", + "requires_python", + "summary", + "version", +} + +_LIST_FIELDS = { + "classifiers", + "dynamic", + "obsoletes", + "obsoletes_dist", + "platforms", + "provides", + "provides_dist", + "provides_extra", + "requires", + "requires_dist", + "requires_external", + "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.""" + return [k.strip() for k in data.split(",")] + + +def _parse_project_urls(data: List[str]) -> Dict[str, str]: + """Parse a list of label/URL string pairings separated by a comma.""" + urls = {} + for pair in data: + # Our logic is slightly tricky here as we want to try and do + # *something* reasonable with malformed data. + # + # The main thing that we have to worry about, is data that does + # not have a ',' at all to split the label from the Value. There + # isn't a singular right answer here, and we will fail validation + # later on (if the caller is validating) so it doesn't *really* + # matter, but since the missing value has to be an empty str + # and our return value is dict[str, str], if we let the key + # be the missing value, then they'd have multiple '' values that + # overwrite each other in a accumulating dict. + # + # The other potentional issue is that it's possible to have the + # same label multiple times in the metadata, with no solid "right" + # answer with what to do in that case. As such, we'll do the only + # thing we can, which is treat the field as unparseable and add it + # to our list of unparsed fields. + parts = [p.strip() for p in pair.split(",", 1)] + parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items + + # TODO: The spec doesn't say anything about if the keys should be + # considered case sensitive or not... logically they should + # be case-preserving and case-insensitive, but doing that + # would open up more cases where we might have duplicate + # entries. + label, url = parts + if label in urls: + # The label already exists in our set of urls, so this field + # is unparseable, and we can just add the whole thing to our + # unparseable data and stop processing it. + raise KeyError("duplicate labels in project urls") + urls[label] = url + + return urls + + +def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str: + """Get the body of the message.""" + # If our source is a str, then our caller has managed encodings for us, + # and we don't need to deal with it. + if isinstance(source, str): + payload: str = msg.get_payload() + return payload + # If our source is a bytes, then we're managing the encoding and we need + # to deal with it. + else: + bpayload: bytes = msg.get_payload(decode=True) + try: + return bpayload.decode("utf8", "strict") + except UnicodeDecodeError: + raise ValueError("payload in an invalid encoding") + + +# The various parse_FORMAT functions here are intended to be as lenient as +# possible in their parsing, while still returning a correctly typed +# RawMetadata. +# +# To aid in this, we also generally want to do as little touching of the +# data as possible, except where there are possibly some historic holdovers +# that make valid data awkward to work with. +# +# While this is a lower level, intermediate format than our ``Metadata`` +# class, some light touch ups can make a massive difference in usability. + +# Map METADATA fields to RawMetadata. +_EMAIL_TO_RAW_MAPPING = { + "author": "author", + "author-email": "author_email", + "classifier": "classifiers", + "description": "description", + "description-content-type": "description_content_type", + "download-url": "download_url", + "dynamic": "dynamic", + "home-page": "home_page", + "keywords": "keywords", + "license": "license", + "maintainer": "maintainer", + "maintainer-email": "maintainer_email", + "metadata-version": "metadata_version", + "name": "name", + "obsoletes": "obsoletes", + "obsoletes-dist": "obsoletes_dist", + "platform": "platforms", + "project-url": "project_urls", + "provides": "provides", + "provides-dist": "provides_dist", + "provides-extra": "provides_extra", + "requires": "requires", + "requires-dist": "requires_dist", + "requires-external": "requires_external", + "requires-python": "requires_python", + "summary": "summary", + "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 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 + parsed and translated into Python's built-in types are converted + appropriately. All other fields are left as-is. Fields that are allowed to + appear multiple times are stored as lists. + + The second dict contains all other fields from the metadata. This includes + any unrecognized fields. It also includes any fields which are expected to + be parsed into a built-in type but were not formatted appropriately. Finally, + any fields that are expected to appear only once but are repeated are + included in this dict. + + """ + raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {} + unparsed: Dict[str, List[str]] = {} + + if isinstance(data, str): + parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data) + else: + parsed = email.parser.BytesParser(policy=email.policy.compat32).parsebytes(data) + + # We have to wrap parsed.keys() in a set, because in the case of multiple + # values for a key (a list), the key will appear multiple times in the + # list of keys, but we're avoiding that by using get_all(). + for name in frozenset(parsed.keys()): + # Header names in RFC are case insensitive, so we'll normalize to all + # lower case to make comparisons easier. + name = name.lower() + + # 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) or [] + + # The way the email module works when parsing bytes is that it + # unconditionally decodes the bytes as ascii using the surrogateescape + # handler. When you pull that data back out (such as with get_all() ), + # it looks to see if the str has any surrogate escapes, and if it does + # it wraps it in a Header object instead of returning the string. + # + # As such, we'll look for those Header objects, and fix up the encoding. + value = [] + # Flag if we have run into any issues processing the headers, thus + # signalling that the data belongs in 'unparsed'. + valid_encoding = True + for h in headers: + # It's unclear if this can return more types than just a Header or + # a str, so we'll just assert here to make sure. + assert isinstance(h, (email.header.Header, str)) + + # If it's a header object, we need to do our little dance to get + # the real data out of it. In cases where there is invalid data + # we're going to end up with mojibake, but there's no obvious, good + # way around that without reimplementing parts of the Header object + # ourselves. + # + # That should be fine since, if mojibacked happens, this key is + # going into the unparsed dict anyways. + if isinstance(h, email.header.Header): + # The Header object stores it's data as chunks, and each chunk + # can be independently encoded, so we'll need to check each + # of them. + chunks: List[Tuple[bytes, Optional[str]]] = [] + for bin, encoding in email.header.decode_header(h): + try: + bin.decode("utf8", "strict") + except UnicodeDecodeError: + # Enable mojibake. + encoding = "latin1" + valid_encoding = False + else: + encoding = "utf8" + chunks.append((bin, encoding)) + + # Turn our chunks back into a Header object, then let that + # Header object do the right thing to turn them into a + # string for us. + value.append(str(email.header.make_header(chunks))) + # This is already a string, so just add it. + else: + value.append(h) + + # We've processed all of our values to get them into a list of str, + # but we may have mojibake data, in which case this is an unparsed + # field. + if not valid_encoding: + unparsed[name] = value + continue + + raw_name = _EMAIL_TO_RAW_MAPPING.get(name) + if raw_name is None: + # This is a bit of a weird situation, we've encountered a key that + # we don't know what it means, so we don't know whether it's meant + # to be a list or not. + # + # Since we can't really tell one way or another, we'll just leave it + # as a list, even though it may be a single item list, because that's + # what makes the most sense for email headers. + unparsed[name] = value + continue + + # If this is one of our string fields, then we'll check to see if our + # value is a list of a single item. If it is then we'll assume that + # it was emitted as a single string, and unwrap the str from inside + # the list. + # + # If it's any other kind of data, then we haven't the faintest clue + # what we should parse it as, and we have to just add it to our list + # of unparsed stuff. + if raw_name in _STRING_FIELDS and len(value) == 1: + raw[raw_name] = value[0] + # 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_FIELDS: + raw[raw_name] = value + # Special Case: Keywords + # The keywords field is implemented in the metadata spec as a str, + # but it conceptually is a list of strings, and is serialized using + # ", ".join(keywords), so we'll do some light data massaging to turn + # this into what it logically is. + elif raw_name == "keywords" and len(value) == 1: + raw[raw_name] = _parse_keywords(value[0]) + # Special Case: Project-URL + # The project urls is implemented in the metadata spec as a list of + # specially-formatted strings that represent a key and a value, which + # is fundamentally a mapping, however the email format doesn't support + # mappings in a sane way, so it was crammed into a list of strings + # instead. + # + # We will do a little light data massaging to turn this into a map as + # it logically should be. + elif raw_name == "project_urls": + try: + raw[raw_name] = _parse_project_urls(value) + except KeyError: + unparsed[name] = value + # Nothing that we've done has managed to parse this, so it'll just + # throw it in our unparseable data and move on. + else: + unparsed[name] = value + + # We need to support getting the Description from the message payload in + # addition to getting it from the the headers. This does mean, though, there + # is the possibility of it being set both ways, in which case we put both + # in 'unparsed' since we don't know which is right. + try: + payload = _get_payload(parsed, data) + except ValueError: + unparsed.setdefault("description", []).append( + parsed.get_payload(decode=isinstance(data, bytes)) + ) + else: + if payload: + # Check to see if we've already got a description, if so then both + # it, and this body move to unparseable. + if "description" in raw: + description_header = cast(str, raw.pop("description")) + unparsed.setdefault("description", []).extend( + [description_header, payload] + ) + elif "description" in unparsed: + unparsed["description"].append(payload) + else: + raw["description"] = payload + + # We need to cast our `raw` to a metadata, because a TypedDict only support + # literal key names, but we're computing our key names on purpose, but the + # 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/distutils/_vendor/packaging/py.typed b/distutils/_vendor/packaging/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/distutils/_vendor/packaging/requirements.py b/distutils/_vendor/packaging/requirements.py new file mode 100644 index 00000000..bdc43a7e --- /dev/null +++ b/distutils/_vendor/packaging/requirements.py @@ -0,0 +1,90 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +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): + """ + An invalid requirement was found, users should refer to PEP 508. + """ + + +class Requirement: + """Parse a requirement. + + Parse a given requirement string into its parts, such as name, specifier, + URL, and extras. Raises InvalidRequirement on a badly-formed requirement + string. + """ + + # TODO: Can we test whether something is contained within a requirement? + # If so how do we do that? Do we need to test against the _name_ of + # the thing as well as the version? What about the markers? + # TODO: Can we normalize the name and extra name? + + def __init__(self, requirement_string: str) -> None: + try: + parsed = _parse_requirement(requirement_string) + except ParserSyntaxError as e: + raise InvalidRequirement(str(e)) from e + + self.name: str = parsed.name + 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 _iter_parts(self, name: str) -> Iterator[str]: + yield name + + if self.extras: + formatted_extras = ",".join(sorted(self.extras)) + yield f"[{formatted_extras}]" + + if self.specifier: + yield str(self.specifier) + + if self.url: + yield f"@ {self.url}" + if self.marker: + yield " " + + if self.marker: + yield f"; {self.marker}" + + 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__, + *self._iter_parts(canonicalize_name(self.name)), + ) + ) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Requirement): + return NotImplemented + + return ( + canonicalize_name(self.name) == canonicalize_name(other.name) + and self.extras == other.extras + and self.specifier == other.specifier + and self.url == other.url + and self.marker == other.marker + ) diff --git a/distutils/_vendor/packaging/specifiers.py b/distutils/_vendor/packaging/specifiers.py new file mode 100644 index 00000000..2d015bab --- /dev/null +++ b/distutils/_vendor/packaging/specifiers.py @@ -0,0 +1,1017 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier + from packaging.version import Version +""" + +import abc +import itertools +import re +from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union + +from .utils import canonicalize_version +from .version import Version + +UnparsedVersion = Union[Version, str] +UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) +CallableOperator = Callable[[Version, str], bool] + + +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version + + +class InvalidSpecifier(ValueError): + """ + Raised when attempting to create a :class:`Specifier` with a specifier + string that is invalid. + + >>> Specifier("lolwat") + Traceback (most recent call last): + ... + packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' + """ + + +class BaseSpecifier(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __str__(self) -> str: + """ + Returns the str representation of this Specifier-like object. This + should be representative of the Specifier itself. + """ + + @abc.abstractmethod + def __hash__(self) -> int: + """ + Returns a hash value for this Specifier-like object. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Returns a boolean representing whether or not the two Specifier-like + objects are equal. + + :param other: The other object to check against. + """ + + @property + @abc.abstractmethod + def prereleases(self) -> Optional[bool]: + """Whether or not pre-releases as a whole are allowed. + + This can be set to either ``True`` or ``False`` to explicitly enable or disable + prereleases or it can be set to ``None`` (the default) to use default semantics. + """ + + @prereleases.setter + def prereleases(self, value: bool) -> None: + """Setter for :attr:`prereleases`. + + :param value: The value to set. + """ + + @abc.abstractmethod + def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: + """ + Determines if the given item is contained within this specifier. + """ + + @abc.abstractmethod + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """ + Takes an iterable of items and filters them so that only items which + are contained within this specifier are allowed in it. + """ + + +class Specifier(BaseSpecifier): + """This class abstracts handling of version specifiers. + + .. tip:: + + It is generally not required to instantiate this manually. You should instead + prefer to work with :class:`SpecifierSet` instead, which can parse + comma-separated version specifiers (which is what package metadata contains). + """ + + _operator_regex_str = r""" + (?P(~=|==|!=|<=|>=|<|>|===)) + """ + _version_regex_str = r""" + (?P + (?: + # The identity operators allow for an escape hatch that will + # do an exact string match of the version you wish to install. + # This will not be parsed by PEP 440 and we cannot determine + # any semantic meaning from it. This operator is discouraged + # but included entirely as an escape hatch. + (?<====) # Only match for the identity operator + \s* + [^\s;)]* # The arbitrary version can be just about anything, + # we match everything except for whitespace, a + # semi-colon for marker support, and a closing paren + # since versions can be enclosed in them. + ) + | + (?: + # The (non)equality operators allow for wild card and local + # versions to be specified so we have to define these two + # operators separately to enable that. + (?<===|!=) # Only match for equals and not equals + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)* # release + + # You cannot use a wild card and a pre-release, post-release, a dev or + # local version together so group them with a | and make them optional. + (?: + \.\* # Wild card syntax of .* + | + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local + )? + ) + | + (?: + # The compatible operator requires at least two digits in the + # release segment. + (?<=~=) # Only match for the compatible operator + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + ) + | + (?: + # All other operators only allow a sub set of what the + # (non)equality operators do. Specifically they do not allow + # local versions to be specified nor do they allow the prefix + # matching wild cards. + (?=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + "===": "arbitrary", + } + + def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: + """Initialize a Specifier instance. + + :param spec: + The string representation of a specifier which will be parsed and + normalized before use. + :param prereleases: + This tells the specifier if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + :raises InvalidSpecifier: + If the given specifier is invalid (i.e. bad syntax). + """ + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier(f"Invalid specifier: '{spec}'") + + self._spec: Tuple[str, str] = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 + @property # type: ignore[override] + def prereleases(self) -> bool: + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~=", "==="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if Version(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + @property + def operator(self) -> str: + """The operator of this specifier. + + >>> Specifier("==1.2.3").operator + '==' + """ + return self._spec[0] + + @property + def version(self) -> str: + """The version of this specifier. + + >>> Specifier("==1.2.3").version + '1.2.3' + """ + return self._spec[1] + + def __repr__(self) -> str: + """A representation of the Specifier that shows all internal state. + + >>> Specifier('>=1.0.0') + =1.0.0')> + >>> Specifier('>=1.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> Specifier('>=1.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" + + def __str__(self) -> str: + """A string representation of the Specifier that can be round-tripped. + + >>> str(Specifier('>=1.0.0')) + '>=1.0.0' + >>> str(Specifier('>=1.0.0', prereleases=False)) + '>=1.0.0' + """ + return "{}{}".format(*self._spec) + + @property + def _canonical_spec(self) -> Tuple[str, str]: + canonical_version = canonicalize_version( + self._spec[1], + strip_trailing_zero=(self._spec[0] != "~="), + ) + return self._spec[0], canonical_version + + def __hash__(self) -> int: + return hash(self._canonical_spec) + + def __eq__(self, other: object) -> bool: + """Whether or not the two Specifier-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") + True + >>> (Specifier("==1.2.3", prereleases=False) == + ... Specifier("==1.2.3", prereleases=True)) + True + >>> Specifier("==1.2.3") == "==1.2.3" + True + >>> Specifier("==1.2.3") == Specifier("==1.2.4") + False + >>> Specifier("==1.2.3") == Specifier("~=1.2.3") + False + """ + if isinstance(other, str): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._canonical_spec == other._canonical_spec + + def _get_operator(self, op: str) -> CallableOperator: + operator_callable: CallableOperator = getattr( + self, f"_compare_{self._operators[op]}" + ) + return operator_callable + + def _compare_compatible(self, prospective: Version, spec: str) -> bool: + + # Compatible releases have an equivalent combination of >= and ==. That + # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to + # implement this in terms of the other specifiers instead of + # implementing it ourselves. The only thing we need to do is construct + # the other specifiers. + + # We want everything but the last item in the version, but we want to + # ignore suffix segments. + prefix = _version_join( + list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] + ) + + # Add the prefix notation to the end of our string + prefix += ".*" + + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) + + def _compare_equal(self, prospective: Version, spec: str) -> bool: + + # We need special logic to handle prefix matching + if spec.endswith(".*"): + # In the case of prefix matching we want to ignore local segment. + normalized_prospective = canonicalize_version( + prospective.public, strip_trailing_zero=False + ) + # Get the normalized version string ignoring the trailing .* + normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) + # 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 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 + # shortened version. + padded_prospective, _ = _pad_version(split_prospective, split_spec) + + # Shorten the prospective version to be the same length as the spec + # so that we can determine if the specifier is a prefix of the + # prospective version or not. + shortened_prospective = padded_prospective[: len(split_spec)] + + return shortened_prospective == split_spec + else: + # Convert our spec string into a Version + spec_version = Version(spec) + + # If the specifier does not have a local segment, then we want to + # act as if the prospective version also does not have a local + # segment. + if not spec_version.local: + prospective = Version(prospective.public) + + return prospective == spec_version + + def _compare_not_equal(self, prospective: Version, spec: str) -> bool: + return not self._compare_equal(prospective, spec) + + def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) + + def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) + + def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is less than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective < spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a pre-release version, that we do not accept pre-release + # versions for the version mentioned in the specifier (e.g. <3.1 should + # not match 3.1.dev0, but should match 3.0.dev0). + if not spec.is_prerelease and prospective.is_prerelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # less than the spec version *and* it's not a pre-release of the same + # version in the spec. + return True + + def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is greater than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective > spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a post-release version, that we do not accept + # post-release versions for the version mentioned in the specifier + # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). + if not spec.is_postrelease and prospective.is_postrelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # Ensure that we do not allow a local version of the version mentioned + # in the specifier, which is technically greater than, to match. + if prospective.local is not None: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # greater than the spec version *and* it's not a pre-release of the + # same version in the spec. + return True + + def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: + return str(prospective).lower() == str(spec).lower() + + def __contains__(self, item: Union[str, Version]) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in Specifier(">=1.2.3") + True + >>> Version("1.2.3") in Specifier(">=1.2.3") + True + >>> "1.0.0" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, item: UnparsedVersion, prereleases: Optional[bool] = None + ) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this Specifier. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> Specifier(">=1.2.3").contains("1.2.3") + True + >>> Specifier(">=1.2.3").contains(Version("1.2.3")) + True + >>> Specifier(">=1.2.3").contains("1.0.0") + False + >>> Specifier(">=1.2.3").contains("1.3.0a1") + False + >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") + True + >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) + True + """ + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") + normalized_item = _coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable: CallableOperator = self._get_operator(self.operator) + return operator_callable(normalized_item, self.version) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifier. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(Specifier().contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) + ['1.2.3', '1.3', ] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) + ['1.5a1'] + >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + """ + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = _coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later in case nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + + +_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") + + +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] = [] + + 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()) + else: + result.append(item) + 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") + ) + + +def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: + left_split, right_split = [], [] + + # Get the release segment of our versions + left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) + right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) + + # Get the rest of our versions + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) + + # Insert our padding + 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.from_iterable(left_split)), + list(itertools.chain.from_iterable(right_split)), + ) + + +class SpecifierSet(BaseSpecifier): + """This class abstracts handling of a set of version specifiers. + + It can be passed a single specifier (``>=3.0``), a comma-separated list of + specifiers (``>=3.0,!=3.1``), or no specifier at all. + """ + + def __init__( + self, specifiers: str = "", prereleases: Optional[bool] = None + ) -> None: + """Initialize a SpecifierSet instance. + + :param specifiers: + The string representation of a specifier or a comma-separated list of + specifiers which will be parsed and normalized before use. + :param prereleases: + This tells the SpecifierSet if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + + :raises InvalidSpecifier: + If the given ``specifiers`` are not parseable than this exception will be + raised. + """ + + # Split on `,` to break each individual specifier into it's own item, and + # strip each item to remove leading/trailing whitespace. + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + + # 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. + self._prereleases = prereleases + + @property + def prereleases(self) -> Optional[bool]: + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # If we don't have any specifiers, and we don't have a forced value, + # then we'll just return None since we don't know if this should have + # pre-releases or not. + if not self._specs: + return None + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + def __repr__(self) -> str: + """A representation of the specifier set that shows all internal state. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> SpecifierSet('>=1.0.0,!=2.0.0') + =1.0.0')> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"" + + def __str__(self) -> str: + """A string representation of the specifier set that can be round-tripped. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) + '!=1.0.1,>=1.0.0' + >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) + '!=1.0.1,>=1.0.0' + """ + return ",".join(sorted(str(s) for s in self._specs)) + + def __hash__(self) -> int: + return hash(self._specs) + + def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": + """Return a SpecifierSet which is a combination of the two sets. + + :param other: The other object to combine with. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' + =1.0.0')> + >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') + =1.0.0')> + """ + if isinstance(other, str): + other = SpecifierSet(other) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + specifier = SpecifierSet() + specifier._specs = frozenset(self._specs | other._specs) + + if self._prereleases is None and other._prereleases is not None: + specifier._prereleases = other._prereleases + elif self._prereleases is not None and other._prereleases is None: + specifier._prereleases = self._prereleases + elif self._prereleases == other._prereleases: + specifier._prereleases = self._prereleases + else: + raise ValueError( + "Cannot combine SpecifierSets with True and False prerelease " + "overrides." + ) + + return specifier + + def __eq__(self, other: object) -> bool: + """Whether or not the two SpecifierSet-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == + ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") + False + """ + if isinstance(other, (str, Specifier)): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs == other._specs + + def __len__(self) -> int: + """Returns the number of specifiers in this specifier set.""" + return len(self._specs) + + def __iter__(self) -> Iterator[Specifier]: + """ + Returns an iterator over all the underlying :class:`Specifier` instances + in this specifier set. + + >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) + [, =1.0.0')>] + """ + return iter(self._specs) + + def __contains__(self, item: UnparsedVersion) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, + item: UnparsedVersion, + prereleases: Optional[bool] = None, + installed: Optional[bool] = None, + ) -> bool: + """Return whether or not the item is contained in this SpecifierSet. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this SpecifierSet. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) + True + """ + # Ensure that our item is a Version instance. + if not isinstance(item, Version): + item = Version(item) + + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # We can determine if we're going to allow pre-releases by looking to + # see if any of the underlying items supports them. If none of them do + # and this item is a pre-release then we do not allow it and we can + # short circuit that here. + # Note: This means that 1.0.dev1 would not be contained in something + # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 + if not prereleases and item.is_prerelease: + return False + + if installed and item.is_prerelease: + item = Version(item.base_version) + + # We simply dispatch to the underlying specs here to make sure that the + # given version is contained within all of them. + # Note: This use of all() here means that an empty set of specifiers + # will always return True, this is an explicit design decision. + return all(s.contains(item, prereleases=prereleases) for s in self._specs) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifiers in this set. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) + ['1.3', ] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) + [] + >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + + An "empty" SpecifierSet will filter items based on the presence of prerelease + versions in the set. + + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet("").filter(["1.5a1"])) + ['1.5a1'] + >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + """ + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # If we have any specifiers, then we want to wrap our iterable in the + # filter method for each one, this will act as a logical AND amongst + # each specifier. + if self._specs: + for spec in self._specs: + iterable = spec.filter(iterable, prereleases=bool(prereleases)) + return iter(iterable) + # If we do not have any specifiers, then we need to have a rough filter + # which will filter out any pre-releases, unless there are no final + # releases. + else: + filtered: List[UnparsedVersionVar] = [] + found_prereleases: List[UnparsedVersionVar] = [] + + for item in iterable: + parsed_version = _coerce_version(item) + + # Store any item which is a pre-release for later unless we've + # already found a final version or we are accepting prereleases + if parsed_version.is_prerelease and not prereleases: + if not filtered: + found_prereleases.append(item) + else: + filtered.append(item) + + # If we've found no items except for pre-releases, then we'll go + # ahead and use the pre-releases + if not filtered and found_prereleases and prereleases is None: + return iter(found_prereleases) + + return iter(filtered) diff --git a/distutils/_vendor/packaging/tags.py b/distutils/_vendor/packaging/tags.py new file mode 100644 index 00000000..89f19261 --- /dev/null +++ b/distutils/_vendor/packaging/tags.py @@ -0,0 +1,571 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import logging +import platform +import re +import struct +import subprocess +import sys +import sysconfig +from importlib.machinery import EXTENSION_SUFFIXES +from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) + +from . import _manylinux, _musllinux + +logger = logging.getLogger(__name__) + +PythonVersion = Sequence[int] +MacVersion = Tuple[int, int] + +INTERPRETER_SHORT_NAMES: Dict[str, str] = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = struct.calcsize("P") == 4 + + +class Tag: + """ + A representation of the tag triple for a wheel. + + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ + + __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] + + def __init__(self, interpreter: str, abi: str, platform: str) -> None: + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + # The __hash__ of every single element in a Set[Tag] will be evaluated each time + # that a set calls its `.disjoint()` method, which may be called hundreds of + # times when scanning a page of links for packages with tags matching that + # Set[Tag]. Pre-computing the value here produces significant speedups for + # downstream consumers. + self._hash = hash((self._interpreter, self._abi, self._platform)) + + @property + def interpreter(self) -> str: + return self._interpreter + + @property + def abi(self) -> str: + return self._abi + + @property + def platform(self) -> str: + return self._platform + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Tag): + return NotImplemented + + return ( + (self._hash == other._hash) # Short-circuit ASAP for perf reasons. + and (self._platform == other._platform) + and (self._abi == other._abi) + and (self._interpreter == other._interpreter) + ) + + def __hash__(self) -> int: + return self._hash + + def __str__(self) -> str: + return f"{self._interpreter}-{self._abi}-{self._platform}" + + def __repr__(self) -> str: + return f"<{self} @ {id(self)}>" + + +def parse_tag(tag: str) -> FrozenSet[Tag]: + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: + value: Union[int, str, None] = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + +def _normalize_string(string: str) -> str: + return string.replace(".", "_").replace("-", "_").replace(" ", "_") + + +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. The threaded (`--disable-gil`) + builds do not support abi3. + """ + 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]) + 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 + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + 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: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + 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}{threading}") + abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") + return abis + + +def cpython_tags( + python_version: Optional[PythonVersion] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + if not python_version: + python_version = sys.version_info[:2] + + interpreter = f"cp{_version_nodot(python_version[:2])}" + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or platform_tags()) + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + 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 use_abi3: + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) + ) + yield Tag(interpreter, "abi3", platform_) + + +def _generic_abi() -> List[str]: + """ + Return the ABI tag based on EXT_SUFFIX. + """ + # The following are examples of `EXT_SUFFIX`. + # We want to keep the parts which are related to the ABI and remove the + # parts which are related to the platform: + # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 + # - mac: '.cpython-310-darwin.so' => cp310 + # - win: '.cp310-win_amd64.pyd' => cp310 + # - win: '.pyd' => cp37 (uses _cpython_abis()) + # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 + # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' + # => graalpy_38_native + + ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) + if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": + raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") + parts = ext_suffix.split(".") + if len(parts) < 3: + # CPython3.7 and earlier uses ".pyd" on Windows. + return _cpython_abis(sys.version_info[:2]) + soabi = parts[1] + if soabi.startswith("cpython"): + # non-windows + abi = "cp" + soabi.split("-")[1] + elif soabi.startswith("cp"): + # windows + abi = soabi.split("-")[0] + elif soabi.startswith("pypy"): + abi = "-".join(soabi.split("-")[:2]) + elif soabi.startswith("graalpy"): + abi = "-".join(soabi.split("-")[:3]) + elif soabi: + # pyston, ironpython, others? + abi = soabi + else: + return [] + return [_normalize_string(abi)] + + +def generic_tags( + interpreter: Optional[str] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a generic interpreter. + + The tags consist of: + - -- + + The "none" ABI will be added if it was not explicitly provided. + """ + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + else: + abis = list(abis) + platforms = list(platforms or platform_tags()) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + +def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: + """ + Yields Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all previous versions of that major version. + """ + if len(py_version) > 1: + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield f"py{_version_nodot((py_version[0], minor))}" + + +def compatible_tags( + python_version: Optional[PythonVersion] = None, + interpreter: Optional[str] = None, + platforms: Optional[Iterable[str]] = None, +) -> Iterator[Tag]: + """ + Yields the sequence of tags that are compatible with a specific version of Python. + + The tags consist of: + - py*-none- + - -none-any # ... if `interpreter` is provided. + - py*-none-any + """ + if not python_version: + python_version = sys.version_info[:2] + platforms = list(platforms or platform_tags()) + for version in _py_interpreter_range(python_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + if cpu_arch in {"arm64", "x86_64"}: + formats.append("universal2") + + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: + formats.append("universal") + + return formats + + +def mac_platforms( + version: Optional[MacVersion] = None, arch: Optional[str] = None +) -> Iterator[str]: + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + if version == (10, 16): + # When built against an older macOS SDK, Python will report macOS 10.16 + # instead of the real version. + version_str = subprocess.run( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + check=True, + env={"SYSTEM_VERSION_COMPAT": "0"}, + stdout=subprocess.PIPE, + text=True, + ).stdout + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version + if arch is None: + arch = _mac_arch(cpu_arch) + else: + arch = arch + + if (10, 0) <= version and version < (11, 0): + # Prior to Mac OS 11, each yearly release of Mac OS bumped the + # "minor" version number. The major version was always 10. + for minor_version in range(version[1], -1, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=10, minor=minor_version, binary_format=binary_format + ) + + if version >= (11, 0): + # Starting with Mac OS 11, each yearly release bumps the major version + # number. The minor versions are now the midyear updates. + for major_version in range(version[0], 10, -1): + compat_version = major_version, 0 + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=major_version, minor=0, binary_format=binary_format + ) + + if version >= (11, 0): + # Mac OS 11 on x86_64 is compatible with binaries from previous releases. + # Arm64 support was introduced in 11.0, so no Arm binaries from previous + # releases exist. + # + # However, the "universal2" binary format can have a + # macOS version earlier than 11.0 when the x86_64 part of the binary supports + # that version of macOS. + if arch == "x86_64": + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + else: + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_format = "universal2" + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + + +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_armv8l" + _, arch = linux.split("_", 1) + 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]: + yield _normalize_string(sysconfig.get_platform()) + + +def platform_tags() -> Iterator[str]: + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() + + +def interpreter_name() -> str: + """ + Returns the name of the running interpreter. + + Some implementations have a reserved, two-letter abbreviation which will + be returned when appropriate. + """ + name = sys.implementation.name + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def interpreter_version(*, warn: bool = False) -> str: + """ + Returns the version of the running interpreter. + """ + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + return version + + +def _version_nodot(version: PythonVersion) -> str: + return "".join(map(str, version)) + + +def sys_tags(*, warn: bool = False) -> Iterator[Tag]: + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + + interp_name = interpreter_name() + if interp_name == "cp": + yield from cpython_tags(warn=warn) + else: + yield from generic_tags() + + if interp_name == "pp": + interp = "pp3" + elif interp_name == "cp": + interp = "cp" + interpreter_version(warn=warn) + else: + interp = None + yield from compatible_tags(interpreter=interp) diff --git a/distutils/_vendor/packaging/utils.py b/distutils/_vendor/packaging/utils.py new file mode 100644 index 00000000..c2c2f75a --- /dev/null +++ b/distutils/_vendor/packaging/utils.py @@ -0,0 +1,172 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import re +from typing import FrozenSet, NewType, Tuple, Union, cast + +from .tags import Tag, parse_tag +from .version import InvalidVersion, Version + +BuildTag = Union[Tuple[()], Tuple[int, str]] +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. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + + +# 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, *, 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: + """ + This is very similar to Version.__str__, but has one subtle difference + with the way it handles the release segment. + """ + if isinstance(version, str): + try: + parsed = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + else: + parsed = version + + parts = [] + + # Epoch + if parsed.epoch != 0: + parts.append(f"{parsed.epoch}!") + + # Release segment + release_segment = ".".join(str(x) for x in parsed.release) + if strip_trailing_zero: + # NB: This strips trailing '.0's to normalize + release_segment = re.sub(r"(\.0)+$", "", release_segment) + parts.append(release_segment) + + # Pre-release + if parsed.pre is not None: + parts.append("".join(str(x) for x in parsed.pre)) + + # Post-release + if parsed.post is not None: + parts.append(f".post{parsed.post}") + + # Development release + if parsed.dev is not None: + parts.append(f".dev{parsed.dev}") + + # Local version segment + if parsed.local is not None: + parts.append(f"+{parsed.local}") + + return "".join(parts) + + +def parse_wheel_filename( + filename: str, +) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + f"Invalid wheel filename (extension must be '.whl'): {filename}" + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + f"Invalid wheel filename (wrong number of parts): {filename}" + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # 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) + + 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) + if build_match is None: + raise InvalidWheelFilename( + f"Invalid build number: {build_part} in '{filename}'" + ) + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + else: + build = () + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: + if filename.endswith(".tar.gz"): + file_stem = filename[: -len(".tar.gz")] + elif filename.endswith(".zip"): + file_stem = filename[: -len(".zip")] + else: + raise InvalidSdistFilename( + f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" + f" {filename}" + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = file_stem.rpartition("-") + if not sep: + raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") + + name = canonicalize_name(name_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/distutils/_vendor/packaging/version.py b/distutils/_vendor/packaging/version.py new file mode 100644 index 00000000..5faab9bd --- /dev/null +++ b/distutils/_vendor/packaging/version.py @@ -0,0 +1,563 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.version import parse, Version +""" + +import itertools +import re +from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union + +from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType + +__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] + +LocalType = Tuple[Union[int, str], ...] + +CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] +CmpLocalType = Union[ + NegativeInfinityType, + Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], +] +CmpKey = Tuple[ + int, + Tuple[int, ...], + CmpPrePostDevType, + CmpPrePostDevType, + CmpPrePostDevType, + CmpLocalType, +] +VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] + + +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": + """Parse the given version string. + + >>> parse('1.0.dev1') + + + :param version: The version string to parse. + :raises InvalidVersion: When the version string is not a valid version. + """ + return Version(version) + + +class InvalidVersion(ValueError): + """Raised when a version string is not a valid version. + + >>> Version("invalid") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'invalid' + """ + + +class _BaseVersion: + _key: Tuple[Any, ...] + + def __hash__(self) -> int: + return hash(self._key) + + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. + def __lt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key < other._key + + def __le__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key <= other._key + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key == other._key + + def __ge__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key >= other._key + + def __gt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key > other._key + + def __ne__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key != other._key + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +_VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?Palpha|a|beta|b|preview|pre|c|rc)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+VERSION_PATTERN = _VERSION_PATTERN
+"""
+A string containing the regular expression used to match a valid version.
+
+The pattern is not anchored at either end, and is intended for embedding in larger
+expressions (for example, matching a version number as part of a file name). The
+regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
+flags set.
+
+:meta hide-value:
+"""
+
+
+class Version(_BaseVersion):
+    """This class abstracts handling of a project's versions.
+
+    A :class:`Version` instance is comparison aware and can be compared and
+    sorted using the standard Python interfaces.
+
+    >>> v1 = Version("1.0a5")
+    >>> v2 = Version("1.0")
+    >>> v1
+    
+    >>> v2
+    
+    >>> v1 < v2
+    True
+    >>> v1 == v2
+    False
+    >>> v1 > v2
+    False
+    >>> v1 >= v2
+    False
+    >>> v1 <= v2
+    True
+    """
+
+    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+    _key: CmpKey
+
+    def __init__(self, version: str) -> None:
+        """Initialize a Version object.
+
+        :param version:
+            The string representation of a version which will be parsed and normalized
+            before use.
+        :raises InvalidVersion:
+            If the ``version`` does not conform to PEP 440 in any way then this
+            exception will be raised.
+        """
+
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion(f"Invalid version: '{version}'")
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+            post=_parse_letter_version(
+                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+            ),
+            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self) -> str:
+        """A representation of the Version that shows all internal state.
+
+        >>> Version('1.0.0')
+        
+        """
+        return f""
+
+    def __str__(self) -> str:
+        """A string representation of the version that can be rounded-tripped.
+
+        >>> str(Version("1.0a5"))
+        '1.0a5'
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        # Pre-release
+        if self.pre is not None:
+            parts.append("".join(str(x) for x in self.pre))
+
+        # Post-release
+        if self.post is not None:
+            parts.append(f".post{self.post}")
+
+        # Development release
+        if self.dev is not None:
+            parts.append(f".dev{self.dev}")
+
+        # Local version segment
+        if self.local is not None:
+            parts.append(f"+{self.local}")
+
+        return "".join(parts)
+
+    @property
+    def epoch(self) -> int:
+        """The epoch of the version.
+
+        >>> Version("2.0.0").epoch
+        0
+        >>> Version("1!2.0.0").epoch
+        1
+        """
+        return self._version.epoch
+
+    @property
+    def release(self) -> Tuple[int, ...]:
+        """The components of the "release" segment of the version.
+
+        >>> Version("1.2.3").release
+        (1, 2, 3)
+        >>> Version("2.0.0").release
+        (2, 0, 0)
+        >>> Version("1!2.0.0.post0").release
+        (2, 0, 0)
+
+        Includes trailing zeroes but not the epoch or any pre-release / development /
+        post-release suffixes.
+        """
+        return self._version.release
+
+    @property
+    def pre(self) -> Optional[Tuple[str, int]]:
+        """The pre-release segment of the version.
+
+        >>> print(Version("1.2.3").pre)
+        None
+        >>> Version("1.2.3a1").pre
+        ('a', 1)
+        >>> Version("1.2.3b1").pre
+        ('b', 1)
+        >>> Version("1.2.3rc1").pre
+        ('rc', 1)
+        """
+        return self._version.pre
+
+    @property
+    def post(self) -> Optional[int]:
+        """The post-release number of the version.
+
+        >>> print(Version("1.2.3").post)
+        None
+        >>> Version("1.2.3.post1").post
+        1
+        """
+        return self._version.post[1] if self._version.post else None
+
+    @property
+    def dev(self) -> Optional[int]:
+        """The development number of the version.
+
+        >>> print(Version("1.2.3").dev)
+        None
+        >>> Version("1.2.3.dev1").dev
+        1
+        """
+        return self._version.dev[1] if self._version.dev else None
+
+    @property
+    def local(self) -> Optional[str]:
+        """The local version segment of the version.
+
+        >>> print(Version("1.2.3").local)
+        None
+        >>> Version("1.2.3+abc").local
+        'abc'
+        """
+        if self._version.local:
+            return ".".join(str(x) for x in self._version.local)
+        else:
+            return None
+
+    @property
+    def public(self) -> str:
+        """The public portion of the version.
+
+        >>> Version("1.2.3").public
+        '1.2.3'
+        >>> Version("1.2.3+abc").public
+        '1.2.3'
+        >>> Version("1.2.3+abc.dev1").public
+        '1.2.3'
+        """
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self) -> str:
+        """The "base version" of the version.
+
+        >>> Version("1.2.3").base_version
+        '1.2.3'
+        >>> Version("1.2.3+abc").base_version
+        '1.2.3'
+        >>> Version("1!1.2.3+abc.dev1").base_version
+        '1!1.2.3'
+
+        The "base version" is the public version of the project without any pre or post
+        release markers.
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        return "".join(parts)
+
+    @property
+    def is_prerelease(self) -> bool:
+        """Whether this version is a pre-release.
+
+        >>> Version("1.2.3").is_prerelease
+        False
+        >>> Version("1.2.3a1").is_prerelease
+        True
+        >>> Version("1.2.3b1").is_prerelease
+        True
+        >>> Version("1.2.3rc1").is_prerelease
+        True
+        >>> Version("1.2.3dev1").is_prerelease
+        True
+        """
+        return self.dev is not None or self.pre is not None
+
+    @property
+    def is_postrelease(self) -> bool:
+        """Whether this version is a post-release.
+
+        >>> Version("1.2.3").is_postrelease
+        False
+        >>> Version("1.2.3.post1").is_postrelease
+        True
+        """
+        return self.post is not None
+
+    @property
+    def is_devrelease(self) -> bool:
+        """Whether this version is a development release.
+
+        >>> Version("1.2.3").is_devrelease
+        False
+        >>> Version("1.2.3.dev1").is_devrelease
+        True
+        """
+        return self.dev is not None
+
+    @property
+    def major(self) -> int:
+        """The first item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").major
+        1
+        """
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self) -> int:
+        """The second item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").minor
+        2
+        >>> Version("1").minor
+        0
+        """
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self) -> int:
+        """The third item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").micro
+        3
+        >>> Version("1").micro
+        0
+        """
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
+) -> Optional[Tuple[str, int]]:
+
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume if we are given a number, but we are not given a letter
+        # then this is using the implicit post release syntax (e.g. 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+    return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_separators.split(local)
+        )
+    return None
+
+
+def _cmpkey(
+    epoch: int,
+    release: Tuple[int, ...],
+    pre: Optional[Tuple[str, int]],
+    post: Optional[Tuple[str, int]],
+    dev: Optional[Tuple[str, int]],
+    local: Optional[LocalType],
+) -> CmpKey:
+
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    _release = tuple(
+        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # 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: CmpPrePostDevType = NegativeInfinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        _pre = Infinity
+    else:
+        _pre = pre
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        _post: CmpPrePostDevType = NegativeInfinity
+
+    else:
+        _post = post
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        _dev: CmpPrePostDevType = Infinity
+
+    else:
+        _dev = dev
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        _local: CmpLocalType = NegativeInfinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
+
+    return epoch, _release, _pre, _post, _dev, _local
diff --git a/distutils/_vendor/ruff.toml b/distutils/_vendor/ruff.toml
new file mode 100644
index 00000000..00fee625
--- /dev/null
+++ b/distutils/_vendor/ruff.toml
@@ -0,0 +1 @@
+exclude = ["*"]

From 96ce7aa3f26e60c72dcaa4e3b467bc563b100a17 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 11 Apr 2024 19:31:14 -0400
Subject: [PATCH 143/221] Use vendored packaging.

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

diff --git a/distutils/dist.py b/distutils/dist.py
index 4b3824df..f4eb6e89 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -12,7 +12,7 @@
 import logging
 from email import message_from_file
 
-from packaging.utils import canonicalize_name, canonicalize_version
+from ._vendor.packaging.utils import canonicalize_name, canonicalize_version
 
 try:
     import warnings

From 842cc23a1b0af16fa09b8e4b86433531716ffc8d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Fri, 12 Apr 2024 09:03:46 -0400
Subject: [PATCH 144/221] 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 822809de..aa3b65f1 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 145/221] 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 01dd7907..4beb1236 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 675bcebd..bb3bee7e 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 1a4d67f4..68caf5a6 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 8f186b72..00ca61d5 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 1a07746b..8b4f7046 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 2763db9c..ca198873 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 c632b391..53c131e9 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 0aaf0a53..7e42227e 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 146/221] 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 bcf9580c..cdfe9d74 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 4beb1236..5880ad2b 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 bb3bee7e..64af0db0 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 68caf5a6..6a5e6ed0 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 7ba35eef..31ae4350 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 c4d2a45d..bbea1555 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 c025f120..e41b6064 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 0eb9b861..6c8193e9 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 8b4f7046..b8694dd6 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 cb4db4e1..cfe7fa9e 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 16ac5ca7..08c72c1b 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 7e42227e..f89d1b35 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 bfd30700..ce5bc55f 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 147/221] =?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 6a5e6ed0..29d9c278 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 ce5bc55f..a24c9401 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 148/221] =?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 06ce3bc6..3ce34115 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 1a188c35..e374d5c5 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 4a2ae0ac..0148f157 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 17769e91..76ecb96a 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 fbb95a8f..78485dc2 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 4f081c7e..d08910ec 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 7f9e1e00..052f6e46 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 d496d5d4..c1341e43 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 cdfe9d74..03181cfb 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 8849474c..02dbf165 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 397ebf82..00d34bc7 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 237b1465..f681b553 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 5880ad2b..41adf014 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 64af0db0..6a75e32f 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 d8704e35..d18ed503 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 b3f679b6..811e607e 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 a15781f2..aa9ed578 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 e16011d4..a15d0af5 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 29d9c278..37bc5850 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 28f55fb9..6b42a34f 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 9413f7cf..4167a83f 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 573741d7..38a5ff51 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 927c3ed3..575cebdb 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 31ae4350..b63a1af2 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 f3e8f344..4fbb3440 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 be4c2433..b1f346f0 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 20f07aaa..e66b13a1 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 5a24246c..e5e6b379 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 b76cb9bc..6414ef5c 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 a9124f2b..e61a9ea8 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 e0defd77..83f96a9e 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 799de948..309ce696 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 20609504..539f09d8 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 819fe56f..2021bed8 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 bbea1555..1759120c 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 00ca61d5..94e71635 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 e41b6064..cb646c6d 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 6c8193e9..960def9c 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 32057626..5ce47936 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 239f3158..8abb09cf 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 402c0c06..6a0105e4 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 b8694dd6..ac8b68c0 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 48adceb1..046b5bbb 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 40215b83..1a38e9fa 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 6d9b8532..c475e5d0 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 e5d406a3..76d3551c 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 ddf7bf1d..9cd2b8a9 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 2b5eafd2..145cce91 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 cfe7fa9e..78928fea 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 e6804088..769623cb 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 8617fa99..25483ad7 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 f8554542..9c69b3e7 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 ae66bc4e..ca5d9d57 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 6730878e..8bc0e98a 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 7e05ec5f..208b1f6e 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 b6512e6d..d23b907c 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 8215300b..580cb2a2 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 e2459aa0..9b11fa40 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 684662d3..f366aa65 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 11c23d83..be5ae0a6 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 90c8f906..fc0a7885 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 95aa2998..59167180 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 fc67d75f..0a66193d 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 6fc9ed08..84cda619 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 8e52873d..9ed4d16d 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 297ae44b..77bb147b 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 6c701914..4c2abd24 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 bf1a9d9b..6a379a63 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 08c72c1b..08f0f839 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 198c10da..e453d01f 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 8b86b6ea..2c74f06b 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 0efe39fe..964106fa 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 4da2acb6..5d9f13a4 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 ec6a0c80..d67779fc 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 5fde7a59..2bd82346 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 dfb34122..58e24f01 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 f65a5a25..23b6c732 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 591c5ce0..d071bbe9 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 450f68c9..66a41947 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 ec4c9982..abbac4c2 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 131c1344..ce13d6bd 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 fe787f44..c5c910a8 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 ca198873..f17edf2f 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 5c5bc59a..0692f001 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 53c131e9..78d8b1e3 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 f89d1b35..ddf1789b 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 95fc8eeb..a5d9ee45 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 d749fe25..a1fe2b57 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 a24c9401..9ee77721 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 8ab76dde..aa7c5385 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 c75e4948..31c42016 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 6d70b7f0..c7a7ca61 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 149/221] =?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 d08910ec..a2159fef 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 03181cfb..8876d730 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 f681b553..ade98445 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 41adf014..06502d20 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 6a75e32f..649968a5 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 aa9ed578..82e1e020 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 37bc5850..5f3902a0 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 575cebdb..85165717 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 6414ef5c..97bae827 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 cb646c6d..dccc5492 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 5ce47936..71ffb2ab 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 ddf1789b..1508e1cc 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 6f90cfe2..0f846e3c 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 150/221] 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 9b11fa40..bdbcd4fa 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 aa7c5385..6e26e030 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 151/221] 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 6e26e030..90adbc71 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 152/221] 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 90adbc71..30546a9d 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 153/221] 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 30546a9d..806d233c 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 154/221] 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 5ad21cc7..6810a5e2 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 155/221] 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 811e607e..360575d0 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 a15d0af5..56e6fa2e 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 85165717..8e920be4 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 97bae827..387d27c9 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 1759120c..c32ffb6c 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 156/221] =?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 6810a5e2..d11a8346 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 38a5ff51..d4b2b0a3 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 c32ffb6c..f29a34fa 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 dccc5492..e905aede 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 c475e5d0..20dfe8f1 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 157/221] 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 6b42a34f..28599e10 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 158/221] 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 e5e6b379..ee6c54da 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 e61a9ea8..cf541f8a 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 159/221] 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 59167180..bad3fb7e 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 160/221] 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 82e1e020..06d949af 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 aa0c0a7f..00000000
--- 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 161/221] 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 76d3551c..00000000
--- 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 ce13d6bd..bc14d3c0 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 162/221] 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 bc14d3c0..c5589666 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 b81e1a5c23e6f94631d5ee1c5accf8ab0f0d4a23 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sun, 10 Mar 2024 14:51:21 +0100
Subject: [PATCH 163/221] Remove extra pairs of quotes from litteral strings

Automated formatting runs such as #149 / ab77f7d left over these extra quotes.
---
 distutils/tests/test_cygwinccompiler.py | 10 +++++-----
 distutils/tests/test_sysconfig.py       |  4 ++--
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py
index 0a66193d..d95654f5 100644
--- a/distutils/tests/test_cygwinccompiler.py
+++ b/distutils/tests/test_cygwinccompiler.py
@@ -80,25 +80,25 @@ def test_get_msvcr(self):
 
         # MSVC 7.0
         sys.version = (
-            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1300 32 bits (Intel)]'
+            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1300 32 bits (Intel)]'
         )
         assert get_msvcr() == ['msvcr70']
 
         # MSVC 7.1
         sys.version = (
-            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1310 32 bits (Intel)]'
+            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bits (Intel)]'
         )
         assert get_msvcr() == ['msvcr71']
 
         # VS2005 / MSVC 8.0
         sys.version = (
-            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1400 32 bits (Intel)]'
+            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1400 32 bits (Intel)]'
         )
         assert get_msvcr() == ['msvcr80']
 
         # VS2008 / MSVC 9.0
         sys.version = (
-            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.1500 32 bits (Intel)]'
+            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1500 32 bits (Intel)]'
         )
         assert get_msvcr() == ['msvcr90']
 
@@ -110,7 +110,7 @@ def test_get_msvcr(self):
 
         # unknown
         sys.version = (
-            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' '[MSC v.2000 32 bits (Intel)]'
+            '2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.2000 32 bits (Intel)]'
         )
         with pytest.raises(ValueError):
             get_msvcr()
diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py
index c5589666..faa8e31c 100644
--- a/distutils/tests/test_sysconfig.py
+++ b/distutils/tests/test_sysconfig.py
@@ -132,12 +132,12 @@ def test_customize_compiler(self):
         assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags'
         assert comp.exes['compiler'] == 'env_cc --sc-cflags --env-cflags --env-cppflags'
         assert comp.exes['compiler_so'] == (
-            'env_cc --sc-cflags ' '--env-cflags ' '--env-cppflags --sc-ccshared'
+            'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared'
         )
         assert comp.exes['compiler_cxx'] == 'env_cxx --env-cxx-flags'
         assert comp.exes['linker_exe'] == 'env_cc'
         assert comp.exes['linker_so'] == (
-            'env_ldshared --env-ldflags --env-cflags' ' --env-cppflags'
+            'env_ldshared --env-ldflags --env-cflags --env-cppflags'
         )
         assert comp.shared_lib_extension == 'sc_shutil_suffix'
 

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 164/221] 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 f29a34fa..668ce7eb 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 165/221] 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 58e24f01..6f6aabee 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 166/221] 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 2021bed8..8a3aca65 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 167/221] 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 a1fe2b57..caf4cd33 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 b26678d5cc10ae3e97a1025a6298d75cd4ec9ab6 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:23:53 -0400
Subject: [PATCH 168/221] Revert the canonicalization of the version. Ref
 pypa/setuptools#3593.

---
 distutils/dist.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/distutils/dist.py b/distutils/dist.py
index f4eb6e89..fb996e4d 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -12,7 +12,7 @@
 import logging
 from email import message_from_file
 
-from ._vendor.packaging.utils import canonicalize_name, canonicalize_version
+from ._vendor.packaging.utils import canonicalize_name
 
 try:
     import warnings
@@ -1193,7 +1193,7 @@ def get_version(self):
     def get_fullname(self):
         return "{}-{}".format(
             canonicalize_name(self.get_name()).replace('-', '_'),
-            canonicalize_version(self.get_version()),
+            self.get_version(),
         )
 
     def get_author(self):

From c28f347e93b82fb2e81cb40f284bead6ac3ee095 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:26:43 -0400
Subject: [PATCH 169/221] Revert "Update tests to match new expectation."

This reverts commit be32fecc787c3de8c292638c5004a8bcf92dc540.
---
 distutils/tests/test_sdist.py | 32 ++++++++++++++++----------------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py
index 359325d5..9856193f 100644
--- a/distutils/tests/test_sdist.py
+++ b/distutils/tests/test_sdist.py
@@ -118,9 +118,9 @@ def test_prune_file_list(self):
         # now let's check what we have
         dist_folder = join(self.tmp_dir, 'dist')
         files = os.listdir(dist_folder)
-        assert files == ['ns_fake_pkg-1.zip']
+        assert files == ['ns_fake_pkg-1.0.zip']
 
-        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.zip'))
+        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
         try:
             content = zip_file.namelist()
         finally:
@@ -135,7 +135,7 @@ def test_prune_file_list(self):
             'somecode/',
             'somecode/__init__.py',
         ]
-        assert sorted(content) == ['ns_fake_pkg-1/' + x for x in expected]
+        assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
 
     @pytest.mark.usefixtures('needs_zlib')
     @pytest.mark.skipif("not find_executable('tar')")
@@ -153,10 +153,10 @@ def test_make_distribution(self):
         dist_folder = join(self.tmp_dir, 'dist')
         result = os.listdir(dist_folder)
         result.sort()
-        assert result == ['ns_fake_pkg-1.tar', 'ns_fake_pkg-1.tar.gz']
+        assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
 
-        os.remove(join(dist_folder, 'ns_fake_pkg-1.tar'))
-        os.remove(join(dist_folder, 'ns_fake_pkg-1.tar.gz'))
+        os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar'))
+        os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar.gz'))
 
         # now trying a tar then a gztar
         cmd.formats = ['tar', 'gztar']
@@ -166,7 +166,7 @@ def test_make_distribution(self):
 
         result = os.listdir(dist_folder)
         result.sort()
-        assert result == ['ns_fake_pkg-1.tar', 'ns_fake_pkg-1.tar.gz']
+        assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
 
     @pytest.mark.usefixtures('needs_zlib')
     def test_add_defaults(self):
@@ -219,9 +219,9 @@ def test_add_defaults(self):
         # now let's check what we have
         dist_folder = join(self.tmp_dir, 'dist')
         files = os.listdir(dist_folder)
-        assert files == ['ns_fake_pkg-1.zip']
+        assert files == ['ns_fake_pkg-1.0.zip']
 
-        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.zip'))
+        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
         try:
             content = zip_file.namelist()
         finally:
@@ -247,7 +247,7 @@ def test_add_defaults(self):
             'somecode/doc.dat',
             'somecode/doc.txt',
         ]
-        assert sorted(content) == ['ns_fake_pkg-1/' + x for x in expected]
+        assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
 
         # checking the MANIFEST
         manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8')
@@ -420,16 +420,16 @@ def test_manual_manifest(self):
 
         assert list(clean_lines(cmd.manifest)) == ['README.manual']
 
-        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz')
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
         archive = tarfile.open(archive_name)
         try:
             filenames = [tarinfo.name for tarinfo in archive]
         finally:
             archive.close()
         assert sorted(filenames) == [
-            'ns_fake_pkg-1',
-            'ns_fake_pkg-1/PKG-INFO',
-            'ns_fake_pkg-1/README.manual',
+            'ns_fake_pkg-1.0',
+            'ns_fake_pkg-1.0/PKG-INFO',
+            'ns_fake_pkg-1.0/README.manual',
         ]
 
     @pytest.mark.usefixtures('needs_zlib')
@@ -449,7 +449,7 @@ def test_make_distribution_owner_group(self):
         cmd.run()
 
         # making sure we have the good rights
-        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz')
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
         archive = tarfile.open(archive_name)
         try:
             for member in archive.getmembers():
@@ -467,7 +467,7 @@ def test_make_distribution_owner_group(self):
         cmd.run()
 
         # making sure we have the good rights
-        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.tar.gz')
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
         archive = tarfile.open(archive_name)
 
         # note that we are not testing the group ownership here

From 8b9f35e00549615b43793efd3c90f75739b55abf Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 09:31:27 -0400
Subject: [PATCH 170/221] 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 8a3aca65..370c6ffd 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 171/221] 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 caf4cd33..a54481c0 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 172/221] 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 00000000..b7be7267
--- /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 00000000..0af38140
--- /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 f17edf2f..6f05fa69 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 a54481c0..0248bde8 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 173/221] Exclude compat package from coverage.

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

diff --git a/.coveragerc b/.coveragerc
index 35b98b1d..bcef31d9 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 174/221] 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 b7be7267..b1ee3fe8 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 7a6f42b3e6ed87032129aa1419c4286ca74b5d3f Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 20:37:49 -0400
Subject: [PATCH 175/221] =?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/unixccompiler.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index 0248bde8..da97688c 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -22,11 +22,11 @@
 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
 from .ccompiler import CCompiler, gen_lib_options, gen_preprocess_options
+from .compat import consolidate_linker_args
 from .errors import CompileError, DistutilsExecError, LibError, LinkError
 
 # XXX Things not currently handled:

From a996148852f113b0a78cb222b8a2cd4dea54594b Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 20:41:52 -0400
Subject: [PATCH 176/221] Move compatibility modules into compat package.

---
 distutils/_modified.py                      |  2 +-
 distutils/compat/py38.py                    | 10 ++++++++++
 distutils/{py39compat.py => compat/py39.py} |  0
 distutils/py38compat.py                     |  8 --------
 distutils/sysconfig.py                      |  4 ++--
 distutils/util.py                           |  2 +-
 6 files changed, 14 insertions(+), 12 deletions(-)
 rename distutils/{py39compat.py => compat/py39.py} (100%)
 delete mode 100644 distutils/py38compat.py

diff --git a/distutils/_modified.py b/distutils/_modified.py
index 78485dc2..9b375181 100644
--- a/distutils/_modified.py
+++ b/distutils/_modified.py
@@ -5,7 +5,7 @@
 
 from ._functools import splat
 from .errors import DistutilsFileError
-from .py39compat import zip_strict
+from .compat.py39 import zip_strict
 
 
 def _newer(source, target):
diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py
index 0af38140..79afc3b2 100644
--- a/distutils/compat/py38.py
+++ b/distutils/compat/py38.py
@@ -21,3 +21,13 @@ def removesuffix(self, suffix):
 
     def removeprefix(self, prefix):
         return self.removeprefix(prefix)
+
+
+def aix_platform(osname, version, release):
+    try:
+        import _aix_support
+
+        return _aix_support.aix_platform()
+    except ImportError:
+        pass
+    return f"{osname}-{version}.{release}"
diff --git a/distutils/py39compat.py b/distutils/compat/py39.py
similarity index 100%
rename from distutils/py39compat.py
rename to distutils/compat/py39.py
diff --git a/distutils/py38compat.py b/distutils/py38compat.py
deleted file mode 100644
index ab12119f..00000000
--- a/distutils/py38compat.py
+++ /dev/null
@@ -1,8 +0,0 @@
-def aix_platform(osname, version, release):
-    try:
-        import _aix_support
-
-        return _aix_support.aix_platform()
-    except ImportError:
-        pass
-    return f"{osname}-{version}.{release}"
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 1a38e9fa..514e06e3 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -16,7 +16,7 @@
 import sys
 import sysconfig
 
-from . import py39compat
+from .compat import py39
 from ._functools import pass_none
 from .errors import DistutilsPlatformError
 
@@ -538,7 +538,7 @@ def get_config_vars(*args):
     global _config_vars
     if _config_vars is None:
         _config_vars = sysconfig.get_config_vars().copy()
-        py39compat.add_ext_suffix(_config_vars)
+        py39.add_ext_suffix(_config_vars)
 
     return [_config_vars.get(name) for name in args] if args else _config_vars
 
diff --git a/distutils/util.py b/distutils/util.py
index 9ee77721..2cdea143 100644
--- a/distutils/util.py
+++ b/distutils/util.py
@@ -34,7 +34,7 @@ def get_host_platform():
         if os.name == "posix" and hasattr(os, 'uname'):
             osname, host, release, version, machine = os.uname()
             if osname[:3] == "aix":
-                from .py38compat import aix_platform
+                from .compat.py38 import aix_platform
 
                 return aix_platform(osname, version, release)
 

From b164d6637dde7a25d99196e3687d949c674c0413 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 20:45:42 -0400
Subject: [PATCH 177/221] Move compatibility module into compat package.

---
 conftest.py                                       | 2 +-
 distutils/tests/compat/__init__.py                | 0
 distutils/tests/{py38compat.py => compat/py38.py} | 0
 distutils/tests/test_archive_util.py              | 2 +-
 distutils/tests/test_bdist_rpm.py                 | 2 +-
 distutils/tests/test_build_ext.py                 | 2 +-
 distutils/tests/test_extension.py                 | 2 +-
 distutils/tests/test_filelist.py                  | 2 +-
 distutils/tests/test_sdist.py                     | 2 +-
 distutils/tests/test_spawn.py                     | 2 +-
 distutils/tests/test_unixccompiler.py             | 2 +-
 11 files changed, 9 insertions(+), 9 deletions(-)
 create mode 100644 distutils/tests/compat/__init__.py
 rename distutils/tests/{py38compat.py => compat/py38.py} (100%)

diff --git a/conftest.py b/conftest.py
index 3ce34115..4a3bbd34 100644
--- a/conftest.py
+++ b/conftest.py
@@ -56,7 +56,7 @@ def _save_cwd():
 
 @pytest.fixture
 def distutils_managed_tempdir(request):
-    from distutils.tests import py38compat as os_helper
+    from distutils.tests.compat import py38 as os_helper
 
     self = request.instance
     self.tempdirs = []
diff --git a/distutils/tests/compat/__init__.py b/distutils/tests/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/distutils/tests/py38compat.py b/distutils/tests/compat/py38.py
similarity index 100%
rename from distutils/tests/py38compat.py
rename to distutils/tests/compat/py38.py
diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py
index 145cce91..02af2aa0 100644
--- a/distutils/tests/test_archive_util.py
+++ b/distutils/tests/test_archive_util.py
@@ -23,7 +23,7 @@
 import path
 import pytest
 
-from .py38compat import check_warnings
+from .compat.py38 import check_warnings
 from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id
 
 
diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py
index 769623cb..a5cb42c3 100644
--- a/distutils/tests/test_bdist_rpm.py
+++ b/distutils/tests/test_bdist_rpm.py
@@ -9,7 +9,7 @@
 
 import pytest
 
-from .py38compat import requires_zlib
+from .compat.py38 import requires_zlib
 
 SETUP_PY = """\
 from distutils.core import setup
diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py
index ca5d9d57..cc83e7fb 100644
--- a/distutils/tests/test_build_ext.py
+++ b/distutils/tests/test_build_ext.py
@@ -31,7 +31,7 @@
 import path
 import pytest
 
-from . import py38compat as import_helper
+from .compat import py38 as import_helper
 
 
 @pytest.fixture()
diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py
index 77bb147b..527a1355 100644
--- a/distutils/tests/test_extension.py
+++ b/distutils/tests/test_extension.py
@@ -6,7 +6,7 @@
 
 import pytest
 
-from .py38compat import check_warnings
+from .compat.py38 import check_warnings
 
 
 class TestExtension:
diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py
index 6a379a63..ec7e5cf3 100644
--- a/distutils/tests/test_filelist.py
+++ b/distutils/tests/test_filelist.py
@@ -10,7 +10,7 @@
 import jaraco.path
 import pytest
 
-from . import py38compat as os_helper
+from .compat import py38 as os_helper
 
 MANIFEST_IN = """\
 include ok
diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py
index 66a41947..a85997f1 100644
--- a/distutils/tests/test_sdist.py
+++ b/distutils/tests/test_sdist.py
@@ -20,7 +20,7 @@
 import pytest
 from more_itertools import ilen
 
-from .py38compat import check_warnings
+from .compat.py38 import check_warnings
 from .unix_compat import grp, pwd, require_uid_0, require_unix_id
 
 SETUP_PY = """
diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py
index abbac4c2..7ec58626 100644
--- a/distutils/tests/test_spawn.py
+++ b/distutils/tests/test_spawn.py
@@ -12,7 +12,7 @@
 import path
 import pytest
 
-from . import py38compat as os_helper
+from .compat import py38 as os_helper
 
 
 class TestSpawn(support.TempdirManager):
diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index 6f05fa69..543aa20d 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -12,7 +12,7 @@
 import pytest
 
 from . import support
-from .py38compat import EnvironmentVarGuard
+from .compat.py38 import EnvironmentVarGuard
 
 
 @pytest.fixture(autouse=True)

From 89522f9748ea637d119b6361253a2021cdd38553 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 21:24:11 -0400
Subject: [PATCH 178/221] Fix return type to match implementation.

---
 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 b1ee3fe8..4a7321fe 100644
--- a/distutils/compat/__init__.py
+++ b/distutils/compat/__init__.py
@@ -3,7 +3,7 @@
 from .py38 import removeprefix
 
 
-def consolidate_linker_args(args: list[str]) -> str:
+def consolidate_linker_args(args: list[str]) -> list[str] | str:
     """
     Ensure the return value is a string for backward compatibility.
 

From c6b23d017b520b1c02863e7b331c3be8b42c74a5 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 21:31:40 -0400
Subject: [PATCH 179/221] =?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

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

diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py
index 79afc3b2..2d442111 100644
--- a/distutils/compat/py38.py
+++ b/distutils/compat/py38.py
@@ -25,7 +25,7 @@ def removeprefix(self, prefix):
 
 def aix_platform(osname, version, release):
     try:
-        import _aix_support
+        import _aix_support  # type: ignore
 
         return _aix_support.aix_platform()
     except ImportError:

From e5268b516ca3c04f6b4840d094623d494c54a235 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 13 Apr 2024 21:59:38 -0400
Subject: [PATCH 180/221] Oops. Meant 2025.

---
 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 4a7321fe..e12534a3 100644
--- a/distutils/compat/__init__.py
+++ b/distutils/compat/__init__.py
@@ -7,7 +7,7 @@ def consolidate_linker_args(args: list[str]) -> list[str] | str:
     """
     Ensure the return value is a string for backward compatibility.
 
-    Retain until at least 2024-04-31. See pypa/distutils#246
+    Retain until at least 2025-04-31. See pypa/distutils#246
     """
 
     if not all(arg.startswith('-Wl,') for arg in args):

From 7ad4f2fa9fb2b030d3ecc231fc24de181705622d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Tue, 16 Apr 2024 10:31:45 -0400
Subject: [PATCH 181/221] Pin against pytest 8.1.x due to
 pytest-dev/pytest#12194.

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index f46b6cbf..05ac4c76 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,7 +20,7 @@ install_requires =
 [options.extras_require]
 testing =
 	# upstream
-	pytest >= 6, != 8.1.1
+	pytest >= 6, != 8.1.*
 	pytest-checkdocs >= 2.4
 	pytest-cov
 	pytest-mypy

From f4529af6a66e34d423860566be7882d665e10569 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= 
Date: Tue, 16 Apr 2024 22:37:50 +0200
Subject: [PATCH 182/221] Move project metadata to `pyproject.toml`
 (jaraco/skeleton#122)

Intentionally omitted specifying `tool.setuptools.include-package-data`: it's true by default in `pyproject.toml` according to https://setuptools.pypa.io/en/latest/userguide/datafiles.html#include-package-data.

Closes jaraco/skeleton#121
---
 pyproject.toml | 49 ++++++++++++++++++++++++++++++++++++++++++++++++-
 setup.cfg      | 42 ------------------------------------------
 2 files changed, 48 insertions(+), 43 deletions(-)
 delete mode 100644 setup.cfg

diff --git a/pyproject.toml b/pyproject.toml
index a853c578..869fe7e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,52 @@
 [build-system]
-requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"]
+requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"]
 build-backend = "setuptools.build_meta"
 
+[project]
+name = "PROJECT"
+authors = [
+	{ name = "Jason R. Coombs", email = "jaraco@jaraco.com" },
+]
+description = "PROJECT_DESCRIPTION"
+readme = "README.rst"
+classifiers = [
+	"Development Status :: 5 - Production/Stable",
+	"Intended Audience :: Developers",
+	"License :: OSI Approved :: MIT License",
+	"Programming Language :: Python :: 3",
+	"Programming Language :: Python :: 3 :: Only",
+]
+requires-python = ">=3.8"
+dependencies = [
+]
+dynamic = ["version"]
+
+[project.optional-dependencies]
+testing = [
+	# upstream
+	"pytest >= 6, != 8.1.*",
+	"pytest-checkdocs >= 2.4",
+	"pytest-cov",
+	"pytest-mypy",
+	"pytest-enabler >= 2.2",
+	"pytest-ruff >= 0.2.1",
+
+	# local
+]
+docs = [
+	# upstream
+	"sphinx >= 3.5",
+	"jaraco.packaging >= 9.3",
+	"rst.linker >= 1.9",
+	"furo",
+	"sphinx-lint",
+
+	# local
+]
+
+[project.urls]
+Homepage = "https://github.com/PROJECT_PATH"
+
+[project.scripts]
+
 [tool.setuptools_scm]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 05ac4c76..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,42 +0,0 @@
-[metadata]
-name = PROJECT
-author = Jason R. Coombs
-author_email = jaraco@jaraco.com
-description = PROJECT_DESCRIPTION
-long_description = file:README.rst
-url = https://github.com/PROJECT_PATH
-classifiers =
-	Development Status :: 5 - Production/Stable
-	Intended Audience :: Developers
-	License :: OSI Approved :: MIT License
-	Programming Language :: Python :: 3
-	Programming Language :: Python :: 3 :: Only
-
-[options]
-include_package_data = true
-python_requires = >=3.8
-install_requires =
-
-[options.extras_require]
-testing =
-	# upstream
-	pytest >= 6, != 8.1.*
-	pytest-checkdocs >= 2.4
-	pytest-cov
-	pytest-mypy
-	pytest-enabler >= 2.2
-	pytest-ruff >= 0.2.1
-
-	# local
-
-docs =
-	# upstream
-	sphinx >= 3.5
-	jaraco.packaging >= 9.3
-	rst.linker >= 1.9
-	furo
-	sphinx-lint
-
-	# local
-
-[options.entry_points]

From 6c1cb088509d55f27c8e8e7777cba5fac24b2be7 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 18 Apr 2024 16:38:01 -0400
Subject: [PATCH 183/221] Migrated config to pyproject.toml using
 jaraco.develop.migrate-config and ini2toml.

---
 pyproject.toml | 54 +++++++++++++++++++++++++++++++++++++++++++++++++-
 setup.cfg      | 50 ----------------------------------------------
 2 files changed, 53 insertions(+), 51 deletions(-)
 delete mode 100644 setup.cfg

diff --git a/pyproject.toml b/pyproject.toml
index 1faf0ec2..738546e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,59 @@
 [build-system]
-requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"]
+requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"]
 build-backend = "setuptools.build_meta"
 
+[project]
+name = "distutils"
+authors = [
+	{ name = "Jason R. Coombs", email = "jaraco@jaraco.com" },
+]
+description = "Distribution utilities formerly from standard library"
+readme = "README.rst"
+classifiers = [
+	"Development Status :: 5 - Production/Stable",
+	"Intended Audience :: Developers",
+	"License :: OSI Approved :: MIT License",
+	"Programming Language :: Python :: 3",
+	"Programming Language :: Python :: 3 :: Only",
+]
+requires-python = ">=3.8"
+dependencies = []
+dynamic = ["version"]
+
+[project.urls]
+Homepage = "https://github.com/pypa/distutils"
+
+[project.optional-dependencies]
+testing = [
+	# upstream
+	"pytest >= 6, != 8.1.1",
+	"pytest-checkdocs >= 2.4",
+	"pytest-cov",
+	"pytest-mypy",
+	"pytest-enabler >= 2.2",
+	"pytest-ruff >= 0.2.1",
+
+	# local
+	"pytest >= 7.4.3", # 186
+	"jaraco.envs>=2.4",
+	"jaraco.path",
+	"jaraco.text",
+	"path >= 10.6",
+	"docutils",
+	"pyfakefs",
+	"more_itertools",
+]
+docs = [
+	# upstream
+	"sphinx >= 3.5",
+	"jaraco.packaging >= 9.3",
+	"rst.linker >= 1.9",
+	"furo",
+	"sphinx-lint",
+
+	# local
+]
+
 [tool.setuptools_scm]
 
 [tool.pytest-enabler.mypy]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index fc6d67ea..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,50 +0,0 @@
-[metadata]
-name = distutils
-author = Jason R. Coombs
-author_email = jaraco@jaraco.com
-description = Distribution utilities formerly from standard library
-long_description = file:README.rst
-url = https://github.com/pypa/distutils
-classifiers =
-	Development Status :: 5 - Production/Stable
-	Intended Audience :: Developers
-	License :: OSI Approved :: MIT License
-	Programming Language :: Python :: 3
-	Programming Language :: Python :: 3 :: Only
-
-[options]
-include_package_data = true
-python_requires = >=3.8
-install_requires =
-
-[options.extras_require]
-testing =
-	# upstream
-	pytest >= 6, != 8.1.1
-	pytest-checkdocs >= 2.4
-	pytest-cov
-	pytest-mypy
-	pytest-enabler >= 2.2
-	pytest-ruff >= 0.2.1
-
-	# local
-	pytest >= 7.4.3  #186
-	jaraco.envs>=2.4
-	jaraco.path
-	jaraco.text
-	path >= 10.6
-	docutils
-	pyfakefs
-	more_itertools
-
-docs =
-	# upstream
-	sphinx >= 3.5
-	jaraco.packaging >= 9.3
-	rst.linker >= 1.9
-	furo
-	sphinx-lint
-
-	# local
-
-[options.entry_points]

From 7dcde5e3cc2210f30b88fcb203c842c41027681b Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 15:44:11 -0400
Subject: [PATCH 184/221] Extract _make_executable for TestSpawn.

---
 distutils/tests/test_spawn.py | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py
index 7ec58626..1f623837 100644
--- a/distutils/tests/test_spawn.py
+++ b/distutils/tests/test_spawn.py
@@ -45,14 +45,9 @@ def test_spawn(self):
         spawn([exe])  # should work without any error
 
     def test_find_executable(self, tmp_path):
-        program_noeext = 'program'
-        # Give the temporary program an ".exe" suffix for all.
-        # It's needed on Windows and not harmful on other platforms.
-        program = program_noeext + ".exe"
-
-        program_path = tmp_path / program
-        program_path.write_text("", encoding='utf-8')
-        program_path.chmod(stat.S_IXUSR)
+        program_path = self._make_executable(tmp_path, '.exe')
+        program = program_path.name
+        program_noeext = program_path.with_suffix('').name
         filename = str(program_path)
         tmp_dir = path.Path(tmp_path)
 
@@ -121,6 +116,15 @@ def test_find_executable(self, tmp_path):
                 rv = find_executable(program)
                 assert rv == filename
 
+    @staticmethod
+    def _make_executable(tmp_path, ext):
+        # Give the temporary program a suffix regardless of platform.
+        # It's needed on Windows and not harmful on others.
+        program = tmp_path.joinpath('program').with_suffix(ext)
+        program.write_text("", encoding='utf-8')
+        program.chmod(stat.S_IXUSR)
+        return program
+
     def test_spawn_missing_exe(self):
         with pytest.raises(DistutilsExecError) as ctx:
             spawn(['does-not-exist'])

From 041c42e7e4b12350dadfe5ac680c9637d1c56858 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 18:10:02 -0400
Subject: [PATCH 185/221] Move and reword comment for brevity and clarity.

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

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 046b5bbb..76a2dc3f 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -83,14 +83,13 @@ def find_executable(executable, path=None):
 
     if path is None:
         path = os.environ.get('PATH', None)
+        # bpo-35755: Don't fall through if PATH is the empty string
         if path is None:
             try:
                 path = os.confstr("CS_PATH")
             except (AttributeError, ValueError):
                 # os.confstr() or CS_PATH is not available
                 path = os.defpath
-        # bpo-35755: Don't use os.defpath if the PATH environment variable is
-        # set to an empty string
 
     # PATH='' doesn't match, whereas PATH=':' looks in the current directory
     if not path:

From b07b4edd742e47e8a721c60fdaaf9f0d4189fd09 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 21:52:29 -0400
Subject: [PATCH 186/221] Remove C901 exclusion; code is now compliant.

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

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 76a2dc3f..3927c1fe 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -15,7 +15,7 @@
 from .errors import DistutilsExecError
 
 
-def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):  # noqa: C901
+def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):
     """Run another program, specified as a command list 'cmd', in a new process.
 
     'cmd' is just the argument list for the new process, ie.

From ef8f235a98c06ca164d64b5debde30fdab4bedf2 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 21:54:38 -0400
Subject: [PATCH 187/221] Remove apparently unnecessary cast to list.

---
 distutils/spawn.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 3927c1fe..a321c5f0 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -31,10 +31,6 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):
     Raise DistutilsExecError if running the program fails in any way; just
     return on success.
     """
-    # cmd is documented as a list, but just in case some code passes a tuple
-    # in, protect our %-formatting code against horrible death
-    cmd = list(cmd)
-
     log.info(subprocess.list2cmdline(cmd))
     if dry_run:
         return

From 03f1d85b720a86a23908c91c690cecbb754960a7 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 21:56:20 -0400
Subject: [PATCH 188/221] Use proper boolean literals.

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

diff --git a/distutils/spawn.py b/distutils/spawn.py
index a321c5f0..a7e21d2e 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -15,7 +15,7 @@
 from .errors import DistutilsExecError
 
 
-def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):
+def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
     """Run another program, specified as a command list 'cmd', in a new process.
 
     'cmd' is just the argument list for the new process, ie.

From e85efeebdd24bb05ab2e6fed687a06d349775eaa Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 22:00:36 -0400
Subject: [PATCH 189/221] Replace Popen with check_call.

---
 distutils/spawn.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/distutils/spawn.py b/distutils/spawn.py
index a7e21d2e..0d86552e 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -50,18 +50,17 @@ def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
             env[MACOSX_VERSION_VAR] = macosx_target_ver
 
     try:
-        proc = subprocess.Popen(cmd, env=env)
-        proc.wait()
-        exitcode = proc.returncode
+        subprocess.check_call(cmd, env=env)
     except OSError as exc:
         if not DEBUG:
             cmd = cmd[0]
         raise DistutilsExecError(f"command {cmd!r} failed: {exc.args[-1]}") from exc
-
-    if exitcode:
+    except subprocess.CalledProcessError as err:
         if not DEBUG:
             cmd = cmd[0]
-        raise DistutilsExecError(f"command {cmd!r} failed with exit code {exitcode}")
+        raise DistutilsExecError(
+            f"command {cmd!r} failed with exit code {err.returncode}"
+        ) from err
 
 
 def find_executable(executable, path=None):

From 976e935fa29c7a8c23298c28537f9d2ce5f399b9 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 22:03:14 -0400
Subject: [PATCH 190/221] Extract function for _debug wrapper.

---
 distutils/spawn.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 0d86552e..76050655 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -15,6 +15,13 @@
 from .errors import DistutilsExecError
 
 
+def _debug(cmd):
+    """
+    Render a subprocess command differently depending on DEBUG.
+    """
+    return cmd if DEBUG else cmd[0]
+
+
 def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
     """Run another program, specified as a command list 'cmd', in a new process.
 
@@ -52,14 +59,12 @@ def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
     try:
         subprocess.check_call(cmd, env=env)
     except OSError as exc:
-        if not DEBUG:
-            cmd = cmd[0]
-        raise DistutilsExecError(f"command {cmd!r} failed: {exc.args[-1]}") from exc
+        raise DistutilsExecError(
+            f"command {_debug(cmd)!r} failed: {exc.args[-1]}"
+        ) from exc
     except subprocess.CalledProcessError as err:
-        if not DEBUG:
-            cmd = cmd[0]
         raise DistutilsExecError(
-            f"command {cmd!r} failed with exit code {err.returncode}"
+            f"command {_debug(cmd)!r} failed with exit code {err.returncode}"
         ) from err
 
 

From d6652a4b1b15e27c27a38d4d989f3505541a156d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 22:23:19 -0400
Subject: [PATCH 191/221] Extract function to inject macos version.

---
 distutils/spawn.py | 31 +++++++++++++++++++++----------
 1 file changed, 21 insertions(+), 10 deletions(-)

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 76050655..081e2549 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -6,10 +6,15 @@
 executable name.
 """
 
+from __future__ import annotations
+
 import os
+import platform
 import subprocess
 import sys
 
+from typing import Mapping
+
 from ._log import log
 from .debug import DEBUG
 from .errors import DistutilsExecError
@@ -22,6 +27,21 @@ def _debug(cmd):
     return cmd if DEBUG else cmd[0]
 
 
+def _inject_macos_ver(env: Mapping[str:str] | None) -> Mapping[str:str] | None:
+    if platform.system() != 'Darwin':
+        return env
+
+    from distutils.util import MACOSX_VERSION_VAR, get_macosx_target_ver
+
+    target_ver = get_macosx_target_ver()
+    update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {}
+    return {**_resolve(env), **update}
+
+
+def _resolve(env: Mapping[str:str] | None) -> Mapping[str:str]:
+    return os.environ if env is None else env
+
+
 def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
     """Run another program, specified as a command list 'cmd', in a new process.
 
@@ -47,17 +67,8 @@ def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
         if executable is not None:
             cmd[0] = executable
 
-    env = env if env is not None else dict(os.environ)
-
-    if sys.platform == 'darwin':
-        from distutils.util import MACOSX_VERSION_VAR, get_macosx_target_ver
-
-        macosx_target_ver = get_macosx_target_ver()
-        if macosx_target_ver:
-            env[MACOSX_VERSION_VAR] = macosx_target_ver
-
     try:
-        subprocess.check_call(cmd, env=env)
+        subprocess.check_call(cmd, env=_inject_macos_ver(env))
     except OSError as exc:
         raise DistutilsExecError(
             f"command {_debug(cmd)!r} failed: {exc.args[-1]}"

From 806b1ca68e7d7668e7e371131ec08b4d4b86354c Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 21 Apr 2024 02:42:25 -0400
Subject: [PATCH 192/221] =?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/_modified.py | 2 +-
 distutils/spawn.py     | 1 -
 distutils/sysconfig.py | 2 +-
 3 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/distutils/_modified.py b/distutils/_modified.py
index 9b375181..07b2ead0 100644
--- a/distutils/_modified.py
+++ b/distutils/_modified.py
@@ -4,8 +4,8 @@
 import os.path
 
 from ._functools import splat
-from .errors import DistutilsFileError
 from .compat.py39 import zip_strict
+from .errors import DistutilsFileError
 
 
 def _newer(source, target):
diff --git a/distutils/spawn.py b/distutils/spawn.py
index 081e2549..234d5cd1 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -12,7 +12,6 @@
 import platform
 import subprocess
 import sys
-
 from typing import Mapping
 
 from ._log import log
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 514e06e3..4ed51c1f 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -16,8 +16,8 @@
 import sys
 import sysconfig
 
-from .compat import py39
 from ._functools import pass_none
+from .compat import py39
 from .errors import DistutilsPlatformError
 
 IS_PYPY = '__pypy__' in sys.builtin_module_names

From 744cf2a2befb6a616657c105e5c9be9f3f921224 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Wed, 24 Apr 2024 10:48:06 -0400
Subject: [PATCH 193/221] Allow macos on Python 3.8 to fail as GitHub CI has
 dropped support.

Closes jaraco/skeleton#124.
---
 .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 ac0ff69e..5ace4c50 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -50,7 +50,7 @@ jobs:
         - python: pypy3.10
           platform: ubuntu-latest
     runs-on: ${{ matrix.platform }}
-    continue-on-error: ${{ matrix.python == '3.13' }}
+    continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }}
     steps:
       - uses: actions/checkout@v4
       - name: Setup Python

From bcf8f079eb729e7bcd50c10cf4da522620b00635 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Wed, 24 Apr 2024 11:06:12 -0400
Subject: [PATCH 194/221] Move project.urls to appear in the order that
 ini2toml generates it. Remove project.scripts.

---
 pyproject.toml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 869fe7e5..04b14cbc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,9 @@ dependencies = [
 ]
 dynamic = ["version"]
 
+[project.urls]
+Homepage = "https://github.com/PROJECT_PATH"
+
 [project.optional-dependencies]
 testing = [
 	# upstream
@@ -44,9 +47,4 @@ docs = [
 	# local
 ]
 
-[project.urls]
-Homepage = "https://github.com/PROJECT_PATH"
-
-[project.scripts]
-
 [tool.setuptools_scm]

From 67aab1554c7c9cbb19bb546a5b6476267030c5b5 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 2 May 2024 15:36:22 -0400
Subject: [PATCH 195/221] Revert "Allow macos on Python 3.8 to fail as GitHub
 CI has dropped support."

This reverts commit 744cf2a2befb6a616657c105e5c9be9f3f921224.
---
 .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 5ace4c50..ac0ff69e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -50,7 +50,7 @@ jobs:
         - python: pypy3.10
           platform: ubuntu-latest
     runs-on: ${{ matrix.platform }}
-    continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }}
+    continue-on-error: ${{ matrix.python == '3.13' }}
     steps:
       - uses: actions/checkout@v4
       - name: Setup Python

From 797889620e96f822fd93bc42aef1fa0ae367dc48 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Fri, 17 May 2024 17:39:19 -0400
Subject: [PATCH 196/221] Use `set` instead of `True`-only `dict`

---
 distutils/dir_util.py | 10 +++++-----
 distutils/dist.py     | 18 +++++++++---------
 2 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/distutils/dir_util.py b/distutils/dir_util.py
index 370c6ffd..175f5c26 100644
--- a/distutils/dir_util.py
+++ b/distutils/dir_util.py
@@ -10,7 +10,7 @@
 
 # cache for by mkpath() -- in addition to cheapening redundant calls,
 # eliminates redundant "creating /foo/bar/baz" messages in dry-run mode
-_path_created = {}
+_path_created = set()
 
 
 def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
@@ -45,7 +45,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
     created_dirs = []
     if os.path.isdir(name) or name == '':
         return created_dirs
-    if _path_created.get(os.path.abspath(name)):
+    if os.path.abspath(name) in _path_created:
         return created_dirs
 
     (head, tail) = os.path.split(name)
@@ -63,7 +63,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
         head = os.path.join(head, d)
         abs_head = os.path.abspath(head)
 
-        if _path_created.get(abs_head):
+        if abs_head in _path_created:
             continue
 
         if verbose >= 1:
@@ -79,7 +79,7 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
                     )
             created_dirs.append(head)
 
-        _path_created[abs_head] = 1
+        _path_created.add(abs_head)
     return created_dirs
 
 
@@ -222,7 +222,7 @@ def remove_tree(directory, verbose=1, dry_run=0):
             # remove dir from cache if it's already there
             abspath = os.path.abspath(cmd[1])
             if abspath in _path_created:
-                _path_created.pop(abspath)
+                _path_created.remove(abspath)
         except OSError as exc:
             log.warning("error removing %s: %s", directory, exc)
 
diff --git a/distutils/dist.py b/distutils/dist.py
index 668ce7eb..b62db8cb 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -694,12 +694,12 @@ def handle_display_options(self, option_order):
         # display that metadata in the order in which the user supplied the
         # metadata options.
         any_display_options = 0
-        is_display_option = {}
+        is_display_option = set()
         for option in self.display_options:
-            is_display_option[option[0]] = 1
+            is_display_option.add(option[0])
 
         for opt, val in option_order:
-            if val and is_display_option.get(opt):
+            if val and opt in is_display_option:
                 opt = translate_longopt(opt)
                 value = getattr(self.metadata, "get_" + opt)()
                 if opt in ('keywords', 'platforms'):
@@ -740,13 +740,13 @@ def print_commands(self):
         import distutils.command
 
         std_commands = distutils.command.__all__
-        is_std = {}
+        is_std = set()
         for cmd in std_commands:
-            is_std[cmd] = 1
+            is_std.add(cmd)
 
         extra_commands = []
         for cmd in self.cmdclass.keys():
-            if not is_std.get(cmd):
+            if cmd not in is_std:
                 extra_commands.append(cmd)
 
         max_length = 0
@@ -771,13 +771,13 @@ def get_command_list(self):
         import distutils.command
 
         std_commands = distutils.command.__all__
-        is_std = {}
+        is_std = set()
         for cmd in std_commands:
-            is_std[cmd] = 1
+            is_std.add(cmd)
 
         extra_commands = []
         for cmd in self.cmdclass.keys():
-            if not is_std.get(cmd):
+            if cmd not in is_std:
                 extra_commands.append(cmd)
 
         rv = []

From 1e1e1b7ff9e50a5d74853994b96100758f34cdd4 Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Fri, 17 May 2024 18:06:18 -0400
Subject: [PATCH 197/221] Use actual boolean parameters and variables

---
 distutils/_msvccompiler.py              | 10 +++---
 distutils/archive_util.py               | 14 +++++---
 distutils/bcppcompiler.py               | 12 +++----
 distutils/ccompiler.py                  | 28 +++++++--------
 distutils/cmd.py                        | 26 ++++++++------
 distutils/command/bdist.py              |  4 +--
 distutils/command/bdist_dumb.py         |  8 ++---
 distutils/command/bdist_rpm.py          | 12 +++----
 distutils/command/build.py              |  2 +-
 distutils/command/build_clib.py         |  2 +-
 distutils/command/build_ext.py          |  4 +--
 distutils/command/build_py.py           | 10 +++---
 distutils/command/check.py              |  6 ++--
 distutils/command/config.py             |  6 ++--
 distutils/command/install.py            | 10 +++---
 distutils/command/install_data.py       |  4 +--
 distutils/command/install_headers.py    |  2 +-
 distutils/command/install_lib.py        |  2 +-
 distutils/command/install_scripts.py    |  2 +-
 distutils/command/register.py           |  6 ++--
 distutils/command/sdist.py              | 24 ++++++-------
 distutils/command/upload.py             |  2 +-
 distutils/config.py                     |  2 +-
 distutils/cygwinccompiler.py            |  6 ++--
 distutils/dir_util.py                   | 20 +++++------
 distutils/dist.py                       | 28 +++++++--------
 distutils/extension.py                  | 10 +++---
 distutils/fancy_getopt.py               |  6 ++--
 distutils/file_util.py                  | 12 +++----
 distutils/filelist.py                   | 14 ++++----
 distutils/msvc9compiler.py              | 12 +++----
 distutils/msvccompiler.py               | 12 +++----
 distutils/sysconfig.py                  | 10 ++++--
 distutils/tests/test_bdist.py           |  2 +-
 distutils/tests/test_bdist_rpm.py       |  4 +--
 distutils/tests/test_build_ext.py       | 22 ++++++------
 distutils/tests/test_build_py.py        | 12 ++++---
 distutils/tests/test_build_scripts.py   |  2 +-
 distutils/tests/test_check.py           |  8 ++---
 distutils/tests/test_cmd.py             |  2 +-
 distutils/tests/test_dir_util.py        | 36 +++++++++----------
 distutils/tests/test_dist.py            |  2 +-
 distutils/tests/test_file_util.py       | 14 ++++----
 distutils/tests/test_install.py         |  2 +-
 distutils/tests/test_install_data.py    |  2 +-
 distutils/tests/test_install_lib.py     |  2 +-
 distutils/tests/test_install_scripts.py |  8 ++---
 distutils/tests/test_register.py        | 14 ++++----
 distutils/tests/test_text_file.py       | 34 +++++++++++++-----
 distutils/tests/test_upload.py          |  4 +--
 distutils/unixccompiler.py              |  6 ++--
 distutils/util.py                       | 12 +++----
 distutils/zosccompiler.py               |  4 +--
 docs/distutils/apiref.rst               | 46 ++++++++++++-------------
 docs/distutils/configfile.rst           |  2 +-
 docs/distutils/setupscript.rst          |  2 +-
 56 files changed, 302 insertions(+), 268 deletions(-)

diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py
index a2159fef..b0322410 100644
--- a/distutils/_msvccompiler.py
+++ b/distutils/_msvccompiler.py
@@ -218,7 +218,7 @@ class MSVCCompiler(CCompiler):
     static_lib_format = shared_lib_format = '%s%s'
     exe_extension = '.exe'
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
         # target platform (.plat_name is consistent with 'bdist')
         self.plat_name = None
@@ -334,7 +334,7 @@ def compile(  # noqa: C901
         output_dir=None,
         macros=None,
         include_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         depends=None,
@@ -423,7 +423,7 @@ def compile(  # noqa: C901
         return objects
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         if not self.initialized:
             self.initialize()
@@ -452,7 +452,7 @@ def link(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -551,7 +551,7 @@ def runtime_library_dir_option(self, dir):
     def library_option(self, lib):
         return self.library_filename(lib)
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         # Prefer a debugging library if found (and requested), but deal
         # with it if we don't have one.
         if debug:
diff --git a/distutils/archive_util.py b/distutils/archive_util.py
index 052f6e46..7b2d1771 100644
--- a/distutils/archive_util.py
+++ b/distutils/archive_util.py
@@ -56,7 +56,13 @@ def _get_uid(name):
 
 
 def make_tarball(
-    base_name, base_dir, compress="gzip", verbose=0, dry_run=0, owner=None, group=None
+    base_name,
+    base_dir,
+    compress="gzip",
+    verbose=False,
+    dry_run=False,
+    owner=None,
+    group=None,
 ):
     """Create a (possibly compressed) tar file from all the files under
     'base_dir'.
@@ -134,7 +140,7 @@ def _set_uid_gid(tarinfo):
     return archive_name
 
 
-def make_zipfile(base_name, base_dir, verbose=0, dry_run=0):  # noqa: C901
+def make_zipfile(base_name, base_dir, verbose=False, dry_run=False):  # noqa: C901
     """Create a zip file from all the files under 'base_dir'.
 
     The output zip file will be named 'base_name' + ".zip".  Uses either the
@@ -224,8 +230,8 @@ def make_archive(
     format,
     root_dir=None,
     base_dir=None,
-    verbose=0,
-    dry_run=0,
+    verbose=False,
+    dry_run=False,
     owner=None,
     group=None,
 ):
diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py
index c1341e43..4f47058c 100644
--- a/distutils/bcppcompiler.py
+++ b/distutils/bcppcompiler.py
@@ -61,7 +61,7 @@ class BCPPCompiler(CCompiler):
     static_lib_format = shared_lib_format = '%s%s'
     exe_extension = '.exe'
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
 
         # These executables are assumed to all be in the path.
@@ -90,7 +90,7 @@ def compile(  # noqa: C901
         output_dir=None,
         macros=None,
         include_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         depends=None,
@@ -161,7 +161,7 @@ def compile(  # noqa: C901
     # compile ()
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         (objects, output_dir) = self._fix_object_args(objects, output_dir)
         output_filename = self.library_filename(output_libname, output_dir=output_dir)
@@ -189,7 +189,7 @@ def link(  # noqa: C901
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -313,7 +313,7 @@ def link(  # noqa: C901
 
     # -- Miscellaneous methods -----------------------------------------
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         # List of effective library names to try, in order of preference:
         # xxx_bcpp.lib is better than xxx.lib
         # and xxx_d.lib is better than xxx.lib if debug is set
@@ -339,7 +339,7 @@ def find_library_file(self, dirs, lib, debug=0):
             return None
 
     # overwrite the one from CCompiler to support rc and res-files
-    def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):
+    def object_filenames(self, source_filenames, strip_dir=False, output_dir=''):
         if output_dir is None:
             output_dir = ''
         obj_names = []
diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py
index 8876d730..c4dced38 100644
--- a/distutils/ccompiler.py
+++ b/distutils/ccompiler.py
@@ -104,7 +104,7 @@ class CCompiler:
     library dirs specific to this compiler class
     """
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         self.dry_run = dry_run
         self.force = force
         self.verbose = verbose
@@ -342,7 +342,7 @@ def _setup_compile(self, outdir, macros, incdirs, sources, depends, extra):
             extra = []
 
         # Get the list of expected output (object) files
-        objects = self.object_filenames(sources, strip_dir=0, output_dir=outdir)
+        objects = self.object_filenames(sources, strip_dir=False, output_dir=outdir)
         assert len(objects) == len(sources)
 
         pp_opts = gen_preprocess_options(macros, incdirs)
@@ -532,7 +532,7 @@ def compile(
         output_dir=None,
         macros=None,
         include_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         depends=None,
@@ -609,7 +609,7 @@ def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
         pass
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         """Link a bunch of stuff together to create a static library file.
         The "bunch of stuff" consists of the list of object files supplied
@@ -650,7 +650,7 @@ def link(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -712,7 +712,7 @@ def link_shared_lib(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -743,7 +743,7 @@ def link_shared_object(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -773,7 +773,7 @@ def link_executable(
         libraries=None,
         library_dirs=None,
         runtime_library_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         target_lang=None,
@@ -909,7 +909,7 @@ def has_function(  # noqa: C901
                 os.remove(fn)
         return True
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         """Search the specified list of directories for a static or shared
         library file 'lib' and return the full path to that file.  If
         'debug' true, look for a debugging version (if that makes sense on
@@ -952,7 +952,7 @@ def find_library_file(self, dirs, lib, debug=0):
     #   * exe_extension -
     #     extension for executable files, eg. '' or '.exe'
 
-    def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):
+    def object_filenames(self, source_filenames, strip_dir=False, output_dir=''):
         if output_dir is None:
             output_dir = ''
         return list(
@@ -987,13 +987,13 @@ def _make_relative(base):
         # If abs, chop off leading /
         return no_drive[os.path.isabs(no_drive) :]
 
-    def shared_object_filename(self, basename, strip_dir=0, output_dir=''):
+    def shared_object_filename(self, basename, strip_dir=False, output_dir=''):
         assert output_dir is not None
         if strip_dir:
             basename = os.path.basename(basename)
         return os.path.join(output_dir, basename + self.shared_lib_extension)
 
-    def executable_filename(self, basename, strip_dir=0, output_dir=''):
+    def executable_filename(self, basename, strip_dir=False, output_dir=''):
         assert output_dir is not None
         if strip_dir:
             basename = os.path.basename(basename)
@@ -1003,7 +1003,7 @@ def library_filename(
         self,
         libname,
         lib_type='static',
-        strip_dir=0,
+        strip_dir=False,
         output_dir='',  # or 'shared'
     ):
         assert output_dir is not None
@@ -1125,7 +1125,7 @@ def show_compilers():
     pretty_printer.print_help("List of available compilers:")
 
 
-def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0):
+def new_compiler(plat=None, compiler=None, verbose=False, dry_run=False, force=False):
     """Generate an instance of some CCompiler subclass for the supplied
     platform/compiler combination.  'plat' defaults to 'os.name'
     (eg. 'posix', 'nt'), and 'compiler' defaults to the default compiler
diff --git a/distutils/cmd.py b/distutils/cmd.py
index 02dbf165..fc35bd97 100644
--- a/distutils/cmd.py
+++ b/distutils/cmd.py
@@ -87,13 +87,13 @@ def __init__(self, dist):
 
         # The 'help' flag is just used for command-line parsing, so
         # none of that complicated bureaucracy is needed.
-        self.help = 0
+        self.help = False
 
         # 'finalized' records whether or not 'finalize_options()' has been
         # called.  'finalize_options()' itself should not pay attention to
         # this flag: it is the business of 'ensure_finalized()', which
         # always calls 'finalize_options()', to respect/update it.
-        self.finalized = 0
+        self.finalized = False
 
     # XXX A more explicit way to customize dry_run would be better.
     def __getattr__(self, attr):
@@ -109,7 +109,7 @@ def __getattr__(self, attr):
     def ensure_finalized(self):
         if not self.finalized:
             self.finalize_options()
-        self.finalized = 1
+        self.finalized = True
 
     # Subclasses must define:
     #   initialize_options()
@@ -293,7 +293,7 @@ def set_undefined_options(self, src_cmd, *option_pairs):
             if getattr(self, dst_option) is None:
                 setattr(self, dst_option, getattr(src_cmd_obj, src_option))
 
-    def get_finalized_command(self, command, create=1):
+    def get_finalized_command(self, command, create=True):
         """Wrapper around Distribution's 'get_command_obj()' method: find
         (create if necessary and 'create' is true) the command object for
         'command', call its 'ensure_finalized()' method, and return the
@@ -305,7 +305,7 @@ def get_finalized_command(self, command, create=1):
 
     # XXX rename to 'get_reinitialized_command()'? (should do the
     # same in dist.py, if so)
-    def reinitialize_command(self, command, reinit_subcommands=0):
+    def reinitialize_command(self, command, reinit_subcommands=False):
         return self.distribution.reinitialize_command(command, reinit_subcommands)
 
     def run_command(self, command):
@@ -340,7 +340,13 @@ def mkpath(self, name, mode=0o777):
         dir_util.mkpath(name, mode, dry_run=self.dry_run)
 
     def copy_file(
-        self, infile, outfile, preserve_mode=1, preserve_times=1, link=None, level=1
+        self,
+        infile,
+        outfile,
+        preserve_mode=True,
+        preserve_times=True,
+        link=None,
+        level=1,
     ):
         """Copy a file respecting verbose, dry-run and force flags.  (The
         former two default to whatever is in the Distribution object, and
@@ -359,9 +365,9 @@ def copy_tree(
         self,
         infile,
         outfile,
-        preserve_mode=1,
-        preserve_times=1,
-        preserve_symlinks=0,
+        preserve_mode=True,
+        preserve_times=True,
+        preserve_symlinks=False,
         level=1,
     ):
         """Copy an entire directory tree respecting verbose, dry-run,
@@ -381,7 +387,7 @@ def move_file(self, src, dst, level=1):
         """Move a file respecting dry-run flag."""
         return file_util.move_file(src, dst, dry_run=self.dry_run)
 
-    def spawn(self, cmd, search_path=1, level=1):
+    def spawn(self, cmd, search_path=True, level=1):
         """Spawn an external command respecting dry-run flag."""
         from distutils.spawn import spawn
 
diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py
index ade98445..21bc7c5d 100644
--- a/distutils/command/bdist.py
+++ b/distutils/command/bdist.py
@@ -94,7 +94,7 @@ def initialize_options(self):
         self.plat_name = None
         self.formats = None
         self.dist_dir = None
-        self.skip_build = 0
+        self.skip_build = False
         self.group = None
         self.owner = None
 
@@ -150,5 +150,5 @@ def run(self):
             # If we're going to need to run this command again, tell it to
             # keep its temporary files around so subsequent runs go faster.
             if cmd_name in commands[i + 1 :]:
-                sub_cmd.keep_temp = 1
+                sub_cmd.keep_temp = True
             self.run_command(cmd_name)
diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py
index 06502d20..0f15b9fa 100644
--- a/distutils/command/bdist_dumb.py
+++ b/distutils/command/bdist_dumb.py
@@ -63,10 +63,10 @@ def initialize_options(self):
         self.bdist_dir = None
         self.plat_name = None
         self.format = None
-        self.keep_temp = 0
+        self.keep_temp = False
         self.dist_dir = None
         self.skip_build = None
-        self.relative = 0
+        self.relative = False
         self.owner = None
         self.group = None
 
@@ -95,10 +95,10 @@ def run(self):
         if not self.skip_build:
             self.run_command('build')
 
-        install = self.reinitialize_command('install', reinit_subcommands=1)
+        install = self.reinitialize_command('install', reinit_subcommands=True)
         install.root = self.bdist_dir
         install.skip_build = self.skip_build
-        install.warn_dir = 0
+        install.warn_dir = False
 
         log.info("installing to %s", self.bdist_dir)
         self.run_command('install')
diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index 649968a5..769dfa6d 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -187,13 +187,13 @@ def initialize_options(self):
         self.build_requires = None
         self.obsoletes = None
 
-        self.keep_temp = 0
-        self.use_rpm_opt_flags = 1
-        self.rpm3_mode = 1
-        self.no_autoreq = 0
+        self.keep_temp = False
+        self.use_rpm_opt_flags = True
+        self.rpm3_mode = True
+        self.no_autoreq = False
 
         self.force_arch = None
-        self.quiet = 0
+        self.quiet = False
 
     def finalize_options(self):
         self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
@@ -223,7 +223,7 @@ def finalize_options(self):
 
         # don't pass CFLAGS to pure python distributions
         if not self.distribution.has_ext_modules():
-            self.use_rpm_opt_flags = 0
+            self.use_rpm_opt_flags = False
 
         self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
         self.finalize_package_data()
diff --git a/distutils/command/build.py b/distutils/command/build.py
index d18ed503..0128500b 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -62,7 +62,7 @@ def initialize_options(self):
         self.compiler = None
         self.plat_name = None
         self.debug = None
-        self.force = 0
+        self.force = False
         self.executable = None
         self.parallel = None
 
diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py
index 360575d0..85148d4b 100644
--- a/distutils/command/build_clib.py
+++ b/distutils/command/build_clib.py
@@ -57,7 +57,7 @@ def initialize_options(self):
         self.define = None
         self.undef = None
         self.debug = None
-        self.force = 0
+        self.force = False
         self.compiler = None
 
     def finalize_options(self):
diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py
index 06d949af..bfe68600 100644
--- a/distutils/command/build_ext.py
+++ b/distutils/command/build_ext.py
@@ -109,7 +109,7 @@ def initialize_options(self):
         self.build_lib = None
         self.plat_name = None
         self.build_temp = None
-        self.inplace = 0
+        self.inplace = False
         self.package = None
 
         self.include_dirs = None
@@ -175,7 +175,7 @@ def finalize_options(self):  # noqa: C901
         # Make sure Python's include directories (for Python.h, pyconfig.h,
         # etc.) are in the include search path.
         py_include = sysconfig.get_python_inc()
-        plat_py_include = sysconfig.get_python_inc(plat_specific=1)
+        plat_py_include = sysconfig.get_python_inc(plat_specific=True)
         if self.include_dirs is None:
             self.include_dirs = self.distribution.include_dirs or []
         if isinstance(self.include_dirs, str):
diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py
index 56e6fa2e..40a99b90 100644
--- a/distutils/command/build_py.py
+++ b/distutils/command/build_py.py
@@ -38,7 +38,7 @@ def initialize_options(self):
         self.package = None
         self.package_data = None
         self.package_dir = None
-        self.compile = 0
+        self.compile = False
         self.optimize = 0
         self.force = None
 
@@ -95,7 +95,7 @@ def run(self):
             self.build_packages()
             self.build_package_data()
 
-        self.byte_compile(self.get_outputs(include_bytecode=0))
+        self.byte_compile(self.get_outputs(include_bytecode=False))
 
     def get_data_files(self):
         """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
@@ -264,7 +264,7 @@ def find_modules(self):
                 (package_dir, checked) = packages[package]
             except KeyError:
                 package_dir = self.get_package_dir(package)
-                checked = 0
+                checked = False
 
             if not checked:
                 init_py = self.check_package(package, package_dir)
@@ -306,7 +306,7 @@ def get_module_outfile(self, build_dir, package, module):
         outfile_path = [build_dir] + list(package) + [module + ".py"]
         return os.path.join(*outfile_path)
 
-    def get_outputs(self, include_bytecode=1):
+    def get_outputs(self, include_bytecode=True):
         modules = self.find_all_modules()
         outputs = []
         for package, module, _module_file in modules:
@@ -347,7 +347,7 @@ def build_module(self, module, module_file, package):
         outfile = self.get_module_outfile(self.build_lib, package, module)
         dir = os.path.dirname(outfile)
         self.mkpath(dir)
-        return self.copy_file(module_file, outfile, preserve_mode=0)
+        return self.copy_file(module_file, outfile, preserve_mode=False)
 
     def build_modules(self):
         modules = self.find_modules()
diff --git a/distutils/command/check.py b/distutils/command/check.py
index 28599e10..11d40a96 100644
--- a/distutils/command/check.py
+++ b/distutils/command/check.py
@@ -21,7 +21,7 @@ def __init__(
             report_level,
             halt_level,
             stream=None,
-            debug=0,
+            debug=False,
             encoding='ascii',
             error_handler='replace',
         ):
@@ -58,9 +58,9 @@ class check(Command):
 
     def initialize_options(self):
         """Sets default values for options."""
-        self.restructuredtext = 0
+        self.restructuredtext = False
         self.metadata = 1
-        self.strict = 0
+        self.strict = False
         self._warnings = 0
 
     def finalize_options(self):
diff --git a/distutils/command/config.py b/distutils/command/config.py
index d4b2b0a3..e82f0fd0 100644
--- a/distutils/command/config.py
+++ b/distutils/command/config.py
@@ -94,7 +94,7 @@ def _check_compiler(self):
 
         if not isinstance(self.compiler, CCompiler):
             self.compiler = new_compiler(
-                compiler=self.compiler, dry_run=self.dry_run, force=1
+                compiler=self.compiler, dry_run=self.dry_run, force=True
             )
             customize_compiler(self.compiler)
             if self.include_dirs:
@@ -292,8 +292,8 @@ def check_func(
         include_dirs=None,
         libraries=None,
         library_dirs=None,
-        decl=0,
-        call=0,
+        decl=False,
+        call=False,
     ):
         """Determine if function 'func' is available by constructing a
         source file that refers to 'func', and compiles and links it.
diff --git a/distutils/command/install.py b/distutils/command/install.py
index 8e920be4..9b82cf1e 100644
--- a/distutils/command/install.py
+++ b/distutils/command/install.py
@@ -258,7 +258,7 @@ def initialize_options(self):
         self.prefix = None
         self.exec_prefix = None
         self.home = None
-        self.user = 0
+        self.user = False
 
         # These select only the installation base; it's up to the user to
         # specify the installation scheme (currently, that means supplying
@@ -293,7 +293,7 @@ def initialize_options(self):
         # 'install_path_file' is always true unless some outsider meddles
         # with it.
         self.extra_path = None
-        self.install_path_file = 1
+        self.install_path_file = True
 
         # 'force' forces installation, even if target files are not
         # out-of-date.  'skip_build' skips running the "build" command,
@@ -301,9 +301,9 @@ def initialize_options(self):
         # a user option, it's just there so the bdist_* commands can turn
         # it off) determines whether we warn about installing to a
         # directory not in sys.path.
-        self.force = 0
-        self.skip_build = 0
-        self.warn_dir = 1
+        self.force = False
+        self.skip_build = False
+        self.warn_dir = True
 
         # These are only here as a conduit from the 'build' command to the
         # 'install_*' commands that do the real work.  ('build_base' isn't
diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py
index b63a1af2..a4da8924 100644
--- a/distutils/command/install_data.py
+++ b/distutils/command/install_data.py
@@ -31,9 +31,9 @@ def initialize_options(self):
         self.install_dir = None
         self.outfiles = []
         self.root = None
-        self.force = 0
+        self.force = False
         self.data_files = self.distribution.data_files
-        self.warn_dir = 1
+        self.warn_dir = True
 
     def finalize_options(self):
         self.set_undefined_options(
diff --git a/distutils/command/install_headers.py b/distutils/command/install_headers.py
index 085272c1..fbb3b242 100644
--- a/distutils/command/install_headers.py
+++ b/distutils/command/install_headers.py
@@ -19,7 +19,7 @@ class install_headers(Command):
 
     def initialize_options(self):
         self.install_dir = None
-        self.force = 0
+        self.force = False
         self.outfiles = []
 
     def finalize_options(self):
diff --git a/distutils/command/install_lib.py b/distutils/command/install_lib.py
index b1f346f0..efa60585 100644
--- a/distutils/command/install_lib.py
+++ b/distutils/command/install_lib.py
@@ -54,7 +54,7 @@ def initialize_options(self):
         # let the 'install' command dictate our installation directory
         self.install_dir = None
         self.build_dir = None
-        self.force = 0
+        self.force = False
         self.compile = None
         self.optimize = None
         self.skip_build = None
diff --git a/distutils/command/install_scripts.py b/distutils/command/install_scripts.py
index e66b13a1..bb43387f 100644
--- a/distutils/command/install_scripts.py
+++ b/distutils/command/install_scripts.py
@@ -26,7 +26,7 @@ class install_scripts(Command):
 
     def initialize_options(self):
         self.install_dir = None
-        self.force = 0
+        self.force = False
         self.build_dir = None
         self.skip_build = None
 
diff --git a/distutils/command/register.py b/distutils/command/register.py
index ee6c54da..0311d45a 100644
--- a/distutils/command/register.py
+++ b/distutils/command/register.py
@@ -37,8 +37,8 @@ class register(PyPIRCCommand):
 
     def initialize_options(self):
         PyPIRCCommand.initialize_options(self)
-        self.list_classifiers = 0
-        self.strict = 0
+        self.list_classifiers = False
+        self.strict = False
 
     def finalize_options(self):
         PyPIRCCommand.finalize_options(self)
@@ -74,7 +74,7 @@ def check_metadata(self):
         check = self.distribution.get_command_obj('check')
         check.ensure_finalized()
         check.strict = self.strict
-        check.restructuredtext = 1
+        check.restructuredtext = True
         check.run()
 
     def _set_config(self):
diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py
index 387d27c9..e737cf2a 100644
--- a/distutils/command/sdist.py
+++ b/distutils/command/sdist.py
@@ -125,14 +125,14 @@ def initialize_options(self):
 
         # 'use_defaults': if true, we will include the default file set
         # in the manifest
-        self.use_defaults = 1
-        self.prune = 1
+        self.use_defaults = True
+        self.prune = True
 
-        self.manifest_only = 0
-        self.force_manifest = 0
+        self.manifest_only = False
+        self.force_manifest = False
 
         self.formats = ['gztar']
-        self.keep_temp = 0
+        self.keep_temp = False
         self.dist_dir = None
 
         self.archive_files = None
@@ -353,12 +353,12 @@ def read_template(self):
         log.info("reading manifest template '%s'", self.template)
         template = TextFile(
             self.template,
-            strip_comments=1,
-            skip_blanks=1,
-            join_lines=1,
-            lstrip_ws=1,
-            rstrip_ws=1,
-            collapse_join=1,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            lstrip_ws=True,
+            rstrip_ws=True,
+            collapse_join=True,
         )
 
         try:
@@ -401,7 +401,7 @@ def prune_file_list(self):
 
         vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', '_darcs']
         vcs_ptrn = r'(^|{})({})({}).*'.format(seps, '|'.join(vcs_dirs), seps)
-        self.filelist.exclude_pattern(vcs_ptrn, is_regex=1)
+        self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)
 
     def write_manifest(self):
         """Write the file list in 'self.filelist' (presumably as filled in
diff --git a/distutils/command/upload.py b/distutils/command/upload.py
index cf541f8a..63751e72 100644
--- a/distutils/command/upload.py
+++ b/distutils/command/upload.py
@@ -41,7 +41,7 @@ def initialize_options(self):
         PyPIRCCommand.initialize_options(self)
         self.username = ''
         self.password = ''
-        self.show_response = 0
+        self.show_response = False
         self.sign = False
         self.identity = None
 
diff --git a/distutils/config.py b/distutils/config.py
index 83f96a9e..7b273e16 100644
--- a/distutils/config.py
+++ b/distutils/config.py
@@ -129,7 +129,7 @@ def initialize_options(self):
         """Initialize options."""
         self.repository = None
         self.realm = None
-        self.show_response = 0
+        self.show_response = False
 
     def finalize_options(self):
         """Finalizes options."""
diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py
index 539f09d8..f2e62bd7 100644
--- a/distutils/cygwinccompiler.py
+++ b/distutils/cygwinccompiler.py
@@ -83,7 +83,7 @@ class CygwinCCompiler(UnixCCompiler):
     dylib_lib_format = "cyg%s%s"
     exe_extension = ".exe"
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
 
         status, details = check_config_h()
@@ -154,7 +154,7 @@ def link(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -265,7 +265,7 @@ class Mingw32CCompiler(CygwinCCompiler):
 
     compiler_type = 'mingw32'
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
 
         shared_option = "-shared"
diff --git a/distutils/dir_util.py b/distutils/dir_util.py
index 370c6ffd..4108c6c5 100644
--- a/distutils/dir_util.py
+++ b/distutils/dir_util.py
@@ -13,7 +13,7 @@
 _path_created = {}
 
 
-def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
+def mkpath(name, mode=0o777, verbose=True, dry_run=False):  # noqa: C901
     """Create a directory and any missing ancestor directories.
 
     If the directory already exists (or if 'name' is the empty string, which
@@ -79,11 +79,11 @@ def mkpath(name, mode=0o777, verbose=1, dry_run=0):  # noqa: C901
                     )
             created_dirs.append(head)
 
-        _path_created[abs_head] = 1
+        _path_created[abs_head] = True
     return created_dirs
 
 
-def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0):
+def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False):
     """Create all the empty directories under 'base_dir' needed to put 'files'
     there.
 
@@ -104,12 +104,12 @@ def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0):
 def copy_tree(  # noqa: C901
     src,
     dst,
-    preserve_mode=1,
-    preserve_times=1,
-    preserve_symlinks=0,
-    update=0,
-    verbose=1,
-    dry_run=0,
+    preserve_mode=True,
+    preserve_times=True,
+    preserve_symlinks=False,
+    update=False,
+    verbose=True,
+    dry_run=False,
 ):
     """Copy an entire directory tree 'src' to a new location 'dst'.
 
@@ -202,7 +202,7 @@ def _build_cmdtuple(path, cmdtuples):
     cmdtuples.append((os.rmdir, path))
 
 
-def remove_tree(directory, verbose=1, dry_run=0):
+def remove_tree(directory, verbose=True, dry_run=False):
     """Recursively remove an entire directory tree.
 
     Any errors are ignored (apart from being reported to stdout if 'verbose'
diff --git a/distutils/dist.py b/distutils/dist.py
index 668ce7eb..13b939ae 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -137,9 +137,9 @@ def __init__(self, attrs=None):  # noqa: C901
         """
 
         # Default values for our command-line options
-        self.verbose = 1
-        self.dry_run = 0
-        self.help = 0
+        self.verbose = True
+        self.dry_run = False
+        self.help = False
         for attr in self.display_option_names:
             setattr(self, attr, 0)
 
@@ -579,7 +579,7 @@ def _parse_command_opts(self, parser, args):  # noqa: C901
         parser.set_negative_aliases(negative_opt)
         (args, opts) = parser.getopt(args[1:])
         if hasattr(opts, 'help') and opts.help:
-            self._show_help(parser, display_options=0, commands=[cmd_class])
+            self._show_help(parser, display_options=False, commands=[cmd_class])
             return
 
         if hasattr(cmd_class, 'help_options') and isinstance(
@@ -622,7 +622,7 @@ def finalize_options(self):
                 setattr(self.metadata, attr, value)
 
     def _show_help(
-        self, parser, global_options=1, display_options=1, commands: Iterable = ()
+        self, parser, global_options=True, display_options=True, commands: Iterable = ()
     ):
         """Show help for the setup script command-line in the form of
         several lists of command-line options.  'parser' should be a
@@ -696,7 +696,7 @@ def handle_display_options(self, option_order):
         any_display_options = 0
         is_display_option = {}
         for option in self.display_options:
-            is_display_option[option[0]] = 1
+            is_display_option[option[0]] = True
 
         for opt, val in option_order:
             if val and is_display_option.get(opt):
@@ -742,7 +742,7 @@ def print_commands(self):
         std_commands = distutils.command.__all__
         is_std = {}
         for cmd in std_commands:
-            is_std[cmd] = 1
+            is_std[cmd] = True
 
         extra_commands = []
         for cmd in self.cmdclass.keys():
@@ -773,7 +773,7 @@ def get_command_list(self):
         std_commands = distutils.command.__all__
         is_std = {}
         for cmd in std_commands:
-            is_std[cmd] = 1
+            is_std[cmd] = True
 
         extra_commands = []
         for cmd in self.cmdclass.keys():
@@ -844,7 +844,7 @@ def get_command_class(self, command):
 
         raise DistutilsModuleError("invalid command '%s'" % command)
 
-    def get_command_obj(self, command, create=1):
+    def get_command_obj(self, command, create=True):
         """Return the command object for 'command'.  Normally this object
         is cached on a previous call to 'get_command_obj()'; if no command
         object for 'command' is in the cache, then we either create and
@@ -860,7 +860,7 @@ def get_command_obj(self, command, create=1):
 
             klass = self.get_command_class(command)
             cmd_obj = self.command_obj[command] = klass(self)
-            self.have_run[command] = 0
+            self.have_run[command] = False
 
             # Set any options that were supplied in config files
             # or on the command line.  (NB. support for error
@@ -915,7 +915,7 @@ def _set_command_options(self, command_obj, option_dict=None):  # noqa: C901
             except ValueError as msg:
                 raise DistutilsOptionError(msg)
 
-    def reinitialize_command(self, command, reinit_subcommands=0):
+    def reinitialize_command(self, command, reinit_subcommands=False):
         """Reinitializes a command to the state it was in when first
         returned by 'get_command_obj()': ie., initialized but not yet
         finalized.  This provides the opportunity to sneak option
@@ -945,8 +945,8 @@ def reinitialize_command(self, command, reinit_subcommands=0):
         if not command.finalized:
             return command
         command.initialize_options()
-        command.finalized = 0
-        self.have_run[command_name] = 0
+        command.finalized = False
+        self.have_run[command_name] = False
         self._set_command_options(command)
 
         if reinit_subcommands:
@@ -986,7 +986,7 @@ def run_command(self, command):
         cmd_obj = self.get_command_obj(command)
         cmd_obj.ensure_finalized()
         cmd_obj.run()
-        self.have_run[command] = 1
+        self.have_run[command] = True
 
     # -- Distribution query methods ------------------------------------
 
diff --git a/distutils/extension.py b/distutils/extension.py
index 94e71635..793f8972 100644
--- a/distutils/extension.py
+++ b/distutils/extension.py
@@ -150,11 +150,11 @@ def read_setup_file(filename):  # noqa: C901
     #    ... [ ...] [ ...] [ ...]
     file = TextFile(
         filename,
-        strip_comments=1,
-        skip_blanks=1,
-        join_lines=1,
-        lstrip_ws=1,
-        rstrip_ws=1,
+        strip_comments=True,
+        skip_blanks=True,
+        join_lines=True,
+        lstrip_ws=True,
+        rstrip_ws=True,
     )
     try:
         extensions = []
diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py
index e905aede..abbbe267 100644
--- a/distutils/fancy_getopt.py
+++ b/distutils/fancy_getopt.py
@@ -178,7 +178,7 @@ def _grok_option_table(self):  # noqa: C901
                 if short:
                     short = short + ':'
                 long = long[0:-1]
-                self.takes_arg[long] = 1
+                self.takes_arg[long] = True
             else:
                 # Is option is a "negative alias" for some other option (eg.
                 # "quiet" == "!verbose")?
@@ -191,7 +191,7 @@ def _grok_option_table(self):  # noqa: C901
                         )
 
                     self.long_opts[-1] = long  # XXX redundant?!
-                self.takes_arg[long] = 0
+                self.takes_arg[long] = False
 
             # If this is an alias option, make sure its "takes arg" flag is
             # the same as the option it's aliased to.
@@ -268,7 +268,7 @@ def getopt(self, args=None, object=None):  # noqa: C901
 
             attr = self.attr_name[opt]
             # The only repeating option at the moment is 'verbose'.
-            # It has a negative option -q quiet, which should set verbose = 0.
+            # It has a negative option -q quiet, which should set verbose = False.
             if val and self.repeat.get(attr) is not None:
                 val = getattr(object, attr, 0) + 1
             setattr(object, attr, val)
diff --git a/distutils/file_util.py b/distutils/file_util.py
index 960def9c..0f20091e 100644
--- a/distutils/file_util.py
+++ b/distutils/file_util.py
@@ -63,12 +63,12 @@ def _copy_file_contents(src, dst, buffer_size=16 * 1024):  # noqa: C901
 def copy_file(  # noqa: C901
     src,
     dst,
-    preserve_mode=1,
-    preserve_times=1,
-    update=0,
+    preserve_mode=True,
+    preserve_times=True,
+    update=False,
     link=None,
-    verbose=1,
-    dry_run=0,
+    verbose=True,
+    dry_run=False,
 ):
     """Copy a file 'src' to 'dst'.  If 'dst' is a directory, then 'src' is
     copied there with the same name; otherwise, it must be a filename.  (If
@@ -168,7 +168,7 @@ def copy_file(  # noqa: C901
 
 
 # XXX I suspect this is Unix-specific -- need porting help!
-def move_file(src, dst, verbose=1, dry_run=0):  # noqa: C901
+def move_file(src, dst, verbose=True, dry_run=False):  # noqa: C901
     """Move a file 'src' to 'dst'.  If 'dst' is a directory, the file will
     be moved into it with the same name; otherwise, 'src' is just renamed
     to 'dst'.  Return the new full name of the file.
diff --git a/distutils/filelist.py b/distutils/filelist.py
index 71ffb2ab..78276a4d 100644
--- a/distutils/filelist.py
+++ b/distutils/filelist.py
@@ -119,13 +119,13 @@ def process_template_line(self, line):  # noqa: C901
         if action == 'include':
             self.debug_print("include " + ' '.join(patterns))
             for pattern in patterns:
-                if not self.include_pattern(pattern, anchor=1):
+                if not self.include_pattern(pattern, anchor=True):
                     log.warning("warning: no files found matching '%s'", pattern)
 
         elif action == 'exclude':
             self.debug_print("exclude " + ' '.join(patterns))
             for pattern in patterns:
-                if not self.exclude_pattern(pattern, anchor=1):
+                if not self.exclude_pattern(pattern, anchor=True):
                     log.warning(
                         (
                             "warning: no previously-included files "
@@ -137,7 +137,7 @@ def process_template_line(self, line):  # noqa: C901
         elif action == 'global-include':
             self.debug_print("global-include " + ' '.join(patterns))
             for pattern in patterns:
-                if not self.include_pattern(pattern, anchor=0):
+                if not self.include_pattern(pattern, anchor=False):
                     log.warning(
                         (
                             "warning: no files found matching '%s' "
@@ -149,7 +149,7 @@ def process_template_line(self, line):  # noqa: C901
         elif action == 'global-exclude':
             self.debug_print("global-exclude " + ' '.join(patterns))
             for pattern in patterns:
-                if not self.exclude_pattern(pattern, anchor=0):
+                if not self.exclude_pattern(pattern, anchor=False):
                     log.warning(
                         (
                             "warning: no previously-included files matching "
@@ -197,7 +197,7 @@ def process_template_line(self, line):  # noqa: C901
 
     # Filtering/selection methods
 
-    def include_pattern(self, pattern, anchor=1, prefix=None, is_regex=0):
+    def include_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
         """Select strings (presumably filenames) from 'self.files' that
         match 'pattern', a Unix-style wildcard (glob) pattern.  Patterns
         are not quite the same as implemented by the 'fnmatch' module: '*'
@@ -238,7 +238,7 @@ def include_pattern(self, pattern, anchor=1, prefix=None, is_regex=0):
                 files_found = True
         return files_found
 
-    def exclude_pattern(self, pattern, anchor=1, prefix=None, is_regex=0):
+    def exclude_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
         """Remove strings (presumably filenames) from 'files' that match
         'pattern'.  Other parameters are the same as for
         'include_pattern()', above.
@@ -332,7 +332,7 @@ def glob_to_re(pattern):
     return pattern_re
 
 
-def translate_pattern(pattern, anchor=1, prefix=None, is_regex=0):
+def translate_pattern(pattern, anchor=True, prefix=None, is_regex=False):
     """Translate a shell-like wildcard pattern to a compiled regular
     expression.  Return the compiled regex.  If 'is_regex' true,
     then 'pattern' is directly compiled to a regex (if it's a string)
diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py
index 6a0105e4..2bde3f4e 100644
--- a/distutils/msvc9compiler.py
+++ b/distutils/msvc9compiler.py
@@ -346,7 +346,7 @@ class MSVCCompiler(CCompiler):
     static_lib_format = shared_lib_format = '%s%s'
     exe_extension = '.exe'
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
         self.__version = VERSION
         self.__root = r"Software\Microsoft\VisualStudio"
@@ -460,7 +460,7 @@ def initialize(self, plat_name=None):  # noqa: C901
 
     # -- Worker methods ------------------------------------------------
 
-    def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):
+    def object_filenames(self, source_filenames, strip_dir=False, output_dir=''):
         # Copied from ccompiler.py, extended to return .res as 'object'-file
         # for .rc input file
         if output_dir is None:
@@ -491,7 +491,7 @@ def compile(  # noqa: C901
         output_dir=None,
         macros=None,
         include_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         depends=None,
@@ -578,7 +578,7 @@ def compile(  # noqa: C901
         return objects
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         if not self.initialized:
             self.initialize()
@@ -606,7 +606,7 @@ def link(  # noqa: C901
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -783,7 +783,7 @@ def runtime_library_dir_option(self, dir):
     def library_option(self, lib):
         return self.library_filename(lib)
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         # Prefer a debugging library if found (and requested), but deal
         # with it if we don't have one.
         if debug:
diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py
index ac8b68c0..223e627a 100644
--- a/distutils/msvccompiler.py
+++ b/distutils/msvccompiler.py
@@ -253,7 +253,7 @@ class MSVCCompiler(CCompiler):
     static_lib_format = shared_lib_format = '%s%s'
     exe_extension = '.exe'
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
         self.__version = get_build_version()
         self.__arch = get_build_architecture()
@@ -354,7 +354,7 @@ def initialize(self):
 
     # -- Worker methods ------------------------------------------------
 
-    def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):
+    def object_filenames(self, source_filenames, strip_dir=False, output_dir=''):
         # Copied from ccompiler.py, extended to return .res as 'object'-file
         # for .rc input file
         if output_dir is None:
@@ -385,7 +385,7 @@ def compile(  # noqa: C901
         output_dir=None,
         macros=None,
         include_dirs=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         depends=None,
@@ -472,7 +472,7 @@ def compile(  # noqa: C901
         return objects
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         if not self.initialized:
             self.initialize()
@@ -500,7 +500,7 @@ def link(  # noqa: C901
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -585,7 +585,7 @@ def runtime_library_dir_option(self, dir):
     def library_option(self, lib):
         return self.library_filename(lib)
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         # Prefer a debugging library if found (and requested), but deal
         # with it if we don't have one.
         if debug:
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 4ed51c1f..4ef57a89 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -108,7 +108,7 @@ def get_python_version():
     return '%d.%d' % sys.version_info[:2]
 
 
-def get_python_inc(plat_specific=0, prefix=None):
+def get_python_inc(plat_specific=False, prefix=None):
     """Return the directory containing installed Python header files.
 
     If 'plat_specific' is false (the default), this is the path to the
@@ -213,7 +213,7 @@ def _posix_lib(standard_lib, libpython, early_prefix, prefix):
         return os.path.join(libpython, "site-packages")
 
 
-def get_python_lib(plat_specific=0, standard_lib=0, prefix=None):
+def get_python_lib(plat_specific=False, standard_lib=False, prefix=None):
     """Return the directory containing the Python library (standard or
     site additions).
 
@@ -399,7 +399,11 @@ def parse_makefile(fn, g=None):  # noqa: C901
     from distutils.text_file import TextFile
 
     fp = TextFile(
-        fn, strip_comments=1, skip_blanks=1, join_lines=1, errors="surrogateescape"
+        fn,
+        strip_comments=True,
+        skip_blanks=True,
+        join_lines=True,
+        errors="surrogateescape",
     )
 
     if g is None:
diff --git a/distutils/tests/test_bdist.py b/distutils/tests/test_bdist.py
index 18048077..a06ac0e7 100644
--- a/distutils/tests/test_bdist.py
+++ b/distutils/tests/test_bdist.py
@@ -31,7 +31,7 @@ def test_skip_build(self):
         # bug #10946: bdist --skip-build should trickle down to subcommands
         dist = self.create_dist()[1]
         cmd = bdist(dist)
-        cmd.skip_build = 1
+        cmd.skip_build = True
         cmd.ensure_finalized()
         dist.command_obj['bdist'] = cmd
 
diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py
index a5cb42c3..0d3138ba 100644
--- a/distutils/tests/test_bdist_rpm.py
+++ b/distutils/tests/test_bdist_rpm.py
@@ -72,7 +72,7 @@ def test_quiet(self):
         cmd.fix_python = True
 
         # running in quiet mode
-        cmd.quiet = 1
+        cmd.quiet = True
         cmd.ensure_finalized()
         cmd.run()
 
@@ -114,7 +114,7 @@ def test_no_optimize_flag(self):
         cmd = bdist_rpm(dist)
         cmd.fix_python = True
 
-        cmd.quiet = 1
+        cmd.quiet = True
         cmd.ensure_finalized()
         cmd.run()
 
diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py
index cc83e7fb..0b8ff2d5 100644
--- a/distutils/tests/test_build_ext.py
+++ b/distutils/tests/test_build_ext.py
@@ -140,7 +140,7 @@ def test_solaris_enable_shared(self):
         from distutils.sysconfig import _config_vars
 
         old_var = _config_vars.get('Py_ENABLE_SHARED')
-        _config_vars['Py_ENABLE_SHARED'] = 1
+        _config_vars['Py_ENABLE_SHARED'] = True
         try:
             cmd.ensure_finalized()
         finally:
@@ -164,7 +164,7 @@ def test_user_site(self):
         assert 'user' in options
 
         # setting a value
-        cmd.user = 1
+        cmd.user = True
 
         # setting user based lib and include
         lib = os.path.join(site.USER_BASE, 'lib')
@@ -209,7 +209,7 @@ def test_finalize_options(self):
         for p in py_include.split(os.path.pathsep):
             assert p in cmd.include_dirs
 
-        plat_py_include = sysconfig.get_python_inc(plat_specific=1)
+        plat_py_include = sysconfig.get_python_inc(plat_specific=True)
         for p in plat_py_include.split(os.path.pathsep):
             assert p in cmd.include_dirs
 
@@ -381,7 +381,7 @@ def test_get_outputs(self):
         old_wd = os.getcwd()
         os.chdir(other_tmp_dir)
         try:
-            cmd.inplace = 1
+            cmd.inplace = True
             cmd.run()
             so_file = cmd.get_outputs()[0]
         finally:
@@ -392,7 +392,7 @@ def test_get_outputs(self):
         so_dir = os.path.dirname(so_file)
         assert so_dir == other_tmp_dir
 
-        cmd.inplace = 0
+        cmd.inplace = False
         cmd.compiler = None
         cmd.run()
         so_file = cmd.get_outputs()[0]
@@ -401,7 +401,7 @@ def test_get_outputs(self):
         so_dir = os.path.dirname(so_file)
         assert so_dir == cmd.build_lib
 
-        # inplace = 0, cmd.package = 'bar'
+        # inplace = False, cmd.package = 'bar'
         build_py = cmd.get_finalized_command('build_py')
         build_py.package_dir = {'': 'bar'}
         path = cmd.get_ext_fullpath('foo')
@@ -409,8 +409,8 @@ def test_get_outputs(self):
         path = os.path.split(path)[0]
         assert path == cmd.build_lib
 
-        # inplace = 1, cmd.package = 'bar'
-        cmd.inplace = 1
+        # inplace = True, cmd.package = 'bar'
+        cmd.inplace = True
         other_tmp_dir = os.path.realpath(self.mkdtemp())
         old_wd = os.getcwd()
         os.chdir(other_tmp_dir)
@@ -431,7 +431,7 @@ def test_ext_fullpath(self):
         # dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
         dist = Distribution()
         cmd = self.build_ext(dist)
-        cmd.inplace = 1
+        cmd.inplace = True
         cmd.distribution.package_dir = {'': 'src'}
         cmd.distribution.packages = ['lxml', 'lxml.html']
         curdir = os.getcwd()
@@ -440,7 +440,7 @@ def test_ext_fullpath(self):
         assert wanted == path
 
         # building lxml.etree not inplace
-        cmd.inplace = 0
+        cmd.inplace = False
         cmd.build_lib = os.path.join(curdir, 'tmpdir')
         wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext)
         path = cmd.get_ext_fullpath('lxml.etree')
@@ -455,7 +455,7 @@ def test_ext_fullpath(self):
         assert wanted == path
 
         # building twisted.runner.portmap inplace
-        cmd.inplace = 1
+        cmd.inplace = True
         path = cmd.get_ext_fullpath('twisted.runner.portmap')
         wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext)
         assert wanted == path
diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py
index 8bc0e98a..739c9036 100644
--- a/distutils/tests/test_build_py.py
+++ b/distutils/tests/test_build_py.py
@@ -28,13 +28,15 @@ def test_package_data(self):
         dist = Distribution({"packages": ["pkg"], "package_dir": {"pkg": sources}})
         # script_name need not exist, it just need to be initialized
         dist.script_name = os.path.join(sources, "setup.py")
-        dist.command_obj["build"] = support.DummyCommand(force=0, build_lib=destination)
+        dist.command_obj["build"] = support.DummyCommand(
+            force=False, build_lib=destination
+        )
         dist.packages = ["pkg"]
         dist.package_data = {"pkg": ["README.txt"]}
         dist.package_dir = {"pkg": sources}
 
         cmd = build_py(dist)
-        cmd.compile = 1
+        cmd.compile = True
         cmd.ensure_finalized()
         assert cmd.package_data == dist.package_data
 
@@ -82,7 +84,7 @@ def test_byte_compile(self):
         os.chdir(project_dir)
         self.write_file('boiledeggs.py', 'import antigravity')
         cmd = build_py(dist)
-        cmd.compile = 1
+        cmd.compile = True
         cmd.build_lib = 'here'
         cmd.finalize_options()
         cmd.run()
@@ -98,7 +100,7 @@ def test_byte_compile_optimized(self):
         os.chdir(project_dir)
         self.write_file('boiledeggs.py', 'import antigravity')
         cmd = build_py(dist)
-        cmd.compile = 0
+        cmd.compile = False
         cmd.optimize = 1
         cmd.build_lib = 'here'
         cmd.finalize_options()
@@ -146,7 +148,7 @@ def test_dont_write_bytecode(self, caplog):
         # makes sure byte_compile is not used
         dist = self.create_dist()[1]
         cmd = build_py(dist)
-        cmd.compile = 1
+        cmd.compile = True
         cmd.optimize = 1
 
         old_dont_write_bytecode = sys.dont_write_bytecode
diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py
index 208b1f6e..3582f691 100644
--- a/distutils/tests/test_build_scripts.py
+++ b/distutils/tests/test_build_scripts.py
@@ -42,7 +42,7 @@ def get_build_scripts_cmd(self, target, scripts):
         dist = Distribution()
         dist.scripts = scripts
         dist.command_obj["build"] = support.DummyCommand(
-            build_scripts=target, force=1, executable=sys.executable
+            build_scripts=target, force=True, executable=sys.executable
         )
         return build_scripts(dist)
 
diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py
index 580cb2a2..b672b1f9 100644
--- a/distutils/tests/test_check.py
+++ b/distutils/tests/test_check.py
@@ -62,7 +62,7 @@ def test_check_metadata(self):
             self._run({}, **{'strict': 1})
 
         # and of course, no error when all metadata are present
-        cmd = self._run(metadata, strict=1)
+        cmd = self._run(metadata, strict=True)
         assert cmd._warnings == 0
 
         # now a test with non-ASCII characters
@@ -126,7 +126,7 @@ def test_check_restructuredtext(self):
         cmd.check_restructuredtext()
         assert cmd._warnings == 1
 
-        # let's see if we have an error with strict=1
+        # let's see if we have an error with strict=True
         metadata = {
             'url': 'xxx',
             'author': 'xxx',
@@ -140,12 +140,12 @@ def test_check_restructuredtext(self):
 
         # and non-broken rest, including a non-ASCII character to test #12114
         metadata['long_description'] = 'title\n=====\n\ntest \u00df'
-        cmd = self._run(metadata, strict=1, restructuredtext=1)
+        cmd = self._run(metadata, strict=True, restructuredtext=True)
         assert cmd._warnings == 0
 
         # check that includes work to test #31292
         metadata['long_description'] = 'title\n=====\n\n.. include:: includetest.rst'
-        cmd = self._run(metadata, cwd=HERE, strict=1, restructuredtext=1)
+        cmd = self._run(metadata, cwd=HERE, strict=True, restructuredtext=True)
         assert cmd._warnings == 0
 
     def test_check_restructuredtext_with_syntax_highlight(self):
diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py
index f366aa65..76e8f598 100644
--- a/distutils/tests/test_cmd.py
+++ b/distutils/tests/test_cmd.py
@@ -48,7 +48,7 @@ def test_ensure_string_list(self, cmd):
     def test_make_file(self, cmd):
         # making sure it raises when infiles is not a string or a list/tuple
         with pytest.raises(TypeError):
-            cmd.make_file(infiles=1, outfile='', func='func', args=())
+            cmd.make_file(infiles=True, outfile='', func='func', args=())
 
         # making sure execute gets called properly
         def _execute(func, args, exec_msg, level):
diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py
index 84cda619..c8064cd0 100644
--- a/distutils/tests/test_dir_util.py
+++ b/distutils/tests/test_dir_util.py
@@ -29,16 +29,16 @@ def stuff(request, monkeypatch, distutils_managed_tempdir):
 
 class TestDirUtil(support.TempdirManager):
     def test_mkpath_remove_tree_verbosity(self, caplog):
-        mkpath(self.target, verbose=0)
+        mkpath(self.target, verbose=False)
         assert not caplog.records
-        remove_tree(self.root_target, verbose=0)
+        remove_tree(self.root_target, verbose=False)
 
-        mkpath(self.target, verbose=1)
+        mkpath(self.target, verbose=True)
         wanted = ['creating %s' % self.root_target, 'creating %s' % self.target]
         assert caplog.messages == wanted
         caplog.clear()
 
-        remove_tree(self.root_target, verbose=1)
+        remove_tree(self.root_target, verbose=True)
         wanted = ["removing '%s' (and everything under it)" % self.root_target]
         assert caplog.messages == wanted
 
@@ -53,45 +53,45 @@ def test_mkpath_with_custom_mode(self):
         assert stat.S_IMODE(os.stat(self.target2).st_mode) == 0o555 & ~umask
 
     def test_create_tree_verbosity(self, caplog):
-        create_tree(self.root_target, ['one', 'two', 'three'], verbose=0)
+        create_tree(self.root_target, ['one', 'two', 'three'], verbose=False)
         assert caplog.messages == []
-        remove_tree(self.root_target, verbose=0)
+        remove_tree(self.root_target, verbose=False)
 
         wanted = ['creating %s' % self.root_target]
-        create_tree(self.root_target, ['one', 'two', 'three'], verbose=1)
+        create_tree(self.root_target, ['one', 'two', 'three'], verbose=True)
         assert caplog.messages == wanted
 
-        remove_tree(self.root_target, verbose=0)
+        remove_tree(self.root_target, verbose=False)
 
     def test_copy_tree_verbosity(self, caplog):
-        mkpath(self.target, verbose=0)
+        mkpath(self.target, verbose=False)
 
-        copy_tree(self.target, self.target2, verbose=0)
+        copy_tree(self.target, self.target2, verbose=False)
         assert caplog.messages == []
 
-        remove_tree(self.root_target, verbose=0)
+        remove_tree(self.root_target, verbose=False)
 
-        mkpath(self.target, verbose=0)
+        mkpath(self.target, verbose=False)
         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)
+        copy_tree(self.target, self.target2, verbose=True)
         assert caplog.messages == wanted
 
-        remove_tree(self.root_target, verbose=0)
-        remove_tree(self.target2, verbose=0)
+        remove_tree(self.root_target, verbose=False)
+        remove_tree(self.target2, verbose=False)
 
     def test_copy_tree_skips_nfs_temp_files(self):
-        mkpath(self.target, verbose=0)
+        mkpath(self.target, verbose=False)
 
         jaraco.path.build({'ok.txt': 'some content', '.nfs123abc': ''}, self.target)
 
         copy_tree(self.target, self.target2)
         assert os.listdir(self.target2) == ['ok.txt']
 
-        remove_tree(self.root_target, verbose=0)
-        remove_tree(self.target2, verbose=0)
+        remove_tree(self.root_target, verbose=False)
+        remove_tree(self.target2, verbose=False)
 
     def test_ensure_relative(self):
         if os.sep == '/':
diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py
index 9ed4d16d..5bd206fe 100644
--- a/distutils/tests/test_dist.py
+++ b/distutils/tests/test_dist.py
@@ -468,7 +468,7 @@ def test_show_help(self, request, capsys):
         # smoke test, just makes sure some help is displayed
         dist = Distribution()
         sys.argv = []
-        dist.help = 1
+        dist.help = True
         dist.script_name = 'setup.py'
         dist.parse_command_line()
 
diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py
index 4c2abd24..420dc348 100644
--- a/distutils/tests/test_file_util.py
+++ b/distutils/tests/test_file_util.py
@@ -22,23 +22,23 @@ class TestFileUtil:
     def test_move_file_verbosity(self, caplog):
         jaraco.path.build({self.source: 'some content'})
 
-        move_file(self.source, self.target, verbose=0)
+        move_file(self.source, self.target, verbose=False)
         assert not caplog.messages
 
         # back to original state
-        move_file(self.target, self.source, verbose=0)
+        move_file(self.target, self.source, verbose=False)
 
-        move_file(self.source, self.target, verbose=1)
+        move_file(self.source, self.target, verbose=True)
         wanted = [f'moving {self.source} -> {self.target}']
         assert caplog.messages == wanted
 
         # back to original state
-        move_file(self.target, self.source, verbose=0)
+        move_file(self.target, self.source, verbose=False)
 
         caplog.clear()
         # now the target is a dir
         os.mkdir(self.target_dir)
-        move_file(self.source, self.target_dir, verbose=1)
+        move_file(self.source, self.target_dir, verbose=True)
         wanted = [f'moving {self.source} -> {self.target_dir}']
         assert caplog.messages == wanted
 
@@ -48,7 +48,7 @@ def test_move_file_exception_unpacking_rename(self):
             DistutilsFileError
         ):
             jaraco.path.build({self.source: 'spam eggs'})
-            move_file(self.source, self.target, verbose=0)
+            move_file(self.source, self.target, verbose=False)
 
     def test_move_file_exception_unpacking_unlink(self):
         # see issue 22182
@@ -58,7 +58,7 @@ def test_move_file_exception_unpacking_unlink(self):
             DistutilsFileError
         ):
             jaraco.path.build({self.source: 'spam eggs'})
-            move_file(self.source, self.target, verbose=0)
+            move_file(self.source, self.target, verbose=False)
 
     def test_copy_file_hard_link(self):
         jaraco.path.build({self.source: 'some content'})
diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py
index 08f0f839..8a0c5647 100644
--- a/distutils/tests/test_install.py
+++ b/distutils/tests/test_install.py
@@ -100,7 +100,7 @@ def _expanduser(path):
         assert 'user' in options
 
         # setting a value
-        cmd.user = 1
+        cmd.user = True
 
         # user base and site shouldn't be created yet
         assert not os.path.exists(site.USER_BASE)
diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py
index e453d01f..f34070b1 100644
--- a/distutils/tests/test_install_data.py
+++ b/distutils/tests/test_install_data.py
@@ -41,7 +41,7 @@ def test_simple_run(self):
         cmd.outfiles = []
 
         # let's try with warn_dir one
-        cmd.warn_dir = 1
+        cmd.warn_dir = True
         cmd.ensure_finalized()
         cmd.run()
 
diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py
index 964106fa..f685a579 100644
--- a/distutils/tests/test_install_lib.py
+++ b/distutils/tests/test_install_lib.py
@@ -97,7 +97,7 @@ def test_dont_write_bytecode(self, caplog):
         # makes sure byte_compile is not used
         dist = self.create_dist()[1]
         cmd = install_lib(dist)
-        cmd.compile = 1
+        cmd.compile = True
         cmd.optimize = 1
 
         old_dont_write_bytecode = sys.dont_write_bytecode
diff --git a/distutils/tests/test_install_scripts.py b/distutils/tests/test_install_scripts.py
index 5d9f13a4..868b1c22 100644
--- a/distutils/tests/test_install_scripts.py
+++ b/distutils/tests/test_install_scripts.py
@@ -14,8 +14,8 @@ def test_default_settings(self):
         dist.command_obj["build"] = support.DummyCommand(build_scripts="/foo/bar")
         dist.command_obj["install"] = support.DummyCommand(
             install_scripts="/splat/funk",
-            force=1,
-            skip_build=1,
+            force=True,
+            skip_build=True,
         )
         cmd = install_scripts(dist)
         assert not cmd.force
@@ -40,8 +40,8 @@ def test_installation(self):
         dist.command_obj["build"] = support.DummyCommand(build_scripts=source)
         dist.command_obj["install"] = support.DummyCommand(
             install_scripts=target,
-            force=1,
-            skip_build=1,
+            force=True,
+            skip_build=True,
         )
         cmd = install_scripts(dist)
         cmd.finalize_options()
diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py
index d071bbe9..14dfb832 100644
--- a/distutils/tests/test_register.py
+++ b/distutils/tests/test_register.py
@@ -137,7 +137,7 @@ def _no_way(prompt=''):
 
         register_module.input = _no_way
 
-        cmd.show_response = 1
+        cmd.show_response = True
         cmd.run()
 
         # let's see what the server received : we should
@@ -208,7 +208,7 @@ def test_strict(self):
         # empty metadata
         cmd = self._get_cmd({})
         cmd.ensure_finalized()
-        cmd.strict = 1
+        cmd.strict = True
         with pytest.raises(DistutilsSetupError):
             cmd.run()
 
@@ -224,7 +224,7 @@ def test_strict(self):
 
         cmd = self._get_cmd(metadata)
         cmd.ensure_finalized()
-        cmd.strict = 1
+        cmd.strict = True
         with pytest.raises(DistutilsSetupError):
             cmd.run()
 
@@ -232,7 +232,7 @@ def test_strict(self):
         metadata['long_description'] = 'title\n=====\n\ntext'
         cmd = self._get_cmd(metadata)
         cmd.ensure_finalized()
-        cmd.strict = 1
+        cmd.strict = True
         inputs = Inputs('1', 'tarek', 'y')
         register_module.input = inputs.__call__
         # let's run the command
@@ -265,7 +265,7 @@ def test_strict(self):
 
         cmd = self._get_cmd(metadata)
         cmd.ensure_finalized()
-        cmd.strict = 1
+        cmd.strict = True
         inputs = Inputs('1', 'tarek', 'y')
         register_module.input = inputs.__call__
         # let's run the command
@@ -296,7 +296,7 @@ def test_register_invalid_long_description(self, monkeypatch):
 
     def test_list_classifiers(self, caplog):
         cmd = self._get_cmd()
-        cmd.list_classifiers = 1
+        cmd.list_classifiers = True
         cmd.run()
         assert caplog.messages == ['running check', 'xxx']
 
@@ -305,7 +305,7 @@ def test_show_response(self, caplog):
         cmd = self._get_cmd()
         inputs = Inputs('1', 'tarek', 'y')
         register_module.input = inputs.__call__
-        cmd.show_response = 1
+        cmd.show_response = True
         try:
             cmd.run()
         finally:
diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py
index c5c910a8..f5111565 100644
--- a/distutils/tests/test_text_file.py
+++ b/distutils/tests/test_text_file.py
@@ -60,7 +60,11 @@ def test_input(count, description, file, expected_result):
         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
+            filename,
+            strip_comments=False,
+            skip_blanks=False,
+            lstrip_ws=False,
+            rstrip_ws=False,
         )
         try:
             test_input(1, "no processing", in_file, result1)
@@ -68,7 +72,11 @@ def test_input(count, description, file, expected_result):
             in_file.close()
 
         in_file = TextFile(
-            filename, strip_comments=1, skip_blanks=0, lstrip_ws=0, rstrip_ws=0
+            filename,
+            strip_comments=True,
+            skip_blanks=False,
+            lstrip_ws=False,
+            rstrip_ws=False,
         )
         try:
             test_input(2, "strip comments", in_file, result2)
@@ -76,7 +84,11 @@ def test_input(count, description, file, expected_result):
             in_file.close()
 
         in_file = TextFile(
-            filename, strip_comments=0, skip_blanks=1, lstrip_ws=0, rstrip_ws=0
+            filename,
+            strip_comments=False,
+            skip_blanks=True,
+            lstrip_ws=False,
+            rstrip_ws=False,
         )
         try:
             test_input(3, "strip blanks", in_file, result3)
@@ -90,7 +102,11 @@ def test_input(count, description, file, expected_result):
             in_file.close()
 
         in_file = TextFile(
-            filename, strip_comments=1, skip_blanks=1, join_lines=1, rstrip_ws=1
+            filename,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            rstrip_ws=True,
         )
         try:
             test_input(5, "join lines without collapsing", in_file, result5)
@@ -99,11 +115,11 @@ def test_input(count, description, file, expected_result):
 
         in_file = TextFile(
             filename,
-            strip_comments=1,
-            skip_blanks=1,
-            join_lines=1,
-            rstrip_ws=1,
-            collapse_join=1,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            rstrip_ws=True,
+            collapse_join=True,
         )
         try:
             test_input(6, "join lines with collapsing", in_file, result6)
diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py
index 0692f001..56df209c 100644
--- a/distutils/tests/test_upload.py
+++ b/distutils/tests/test_upload.py
@@ -117,7 +117,7 @@ def test_upload(self, caplog):
         # lets run it
         pkg_dir, dist = self.create_dist(dist_files=dist_files)
         cmd = upload(dist)
-        cmd.show_response = 1
+        cmd.show_response = True
         cmd.ensure_finalized()
         cmd.run()
 
@@ -167,7 +167,7 @@ def test_upload_correct_cr(self):
             dist_files=dist_files, description='long description\r'
         )
         cmd = upload(dist)
-        cmd.show_response = 1
+        cmd.show_response = True
         cmd.ensure_finalized()
         cmd.run()
 
diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index da97688c..df622cd4 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -190,7 +190,7 @@ def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
             raise CompileError(msg)
 
     def create_static_lib(
-        self, objects, output_libname, output_dir=None, debug=0, target_lang=None
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
     ):
         objects, output_dir = self._fix_object_args(objects, output_dir)
 
@@ -223,7 +223,7 @@ def link(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
@@ -362,7 +362,7 @@ def _library_root(dir):
 
         return os.path.join(match.group(1), dir[1:]) if apply_root else dir
 
-    def find_library_file(self, dirs, lib, debug=0):
+    def find_library_file(self, dirs, lib, debug=False):
         r"""
         Second-guess the linker with not much hard
         data to go on: GCC seems to prefer the shared library, so
diff --git a/distutils/util.py b/distutils/util.py
index 2cdea143..ef332ba9 100644
--- a/distutils/util.py
+++ b/distutils/util.py
@@ -311,7 +311,7 @@ def split_quoted(s):
 # split_quoted ()
 
 
-def execute(func, args, msg=None, verbose=0, dry_run=0):
+def execute(func, args, msg=None, verbose=False, dry_run=False):
     """Perform some action that affects the outside world (eg.  by
     writing to the filesystem).  Such actions are special because they
     are disabled by the 'dry_run' flag.  This method takes care of all
@@ -349,11 +349,11 @@ def strtobool(val):
 def byte_compile(  # noqa: C901
     py_files,
     optimize=0,
-    force=0,
+    force=False,
     prefix=None,
     base_dir=None,
-    verbose=1,
-    dry_run=0,
+    verbose=True,
+    dry_run=False,
     direct=None,
 ):
     """Byte-compile a collection of Python source files to .pyc
@@ -443,8 +443,8 @@ def byte_compile(  # noqa: C901
                     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)
+             verbose={verbose!r}, dry_run=False,
+             direct=True)
 """
                 )
 
diff --git a/distutils/zosccompiler.py b/distutils/zosccompiler.py
index c7a7ca61..af1e7fa5 100644
--- a/distutils/zosccompiler.py
+++ b/distutils/zosccompiler.py
@@ -135,7 +135,7 @@ def _get_zos_compiler_name(self):
 
         return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl')
 
-    def __init__(self, verbose=0, dry_run=0, force=0):
+    def __init__(self, verbose=False, dry_run=False, force=False):
         super().__init__(verbose, dry_run, force)
         self.zos_compiler = self._get_zos_compiler_name()
         sysconfig.customize_compiler(self)
@@ -172,7 +172,7 @@ def link(
         library_dirs=None,
         runtime_library_dirs=None,
         export_symbols=None,
-        debug=0,
+        debug=False,
         extra_preargs=None,
         extra_postargs=None,
         build_temp=None,
diff --git a/docs/distutils/apiref.rst b/docs/distutils/apiref.rst
index beb17bc3..709186ad 100644
--- a/docs/distutils/apiref.rst
+++ b/docs/distutils/apiref.rst
@@ -361,7 +361,7 @@ This module provides the following functions.
    are not given.
 
 
-.. function:: new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0)
+.. function:: new_compiler(plat=None, compiler=None, verbose=False, dry_run=False, force=False)
 
    Factory function to generate an instance of some CCompiler subclass for the
    supplied platform/compiler combination. *plat* defaults to ``os.name`` (eg.
@@ -383,7 +383,7 @@ This module provides the following functions.
    to :command:`build`, :command:`build_ext`, :command:`build_clib`).
 
 
-.. class:: CCompiler([verbose=0, dry_run=0, force=0])
+.. class:: CCompiler([verbose=False, dry_run=False, force=False])
 
    The abstract base class :class:`CCompiler` defines the interface that  must be
    implemented by real compiler classes.  The class also has  some utility methods
@@ -517,7 +517,7 @@ This module provides the following functions.
       list) to do the job.
 
 
-   .. method:: CCompiler.find_library_file(dirs, lib[, debug=0])
+   .. method:: CCompiler.find_library_file(dirs, lib[, debug=False])
 
       Search the specified list of directories for a static or shared library file
       *lib* and return the full path to that file.  If *debug* is true, look for a
@@ -580,7 +580,7 @@ This module provides the following functions.
    The following methods invoke stages in the build process.
 
 
-   .. method:: CCompiler.compile(sources[, output_dir=None, macros=None, include_dirs=None, debug=0, extra_preargs=None, extra_postargs=None, depends=None])
+   .. method:: CCompiler.compile(sources[, output_dir=None, macros=None, include_dirs=None, debug=False, extra_preargs=None, extra_postargs=None, depends=None])
 
       Compile one or more source files. Generates object files (e.g.  transforms a
       :file:`.c` file to a :file:`.o` file.)
@@ -624,7 +624,7 @@ This module provides the following functions.
       Raises :exc:`CompileError` on failure.
 
 
-   .. method:: CCompiler.create_static_lib(objects, output_libname[, output_dir=None, debug=0, target_lang=None])
+   .. method:: CCompiler.create_static_lib(objects, output_libname[, output_dir=None, debug=False, target_lang=None])
 
       Link a bunch of stuff together to create a static library file. The "bunch of
       stuff" consists of the list of object files supplied as *objects*, the extra
@@ -648,7 +648,7 @@ This module provides the following functions.
       Raises :exc:`LibError` on failure.
 
 
-   .. method:: CCompiler.link(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])
+   .. method:: CCompiler.link(target_desc, objects, output_filename[, output_dir=None, libraries=None, library_dirs=None, runtime_library_dirs=None, export_symbols=None, debug=False, extra_preargs=None, extra_postargs=None, build_temp=None, target_lang=None])
 
       Link a bunch of stuff together to create an executable or shared library file.
 
@@ -690,21 +690,21 @@ This module provides the following functions.
       Raises :exc:`LinkError` on failure.
 
 
-   .. method:: CCompiler.link_executable(objects, output_progname[, output_dir=None, libraries=None, library_dirs=None, runtime_library_dirs=None, debug=0, extra_preargs=None, extra_postargs=None, target_lang=None])
+   .. method:: CCompiler.link_executable(objects, output_progname[, output_dir=None, libraries=None, library_dirs=None, runtime_library_dirs=None, debug=False, extra_preargs=None, extra_postargs=None, target_lang=None])
 
       Link an executable.  *output_progname* is the name of the file executable, while
       *objects* are a list of object filenames to link in. Other arguments  are as for
       the :meth:`link` method.
 
 
-   .. method:: CCompiler.link_shared_lib(objects, output_libname[, 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])
+   .. method:: CCompiler.link_shared_lib(objects, output_libname[, output_dir=None, libraries=None, library_dirs=None, runtime_library_dirs=None, export_symbols=None, debug=False, extra_preargs=None, extra_postargs=None, build_temp=None, target_lang=None])
 
       Link a shared library. *output_libname* is the name of the output  library,
       while *objects* is a list of object filenames to link in.  Other arguments are
       as for the :meth:`link` method.
 
 
-   .. method:: CCompiler.link_shared_object(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])
+   .. method:: CCompiler.link_shared_object(objects, output_filename[, output_dir=None, libraries=None, library_dirs=None, runtime_library_dirs=None, export_symbols=None, debug=False, extra_preargs=None, extra_postargs=None, build_temp=None, target_lang=None])
 
       Link a shared object. *output_filename* is the name of the shared object that
       will be created, while *objects* is a list of object filenames  to link in.
@@ -726,14 +726,14 @@ This module provides the following functions.
    use by the various concrete subclasses.
 
 
-   .. method:: CCompiler.executable_filename(basename[, strip_dir=0, output_dir=''])
+   .. method:: CCompiler.executable_filename(basename[, strip_dir=False, output_dir=''])
 
       Returns the filename of the executable for the given *basename*.  Typically for
       non-Windows platforms this is the same as the basename,  while Windows will get
       a :file:`.exe` added.
 
 
-   .. method:: CCompiler.library_filename(libname[, lib_type='static', strip_dir=0, output_dir=''])
+   .. method:: CCompiler.library_filename(libname[, lib_type='static', strip_dir=False, output_dir=''])
 
       Returns the filename for the given library name on the current platform. On Unix
       a library with *lib_type* of ``'static'`` will typically  be of the form
@@ -741,13 +741,13 @@ This module provides the following functions.
       :file:`liblibname.so`.
 
 
-   .. method:: CCompiler.object_filenames(source_filenames[, strip_dir=0, output_dir=''])
+   .. method:: CCompiler.object_filenames(source_filenames[, strip_dir=False, output_dir=''])
 
       Returns the name of the object files for the given source files.
       *source_filenames* should be a list of filenames.
 
 
-   .. method:: CCompiler.shared_object_filename(basename[, strip_dir=0, output_dir=''])
+   .. method:: CCompiler.shared_object_filename(basename[, strip_dir=False, output_dir=''])
 
       Returns the name of a shared object file for the given file name *basename*.
 
@@ -884,7 +884,7 @@ This module provides a few functions for creating archive files, such as
 tarballs or zipfiles.
 
 
-.. function:: make_archive(base_name, format[, root_dir=None, base_dir=None, verbose=0, dry_run=0])
+.. function:: make_archive(base_name, format[, root_dir=None, base_dir=None, verbose=False, dry_run=False])
 
    Create an archive file (eg. ``zip`` or ``tar``).  *base_name*  is the name of
    the file to create, minus any format-specific extension;  *format* is the
@@ -900,7 +900,7 @@ tarballs or zipfiles.
       Added support for the ``xztar`` format.
 
 
-.. function:: make_tarball(base_name, base_dir[, compress='gzip', verbose=0, dry_run=0])
+.. function:: make_tarball(base_name, base_dir[, compress='gzip', verbose=False, dry_run=False])
 
    'Create an (optional compressed) archive as a tar file from all files in and
    under *base_dir*. *compress* must be ``'gzip'`` (the default),
@@ -915,7 +915,7 @@ tarballs or zipfiles.
       Added support for the ``xz`` compression.
 
 
-.. function:: make_zipfile(base_name, base_dir[, verbose=0, dry_run=0])
+.. function:: make_zipfile(base_name, base_dir[, verbose=False, dry_run=False])
 
    Create a zip file from all files in and under *base_dir*.  The output zip file
    will be named *base_name* + :file:`.zip`.  Uses either the  :mod:`zipfile` Python
@@ -978,7 +978,7 @@ This module provides functions for operating on directories and trees of
 directories.
 
 
-.. function:: mkpath(name[, mode=0o777, verbose=0, dry_run=0])
+.. function:: mkpath(name[, mode=0o777, verbose=False, dry_run=False])
 
    Create a directory and any missing ancestor directories.  If the directory
    already exists (or if *name* is the empty string, which means the current
@@ -989,7 +989,7 @@ directories.
    directories actually created.
 
 
-.. function:: create_tree(base_dir, files[, mode=0o777, verbose=0, dry_run=0])
+.. function:: create_tree(base_dir, files[, mode=0o777, verbose=False, dry_run=False])
 
    Create all the empty directories under *base_dir* needed to put *files* there.
    *base_dir* is just the name of a directory which doesn't necessarily exist
@@ -999,7 +999,7 @@ directories.
    :func:`mkpath`.
 
 
-.. function:: copy_tree(src, dst[, preserve_mode=1, preserve_times=1, preserve_symlinks=0, update=0, verbose=0, dry_run=0])
+.. function:: copy_tree(src, dst[, preserve_mode=True, preserve_times=True, preserve_symlinks=False, update=False, verbose=False, dry_run=False])
 
    Copy an entire directory tree *src* to a new location *dst*.  Both *src* and
    *dst* must be directory names.  If *src* is not a directory, raise
@@ -1026,7 +1026,7 @@ directories.
    .. versionchanged:: 3.3.1
       NFS files are ignored.
 
-.. function:: remove_tree(directory[, verbose=0, dry_run=0])
+.. function:: remove_tree(directory[, verbose=False, dry_run=False])
 
    Recursively remove *directory* and all files and directories underneath it. Any
    errors are ignored (apart from being reported to ``sys.stdout`` if *verbose* is
@@ -1043,7 +1043,7 @@ directories.
 This module contains some utility functions for operating on individual files.
 
 
-.. function:: copy_file(src, dst[, preserve_mode=1, preserve_times=1, update=0, link=None, verbose=0, dry_run=0])
+.. function:: copy_file(src, dst[, preserve_mode=True, preserve_times=True, update=False, link=None, verbose=False, dry_run=False])
 
    Copy file *src* to *dst*. If *dst* is a directory, then *src* is copied there
    with the same name; otherwise, it must be a filename. (If the file exists, it
@@ -1216,7 +1216,7 @@ other utility module.
    .. % Should probably be moved into the standard library.
 
 
-.. function:: execute(func, args[, msg=None, verbose=0, dry_run=0])
+.. function:: execute(func, args[, msg=None, verbose=False, dry_run=False])
 
    Perform some action that affects the outside world (for instance, writing to the
    filesystem).  Such actions are special because they are disabled by the
@@ -1234,7 +1234,7 @@ other utility module.
    :exc:`ValueError` if *val*  is anything else.
 
 
-.. function:: byte_compile(py_files[, optimize=0, force=0, prefix=None, base_dir=None, verbose=1, dry_run=0, direct=None])
+.. function:: byte_compile(py_files[, optimize=0, force=False, prefix=None, base_dir=None, verbose=True, dry_run=False, direct=None])
 
    Byte-compile a collection of Python source files to :file:`.pyc` files in a
    :file:`__pycache__` subdirectory (see :pep:`3147` and :pep:`488`).
diff --git a/docs/distutils/configfile.rst b/docs/distutils/configfile.rst
index 30cccd71..f6cb1840 100644
--- a/docs/distutils/configfile.rst
+++ b/docs/distutils/configfile.rst
@@ -96,7 +96,7 @@ configuration file for this distribution:
 .. code-block:: ini
 
    [build_ext]
-   inplace=1
+   inplace=true
 
 This will affect all builds of this module distribution, whether or not you
 explicitly specify :command:`build_ext`.  If you include :file:`setup.cfg` in
diff --git a/docs/distutils/setupscript.rst b/docs/distutils/setupscript.rst
index 71d2439f..825a6aa9 100644
--- a/docs/distutils/setupscript.rst
+++ b/docs/distutils/setupscript.rst
@@ -273,7 +273,7 @@ search path, though, you can find that directory using the Distutils
 :mod:`distutils.sysconfig` module::
 
     from distutils.sysconfig import get_python_inc
-    incdir = os.path.join(get_python_inc(plat_specific=1), 'Numerical')
+    incdir = os.path.join(get_python_inc(plat_specific=True), 'Numerical')
     setup(...,
           Extension(..., include_dirs=[incdir]),
           )

From 227174d9bcd87a6900c791f98ec87024604fd905 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 18:57:12 +0200
Subject: [PATCH 198/221] Apply ruff rule RUF100

RUF100 Unused `noqa` directive
---
 distutils/bcppcompiler.py             | 2 +-
 distutils/command/__init__.py         | 2 +-
 distutils/msvccompiler.py             | 2 +-
 distutils/tests/test_unixccompiler.py | 2 +-
 ruff.toml                             | 1 +
 5 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py
index c1341e43..d1af267d 100644
--- a/distutils/bcppcompiler.py
+++ b/distutils/bcppcompiler.py
@@ -84,7 +84,7 @@ def __init__(self, verbose=0, dry_run=0, force=0):
 
     # -- Worker methods ------------------------------------------------
 
-    def compile(  # noqa: C901
+    def compile(
         self,
         sources,
         output_dir=None,
diff --git a/distutils/command/__init__.py b/distutils/command/__init__.py
index 028dcfa0..1e8fbe60 100644
--- a/distutils/command/__init__.py
+++ b/distutils/command/__init__.py
@@ -3,7 +3,7 @@
 Package containing implementation of all the standard Distutils
 commands."""
 
-__all__ = [  # noqa: F822
+__all__ = [
     'build',
     'build_py',
     'build_ext',
diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py
index ac8b68c0..05ef01fa 100644
--- a/distutils/msvccompiler.py
+++ b/distutils/msvccompiler.py
@@ -684,6 +684,6 @@ def set_path_env_var(self, name):
     OldMSVCCompiler = MSVCCompiler
     # get_build_architecture not really relevant now we support cross-compile
     from distutils.msvc9compiler import (
-        MacroExpander,  # noqa: F811
+        MacroExpander,
         MSVCCompiler,
     )
diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index 543aa20d..d2c88e91 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -32,7 +32,7 @@ def rpath_foo(self):
 
 
 class TestUnixCCompiler(support.TempdirManager):
-    @pytest.mark.skipif('platform.system == "Windows"')  # noqa: C901
+    @pytest.mark.skipif('platform.system == "Windows"')
     def test_runtime_libdir_option(self):  # noqa: C901
         # Issue #5900; GitHub Issue #37
         #
diff --git a/ruff.toml b/ruff.toml
index 70612985..f2c67aeb 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,6 +1,7 @@
 [lint]
 extend-select = [
 	"C901",
+	"RUF100",
 	"W",
 ]
 ignore = [

From 3494fb0ddd84cf6bb9ebf589d83df9b89cad5bd1 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:03:41 +0200
Subject: [PATCH 199/221] Apply ruff/flake8-implicit-str-concat rule ISC001

ISC001 Implicitly concatenated string literals on one line
---
 distutils/command/bdist_rpm.py | 2 +-
 distutils/command/register.py  | 2 +-
 distutils/fancy_getopt.py      | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index 649968a5..df96507e 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -529,7 +529,7 @@ def _make_spec_file(self):  # noqa: C901
         # are just text that we drop in as-is.  Hmmm.
 
         install_cmd = (
-            '%s install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'
+            '%s install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES'
         ) % def_setup_call
 
         script_options = [
diff --git a/distutils/command/register.py b/distutils/command/register.py
index ee6c54da..1ae0e25e 100644
--- a/distutils/command/register.py
+++ b/distutils/command/register.py
@@ -225,7 +225,7 @@ def send_metadata(self):  # noqa: C901
                 log.info('Server response (%s): %s', code, result)
             else:
                 log.info('You will receive an email shortly.')
-                log.info('Follow the instructions in it to ' 'complete registration.')
+                log.info('Follow the instructions in it to complete registration.')
         elif choice == '3':
             data = {':action': 'password_reset'}
             data['email'] = ''
diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py
index e905aede..94a63217 100644
--- a/distutils/fancy_getopt.py
+++ b/distutils/fancy_getopt.py
@@ -118,11 +118,11 @@ def _check_alias_dict(self, aliases, what):
         for alias, opt in aliases.items():
             if alias not in self.option_index:
                 raise DistutilsGetoptError(
-                    f"invalid {what} '{alias}': " f"option '{alias}' not defined"
+                    f"invalid {what} '{alias}': option '{alias}' not defined"
                 )
             if opt not in self.option_index:
                 raise DistutilsGetoptError(
-                    f"invalid {what} '{alias}': " f"aliased option '{opt}' not defined"
+                    f"invalid {what} '{alias}': aliased option '{opt}' not defined"
                 )
 
     def set_aliases(self, alias):

From b967476b891361467ba7cd67df58cd6087587180 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:06:09 +0200
Subject: [PATCH 200/221] Apply ruff/flake8-implicit-str-concat rule ISC003

ISC003 Explicitly concatenated string should be implicitly concatenated
---
 distutils/command/bdist_dumb.py | 3 +--
 distutils/command/build.py      | 3 +--
 distutils/command/build_ext.py  | 2 +-
 distutils/command/install.py    | 9 +++------
 distutils/dist.py               | 3 +--
 5 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py
index 06502d20..5966e17e 100644
--- a/distutils/command/bdist_dumb.py
+++ b/distutils/command/bdist_dumb.py
@@ -33,8 +33,7 @@ class bdist_dumb(Command):
         (
             'keep-temp',
             'k',
-            "keep the pseudo-installation tree around after "
-            + "creating the distribution archive",
+            "keep the pseudo-installation tree around after creating the distribution archive",
         ),
         ('dist-dir=', 'd', "directory to put final built distributions in"),
         ('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
diff --git a/distutils/command/build.py b/distutils/command/build.py
index d18ed503..3d896d4d 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -26,8 +26,7 @@ class build(Command):
         (
             'build-lib=',
             None,
-            "build directory for all distribution (defaults to either "
-            + "build-purelib or build-platlib",
+            "build directory for all distribution (defaults to either build-purelib or build-platlib",
         ),
         ('build-scripts=', None, "build directory for scripts"),
         ('build-temp=', 't', "temporary build directory"),
diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py
index 06d949af..b80cfdf7 100644
--- a/distutils/command/build_ext.py
+++ b/distutils/command/build_ext.py
@@ -71,7 +71,7 @@ class build_ext(Command):
             'inplace',
             'i',
             "ignore build-lib and put compiled extensions into the source "
-            + "directory alongside your pure Python modules",
+            "directory alongside your pure Python modules",
         ),
         (
             'include-dirs=',
diff --git a/distutils/command/install.py b/distutils/command/install.py
index 8e920be4..e73d388d 100644
--- a/distutils/command/install.py
+++ b/distutils/command/install.py
@@ -193,8 +193,7 @@ class install(Command):
         (
             'install-platbase=',
             None,
-            "base installation directory for platform-specific files "
-            + "(instead of --exec-prefix or --home)",
+            "base installation directory for platform-specific files (instead of --exec-prefix or --home)",
         ),
         ('root=', None, "install everything relative to this alternate root directory"),
         # Or, explicitly set the installation scheme
@@ -211,8 +210,7 @@ class install(Command):
         (
             'install-lib=',
             None,
-            "installation directory for all module distributions "
-            + "(overrides --install-purelib and --install-platlib)",
+            "installation directory for all module distributions (overrides --install-purelib and --install-platlib)",
         ),
         ('install-headers=', None, "installation directory for C/C++ headers"),
         ('install-scripts=', None, "installation directory for Python scripts"),
@@ -348,8 +346,7 @@ def finalize_options(self):  # noqa: C901
             self.install_base or self.install_platbase
         ):
             raise DistutilsOptionError(
-                "must supply either prefix/exec-prefix/home or "
-                + "install-base/install-platbase -- not both"
+                "must supply either prefix/exec-prefix/home or install-base/install-platbase -- not both"
             )
 
         if self.home and (self.prefix or self.exec_prefix):
diff --git a/distutils/dist.py b/distutils/dist.py
index 668ce7eb..1fd20d8c 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -652,8 +652,7 @@ def _show_help(
         if display_options:
             parser.set_option_table(self.display_options)
             parser.print_help(
-                "Information display options (just display "
-                + "information, ignore any commands)"
+                "Information display options (just display information, ignore any commands)"
             )
             print()
 

From 41f9d80e962dabe52ed65bb07cc7e64018bfb624 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:06:54 +0200
Subject: [PATCH 201/221] Enable ruff/flake8-implicit-str-concat rules (ISC)

---
 ruff.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ruff.toml b/ruff.toml
index 70612985..e70a65b9 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,6 +1,7 @@
 [lint]
 extend-select = [
 	"C901",
+	"ISC",
 	"W",
 ]
 ignore = [

From 9bf2d70dc3743ffd56dc8997db0a9b9e24e1384c Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:29:44 +0200
Subject: [PATCH 202/221] Apply ruff rule RUF010

RUF010 Use explicit conversion flag
---
 distutils/command/bdist_dumb.py | 2 +-
 distutils/command/bdist_rpm.py  | 2 +-
 distutils/dist.py               | 2 +-
 distutils/version.py            | 6 +++---
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py
index 06502d20..b103516d 100644
--- a/distutils/command/bdist_dumb.py
+++ b/distutils/command/bdist_dumb.py
@@ -116,7 +116,7 @@ def run(self):
             ):
                 raise DistutilsPlatformError(
                     "can't make a dumb built distribution where "
-                    f"base and platbase are different ({repr(install.install_base)}, {repr(install.install_platbase)})"
+                    f"base and platbase are different ({install.install_base!r}, {install.install_platbase!r})"
                 )
             else:
                 archive_root = os.path.join(
diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index 649968a5..64a29a24 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -370,7 +370,7 @@ def run(self):  # noqa: C901
 
             status = out.close()
             if status:
-                raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd))
+                raise DistutilsExecError("Failed to execute: %r" % q_cmd)
 
         finally:
             out.close()
diff --git a/distutils/dist.py b/distutils/dist.py
index 668ce7eb..f1f8db9a 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -262,7 +262,7 @@ def __init__(self, attrs=None):  # noqa: C901
                 elif hasattr(self, key):
                     setattr(self, key, val)
                 else:
-                    msg = "Unknown distribution option: %s" % repr(key)
+                    msg = "Unknown distribution option: %r" % key
                     warnings.warn(msg)
 
         # no-user-cfg is handled before other command line args
diff --git a/distutils/version.py b/distutils/version.py
index 806d233c..942b56bf 100644
--- a/distutils/version.py
+++ b/distutils/version.py
@@ -60,7 +60,7 @@ def __init__(self, vstring=None):
         )
 
     def __repr__(self):
-        return f"{self.__class__.__name__} ('{str(self)}')"
+        return f"{self.__class__.__name__} ('{self}')"
 
     def __eq__(self, other):
         c = self._cmp(other)
@@ -153,7 +153,7 @@ class StrictVersion(Version):
     def parse(self, vstring):
         match = self.version_re.match(vstring)
         if not match:
-            raise ValueError("invalid version number '%s'" % vstring)
+            raise ValueError(f"invalid version number '{vstring}'")
 
         (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6)
 
@@ -330,7 +330,7 @@ def __str__(self):
         return self.vstring
 
     def __repr__(self):
-        return "LooseVersion ('%s')" % str(self)
+        return f"LooseVersion ('{self}')"
 
     def _cmp(self, other):
         if isinstance(other, str):

From d8d8be78fc5a5c3e1b1f82cdb0dd8b20931a9d53 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:30:28 +0200
Subject: [PATCH 203/221] Enable ruff rule RUF010

---
 ruff.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ruff.toml b/ruff.toml
index 70612985..49934c7d 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,6 +1,7 @@
 [lint]
 extend-select = [
 	"C901",
+	"RUF010",
 	"W",
 ]
 ignore = [

From 187aeb3d572884af1310a8b6785949aa5a5e177b Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:18:45 +0200
Subject: [PATCH 204/221] Apply ruff/pyupgrade rule UP031

UP031 Use format specifiers instead of percent format
---
 distutils/_modified.py             |  2 +-
 distutils/archive_util.py          | 11 ++++-----
 distutils/bcppcompiler.py          |  6 ++---
 distutils/ccompiler.py             | 38 ++++++++++++++----------------
 distutils/cmd.py                   | 10 ++++----
 distutils/command/bdist.py         |  6 ++---
 distutils/command/bdist_dumb.py    |  4 ++--
 distutils/command/bdist_rpm.py     | 24 +++++++++----------
 distutils/command/build.py         |  2 +-
 distutils/command/build_clib.py    | 12 +++++-----
 distutils/command/build_ext.py     | 12 +++++-----
 distutils/command/build_py.py      |  8 +++----
 distutils/command/build_scripts.py |  2 +-
 distutils/command/check.py         |  4 ++--
 distutils/command/config.py        | 10 ++++----
 distutils/command/install.py       | 12 +++++-----
 distutils/command/install_lib.py   |  2 +-
 distutils/command/register.py      |  9 ++++---
 distutils/command/sdist.py         | 12 +++++-----
 distutils/command/upload.py        |  8 +++----
 distutils/config.py                |  4 ++--
 distutils/core.py                  | 11 ++++-----
 distutils/cygwinccompiler.py       | 26 ++++++++++----------
 distutils/dir_util.py              |  2 +-
 distutils/dist.py                  | 28 +++++++++++-----------
 distutils/extension.py             |  6 ++---
 distutils/fancy_getopt.py          | 14 +++++------
 distutils/file_util.py             |  6 ++---
 distutils/filelist.py              | 16 ++++++-------
 distutils/msvc9compiler.py         | 18 +++++++-------
 distutils/msvccompiler.py          | 14 +++++------
 distutils/sysconfig.py             |  4 ++--
 distutils/tests/__init__.py        |  2 +-
 distutils/tests/test_bdist.py      |  2 +-
 distutils/tests/test_bdist_dumb.py |  2 +-
 distutils/tests/test_build_clib.py |  2 +-
 distutils/tests/test_build_ext.py  |  4 ++--
 distutils/tests/test_build_py.py   |  4 ++--
 distutils/tests/test_clean.py      |  2 +-
 distutils/tests/test_config_cmd.py |  6 ++---
 distutils/tests/test_dir_util.py   |  6 ++---
 distutils/tests/test_file_util.py  |  2 +-
 distutils/tests/test_install.py    |  4 ++--
 distutils/tests/test_spawn.py      |  4 ++--
 distutils/text_file.py             |  2 +-
 distutils/util.py                  | 10 ++++----
 distutils/version.py               |  4 ++--
 distutils/versionpredicate.py      | 10 ++++----
 48 files changed, 202 insertions(+), 207 deletions(-)

diff --git a/distutils/_modified.py b/distutils/_modified.py
index 07b2ead0..6532aa10 100644
--- a/distutils/_modified.py
+++ b/distutils/_modified.py
@@ -24,7 +24,7 @@ def newer(source, target):
     Raises DistutilsFileError if 'source' does not exist.
     """
     if not os.path.exists(source):
-        raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source))
+        raise DistutilsFileError(f"file '{os.path.abspath(source)}' does not exist")
 
     return _newer(source, target)
 
diff --git a/distutils/archive_util.py b/distutils/archive_util.py
index 052f6e46..27b497f3 100644
--- a/distutils/archive_util.py
+++ b/distutils/archive_util.py
@@ -113,7 +113,7 @@ def _set_uid_gid(tarinfo):
         return tarinfo
 
     if not dry_run:
-        tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
+        tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}')
         try:
             tar.add(base_dir, filter=_set_uid_gid)
         finally:
@@ -160,12 +160,11 @@ def make_zipfile(base_name, base_dir, verbose=0, dry_run=0):  # noqa: C901
             # XXX really should distinguish between "couldn't find
             # external 'zip' command" and "zip failed".
             raise DistutilsExecError(
-                (
-                    "unable to create zip file '%s': "
+
+                    f"unable to create zip file '{zip_filename}': "
                     "could neither import the 'zipfile' module nor "
                     "find a standalone zip utility"
-                )
-                % zip_filename
+
             )
 
     else:
@@ -260,7 +259,7 @@ def make_archive(
     try:
         format_info = ARCHIVE_FORMATS[format]
     except KeyError:
-        raise ValueError("unknown archive format '%s'" % format)
+        raise ValueError(f"unknown archive format '{format}'")
 
     func = format_info[0]
     for arg, val in format_info[1]:
diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py
index c1341e43..d6f518bb 100644
--- a/distutils/bcppcompiler.py
+++ b/distutils/bcppcompiler.py
@@ -234,11 +234,11 @@ def link(  # noqa: C901
                 head, tail = os.path.split(output_filename)
                 modname, ext = os.path.splitext(tail)
                 temp_dir = os.path.dirname(objects[0])  # preserve tree structure
-                def_file = os.path.join(temp_dir, '%s.def' % modname)
+                def_file = os.path.join(temp_dir, f'{modname}.def')
                 contents = ['EXPORTS']
                 for sym in export_symbols or []:
                     contents.append(f'  {sym}=_{sym}')
-                self.execute(write_file, (def_file, contents), "writing %s" % def_file)
+                self.execute(write_file, (def_file, contents), f"writing {def_file}")
 
             # Borland C++ has problems with '/' in paths
             objects2 = map(os.path.normpath, objects)
@@ -254,7 +254,7 @@ def link(  # noqa: C901
                     objects.append(file)
 
             for ell in library_dirs:
-                ld_args.append("/L%s" % os.path.normpath(ell))
+                ld_args.append(f"/L{os.path.normpath(ell)}")
             ld_args.append("/L.")  # we sometimes use relative paths
 
             # list of object files
diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py
index 8876d730..0671bd3f 100644
--- a/distutils/ccompiler.py
+++ b/distutils/ccompiler.py
@@ -202,7 +202,7 @@ def _check_macro_definitions(self, definitions):
                 and isinstance(defn[0], str)
             ):
                 raise TypeError(
-                    ("invalid macro definition '%s': " % defn)
+                    (f"invalid macro definition '{defn}': ")
                     + "must be tuple (string,), (string, string), or "
                     + "(string, None)"
                 )
@@ -859,7 +859,7 @@ def has_function(  # noqa: C901
         fd, fname = tempfile.mkstemp(".c", funcname, text=True)
         with os.fdopen(fd, "w", encoding='utf-8') as f:
             for incl in includes:
-                f.write("""#include "%s"\n""" % incl)
+                f.write(f"""#include "{incl}"\n""")
             if not includes:
                 # Use "char func(void);" as the prototype to follow
                 # what autoconf does.  This prototype does not match
@@ -869,22 +869,20 @@ def has_function(  # noqa: C901
                 # know the exact argument types, and the has_function
                 # interface does not provide that level of information.
                 f.write(
-                    """\
+                    f"""\
 #ifdef __cplusplus
 extern "C"
 #endif
-char %s(void);
+char {funcname}(void);
 """
-                    % funcname
                 )
             f.write(
-                """\
-int main (int argc, char **argv) {
-    %s();
+                f"""\
+int main (int argc, char **argv) {{
+    {funcname}();
     return 0;
-}
+}}
 """
-                % funcname
             )
 
         try:
@@ -1032,7 +1030,7 @@ def debug_print(self, msg):
             print(msg)
 
     def warn(self, msg):
-        sys.stderr.write("warning: %s\n" % msg)
+        sys.stderr.write(f"warning: {msg}\n")
 
     def execute(self, func, args, msg=None, level=1):
         execute(func, args, msg, self.dry_run)
@@ -1145,9 +1143,9 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0):
 
         (module_name, class_name, long_description) = compiler_class[compiler]
     except KeyError:
-        msg = "don't know how to compile C/C++ code on platform '%s'" % plat
+        msg = f"don't know how to compile C/C++ code on platform '{plat}'"
         if compiler is not None:
-            msg = msg + " with '%s' compiler" % compiler
+            msg = msg + f" with '{compiler}' compiler"
         raise DistutilsPlatformError(msg)
 
     try:
@@ -1157,7 +1155,7 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0):
         klass = vars(module)[class_name]
     except ImportError:
         raise DistutilsModuleError(
-            "can't compile C/C++ code: unable to load module '%s'" % module_name
+            f"can't compile C/C++ code: unable to load module '{module_name}'"
         )
     except KeyError:
         raise DistutilsModuleError(
@@ -1196,15 +1194,15 @@ def gen_preprocess_options(macros, include_dirs):
     for macro in macros:
         if not (isinstance(macro, tuple) and 1 <= len(macro) <= 2):
             raise TypeError(
-                "bad macro definition '%s': "
-                "each element of 'macros' list must be a 1- or 2-tuple" % macro
+                f"bad macro definition '{macro}': "
+                "each element of 'macros' list must be a 1- or 2-tuple"
             )
 
         if len(macro) == 1:  # undefine this macro
-            pp_opts.append("-U%s" % macro[0])
+            pp_opts.append(f"-U{macro[0]}")
         elif len(macro) == 2:
             if macro[1] is None:  # define with no explicit value
-                pp_opts.append("-D%s" % macro[0])
+                pp_opts.append(f"-D{macro[0]}")
             else:
                 # XXX *don't* need to be clever about quoting the
                 # macro value here, because we're going to avoid the
@@ -1212,7 +1210,7 @@ def gen_preprocess_options(macros, include_dirs):
                 pp_opts.append("-D{}={}".format(*macro))
 
     for dir in include_dirs:
-        pp_opts.append("-I%s" % dir)
+        pp_opts.append(f"-I{dir}")
     return pp_opts
 
 
@@ -1245,7 +1243,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
+                    f"no library file corresponding to '{lib}' found (skipping)"
                 )
         else:
             lib_opts.append(compiler.library_option(lib))
diff --git a/distutils/cmd.py b/distutils/cmd.py
index 02dbf165..0daf6406 100644
--- a/distutils/cmd.py
+++ b/distutils/cmd.py
@@ -135,7 +135,7 @@ def initialize_options(self):
         This method must be implemented by all command classes.
         """
         raise RuntimeError(
-            "abstract method -- subclass %s must override" % self.__class__
+            f"abstract method -- subclass {self.__class__} must override"
         )
 
     def finalize_options(self):
@@ -150,14 +150,14 @@ def finalize_options(self):
         This method must be implemented by all command classes.
         """
         raise RuntimeError(
-            "abstract method -- subclass %s must override" % self.__class__
+            f"abstract method -- subclass {self.__class__} must override"
         )
 
     def dump_options(self, header=None, indent=""):
         from distutils.fancy_getopt import longopt_xlate
 
         if header is None:
-            header = "command options for '%s':" % self.get_command_name()
+            header = f"command options for '{self.get_command_name()}':"
         self.announce(indent + header, level=logging.INFO)
         indent = indent + "  "
         for option, _, _ in self.user_options:
@@ -178,7 +178,7 @@ def run(self):
         This method must be implemented by all command classes.
         """
         raise RuntimeError(
-            "abstract method -- subclass %s must override" % self.__class__
+            f"abstract method -- subclass {self.__class__} must override"
         )
 
     def announce(self, msg, level=logging.DEBUG):
@@ -412,7 +412,7 @@ def make_file(
         timestamp checks.
         """
         if skip_msg is None:
-            skip_msg = "skipping %s (inputs unchanged)" % outfile
+            skip_msg = f"skipping {outfile} (inputs unchanged)"
 
         # Allow 'infiles' to be a single string
         if isinstance(infiles, str):
diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py
index ade98445..d7f8b248 100644
--- a/distutils/command/bdist.py
+++ b/distutils/command/bdist.py
@@ -41,7 +41,7 @@ class bdist(Command):
             'plat-name=',
             'p',
             "platform name to embed in generated filenames "
-            "(default: %s)" % get_platform(),
+            f"(default: {get_platform()})",
         ),
         ('formats=', None, "formats for distribution (comma-separated list)"),
         (
@@ -120,7 +120,7 @@ def finalize_options(self):
             except KeyError:
                 raise DistutilsPlatformError(
                     "don't know how to create built distributions "
-                    "on platform %s" % os.name
+                    f"on platform {os.name}"
                 )
 
         if self.dist_dir is None:
@@ -133,7 +133,7 @@ def run(self):
             try:
                 commands.append(self.format_commands[format][0])
             except KeyError:
-                raise DistutilsOptionError("invalid format '%s'" % format)
+                raise DistutilsOptionError(f"invalid format '{format}'")
 
         # Reinitialize and run each command.
         for i in range(len(self.formats)):
diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py
index 06502d20..0cec1978 100644
--- a/distutils/command/bdist_dumb.py
+++ b/distutils/command/bdist_dumb.py
@@ -23,7 +23,7 @@ class bdist_dumb(Command):
             'plat-name=',
             'p',
             "platform name to embed in generated filenames "
-            "(default: %s)" % get_platform(),
+            f"(default: {get_platform()})",
         ),
         (
             'format=',
@@ -81,7 +81,7 @@ def finalize_options(self):
             except KeyError:
                 raise DistutilsPlatformError(
                     "don't know how to create dumb built distributions "
-                    "on platform %s" % os.name
+                    f"on platform {os.name}"
                 )
 
         self.set_undefined_options(
diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index 649968a5..f08981f7 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -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
+                f"don't know how to create RPM distributions on platform {os.name}"
             )
         if self.binary_only and self.source_only:
             raise DistutilsOptionError(
@@ -295,9 +295,9 @@ def run(self):  # noqa: C901
 
         # Spec file goes into 'dist_dir' if '--spec-only specified',
         # build/rpm. otherwise.
-        spec_path = os.path.join(spec_dir, "%s.spec" % self.distribution.get_name())
+        spec_path = os.path.join(spec_dir, f"{self.distribution.get_name()}.spec")
         self.execute(
-            write_file, (spec_path, self._make_spec_file()), "writing '%s'" % spec_path
+            write_file, (spec_path, self._make_spec_file()), f"writing '{spec_path}'"
         )
 
         if self.spec_only:  # stop if requested
@@ -322,7 +322,7 @@ def run(self):  # noqa: C901
             if os.path.exists(self.icon):
                 self.copy_file(self.icon, source_dir)
             else:
-                raise DistutilsFileError("icon file '%s' does not exist" % self.icon)
+                raise DistutilsFileError(f"icon file '{self.icon}' does not exist")
 
         # build package
         log.info("building RPMs")
@@ -334,9 +334,9 @@ def run(self):  # noqa: C901
             rpm_cmd.append('-bb')
         else:
             rpm_cmd.append('-ba')
-        rpm_cmd.extend(['--define', '__python %s' % self.python])
+        rpm_cmd.extend(['--define', f'__python {self.python}'])
         if self.rpm3_mode:
-            rpm_cmd.extend(['--define', '_topdir %s' % os.path.abspath(self.rpm_base)])
+            rpm_cmd.extend(['--define', f'_topdir {os.path.abspath(self.rpm_base)}'])
         if not self.keep_temp:
             rpm_cmd.append('--clean')
 
@@ -370,7 +370,7 @@ def run(self):  # noqa: C901
 
             status = out.close()
             if status:
-                raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd))
+                raise DistutilsExecError(f"Failed to execute: {repr(q_cmd)}")
 
         finally:
             out.close()
@@ -426,7 +426,7 @@ def _make_spec_file(self):  # noqa: C901
         # 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()
+            f'  {line.strip()} \\' for line in vendor_hook.splitlines()
         ])
         problem = "brp-python-bytecompile \\\n"
         fixed = "brp-python-bytecompile %{__python} \\\n"
@@ -468,7 +468,7 @@ def _make_spec_file(self):  # noqa: C901
             if not self.distribution.has_ext_modules():
                 spec_file.append('BuildArch: noarch')
         else:
-            spec_file.append('BuildArch: %s' % self.force_arch)
+            spec_file.append(f'BuildArch: {self.force_arch}')
 
         for field in (
             'Vendor',
@@ -518,7 +518,7 @@ def _make_spec_file(self):  # noqa: C901
         # rpm scripts
         # figure out default build script
         def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}"
-        def_build = "%s build" % def_setup_call
+        def_build = f"{def_setup_call} build"
         if self.use_rpm_opt_flags:
             def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
 
@@ -529,8 +529,8 @@ def _make_spec_file(self):  # noqa: C901
         # are just text that we drop in as-is.  Hmmm.
 
         install_cmd = (
-            '%s install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'
-        ) % def_setup_call
+            f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'
+        )
 
         script_options = [
             ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),
diff --git a/distutils/command/build.py b/distutils/command/build.py
index d18ed503..7abc4318 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -35,7 +35,7 @@ class build(Command):
             'plat-name=',
             'p',
             "platform name to build for, if supported "
-            "(default: %s)" % get_platform(),
+            f"(default: {get_platform()})",
         ),
         ('compiler=', 'c', "specify the compiler type"),
         ('parallel=', 'j', "number of parallel build jobs"),
diff --git a/distutils/command/build_clib.py b/distutils/command/build_clib.py
index 360575d0..f5e91042 100644
--- a/distutils/command/build_clib.py
+++ b/distutils/command/build_clib.py
@@ -138,8 +138,8 @@ def check_library_list(self, libraries):
 
             if '/' in name or (os.sep != '/' and os.sep in name):
                 raise DistutilsSetupError(
-                    "bad library name '%s': "
-                    "may not contain directory separators" % lib[0]
+                    f"bad library name '{lib[0]}': "
+                    "may not contain directory separators"
                 )
 
             if not isinstance(build_info, dict):
@@ -166,9 +166,9 @@ def get_source_files(self):
             sources = build_info.get('sources')
             if sources is None or not isinstance(sources, (list, tuple)):
                 raise DistutilsSetupError(
-                    "in 'libraries' option (library '%s'), "
+                    f"in 'libraries' option (library '{lib_name}'), "
                     "'sources' must be present and must be "
-                    "a list of source filenames" % lib_name
+                    "a list of source filenames"
                 )
 
             filenames.extend(sources)
@@ -179,9 +179,9 @@ def build_libraries(self, libraries):
             sources = build_info.get('sources')
             if sources is None or not isinstance(sources, (list, tuple)):
                 raise DistutilsSetupError(
-                    "in 'libraries' option (library '%s'), "
+                    f"in 'libraries' option (library '{lib_name}'), "
                     "'sources' must be present and must be "
-                    "a list of source filenames" % lib_name
+                    "a list of source filenames"
                 )
             sources = list(sources)
 
diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py
index 06d949af..fe06eed0 100644
--- a/distutils/command/build_ext.py
+++ b/distutils/command/build_ext.py
@@ -57,7 +57,7 @@ class build_ext(Command):
     #     takes care of both command-line and client options
     #     in between initialize_options() and finalize_options())
 
-    sep_by = " (separated by '%s')" % os.pathsep
+    sep_by = f" (separated by '{os.pathsep}')"
     user_options = [
         ('build-lib=', 'b', "directory for compiled extension modules"),
         ('build-temp=', 't', "directory for temporary files (build by-products)"),
@@ -65,7 +65,7 @@ class build_ext(Command):
             'plat-name=',
             'p',
             "platform name to cross-compile for, if supported "
-            "(default: %s)" % get_platform(),
+            f"(default: {get_platform()})",
         ),
         (
             'inplace',
@@ -517,9 +517,9 @@ def build_extension(self, ext):
         sources = ext.sources
         if sources is None or not isinstance(sources, (list, tuple)):
             raise DistutilsSetupError(
-                "in 'ext_modules' option (extension '%s'), "
+                f"in 'ext_modules' option (extension '{ext.name}'), "
                 "'sources' must be present and must be "
-                "a list of source filenames" % ext.name
+                "a list of source filenames"
             )
         # sort to make the resulting .so file build reproducible
         sources = sorted(sources)
@@ -663,7 +663,7 @@ def find_swig(self):
             # Windows (or so I presume!).  If we find it there, great;
             # if not, act like Unix and assume it's in the PATH.
             for vers in ("1.3", "1.2", "1.1"):
-                fn = os.path.join("c:\\swig%s" % vers, "swig.exe")
+                fn = os.path.join(f"c:\\swig{vers}", "swig.exe")
                 if os.path.isfile(fn):
                     return fn
             else:
@@ -671,7 +671,7 @@ def find_swig(self):
         else:
             raise DistutilsPlatformError(
                 "I don't know how to find (much less run) SWIG "
-                "on platform '%s'" % os.name
+                f"on platform '{os.name}'"
             )
 
     # -- Name generators -----------------------------------------------
diff --git a/distutils/command/build_py.py b/distutils/command/build_py.py
index 56e6fa2e..ede4ff0d 100644
--- a/distutils/command/build_py.py
+++ b/distutils/command/build_py.py
@@ -191,12 +191,12 @@ def check_package(self, package, package_dir):
         if package_dir != "":
             if not os.path.exists(package_dir):
                 raise DistutilsFileError(
-                    "package directory '%s' does not exist" % package_dir
+                    f"package directory '{package_dir}' does not exist"
                 )
             if not os.path.isdir(package_dir):
                 raise DistutilsFileError(
-                    "supposed package directory '%s' exists, "
-                    "but is not a directory" % package_dir
+                    f"supposed package directory '{package_dir}' exists, "
+                    "but is not a directory"
                 )
 
         # Directories without __init__.py are namespace packages (PEP 420).
@@ -228,7 +228,7 @@ def find_package_modules(self, package, package_dir):
                 module = os.path.splitext(os.path.basename(f))[0]
                 modules.append((package, module, f))
             else:
-                self.debug_print("excluding %s" % setup_script)
+                self.debug_print(f"excluding {setup_script}")
         return modules
 
     def find_modules(self):
diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 5f3902a0..9e5963c2 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -96,7 +96,7 @@ def _copy_script(self, script, outfiles, updated_files):  # noqa: C901
         else:
             first_line = f.readline()
             if not first_line:
-                self.warn("%s is an empty file (skipping)" % script)
+                self.warn(f"{script} is an empty file (skipping)")
                 return
 
             shebang_match = shebang_pattern.match(first_line)
diff --git a/distutils/command/check.py b/distutils/command/check.py
index 28599e10..295927a9 100644
--- a/distutils/command/check.py
+++ b/distutils/command/check.py
@@ -106,7 +106,7 @@ def check_metadata(self):
                 missing.append(attr)
 
         if missing:
-            self.warn("missing required meta-data: %s" % ', '.join(missing))
+            self.warn("missing required meta-data: {}".format(', '.join(missing)))
 
     def check_restructuredtext(self):
         """Checks if the long string fields are reST-compliant."""
@@ -147,7 +147,7 @@ def _check_rst_data(self, data):
         except AttributeError as e:
             reporter.messages.append((
                 -1,
-                'Could not finish the parsing: %s.' % e,
+                f'Could not finish the parsing: {e}.',
                 '',
                 {},
             ))
diff --git a/distutils/command/config.py b/distutils/command/config.py
index d4b2b0a3..a45ea701 100644
--- a/distutils/command/config.py
+++ b/distutils/command/config.py
@@ -109,7 +109,7 @@ def _gen_temp_sourcefile(self, body, headers, lang):
         with open(filename, "w", encoding='utf-8') as file:
             if headers:
                 for header in headers:
-                    file.write("#include <%s>\n" % header)
+                    file.write(f"#include <{header}>\n")
                 file.write("\n")
             file.write(body)
             if body[-1] != "\n":
@@ -126,7 +126,7 @@ def _preprocess(self, body, headers, include_dirs, lang):
     def _compile(self, body, headers, include_dirs, lang):
         src = self._gen_temp_sourcefile(body, headers, lang)
         if self.dump_source:
-            dump_file(src, "compiling '%s':" % src)
+            dump_file(src, f"compiling '{src}':")
         (obj,) = self.compiler.object_filenames([src])
         self.temp_files.extend([src, obj])
         self.compiler.compile([src], include_dirs=include_dirs)
@@ -311,12 +311,12 @@ def check_func(
         self._check_compiler()
         body = []
         if decl:
-            body.append("int %s ();" % func)
+            body.append(f"int {func} ();")
         body.append("int main () {")
         if call:
-            body.append("  %s();" % func)
+            body.append(f"  {func}();")
         else:
-            body.append("  %s;" % func)
+            body.append(f"  {func};")
         body.append("}")
         body = "\n".join(body) + "\n"
 
diff --git a/distutils/command/install.py b/distutils/command/install.py
index 8e920be4..960ff64b 100644
--- a/distutils/command/install.py
+++ b/distutils/command/install.py
@@ -245,7 +245,7 @@ class install(Command):
         user_options.append((
             'user',
             None,
-            "install in user site-package '%s'" % USER_SITE,
+            f"install in user site-package '{USER_SITE}'",
         ))
         boolean_options.append('user')
 
@@ -600,7 +600,7 @@ def finalize_other(self):
                 self.select_scheme(os.name)
             except KeyError:
                 raise DistutilsPlatformError(
-                    "I don't know how to install stuff on '%s'" % os.name
+                    f"I don't know how to install stuff on '{os.name}'"
                 )
 
     def select_scheme(self, name):
@@ -685,7 +685,7 @@ def create_home_path(self):
         home = convert_path(os.path.expanduser("~"))
         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)
+                self.debug_print(f"os.makedirs('{path}', 0o700)")
                 os.makedirs(path, 0o700)
 
     # -- Command execution methods -------------------------------------
@@ -720,7 +720,7 @@ def run(self):
             self.execute(
                 write_file,
                 (self.record, outputs),
-                "writing list of installed files to '%s'" % self.record,
+                f"writing list of installed files to '{self.record}'",
             )
 
         sys_path = map(os.path.normpath, sys.path)
@@ -745,10 +745,10 @@ def create_path_file(self):
         filename = os.path.join(self.install_libbase, self.path_file + ".pth")
         if self.install_path_file:
             self.execute(
-                write_file, (filename, [self.extra_dirs]), "creating %s" % filename
+                write_file, (filename, [self.extra_dirs]), f"creating {filename}"
             )
         else:
-            self.warn("path file '%s' not created" % filename)
+            self.warn(f"path file '{filename}' not created")
 
     # -- Reporting methods ---------------------------------------------
 
diff --git a/distutils/command/install_lib.py b/distutils/command/install_lib.py
index b1f346f0..76993b1e 100644
--- a/distutils/command/install_lib.py
+++ b/distutils/command/install_lib.py
@@ -114,7 +114,7 @@ def install(self):
             outfiles = self.copy_tree(self.build_dir, self.install_dir)
         else:
             self.warn(
-                "'%s' does not exist -- no Python modules to install" % self.build_dir
+                f"'{self.build_dir}' does not exist -- no Python modules to install"
             )
             return
         return outfiles
diff --git a/distutils/command/register.py b/distutils/command/register.py
index ee6c54da..6b837275 100644
--- a/distutils/command/register.py
+++ b/distutils/command/register.py
@@ -88,7 +88,7 @@ def _set_config(self):
             self.has_config = True
         else:
             if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
-                raise ValueError('%s not found in .pypirc' % self.repository)
+                raise ValueError(f'{self.repository} not found in .pypirc')
             if self.repository == 'pypi':
                 self.repository = self.DEFAULT_REPOSITORY
             self.has_config = False
@@ -192,7 +192,7 @@ def send_metadata(self):  # noqa: C901
                         logging.INFO,
                     )
                     self.announce(
-                        '(the login will be stored in %s)' % self._get_rc_file(),
+                        f'(the login will be stored in {self._get_rc_file()})',
                         logging.INFO,
                     )
                     choice = 'X'
@@ -277,7 +277,7 @@ def post_to_server(self, data, auth=None):  # noqa: C901
         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(f'\nContent-Disposition: form-data; name="{key}"')
                 body.write("\n\n")
                 body.write(value)
                 if value and value[-1] == '\r':
@@ -288,8 +288,7 @@ def post_to_server(self, data, auth=None):  # noqa: C901
 
         # build the Request
         headers = {
-            'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'
-            % boundary,
+            'Content-type': f'multipart/form-data; boundary={boundary}; charset=utf-8',
             'Content-length': str(len(body)),
         }
         req = urllib.request.Request(self.repository, body, headers)
diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py
index 387d27c9..b483b8bd 100644
--- a/distutils/command/sdist.py
+++ b/distutils/command/sdist.py
@@ -150,7 +150,7 @@ def finalize_options(self):
 
         bad_format = archive_util.check_archive_formats(self.formats)
         if bad_format:
-            raise DistutilsOptionError("unknown archive format '%s'" % bad_format)
+            raise DistutilsOptionError(f"unknown archive format '{bad_format}'")
 
         if self.dist_dir is None:
             self.dist_dir = "dist"
@@ -288,7 +288,7 @@ def _add_defaults_standards(self):
                 if self._cs_path_exists(fn):
                     self.filelist.append(fn)
                 else:
-                    self.warn("standard file '%s' not found" % fn)
+                    self.warn(f"standard file '{fn}' not found")
 
     def _add_defaults_optional(self):
         optional = ['tests/test*.py', 'test/test*.py', 'setup.cfg']
@@ -411,7 +411,7 @@ def write_manifest(self):
         if self._manifest_is_not_generated():
             log.info(
                 "not writing to manually maintained "
-                "manifest file '%s'" % self.manifest
+                f"manifest file '{self.manifest}'"
             )
             return
 
@@ -420,7 +420,7 @@ def write_manifest(self):
         self.execute(
             file_util.write_file,
             (self.manifest, content),
-            "writing manifest file '%s'" % self.manifest,
+            f"writing manifest file '{self.manifest}'",
         )
 
     def _manifest_is_not_generated(self):
@@ -468,10 +468,10 @@ def make_release_tree(self, base_dir, files):
 
         if hasattr(os, 'link'):  # can make hard links on this system
             link = 'hard'
-            msg = "making hard links in %s..." % base_dir
+            msg = f"making hard links in {base_dir}..."
         else:  # nope, have to copy
             link = None
-            msg = "copying files to %s..." % base_dir
+            msg = f"copying files to {base_dir}..."
 
         if not files:
             log.warning("no files to distribute -- empty manifest?")
diff --git a/distutils/command/upload.py b/distutils/command/upload.py
index cf541f8a..773e222c 100644
--- a/distutils/command/upload.py
+++ b/distutils/command/upload.py
@@ -75,7 +75,7 @@ def upload_file(self, command, pyversion, filename):  # noqa: C901
         # Makes sure the repository URL is compliant
         schema, netloc, url, params, query, fragments = urlparse(self.repository)
         if params or query or fragments:
-            raise AssertionError("Incompatible url %s" % self.repository)
+            raise AssertionError(f"Incompatible url {self.repository}")
 
         if schema not in ('http', 'https'):
             raise AssertionError("unsupported schema " + schema)
@@ -153,10 +153,10 @@ def upload_file(self, command, pyversion, filename):  # noqa: C901
         end_boundary = sep_boundary + b'--\r\n'
         body = io.BytesIO()
         for key, values in data.items():
-            title = '\r\nContent-Disposition: form-data; name="%s"' % key
+            title = f'\r\nContent-Disposition: form-data; name="{key}"'
             for value in make_iterable(values):
                 if type(value) is tuple:
-                    title += '; filename="%s"' % value[0]
+                    title += f'; filename="{value[0]}"'
                     value = value[1]
                 else:
                     value = str(value).encode('utf-8')
@@ -172,7 +172,7 @@ def upload_file(self, command, pyversion, filename):  # noqa: C901
 
         # build the Request
         headers = {
-            'Content-type': 'multipart/form-data; boundary=%s' % boundary,
+            'Content-type': f'multipart/form-data; boundary={boundary}',
             'Content-length': str(len(body)),
             'Authorization': auth,
         }
diff --git a/distutils/config.py b/distutils/config.py
index 83f96a9e..8ab27f34 100644
--- a/distutils/config.py
+++ b/distutils/config.py
@@ -30,7 +30,7 @@ class PyPIRCCommand(Command):
     realm = None
 
     user_options = [
-        ('repository=', 'r', "url of repository [default: %s]" % DEFAULT_REPOSITORY),
+        ('repository=', 'r', f"url of repository [default: {DEFAULT_REPOSITORY}]"),
         ('show-response', None, 'display full response text from server'),
     ]
 
@@ -51,7 +51,7 @@ def _read_pypirc(self):  # noqa: C901
         """Reads the .pypirc file."""
         rc = self._get_rc_file()
         if os.path.exists(rc):
-            self.announce('Using PyPI login from %s' % rc)
+            self.announce(f'Using PyPI login from {rc}')
             repository = self.repository or self.DEFAULT_REPOSITORY
 
             config = RawConfigParser()
diff --git a/distutils/core.py b/distutils/core.py
index 309ce696..7238b618 100644
--- a/distutils/core.py
+++ b/distutils/core.py
@@ -146,7 +146,7 @@ class found in 'cmdclass' is used in place of the default, which is
         _setup_distribution = dist = klass(attrs)
     except DistutilsSetupError as msg:
         if 'name' not in attrs:
-            raise SystemExit("error in setup command: %s" % msg)
+            raise SystemExit(f"error in setup command: {msg}")
         else:
             raise SystemExit("error in {} setup command: {}".format(attrs['name'], msg))
 
@@ -170,7 +170,7 @@ class found in 'cmdclass' is used in place of the default, which is
     try:
         ok = dist.parse_command_line()
     except DistutilsArgError as msg:
-        raise SystemExit(gen_usage(dist.script_name) + "\nerror: %s" % msg)
+        raise SystemExit(gen_usage(dist.script_name) + f"\nerror: {msg}")
 
     if DEBUG:
         print("options (after parsing command line):")
@@ -274,11 +274,10 @@ def run_setup(script_name, script_args=None, stop_after="run"):
 
     if _setup_distribution is None:
         raise RuntimeError(
-            (
+
                 "'distutils.core.setup()' was never called -- "
-                "perhaps '%s' is not a Distutils setup script?"
-            )
-            % script_name
+                f"perhaps '{script_name}' is not a Distutils setup script?"
+
         )
 
     # I wonder if the setup script's namespace -- g and l -- would be of
diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py
index 539f09d8..506b88c9 100644
--- a/distutils/cygwinccompiler.py
+++ b/distutils/cygwinccompiler.py
@@ -61,7 +61,7 @@ def get_msvcr():
     try:
         return _msvcr_lookup[msc_ver]
     except KeyError:
-        raise ValueError("Unknown MS Compiler version %s " % msc_ver)
+        raise ValueError(f"Unknown MS Compiler version {msc_ver} ")
 
 
 _runtime_library_dirs_msg = (
@@ -91,8 +91,8 @@ def __init__(self, verbose=0, dry_run=0, force=0):
         if status is not CONFIG_H_OK:
             self.warn(
                 "Python's pyconfig.h doesn't seem to support your compiler. "
-                "Reason: %s. "
-                "Compiling may fail because of undefined preprocessor macros." % details
+                f"Reason: {details}. "
+                "Compiling may fail because of undefined preprocessor macros."
             )
 
         self.cc = os.environ.get('CC', 'gcc')
@@ -102,10 +102,10 @@ def __init__(self, verbose=0, dry_run=0, force=0):
         shared_option = "-shared"
 
         self.set_executables(
-            compiler='%s -mcygwin -O -Wall' % self.cc,
-            compiler_so='%s -mcygwin -mdll -O -Wall' % self.cc,
-            compiler_cxx='%s -mcygwin -O -Wall' % self.cxx,
-            linker_exe='%s -mcygwin' % self.cc,
+            compiler=f'{self.cc} -mcygwin -O -Wall',
+            compiler_so=f'{self.cc} -mcygwin -mdll -O -Wall',
+            compiler_cxx=f'{self.cxx} -mcygwin -O -Wall',
+            linker_exe=f'{self.cc} -mcygwin',
             linker_so=(f'{self.linker_dll} -mcygwin {shared_option}'),
         )
 
@@ -195,10 +195,10 @@ def link(
             def_file = os.path.join(temp_dir, dll_name + ".def")
 
             # Generate .def file
-            contents = ["LIBRARY %s" % os.path.basename(output_filename), "EXPORTS"]
+            contents = [f"LIBRARY {os.path.basename(output_filename)}", "EXPORTS"]
             for sym in export_symbols:
                 contents.append(sym)
-            self.execute(write_file, (def_file, contents), "writing %s" % def_file)
+            self.execute(write_file, (def_file, contents), f"writing {def_file}")
 
             # next add options for def-file
 
@@ -274,10 +274,10 @@ def __init__(self, verbose=0, dry_run=0, force=0):
             raise CCompilerError('Cygwin gcc cannot be used with --compiler=mingw32')
 
         self.set_executables(
-            compiler='%s -O -Wall' % self.cc,
-            compiler_so='%s -mdll -O -Wall' % self.cc,
-            compiler_cxx='%s -O -Wall' % self.cxx,
-            linker_exe='%s' % self.cc,
+            compiler=f'{self.cc} -O -Wall',
+            compiler_so=f'{self.cc} -mdll -O -Wall',
+            compiler_cxx=f'{self.cxx} -O -Wall',
+            linker_exe=f'{self.cc}',
             linker_so=f'{self.linker_dll} {shared_option}',
         )
 
diff --git a/distutils/dir_util.py b/distutils/dir_util.py
index 370c6ffd..1d5573ef 100644
--- a/distutils/dir_util.py
+++ b/distutils/dir_util.py
@@ -133,7 +133,7 @@ def copy_tree(  # noqa: C901
     from distutils.file_util import copy_file
 
     if not dry_run and not os.path.isdir(src):
-        raise DistutilsFileError("cannot copy tree '%s': not a directory" % src)
+        raise DistutilsFileError(f"cannot copy tree '{src}': not a directory")
     try:
         names = os.listdir(src)
     except OSError as e:
diff --git a/distutils/dist.py b/distutils/dist.py
index 668ce7eb..2eb5e1a1 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -262,7 +262,7 @@ def __init__(self, attrs=None):  # noqa: C901
                 elif hasattr(self, key):
                     setattr(self, key, val)
                 else:
-                    msg = "Unknown distribution option: %s" % repr(key)
+                    msg = f"Unknown distribution option: {repr(key)}"
                     warnings.warn(msg)
 
         # no-user-cfg is handled before other command line args
@@ -311,9 +311,9 @@ def dump_option_dicts(self, header=None, commands=None, indent=""):
         for cmd_name in commands:
             opt_dict = self.command_options.get(cmd_name)
             if opt_dict is None:
-                self.announce(indent + "no option dict for '%s' command" % cmd_name)
+                self.announce(indent + f"no option dict for '{cmd_name}' command")
             else:
-                self.announce(indent + "option dict for '%s' command:" % cmd_name)
+                self.announce(indent + f"option dict for '{cmd_name}' command:")
                 out = pformat(opt_dict)
                 for line in out.split('\n'):
                     self.announce(indent + "  " + line)
@@ -339,7 +339,7 @@ def find_config_files(self):
         files = [str(path) for path in self._gen_paths() if os.path.isfile(path)]
 
         if DEBUG:
-            self.announce("using config files: %s" % ', '.join(files))
+            self.announce("using config files: {}".format(', '.join(files)))
 
         return files
 
@@ -395,7 +395,7 @@ def parse_config_files(self, filenames=None):  # noqa: C901
         parser = ConfigParser()
         for filename in filenames:
             if DEBUG:
-                self.announce("  reading %s" % filename)
+                self.announce(f"  reading {filename}")
             parser.read(filename, encoding='utf-8')
             for section in parser.sections():
                 options = parser.options(section)
@@ -525,7 +525,7 @@ def _parse_command_opts(self, parser, args):  # noqa: C901
         # Pull the current command from the head of the command line
         command = args[0]
         if not command_re.match(command):
-            raise SystemExit("invalid command name '%s'" % command)
+            raise SystemExit(f"invalid command name '{command}'")
         self.commands.append(command)
 
         # Dig up the command class that implements this command, so we
@@ -540,7 +540,7 @@ def _parse_command_opts(self, parser, args):  # noqa: C901
         # to be sure that the basic "command" interface is implemented.
         if not issubclass(cmd_class, Command):
             raise DistutilsClassError(
-                "command class %s must subclass Command" % cmd_class
+                f"command class {cmd_class} must subclass Command"
             )
 
         # Also make sure that the command object provides a list of its
@@ -668,7 +668,7 @@ def _show_help(
                 )
             else:
                 parser.set_option_table(klass.user_options)
-            parser.print_help("Options for '%s' command:" % klass.__name__)
+            parser.print_help(f"Options for '{klass.__name__}' command:")
             print()
 
         print(gen_usage(self.script_name))
@@ -842,7 +842,7 @@ def get_command_class(self, command):
             self.cmdclass[command] = klass
             return klass
 
-        raise DistutilsModuleError("invalid command '%s'" % command)
+        raise DistutilsModuleError(f"invalid command '{command}'")
 
     def get_command_obj(self, command, create=1):
         """Return the command object for 'command'.  Normally this object
@@ -855,7 +855,7 @@ def get_command_obj(self, command, create=1):
             if DEBUG:
                 self.announce(
                     "Distribution.get_command_obj(): "
-                    "creating '%s' command object" % command
+                    f"creating '{command}' command object"
                 )
 
             klass = self.get_command_class(command)
@@ -887,7 +887,7 @@ def _set_command_options(self, command_obj, option_dict=None):  # noqa: C901
             option_dict = self.get_option_dict(command_name)
 
         if DEBUG:
-            self.announce("  setting options for '%s' command:" % command_name)
+            self.announce(f"  setting options for '{command_name}' command:")
         for option, (source, value) in option_dict.items():
             if DEBUG:
                 self.announce(f"    {option} = {value} (from {source})")
@@ -1149,9 +1149,9 @@ def write_pkg_file(self, file):
             version = '1.1'
 
         # required fields
-        file.write('Metadata-Version: %s\n' % version)
-        file.write('Name: %s\n' % self.get_name())
-        file.write('Version: %s\n' % self.get_version())
+        file.write(f'Metadata-Version: {version}\n')
+        file.write(f'Name: {self.get_name()}\n')
+        file.write(f'Version: {self.get_version()}\n')
 
         def maybe_write(header, val):
             if val:
diff --git a/distutils/extension.py b/distutils/extension.py
index 94e71635..914d176d 100644
--- a/distutils/extension.py
+++ b/distutils/extension.py
@@ -130,7 +130,7 @@ def __init__(
         if len(kw) > 0:
             options = [repr(option) for option in kw]
             options = ', '.join(sorted(options))
-            msg = "Unknown Extension options: %s" % options
+            msg = f"Unknown Extension options: {options}"
             warnings.warn(msg)
 
     def __repr__(self):
@@ -167,7 +167,7 @@ def read_setup_file(filename):  # noqa: C901
                 continue
 
             if line[0] == line[-1] == "*":
-                file.warn("'%s' lines not handled yet" % line)
+                file.warn(f"'{line}' lines not handled yet")
                 continue
 
             line = expand_makefile_vars(line, vars)
@@ -233,7 +233,7 @@ def read_setup_file(filename):  # noqa: C901
                     # and append it to sources.  Hmmmm.
                     ext.extra_objects.append(word)
                 else:
-                    file.warn("unrecognized argument '%s'" % word)
+                    file.warn(f"unrecognized argument '{word}'")
 
             extensions.append(ext)
     finally:
diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py
index e905aede..69143029 100644
--- a/distutils/fancy_getopt.py
+++ b/distutils/fancy_getopt.py
@@ -21,7 +21,7 @@
 # utilities, we use '-' in place of '_'.  (The spirit of LISP lives on!)
 # The similarities to NAME are again not a coincidence...
 longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
-longopt_re = re.compile(r'^%s$' % longopt_pat)
+longopt_re = re.compile(rf'^{longopt_pat}$')
 
 # For recognizing "negative alias" options, eg. "quiet=!verbose"
 neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$")
@@ -95,7 +95,7 @@ def set_option_table(self, option_table):
     def add_option(self, long_option, short_option=None, help_string=None):
         if long_option in self.option_index:
             raise DistutilsGetoptError(
-                "option conflict: already an option '%s'" % long_option
+                f"option conflict: already an option '{long_option}'"
             )
         else:
             option = (long_option, short_option, help_string)
@@ -162,13 +162,13 @@ 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
+                    f"invalid long option '{long}': must be a string of length >= 2"
                 )
 
             if not ((short is None) or (isinstance(short, str) and len(short) == 1)):
                 raise DistutilsGetoptError(
-                    "invalid short option '%s': "
-                    "must a single character or None" % short
+                    f"invalid short option '{short}': "
+                    "must a single character or None"
                 )
 
             self.repeat[long] = repeat
@@ -210,8 +210,8 @@ def _grok_option_table(self):  # noqa: C901
             # '='.
             if not longopt_re.match(long):
                 raise DistutilsGetoptError(
-                    "invalid long option name '%s' "
-                    "(must be letters, numbers, hyphens only" % long
+                    f"invalid long option name '{long}' "
+                    "(must be letters, numbers, hyphens only"
                 )
 
             self.attr_name[long] = self.get_attr_name(long)
diff --git a/distutils/file_util.py b/distutils/file_util.py
index 960def9c..afa7b0f6 100644
--- a/distutils/file_util.py
+++ b/distutils/file_util.py
@@ -106,7 +106,7 @@ def copy_file(  # noqa: C901
 
     if not os.path.isfile(src):
         raise DistutilsFileError(
-            "can't copy '%s': doesn't exist or not a regular file" % src
+            f"can't copy '{src}': doesn't exist or not a regular file"
         )
 
     if os.path.isdir(dst):
@@ -123,7 +123,7 @@ def copy_file(  # noqa: C901
     try:
         action = _copy_action[link]
     except KeyError:
-        raise ValueError("invalid value '%s' for 'link' argument" % link)
+        raise ValueError(f"invalid value '{link}' for 'link' argument")
 
     if verbose >= 1:
         if os.path.basename(dst) == os.path.basename(src):
@@ -186,7 +186,7 @@ def move_file(src, dst, verbose=1, dry_run=0):  # noqa: C901
         return dst
 
     if not isfile(src):
-        raise DistutilsFileError("can't move '%s': not a regular file" % src)
+        raise DistutilsFileError(f"can't move '{src}': not a regular file")
 
     if isdir(dst):
         dst = os.path.join(dst, basename(src))
diff --git a/distutils/filelist.py b/distutils/filelist.py
index 71ffb2ab..af195833 100644
--- a/distutils/filelist.py
+++ b/distutils/filelist.py
@@ -84,24 +84,24 @@ def _parse_template_line(self, line):
         if action in ('include', 'exclude', 'global-include', 'global-exclude'):
             if len(words) < 2:
                 raise DistutilsTemplateError(
-                    "'%s' expects   ..." % action
+                    f"'{action}' expects   ..."
                 )
             patterns = [convert_path(w) for w in words[1:]]
         elif action in ('recursive-include', 'recursive-exclude'):
             if len(words) < 3:
                 raise DistutilsTemplateError(
-                    "'%s' expects    ..." % action
+                    f"'{action}' expects    ..."
                 )
             dir = convert_path(words[1])
             patterns = [convert_path(w) for w in words[2:]]
         elif action in ('graft', 'prune'):
             if len(words) != 2:
                 raise DistutilsTemplateError(
-                    "'%s' expects a single " % action
+                    f"'{action}' expects a single "
                 )
             dir_pattern = convert_path(words[1])
         else:
-            raise DistutilsTemplateError("unknown action '%s'" % action)
+            raise DistutilsTemplateError(f"unknown action '{action}'")
 
         return (action, patterns, dir, dir_pattern)
 
@@ -192,7 +192,7 @@ def process_template_line(self, line):  # noqa: C901
                 )
         else:
             raise DistutilsInternalError(
-                "this cannot happen: invalid action '%s'" % action
+                f"this cannot happen: invalid action '{action}'"
             )
 
     # Filtering/selection methods
@@ -225,7 +225,7 @@ def include_pattern(self, pattern, anchor=1, prefix=None, is_regex=0):
         # XXX docstring lying about what the special chars are?
         files_found = False
         pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
-        self.debug_print("include_pattern: applying regex r'%s'" % pattern_re.pattern)
+        self.debug_print(f"include_pattern: applying regex r'{pattern_re.pattern}'")
 
         # delayed loading of allfiles list
         if self.allfiles is None:
@@ -247,7 +247,7 @@ def exclude_pattern(self, pattern, anchor=1, prefix=None, is_regex=0):
         """
         files_found = False
         pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
-        self.debug_print("exclude_pattern: applying regex r'%s'" % pattern_re.pattern)
+        self.debug_print(f"exclude_pattern: applying regex r'{pattern_re.pattern}'")
         for i in range(len(self.files) - 1, -1, -1):
             if pattern_re.search(self.files[i]):
                 self.debug_print(" removing " + self.files[i])
@@ -327,7 +327,7 @@ def glob_to_re(pattern):
         # we're using a regex to manipulate a regex, so we need
         # to escape the backslash twice
         sep = r'\\\\'
-    escaped = r'\1[^%s]' % sep
+    escaped = rf'\1[^{sep}]'
     pattern_re = re.sub(r'((?
Date: Sat, 18 May 2024 19:37:57 +0200
Subject: [PATCH 205/221] Round of `ruff format` after `ruff check`

---
 distutils/archive_util.py      | 8 +++-----
 distutils/command/bdist_rpm.py | 3 ++-
 distutils/command/build.py     | 3 +--
 distutils/command/sdist.py     | 3 +--
 distutils/core.py              | 6 ++----
 distutils/sysconfig.py         | 3 +--
 6 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/distutils/archive_util.py b/distutils/archive_util.py
index 27b497f3..9361bf95 100644
--- a/distutils/archive_util.py
+++ b/distutils/archive_util.py
@@ -160,11 +160,9 @@ def make_zipfile(base_name, base_dir, verbose=0, dry_run=0):  # noqa: C901
             # XXX really should distinguish between "couldn't find
             # external 'zip' command" and "zip failed".
             raise DistutilsExecError(
-
-                    f"unable to create zip file '{zip_filename}': "
-                    "could neither import the 'zipfile' module nor "
-                    "find a standalone zip utility"
-
+                f"unable to create zip file '{zip_filename}': "
+                "could neither import the 'zipfile' module nor "
+                "find a standalone zip utility"
             )
 
     else:
diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index f08981f7..d6c46116 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -529,7 +529,8 @@ def _make_spec_file(self):  # noqa: C901
         # are just text that we drop in as-is.  Hmmm.
 
         install_cmd = (
-            f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'
+            f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT '
+            '--record=INSTALLED_FILES'
         )
 
         script_options = [
diff --git a/distutils/command/build.py b/distutils/command/build.py
index 7abc4318..74695941 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -34,8 +34,7 @@ class build(Command):
         (
             'plat-name=',
             'p',
-            "platform name to build for, if supported "
-            f"(default: {get_platform()})",
+            f"platform name to build for, if supported (default: {get_platform()})",
         ),
         ('compiler=', 'c', "specify the compiler type"),
         ('parallel=', 'j', "number of parallel build jobs"),
diff --git a/distutils/command/sdist.py b/distutils/command/sdist.py
index b483b8bd..878e575d 100644
--- a/distutils/command/sdist.py
+++ b/distutils/command/sdist.py
@@ -410,8 +410,7 @@ def write_manifest(self):
         """
         if self._manifest_is_not_generated():
             log.info(
-                "not writing to manually maintained "
-                f"manifest file '{self.manifest}'"
+                f"not writing to manually maintained manifest file '{self.manifest}'"
             )
             return
 
diff --git a/distutils/core.py b/distutils/core.py
index 7238b618..82113c47 100644
--- a/distutils/core.py
+++ b/distutils/core.py
@@ -274,10 +274,8 @@ def run_setup(script_name, script_args=None, stop_after="run"):
 
     if _setup_distribution is None:
         raise RuntimeError(
-
-                "'distutils.core.setup()' was never called -- "
-                f"perhaps '{script_name}' is not a Distutils setup script?"
-
+            "'distutils.core.setup()' was never called -- "
+            f"perhaps '{script_name}' is not a Distutils setup script?"
         )
 
     # I wonder if the setup script's namespace -- g and l -- would be of
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 13251786..9d85cdfd 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -262,8 +262,7 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None):
             return os.path.join(prefix, "Lib", "site-packages")
     else:
         raise DistutilsPlatformError(
-            "I don't know where Python installs its library "
-            f"on platform '{os.name}'"
+            f"I don't know where Python installs its library on platform '{os.name}'"
         )
 
 

From 4f82943206f9efcaf72c8e7b9207a3218d5f9aac Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Sat, 18 May 2024 19:21:07 +0200
Subject: [PATCH 206/221] Enable ruff/pyupgrade rules (UP)

---
 ruff.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ruff.toml b/ruff.toml
index 70612985..b65c0559 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,6 +1,7 @@
 [lint]
 extend-select = [
 	"C901",
+	"UP",
 	"W",
 ]
 ignore = [

From 4549de12976703a135c0b9db71e5aaa3ba0f34a6 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Wed, 29 May 2024 12:24:29 -0400
Subject: [PATCH 207/221] Use mkstemp unconditionally. mktemp has been
 deprecated since Python 2.3.

---
 distutils/util.py | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/distutils/util.py b/distutils/util.py
index 2cdea143..0a8b3d69 100644
--- a/distutils/util.py
+++ b/distutils/util.py
@@ -12,6 +12,7 @@
 import subprocess
 import sys
 import sysconfig
+import tempfile
 
 from ._log import log
 from ._modified import newer
@@ -405,20 +406,10 @@ def byte_compile(  # noqa: C901
     # "Indirect" byte-compilation: write a temporary script and then
     # run it with the appropriate flags.
     if not direct:
-        try:
-            from tempfile import mkstemp
-
-            (script_fd, script_name) = mkstemp(".py")
-        except ImportError:
-            from tempfile import mktemp
-
-            (script_fd, script_name) = None, mktemp(".py")
+        (script_fd, script_name) = tempfile.mkstemp(".py")
         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", encoding='utf-8')
-            else:  # pragma: no cover
-                script = open(script_name, "w", encoding='utf-8')
+            script = os.fdopen(script_fd, "w", encoding='utf-8')
 
             with script:
                 script.write(

From a595a0fad054cd20b69d3e954c99174e3a548938 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Fri, 31 May 2024 03:53:48 -0400
Subject: [PATCH 208/221] Rename extras to align with core metadata spec.

Closes jaraco/skeleton#125.
---
 .readthedocs.yaml | 2 +-
 pyproject.toml    | 4 ++--
 tox.ini           | 6 +++---
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 85dfea9d..dc8516ac 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,7 +3,7 @@ python:
   install:
   - path: .
     extra_requirements:
-      - docs
+      - doc
 
 # required boilerplate readthedocs/readthedocs.org#10401
 build:
diff --git a/pyproject.toml b/pyproject.toml
index 04b14cbc..50845ee3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,7 @@ dynamic = ["version"]
 Homepage = "https://github.com/PROJECT_PATH"
 
 [project.optional-dependencies]
-testing = [
+test = [
 	# upstream
 	"pytest >= 6, != 8.1.*",
 	"pytest-checkdocs >= 2.4",
@@ -36,7 +36,7 @@ testing = [
 
 	# local
 ]
-docs = [
+doc = [
 	# upstream
 	"sphinx >= 3.5",
 	"jaraco.packaging >= 9.3",
diff --git a/tox.ini b/tox.ini
index 4c39a5b1..cc4db36e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ commands =
 	pytest {posargs}
 usedevelop = True
 extras =
-	testing
+	test
 
 [testenv:diffcov]
 description = run tests and check that diff from main is covered
@@ -22,8 +22,8 @@ commands =
 [testenv:docs]
 description = build the documentation
 extras =
-	docs
-	testing
+	doc
+	test
 changedir = docs
 commands =
 	python -m sphinx -W --keep-going . {toxinidir}/build/html

From a37185d950533623d77c08159ab23bbd99a465c1 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Wed, 19 Jun 2024 13:46:47 -0400
Subject: [PATCH 209/221] Pin to pytest<8.1.

Closes pypa/distutils#259
Ref pytest-dev/pytest#12490
---
 pyproject.toml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pyproject.toml b/pyproject.toml
index cda381ab..30bfd7d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,9 @@ test = [
 	"docutils",
 	"pyfakefs",
 	"more_itertools",
+
+	# workaround for pytest-dev/pytest#12490
+	"pytest < 8.1; python_version < '3.12'",
 ]
 doc = [
 	# upstream

From c9729e1a0f66b7adad70c629518b7dab82ccd8c6 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Fri, 21 Jun 2024 13:09:07 -0400
Subject: [PATCH 210/221] Prefer "Source" to "Homepage" for the repository
 label.

Closes jaraco/skeleton#129
---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 50845ee3..ad67d3b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ dependencies = [
 dynamic = ["version"]
 
 [project.urls]
-Homepage = "https://github.com/PROJECT_PATH"
+Source = "https://github.com/PROJECT_PATH"
 
 [project.optional-dependencies]
 test = [

From 6ce37c41abc81767db601644cde2c5b015524b1b Mon Sep 17 00:00:00 2001
From: Stephen Brennan 
Date: Tue, 25 Jun 2024 10:27:30 -0700
Subject: [PATCH 211/221] Use a separate build directory for free-threading

Signed-off-by: Stephen Brennan 
---
 distutils/command/build.py    | 5 +++++
 distutils/tests/test_build.py | 3 +++
 2 files changed, 8 insertions(+)

diff --git a/distutils/command/build.py b/distutils/command/build.py
index d18ed503..766a2ab1 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -4,6 +4,7 @@
 
 import os
 import sys
+import sysconfig
 
 from ..core import Command
 from ..errors import DistutilsOptionError
@@ -81,6 +82,10 @@ def finalize_options(self):  # noqa: C901
 
         plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}"
 
+        # Python 3.13+ with --disable-gil shouldn't share build directories
+        if sysconfig.get_config_var('Py_GIL_DISABLED'):
+            plat_specifier += 't'
+
         # 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
         # process for C modules
diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py
index 25483ad7..8fb1bc1b 100644
--- a/distutils/tests/test_build.py
+++ b/distutils/tests/test_build.py
@@ -4,6 +4,7 @@
 import sys
 from distutils.command.build import build
 from distutils.tests import support
+from sysconfig import get_config_var
 from sysconfig import get_platform
 
 
@@ -24,6 +25,8 @@ def test_finalize_options(self):
         # examples:
         #   build/lib.macosx-10.3-i386-cpython39
         plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}'
+        if get_config_var('Py_GIL_DISABLED'):
+            plat_spec += 't'
         if hasattr(sys, 'gettotalrefcount'):
             assert cmd.build_platlib.endswith('-pydebug')
             plat_spec += '-pydebug'

From c9371354c343f78f6c6bf958272ea39d4be0c269 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 20 Apr 2024 22:58:10 -0400
Subject: [PATCH 212/221] Deprecate find_executable.

---
 distutils/spawn.py                   | 10 +++++++---
 distutils/tests/__init__.py          |  5 +++--
 distutils/tests/test_archive_util.py |  4 ++--
 distutils/tests/test_bdist_rpm.py    | 10 +++++-----
 distutils/tests/test_sdist.py        | 10 +++++-----
 pytest.ini                           |  5 ++++-
 6 files changed, 26 insertions(+), 18 deletions(-)

diff --git a/distutils/spawn.py b/distutils/spawn.py
index 234d5cd1..429d1ccb 100644
--- a/distutils/spawn.py
+++ b/distutils/spawn.py
@@ -2,16 +2,17 @@
 
 Provides the 'spawn()' function, a front-end to various platform-
 specific functions for launching another program in a sub-process.
-Also provides the 'find_executable()' to search the path for a given
-executable name.
 """
 
 from __future__ import annotations
 
 import os
 import platform
+import shutil
 import subprocess
 import sys
+import warnings
+
 from typing import Mapping
 
 from ._log import log
@@ -62,7 +63,7 @@ def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
         return
 
     if search_path:
-        executable = find_executable(cmd[0])
+        executable = shutil.which(cmd[0])
         if executable is not None:
             cmd[0] = executable
 
@@ -84,6 +85,9 @@ def find_executable(executable, path=None):
     A string listing directories separated by 'os.pathsep'; defaults to
     os.environ['PATH'].  Returns the complete filename or None if not found.
     """
+    warnings.warn(
+        'Use shutil.which instead of find_executable', DeprecationWarning, stacklevel=2
+    )
     _, ext = os.path.splitext(executable)
     if (sys.platform == 'win32') and (ext != '.exe'):
         executable = executable + '.exe'
diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py
index 20dfe8f1..16b68b4e 100644
--- a/distutils/tests/__init__.py
+++ b/distutils/tests/__init__.py
@@ -7,6 +7,7 @@
 by import rather than matching pre-defined names.
 """
 
+import shutil
 from typing import Sequence
 
 
@@ -19,7 +20,7 @@ def missing_compiler_executable(cmd_names: Sequence[str] = []):  # pragma: no co
     missing.
 
     """
-    from distutils import ccompiler, errors, spawn, sysconfig
+    from distutils import ccompiler, errors, sysconfig
 
     compiler = ccompiler.new_compiler()
     sysconfig.customize_compiler(compiler)
@@ -37,5 +38,5 @@ def missing_compiler_executable(cmd_names: Sequence[str] = []):  # pragma: no co
             assert cmd is not None, "the '%s' executable is not configured" % name
         elif not cmd:
             continue
-        if spawn.find_executable(cmd[0]) is None:
+        if shutil.which(cmd[0]) is None:
             return cmd[0]
diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py
index 02af2aa0..abbcd36c 100644
--- a/distutils/tests/test_archive_util.py
+++ b/distutils/tests/test_archive_util.py
@@ -135,7 +135,7 @@ def _create_files(self):
         return tmpdir
 
     @pytest.mark.usefixtures('needs_zlib')
-    @pytest.mark.skipif("not (find_executable('tar') and find_executable('gzip'))")
+    @pytest.mark.skipif("not (shutil.which('tar') and shutil.which('gzip'))")
     def test_tarfile_vs_tar(self):
         tmpdir = self._create_files()
         tmpdir2 = self.mkdtemp()
@@ -190,7 +190,7 @@ def test_tarfile_vs_tar(self):
         tarball = base_name + '.tar'
         assert os.path.exists(tarball)
 
-    @pytest.mark.skipif("not find_executable('compress')")
+    @pytest.mark.skipif("not shutil.which('compress')")
     def test_compress_deprecated(self):
         tmpdir = self._create_files()
         base_name = os.path.join(self.mkdtemp(), 'archive')
diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py
index a5cb42c3..28edda4d 100644
--- a/distutils/tests/test_bdist_rpm.py
+++ b/distutils/tests/test_bdist_rpm.py
@@ -1,10 +1,10 @@
 """Tests for distutils.command.bdist_rpm."""
 
 import os
+import shutil  # noqa: F401
 import sys
 from distutils.command.bdist_rpm import bdist_rpm
 from distutils.core import Distribution
-from distutils.spawn import find_executable  # noqa: F401
 from distutils.tests import support
 
 import pytest
@@ -43,8 +43,8 @@ class TestBuildRpm(
 ):
     @mac_woes
     @requires_zlib()
-    @pytest.mark.skipif("not find_executable('rpm')")
-    @pytest.mark.skipif("not find_executable('rpmbuild')")
+    @pytest.mark.skipif("not shutil.which('rpm')")
+    @pytest.mark.skipif("not shutil.which('rpmbuild')")
     def test_quiet(self):
         # let's create a package
         tmp_dir = self.mkdtemp()
@@ -86,8 +86,8 @@ def test_quiet(self):
     @mac_woes
     @requires_zlib()
     # https://bugs.python.org/issue1533164
-    @pytest.mark.skipif("not find_executable('rpm')")
-    @pytest.mark.skipif("not find_executable('rpmbuild')")
+    @pytest.mark.skipif("not shutil.which('rpm')")
+    @pytest.mark.skipif("not shutil.which('rpmbuild')")
     def test_no_optimize_flag(self):
         # let's create a package that breaks bdist_rpm
         tmp_dir = self.mkdtemp()
diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py
index a85997f1..6a1aa518 100644
--- a/distutils/tests/test_sdist.py
+++ b/distutils/tests/test_sdist.py
@@ -2,6 +2,7 @@
 
 import os
 import pathlib
+import shutil  # noqa: F401
 import tarfile
 import warnings
 import zipfile
@@ -10,7 +11,6 @@
 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
@@ -137,8 +137,8 @@ def test_prune_file_list(self):
         assert sorted(content) == ['fake-1.0/' + x for x in expected]
 
     @pytest.mark.usefixtures('needs_zlib')
-    @pytest.mark.skipif("not find_executable('tar')")
-    @pytest.mark.skipif("not find_executable('gzip')")
+    @pytest.mark.skipif("not shutil.which('tar')")
+    @pytest.mark.skipif("not shutil.which('gzip')")
     def test_make_distribution(self):
         # now building a sdist
         dist, cmd = self.get_cmd()
@@ -434,8 +434,8 @@ def test_manual_manifest(self):
     @pytest.mark.usefixtures('needs_zlib')
     @require_unix_id
     @require_uid_0
-    @pytest.mark.skipif("not find_executable('tar')")
-    @pytest.mark.skipif("not find_executable('gzip')")
+    @pytest.mark.skipif("not shutil.which('tar')")
+    @pytest.mark.skipif("not shutil.which('gzip')")
     def test_make_distribution_owner_group(self):
         # now building a sdist
         dist, cmd = self.get_cmd()
diff --git a/pytest.ini b/pytest.ini
index f9b1d1fc..b53e0d93 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -35,5 +35,8 @@ filterwarnings=
 	# suppress warnings in deprecated compilers
 	ignore:(bcpp|msvc9?)compiler is deprecated
 
-	# suppress well know deprecation warning
+	# suppress well known deprecation warning
 	ignore:distutils.log.Log is deprecated
+
+	# suppress known deprecation
+	ignore:Use shutil.which instead of find_executable:DeprecationWarning

From 57cab7118928829a05b7a2d5cde159fd83e387ff Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 27 Jun 2024 04:33:57 -0400
Subject: [PATCH 213/221] Apply canonicalize_version with
 strip_trailing_zero=False.

---
 distutils/dist.py | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/distutils/dist.py b/distutils/dist.py
index 9f570593..d3a3f23a 100644
--- a/distutils/dist.py
+++ b/distutils/dist.py
@@ -13,7 +13,7 @@
 from collections.abc import Iterable
 from email import message_from_file
 
-from ._vendor.packaging.utils import canonicalize_name
+from ._vendor.packaging.utils import canonicalize_name, canonicalize_version
 
 try:
     import warnings
@@ -1191,9 +1191,25 @@ def get_version(self):
         return self.version or "0.0.0"
 
     def get_fullname(self):
+        return self._fullname(self.get_name(), self.get_version())
+
+    @staticmethod
+    def _fullname(name: str, version: str) -> str:
+        """
+        >>> DistributionMetadata._fullname('setup.tools', '1.0-2')
+        'setup_tools-1.0.post2'
+        >>> DistributionMetadata._fullname('setup-tools', '1.2post2')
+        'setup_tools-1.2.post2'
+        >>> DistributionMetadata._fullname('setup-tools', '1.0-r2')
+        'setup_tools-1.0.post2'
+        >>> DistributionMetadata._fullname('setup.tools', '1.0.post')
+        'setup_tools-1.0.post0'
+        >>> DistributionMetadata._fullname('setup.tools', '1.0+ubuntu-1')
+        'setup_tools-1.0+ubuntu.1'
+        """
         return "{}-{}".format(
-            canonicalize_name(self.get_name()).replace('-', '_'),
-            self.get_version(),
+            canonicalize_name(name).replace('-', '_'),
+            canonicalize_version(version, strip_trailing_zero=False),
         )
 
     def get_author(self):

From 5dcc30ff1f8435ef04f0a3b14290b38f09a55b91 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 27 Jun 2024 04:46:29 -0400
Subject: [PATCH 214/221] Move local ruff rules into a local section.

---
 ruff.toml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/ruff.toml b/ruff.toml
index f2c67aeb..3550acad 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,8 +1,10 @@
 [lint]
 extend-select = [
 	"C901",
-	"RUF100",
 	"W",
+
+	# local
+	"RUF100",
 ]
 ignore = [
 	# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules

From 63d53880df641768c924e37f028755e2b46dabfd Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 27 Jun 2024 05:55:07 -0400
Subject: [PATCH 215/221] Combine strings for clarity.

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

diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py
index ca71e18a..4585512f 100644
--- a/distutils/ccompiler.py
+++ b/distutils/ccompiler.py
@@ -203,8 +203,7 @@ def _check_macro_definitions(self, definitions):
             ):
                 raise TypeError(
                     f"invalid macro definition '{defn}': "
-                    "must be tuple (string,), (string, string), or "
-                    "(string, None)"
+                    "must be tuple (string,), (string, string), or (string, None)"
                 )
 
     # -- Bookkeeping methods -------------------------------------------

From 92c9626038a93465495b0d934a585d0351a0c6da Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 27 Jun 2024 06:01:43 -0400
Subject: [PATCH 216/221] Extract method for checking macro definition.

---
 distutils/ccompiler.py | 35 +++++++++++++++++++----------------
 1 file changed, 19 insertions(+), 16 deletions(-)

diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py
index b860acb7..9974e520 100644
--- a/distutils/ccompiler.py
+++ b/distutils/ccompiler.py
@@ -188,23 +188,26 @@ def _find_macro(self, name):
         return None
 
     def _check_macro_definitions(self, definitions):
-        """Ensures that every element of 'definitions' is a valid macro
-        definition, ie. either (name,value) 2-tuple or a (name,) tuple.  Do
-        nothing if all definitions are OK, raise TypeError otherwise.
-        """
+        """Ensure that every element of 'definitions' is valid."""
         for defn in definitions:
-            if not (
-                isinstance(defn, tuple)
-                and (
-                    len(defn) in (1, 2)
-                    and (isinstance(defn[1], str) or defn[1] is None)
-                )
-                and isinstance(defn[0], str)
-            ):
-                raise TypeError(
-                    f"invalid macro definition '{defn}': "
-                    "must be tuple (string,), (string, string), or (string, None)"
-                )
+            self._check_macro_definition(defn)
+
+    def _check_macro_definition(self, defn):
+        """
+        Raise a TypeError if defn is not valid.
+
+        A valid definition is either a (name, value) 2-tuple or a (name,) tuple.
+        """
+        valid = (
+            isinstance(defn, tuple)
+            and (len(defn) in (1, 2) and (isinstance(defn[1], str) or defn[1] is None))
+            and isinstance(defn[0], str)
+        )
+        if not valid:
+            raise TypeError(
+                f"invalid macro definition '{defn}': "
+                "must be tuple (string,), (string, string), or (string, None)"
+            )
 
     # -- Bookkeeping methods -------------------------------------------
 

From 4fd2d569e37268b8c23f3cc45879465f9c269d92 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Thu, 27 Jun 2024 06:14:54 -0400
Subject: [PATCH 217/221] Extract method for _is_valid_macro.

---
 distutils/ccompiler.py | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py
index 9974e520..87ca89e1 100644
--- a/distutils/ccompiler.py
+++ b/distutils/ccompiler.py
@@ -6,6 +6,7 @@
 import os
 import re
 import sys
+import types
 import warnings
 
 from ._itertools import always_iterable
@@ -190,7 +191,7 @@ def _find_macro(self, name):
     def _check_macro_definitions(self, definitions):
         """Ensure that every element of 'definitions' is valid."""
         for defn in definitions:
-            self._check_macro_definition(defn)
+            self._check_macro_definition(*defn)
 
     def _check_macro_definition(self, defn):
         """
@@ -198,17 +199,19 @@ def _check_macro_definition(self, defn):
 
         A valid definition is either a (name, value) 2-tuple or a (name,) tuple.
         """
-        valid = (
-            isinstance(defn, tuple)
-            and (len(defn) in (1, 2) and (isinstance(defn[1], str) or defn[1] is None))
-            and isinstance(defn[0], str)
-        )
-        if not valid:
+        if not isinstance(defn, tuple) or not self._is_valid_macro(*defn):
             raise TypeError(
                 f"invalid macro definition '{defn}': "
                 "must be tuple (string,), (string, string), or (string, None)"
             )
 
+    @staticmethod
+    def _is_valid_macro(name, value=None):
+        """
+        A valid macro is a ``name : str`` and a ``value : str | None``.
+        """
+        return isinstance(name, str) and isinstance(value, (str, types.NoneType))
+
     # -- Bookkeeping methods -------------------------------------------
 
     def define_macro(self, name, value=None):

From a100e3b41d563bb0736a3090ce4caa70ff471f8e Mon Sep 17 00:00:00 2001
From: Christoph Reiter 
Date: Fri, 28 Jun 2024 06:46:33 +0200
Subject: [PATCH 218/221] CI: run pytest without arguments to avoid stdlib
 distutils being imported

distutils currently doesn't support pytest collection that doesn't
start at least at the distutils dir or above (and not distutils/tests)
since it requires the local distutils being imported before the tests are run,
otherwise the stdlib distutils takes precedence.

Adjust the pytest call to not pass a path to work around this.

Since pytest currently fails to skip collecting venvs with mingw python
(see https://github.com/pytest-dev/pytest/issues/12544) move the venv
to /tmp instead.
---
 .github/workflows/main.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d4c7a392..9e4bf5bb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -146,8 +146,8 @@ jobs:
         run: |
           export VIRTUALENV_NO_SETUPTOOLS=1
 
-          python -m virtualenv venv
-          source venv/bin/activate
+          python -m virtualenv /tmp/venv
+          source /tmp/venv/bin/activate
 
           # python-ruff doesn't work without rust
           sed -i '/pytest-ruff/d' pyproject.toml
@@ -156,8 +156,8 @@ jobs:
       - name: Run tests
         shell: msys2 {0}
         run: |
-          source venv/bin/activate
-          pytest distutils/tests
+          source /tmp/venv/bin/activate
+          pytest
 
   ci_setuptools:
     # Integration testing with setuptools

From 5bdd6d3e3c25b1b4e7cac17977639131c709e6a7 Mon Sep 17 00:00:00 2001
From: Christoph Reiter 
Date: Fri, 28 Jun 2024 07:22:15 +0200
Subject: [PATCH 219/221] CI: explicitely CC/CXX for clang only mingw
 environments

MSYS2 has stopped installing gcc compatibility binaries in clang environments
by default some time ago, and distutils is currently hardcoded to look for "gcc",
while only cc/c++ and clang/clang++ are in PATH.

Work around for now by explicitely setting CC/CXX to override the defaults.

Idealy distutils would try to look harder for a valid compiler before giving up,
but this can be improved in the future.
---
 .github/workflows/main.yml | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9e4bf5bb..b6b757db 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -124,10 +124,10 @@ jobs:
     strategy:
       matrix:
         include:
-          - { sys: mingw64, env: x86_64 }
-          - { sys: mingw32, env: i686 }
-          - { sys: ucrt64,  env: ucrt-x86_64 }
-          - { sys: clang64, env: clang-x86_64 }
+          - { sys: mingw64, env: x86_64, cc: gcc, cxx: g++ }
+          - { sys: mingw32, env: i686, cc: gcc, cxx: g++ }
+          - { sys: ucrt64,  env: ucrt-x86_64, cc: gcc, cxx: g++ }
+          - { sys: clang64, env: clang-x86_64, cc: clang, cxx: clang++}
     runs-on: windows-latest
     steps:
       - uses: actions/checkout@v4
@@ -155,6 +155,9 @@ jobs:
           pip install -e .[test]
       - name: Run tests
         shell: msys2 {0}
+        env:
+          CC: ${{ matrix.cc }}
+          CXX: ${{ matrix.cxx }}
         run: |
           source /tmp/venv/bin/activate
           pytest

From 3764aff62fe9b9c853c8157990ae53a93eb9d4f9 Mon Sep 17 00:00:00 2001
From: Dimitri Papadopoulos
 <3234522+DimitriPapadopoulos@users.noreply.github.com>
Date: Thu, 27 Jun 2024 21:12:19 +0200
Subject: [PATCH 220/221] Use brackets for the default value of option
 arguments

The goal is to standardize the format of the help text printed
by commands. It is not easy to choose between brackets `[]` and
parentheses `()`. I went for the docopt style, which is the
closest to a standard I could find:

	http://docopt.org/

	[...] and whether that argument has a default value ([default: 10]).
---
 distutils/command/bdist.py        | 2 +-
 distutils/command/bdist_dumb.py   | 4 ++--
 distutils/command/bdist_rpm.py    | 2 +-
 distutils/command/build.py        | 2 +-
 distutils/command/build_ext.py    | 2 +-
 distutils/command/clean.py        | 8 ++++----
 distutils/command/install_data.py | 2 +-
 7 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/distutils/command/bdist.py b/distutils/command/bdist.py
index 833f7616..1738f4e5 100644
--- a/distutils/command/bdist.py
+++ b/distutils/command/bdist.py
@@ -41,7 +41,7 @@ class bdist(Command):
             'plat-name=',
             'p',
             "platform name to embed in generated filenames "
-            f"(default: {get_platform()})",
+            f"[default: {get_platform()}]",
         ),
         ('formats=', None, "formats for distribution (comma-separated list)"),
         (
diff --git a/distutils/command/bdist_dumb.py b/distutils/command/bdist_dumb.py
index bf9b6ad6..67b0c8cc 100644
--- a/distutils/command/bdist_dumb.py
+++ b/distutils/command/bdist_dumb.py
@@ -23,7 +23,7 @@ class bdist_dumb(Command):
             'plat-name=',
             'p',
             "platform name to embed in generated filenames "
-            f"(default: {get_platform()})",
+            f"[default: {get_platform()}]",
         ),
         (
             'format=',
@@ -40,7 +40,7 @@ class bdist_dumb(Command):
         (
             'relative',
             None,
-            "build the archive using relative paths (default: false)",
+            "build the archive using relative paths [default: false]",
         ),
         (
             'owner=',
diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py
index cb98cd50..d443eb09 100644
--- a/distutils/command/bdist_rpm.py
+++ b/distutils/command/bdist_rpm.py
@@ -40,7 +40,7 @@ class bdist_rpm(Command):
             'python=',
             None,
             "path to Python interpreter to hard-code in the .spec file "
-            "(default: \"python\")",
+            "[default: \"python\"]",
         ),
         (
             'fix-python',
diff --git a/distutils/command/build.py b/distutils/command/build.py
index 84dc43c9..caf55073 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -34,7 +34,7 @@ class build(Command):
         (
             'plat-name=',
             'p',
-            f"platform name to build for, if supported (default: {get_platform()})",
+            f"platform name to build for, if supported [default: {get_platform()}]",
         ),
         ('compiler=', 'c', "specify the compiler type"),
         ('parallel=', 'j', "number of parallel build jobs"),
diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py
index f1ebc040..a1d1753d 100644
--- a/distutils/command/build_ext.py
+++ b/distutils/command/build_ext.py
@@ -65,7 +65,7 @@ class build_ext(Command):
             'plat-name=',
             'p',
             "platform name to cross-compile for, if supported "
-            f"(default: {get_platform()})",
+            f"[default: {get_platform()}]",
         ),
         (
             'inplace',
diff --git a/distutils/command/clean.py b/distutils/command/clean.py
index 4167a83f..fb54a60e 100644
--- a/distutils/command/clean.py
+++ b/distutils/command/clean.py
@@ -14,17 +14,17 @@
 class clean(Command):
     description = "clean up temporary files from 'build' command"
     user_options = [
-        ('build-base=', 'b', "base build directory (default: 'build.build-base')"),
+        ('build-base=', 'b', "base build directory [default: 'build.build-base']"),
         (
             'build-lib=',
             None,
-            "build directory for all modules (default: 'build.build-lib')",
+            "build directory for all modules [default: 'build.build-lib']",
         ),
-        ('build-temp=', 't', "temporary build directory (default: 'build.build-temp')"),
+        ('build-temp=', 't', "temporary build directory [default: 'build.build-temp']"),
         (
             'build-scripts=',
             None,
-            "build directory for scripts (default: 'build.build-scripts')",
+            "build directory for scripts [default: 'build.build-scripts']",
         ),
         ('bdist-base=', None, "temporary directory for built distributions"),
         ('all', 'a', "remove all build output, not just temporary by-products"),
diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py
index a4da8924..624c0b90 100644
--- a/distutils/command/install_data.py
+++ b/distutils/command/install_data.py
@@ -19,7 +19,7 @@ class install_data(Command):
             'install-dir=',
             'd',
             "base directory for installing data files "
-            "(default: installation base dir)",
+            "[default: installation base dir]",
         ),
         ('root=', None, "install everything relative to this alternate root directory"),
         ('force', 'f', "force installation (overwrite existing files)"),

From 3b86c5ea257afa5700fcd74a40f60663b0bcc13d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Fri, 28 Jun 2024 11:38:01 -0400
Subject: [PATCH 221/221] Suppress EncodingWarnings in docutils.

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

diff --git a/pytest.ini b/pytest.ini
index b53e0d93..dd57c6ef 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -40,3 +40,7 @@ filterwarnings=
 
 	# suppress known deprecation
 	ignore:Use shutil.which instead of find_executable:DeprecationWarning
+
+	# https://sourceforge.net/p/docutils/bugs/490/
+	ignore:'encoding' argument not specified::docutils.io
+	ignore:UTF-8 Mode affects locale.getpreferredencoding()::docutils.io