Coverage for /Users/coordt/Documents/code/bump-my-version/bumpversion/config/models.py: 77%
110 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-06-12 09:26 -0500
« prev ^ index » next coverage.py v7.4.4, created at 2024-06-12 09:26 -0500
1"""Bump My Version configuration models."""
3from __future__ import annotations
5import re
6from collections import defaultdict
7from itertools import chain
8from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Tuple, Union
10from pydantic import BaseModel, Field
11from pydantic_settings import BaseSettings, SettingsConfigDict
13from bumpversion.ui import get_indented_logger
14from bumpversion.versioning.models import VersionComponentSpec # NOQA: TCH001
16if TYPE_CHECKING: 16 ↛ 17line 16 didn't jump to line 17, because the condition on line 16 was never true
17 from bumpversion.scm import SCMInfo
18 from bumpversion.version_part import VersionConfig
19 from bumpversion.versioning.models import VersionSpec
21logger = get_indented_logger(__name__)
24class FileChange(BaseModel):
25 """A change to make to a file."""
27 parse: str
28 serialize: tuple
29 search: str
30 replace: str
31 regex: bool
32 ignore_missing_version: bool
33 ignore_missing_file: bool
34 filename: Optional[str] = None
35 glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins
36 glob_exclude: Optional[tuple] = None
37 key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file
38 include_bumps: Optional[tuple] = None
39 exclude_bumps: Optional[tuple] = None
41 def __hash__(self):
42 """Return a hash of the model."""
43 return hash(tuple(sorted(self.model_dump().items())))
45 def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]:
46 """
47 Render the search pattern and return the compiled regex pattern and the raw pattern.
49 Args:
50 context: The context to use for rendering the search pattern
52 Returns:
53 A tuple of the compiled regex pattern and the raw pattern as a string.
54 """
55 logger.debug("Rendering search pattern with context")
56 logger.indent()
57 # the default search pattern is escaped, so we can still use it in a regex
58 raw_pattern = self.search.format(**context)
59 default = re.compile(re.escape(raw_pattern), re.MULTILINE | re.DOTALL)
60 if not self.regex: 60 ↛ 65line 60 didn't jump to line 65, because the condition on line 60 was never false
61 logger.debug("No RegEx flag detected. Searching for the default pattern: '%s'", default.pattern)
62 logger.dedent()
63 return default, raw_pattern
65 re_context = {key: re.escape(str(value)) for key, value in context.items()}
66 regex_pattern = self.search.format(**re_context)
67 try:
68 search_for_re = re.compile(regex_pattern, re.MULTILINE | re.DOTALL)
69 logger.debug("Searching for the regex: '%s'", search_for_re.pattern)
70 logger.dedent()
71 return search_for_re, raw_pattern
72 except re.error as e:
73 logger.error("Invalid regex '%s': %s.", default, e)
75 logger.debug("Invalid regex. Searching for the default pattern: '%s'", raw_pattern)
76 logger.dedent()
78 return default, raw_pattern
81class Config(BaseSettings):
82 """Bump Version configuration."""
84 current_version: Optional[str]
85 parse: str
86 serialize: tuple = Field(min_length=1)
87 search: str
88 replace: str
89 regex: bool
90 ignore_missing_version: bool
91 ignore_missing_files: bool
92 tag: bool
93 sign_tags: bool
94 tag_name: str
95 tag_message: Optional[str]
96 allow_dirty: bool
97 commit: bool
98 message: str
99 commit_args: Optional[str]
100 scm_info: Optional["SCMInfo"]
101 parts: Dict[str, VersionComponentSpec]
102 files: List[FileChange] = Field(default_factory=list)
103 included_paths: List[str] = Field(default_factory=list)
104 excluded_paths: List[str] = Field(default_factory=list)
105 model_config = SettingsConfigDict(env_prefix="bumpversion_")
106 _resolved_filemap: Optional[Dict[str, List[FileChange]]] = None
108 def add_files(self, filename: Union[str, List[str]]) -> None:
109 """Add a filename to the list of files."""
110 filenames = [filename] if isinstance(filename, str) else filename
111 files = set(self.files)
112 for name in filenames:
113 files.add(
114 FileChange(
115 filename=name,
116 glob=None,
117 key_path=None,
118 parse=self.parse,
119 serialize=self.serialize,
120 search=self.search,
121 replace=self.replace,
122 regex=self.regex,
123 ignore_missing_version=self.ignore_missing_version,
124 ignore_missing_file=self.ignore_missing_files,
125 include_bumps=tuple(self.parts.keys()),
126 exclude_bumps=(),
127 )
128 )
129 self.files = list(files)
131 self._resolved_filemap = None
133 @property
134 def resolved_filemap(self) -> Dict[str, List[FileChange]]:
135 """Return the cached resolved filemap."""
136 if self._resolved_filemap is None:
137 self._resolved_filemap = self._resolve_filemap()
138 return self._resolved_filemap
140 def _resolve_filemap(self) -> Dict[str, List[FileChange]]:
141 """Return a map of filenames to file configs, expanding any globs."""
142 from bumpversion.config.utils import resolve_glob_files
144 output = defaultdict(list)
145 new_files = []
146 for file_cfg in self.files:
147 if file_cfg.glob: 147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true
148 new_files.extend(resolve_glob_files(file_cfg))
149 else:
150 new_files.append(file_cfg)
152 for file_cfg in new_files:
153 output[file_cfg.filename].append(file_cfg)
154 return output
156 @property
157 def files_to_modify(self) -> List[FileChange]:
158 """Return a list of files to modify."""
159 files_not_excluded = [filename for filename in self.resolved_filemap if filename not in self.excluded_paths]
160 inclusion_set = set(self.included_paths) | set(files_not_excluded)
161 return list(
162 chain.from_iterable(
163 file_cfg_list for key, file_cfg_list in self.resolved_filemap.items() if key in inclusion_set
164 )
165 )
167 @property
168 def version_config(self) -> "VersionConfig":
169 """Return the version configuration."""
170 from bumpversion.version_part import VersionConfig
172 return VersionConfig(self.parse, self.serialize, self.search, self.replace, self.parts)
174 def version_spec(self, version: Optional[str] = None) -> "VersionSpec":
175 """Return the version specification."""
176 from bumpversion.versioning.models import VersionSpec
178 return VersionSpec(self.parts)