Skip to content

Commit be87721

Browse files
committed
Refactored versioning functions and version parts
1 parent 0e01253 commit be87721

14 files changed

+327
-260
lines changed

bumpversion/autocast.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Automatically detect the true Python type of a string and cast it to the correct type.
33
44
Based on https://github.com/cgreer/cgAutoCast/blob/master/cgAutoCast.py
5+
6+
Only used by Legacy configuration file parser.
57
"""
68

79
import contextlib

bumpversion/bump.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
if TYPE_CHECKING: # pragma: no-coverage
77
from bumpversion.files import ConfiguredFile
8-
from bumpversion.version_part import Version
8+
from bumpversion.versioning.models import Version
99

1010
from bumpversion.config import Config
1111
from bumpversion.config.files import update_config_file

bumpversion/config/files.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
if TYPE_CHECKING: # pragma: no-coverage
1212
from bumpversion.config.models import Config
13-
from bumpversion.version_part import Version
13+
from bumpversion.versioning.models import Version
1414

1515
logger = get_indented_logger(__name__)
1616

bumpversion/files.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from bumpversion.config.models import FileChange, VersionPartConfig
1010
from bumpversion.exceptions import VersionNotFoundError
1111
from bumpversion.ui import get_indented_logger
12-
from bumpversion.version_part import Version, VersionConfig
12+
from bumpversion.version_part import VersionConfig
13+
from bumpversion.versioning.models import Version
1314

1415
logger = get_indented_logger(__name__)
1516

bumpversion/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
if TYPE_CHECKING: # pragma: no-coverage
88
from bumpversion.config import Config
9-
from bumpversion.version_part import Version
9+
from bumpversion.versioning.models import Version
1010

1111

1212
def extract_regex_flags(regex_pattern: str) -> Tuple[str, str]:

bumpversion/version_part.py

+3-114
Original file line numberDiff line numberDiff line change
@@ -2,130 +2,19 @@
22
import re
33
import string
44
from copy import copy
5-
from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union
5+
from typing import Any, Dict, List, MutableMapping, Optional, Tuple
66

77
from click import UsageError
88

99
from bumpversion.config.models import VersionPartConfig
10-
from bumpversion.exceptions import FormattingError, InvalidVersionPartError, MissingValueError
11-
from bumpversion.functions import NumericFunction, PartFunction, ValuesFunction
10+
from bumpversion.exceptions import FormattingError, MissingValueError
1211
from bumpversion.ui import get_indented_logger
1312
from bumpversion.utils import key_val_string, labels_for_format
13+
from bumpversion.versioning.models import Version, VersionPart
1414

1515
logger = get_indented_logger(__name__)
1616

1717

18-
class VersionPart:
19-
"""
20-
Represent part of a version number.
21-
22-
Determines the PartFunction that rules how the part behaves when increased or reset
23-
based on the configuration given.
24-
"""
25-
26-
def __init__(self, config: VersionPartConfig, value: Union[str, int, None] = None):
27-
self._value = str(value) if value is not None else None
28-
self.config = config
29-
self.func: Optional[PartFunction] = None
30-
if config.values:
31-
str_values = [str(v) for v in config.values]
32-
str_optional_value = str(config.optional_value) if config.optional_value is not None else None
33-
str_first_value = str(config.first_value) if config.first_value is not None else None
34-
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
35-
else:
36-
self.func = NumericFunction(config.optional_value, config.first_value or "0")
37-
38-
@property
39-
def value(self) -> str:
40-
"""Return the value of the part."""
41-
return self._value or self.func.optional_value
42-
43-
def copy(self) -> "VersionPart":
44-
"""Return a copy of the part."""
45-
return VersionPart(self.config, self._value)
46-
47-
def bump(self) -> "VersionPart":
48-
"""Return a part with bumped value."""
49-
return VersionPart(self.config, self.func.bump(self.value))
50-
51-
def null(self) -> "VersionPart":
52-
"""Return a part with first value."""
53-
return VersionPart(self.config, self.func.first_value)
54-
55-
@property
56-
def is_optional(self) -> bool:
57-
"""Is the part optional?"""
58-
return self.value == self.func.optional_value
59-
60-
@property
61-
def is_independent(self) -> bool:
62-
"""Is the part independent of the other parts?"""
63-
return self.config.independent
64-
65-
def __format__(self, format_spec: str) -> str:
66-
try:
67-
val = int(self.value)
68-
except ValueError:
69-
return self.value
70-
else:
71-
return int.__format__(val, format_spec)
72-
73-
def __repr__(self) -> str:
74-
return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"
75-
76-
def __eq__(self, other: Any) -> bool:
77-
return self.value == other.value if isinstance(other, VersionPart) else False
78-
79-
80-
class Version:
81-
"""The specification of a version and its parts."""
82-
83-
def __init__(self, values: Dict[str, VersionPart], original: Optional[str] = None):
84-
self.values = values
85-
self.original = original
86-
87-
def __getitem__(self, key: str) -> VersionPart:
88-
return self.values[key]
89-
90-
def __len__(self) -> int:
91-
return len(self.values)
92-
93-
def __iter__(self):
94-
return iter(self.values)
95-
96-
def __repr__(self):
97-
return f"<bumpversion.Version:{key_val_string(self.values)}>"
98-
99-
def __eq__(self, other: Any) -> bool:
100-
return (
101-
all(value == other.values[key] for key, value in self.values.items())
102-
if isinstance(other, Version)
103-
else False
104-
)
105-
106-
def bump(self, part_name: str, order: List[str]) -> "Version":
107-
"""Increase the value of the given part."""
108-
bumped = False
109-
110-
new_values = {}
111-
112-
for label in order:
113-
if label not in self.values:
114-
continue
115-
if label == part_name:
116-
new_values[label] = self.values[label].bump()
117-
bumped = True
118-
elif bumped and not self.values[label].is_independent:
119-
new_values[label] = self.values[label].null()
120-
else:
121-
new_values[label] = self.values[label].copy()
122-
123-
if not bumped:
124-
raise InvalidVersionPartError(f"No part named {part_name!r}")
125-
126-
return Version(new_values)
127-
128-
12918
class VersionConfig:
13019
"""
13120
Hold a complete representation of a version string.

