Skip to content

Commit

Permalink
Added CalVer function and formatting
Browse files Browse the repository at this point in the history
- Version parts now have a `calver_format` attribute for CalVer parts.
  • Loading branch information
coordt committed Mar 26, 2024
1 parent 2cd36ee commit 7a0e639
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 52 deletions.
2 changes: 1 addition & 1 deletion bumpversion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
context_settings={
"help_option_names": ["-h", "--help"],
},
add_help_option=False,
add_help_option=True,
)
@click.version_option(version=__version__)
@click.pass_context
Expand Down
41 changes: 41 additions & 0 deletions bumpversion/versioning/functions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
"""Generators for version parts."""

import datetime
import re
from typing import List, Optional, Union


def get_datetime_info(current_dt: datetime.datetime) -> dict:
"""Return the full structure of the given datetime for formatting."""
return {
"YYYY": current_dt.strftime("%Y"),
"YY": current_dt.strftime("%y").lstrip("0") or "0",
"0Y": current_dt.strftime("%y"),
"MMM": current_dt.strftime("%b"),
"MM": str(current_dt.month),
"0M": current_dt.strftime("%m"),
"DD": str(current_dt.day),
"0D": current_dt.strftime("%d"),
"JJJ": current_dt.strftime("%j").lstrip("0"),
"00J": current_dt.strftime("%j"),
"Q": str((current_dt.month - 1) // 3 + 1),
"WW": current_dt.strftime("%W").lstrip("0") or "0",
"0W": current_dt.strftime("%W"),
"UU": current_dt.strftime("%U").lstrip("0") or "0",
"0U": current_dt.strftime("%U"),
"VV": current_dt.strftime("%V").lstrip("0") or "0",
"0V": current_dt.strftime("%V"),
"GGGG": current_dt.strftime("%G"),
"GG": current_dt.strftime("%G")[2:].lstrip("0") or "0",
"0G": current_dt.strftime("%G")[2:],
}


class PartFunction:
"""Base class for a version part function."""

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


class CalVerFunction(PartFunction):
"""This is a class that provides a CalVer function for version parts."""

def __init__(self, calver_format: str):
self.independent = False
self.calver_format = calver_format
self.first_value = self.bump()
self.optional_value = "There isn't an optional value for CalVer."

def bump(self, value: Optional[str] = None) -> str:
"""Return the optional value."""
return self.calver_format.format(**get_datetime_info(datetime.datetime.now()))


class NumericFunction(PartFunction):
"""
This is a class that provides a numeric function for version parts.
Expand Down
32 changes: 27 additions & 5 deletions bumpversion/versioning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from bumpversion.exceptions import InvalidVersionPartError
from bumpversion.utils import key_val_string
from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction
from bumpversion.versioning.functions import CalVerFunction, NumericFunction, PartFunction, ValuesFunction


class VersionComponent:
Expand All @@ -26,18 +26,23 @@ def __init__(
optional_value: Optional[str] = None,
first_value: Union[str, int, None] = None,
independent: 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.source = source
self.calver_format = calver_format
if values:
str_values = [str(v) for v in values]
str_optional_value = str(optional_value) if optional_value is not None else None
str_first_value = str(first_value) if first_value is not None else None
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
elif calver_format:
self.func = CalVerFunction(calver_format)
self._value = self._value or self.func.first_value
else:
self.func = NumericFunction(optional_value, first_value or "0")

Expand All @@ -53,6 +58,7 @@ def copy(self) -> "VersionComponent":
optional_value=self.func.optional_value,
first_value=self.func.first_value,
independent=self.independent,
calver_format=self.calver_format,
source=self.source,
value=self._value,
)
Expand Down Expand Up @@ -101,13 +107,28 @@ class VersionComponentSpec(BaseModel):
This is used to read in the configuration from the bumpversion config file.
"""

values: Optional[list] = None # Optional. Numeric is used if missing or no items in list
values: Optional[list] = None
"""The possible values for the component. If it and `calver_format` is None, the component is numeric."""

optional_value: Optional[str] = None # Optional.
# Defaults to first value. 0 in the case of numeric. Empty string means nothing is optional.
first_value: Union[str, int, None] = None # Optional. Defaults to first value in values
"""The value that is optional to include in the version.
- Defaults to first value in values or 0 in the case of numeric.
- Empty string means nothing is optional.
- CalVer components ignore this."""

first_value: Union[str, int, None] = None
"""The first value to increment from."""

independent: bool = False
"""Is the component independent of the other components?"""

calver_format: Optional[str] = None
"""The format string for a CalVer component."""

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

def create_component(self, value: Union[str, int, None] = None) -> VersionComponent:
"""Generate a version component from the configuration."""
Expand All @@ -116,6 +137,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,
calver_format=self.calver_format,
# source=self.source,
value=value,
)
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ docs = [
]
test = [
"coverage",
"freezegun",
"pre-commit",
"pytest-cov",
"pytest",
"pytest-mock",
"pytest-sugar",
]

