Skip to content

Commit

Permalink
Refactored versioning functions and version parts
Browse files Browse the repository at this point in the history
  • Loading branch information
coordt committed Dec 19, 2023
1 parent 0e01253 commit be87721
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 260 deletions.
2 changes: 2 additions & 0 deletions bumpversion/autocast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Automatically detect the true Python type of a string and cast it to the correct type.
Based on https://github.com/cgreer/cgAutoCast/blob/master/cgAutoCast.py
Only used by Legacy configuration file parser.
"""

import contextlib
Expand Down
2 changes: 1 addition & 1 deletion bumpversion/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

if TYPE_CHECKING: # pragma: no-coverage
from bumpversion.files import ConfiguredFile
from bumpversion.version_part import Version
from bumpversion.versioning.models import Version

from bumpversion.config import Config
from bumpversion.config.files import update_config_file
Expand Down
2 changes: 1 addition & 1 deletion bumpversion/config/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

if TYPE_CHECKING: # pragma: no-coverage
from bumpversion.config.models import Config
from bumpversion.version_part import Version
from bumpversion.versioning.models import Version

logger = get_indented_logger(__name__)

Expand Down
3 changes: 2 additions & 1 deletion bumpversion/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from bumpversion.config.models import FileChange, VersionPartConfig
from bumpversion.exceptions import VersionNotFoundError
from bumpversion.ui import get_indented_logger
from bumpversion.version_part import Version, VersionConfig
from bumpversion.version_part import VersionConfig
from bumpversion.versioning.models import Version

logger = get_indented_logger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion bumpversion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

if TYPE_CHECKING: # pragma: no-coverage
from bumpversion.config import Config
from bumpversion.version_part import Version
from bumpversion.versioning.models import Version


def extract_regex_flags(regex_pattern: str) -> Tuple[str, str]:
Expand Down
117 changes: 3 additions & 114 deletions bumpversion/version_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,19 @@
import re
import string
from copy import copy
from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union
from typing import Any, Dict, List, MutableMapping, Optional, Tuple

from click import UsageError

from bumpversion.config.models import VersionPartConfig
from bumpversion.exceptions import FormattingError, InvalidVersionPartError, MissingValueError
from bumpversion.functions import NumericFunction, PartFunction, ValuesFunction
from bumpversion.exceptions import FormattingError, MissingValueError
from bumpversion.ui import get_indented_logger
from bumpversion.utils import key_val_string, labels_for_format
from bumpversion.versioning.models import Version, VersionPart

logger = get_indented_logger(__name__)


class VersionPart:
"""
Represent part of a version number.
Determines the PartFunction that rules how the part behaves when increased or reset
based on the configuration given.
"""

def __init__(self, config: VersionPartConfig, value: Union[str, int, None] = None):
self._value = str(value) if value is not None else None
self.config = config
self.func: Optional[PartFunction] = None
if config.values:
str_values = [str(v) for v in config.values]
str_optional_value = str(config.optional_value) if config.optional_value is not None else None
str_first_value = str(config.first_value) if config.first_value is not None else None
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
else:
self.func = NumericFunction(config.optional_value, config.first_value or "0")

@property
def value(self) -> str:
"""Return the value of the part."""
return self._value or self.func.optional_value

def copy(self) -> "VersionPart":
"""Return a copy of the part."""
return VersionPart(self.config, self._value)

def bump(self) -> "VersionPart":
"""Return a part with bumped value."""
return VersionPart(self.config, self.func.bump(self.value))

def null(self) -> "VersionPart":
"""Return a part with first value."""
return VersionPart(self.config, self.func.first_value)

@property
def is_optional(self) -> bool:
"""Is the part optional?"""
return self.value == self.func.optional_value

@property
def is_independent(self) -> bool:
"""Is the part independent of the other parts?"""
return self.config.independent

def __format__(self, format_spec: str) -> str:
try:
val = int(self.value)
except ValueError:
return self.value
else:
return int.__format__(val, format_spec)

def __repr__(self) -> str:
return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"

def __eq__(self, other: Any) -> bool:
return self.value == other.value if isinstance(other, VersionPart) else False


class Version:
"""The specification of a version and its parts."""

def __init__(self, values: Dict[str, VersionPart], original: Optional[str] = None):
self.values = values
self.original = original

def __getitem__(self, key: str) -> VersionPart:
return self.values[key]

def __len__(self) -> int:
return len(self.values)

def __iter__(self):
return iter(self.values)

def __repr__(self):
return f"<bumpversion.Version:{key_val_string(self.values)}>"

def __eq__(self, other: Any) -> bool:
return (
all(value == other.values[key] for key, value in self.values.items())
if isinstance(other, Version)
else False
)

def bump(self, part_name: str, order: List[str]) -> "Version":
"""Increase the value of the given part."""
bumped = False

new_values = {}

for label in order:
if label not in self.values:
continue
if label == part_name:
new_values[label] = self.values[label].bump()
bumped = True
elif bumped and not self.values[label].is_independent:
new_values[label] = self.values[label].null()
else:
new_values[label] = self.values[label].copy()

if not bumped:
raise InvalidVersionPartError(f"No part named {part_name!r}")

return Version(new_values)


class VersionConfig:
"""
Hold a complete representation of a version string.
Expand Down
1 change: 1 addition & 0 deletions bumpversion/versioning/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Module for managing Versions and their internal parts."""
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class NumericFunction(PartFunction):
considered (e.g. 'r3-001' --> 'r4-001').
"""

