diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1a9961..87fa9dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### Added diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 81a78e84..49fac2a4 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -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, diff --git a/src/setuptools_scm/_integration/deprecation.py b/src/setuptools_scm/_integration/deprecation.py new file mode 100644 index 00000000..efb5e372 --- /dev/null +++ b/src/setuptools_scm/_integration/deprecation.py @@ -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" + "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." + ) + + +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: ...}") diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index f041484f..58abce74 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -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, ) -> PyProjectData: """Read and parse pyproject configuration. @@ -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: + 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) @@ -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 diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py index 4e485600..893a9ad4 100644 --- a/src/setuptools_scm/_integration/setup_cfg.py +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -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) diff --git a/testing/test_deprecation.py b/testing/test_deprecation.py new file mode 100644 index 00000000..d738eb55 --- /dev/null +++ b/testing/test_deprecation.py @@ -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 diff --git a/testing/test_integration.py b/testing/test_integration.py index be6e3cfe..e85b5bba 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -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: @@ -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: diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py index 1962882a..dc26e955 100644 --- a/testing/test_pyproject_reading.py +++ b/testing/test_pyproject_reading.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from unittest.mock import Mock import pytest @@ -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