Skip to content

Commit 8ac404a

Browse files
Merge pull request #1152 from RonnyPfannschmidt/revise
revise importing on python 3.9 + bugfixes
2 parents 71e657b + d9d8ed2 commit 8ac404a

File tree

10 files changed

+133
-38
lines changed

10 files changed

+133
-38
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ repos:
77
- id: debug-statements
88

99
- repo: https://github.com/astral-sh/ruff-pre-commit
10-
rev: v0.12.3
10+
rev: v0.12.4
1111
hooks:
12-
- id: ruff
12+
- id: ruff-check
1313
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
1414
- id: ruff-format
1515

1616
- repo: https://github.com/pre-commit/mirrors-mypy
17-
rev: v1.16.1
17+
rev: v1.17.0
1818
hooks:
1919
- id: mypy
2020
args: [--strict]

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
7+
- add `setuptools-scm` console_scripts entry point to make the CLI directly executable
8+
- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND`
9+
10+
### Changed
11+
12+
- add `pip` to test optional dependencies for improved uv venv compatibility
13+
- migrate to selectable entrypoints for better extensibility
14+
- improve typing for entry_points
15+
16+
### Fixed
17+
18+
- fix #1145: ensure GitWorkdir.get_head_date returns consistent UTC dates regardless of local timezone
19+
- fix #687: ensure calendar versioning tests use consistent time context to prevent failures around midnight in non-UTC timezones
20+
- reintroduce Python 3.9 entrypoints shim for compatibility
21+
- fix #1136: update customizing.md to fix missing import
22+
323
## v8.3.1
424

525
### Fixed

changelog.d/20250612_144312_me_hg_command.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ dependencies = [
4646
"setuptools", # >= 61",
4747
'tomli>=1; python_version < "3.11"',
4848
'typing-extensions; python_version < "3.10"',
49-
'importlib-metadata>=4.6; python_version < "3.10"',
5049
]
5150
[project.optional-dependencies]
5251
docs = [

src/setuptools_scm/_entrypoints.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,29 @@
2121
from ._config import Configuration
2222
from ._config import ParseFunction
2323

24-
if sys.version_info[:2] < (3, 10):
25-
import importlib_metadata as im
26-
else:
27-
from importlib import metadata as im
28-
24+
from importlib import metadata as im
2925

3026
log = _log.log.getChild("entrypoints")
3127

3228

33-
def entry_points(**kw: Any) -> im.EntryPoints:
34-
if sys.version_info[:2] < (3, 10):
35-
import importlib_metadata as im
36-
else:
37-
import importlib.metadata as im
29+
if sys.version_info[:2] < (3, 10):
30+
31+
def entry_points(*, group: str, name: str | None = None) -> list[im.EntryPoint]:
32+
# Python 3.9: entry_points() returns dict, need to handle filtering manually
33+
34+
eps = im.entry_points() # Returns dict
35+
36+
group_eps = eps.get(group, [])
37+
if name is not None:
38+
return [ep for ep in group_eps if ep.name == name]
39+
return group_eps
40+
else:
3841

39-
return im.entry_points(**kw)
42+
def entry_points(*, group: str, name: str | None = None) -> im.EntryPoints:
43+
kw = {"group": group}
44+
if name is not None:
45+
kw["name"] = name
46+
return im.entry_points(**kw)
4047

4148

4249
def version_from_entrypoint(

src/setuptools_scm/_run_cmd.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ def parse_success(
8181
return parse(self.stdout)
8282

8383

84+
KEEP_GIT_ENV = (
85+
"GIT_CEILING_DIRECTORIES",
86+
"GIT_EXEC_PATH",
87+
"GIT_SSH",
88+
"GIT_SSH_COMMAND",
89+
"GIT_AUTHOR_DATE",
90+
"GIT_COMMITTER_DATE",
91+
)
92+
93+
8494
def no_git_env(env: Mapping[str, str]) -> dict[str, str]:
8595
# adapted from pre-commit
8696
# Too many bugs dealing with environment variables and GIT:
@@ -95,11 +105,7 @@ def no_git_env(env: Mapping[str, str]) -> dict[str, str]:
95105
if k.startswith("GIT_"):
96106
log.debug("%s: %s", k, v)
97107
return {
98-
k: v
99-
for k, v in env.items()
100-
if not k.startswith("GIT_")
101-
or k
102-
in ("GIT_CEILING_DIRECTORIES", "GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND")
108+
k: v for k, v in env.items() if not k.startswith("GIT_") or k in KEEP_GIT_ENV
103109
}
104110

105111

src/setuptools_scm/git.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@ def parse_timestamp(timestamp_text: str) -> date | None:
123123
return None
124124
if sys.version_info < (3, 11) and timestamp_text.endswith("Z"):
125125
timestamp_text = timestamp_text[:-1] + "+00:00"
126-
return datetime.fromisoformat(timestamp_text).date()
126+
127+
# Convert to UTC to ensure consistent date regardless of local timezone
128+
dt = datetime.fromisoformat(timestamp_text)
129+
log.debug("dt: %s", dt)
130+
dt_utc = dt.astimezone(timezone.utc).date()
131+
log.debug("dt utc: %s", dt_utc)
132+
return dt_utc
127133

128134
res = run_git(
129135
[

src/setuptools_scm/version.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,12 @@ def meta(
212212
branch: str | None = None,
213213
config: _config.Configuration,
214214
node_date: date | None = None,
215+
time: datetime | None = None,
215216
) -> ScmVersion:
216217
parsed_version = _parse_tag(tag, preformatted, config)
217218
log.info("version %s -> %s", tag, parsed_version)
218219
assert parsed_version is not None, f"Can't parse version {tag}"
219-
return ScmVersion(
220+
scm_version = ScmVersion(
220221
parsed_version,
221222
distance=distance,
222223
node=node,
@@ -226,6 +227,9 @@ def meta(
226227
config=config,
227228
node_date=node_date,
228229
)
230+
if time is not None:
231+
scm_version = dataclasses.replace(scm_version, time=time)
232+
return scm_version
229233

230234

231235
def guess_next_version(tag_version: ScmVersion) -> str:
@@ -365,19 +369,25 @@ def guess_next_date_ver(
365369
head_date = node_date or today
366370
# compute patch
367371
if match is None:
368-
tag_date = today
372+
# For legacy non-date tags, always use patch=0 (treat as "other day")
373+
# Use yesterday to ensure tag_date != head_date
374+
from datetime import timedelta
375+
376+
tag_date = head_date - timedelta(days=1)
369377
else:
370378
tag_date = (
371379
datetime.strptime(match.group("date"), date_fmt)
372380
.replace(tzinfo=timezone.utc)
373381
.date()
374382
)
375383
if tag_date == head_date:
376-
patch = "0" if match is None else (match.group("patch") or "0")
377-
patch = int(patch) + 1
384+
assert match is not None
385+
# Same day as existing date tag - increment patch
386+
patch = int(match.group("patch") or "0") + 1
378387
else:
388+
# Different day or legacy non-date tag - use patch 0
379389
if tag_date > head_date and match is not None:
380-
# warn on future times
390+
# warn on future times (only for actual date tags, not legacy)
381391
warnings.warn(
382392
f"your previous tag ({tag_date}) is ahead your node date ({head_date})"
383393
)

testing/test_git.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,36 @@ def test_git_getdate_git_2_45_0_plus(
515515
assert git_wd.get_head_date() == date(2024, 4, 30)
516516

517517

518+
def test_git_getdate_timezone_consistency(
519+
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
520+
) -> None:
521+
"""Test that get_head_date returns consistent UTC dates regardless of local timezone.
522+
523+
This test forces a git commit with a timestamp that represents a time
524+
after midnight in a positive timezone offset but still the previous day in UTC.
525+
This is the exact scenario that was causing test failures in issue #1145.
526+
"""
527+
# Create a timestamp that's problematic:
528+
# - In Europe/Berlin (UTC+2): 2025-06-12 00:30:00 (June 12th)
529+
# - In UTC: 2025-06-11 22:30:00 (June 11th)
530+
problematic_timestamp = "2025-06-12T00:30:00+02:00"
531+
532+
# Force git to use this specific timestamp for the commit
533+
monkeypatch.setenv("GIT_AUTHOR_DATE", problematic_timestamp)
534+
monkeypatch.setenv("GIT_COMMITTER_DATE", problematic_timestamp)
535+
536+
wd.commit_testfile()
537+
538+
git_wd = git.GitWorkdir(wd.cwd)
539+
result_date = git_wd.get_head_date()
540+
541+
# The correct behavior is to return the UTC date (2025-06-11)
542+
# If the bug is present, it would return the timezone-local date (2025-06-12)
543+
expected_utc_date = date(2025, 6, 11)
544+
545+
assert result_date == expected_utc_date
546+
547+
518548
@pytest.fixture
519549
def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir:
520550
if not has_command("gpg", args=["--version"], warn=False):

testing/test_version.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from dataclasses import replace
44
from datetime import date
5+
from datetime import datetime
56
from datetime import timedelta
7+
from datetime import timezone
68
from typing import Any
79

810
import pytest
@@ -269,11 +271,14 @@ def test_custom_version_schemes() -> None:
269271
assert custom_computed == no_guess_dev_version(version)
270272

271273

274+
# Fixed time for consistent test behavior across timezone boundaries
275+
# This prevents issue #687 where tests failed around midnight in non-UTC timezones
276+
_TEST_TIME = datetime(2023, 12, 15, 12, 0, 0, tzinfo=timezone.utc)
277+
278+
272279
def date_offset(base_date: date | None = None, days_offset: int = 0) -> date:
273280
if base_date is None:
274-
from setuptools_scm.version import _source_epoch_or_utc_now
275-
276-
base_date = _source_epoch_or_utc_now().date()
281+
base_date = _TEST_TIME.date()
277282
return base_date - timedelta(days=days_offset)
278283

279284

@@ -304,12 +309,23 @@ def date_to_str(
304309
id="leading 0s",
305310
),
306311
pytest.param(
307-
meta(date_to_str(days_offset=3), config=c_non_normalize, dirty=True),
312+
meta(
313+
date_to_str(days_offset=3),
314+
config=c_non_normalize,
315+
dirty=True,
316+
time=_TEST_TIME,
317+
),
308318
date_to_str() + ".0.dev0",
309319
id="dirty other day",
310320
),
311321
pytest.param(
312-
meta(date_to_str(), config=c_non_normalize, distance=2, branch="default"),
322+
meta(
323+
date_to_str(),
324+
config=c_non_normalize,
325+
distance=2,
326+
branch="default",
327+
time=_TEST_TIME,
328+
),
313329
date_to_str() + ".1.dev2",
314330
id="normal branch",
315331
),
@@ -382,8 +398,8 @@ def test_calver_by_date(version: ScmVersion, expected_next: str) -> None:
382398
[
383399
pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact stays"),
384400
pytest.param(
385-
meta("1.0.0", config=c_non_normalize, dirty=True),
386-
"09.02.13.1.dev0",
401+
meta("1.0.0", config=c_non_normalize, dirty=True, time=_TEST_TIME),
402+
"23.12.15.0.dev0",
387403
id="SemVer dirty is replaced by date",
388404
marks=pytest.mark.filterwarnings("ignore:.*legacy version.*:UserWarning"),
389405
),
@@ -397,7 +413,12 @@ def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None:
397413
def test_calver_by_date_future_warning() -> None:
398414
with pytest.warns(UserWarning, match="your previous tag*"):
399415
calver_by_date(
400-
meta(date_to_str(days_offset=-2), config=c_non_normalize, distance=2)
416+
meta(
417+
date_to_str(days_offset=-2),
418+
config=c_non_normalize,
419+
distance=2,
420+
time=_TEST_TIME,
421+
)
401422
)
402423

403424

0 commit comments

Comments
 (0)