Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions src/setuptools_scm/_integration/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import warnings

from pathlib import Path


def warn_dynamic_version(path: Path, section: str, expression: str) -> None:
warnings.warn(
f"{path}: at [{section}]\n"
f"{expression} forcing setuptools to override the version setuptools-scm sets\n"

Choose a reason for hiding this comment

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

English isn't my first language, but I think there's a missing "is" before "forcing", and possibly a missing "that" before "setuptools-scm".

"When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n"
Copy link

@pjonsson pjonsson Aug 26, 2025

Choose a reason for hiding this comment

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

Missing space in removeit.

Edit: its => it is (or it's)

"Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors."
)


def warn_pyproject_setuptools_dynamic_version(path: Path) -> None:
warn_dynamic_version(path, "tool.setuptools.dynamic", "version = {attr = ...}")


def warn_setup_cfg_dynamic_version(path: Path) -> None:
warn_dynamic_version(path, "metadata", "version = attr: ...}")
17 changes: 16 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,17 @@ def read_pyproject(
requires,
)

setuptools_dynamic_version = (
defn.get("tool", {})
.get("setuptools", {})
.get("dynamic", {})
.get("version", None)
)
if setuptools_dynamic_version is not None:
from .deprecation import warn_pyproject_setuptools_dynamic_version

warn_pyproject_setuptools_dynamic_version(path)

return pyproject_data


Expand Down
5 changes: 5 additions & 0 deletions src/setuptools_scm/_integration/setup_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ 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:
from .deprecation import warn_setup_cfg_dynamic_version

warn_setup_cfg_dynamic_version(path)
version = None
return SetuptoolsBasicData(path=path, name=name, version=version)


Expand Down
24 changes: 24 additions & 0 deletions testing/test_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Test deprecation warnings and their exact text."""

from pathlib import Path

import pytest

from setuptools_scm._integration.deprecation import warn_dynamic_version


def test_warn_dynamic_version_full_text() -> None:
"""Test the complete warning text for warn_dynamic_version function."""
test_path = Path("test_file.toml")
expected_warning = (
f"{test_path}: at [test.section]\n"
"test_expression forcing setuptools to override the version setuptools-scm sets\n"
"When using setuptools-scm its invalid to use setuptools dynamic version as well, please removeit.\n"
"Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors."
)

with pytest.warns(UserWarning) as warning_info: # noqa: PT030
warn_dynamic_version(test_path, "test.section", "test_expression")

assert len(warning_info) == 1
assert str(warning_info[0].message) == expected_warning
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=r"setup\.cfg: at \[metadata\]",
):
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
37 changes: 37 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,39 @@ 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\]",
):
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