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

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

2 

3from __future__ import annotations 

4 

5from collections import defaultdict, deque 

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

7 

8from pydantic import BaseModel 

9 

10from bumpversion.exceptions import InvalidVersionPartError 

11from bumpversion.utils import key_val_string 

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

13 

14 

15class 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 

97class 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 

124class 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 

170class 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)