Coverage for /Users/OORDCOR/Documents/code/bump-my-version/bumpversion/versioning/models.py: 89%
116 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-02-24 07:45 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2024-02-24 07:45 -0600
1"""Models for managing versioning of software projects."""
3from __future__ import annotations
5from collections import defaultdict, deque
6from typing import Any, Dict, List, Optional, Union
8from pydantic import BaseModel
10from bumpversion.exceptions import InvalidVersionPartError
11from bumpversion.utils import key_val_string
12from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction
15class VersionComponent:
16 """
17 Represent part of a version number.
19 Determines the PartFunction that rules how the part behaves when increased or reset
20 based on the configuration given.
21 """
23 def __init__(
24 self,
25 values: Optional[list] = None,
26 optional_value: Optional[str] = None,
27 first_value: Union[str, int, None] = None,
28 independent: bool = False,
29 source: Optional[str] = None,
30 value: Union[str, int, None] = None,
31 ):
32 self._value = str(value) if value is not None else None
33 self.func: Optional[PartFunction] = None
34 self.independent = independent
35 self.source = source
36 if values:
37 str_values = [str(v) for v in values]
38 str_optional_value = str(optional_value) if optional_value is not None else None
39 str_first_value = str(first_value) if first_value is not None else None
40 self.func = ValuesFunction(str_values, str_optional_value, str_first_value)
41 else:
42 self.func = NumericFunction(optional_value, first_value or "0")
44 @property
45 def value(self) -> str:
46 """Return the value of the part."""
47 return self._value or self.func.optional_value
49 def copy(self) -> "VersionComponent":
50 """Return a copy of the part."""
51 return VersionComponent(
52 values=getattr(self.func, "_values", None),
53 optional_value=self.func.optional_value,
54 first_value=self.func.first_value,
55 independent=self.independent,
56 source=self.source,
57 value=self._value,
58 )
60 def bump(self) -> "VersionComponent":
61 """Return a part with bumped value."""
62 new_component = self.copy()
63 new_component._value = self.func.bump(self.value)
64 return new_component
66 def null(self) -> "VersionComponent":
67 """Return a part with first value."""
68 new_component = self.copy()
69 new_component._value = self.func.first_value
70 return new_component
72 @property
73 def is_optional(self) -> bool:
74 """Is the part optional?"""
75 return self.value == self.func.optional_value
77 @property
78 def is_independent(self) -> bool:
79 """Is the part independent of the other parts?"""
80 return self.independent
82 def __format__(self, format_spec: str) -> str:
83 try:
84 val = int(self.value)
85 except ValueError:
86 return self.value
87 else:
88 return int.__format__(val, format_spec)
90 def __repr__(self) -> str:
91 return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"
93 def __eq__(self, other: Any) -> bool:
94 return self.value == other.value if isinstance(other, VersionComponent) else False
97class VersionComponentSpec(BaseModel):
98 """
99 Configuration of a version component.
101 This is used to read in the configuration from the bumpversion config file.
102 """
104 values: Optional[list] = None # Optional. Numeric is used if missing or no items in list
105 optional_value: Optional[str] = None # Optional.
106 # Defaults to first value. 0 in the case of numeric. Empty string means nothing is optional.
107 first_value: Union[str, int, None] = None # Optional. Defaults to first value in values
108 independent: bool = False
109 # source: Optional[str] = None # Name of environment variable or context variable to use as the source for value
110 depends_on: Optional[str] = None # The name of the component this component depends on
112 def create_component(self, value: Union[str, int, None] = None) -> VersionComponent:
113 """Generate a version component from the configuration."""
114 return VersionComponent(
115 values=self.values,
116 optional_value=self.optional_value,
117 first_value=self.first_value,
118 independent=self.independent,
119 # source=self.source,
120 value=value,
121 )
124class VersionSpec:
125 """The specification of a version's components and their relationships."""
127 def __init__(self, components: Dict[str, VersionComponentSpec], order: Optional[List[str]] = None):
128 if not components: 128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true
129 raise ValueError("A VersionSpec must have at least one component.")
130 if not order: 130 ↛ 132line 130 didn't jump to line 132, because the condition on line 130 was never false
131 order = list(components.keys())
132 if len(set(order) - set(components.keys())) > 0: 132 ↛ 133line 132 didn't jump to line 133, because the condition on line 132 was never true
133 raise ValueError("The order of components refers to items that are not in your components.")
135 self.component_configs = components
136 self.order = order
137 self.dependency_map = defaultdict(list)
138 previous_component = self.order[0]
139 for component in self.order[1:]:
140 if self.component_configs[component].independent:
141 continue
142 elif self.component_configs[component].depends_on: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true
143 self.dependency_map[self.component_configs[component].depends_on].append(component)
144 else:
145 self.dependency_map[previous_component].append(component)
146 previous_component = component
148 def create_version(self, values: Dict[str, str]) -> "Version":
149 """Generate a version from the given values."""
150 components = {
151 key: comp_config.create_component(value=values.get(key))
152 for key, comp_config in self.component_configs.items()
153 }
154 return Version(version_spec=self, components=components)
156 def get_dependents(self, component_name: str) -> List[str]:
157 """Return the parts that depend on the given part."""
158 stack = deque(self.dependency_map.get(component_name, []), maxlen=len(self.order))
159 visited = []
161 while stack:
162 e = stack.pop()
163 if e not in visited: 163 ↛ 161line 163 didn't jump to line 161, because the condition on line 163 was never false
164 visited.append(e)
165 stack.extendleft(self.dependency_map[e])
167 return visited
170class Version:
171 """The specification of a version and its parts."""
173 def __init__(
174 self, version_spec: VersionSpec, components: Dict[str, VersionComponent], original: Optional[str] = None
175 ):
176 self.version_spec = version_spec
177 self.components = components
178 self.original = original
180 def values(self) -> Dict[str, str]:
181 """Return the values of the parts."""
182 return {key: value.value for key, value in self.components.items()}
184 def __getitem__(self, key: str) -> VersionComponent:
185 return self.components[key]
187 def __len__(self) -> int:
188 return len(self.components)
190 def __iter__(self):
191 return iter(self.components)
193 def __repr__(self):
194 return f"<bumpversion.Version:{key_val_string(self.components)}>"
196 def __eq__(self, other: Any) -> bool:
197 return (
198 all(value == other.components[key] for key, value in self.components.items())
199 if isinstance(other, Version)
200 else False
201 )
203 def required_components(self) -> List[str]:
204 """Return the names of the parts that are required."""
205 return [key for key, value in self.components.items() if value.value != value.func.optional_value]
207 def bump(self, component_name: str) -> "Version":
208 """Increase the value of the specified component, reset its dependents, and return a new Version."""
209 if component_name not in self.components: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true
210 raise InvalidVersionPartError(f"No part named {component_name!r}")
212 components_to_reset = self.version_spec.get_dependents(component_name)
214 new_values = dict(self.components.items())
215 new_values[component_name] = self.components[component_name].bump()
216 for component in components_to_reset:
217 if not self.components[component].is_independent: 217 ↛ 216line 217 didn't jump to line 216, because the condition on line 217 was never false
218 new_values[component] = self.components[component].null()
220 return Version(self.version_spec, new_values, self.original)