From 53ee848988d3dae0665e47aa29d8fa02e604b87f Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Wed, 27 Mar 2024 08:22:51 -0500 Subject: [PATCH] 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. --- bumpversion/versioning/functions.py | 5 ++ bumpversion/versioning/models.py | 52 +++++++++++++++++--- tests/fixtures/basic_cfg_expected.txt | 12 +++-- tests/fixtures/basic_cfg_expected.yaml | 4 ++ tests/fixtures/basic_cfg_expected_full.json | 4 ++ tests/test_config/test_files.py | 4 +- tests/test_versioning/test_models_version.py | 52 +++++++++++++------- 7 files changed, 103 insertions(+), 30 deletions(-) diff --git a/bumpversion/versioning/functions.py b/bumpversion/versioning/functions.py index bb561dc2..699f4acf 100644 --- a/bumpversion/versioning/functions.py +++ b/bumpversion/versioning/functions.py @@ -13,6 +13,7 @@ class PartFunction: first_value: str optional_value: str independent: bool + always_increment: bool def bump(self, value: str) -> str: """Increase the value.""" @@ -32,6 +33,7 @@ def __init__(self, value: Union[str, int, None] = None): self.first_value = str(value) self.optional_value = str(value) self.independent = True + self.always_increment = False def bump(self, value: Optional[str] = None) -> str: """Return the optional value.""" @@ -46,6 +48,8 @@ def __init__(self, calver_format: str): self.calver_format = calver_format self.first_value = self.bump() self.optional_value = "There isn't an optional value for CalVer." + self.independent = False + self.always_increment = True def bump(self, value: Optional[str] = None) -> str: """Return the optional value.""" @@ -75,6 +79,7 @@ def __init__(self, optional_value: Union[str, int, None] = None, first_value: Un self.first_value = str(first_value or 0) self.optional_value = str(optional_value or self.first_value) self.independent = False + self.always_increment = False def bump(self, value: Union[str, int]) -> str: """Increase the first numerical value by one.""" diff --git a/bumpversion/versioning/models.py b/bumpversion/versioning/models.py index 6171a06b..ac0069ee 100644 --- a/bumpversion/versioning/models.py +++ b/bumpversion/versioning/models.py @@ -3,9 +3,10 @@ from __future__ import annotations from collections import defaultdict, deque -from typing import Any, Dict, List, Optional, Union +from itertools import chain +from typing import Any, Dict, List, Optional, Tuple, Union -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from bumpversion.exceptions import InvalidVersionPartError from bumpversion.utils import key_val_string @@ -26,13 +27,15 @@ def __init__( optional_value: Optional[str] = None, first_value: Union[str, int, None] = None, independent: bool = False, + always_increment: bool = False, calver_format: Optional[str] = None, source: Optional[str] = None, value: Union[str, int, None] = None, ): self._value = str(value) if value is not None else None self.func: Optional[PartFunction] = None - self.independent = independent + self.always_increment = always_increment + self.independent = True if always_increment else independent self.source = source self.calver_format = calver_format if values: @@ -58,6 +61,7 @@ def copy(self) -> "VersionComponent": optional_value=self.func.optional_value, first_value=self.func.first_value, independent=self.independent, + always_increment=self.always_increment, calver_format=self.calver_format, source=self.source, value=self._value, @@ -123,6 +127,9 @@ class VersionComponentSpec(BaseModel): independent: bool = False """Is the component independent of the other components?""" + always_increment: bool = False + """Should the component always increment, even if it is not necessary?""" + calver_format: Optional[str] = None """The format string for a CalVer component.""" @@ -130,6 +137,14 @@ class VersionComponentSpec(BaseModel): depends_on: Optional[str] = None """The name of the component this component depends on.""" + @model_validator(mode="before") + @classmethod + def set_always_increment_with_calver(cls, data: Any) -> Any: + """Set always_increment to True if calver_format is present.""" + if isinstance(data, dict) and data.get("calver_format"): + data["always_increment"] = True + return data + def create_component(self, value: Union[str, int, None] = None) -> VersionComponent: """Generate a version component from the configuration.""" return VersionComponent( @@ -137,6 +152,7 @@ def create_component(self, value: Union[str, int, None] = None) -> VersionCompon optional_value=self.optional_value, first_value=self.first_value, independent=self.independent, + always_increment=self.always_increment, calver_format=self.calver_format, # source=self.source, value=value, @@ -158,6 +174,7 @@ def __init__(self, components: Dict[str, VersionComponentSpec], order: Optional[ self.order = order self.dependency_map = defaultdict(list) previous_component = self.order[0] + self.always_increment = [name for name, config in self.component_configs.items() if config.always_increment] for component in self.order[1:]: if self.component_configs[component].independent: continue @@ -231,12 +248,35 @@ def bump(self, component_name: str) -> "Version": if component_name not in self.components: raise InvalidVersionPartError(f"No part named {component_name!r}") - components_to_reset = self.version_spec.get_dependents(component_name) - new_values = dict(self.components.items()) - new_values[component_name] = self.components[component_name].bump() + always_incr_values, components_to_reset = self._always_increment() + new_values.update(always_incr_values) + + if component_name not in components_to_reset: + new_values[component_name] = self.components[component_name].bump() + components_to_reset |= set(self.version_spec.get_dependents(component_name)) + for component in components_to_reset: if not self.components[component].is_independent: new_values[component] = self.components[component].null() return Version(self.version_spec, new_values, self.original) + + def _always_incr_dependencies(self) -> dict: + """Return the components that always increment and depend on the given component.""" + return {name: self.version_spec.get_dependents(name) for name in self.version_spec.always_increment} + + def _increment_always_incr(self) -> dict: + """Increase the values of the components that always increment.""" + components = self.version_spec.always_increment + return {name: self.components[name].bump() for name in components} + + def _always_increment(self) -> Tuple[dict, set]: + """Return the components that always increment and their dependents.""" + values = self._increment_always_incr() + dependents = self._always_incr_dependencies() + for component_name, value in values.items(): + if value == self.components[component_name]: + dependents.pop(component_name, None) + unique_dependents = set(chain.from_iterable(dependents.values())) + return values, unique_dependents diff --git a/tests/fixtures/basic_cfg_expected.txt b/tests/fixtures/basic_cfg_expected.txt index 9867f9f4..c33ea7db 100644 --- a/tests/fixtures/basic_cfg_expected.txt +++ b/tests/fixtures/basic_cfg_expected.txt @@ -41,25 +41,29 @@ 'included_paths': [], 'message': 'Bump version: {current_version} → {new_version}', 'parse': '(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?', - 'parts': {'major': {'calver_format': None, + 'parts': {'major': {'always_increment': False, + 'calver_format': None, 'depends_on': None, 'first_value': None, 'independent': False, 'optional_value': None, 'values': None}, - 'minor': {'calver_format': None, + 'minor': {'always_increment': False, + 'calver_format': None, 'depends_on': None, 'first_value': None, 'independent': False, 'optional_value': None, 'values': None}, - 'patch': {'calver_format': None, + 'patch': {'always_increment': False, + 'calver_format': None, 'depends_on': None, 'first_value': None, 'independent': False, 'optional_value': None, 'values': None}, - 'release': {'calver_format': None, + 'release': {'always_increment': False, + 'calver_format': None, 'depends_on': None, 'first_value': None, 'independent': False, diff --git a/tests/fixtures/basic_cfg_expected.yaml b/tests/fixtures/basic_cfg_expected.yaml index 5d7fb01e..cad4c496 100644 --- a/tests/fixtures/basic_cfg_expected.yaml +++ b/tests/fixtures/basic_cfg_expected.yaml @@ -49,6 +49,7 @@ message: "Bump version: {current_version} → {new_version}" parse: "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?" parts: major: + always_increment: false calver_format: null depends_on: null first_value: null @@ -56,6 +57,7 @@ parts: optional_value: null values: null minor: + always_increment: false calver_format: null depends_on: null first_value: null @@ -63,6 +65,7 @@ parts: optional_value: null values: null patch: + always_increment: false calver_format: null depends_on: null first_value: null @@ -70,6 +73,7 @@ parts: optional_value: null values: null release: + always_increment: false calver_format: null depends_on: null first_value: null diff --git a/tests/fixtures/basic_cfg_expected_full.json b/tests/fixtures/basic_cfg_expected_full.json index 24cb4893..eb2bc2aa 100644 --- a/tests/fixtures/basic_cfg_expected_full.json +++ b/tests/fixtures/basic_cfg_expected_full.json @@ -58,6 +58,7 @@ "parse": "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+))?", "parts": { "major": { + "always_increment": false, "calver_format": null, "depends_on": null, "first_value": null, @@ -66,6 +67,7 @@ "values": null }, "minor": { + "always_increment": false, "calver_format": null, "depends_on": null, "first_value": null, @@ -74,6 +76,7 @@ "values": null }, "patch": { + "always_increment": false, "calver_format": null, "depends_on": null, "first_value": null, @@ -82,6 +85,7 @@ "values": null }, "release": { + "always_increment": false, "calver_format": null, "depends_on": null, "first_value": null, diff --git a/tests/test_config/test_files.py b/tests/test_config/test_files.py index 10cd44cc..5fbd7d2d 100644 --- a/tests/test_config/test_files.py +++ b/tests/test_config/test_files.py @@ -8,7 +8,7 @@ import pytest from pytest import LogCaptureFixture, param, TempPathFactory -from bumpversion.utils import get_context +from bumpversion.context import get_context from bumpversion import config from bumpversion.config.files import find_config_file, CONFIG_FILE_SEARCH_ORDER import bumpversion.config.utils @@ -198,7 +198,7 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path): """ Check the PEP440 config file. """ - from bumpversion.utils import get_context + from bumpversion.context import get_context from bumpversion.bump import get_next_version from bumpversion import cli import subprocess diff --git a/tests/test_versioning/test_models_version.py b/tests/test_versioning/test_models_version.py index e30ea56a..a1046bef 100644 --- a/tests/test_versioning/test_models_version.py +++ b/tests/test_versioning/test_models_version.py @@ -16,6 +16,7 @@ def semver_version_spec(): "minor": VersionComponentSpec(), "patch": VersionComponentSpec(), "build": VersionComponentSpec(optional_value="0", independent=True), + "auto": VersionComponentSpec(optional_value="0", independent=True, always_increment=True), } return VersionSpec(config) @@ -46,6 +47,7 @@ def test_acts_like_a_dict(self, semver_version_spec: VersionSpec): assert version["minor"].value == "2" assert version["patch"].value == "3" assert version["build"].value == "0" + assert version["auto"].value == "0" with pytest.raises(KeyError): version["invalid"] @@ -55,21 +57,21 @@ def test_length_is_number_of_parts(self, semver_version_spec: VersionSpec): version = semver_version_spec.create_version({"major": "1"}) # Assert - assert len(version) == 4 + assert len(version) == 5 def test_is_iterable(self, semver_version_spec: VersionSpec): # Arrange version = semver_version_spec.create_version({"major": "1"}) # Assert - assert list(version) == ["major", "minor", "patch", "build"] + assert list(version) == ["major", "minor", "patch", "build", "auto"] def test_has_a_string_representation(self, semver_version_spec: VersionSpec): # Arrange version = semver_version_spec.create_version({"major": "1", "minor": "2", "patch": "3"}) # Assert - assert repr(version) == "" + assert repr(version) == "" def test_has_an_equality_comparison(self, semver_version_spec: VersionSpec): # Arrange @@ -110,7 +112,9 @@ def test_returns_a_new_version(self, semver_version_spec: VersionSpec): def test_changes_component_and_dependents(self, semver_version_spec: VersionSpec): """Bumping a version bumps the specified component and changes its dependents.""" # Arrange - version1 = semver_version_spec.create_version({"major": "1", "minor": "2", "patch": "3", "build": "4"}) + version1 = semver_version_spec.create_version( + {"major": "1", "minor": "2", "patch": "3", "build": "4", "auto": "5"} + ) # Act patch_version = version1.bump("patch") @@ -123,10 +127,10 @@ def test_changes_component_and_dependents(self, semver_version_spec: VersionSpec minor_version_str = ".".join([item.value for item in minor_version.components.values()]) major_version_str = ".".join([item.value for item in major_version.components.values()]) build_version_str = ".".join([item.value for item in build_version.components.values()]) - assert patch_version_str == "1.2.4.4" - assert minor_version_str == "1.3.0.4" - assert major_version_str == "2.0.0.4" - assert build_version_str == "1.2.3.5" + assert patch_version_str == "1.2.4.4.6" + assert minor_version_str == "1.3.0.4.6" + assert major_version_str == "2.0.0.4.6" + assert build_version_str == "1.2.3.5.6" class TestRequiredComponents: """Tests of the required_keys function.""" @@ -221,8 +225,8 @@ def test_bump_does_not_change_original_version(self, calver_version_spec: Versio # Assert assert version1["patch"].value == "3" assert version1["release"].value == "2020.4.1" - assert version2["patch"].value == "4" - assert version2["release"].value == "2020.4.1" + assert version2["patch"].value == "0" + assert version2["release"].value == "2020.5.1" @freeze_time("2020-05-01") def test_bump_returns_a_new_version(self, calver_version_spec: VersionSpec): @@ -233,14 +237,30 @@ def test_bump_returns_a_new_version(self, calver_version_spec: VersionSpec): # Act version2 = version1.bump("patch") + assert version2["release"].value == "2020.5.1" + assert version2["patch"].value == "0" + # Assert assert version1 is not version2 + @freeze_time("2020-05-01") + def test_bump_always_increments_calver(self, calver_version_spec: VersionSpec): + """Bumping a version always increments the calver.""" + # Arrange + version1 = calver_version_spec.create_version({"release": "2020.4.1", "patch": "3", "build": "10"}) + + # Act + version2 = version1.bump("patch") + + # Assert + assert version2["release"].value == "2020.5.1" + assert version2["patch"].value == "0" + @freeze_time("2020-05-01") def test_bump_changes_component_and_dependents(self, calver_version_spec: VersionSpec): """Bumping a version bumps the specified component and changes its dependents.""" # Arrange - version1 = calver_version_spec.create_version({"release": "2020.4.1", "patch": "3", "build": "4"}) + version1 = calver_version_spec.create_version({"release": "2020.5.1", "patch": "3", "build": "4"}) # Act patch_version = version1.bump("patch") @@ -251,9 +271,9 @@ def test_bump_changes_component_and_dependents(self, calver_version_spec: Versio patch_version_str = ".".join([item.value for item in patch_version.components.values()]) release_version_str = ".".join([item.value for item in release_version.components.values()]) build_version_str = ".".join([item.value for item in build_version.components.values()]) - assert patch_version_str == "2020.4.1.4.4" + assert patch_version_str == "2020.5.1.4.4" assert release_version_str == "2020.5.1.0.4" - assert build_version_str == "2020.4.1.3.5" + assert build_version_str == "2020.5.1.3.5" class TestRequiredComponents: """Tests of the required_keys function.""" @@ -262,11 +282,7 @@ class TestRequiredComponents: ["values", "expected"], [ param({"release": "2020.4.1", "patch": "3"}, ["release", "patch"], id="release-patch"), - param( - {"release": "2020.4.1", "build": "4"}, - ["release", "build"], - id="release-build", - ), + param({"release": "2020.4.1", "build": "4"}, ["release", "build"], id="release-build"), param({"release": "2020.4.1"}, ["release"], id="release"), ], )