Skip to content

Commit 70f6942

Browse files
Merge pull request #1219 from RonnyPfannschmidt/fix-1216-explicitly-deprecate-setuptools-dynamic-version-when-active
Fix 1216 explicitly deprecate setuptools dynamic version when active
2 parents bfd87c5 + 14d85c0 commit 70f6942

File tree

11 files changed

+191
-26
lines changed

11 files changed

+191
-26
lines changed

.github/workflows/api-check.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,48 @@ jobs:
3232
run: |
3333
pip install -U pip setuptools
3434
pip install -e .[test]
35+
pip install griffe
3536
3637
- name: Run griffe API check
3738
id: griffe-check
39+
continue-on-error: true
3840
run: |
41+
echo "Running griffe API stability check..."
3942
if griffe check setuptools_scm -ssrc -f github; then
4043
echo "api_check_result=success" >> $GITHUB_OUTPUT
44+
echo "exit_code=0" >> $GITHUB_OUTPUT
4145
else
46+
exit_code=$?
4247
echo "api_check_result=warning" >> $GITHUB_OUTPUT
43-
echo "API stability check detected changes but will not fail the build" >> $GITHUB_STEP_SUMMARY
48+
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
49+
exit $exit_code
4450
fi
4551
4652
- name: Report API check result
47-
if: steps.griffe-check.outputs.api_check_result == 'warning'
53+
if: always()
4854
uses: actions/github-script@v8
4955
with:
5056
script: |
51-
core.warning('API stability check detected breaking changes. Please review the API changes above.')
52-
core.summary.addRaw('⚠️ API Stability Warning: Breaking changes detected in the public API')
53-
await core.summary.write()
57+
const result = '${{ steps.griffe-check.outputs.api_check_result }}'
58+
const exitCode = '${{ steps.griffe-check.outputs.exit_code }}'
59+
60+
if (result === 'success') {
61+
core.notice('API stability check passed - no breaking changes detected')
62+
await core.summary
63+
.addHeading('✅ API Stability Check: Passed', 2)
64+
.addRaw('No breaking changes detected in the public API')
65+
.write()
66+
} else if (result === 'warning') {
67+
core.warning(`API stability check detected breaking changes (exit code: ${exitCode}). Please review the API changes above.`)
68+
await core.summary
69+
.addHeading('⚠️ API Stability Warning', 2)
70+
.addRaw('Breaking changes detected in the public API. Please review the changes reported above.')
71+
.addRaw(`\n\nExit code: ${exitCode}`)
72+
.write()
73+
} else {
74+
core.error('API stability check failed to run properly')
75+
await core.summary
76+
.addHeading('❌ API Stability Check: Failed', 2)
77+
.addRaw('The griffe check failed to execute. This may indicate griffe is not installed or there was an error.')
78+
.write()
79+
}

