Skip to content

Commit 909396d

Browse files
committed
Changed the management of file changes
File changes are hashable to weed out duplication.
1 parent 84556f8 commit 909396d

File tree

7 files changed

+53
-38
lines changed

7 files changed

+53
-38
lines changed

bumpversion/config/models.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class FileChange(BaseModel):
3232
"""A change to make to a file."""
3333

3434
parse: str
35-
serialize: List[str]
35+
serialize: tuple
3636
search: str
3737
replace: str
3838
regex: bool
@@ -41,6 +41,10 @@ class FileChange(BaseModel):
4141
glob: Optional[str] = None # Conflicts with filename. If both are specified, glob wins
4242
key_path: Optional[str] = None # If specified, and has an appropriate extension, will be treated as a data file
4343

44+
def __hash__(self):
45+
"""Return a hash of the model."""
46+
return hash(tuple(sorted(self.model_dump().items())))
47+
4448
def get_search_pattern(self, context: MutableMapping) -> Tuple[re.Pattern, str]:
4549
"""
4650
Render the search pattern and return the compiled regex pattern and the raw pattern.
@@ -82,7 +86,7 @@ class Config(BaseSettings):
8286

8387
current_version: Optional[str]
8488
parse: str
85-
serialize: List[str] = Field(min_length=1)
89+
serialize: tuple = Field(min_length=1)
8690
search: str
8791
replace: str
8892
regex: bool
@@ -97,7 +101,7 @@ class Config(BaseSettings):
97101
commit_args: Optional[str]
98102
scm_info: Optional["SCMInfo"]
99103
parts: Dict[str, VersionPartConfig]
100-
files: List[FileChange]
104+
files: List[FileChange] = Field(default_factory=list)
101105
included_paths: List[str] = Field(default_factory=list)
102106
excluded_paths: List[str] = Field(default_factory=list)
103107
model_config = SettingsConfigDict(env_prefix="bumpversion_")
@@ -106,8 +110,9 @@ class Config(BaseSettings):
106110
def add_files(self, filename: Union[str, List[str]]) -> None:
107111
"""Add a filename to the list of files."""
108112
filenames = [filename] if isinstance(filename, str) else filename
113+
files = set(self.files)
109114
for name in filenames:
110-
self.files.append(
115+
files.add(
111116
FileChange(
112117
filename=name,
113118
glob=None,
@@ -120,6 +125,8 @@ def add_files(self, filename: Union[str, List[str]]) -> None:
120125
ignore_missing_version=self.ignore_missing_version,
121126
)
122127
)
128+
self.files = list(files)
129+
123130
self._resolved_filemap = None
124131

125132
@property

bumpversion/version_part.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re
33
import string
44
from copy import copy
5-
from typing import Any, Dict, List, MutableMapping, Optional, Union
5+
from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union
66

77
from click import UsageError
88

@@ -134,7 +134,7 @@ class VersionConfig:
134134
def __init__(
135135
self,
136136
parse: str,
137-
serialize: List[str],
137+
serialize: Tuple[str],
138138
search: str,
139139
replace: str,
140140
part_configs: Optional[Dict[str, VersionPartConfig]] = None,

bumpversion/yaml_dump.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from collections import UserDict
44
from io import StringIO
55
from textwrap import indent
6-
from typing import Any, Callable
6+
from typing import Any, Callable, Union
77

88
DumperFunc = Callable[[Any], str]
99

@@ -89,7 +89,7 @@ def format_dict(val: dict) -> str:
8989

9090
for key, value in sorted(val.items()):
9191
rendered_value = dump(value).strip()
92-
if isinstance(value, (dict, list)):
92+
if isinstance(value, (dict, list, tuple)):
9393
rendered_value = f"\n{indent(rendered_value, INDENT)}"
9494
else:
9595
rendered_value = f" {rendered_value}"
@@ -101,7 +101,7 @@ def format_dict(val: dict) -> str:
101101
YAML_DUMPERS.add_dumper(dict, format_dict)
102102

103103

104-
def format_list(val: list) -> str:
104+
def format_sequence(val: Union[list, tuple]) -> str:
105105
"""Return a string representation of a value."""
106106
buffer = StringIO()
107107

@@ -110,7 +110,7 @@ def format_list(val: list) -> str:
110110
if isinstance(item, dict):
111111
rendered_value = indent(rendered_value, INDENT).strip()
112112

113-
if isinstance(item, list):
113+
if isinstance(item, (list, tuple)):
114114
rendered_value = f"\n{indent(rendered_value, INDENT)}"
115115
else:
116116
rendered_value = f" {rendered_value}"
@@ -119,7 +119,8 @@ def format_list(val: list) -> str:
119119
return buffer.getvalue()
120120

121121

122-
YAML_DUMPERS.add_dumper(list, format_list)
122+
YAML_DUMPERS.add_dumper(list, format_sequence)
123+
YAML_DUMPERS.add_dumper(tuple, format_sequence)
123124

124125

125126
def format_none(_: None) -> str:

tests/fixtures/basic_cfg_expected.txt

+7-7
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
'regex': False,
1212
'replace': '{new_version}',
1313
'search': '{current_version}',
14-
'serialize': ['{major}.{minor}.{patch}-{release}',
15-
'{major}.{minor}.{patch}']},
14+
'serialize': ('{major}.{minor}.{patch}-{release}',
15+
'{major}.{minor}.{patch}')},
1616
{'filename': 'bumpversion/__init__.py',
1717
'glob': None,
1818
'ignore_missing_version': False,
@@ -21,8 +21,8 @@
2121
'regex': False,
2222
'replace': '{new_version}',
2323
'search': '{current_version}',
24-
'serialize': ['{major}.{minor}.{patch}-{release}',
25-
'{major}.{minor}.{patch}']},
24+
'serialize': ('{major}.{minor}.{patch}-{release}',
25+
'{major}.{minor}.{patch}')},
2626
{'filename': 'CHANGELOG.md',
2727
'glob': None,
2828
'ignore_missing_version': False,
@@ -31,8 +31,8 @@
3131
'regex': False,
3232
'replace': '**unreleased**\n**v{new_version}**',
3333
'search': '**unreleased**',
34-
'serialize': ['{major}.{minor}.{patch}-{release}',
35-
'{major}.{minor}.{patch}']}],
34+
'serialize': ('{major}.{minor}.{patch}-{release}',
35+
'{major}.{minor}.{patch}')}],
3636
'ignore_missing_version': False,
3737
'included_paths': [],
3838
'message': 'Bump version: {current_version} → {new_version}',
@@ -63,7 +63,7 @@
6363
'short_branch_name': None,
6464
'tool': None},
6565
'search': '{current_version}',
66-
'serialize': ['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'],
66+
'serialize': ('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}'),
6767
'sign_tags': False,
6868
'tag': True,
6969
'tag_message': 'Bump version: {current_version} → {new_version}',

tests/test_cli.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker
139139
assert the_config.current_version == "1.1.0"
140140
assert the_config.allow_dirty
141141
assert the_config.parse == r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)"
142-
assert the_config.serialize == ["XXX{spam};{blob};{slurp}"]
142+
assert the_config.serialize == ("XXX{spam};{blob};{slurp}",)
143143
assert the_config.search == "my-search"
144144
assert the_config.replace == "my-replace"
145145
assert the_config.commit is False
@@ -203,7 +203,7 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path):
203203
"current_version=1.0.0",
204204
"excluded_paths=[]",
205205
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
206-
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
206+
"serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
207207
"search={current_version}",
208208
"replace={new_version}",
209209
"regex=False",
@@ -220,19 +220,19 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path):
220220
(
221221
"files=[{'parse': "
222222
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
223-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
224-
"'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': "
223+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
224+
"'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
225225
"'{new_version}', 'regex': False, 'ignore_missing_version': False, "
226226
"'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
227227
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
228-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
229-
"'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': "
228+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
229+
"'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
230230
"'{new_version}', 'regex': False, 'ignore_missing_version': False, "
231231
"'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
232232
"{'parse': "
233233
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
234-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
235-
"'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': "
234+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
235+
"'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
236236
"'**unreleased**\\n**v{new_version}**', 'regex': False, "
237237
"'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
238238
"'key_path': None}]"
@@ -263,7 +263,7 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path):
263263
"current_version=1.0.0",
264264
"excluded_paths=[]",
265265
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
266-
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
266+
"serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
267267
"search={current_version}",
268268
"replace={new_version}",
269269
"regex=False",
@@ -280,19 +280,19 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path):
280280
(
281281
"files=[{'parse': "
282282
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
283-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
284-
"'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': "
283+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
284+
"'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
285285
"'{new_version}', 'regex': False, 'ignore_missing_version': False, "
286286
"'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
287287
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
288-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
289-
"'{major}.{minor}.{patch}'], 'search': '{current_version}', 'replace': "
288+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
289+
"'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
290290
"'{new_version}', 'regex': False, 'ignore_missing_version': False, "
291291
"'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
292292
"{'parse': "
293293
"'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
294-
"'serialize': ['{major}.{minor}.{patch}-{release}', "
295-
"'{major}.{minor}.{patch}'], 'search': '**unreleased**', 'replace': "
294+
"'serialize': ('{major}.{minor}.{patch}-{release}', "
295+
"'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
296296
"'**unreleased**\\n**v{new_version}**', 'regex': False, "
297297
"'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
298298
"'key_path': None}]"

tests/test_config/test_files.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def test_multiple_config_files(tmp_path: Path):
140140

141141
assert cfg.current_version == "0.10.5"
142142
assert cfg.parse == "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?"
143-
assert cfg.serialize == ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"]
143+
assert cfg.serialize == ("{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}")
144144

145145

146146
TOML_EXPECTED_DIFF = (
@@ -283,7 +283,7 @@ def test_file_overrides_config(fixtures_path: Path):
283283
assert file_map["should_override_parse.txt"].ignore_missing_version == conf.ignore_missing_version
284284

285285
assert file_map["should_override_serialize.txt"].parse == conf.parse
286-
assert file_map["should_override_serialize.txt"].serialize == ["{major}"]
286+
assert file_map["should_override_serialize.txt"].serialize == ("{major}",)
287287
assert file_map["should_override_serialize.txt"].search == conf.search
288288
assert file_map["should_override_serialize.txt"].replace == conf.replace
289289
assert file_map["should_override_serialize.txt"].regex == conf.regex

tests/test_yaml.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
def test_dump_unknown():
9-
assert yaml_dump.dump((1, 2)) == '"(1, 2)"'
9+
assert yaml_dump.dump({1, 2}) == '"{1, 2}"'
1010

1111

1212
def test_format_str():
@@ -42,10 +42,15 @@ def test_format_dict():
4242
"key8": True,
4343
"key9": False,
4444
"key10": 1.43,
45+
"key11": (1, 2, 3),
4546
}
4647
expected = (
4748
'key: "strval"\n'
4849
"key10: 1.43\n"
50+
"key11:\n"
51+
" - 1\n"
52+
" - 2\n"
53+
" - 3\n"
4954
"key2: 30\n"
5055
"key3: 2023-06-19 13:45:30\n"
5156
"key4: 2023-06-19\n"
@@ -63,8 +68,10 @@ def test_format_dict():
6368

6469

6570
def test_format_list():
66-
assert yaml_dump.format_list(["item"]) == '- "item"\n'
67-
assert yaml_dump.format_list(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n'
71+
assert yaml_dump.format_sequence(["item"]) == '- "item"\n'
72+
assert yaml_dump.format_sequence(["item", ["item2"]]) == '- "item"\n-\n - "item2"\n'
73+
assert yaml_dump.format_sequence(("item",)) == '- "item"\n'
74+
assert yaml_dump.format_sequence(("item", ("item2",))) == '- "item"\n-\n - "item2"\n'
6875

6976

7077
def test_dump_none_val():

0 commit comments

Comments
 (0)