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

1"""Bump My Version configuration models."""

2 from __future__ import annotations

3

4 import re

5 from collections import defaultdict

6 from itertools import chain

7 from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union

8

9 from pydantic import BaseModel, Field

10 from pydantic_settings import BaseSettings, SettingsConfigDict

11

12 from bumpversion.ui import get_indented_logger

13

14 if TYPE_CHECKING: 14 ↛ 15line 14 didn't jump to line 15, because the condition on line 14 was never true

15 from bumpversion.scm import SCMInfo

16 from bumpversion.version_part import VersionConfig

17

18 logger = get_indented_logger(__name__)

19

20

21 class VersionPartConfig(BaseModel):

22 """Configuration of a part of the version."""

23

24 values: Optional[list] = None # Optional. Numeric is used if missing or no items in list

25 optional_value: Optional[str] = None # Optional.

26 # Defaults to first value. 0 in the case of numeric. Empty string means nothing is optional.

27 first_value: Union[str, int, None] = None # Optional. Defaults to first value in values

28 independent: bool = False

29

30

31 class FileChange(BaseModel):

32 """A change to make to a file."""

33

34 parse: str

35 serialize: tuple

36 search: str

37 replace: str

38 regex: bool

39 ignore_missing_version: bool

40 filename: Optional[str] = None

41 glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins

42 key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file

43

44 def __hash__(self):

45 """Return a hash of the model."""

46 return hash(tuple(sorted(self.model_dump().items())))

47

48 def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]:

49 """

50 Render the search pattern and return the compiled regex pattern and the raw pattern.

51

52 Args:

53 context: The context to use for rendering the search pattern

54

55 Returns:

56 A tuple of the compiled regex pattern and the raw pattern as a string.

57 """

58 logger.debug("Rendering search pattern with context")

59 logger.indent()

60 # the default search pattern is escaped, so we can still use it in a regex

61 raw_pattern = self.search.format(**context)

62 default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL)

63 if not self.regex: 63 ↛ 68line 63 didn't jump to line 68, because the condition on line 63 was never false

64 logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern)

65 logger.dedent()

66 return default, raw_pattern

67

68 re_context = {key: re.escape(str(value)) for key, value in context.items()}

69 regex_pattern = self.search.format(**re_context)

70 try:

71 search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL)

72 logger.debug("Searching for the regex: '%s'", search_for_re.pattern)

73 logger.dedent()

74 return search_for_re, raw_pattern

75 except re.error as e:

76 logger.error("Invalid regex '%s': %s.", default, e)

77

78 logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern)

79 logger.dedent()

80

81 return default, raw_pattern

82

83

84 class Config(BaseSettings):

85 """Bump Version configuration."""

86

87 current_version: Optional[str]

88 parse: str

89 serialize: tuple = Field(min_length=1)

90 search: str

91 replace: str

92 regex: bool

93 ignore_missing_version: bool

94 tag: bool

95 sign_tags: bool

96 tag_name: str

97 tag_message: Optional[str]

98 allow_dirty: bool

99 commit: bool

100 message: str

101 commit_args: Optional[str]

102 scm_info: Optional["SCMInfo"]

103 parts: Dict[str, VersionPartConfig]

104 files: List[FileChange] = Field(default_factory=list)

105 included_paths: List[str] = Field(default_factory=list)

106 excluded_paths: List[str] = Field(default_factory=list)

107 model_config = SettingsConfigDict(env_prefix="bumpversion_")

108 _resolved_filemap: Optional[Dict[str, List[FileChange]]] = None

109

110 def add_files(self, filename: Union[str, List[str]]) -> None:

111 """Add a filename to the list of files."""

112 filenames = [filename] if isinstance(filename, str) else filename

113 files = set(self.files)

114 for name in filenames:

115 files.add(

116 FileChange(

117 filename=name,

118 glob=None,

119 key_path=None,

120 parse=self.parse,

121 serialize=self.serialize,

122 search=self.search,

123 replace=self.replace,

124 regex=self.regex,

125 ignore_missing_version=self.ignore_missing_version,

126 )

127 )

128 self.files = list(files)

129

130 self._resolved_filemap = None

131

132 @property

133 def resolved_filemap(self) -> Dict[str, List[FileChange]]:

134 """Return the cached resolved filemap."""

135 if self._resolved_filemap is None:

136 self._resolved_filemap = self._resolve_filemap()

137 return self._resolved_filemap

138

139 def _resolve_filemap(self) -> Dict[str, List[FileChange]]:

140 """Return a map of filenames to file configs, expanding any globs."""

141 from bumpversion.config.utils import resolve_glob_files

142

143 output = defaultdict(list)

144 new_files = []

145 for file_cfg in self.files:

146 if file_cfg.glob: 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true

147 new_files.extend(resolve_glob_files(file_cfg))

148 else:

149 new_files.append(file_cfg)

150

151 for file_cfg in new_files:

152 output[file_cfg.filename].append(file_cfg)

153 return output

154

155 @property

156 def files_to_modify(self) -> List[FileChange]:

157 """Return a list of files to modify."""

158 files_not_excluded = [filename for filename in self.resolved_filemap if filename not in self.excluded_paths]

159 inclusion_set = set(self.included_paths) | set(files_not_excluded)

160 return list(

161 chain.from_iterable(

162 file_cfg_list for key, file_cfg_list in self.resolved_filemap.items() if key in inclusion_set

163 )

164 )

165

166 @property

167 def version_config(self) -> "VersionConfig":

168 """Return the version configuration."""

169 from bumpversion.version_part import VersionConfig

170

171 return VersionConfig(self.parse, self.serialize, self.search, self.replace, self.parts)