.github/workflows/python-tests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
msystem: MINGW64
6969
install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools
7070
update: true
71-
- name: Setup GnuPG
71+
- name: Setup GnuPG and Mercurial on Windows
7272
# At present, the Windows VMs only come with the copy of GnuPG that's bundled
7373
# with Git for Windows. If we want to use this version _and_ be able to set
7474
# arbitrary GnuPG home directories, then the test would need to figure out when
@@ -84,11 +84,15 @@ jobs:
8484
# Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows
8585
# doesn't invoke the bundled GnuPG, otherwise we'll run into
8686
# <https://dev.gnupg.org/T5504>. See also: <https://dev.gnupg.org/T3020>.
87+
#
88+
# Windows runners no longer ship with Mercurial pre-installed, so we install
89+
# it via Chocolatey using the 'hg' package.
8790
run: |
8891
$env:PATH = "C:\Program Files\Git\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\Chocolatey\bin"
8992
[Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine")
90-
choco install gnupg -y --no-progress
93+
choco install gnupg hg -y --no-progress
9194
echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH
95+
echo "C:\Program Files\Mercurial\" >> $env:GITHUB_PATH
9296
git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe"
9397
if: runner.os == 'Windows'
9498
- run: uv sync --group test --group docs --extra rich

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ repos:
1818
hooks:
1919
- id: mypy
2020
args: [--strict]
21-
language_version: "3.10"
2221
additional_dependencies:
2322
- types-setuptools
2423
- tokenize-rt==3.2.0

CHANGELOG.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
4+
## v9.2.1 (unreleased)
5+
6+
### Fixed
7+
8+
- fix #1216: accept and create a warning for usages of `version = attr:` in setuptools config.
9+
unfortunately dozens of projects cargo-culted that antipattern
10+
11+
312
## v9.2.0
413

514
### Added
@@ -11,24 +20,24 @@
1120
version inference is automatically enabled with default settings.
1221

1322

14-
### removed
23+
### Removed
1524

1625
- unchecked simplified activation - too many projects use setups where it would fail
1726

18-
### changed
27+
### Changed
1928

2029
- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools
2130

2231
## v9.1.1 (yanked)
2332

24-
### fixed
33+
### Fixed
2534

2635
- fix #1194: correctly handle version keyword when pyproject metadata is missing
2736

2837

2938
## v9.1.0 (yanked)
3039

31-
### fixed
40+
### Fixed
3241

3342
- complete reiteration of the decision logic for enabling version inference on setuptools_scm
3443

@@ -40,9 +49,9 @@
4049

4150
## v9.0.3 (yanked)
4251

43-
### fixed
52+
### Fixed
4453

45-
- fix 1184: verify version is dynamic if the dependency is used as indicator for enabling
54+
- fix #1184: verify version is dynamic if the dependency is used as indicator for enabling
4655

4756
## v9.0.2 (yanked)
4857

src/setuptools_scm/_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,6 @@ def from_data(
310310
# Handle nested SCM configuration
311311

312312
scm_config = ScmConfiguration.from_data(scm_data)
313-
314313
return cls(
315314
relative_to=relative_to,
316315
version_cls=version_cls,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import warnings
2+
3+
from pathlib import Path
4+
5+
6+
def warn_dynamic_version(path: Path, section: str, expression: str) -> None:
7+
warnings.warn(
8+
f"{path}: at [{section}]\n"
9+
f"{expression} is forcing setuptools to override the version setuptools-scm did already set\n"
10+
"When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n"
11+
"Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors."
12+
)
13+
14+
15+
def warn_pyproject_setuptools_dynamic_version(path: Path) -> None:
16+
warn_dynamic_version(path, "tool.setuptools.dynamic", "version = {attr = ...}")
17+
18+
19+
def warn_setup_cfg_dynamic_version(path: Path) -> None:
20+
warn_dynamic_version(path, "metadata", "version = attr: ...")

src/setuptools_scm/_integration/pyproject_reading.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,23 @@ def read_pyproject(
174174
tool_name: str = DEFAULT_TOOL_NAME,
175175
canonical_build_package_name: str = "setuptools-scm",
176176
_given_result: _t.GivenPyProjectResult = None,
177+
_given_definition: TOML_RESULT | None = None,
177178
) -> PyProjectData:
178179
"""Read and parse pyproject configuration.
179180
180-
This function supports dependency injection for tests via `_given_result`.
181-
182-
Parameters:
183-
- path: Path to the pyproject file
184-
- tool_name: The tool section name (default: `setuptools_scm`)
185-
- canonical_build_package_name: Normalized build requirement name
186-
- _given_result: Optional testing hook. Can be:
187-
- PyProjectData: returned directly
188-
- InvalidTomlError | FileNotFoundError: raised directly
189-
- None: read from filesystem
181+
This function supports dependency injection for tests via ``_given_result``
182+
and ``_given_definition``.
183+
184+
:param path: Path to the pyproject file
185+
:param tool_name: The tool section name (default: ``setuptools_scm``)
186+
:param canonical_build_package_name: Normalized build requirement name
187+
:param _given_result: Optional testing hook. Can be:
188+
- ``PyProjectData``: returned directly
189+
- ``InvalidTomlError`` | ``FileNotFoundError``: raised directly
190+
- ``None``: read from filesystem (default)
191+
:param _given_definition: Optional testing hook to provide parsed TOML content.
192+
When provided, this dictionary is used instead of reading and parsing
193+
the file from disk. Ignored if ``_given_result`` is provided.
190194
"""
191195

192196
if _given_result is not None:
@@ -195,7 +199,10 @@ def read_pyproject(
195199
if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)):
196200
raise _given_result
197201

198-
defn = read_toml_content(path)
202+
if _given_definition is not None:
203+
defn = _given_definition
204+
else:
205+
defn = read_toml_content(path)
199206

200207
requires: list[str] = defn.get("build-system", {}).get("requires", [])
201208
is_required = has_build_package(requires, canonical_build_package_name)
@@ -224,6 +231,17 @@ def read_pyproject(
224231
requires,
225232
)
226233

234+
setuptools_dynamic_version = (
235+
defn.get("tool", {})
236+
.get("setuptools", {})
237+
.get("dynamic", {})
238+
.get("version", None)
239+
)
240+
if setuptools_dynamic_version is not None:
241+
from .deprecation import warn_pyproject_setuptools_dynamic_version
242+
243+
warn_pyproject_setuptools_dynamic_version(path)
244+
227245
return pyproject_data
228246

229247

src/setuptools_scm/_integration/setup_cfg.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBas
2525

2626
name = parser.get("metadata", "name", fallback=None)
2727
version = parser.get("metadata", "version", fallback=None)
28+
if version is not None and "attr" in version:
29+
from .deprecation import warn_setup_cfg_dynamic_version
30+
31+
warn_setup_cfg_dynamic_version(path)
32+
version = None
2833
return SetuptoolsBasicData(path=path, name=name, version=version)
2934

3035

testing/test_deprecation.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Test deprecation warnings and their exact text."""
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from setuptools_scm._integration.deprecation import warn_dynamic_version
8+
9+
10+
def test_warn_dynamic_version_full_text() -> None:
11+
"""Test the complete warning text for warn_dynamic_version function."""
12+
test_path = Path("test_file.toml")
13+
expected_warning = (
14+
f"{test_path}: at [test.section]\n"
15+
"test_expression is forcing setuptools to override the version setuptools-scm did already set\n"
16+
"When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n"
17+
"Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors."
18+
)
19+
20+
with pytest.warns(UserWarning) as warning_info: # noqa: PT030
21+
warn_dynamic_version(test_path, "test.section", "test_expression")
22+
23+
assert len(warning_info) == 1
24+
assert str(warning_info[0].message) == expected_warning

testing/test_integration.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from setuptools_scm._integration import setuptools as setuptools_integration
1919
from setuptools_scm._integration.pyproject_reading import PyProjectData
2020
from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData
21+
from setuptools_scm._integration.setup_cfg import read_setup_cfg
2122
from setuptools_scm._requirement_cls import extract_package_name
2223

2324
if TYPE_CHECKING:
@@ -457,6 +458,29 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None:
457458
assert data.version == "1.2.3"
458459

459460

461+
@pytest.mark.issue(1216)
462+
def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None:
463+
cfg = tmp_path / "setup.cfg"
464+
cfg.write_text(
465+
textwrap.dedent(
466+
"""
467+
[metadata]
468+
name = example-broken
469+
version = attr: example_broken.__version__
470+
"""
471+
),
472+
encoding="utf-8",
473+
)
474+
475+
with pytest.warns(
476+
UserWarning,
477+
match=r"setup\.cfg: at \[metadata\]",
478+
):
479+
legacy_data = read_setup_cfg(cfg)
480+
481+
assert legacy_data.version is None
482+
483+
460484
def test_setup_cfg_version_prevents_inference_version_keyword(
461485
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
462486
) -> None:

0 commit comments

Comments
 (0)