From c29955f9be8e44b2ea5fea12f86b7bd46a0b3958 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Jul 2023 11:53:15 -0400 Subject: [PATCH 01/51] 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 02/51] 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 03/51] 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 04/51] 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 0e2032c4754c598ba75e467c64009ba4490ddea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Aug 2023 18:42:14 -0400 Subject: [PATCH 05/51] 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 06/51] 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 07/51] 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 08/51] 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 09/51] 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 10/51] 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 11/51] 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 12/51] 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 4a7033164d5bd4fe7ee4d96dae1c0cbfb122df9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Nov 2023 11:45:37 -0400 Subject: [PATCH 13/51] 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 14/51] "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 15/51] 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 16/51] 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 17/51] 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 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 25/51] 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 26/51] 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 27/51] 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 28/51] 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 29/51] 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 30/51] 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 31/51] 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 32/51] 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 33/51] 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 34/51] 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 35/51] 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 36/51] 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 26f420a97e73a2ab695023f6cc21f5c786d2b289 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Nov 2023 11:43:20 -0500 Subject: [PATCH 37/51] 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 38/51] 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 39/51] 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 40/51] 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 41/51] 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 42/51] 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 43/51] 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 44/51] 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 45/51] =?UTF-8?q?ruff:=20extended-ignore=20=E2=86=92=20ign?= =?UTF-8?q?ore=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 46/51] 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 47/51] 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 48/51] 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 49/51] 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 50/51] 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 51/51] 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