Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog


## v9.2.0

### fixed

- fix #1216: accept and create a warning for usages of `version = attr:` in setuptools config.
unfortunately dozens of projects cargo-culted that antipattern


## v9.2.0

Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two ## v9.2.0 sections in the changelog, which will create confusion. The second one should likely be a different version number or these sections should be merged.

Suggested change

Copilot uses AI. Check for mistakes.

### Added
Expand Down
1 change: 0 additions & 1 deletion src/setuptools_scm/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ def from_data(
# Handle nested SCM configuration

scm_config = ScmConfiguration.from_data(scm_data)

return cls(
relative_to=relative_to,
version_cls=version_cls,
Expand Down
18 changes: 17 additions & 1 deletion src/setuptools_scm/_integration/pyproject_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def read_pyproject(
tool_name: str = DEFAULT_TOOL_NAME,
canonical_build_package_name: str = "setuptools-scm",
_given_result: _t.GivenPyProjectResult = None,
_given_definition: TOML_RESULT | None = None,
Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _given_definition parameter appears to be a testing-only feature but lacks documentation in the docstring. Consider adding documentation for this parameter or making it clear that it's for internal testing use only.

Copilot uses AI. Check for mistakes.

) -> PyProjectData:
"""Read and parse pyproject configuration.

Expand All @@ -195,7 +196,10 @@ def read_pyproject(
if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)):
raise _given_result

defn = read_toml_content(path)
if _given_definition is not None:
Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The conditional logic for _given_definition vs read_toml_content(path) should be moved earlier in the function, before the existing _given_result handling, to maintain consistent parameter precedence and avoid potential confusion about which takes priority.

Copilot uses AI. Check for mistakes.

defn = _given_definition
else:
defn = read_toml_content(path)

requires: list[str] = defn.get("build-system", {}).get("requires", [])
is_required = has_build_package(requires, canonical_build_package_name)
Expand Down Expand Up @@ -224,6 +228,18 @@ def read_pyproject(
requires,
)

setuptools_dynamic_version = (
defn.get("tool", {})
.get("setuptools", {})
.get("dynamic", {})
.get("version", None)
)
if setuptools_dynamic_version is not None:
warnings.warn(
f"{path}: at [tool.setuptools.dynamic]\n"
"version = {attr = ...} is sabotaging setuptools-scm"
)

return pyproject_data


Expand Down
7 changes: 7 additions & 0 deletions src/setuptools_scm/_integration/setup_cfg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import warnings

from dataclasses import dataclass
from pathlib import Path
Expand All @@ -25,6 +26,12 @@ def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBas

name = parser.get("metadata", "name", fallback=None)
version = parser.get("metadata", "version", fallback=None)
if version is not None and "attr" in version:
warnings.warn(
"setup.cfg: ignoring invalid dynamic version - version = attr: ..."
" is sabotaging setuptools-scm"
)
version = None
return SetuptoolsBasicData(path=path, name=name, version=version)


Expand Down
24 changes: 24 additions & 0 deletions testing/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from setuptools_scm._integration import setuptools as setuptools_integration
from setuptools_scm._integration.pyproject_reading import PyProjectData
from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData
from setuptools_scm._integration.setup_cfg import read_setup_cfg
from setuptools_scm._requirement_cls import extract_package_name

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


@pytest.mark.issue(1216)
def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None:
cfg = tmp_path / "setup.cfg"
cfg.write_text(
textwrap.dedent(
"""
[metadata]
name = example-broken
version = attr: example_broken.__version__
"""
),
encoding="utf-8",
)

with pytest.warns(
UserWarning,
match="setup.cfg: ignoring invalid dynamic version - version = attr: ... is sabotaging setuptools-scm",
):
legacy_data = read_setup_cfg(cfg)

assert legacy_data.version is None


def test_setup_cfg_version_prevents_inference_version_keyword(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
38 changes: 38 additions & 0 deletions testing/test_pyproject_reading.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from pathlib import Path
from unittest.mock import Mock

import pytest

Expand Down Expand Up @@ -108,3 +109,40 @@ def test_invalid_requirement_string(self) -> None:
assert (
has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
)


def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that read_pyproject reads existing files correctly."""
monkeypatch.setattr(
"setuptools_scm._integration.pyproject_reading.read_toml_content",
Mock(side_effect=FileNotFoundError("this test should not read")),
)

res = read_pyproject(
_given_definition={
"build-system": {"requires": ["setuptools-scm[simple]"]},
"project": {"name": "test-package", "dynamic": ["version"]},
}
)

assert res.should_infer()


def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None:
with pytest.warns(
UserWarning,
match=r"pyproject.toml: at \[tool\.setuptools\.dynamic\]\n"
r"version = {attr = \.\.\.} is sabotaging setuptools-scm",
):
pyproject_data = read_pyproject(
_given_definition={
"build-system": {"requires": ["setuptools-scm[simple]"]},
"project": {"name": "test-package", "dynamic": ["version"]},
"tool": {
"setuptools": {
"dynamic": {"version": {"attr": "test_package.__version__"}}
}
},
}
)
assert pyproject_data.project_version is None
Loading