Coverage for /Users/OORDCOR/Documents/code/bump-my-version/bumpversion/config/models.py: 78%
105 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-18 15:08 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-18 15:08 -0600
1"""Bump My Version configuration models."""
2from __future__ import annotations
4import re
5from collections import defaultdict
6from itertools import chain
7from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union
9from pydantic import BaseModel, Field
10from pydantic_settings import BaseSettings, SettingsConfigDict
12from bumpversion.ui import get_indented_logger
14if 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
18logger = get_indented_logger(__name__)
21class VersionPartConfig(BaseModel):
22 """Configuration of a part of the version."""
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
31class FileChange(BaseModel):
32 """A change to make to a file."""
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
44 def __hash__(self):
45 """Return a hash of the model."""
46 return hash(tuple(sorted(self.model_dump().items())))
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.
52 Args:
53 context: The context to use for rendering the search pattern
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
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)
78 logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern)
79 logger.dedent()
81 return default, raw_pattern
84class Config(BaseSettings):
85 """Bump Version configuration."""
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
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)
130 self._resolved_filemap = None
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
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
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)
151 for file_cfg in new_files:
152 output[file_cfg.filename].append(file_cfg)
153 return output
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 )
166 @property
167 def version_config(self) -> "VersionConfig":
168 """Return the version configuration."""
169 from bumpversion.version_part import VersionConfig
171 return VersionConfig(self.parse, self.serialize, self.search, self.replace, self.parts)