Coverage for /Users/OORDCOR/Documents/code/bump-my-version/bumpversion/versioning/models.py: 89%

1"""Models for managing versioning of software projects."""

2

3 from __future__ import annotations

4

5 from collections import defaultdict, deque

6 from typing import Any, Dict, List, Optional, Union

7

8 from pydantic import BaseModel

9

10 from bumpversion.exceptions import InvalidVersionPartError

11 from bumpversion.utils import key_val_string

12 from bumpversion.versioning.functions import NumericFunction, PartFunction, ValuesFunction

13

14

15 class VersionComponent:

16 """

17 Represent part of a version number.

18

19 Determines the PartFunction that rules how the part behaves when increased or reset

20 based on the configuration given.

21 """

22

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")

43

44 @property

45 def value(self) -> str:

46 """Return the value of the part."""

47 return self._value or self.func.optional_value

48

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 )

59

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

65

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

71

72 @property

73 def is_optional(self) -> bool:

74 """Is the part optional?"""

75 return self.value == self.func.optional_value

76

77 @property

78 def is_independent(self) -> bool:

79 """Is the part independent of the other parts?"""

80 return self.independent

81

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)

89

90 def __repr__(self) -> str:

91 return f"<bumpversion.VersionPart:{self.func.__class__.__name__}:{self.value}>"

92

93 def __eq__(self, other: Any) -> bool:

94 return self.value == other.value if isinstance(other, VersionComponent) else False

95

96

97 class VersionComponentSpec(BaseModel):

98 """

99 Configuration of a version component.

100

101 This is used to read in the configuration from the bumpversion config file.

102 """

103

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

111

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 )

122

123

124 class VersionSpec:

125 """The specification of a version's components and their relationships."""

126

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.")

134

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

147

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)

155

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 = []

160

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])

166

167 return visited

168

169

170 class Version:

171 """The specification of a version and its parts."""

172

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

179

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()}

183

184 def __getitem__(self, key: str) -> VersionComponent:

185 return self.components[key]

186

187 def __len__(self) -> int:

188 return len(self.components)

189

190 def __iter__(self):

191 return iter(self.components)

192

193 def __repr__(self):

194 return f"<bumpversion.Version:{key_val_string(self.components)}>"

195

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 )

202

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]

206

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}")

211

212 components_to_reset = self.version_spec.get_dependents(component_name)

213

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()

219

220 return Version(self.version_spec, new_values, self.original)