bumpversion/versioning/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module for managing Versions and their internal parts."""

bumpversion/functions.py bumpversion/versioning/functions.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class NumericFunction(PartFunction):
2929
considered (e.g. 'r3-001' --> 'r4-001').
3030
"""
3131

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

3434
def __init__(self, optional_value: Union[str, int, None] = None, first_value: Union[str, int, None] = None):
3535
if first_value is not None and not self.FIRST_NUMERIC.search(str(first_value)):
@@ -43,7 +43,14 @@ def bump(self, value: Union[str, int]) -> str:
4343
match = self.FIRST_NUMERIC.search(str(value))
4444
if not match:
4545
raise ValueError(f"The given value {value} does not contain any digit")
46+
4647
part_prefix, part_numeric, part_suffix = match.groups()
48+
49+
if int(part_numeric) < int(self.first_value):
50+
raise ValueError(
51+
f"The given value {value} is lower than the first value {self.first_value} and cannot be bumped."
52+
)
53+
4754
bumped_numeric = int(part_numeric) + 1
4855

4956
return "".join([part_prefix, str(bumped_numeric), part_suffix])

bumpversion/versioning/models.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Models for managing versioning of software projects."""
2+
from typing import Any, Dict, List, Optional, Union
3+
4+
from bumpversion.config.models import VersionPartConfig
5+
from bumpversion.exceptions import InvalidVersionPartError
6+
from bumpversion.utils import key_val_string
7+
from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction
8+
9+
10+
class VersionPart:
11+
"""
12+
Represent part of a version number.
13+
14+
Determines the PartFunction that rules how the part behaves when increased or reset
15+
based on the configuration given.
16+
"""
17+
18+
def __init__(self, config: VersionPartConfig, value: Union[str, int, None] = None):
19+
self._value = str(value) if value is not None else None
20+
self.config = config
21+
self.func: Optional[PartFunction] = None
22+
if config.values:
23+
str_values = [str(v) for v in config.values]
24+
str_optional_value = str(config.optional_value) if config.optional_value is not None else None
25+
str_first_value = str(config.first_value) if config.first_value is not None else None
26+
self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
27+
else:
28+
self.func = NumericFunction(config.optional_value, config.first_value or "0")
29+
30+
@property
31+
def value(self) -> str:
32+
"""Return the value of the part."""
33+
return self._value or self.func.optional_value
34+
35+
def copy(self) -> "VersionPart":
36+
"""Return a copy of the part."""
37+
return VersionPart(self.config, self._value)
38+
39+
def bump(self) -> "VersionPart":
40+
"""Return a part with bumped value."""
41+
return VersionPart(self.config, self.func.bump(self.value))
42+
43+
def null(self) -> "VersionPart":
44+
"""Return a part with first value."""
45+
return VersionPart(self.config, self.func.first_value)
46+
47+
@property
48+
def is_optional(self) -> bool:
49+
"""Is the part optional?"""
50+
return self.value == self.func.optional_value
51+
52+
@property
53+
def is_independent(self) -> bool:
54+
"""Is the part independent of the other parts?"""
55+
return self.config.independent
56+
57+
def __format__(self, format_spec: str) -> str:
58+
try:
59+
val = int(self.value)
60+
except ValueError:
61+
return self.value
62+
else:
63+
return int.__format__(val, format_spec)
64+
65+
def __repr__(self) -> str:
66+
return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"
67+
68+
def __eq__(self, other: Any) -> bool:
69+
return self.value == other.value if isinstance(other, VersionPart) else False
70+
71+
72+
class Version:
73+
"""The specification of a version and its parts."""
74+
75+
def __init__(self, values: Dict[str, VersionPart], original: Optional[str] = None):
76+
self.values = values
77+
self.original = original
78+
79+
def __getitem__(self, key: str) -> VersionPart:
80+
return self.values[key]
81+
82+
def __len__(self) -> int:
83+
return len(self.values)
84+
85+
def __iter__(self):
86+
return iter(self.values)
87+
88+
def __repr__(self):
89+
return f"<bumpversion.Version:{key_val_string(self.values)}>"
90+
91+
def __eq__(self, other: Any) -> bool:
92+
return (
93+
all(value == other.values[key] for key, value in self.values.items())
94+
if isinstance(other, Version)
95+
else False
96+
)
97+
98+
def bump(self, part_name: str, order: List[str]) -> "Version":
99+
"""Increase the value of the given part."""
100+
bumped = False
101+
102+
new_values = {}
103+
104+
for label in order:
105+
if label not in self.values:
106+
continue
107+
if label == part_name:
108+
new_values[label] = self.values[label].bump()
109+
bumped = True
110+
elif bumped and not self.values[label].is_independent:
111+
new_values[label] = self.values[label].null()
112+
else:
113+
new_values[label] = self.values[label].copy()
114+
115+
if not bumped:
116+
raise InvalidVersionPartError(f"No part named {part_name!r}")
117+
118+
return Version(new_values)

0 commit comments

Comments
 (0)