Skip to content

Commit 7a0e639

Browse files
committed
Added CalVer function and formatting
- Version parts now have a `calver_format` attribute for CalVer parts.
1 parent 2cd36ee commit 7a0e639

10 files changed

+434
-52
lines changed

bumpversion/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
context_settings={
2727
"help_option_names": ["-h", "--help"],
2828
},
29-
add_help_option=False,
29+
add_help_option=True,
3030
)
3131
@click.version_option(version=__version__)
3232
@click.pass_context

bumpversion/versioning/functions.py

+41
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
"""Generators for version parts."""
22

3+
import datetime
34
import re
45
from typing import List, Optional, Union
56

67

8+
def get_datetime_info(current_dt: datetime.datetime) -> dict:
9+
"""Return the full structure of the given datetime for formatting."""
10+
return {
11+
"YYYY": current_dt.strftime("%Y"),
12+
"YY": current_dt.strftime("%y").lstrip("0") or "0",
13+
"0Y": current_dt.strftime("%y"),
14+
"MMM": current_dt.strftime("%b"),
15+
"MM": str(current_dt.month),
16+
"0M": current_dt.strftime("%m"),
17+
"DD": str(current_dt.day),
18+
"0D": current_dt.strftime("%d"),
19+
"JJJ": current_dt.strftime("%j").lstrip("0"),
20+
"00J": current_dt.strftime("%j"),
21+
"Q": str((current_dt.month - 1) // 3 + 1),
22+
"WW": current_dt.strftime("%W").lstrip("0") or "0",
23+
"0W": current_dt.strftime("%W"),
24+
"UU": current_dt.strftime("%U").lstrip("0") or "0",
25+
"0U": current_dt.strftime("%U"),
26+
"VV": current_dt.strftime("%V").lstrip("0") or "0",
27+
"0V": current_dt.strftime("%V"),
28+
"GGGG": current_dt.strftime("%G"),
29+
"GG": current_dt.strftime("%G")[2:].lstrip("0") or "0",
30+
"0G": current_dt.strftime("%G")[2:],
31+
}
32+
33+
734
class PartFunction:
835
"""Base class for a version part function."""
936

@@ -35,6 +62,20 @@ def bump(self, value: Optional[str] = None) -> str:
3562
return value or self.optional_value
3663

3764

65+
class CalVerFunction(PartFunction):
66+
"""This is a class that provides a CalVer function for version parts."""
67+
68+
def __init__(self, calver_format: str):
69+
self.independent = False
70+
self.calver_format = calver_format
71+
self.first_value = self.bump()
72+
self.optional_value = "There isn't an optional value for CalVer."
73+
74+
def bump(self, value: Optional[str] = None) -> str:
75+
"""Return the optional value."""
76+
return self.calver_format.format(**get_datetime_info(datetime.datetime.now()))
77+
78+
3879
class NumericFunction(PartFunction):
3980
"""
4081
This is a class that provides a numeric function for version parts.

bumpversion/versioning/models.py

+27-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from bumpversion.exceptions import InvalidVersionPartError
1111
from bumpversion.utils import key_val_string
12-
from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction
12+
from bumpversion.versioning.functions import CalVerFunction, NumericFunction, PartFunction, ValuesFunction
1313

1414

1515
class VersionComponent:
@@ -26,18 +26,23 @@ def __init__(
2626
optional_value: Optional[str] = None,
2727
first_value: Union[str, int, None] = None,
2828
independent: bool = False,
29+
calver_format: Optional[str] = None,
2930
source: Optional[str] = None,
3031
value: Union[str, int, None] = None,
3132
):
3233
self._value = str(value) if value is not None else None
3334
self.func: Optional[PartFunction] = None
3435
self.independent = independent
3536
self.source = source
37+
self.calver_format = calver_format
3638
if values:
3739
str_values = [str(v) for v in values]
3840
str_optional_value = str(optional_value) if optional_value is not None else None
3941
str_first_value = str(first_value) if first_value is not None else None
4042
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
43+
elif calver_format:
44+
self.func = CalVerFunction(calver_format)
45+
self._value = self._value or self.func.first_value
4146
else:
4247
self.func = NumericFunction(optional_value, first_value or "0")
4348

@@ -53,6 +58,7 @@ def copy(self) -> "VersionComponent":
5358
optional_value=self.func.optional_value,
5459
first_value=self.func.first_value,
5560
independent=self.independent,
61+
calver_format=self.calver_format,
5662
source=self.source,
5763
value=self._value,
5864
)
@@ -101,13 +107,28 @@ class VersionComponentSpec(BaseModel):
101107
This is used to read in the configuration from the bumpversion config file.
102108
"""
103109

104-
values: Optional[list] = None # Optional. Numeric is used if missing or no items in list
110+
values: Optional[list] = None
111+
"""The possible values for the component. If it and `calver_format` is None, the component is numeric."""
112+
105113
optional_value: Optional[str] = None # Optional.
106-
# Defaults to first value. 0 in the case of numeric. Empty string means nothing is optional.
107-
first_value: Union[str, int, None] = None # Optional. Defaults to first value in values
114+
"""The value that is optional to include in the version.
115+
116+
- Defaults to first value in values or 0 in the case of numeric.
117+
- Empty string means nothing is optional.
118+
- CalVer components ignore this."""
119+
120+
first_value: Union[str, int, None] = None
121+
"""The first value to increment from."""
122+
108123
independent: bool = False
124+
"""Is the component independent of the other components?"""
125+
126+
calver_format: Optional[str] = None
127+
"""The format string for a CalVer component."""
128+
109129
# source: Optional[str] = None # Name of environment variable or context variable to use as the source for value
110-
depends_on: Optional[str] = None # The name of the component this component depends on
130+
depends_on: Optional[str] = None
131+
"""The name of the component this component depends on."""
111132

112133
def create_component(self, value: Union[str, int, None] = None) -> VersionComponent:
113134
"""Generate a version component from the configuration."""
@@ -116,6 +137,7 @@ def create_component(self, value: Union[str, int, None] = None) -> VersionCompon
116137
optional_value=self.optional_value,
117138
first_value=self.first_value,
118139
independent=self.independent,
140+
calver_format=self.calver_format,
119141
# source=self.source,
120142
value=value,
121143
)

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ docs = [
7676
]
7777
test = [
7878
"coverage",
79+
"freezegun",
7980
"pre-commit",
8081
"pytest-cov",
8182
"pytest",
8283
"pytest-mock",
84+
"pytest-sugar",
8385
]
8486