FIRST_NUMERIC = re.compile(r"(\D*)(\d+)(.*)")
FIRST_NUMERIC = re.compile(r"(?P<prefix>[^-0-9]*)(?P<number>-?\d+)(?P<suffix>.*)")

def __init__(self, optional_value: Union[str, int, None] = None, first_value: Union[str, int, None] = None):
if first_value is not None and not self.FIRST_NUMERIC.search(str(first_value)):
Expand All @@ -43,7 +43,14 @@ def bump(self, value: Union[str, int]) -> str:
match = self.FIRST_NUMERIC.search(str(value))
if not match:
raise ValueError(f"The given value {value} does not contain any digit")

part_prefix, part_numeric, part_suffix = match.groups()

if int(part_numeric) < int(self.first_value):
raise ValueError(
f"The given value {value} is lower than the first value {self.first_value} and cannot be bumped."
)

bumped_numeric = int(part_numeric) + 1

return "".join([part_prefix, str(bumped_numeric), part_suffix])
Expand Down
118 changes: 118 additions & 0 deletions bumpversion/versioning/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Models for managing versioning of software projects."""
from typing import Any, Dict, List, Optional, Union

from bumpversion.config.models import VersionPartConfig
from bumpversion.exceptions import InvalidVersionPartError
from bumpversion.utils import key_val_string
from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction


class VersionPart:
"""
Represent part of a version number.
Determines the PartFunction that rules how the part behaves when increased or reset
based on the configuration given.
"""

def __init__(self, config: VersionPartConfig, value: Union[str, int, None] = None):
self._value = str(value) if value is not None else None
self.config = config
self.func: Optional[PartFunction] = None
if config.values:
str_values = [str(v) for v in config.values]
str_optional_value = str(config.optional_value) if config.optional_value is not None else None
str_first_value = str(config.first_value) if config.first_value is not None else None
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
else:
self.func = NumericFunction(config.optional_value, config.first_value or "0")

@property
def value(self) -> str:
"""Return the value of the part."""
return self._value or self.func.optional_value

def copy(self) -> "VersionPart":
"""Return a copy of the part."""
return VersionPart(self.config, self._value)

def bump(self) -> "VersionPart":
"""Return a part with bumped value."""
return VersionPart(self.config, self.func.bump(self.value))

def null(self) -> "VersionPart":
"""Return a part with first value."""
return VersionPart(self.config, self.func.first_value)

@property
def is_optional(self) -> bool:
"""Is the part optional?"""
return self.value == self.func.optional_value

@property
def is_independent(self) -> bool:
"""Is the part independent of the other parts?"""
return self.config.independent

def __format__(self, format_spec: str) -> str:
try:
val = int(self.value)
except ValueError:
return self.value
else:
return int.__format__(val, format_spec)

def __repr__(self) -> str:
return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"

def __eq__(self, other: Any) -> bool:
return self.value == other.value if isinstance(other, VersionPart) else False


class Version:
"""The specification of a version and its parts."""

def __init__(self, values: Dict[str, VersionPart], original: Optional[str] = None):
self.values = values
self.original = original

def __getitem__(self, key: str) -> VersionPart:
return self.values[key]

def __len__(self) -> int:
return len(self.values)

def __iter__(self):
return iter(self.values)

def __repr__(self):
return f"<bumpversion.Version:{key_val_string(self.values)}>"

def __eq__(self, other: Any) -> bool:
return (
all(value == other.values[key] for key, value in self.values.items())
if isinstance(other, Version)
else False
)

def bump(self, part_name: str, order: List[str]) -> "Version":
"""Increase the value of the given part."""
bumped = False

new_values = {}

for label in order:
if label not in self.values:
continue
if label == part_name:
new_values[label] = self.values[label].bump()
bumped = True
elif bumped and not self.values[label].is_independent:
new_values[label] = self.values[label].null()
else:
new_values[label] = self.values[label].copy()

if not bumped:
raise InvalidVersionPartError(f"No part named {part_name!r}")

return Version(new_values)

0 comments on commit be87721

Please sign in to comment.