Skip to content

Commit 53ee848

Browse files
committed
Added always_increment attribute for parts
This is a requirement for CalVer to ensure they always increment with each bump, but it will work for any type.
1 parent 5a3e05d commit 53ee848

File tree

7 files changed

+103
-30
lines changed

7 files changed

+103
-30
lines changed

bumpversion/versioning/functions.py

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class PartFunction:
1313
first_value: str
1414
optional_value: str
1515
independent: bool
16+
always_increment: bool
1617

1718
def bump(self, value: str) -> str:
1819
"""Increase the value."""
@@ -32,6 +33,7 @@ def __init__(self, value: Union[str, int, None] = None):
3233
self.first_value = str(value)
3334
self.optional_value = str(value)
3435
self.independent = True
36+
self.always_increment = False
3537

3638
def bump(self, value: Optional[str] = None) -> str:
3739
"""Return the optional value."""
@@ -46,6 +48,8 @@ def __init__(self, calver_format: str):
4648
self.calver_format = calver_format
4749
self.first_value = self.bump()
4850
self.optional_value = "There isn't an optional value for CalVer."
51+
self.independent = False
52+
self.always_increment = True
4953

5054
def bump(self, value: Optional[str] = None) -> str:
5155
"""Return the optional value."""
@@ -75,6 +79,7 @@ def __init__(self, optional_value: Union[str, int, None] = None, first_value: Un
7579
self.first_value = str(first_value or 0)
7680
self.optional_value = str(optional_value or self.first_value)
7781
self.independent = False
82+
self.always_increment = False
7883

7984
def bump(self, value: Union[str, int]) -> str:
8085
"""Increase the first numerical value by one."""

bumpversion/versioning/models.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from __future__ import annotations
44

55
from collections import defaultdict, deque
6-
from typing import Any, Dict, List, Optional, Union
6+
from itertools import chain
7+
from typing import Any, Dict, List, Optional, Tuple, Union
78

8-
from pydantic import BaseModel
9+
from pydantic import BaseModel, model_validator
910

1011
from bumpversion.exceptions import InvalidVersionPartError
1112
from bumpversion.utils import key_val_string
@@ -26,13 +27,15 @@ def __init__(
2627
optional_value: Optional[str] = None,
2728
first_value: Union[str, int, None] = None,
2829
independent: bool = False,
30+
always_increment: bool = False,
2931
calver_format: Optional[str] = None,
3032
source: Optional[str] = None,
3133
value: Union[str, int, None] = None,
3234
):
3335
self._value = str(value) if value is not None else None
3436
self.func: Optional[PartFunction] = None
35-
self.independent = independent
37+
self.always_increment = always_increment
38+
self.independent = True if always_increment else independent
3639
self.source = source
3740
self.calver_format = calver_format
3841
if values:
@@ -58,6 +61,7 @@ def copy(self) -> "VersionComponent":
5861
optional_value=self.func.optional_value,
5962
first_value=self.func.first_value,
6063
independent=self.independent,
64+
always_increment=self.always_increment,
6165
calver_format=self.calver_format,
6266
source=self.source,
6367
value=self._value,
@@ -123,20 +127,32 @@ class VersionComponentSpec(BaseModel):
123127
independent: bool = False
124128
"""Is the component independent of the other components?"""
125129

130+
always_increment: bool = False
131+
"""Should the component always increment, even if it is not necessary?"""
132+
126133
calver_format: Optional[str] = None
127134
"""The format string for a CalVer component."""
128135

129136
# source: Optional[str] = None # Name of environment variable or context variable to use as the source for value
130137
depends_on: Optional[str] = None
131138
"""The name of the component this component depends on."""
132139

140+
@model_validator(mode="before")
141+
@classmethod
142+
def set_always_increment_with_calver(cls, data: Any) -> Any:
143+
"""Set always_increment to True if calver_format is present."""
144+
if isinstance(data, dict) and data.get("calver_format"):
145+
data["always_increment"] = True
146+
return data
147+
133148
def create_component(self, value: Union[str, int, None] = None) -> VersionComponent:
134149
"""Generate a version component from the configuration."""
135150
return VersionComponent(
136151
values=self.values,
137152
optional_value=self.optional_value,
138153
first_value=self.first_value,
139154
independent=self.independent,
155+
always_increment=self.always_increment,
140156
calver_format=self.calver_format,
141157
# source=self.source,
142158
value=value,
@@ -158,6 +174,7 @@ def __init__(self, components: Dict[str, VersionComponentSpec], order: Optional[
158174
self.order = order
159175
self.dependency_map = defaultdict(list)
160176
previous_component = self.order[0]
177+
self.always_increment = [name for name, config in self.component_configs.items() if config.always_increment]
161178
for component in self.order[1:]:
162179
if self.component_configs[component].independent:
163180
continue
@@ -231,12 +248,35 @@ def bump(self, component_name: str) -> "Version":
231248
if component_name not in self.components:
232249
raise InvalidVersionPartError(f"No part named {component_name!r}")
233250

234-
components_to_reset = self.version_spec.get_dependents(component_name)
235-
236251
new_values = dict(self.components.items())
237-
new_values[component_name] = self.components[component_name].bump()
252+
always_incr_values, components_to_reset = self._always_increment()
253+
new_values.update(always_incr_values)
254+
255+
if component_name not in components_to_reset:
256+
new_values[component_name] = self.components[component_name].bump()
257+
components_to_reset |= set(self.version_spec.get_dependents(component_name))
258+
238259
for component in components_to_reset:
239260
if not self.components[component].is_independent:
240261
new_values[component] = self.components[component].null()
241262

242263
return Version(self.version_spec, new_values, self.original)
264+
265+
def _always_incr_dependencies(self) -> dict:
266+
"""Return the components that always increment and depend on the given component."""
267+
return {name: self.version_spec.get_dependents(name) for name in self.version_spec.always_increment}
268+
269+
def _increment_always_incr(self) -> dict:
270+
"""Increase the values of the components that always increment."""
271+
components = self.version_spec.always_increment
272+
return {name: self.components[name].bump() for name in components}
273+
274+
def _always_increment(self) -> Tuple[dict, set]:
275+
"""Return the components that always increment and their dependents."""
276+
values = self._increment_always_incr()
277+
dependents = self._always_incr_dependencies()
278+
for component_name, value in values.items():
279+
if value == self.components[component_name]:
280+
dependents.pop(component_name, None)
281+
unique_dependents = set(chain.from_iterable(dependents.values()))
282+
return values, unique_dependents

tests/fixtures/basic_cfg_expected.txt

+8-4
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,29 @@
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': {'calver_format': None,
44+
'parts': {'major': {'always_increment': False,
45+
'calver_format': None,
4546
'depends_on': None,
4647
'first_value': None,
4748
'independent': False,
4849
'optional_value': None,
4950
'values': None},
50-
'minor': {'calver_format': None,
51+
'minor': {'always_increment': False,
52+
'calver_format': None,
5153
'depends_on': None,
5254
'first_value': None,
5355
'independent': False,
5456
'optional_value': None,
5557
'values': None},
56-
'patch': {'calver_format': None,
58+
'patch': {'always_increment': False,
59+
'calver_format': None,
5760
'depends_on': None,
5861
'first_value': None,
5962
'independent': False,
6063
'optional_value': None,
6164
'values': None},
62-
'release': {'calver_format': None,
65+
'release': {'always_increment': False,
66+
'calver_format': None,
6367
'depends_on': None,
6468
'first_value': None,
6569
'independent': False,

tests/fixtures/basic_cfg_expected.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,31 @@ 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+
always_increment: false
5253
calver_format: null
5354
depends_on: null
5455
first_value: null
5556
independent: false
5657
optional_value: null
5758
values: null
5859
minor:
60+
always_increment: false
5961
calver_format: null
6062
depends_on: null
6163
first_value: null
6264
independent: false
6365
optional_value: null
6466
values: null
6567
patch:
68+
always_increment: false
6669
calver_format: null
6770
depends_on: null
6871
first_value: null
6972
independent: false
7073
optional_value: null
7174
values: null
7275
release:
76+
always_increment: false
7377
calver_format: null
7478
depends_on: null
7579
first_value: null

tests/fixtures/basic_cfg_expected_full.json

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
5959
"parts": {
6060
"major": {
61+
"always_increment": false,
6162
"calver_format": null,
6263
"depends_on": null,
6364
"first_value": null,
@@ -66,6 +67,7 @@
6667
"values": null
6768
},
6869
"minor": {
70+
"always_increment": false,
6971
"calver_format": null,
7072
"depends_on": null,
7173
"first_value": null,
@@ -74,6 +76,7 @@
7476
"values": null
7577
},
7678
"patch": {
79+
"always_increment": false,
7780
"calver_format": null,
7881
"depends_on": null,
7982
"first_value": null,
@@ -82,6 +85,7 @@
8285
"values": null
8386
},
8487
"release": {
88+
"always_increment": false,
8589
"calver_format": null,
8690
"depends_on": null,
8791
"first_value": null,

tests/test_config/test_files.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from pytest import LogCaptureFixture, param, TempPathFactory
1010

11-
from bumpversion.utils import get_context
11+
from bumpversion.context import get_context
1212
from bumpversion import config
1313
from bumpversion.config.files import find_config_file, CONFIG_FILE_SEARCH_ORDER
1414
import bumpversion.config.utils
@@ -198,7 +198,7 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path):
198198
"""
199199
Check the PEP440 config file.
200200
"""
201-
from bumpversion.utils import get_context
201+
from bumpversion.context import get_context
202202
from bumpversion.bump import get_next_version
203203
from bumpversion import cli
204204
import subprocess

0 commit comments

Comments
 (0)