8587
[tool.setuptools.dynamic]

tests/fixtures/basic_cfg_expected.txt

+8-4
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,26 @@
4141
'included_paths': [],
4242
'message': 'Bump version: {current_version} → {new_version}',
4343
'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?',
44-
'parts': {'major': {'depends_on': None,
44+
'parts': {'major': {'calver_format': None,
45+
'depends_on': None,
4546
'first_value': None,
4647
'independent': False,
4748
'optional_value': None,
4849
'values': None},
49-
'minor': {'depends_on': None,
50+
'minor': {'calver_format': None,
51+
'depends_on': None,
5052
'first_value': None,
5153
'independent': False,
5254
'optional_value': None,
5355
'values': None},
54-
'patch': {'depends_on': None,
56+
'patch': {'calver_format': None,
57+
'depends_on': None,
5558
'first_value': None,
5659
'independent': False,
5760
'optional_value': None,
5861
'values': None},
59-
'release': {'depends_on': None,
62+
'release': {'calver_format': None,
63+
'depends_on': None,
6064
'first_value': None,
6165
'independent': False,
6266
'optional_value': 'gamma',

tests/fixtures/basic_cfg_expected.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,28 @@ message: "Bump version: {current_version} → {new_version}"
4949
parse: "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?"
5050
parts:
5151
major:
52+
calver_format: null
5253
depends_on: null
5354
first_value: null
5455
independent: false
5556
optional_value: null
5657
values: null
5758
minor:
59+
calver_format: null
5860
depends_on: null
5961
first_value: null
6062
independent: false
6163
optional_value: null
6264
values: null
6365
patch:
66+
calver_format: null
6467
depends_on: null
6568
first_value: null
6669
independent: false
6770
optional_value: null
6871
values: null
6972
release:
73+
calver_format: null
7074
depends_on: null
7175
first_value: null
7276
independent: false

tests/fixtures/basic_cfg_expected_full.json

+4
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,31 @@
5858
"parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
5959
"parts": {
6060
"major": {
61+
"calver_format": null,
6162
"depends_on": null,
6263
"first_value": null,
6364
"independent": false,
6465
"optional_value": null,
6566
"values": null
6667
},
6768
"minor": {
69+
"calver_format": null,
6870
"depends_on": null,
6971
"first_value": null,
7072
"independent": false,
7173
"optional_value": null,
7274
"values": null
7375
},
7476
"patch": {
77+
"calver_format": null,
7578
"depends_on": null,
7679
"first_value": null,
7780
"independent": false,
7881
"optional_value": null,
7982
"values": null
8083
},
8184
"release": {
85+
"calver_format": null,
8286
"depends_on": null,
8387
"first_value": null,
8488
"independent": false,

tests/test_versioning/test_functions.py

+65-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from pytest import param
3-
4-
from bumpversion.versioning.functions import NumericFunction, ValuesFunction, IndependentFunction
3+
from freezegun import freeze_time
4+
from bumpversion.versioning.functions import NumericFunction, ValuesFunction, IndependentFunction, CalVerFunction
55

66

77
# NumericFunction
@@ -145,3 +145,66 @@ def test_bump_with_value_returns_value(self):
145145
def test_bump_with_no_value_returns_initial_value(self):
146146
func = IndependentFunction("1")
147147
assert func.bump() == "1"
148+
149+
150+
class TestCalVerFunction:
151+
"""The calver function manages incrementing and resetting calver version parts."""
152+
153+
@freeze_time("2020-05-01")
154+
def test_creation_sets_first_value_and_optional_value(self):
155+
func = CalVerFunction("{YYYY}.{MM}")
156+
assert func.optional_value == "There isn't an optional value for CalVer."
157+
assert func.first_value == "2020.5"
158+
assert func.calver_format == "{YYYY}.{MM}"
159+
160+
@freeze_time("2020-05-01")
161+
def test_bump_with_value_ignores_value(self):
162+
func = CalVerFunction("{YYYY}.{MM}.{DD}")
163+
assert func.bump("123456") == "2020.5.1"
164+
165+
@pytest.mark.parametrize(
166+
["calver", "expected"],
167+
[
168+
param("{YYYY}", "2002", id="{YYYY}"),
169+
param("{YY}", "2", id="{YY}"),
170+
param("{0Y}", "02", id="{0Y}"),
171+
param("{MMM}", "May", id="{MMM}"),
172+
param("{MM}", "5", id="{MM}"),
173+
param("{0M}", "05", id="{0M}"),
174+
param("{DD}", "1", id="{DD}"),
175+
param("{0D}", "01", id="{0D}"),
176+
param("{JJJ}", "121", id="{JJJ}"),
177+
param("{00J}", "121", id="{00J}"),
178+
param("{Q}", "2", id="{Q}"),
179+
param("{WW}", "17", id="{WW}"),
180+
param("{0W}", "17", id="{0W}"),
181+
param("{UU}", "17", id="{UU}"),
182+
param("{0U}", "17", id="{0U}"),
183+
param("{VV}", "18", id="{VV}"),
184+
param("{0V}", "18", id="{0V}"),
185+
param("{GGGG}", "2002", id="{GGGG}"),
186+
param("{GG}", "2", id="{GG}"),
187+
param("{0G}", "02", id="{0G}"),
188+
],
189+
)
190+
@freeze_time("2002-05-01")
191+
def test_calver_formatting_renders_correctly(self, calver: str, expected: str):
192+
"""Test that the calver is formatted correctly."""
193+
func = CalVerFunction(calver)
194+
assert func.bump() == expected
195+
196+
@pytest.mark.parametrize(
197+
["calver", "expected"],
198+
[
199+
param("{YYYY}", "2000", id="{YYYY}"),
200+
param("{YY}", "0", id="{YY}"),
201+
param("{0Y}", "00", id="{0Y}"),
202+
param("{GGGG}", "1999", id="{GGGG}"),
203+
param("{GG}", "99", id="{GG}"),
204+
param("{0G}", "99", id="{0G}"),
205+
],
206+
)
207+
@freeze_time("2000-01-01")
208+
def test_century_years_return_zeros(self, calver: str, expected: str):
209+
func = CalVerFunction(calver)
210+
assert func.bump() == expected

0 commit comments

Comments
 (0)