Skip to content

Commit 646af54

Browse files
committed
Refactored file resolution, inclusion, and exclusion
- Fixes #61 - Config now includes `resolved_filemap` property - resolved filemap exapands all globs - Config now includes `files_to_modify` property - files to modify resolves inclusions and exclutions - Improved Config.add_files property
1 parent 557b4d8 commit 646af54

10 files changed

+159
-70
lines changed

bumpversion/bump.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def do_bump(
7979

8080
ctx = get_context(config, version, next_version)
8181

82-
configured_files = resolve_file_config(config.files, config.version_config)
82+
configured_files = resolve_file_config(config.files_to_modify, config.version_config)
8383
modify_files(configured_files, version, next_version, ctx, dry_run)
8484
update_config_file(config_file, config.current_version, next_version_str, dry_run)
8585

bumpversion/cli.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from bumpversion.aliases import AliasedGroup
1010
from bumpversion.bump import do_bump
1111
from bumpversion.config import find_config_file, get_configuration
12-
from bumpversion.files import modify_files, resolve_file_config
12+
from bumpversion.files import ConfiguredFile, modify_files
1313
from bumpversion.logging import setup_logging
1414
from bumpversion.show import do_show, log_list
1515
from bumpversion.ui import print_warning
@@ -300,10 +300,11 @@ def bump(
300300
config.scm_info.tool.assert_nondirty()
301301

302302
if no_configured_files:
303-
config.files = []
303+
config.excluded_paths = list(config.resolved_filemap.keys())
304304

305305
if files:
306306
config.add_files(files)
307+
config.included_paths = files
307308

308309
do_bump(version_part, new_version, config, found_config_file, dry_run)
309310

@@ -495,20 +496,22 @@ def replace(
495496
config.scm_info.tool.assert_nondirty()
496497

497498
if no_configured_files:
498-
config.files = []
499+
config.excluded_paths = list(config.resolved_filemap.keys())
499500

500501
if files:
501502
config.add_files(files)
503+
config.included_paths = files
502504

503-
version = config.version_config.parse(config.current_version)
505+
configured_files = [
506+
ConfiguredFile(file_cfg, config.version_config, search, replace) for file_cfg in config.files_to_modify
507+
]
504508

509+
version = config.version_config.parse(config.current_version)
505510
if new_version:
506511
next_version = config.version_config.parse(new_version)
507512
else:
508513
next_version = None
509514

510515
ctx = get_context(config, version, next_version)
511516

512-
configured_files = resolve_file_config(config.files, config.version_config, search, replace)
513-
514517
modify_files(configured_files, version, next_version, ctx, dry_run)

bumpversion/config.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Configuration management."""
22
from __future__ import annotations
33

4+
import glob
45
import itertools
56
import logging
67
import re
@@ -66,14 +67,51 @@ class Config(BaseSettings):
6667
scm_info: Optional["SCMInfo"]
6768
parts: Dict[str, VersionPartConfig]
6869
files: List[FileConfig]
70+
included_paths: List[str] = []
71+
excluded_paths: List[str] = []
6972

7073
class Config:
7174
env_prefix = "bumpversion_"
7275

7376
def add_files(self, filename: Union[str, List[str]]) -> None:
7477
"""Add a filename to the list of files."""
7578
filenames = [filename] if isinstance(filename, str) else filename
76-
self.files.extend([FileConfig(filename=name) for name in filenames]) # type: ignore[call-arg]
79+
for name in filenames:
80+
if name in self.resolved_filemap:
81+
continue
82+
self.files.append(
83+
FileConfig(
84+
filename=name,
85+
glob=None,
86+
parse=self.parse,
87+
serialize=self.serialize,
88+
search=self.search,
89+
replace=self.replace,
90+
no_regex=self.no_regex,
91+
ignore_missing_version=self.ignore_missing_version,
92+
)
93+
)
94+
95+
@property
96+
def resolved_filemap(self) -> Dict[str, FileConfig]:
97+
"""Return a map of filenames to file configs, expanding any globs."""
98+
new_files = []
99+
for file_cfg in self.files:
100+
if file_cfg.glob:
101+
new_files.extend(get_glob_files(file_cfg))
102+
else:
103+
new_files.append(file_cfg)
104+
105+
return {file_cfg.filename: file_cfg for file_cfg in new_files}
106+
107+
@property
108+
def files_to_modify(self) -> List[FileConfig]:
109+
"""Return a list of files to modify."""
110+
files_not_excluded = [
111+
file_cfg.filename for file_cfg in self.files if file_cfg.filename not in self.excluded_paths
112+
]
113+
inclusion_set = set(self.included_paths) | set(files_not_excluded)
114+
return [file_cfg for file_cfg in self.files if file_cfg.filename in inclusion_set]
77115

78116
@property
79117
def version_config(self) -> "VersionConfig":
@@ -410,3 +448,22 @@ def update_config_file(
410448

411449
if not dry_run:
412450
config_path.write_text(new_config)
451+
452+
453+
def get_glob_files(file_cfg: FileConfig) -> List[FileConfig]:
454+
"""
455+
Return a list of files that match the glob pattern.
456+
457+
Args:
458+
file_cfg: The file configuration containing the glob pattern
459+
460+
Returns:
461+
A list of resolved file configurations according to the pattern.
462+
"""
463+
files = []
464+
for filename_glob in glob.glob(file_cfg.glob, recursive=True):
465+
new_file_cfg = file_cfg.copy()
466+
new_file_cfg.filename = filename_glob
467+
new_file_cfg.glob = None
468+
files.append(new_file_cfg)
469+
return files

bumpversion/files.py

+1-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Methods for changing files."""
2-
import glob
32
import logging
43
import re
54
from copy import deepcopy
@@ -190,14 +189,7 @@ def resolve_file_config(
190189
Returns:
191190
A list of ConfiguredFiles
192191
"""
193-
configured_files = []
194-
for file_cfg in files:
195-
if file_cfg.glob:
196-
configured_files.extend(get_glob_files(file_cfg, version_config))
197-
else:
198-
configured_files.append(ConfiguredFile(file_cfg, version_config, search, replace))
199-
200-
return configured_files
192+
return [ConfiguredFile(file_cfg, version_config, search, replace) for file_cfg in files]
201193

202194

203195
def modify_files(
@@ -222,29 +214,6 @@ def modify_files(
222214
f.replace_version(current_version, new_version, context, dry_run)
223215

224216

225-
def get_glob_files(
226-
file_cfg: FileConfig, version_config: VersionConfig, search: Optional[str] = None, replace: Optional[str] = None
227-
) -> List[ConfiguredFile]:
228-
"""
229-
Return a list of files that match the glob pattern.
230-
231-
Args:
232-
file_cfg: The file configuration containing the glob pattern
233-
version_config: The version configuration
234-
search: The search pattern to use instead of any configured search pattern
235-
replace: The replace pattern to use instead of any configured replace pattern
236-
237-
Returns:
238-
A list of resolved files according to the pattern.
239-
"""
240-
files = []
241-
for filename_glob in glob.glob(file_cfg.glob, recursive=True):
242-
new_file_cfg = file_cfg.copy()
243-
new_file_cfg.filename = filename_glob
244-
files.append(ConfiguredFile(new_file_cfg, version_config, search, replace))
245-
return files
246-
247-
248217
def _check_files_contain_version(
249218
files: List[ConfiguredFile], current_version: Version, context: MutableMapping
250219
) -> None:

tests/fixtures/basic_cfg_expected.txt

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
'commit': True,
33
'commit_args': None,
44
'current_version': '1.0.0',
5+
'excluded_paths': [],
56
'files': [{'filename': 'setup.py',
67
'glob': None,
78
'ignore_missing_version': False,
@@ -30,6 +31,7 @@
3031
'serialize': ['{major}.{minor}.{patch}-{release}',
3132
'{major}.{minor}.{patch}']}],
3233
'ignore_missing_version': False,
34+
'included_paths': [],
3335
'message': 'Bump version: {current_version} → {new_version}',
3436
'no_regex': False,
3537
'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?',

tests/fixtures/basic_cfg_expected.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ allow_dirty: false
22
commit: true
33
commit_args: null
44
current_version: "1.0.0"
5+
excluded_paths:
6+
57
files:
68
- filename: "setup.py"
79
glob: null
@@ -34,6 +36,8 @@ files:
3436
- "{major}.{minor}.{patch}-{release}"
3537
- "{major}.{minor}.{patch}"
3638
ignore_missing_version: false
39+
included_paths:
40+
3741
message: "Bump version: {current_version} → {new_version}"
3842
no_regex: false
3943
parse: "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?"

tests/fixtures/basic_cfg_expected_full.json

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"commit": true,
44
"commit_args": null,
55
"current_version": "1.0.0",
6+
"excluded_paths": [],
67
"files": [
78
{
89
"filename": "setup.py",
@@ -45,6 +46,7 @@
4546
}
4647
],
4748
"ignore_missing_version": false,
49+
"included_paths": [],
4850
"message": "Bump version: {current_version} \u2192 {new_version}",
4951
"no_regex": false,
5052
"parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",

tests/test_cli.py

+53
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,14 @@ def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path):
201201
"DEPRECATED: The --list option is deprecated and will be removed in a future version.",
202202
"new_version=1.0.1-dev",
203203
"current_version=1.0.0",
204+
"excluded_paths=[]",
204205
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
205206
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
206207
"search={current_version}",
207208
"replace={new_version}",
208209
"no_regex=False",
209210
"ignore_missing_version=False",
211+
"included_paths=[]",
210212
"tag=True",
211213
"sign_tags=False",
212214
"tag_name=v{new_version}",
@@ -254,12 +256,14 @@ def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path):
254256
"",
255257
"DEPRECATED: The --list option is deprecated and will be removed in a future version.",
256258
"current_version=1.0.0",
259+
"excluded_paths=[]",
257260
"parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
258261
"serialize=['{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}']",
259262
"search={current_version}",
260263
"replace={new_version}",
261264
"no_regex=False",
262265
"ignore_missing_version=False",
266+
"included_paths=[]",
263267
"tag=True",
264268
"sign_tags=False",
265269
"tag_name=v{new_version}",
@@ -517,3 +521,52 @@ def test_replace_search_with_plain_string(tmp_path, fixtures_path):
517521
print(traceback.print_exception(result.exc_info[1]))
518522

519523
assert result.exit_code == 0
524+
525+
526+
def test_valid_regex_not_ignoring_regex(tmp_path: Path, caplog) -> None:
527+
"""A search string not meant to be a regex (but is) is still found and replaced correctly."""
528+
# Arrange
529+
search = "(unreleased)"
530+
replace = "(2023-01-01)"
531+
532+
version_path = tmp_path / "VERSION"
533+
version_path.write_text("# Changelog\n\n## [0.0.1 (unreleased)](https://cool.url)\n\n- Test unreleased package.\n")
534+
config_file = tmp_path / ".bumpversion.toml"
535+
config_file.write_text(
536+
"[tool.bumpversion]\n"
537+
'current_version = "0.0.1"\n'
538+
"allow_dirty = true\n\n"
539+
"[[tool.bumpversion.files]]\n"
540+
'filename = "VERSION"\n'
541+
"no_regex = true\n"
542+
f'search = "{search}"\n'
543+
f'replace = "{replace}"\n'
544+
)
545+
546+
# Act
547+
runner: CliRunner = CliRunner()
548+
with inside_dir(tmp_path):
549+
result: Result = runner.invoke(
550+
cli.cli,
551+
[
552+
"replace",
553+
"--verbose",
554+
"--no-regex",
555+
"--no-configured-files",
556+
"--search",
557+
search,
558+
"--replace",
559+
replace,
560+
"VERSION",
561+
],
562+
)
563+
564+
# Assert
565+
if result.exit_code != 0:
566+
print(result.output)
567+
568+
assert result.exit_code == 0
569+
assert (
570+
version_path.read_text()
571+
== "# Changelog\n\n## [0.0.1 (2023-01-01)](https://cool.url)\n\n- Test unreleased package.\n"
572+
)

tests/test_config.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pytest import param
1010

1111
from bumpversion import config
12-
from tests.conftest import inside_dir
12+
from tests.conftest import inside_dir, get_config_data
1313

1414

1515
@pytest.fixture(params=[".bumpversion.cfg", "setup.cfg"])
@@ -246,3 +246,31 @@ def test_pep440_config(git_repo: Path, fixtures_path: Path):
246246
# assert result.exit_code == 0
247247
# cfg = config.get_configuration(cfg_path)
248248
# assert cfg.current_version == "1.0.0.dev1+myreallylongbranchna"
249+
250+
251+
@pytest.mark.parametrize(
252+
["glob_pattern", "file_list"],
253+
[
254+
param("*.txt", {Path("file1.txt"), Path("file2.txt")}, id="simple-glob"),
255+
param("**/*.txt", {Path("file1.txt"), Path("file2.txt"), Path("directory/file3.txt")}, id="recursive-glob"),
256+
],
257+
)
258+
def test_get_glob_files(glob_pattern: str, file_list: set, fixtures_path: Path):
259+
"""Get glob files should return all the globbed files and nothing else."""
260+
overrides = {
261+
"current_version": "1.0.0",
262+
"parse": r"(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<release>[a-z]+))?",
263+
"serialize": ["{major}.{minor}.{release}", "{major}.{minor}"],
264+
"files": [
265+
{
266+
"glob": glob_pattern,
267+
}
268+
],
269+
}
270+
conf, version_config, current_version = get_config_data(overrides)
271+
with inside_dir(fixtures_path.joinpath("glob")):
272+
result = config.get_glob_files(conf.files[0])
273+
274+
assert len(result) == len(file_list)
275+
for f in result:
276+
assert Path(f.filename) in file_list

0 commit comments

Comments
 (0)