1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
|
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
import pytest
from setuptools_scm import Configuration
from setuptools_scm import dump_version
from setuptools_scm import get_version
from setuptools_scm._overrides import PRETEND_KEY
from setuptools_scm._run_cmd import has_command
from setuptools_scm.version import format_version
from setuptools_scm.version import guess_next_version
from setuptools_scm.version import meta
from setuptools_scm.version import tag_to_version
c = Configuration()
@pytest.mark.parametrize(
("tag", "expected"),
[
("1.1", "1.2"),
("1.2.dev", "1.2"),
("1.1a2", "1.1a3"),
pytest.param(
"23.24.post2+deadbeef",
"23.24.post3",
marks=pytest.mark.filterwarnings(
"ignore:.*will be stripped of its suffix.*:UserWarning"
),
),
],
)
def test_next_tag(tag: str, expected: str) -> None:
version = meta(tag, config=c)
assert guess_next_version(version) == expected
VERSIONS = {
"exact": meta("1.1", distance=0, dirty=False, config=c),
"dirty": meta("1.1", distance=0, dirty=True, config=c),
"distance-clean": meta("1.1", distance=3, dirty=False, config=c),
"distance-dirty": meta("1.1", distance=3, dirty=True, config=c),
}
# Versions with build metadata in the tag
VERSIONS_WITH_BUILD_METADATA = {
"exact-build": meta("1.1+build.123", distance=0, dirty=False, config=c),
"dirty-build": meta("1.1+build.123", distance=0, dirty=True, config=c),
"distance-clean-build": meta("1.1+build.123", distance=3, dirty=False, config=c),
"distance-dirty-build": meta("1.1+build.123", distance=3, dirty=True, config=c),
"exact-ci": meta("2.0.0+ci.456", distance=0, dirty=False, config=c),
"dirty-ci": meta("2.0.0+ci.456", distance=0, dirty=True, config=c),
"distance-clean-ci": meta("2.0.0+ci.456", distance=2, dirty=False, config=c),
"distance-dirty-ci": meta("2.0.0+ci.456", distance=2, dirty=True, config=c),
}
@pytest.mark.parametrize(
("version", "version_scheme", "local_scheme", "expected"),
[
("exact", "guess-next-dev", "node-and-date", "1.1"),
("dirty", "guess-next-dev", "node-and-date", "1.2.dev0+d20090213"),
("dirty", "guess-next-dev", "no-local-version", "1.2.dev0"),
("distance-clean", "guess-next-dev", "node-and-date", "1.2.dev3"),
("distance-dirty", "guess-next-dev", "node-and-date", "1.2.dev3+d20090213"),
("exact", "post-release", "node-and-date", "1.1"),
("dirty", "post-release", "node-and-date", "1.1.post0+d20090213"),
("distance-clean", "post-release", "node-and-date", "1.1.post3"),
("distance-dirty", "post-release", "node-and-date", "1.1.post3+d20090213"),
],
)
def test_format_version(
version: str, version_scheme: str, local_scheme: str, expected: str
) -> None:
from dataclasses import replace
scm_version = VERSIONS[version]
configured_version = replace(
scm_version,
config=replace(
scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme
),
)
assert format_version(configured_version) == expected
@pytest.mark.parametrize(
("version", "version_scheme", "local_scheme", "expected"),
[
# Exact matches should preserve build metadata from tag
("exact-build", "guess-next-dev", "node-and-date", "1.1+build.123"),
("exact-build", "guess-next-dev", "no-local-version", "1.1+build.123"),
("exact-ci", "guess-next-dev", "node-and-date", "2.0.0+ci.456"),
("exact-ci", "guess-next-dev", "no-local-version", "2.0.0+ci.456"),
# Dirty exact matches - version scheme treats dirty as non-exact, build metadata preserved
(
"dirty-build",
"guess-next-dev",
"node-and-date",
"1.2.dev0+build.123.d20090213",
),
("dirty-build", "guess-next-dev", "no-local-version", "1.2.dev0+build.123"),
("dirty-ci", "guess-next-dev", "node-and-date", "2.0.1.dev0+ci.456.d20090213"),
# Distance cases - build metadata should be preserved and combined with SCM data
(
"distance-clean-build",
"guess-next-dev",
"node-and-date",
"1.2.dev3+build.123",
),
(
"distance-clean-build",
"guess-next-dev",
"no-local-version",
"1.2.dev3+build.123",
),
("distance-clean-ci", "guess-next-dev", "node-and-date", "2.0.1.dev2+ci.456"),
# Distance + dirty cases - build metadata should be preserved and combined with SCM data
(
"distance-dirty-build",
"guess-next-dev",
"node-and-date",
"1.2.dev3+build.123.d20090213",
),
(
"distance-dirty-ci",
"guess-next-dev",
"node-and-date",
"2.0.1.dev2+ci.456.d20090213",
),
# Post-release scheme tests
("exact-build", "post-release", "node-and-date", "1.1+build.123"),
(
"dirty-build",
"post-release",
"node-and-date",
"1.1.post0+build.123.d20090213",
),
(
"distance-clean-build",
"post-release",
"node-and-date",
"1.1.post3+build.123",
),
(
"distance-dirty-build",
"post-release",
"node-and-date",
"1.1.post3+build.123.d20090213",
),
],
)
def test_format_version_with_build_metadata(
version: str, version_scheme: str, local_scheme: str, expected: str
) -> None:
"""Test format_version with tags that contain build metadata."""
from dataclasses import replace
from packaging.version import Version
scm_version = VERSIONS_WITH_BUILD_METADATA[version]
configured_version = replace(
scm_version,
config=replace(
scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme
),
)
result = format_version(configured_version)
# Validate result is a valid PEP 440 version
parsed = Version(result)
assert str(parsed) == result, f"Result should be valid PEP 440: {result}"
assert result == expected, f"Expected {expected}, got {result}"
def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
write_to = "VERSION"
version = str(VERSIONS["exact"].tag)
scm_version = meta(VERSIONS["exact"].tag, config=c)
with pytest.raises(ValueError, match=r"^bad file format:"):
dump_version(tmp_path, version, write_to, scm_version=scm_version)
@pytest.mark.parametrize(
"version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
)
def test_dump_version_works_with_pretend(
version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(PRETEND_KEY, version)
name = "VERSION.txt"
target = tmp_path.joinpath(name)
get_version(root=tmp_path, write_to=name)
assert target.read_text(encoding="utf-8") == version
def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
version = "1.2.3"
monkeypatch.setenv(PRETEND_KEY, version)
name = "VERSION.txt"
project = tmp_path.joinpath("project")
target = project.joinpath(name)
project.mkdir()
get_version(root="..", relative_to=target, version_file=name)
assert target.read_text(encoding="utf-8") == version
def dump_a_version(tmp_path: Path) -> None:
from setuptools_scm._integration.dump_version import write_version_to_path
version = "1.2.3"
scm_version = meta(version, config=c)
write_version_to_path(
tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
)
def test_dump_version_on_old_python(tmp_path: Path) -> None:
python37 = shutil.which("python3.7")
if python37 is None:
pytest.skip("python3.7 not found")
dump_a_version(tmp_path)
subprocess.run(
[python37, "-c", "import VERSION;print(VERSION.version)"],
cwd=tmp_path,
check=True,
)
def test_dump_version_mypy(tmp_path: Path) -> None:
mypy = shutil.which("mypy")
if mypy is None:
pytest.skip("mypy not found")
dump_a_version(tmp_path)
subprocess.run(
[mypy, "--python-version=3.8", "--strict", "VERSION.py"],
cwd=tmp_path,
check=True,
)
def test_dump_version_flake8(tmp_path: Path) -> None:
flake8 = shutil.which("flake8")
if flake8 is None:
pytest.skip("flake8 not found")
dump_a_version(tmp_path)
subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
def test_dump_version_ruff(tmp_path: Path) -> None:
ruff = shutil.which("ruff")
if ruff is None:
pytest.skip("ruff not found")
dump_a_version(tmp_path)
subprocess.run([ruff, "check", "--no-fix", "VERSION.py"], cwd=tmp_path, check=True)
def test_has_command() -> None:
with pytest.warns(RuntimeWarning, match="yadayada"):
assert not has_command("yadayada_setuptools_aint_ne")
def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None:
"""
If the name provided to has_command() exists as a command, but gives a non-zero
return code, there should be a log message generated.
"""
with pytest.warns(RuntimeWarning, match="ls"):
has_command("ls", ["--a-flag-that-doesnt-exist-should-give-output-on-stderr"])
found_it = False
for record in caplog.records:
if "returned non-zero. This is stderr" in record.message:
found_it = True
assert found_it, "Did not find expected log record for "
@pytest.mark.parametrize(
("tag", "expected_version"),
[
("1.1", "1.1"),
("release-1.1", "1.1"),
pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
],
)
def test_tag_to_version(tag: str, expected_version: str) -> None:
version = str(tag_to_version(tag, c))
assert version == expected_version
def test_write_version_to_path_deprecation_warning_none(tmp_path: Path) -> None:
"""Test that write_version_to_path warns when scm_version=None is passed."""
from setuptools_scm._integration.dump_version import write_version_to_path
target_file = tmp_path / "version.py"
# This should raise a deprecation warning when scm_version=None is explicitly passed
with pytest.warns(
DeprecationWarning, match="write_version_to_path called without scm_version"
):
write_version_to_path(
target=target_file,
template=None, # Use default template
version="1.2.3",
scm_version=None, # Explicitly passing None should warn
)
# Verify the file was created and contains the expected content
assert target_file.exists()
content = target_file.read_text(encoding="utf-8")
# Check that the version is correctly formatted
assert "__version__ = version = '1.2.3'" in content
assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
# Check that commit_id is set to None when scm_version is None
assert "__commit_id__ = commit_id = None" in content
def test_write_version_to_path_deprecation_warning_missing(tmp_path: Path) -> None:
"""Test that write_version_to_path warns when scm_version parameter is not provided."""
from setuptools_scm._integration.dump_version import write_version_to_path
target_file = tmp_path / "version.py"
# This should raise a deprecation warning when scm_version is not provided
with pytest.warns(
DeprecationWarning, match="write_version_to_path called without scm_version"
):
write_version_to_path(
target=target_file,
template=None, # Use default template
version="1.2.3",
# scm_version not provided - should warn
)
# Verify the file was created and contains the expected content
assert target_file.exists()
content = target_file.read_text(encoding="utf-8")
# Check that the version is correctly formatted
assert "__version__ = version = '1.2.3'" in content
assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
# Check that commit_id is set to None when scm_version is None
assert "__commit_id__ = commit_id = None" in content
|