Skip to content

Commit 3112db8

Browse files
Merge pull request #529 from ikerreyes/calver-by-date
CalVer by date
2 parents 3c18be6 + bab7738 commit 3112db8

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ v6.0.0
66
* drop egg building (use wheels)
77
* add git node_date metadata to get the commit time-stamp of HEAD
88
* allow version schemes to be priority ordered lists of version schemes
9+
* support for calendar versioning (calver) by date
910

1011
v5.0.2
1112
======

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ setuptools_scm.version_scheme =
6262
python-simplified-semver = setuptools_scm.version:simplified_semver_version
6363
release-branch-semver = setuptools_scm.version:release_branch_semver_version
6464
no-guess-dev = setuptools_scm.version:no_guess_dev_version
65+
calver-by-date = setuptools_scm.version:calver_by_date
6566

6667
[options.extras_require]
6768
toml =

src/setuptools_scm/version.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,71 @@ def no_guess_dev_version(version):
309309
return version.format_with("{tag}.post1.dev{distance}")
310310

311311

312+
def date_ver_match(ver):
313+
match = re.match(
314+
(
315+
r"^(?P<date>(?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})"
316+
r"(?:\.(?P<patch>\d*)){0,1}?$"
317+
),
318+
str(ver),
319+
)
320+
return match
321+
322+
323+
def guess_next_date_ver(version, node_date=None, date_fmt=None):
324+
"""
325+
same-day -> patch +1
326+
other-day -> today
327+
328+
distance is always added as .devX
329+
"""
330+
match = date_ver_match(version)
331+
if match is None:
332+
raise ValueError(
333+
"{version} does not correspond to a valid versioning date, "
334+
"please correct or use a custom version scheme".format(version=version)
335+
)
336+
# deduct date format if not provided
337+
if date_fmt is None:
338+
date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
339+
head_date = node_date or datetime.date.today()
340+
# compute patch
341+
tag_date = datetime.datetime.strptime(match.group("date"), date_fmt).date()
342+
if tag_date == head_date:
343+
patch = match.group("patch") or "0"
344+
patch = int(patch) + 1
345+
else:
346+
if tag_date > head_date:
347+
# warn on future times
348+
warnings.warn(
349+
"your previous tag ({}) is ahead your node date ({})".format(
350+
tag_date, head_date
351+
)
352+
)
353+
patch = 0
354+
next_version = "{node_date:{date_fmt}}.{patch}".format(
355+
node_date=head_date, date_fmt=date_fmt, patch=patch
356+
)
357+
# rely on the Version object to ensure consistency (e.g. remove leading 0s)
358+
# TODO: support for intentionally non-normalized date versions
359+
next_version = str(Version(next_version))
360+
return next_version
361+
362+
363+
def calver_by_date(version):
364+
if version.exact and not version.dirty:
365+
return version.format_with("{tag}")
366+
# TODO: move the release-X check to a new scheme
367+
if version.branch is not None and version.branch.startswith("release-"):
368+
branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
369+
if branch_ver is not None:
370+
ver = branch_ver["version"]
371+
match = date_ver_match(ver)
372+
if match:
373+
return ver
374+
return version.format_next_version(guess_next_date_ver, node_date=version.node_date)
375+
376+
312377
def _format_local_with_time(version, time_format):
313378

314379
if version.exact or version.node is None:

testing/test_version.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import date, timedelta
2+
13
import pytest
24
from setuptools_scm.config import Configuration
35
from setuptools_scm.version import (
@@ -8,6 +10,7 @@
810
no_guess_dev_version,
911
guess_next_version,
1012
format_version,
13+
calver_by_date,
1114
)
1215

1316

@@ -180,3 +183,98 @@ def test_format_version_schemes():
180183
local_scheme="no-local-version",
181184
version_scheme=[lambda v: None, "guess-next-dev"],
182185
)
186+
187+
188+
def date_to_str(date_=None, days_offset=0, fmt="{dt:%y}.{dt.month}.{dt.day}"):
189+
date_ = date_ or date.today()
190+
date_ = date_ - timedelta(days=days_offset)
191+
return fmt.format(dt=date_)
192+
193+
194+
@pytest.mark.parametrize(
195+
"version, expected_next",
196+
[
197+
pytest.param(
198+
meta(date_to_str(days_offset=3), config=c),
199+
date_to_str(days_offset=3),
200+
id="exact",
201+
),
202+
pytest.param(
203+
meta(date_to_str() + ".1", config=c), date_to_str() + ".1", id="exact patch"
204+
),
205+
pytest.param(
206+
meta(date_to_str(fmt="20.01.02"), config=c),
207+
"20.1.2",
208+
id="leading 0s",
209+
),
210+
pytest.param(
211+
meta(date_to_str(days_offset=3), config=c, dirty=True),
212+
date_to_str() + ".0.dev0",
213+
id="dirty other day",
214+
),
215+
pytest.param(
216+
meta(date_to_str(), config=c, distance=2, branch="default"),
217+
date_to_str() + ".1.dev2",
218+
id="normal branch",
219+
),
220+
pytest.param(
221+
meta(date_to_str(fmt="{dt:%Y}.{dt.month}.{dt.day}"), config=c),
222+
date_to_str(fmt="{dt:%Y}.{dt.month}.{dt.day}"),
223+
id="4 digits year",
224+
),
225+
pytest.param(
226+
meta(date_to_str(), config=c, distance=2, branch="release-2021.05.06"),
227+
"2021.05.06",
228+
id="release branch",
229+
),
230+
pytest.param(
231+
meta(date_to_str() + ".2", config=c, distance=2, branch="release-21.5.1"),
232+
"21.5.1",
233+
id="release branch short",
234+
),
235+
pytest.param(
236+
meta(
237+
date_to_str(days_offset=3) + ".2",
238+
config=c,
239+
node_date=date.today() - timedelta(days=2),
240+
),
241+
date_to_str(days_offset=3) + ".2",
242+
id="node date clean",
243+
),
244+
pytest.param(
245+
meta(
246+
date_to_str(days_offset=2) + ".2",
247+
config=c,
248+
distance=2,
249+
node_date=date.today() - timedelta(days=2),
250+
),
251+
date_to_str(date.today() - timedelta(days=2)) + ".3.dev2",
252+
id="node date distance",
253+
),
254+
],
255+
)
256+
def test_calver_by_date(version, expected_next):
257+
computed = calver_by_date(version)
258+
assert computed == expected_next
259+
260+
261+
@pytest.mark.parametrize(
262+
"version, expected_next",
263+
[
264+
pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact"),
265+
pytest.param(
266+
meta("1.0.0", config=c, dirty=True),
267+
"1.0.0",
268+
id="SemVer dirty",
269+
marks=pytest.mark.xfail,
270+
),
271+
],
272+
)
273+
def test_calver_by_date_semver(version, expected_next):
274+
computed = calver_by_date(version)
275+
assert computed == expected_next
276+
277+
278+
def test_calver_by_date_future_warning():
279+
with pytest.warns(UserWarning, match="your previous tag*"):
280+
calver_by_date(meta(date_to_str(days_offset=-2), config=c, distance=2))

0 commit comments

Comments
 (0)