From f6f2cc2c7bb9c73a8845e2e8cdce1e054e0407ce Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 18:23:42 +0100 Subject: [PATCH 1/9] Fix extra character in _normalization.best_effor_version for versions starting with v --- setuptools/_normalization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index fb89323c9d..1372983a12 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -12,7 +12,7 @@ _VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I) _UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I) _NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I) -_PEP440_FALLBACK = re.compile(r"^v?(?P(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I) +_PEP440_FALLBACK = re.compile(r"^(?Pv?(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I) def safe_identifier(name: str) -> str: @@ -79,6 +79,8 @@ def best_effort_version(version: str) -> str: '0.dev0+sanitized' >>> best_effort_version("42.+?1") '42.dev0+sanitized.1' + >>> best_effort_version("v78.1.0-2-g3a3144f0d") + '78.1.0.dev0+sanitized.2.g3a3144f0d' """ try: return safe_version(version) From 2ff5a0e67dddca007b849240d5347b42652584a9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 18:26:10 +0100 Subject: [PATCH 2/9] Allow custom template in _normalization.best_effor_version --- setuptools/_normalization.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 1372983a12..e388b412df 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -64,7 +64,10 @@ def safe_version(version: str) -> str: return str(packaging.version.Version(attempt)) -def best_effort_version(version: str) -> str: +def best_effort_version( + version: str, + template: str = "{safe}.dev0+sanitized.{sanitized}", +) -> str: """Convert an arbitrary string into a version-like string. Fallback when ``safe_version`` is not safe enough. >>> best_effort_version("v0.2 beta") @@ -81,6 +84,8 @@ def best_effort_version(version: str) -> str: '42.dev0+sanitized.1' >>> best_effort_version("v78.1.0-2-g3a3144f0d") '78.1.0.dev0+sanitized.2.g3a3144f0d' + >>> best_effort_version("v80.9.0-18-gdf932b02e-dirty", "{safe}.dev+{sanitized}") + '80.9.0.dev0+18.gdf932b02e.dirty' """ try: return safe_version(version) @@ -94,8 +99,8 @@ def best_effort_version(version: str) -> str: safe = "0" rest = version safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".") - local = f"sanitized.{safe_rest}".strip(".") - return safe_version(f"{safe}.dev0+{local}") + fallback = template.format(safe=safe, sanitized=safe_rest).strip(".") + return safe_version(fallback) def safe_extra(extra: str) -> str: From 346ae7e1701820bfc9837b21d1278d1cc1a065cb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 18:26:42 +0100 Subject: [PATCH 3/9] Use dynamic version for setuptools itself Custom function results should be similar to setuptools-scm but without using setuptools-scm. --- MANIFEST.in | 1 + pyproject.toml | 2 +- setup.cfg | 3 --- setup.py | 16 ++++++++++++++++ 4 files changed, 18 insertions(+), 4 deletions(-) delete mode 100644 setup.cfg diff --git a/MANIFEST.in b/MANIFEST.in index 0643e7ee2d..1ed43fda06 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,6 +16,7 @@ include msvc-build-launcher.cmd include mypy.ini include pytest.ini include tox.ini +include NEWS.rst include setuptools/tests/config/setupcfg_examples.txt include setuptools/config/*.schema.json global-exclude *.py[cod] __pycache__ diff --git a/pyproject.toml b/pyproject.toml index 5988fed8cd..eae07ebf75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ backend-path = ["."] [project] name = "setuptools" -version = "80.9.0" authors = [ { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, ] @@ -31,6 +30,7 @@ license = "MIT" dependencies = [ ] keywords = ["CPAN PyPI distutils eggs package management"] +dynamic = ["version"] [project.urls] Source = "https://github.com/pypa/setuptools" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 38922089ad..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[egg_info] -tag_build = .post -tag_date = 1 diff --git a/setup.py b/setup.py index c28a14e722..7b169179ea 100755 --- a/setup.py +++ b/setup.py @@ -1,10 +1,14 @@ #!/usr/bin/env python import os +import re +import subprocess import sys import textwrap +import time import setuptools +from setuptools import _normalization from setuptools.command.install import install here = os.path.dirname(__file__) @@ -26,6 +30,17 @@ package_data.setdefault('setuptools.command', []).extend(['*.xml']) +def _get_version() -> str: + cmd = ["git", "describe", "--abbrev", "--match", "v?[0-9]*", "--dirty"] + try: + version = subprocess.check_output(cmd, encoding="utf-8") + return _normalization.best_effort_version(version, "{safe}.dev+{sanitized}") + except subprocess.CalledProcessError: # e.g.: git not installed or history missing + with open("NEWS.rst", encoding="utf-8") as fp: + match = re.search(r"v\d+\.\d+.\d+", fp.read()) # latest version + return f"{match[0] if match else '0.0.0'}.dev+{time.strftime('%Y%m%d')}" + + def pypi_link(pkg_filename): """ Given the filename, including md5 fragment, construct the @@ -83,6 +98,7 @@ def _restore_install_lib(self): setup_params = dict( + version=_get_version(), cmdclass={'install': install_with_pth}, package_data=package_data, ) From 072df10205da2115e239620362687a5f2d6fdb87 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 31 Jul 2024 14:10:00 -0400 Subject: [PATCH 4/9] Remove bumpversion and rely on jaraco.develop for finalize operation. --- .bumpversion.cfg | 6 ----- tools/finalize.py | 62 ----------------------------------------------- tox.ini | 3 +-- 3 files changed, 1 insertion(+), 70 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 tools/finalize.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 0cb5e7c880..0000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[bumpversion] -current_version = 80.9.0 -commit = True -tag = True - -[bumpversion:file:pyproject.toml] diff --git a/tools/finalize.py b/tools/finalize.py deleted file mode 100644 index d646e67cd0..0000000000 --- a/tools/finalize.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Finalize the repo for a release. Invokes towncrier and bumpversion. -""" - -__requires__ = ['bump2version', 'towncrier', 'jaraco.develop>=7.21'] - - -import pathlib -import re -import subprocess -import sys - -from jaraco.develop import towncrier - -bump_version_command = [ - sys.executable, - '-m', - 'bumpversion', - towncrier.release_kind(), -] - - -def get_version(): - cmd = bump_version_command + ['--dry-run', '--verbose'] - out = subprocess.check_output(cmd, text=True, encoding='utf-8') - return re.search('^new_version=(.*)', out, re.MULTILINE).group(1) - - -def update_changelog(): - towncrier.run('build', '--yes') - _repair_changelog() - - -def _repair_changelog(): - """ - Workaround for #2666 - """ - changelog_fn = pathlib.Path('NEWS.rst') - changelog = changelog_fn.read_text(encoding='utf-8') - fixed = re.sub(r'^(v[0-9.]+)v[0-9.]+$', r'\1', changelog, flags=re.M) - changelog_fn.write_text(fixed, encoding='utf-8') - subprocess.check_output(['git', 'add', changelog_fn]) - - -def bump_version(): - cmd = bump_version_command + ['--allow-dirty'] - subprocess.check_call(cmd) - - -def ensure_config(): - """ - Double-check that Git has an e-mail configured. - """ - subprocess.check_output(['git', 'config', 'user.email']) - - -if __name__ == '__main__': - print("Cutting release at", get_version()) - ensure_config() - towncrier.check_changes() - update_changelog() - bump_version() diff --git a/tox.ini b/tox.ini index 942e2b9835..11e8b77511 100644 --- a/tox.ini +++ b/tox.ini @@ -63,11 +63,10 @@ description = assemble changelog and tag a release skip_install = True deps = towncrier - bump2version jaraco.develop >= 7.23 pass_env = * commands = - python tools/finalize.py + python -m jaraco.develop.finalize [testenv:vendor] skip_install = True From 28d4522d57a6f0484751bc1185163a350f3f8d3c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 18:44:54 +0100 Subject: [PATCH 5/9] Remove 'setup.py saveopts' step from tox.ini Should be no longer necessary now that `setup.cfg` is gone. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 11e8b77511..7df82a64b9 100644 --- a/tox.ini +++ b/tox.ini @@ -101,8 +101,6 @@ setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" - # unset tag_build and tag_date pypa/setuptools#2500 - python setup.py egg_info -Db "" saveopts python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release From d50b3cbd0ec086a1ba2484b2f012bb60f758352e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 18:54:56 +0100 Subject: [PATCH 6/9] Prevent shallow history in GHA to mess with version inference --- .github/workflows/main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52f11528ae..583fbc168e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,6 +70,8 @@ jobs: timeout-minutes: 75 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install build dependencies # Install dependencies for building packages on pre-release Pythons # jaraco/skeleton#161 @@ -182,6 +184,8 @@ jobs: timeout-minutes: 75 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Cygwin with Python uses: cygwin/cygwin-install-action@v4 with: @@ -243,6 +247,8 @@ jobs: timeout-minutes: 75 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install OS-level dependencies run: | sudo apt-get update @@ -268,6 +274,8 @@ jobs: timeout-minutes: 75 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: From 723ca9213fad32346a99bc67dd82aa766a3d6332 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 20:28:48 +0100 Subject: [PATCH 7/9] Ensure version inference falls back to PKG-INFO when building wheel from sdist --- setup.py | 6 +++++- setuptools/tests/test_setuptools.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b169179ea..c726434bd2 100755 --- a/setup.py +++ b/setup.py @@ -36,8 +36,12 @@ def _get_version() -> str: version = subprocess.check_output(cmd, encoding="utf-8") return _normalization.best_effort_version(version, "{safe}.dev+{sanitized}") except subprocess.CalledProcessError: # e.g.: git not installed or history missing + if os.path.exists("PKG-INFO"): # building wheel from sdist + with open("PKG-INFO", encoding="utf-8") as fp: + if match := re.search(r"^Version: (\d+\.\d+\.\d+.*)$", fp.read(), re.M): + return match[1] with open("NEWS.rst", encoding="utf-8") as fp: - match = re.search(r"v\d+\.\d+.\d+", fp.read()) # latest version + match = re.search(r"v\d+\.\d+\.\d+", fp.read()) # latest version return f"{match[0] if match else '0.0.0'}.dev+{time.strftime('%Y%m%d')}" diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 1d56e1a8a4..39d49f4fcd 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -265,6 +265,15 @@ def test_findall_missing_symlink(tmpdir): assert found == [] +def test_setuptools_own_dists_infers_version(setuptools_sdist, setuptools_wheel): + # Sanity check + # Validates that `setup.py:_get_version` works as expected + assert "0.0.0" not in setuptools_sdist.name + # Validates that `setup.py:_get_version` guarantees the fallback + # code path is finding something in PKG-INFO: + assert "0.0.0" not in setuptools_wheel.name + + @pytest.mark.xfail(reason="unable to exclude tests; #4475 #3260") def test_its_own_wheel_does_not_contain_tests(setuptools_wheel): with ZipFile(setuptools_wheel) as zipfile: From f1956af43e859dc29d924c396c97478dccaed8ca Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Apr 2025 21:10:40 +0100 Subject: [PATCH 8/9] Add news fragment --- newsfragments/4948.misc.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 newsfragments/4948.misc.rst diff --git a/newsfragments/4948.misc.rst b/newsfragments/4948.misc.rst new file mode 100644 index 0000000000..31fa413053 --- /dev/null +++ b/newsfragments/4948.misc.rst @@ -0,0 +1,4 @@ +Setuptools stopped using ``egg_info --tag-build --tag-date`` for its own build +process. Instead version is now inferred from git tag history. +To ensure the proper version is considered, please make sure to fetch the git tags. +Local development tags are added to the latest version if the latest commit is not tagged. From 06343cf7d38ba8c0741486d2d05b1aeb543e901c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 29 May 2025 11:37:39 +0100 Subject: [PATCH 9/9] Refactor _get_version in setup.py --- setup.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index c726434bd2..3f4736f340 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import annotations import os import re @@ -6,6 +7,8 @@ import sys import textwrap import time +from contextlib import suppress +from itertools import starmap import setuptools from setuptools import _normalization @@ -30,19 +33,27 @@ package_data.setdefault('setuptools.command', []).extend(['*.xml']) +_VERSION_FALLBACK = [ + ("PKG-INFO", r"^Version: (\d+\.\d+\.\d+.*)$", "{match[1]}"), # sdist + ("NEWS.rst", r"^v(\d+\.\d+\.\d+)", "{match[1]}.dev+{timestamp}"), # latest version +] + + +def _extract_version(file: str, pattern: str, fmt: str) -> str | None: + with suppress(FileNotFoundError), open(file, encoding="utf-8") as fp: + if match := re.search(pattern, fp.read(), re.M): + return fmt.format(match=match, timestamp=time.strftime("%Y%m%d")) + return None + + def _get_version() -> str: cmd = ["git", "describe", "--abbrev", "--match", "v?[0-9]*", "--dirty"] try: version = subprocess.check_output(cmd, encoding="utf-8") return _normalization.best_effort_version(version, "{safe}.dev+{sanitized}") except subprocess.CalledProcessError: # e.g.: git not installed or history missing - if os.path.exists("PKG-INFO"): # building wheel from sdist - with open("PKG-INFO", encoding="utf-8") as fp: - if match := re.search(r"^Version: (\d+\.\d+\.\d+.*)$", fp.read(), re.M): - return match[1] - with open("NEWS.rst", encoding="utf-8") as fp: - match = re.search(r"v\d+\.\d+\.\d+", fp.read()) # latest version - return f"{match[0] if match else '0.0.0'}.dev+{time.strftime('%Y%m%d')}" + candidates = filter(None, starmap(_extract_version, _VERSION_FALLBACK)) + return next(candidates, f"0.0.0.dev+{time.strftime('%Y%m%d')}") def pypi_link(pkg_filename):