[tool.setuptools.dynamic]
Expand Down
12 changes: 8 additions & 4 deletions tests/fixtures/basic_cfg_expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,26 @@
'included_paths': [],
'message': 'Bump version: {current_version} → {new_version}',
'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?',
'parts': {'major': {'depends_on': None,
'parts': {'major': {'calver_format': None,
'depends_on': None,
'first_value': None,
'independent': False,
'optional_value': None,
'values': None},
'minor': {'depends_on': None,
'minor': {'calver_format': None,
'depends_on': None,
'first_value': None,
'independent': False,
'optional_value': None,
'values': None},
'patch': {'depends_on': None,
'patch': {'calver_format': None,
'depends_on': None,
'first_value': None,
'independent': False,
'optional_value': None,
'values': None},
'release': {'depends_on': None,
'release': {'calver_format': None,
'depends_on': None,
'first_value': None,
'independent': False,
'optional_value': 'gamma',
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/basic_cfg_expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,28 @@ message: "Bump version: {current_version} → {new_version}"
parse: "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?"
parts:
major:
calver_format: null
depends_on: null
first_value: null
independent: false
optional_value: null
values: null
minor:
calver_format: null
depends_on: null
first_value: null
independent: false
optional_value: null
values: null
patch:
calver_format: null
depends_on: null
first_value: null
independent: false
optional_value: null
values: null
release:
calver_format: null
depends_on: null
first_value: null
independent: false
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/basic_cfg_expected_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,31 @@
"parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
"parts": {
"major": {
"calver_format": null,
"depends_on": null,
"first_value": null,
"independent": false,
"optional_value": null,
"values": null
},
"minor": {
"calver_format": null,
"depends_on": null,
"first_value": null,
"independent": false,
"optional_value": null,
"values": null
},
"patch": {
"calver_format": null,
"depends_on": null,
"first_value": null,
"independent": false,
"optional_value": null,
"values": null
},
"release": {
"calver_format": null,
"depends_on": null,
"first_value": null,
"independent": false,
Expand Down
67 changes: 65 additions & 2 deletions tests/test_versioning/test_functions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from pytest import param

from bumpversion.versioning.functions import NumericFunction, ValuesFunction, IndependentFunction
from freezegun import freeze_time
from bumpversion.versioning.functions import NumericFunction, ValuesFunction, IndependentFunction, CalVerFunction


# NumericFunction
Expand Down Expand Up @@ -145,3 +145,66 @@ def test_bump_with_value_returns_value(self):
def test_bump_with_no_value_returns_initial_value(self):
func = IndependentFunction("1")
assert func.bump() == "1"


class TestCalVerFunction:
"""The calver function manages incrementing and resetting calver version parts."""

@freeze_time("2020-05-01")
def test_creation_sets_first_value_and_optional_value(self):
func = CalVerFunction("{YYYY}.{MM}")
assert func.optional_value == "There isn't an optional value for CalVer."
assert func.first_value == "2020.5"
assert func.calver_format == "{YYYY}.{MM}"

@freeze_time("2020-05-01")
def test_bump_with_value_ignores_value(self):
func = CalVerFunction("{YYYY}.{MM}.{DD}")
assert func.bump("123456") == "2020.5.1"

@pytest.mark.parametrize(
["calver", "expected"],
[
param("{YYYY}", "2002", id="{YYYY}"),
param("{YY}", "2", id="{YY}"),
param("{0Y}", "02", id="{0Y}"),
param("{MMM}", "May", id="{MMM}"),
param("{MM}", "5", id="{MM}"),
param("{0M}", "05", id="{0M}"),
param("{DD}", "1", id="{DD}"),
param("{0D}", "01", id="{0D}"),
param("{JJJ}", "121", id="{JJJ}"),
param("{00J}", "121", id="{00J}"),
param("{Q}", "2", id="{Q}"),
param("{WW}", "17", id="{WW}"),
param("{0W}", "17", id="{0W}"),
param("{UU}", "17", id="{UU}"),
param("{0U}", "17", id="{0U}"),
param("{VV}", "18", id="{VV}"),
param("{0V}", "18", id="{0V}"),
param("{GGGG}", "2002", id="{GGGG}"),
param("{GG}", "2", id="{GG}"),
param("{0G}", "02", id="{0G}"),
],
)
@freeze_time("2002-05-01")
def test_calver_formatting_renders_correctly(self, calver: str, expected: str):
"""Test that the calver is formatted correctly."""
func = CalVerFunction(calver)
assert func.bump() == expected

@pytest.mark.parametrize(
["calver", "expected"],
[
param("{YYYY}", "2000", id="{YYYY}"),
param("{YY}", "0", id="{YY}"),
param("{0Y}", "00", id="{0Y}"),
param("{GGGG}", "1999", id="{GGGG}"),
param("{GG}", "99", id="{GG}"),
param("{0G}", "99", id="{0G}"),
],
)
@freeze_time("2000-01-01")
def test_century_years_return_zeros(self, calver: str, expected: str):
func = CalVerFunction(calver)
assert func.bump() == expected
Loading

0 comments on commit 7a0e639

Please sign in to comment.