Skip to content

Commit 1c391be

Browse files
committed
Fixed exclusion logic with wcmatch
1 parent bbf4ae0 commit 1c391be

File tree

2 files changed

+48
-64
lines changed

2 files changed

+48
-64
lines changed

bumpversion/config/utils.py

+2-20
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from __future__ import annotations
44

5-
import fnmatch
65
import re
7-
from typing import Dict, List, Pattern
6+
from typing import Dict, List
87

98
from wcmatch import glob
109

@@ -32,8 +31,6 @@ def get_all_file_configs(config_dict: dict) -> List[FileChange]:
3231

3332
def get_all_part_configs(config_dict: dict) -> Dict[str, VersionComponentSpec]:
3433
"""Make sure all version parts are included."""
35-
import re
36-
3734
try:
3835
parsing_groups = list(re.compile(config_dict["parse"]).groupindex.keys())
3936
except re.error as e:
@@ -51,21 +48,6 @@ def get_all_part_configs(config_dict: dict) -> Dict[str, VersionComponentSpec]:
5148
return part_configs
5249

5350

54-
def glob_exclude_pattern(glob_excludes: List[str]) -> Pattern:
55-
"""Convert a list of glob patterns to a regular expression that matches excluded files."""
56-
glob_excludes = glob_excludes or []
57-
patterns = []
58-
59-
for pat in glob_excludes:
60-
if not pat:
61-
continue
62-
elif pat.endswith("/"):
63-
patterns.append(f"{pat}**") # assume they mean exclude every file in that directory
64-
else:
65-
patterns.append(pat)
66-
return re.compile("|".join([fnmatch.translate(pat) for pat in patterns])) if patterns else re.compile(r"^$")
67-
68-
6951
def resolve_glob_files(file_cfg: FileChange) -> List[FileChange]:
7052
"""
7153
Return a list of file configurations that match the glob pattern.
@@ -78,7 +60,7 @@ def resolve_glob_files(file_cfg: FileChange) -> List[FileChange]:
7860
"""
7961
files: List[FileChange] = []
8062
exclude = file_cfg.glob_exclude or []
81-
glob_flags = glob.GLOBSTAR | glob.FORCEUNIX
63+
glob_flags = glob.GLOBSTAR | glob.FORCEUNIX | glob.SPLIT
8264
for filename_glob in glob.glob(file_cfg.glob, flags=glob_flags, exclude=exclude):
8365
new_file_cfg = file_cfg.model_copy()
8466
new_file_cfg.filename = filename_glob

tests/test_config/test_utils.py

+46-44
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from pytest import param
99

10-
from bumpversion.config.utils import get_all_file_configs, resolve_glob_files, glob_exclude_pattern
10+
from bumpversion.config.utils import get_all_file_configs, resolve_glob_files
1111
from bumpversion.config.models import FileChange
1212
from bumpversion.config import DEFAULTS
1313
from tests.conftest import inside_dir
@@ -114,7 +114,18 @@ def test_all_attributes_are_copied(self, tmp_path: Path):
114114
assert resolved_file.ignore_missing_file is True
115115
assert resolved_file.regex is True
116116

117-
def test_excludes_configured_patterns(self, tmp_path: Path):
117+
@pytest.mark.parametrize(
118+
["glob_exclude", "expected_number"],
119+
[
120+
param(["**/*.cfg"], 0, id="recursive cfg excludes all"),
121+
param(["*.cfg"], 1, id="top level cfg excludes 1"),
122+
param(["subdir/*.cfg"], 1, id="subdir cfg excludes 1"),
123+
param(["subdir/adir/*.cfg"], 2, id="non-matching excludes 0"),
124+
param(["subdir/"], 2, id="raw subdir excludes 0"),
125+
param(["*.cfg", "subdir/*.cfg"], 0, id="combined top level and subdir cfg excludes all"),
126+
],
127+
)
128+
def test_excludes_configured_patterns(self, tmp_path: Path, glob_exclude: List[str], expected_number: int):
118129
"""Test that excludes configured patterns work."""
119130
file1 = tmp_path.joinpath("setup.cfg")
120131
file2 = tmp_path.joinpath("subdir/setup.cfg")
@@ -125,7 +136,7 @@ def test_excludes_configured_patterns(self, tmp_path: Path):
125136
file_cfg = FileChange(
126137
filename=None,
127138
glob="**/*.cfg",
128-
glob_exclude=["subdir/**"],
139+
glob_exclude=glob_exclude,
129140
key_path=None,
130141
parse=r"v(?P<major>\d+)",
131142
serialize=("v{major}",),
@@ -138,50 +149,41 @@ def test_excludes_configured_patterns(self, tmp_path: Path):
138149
with inside_dir(tmp_path):
139150
resolved_files = resolve_glob_files(file_cfg)
140151

141-
assert len(resolved_files) == 1
142-
143-
144-
class TestGlobExcludePattern:
145-
"""Tests for the glob_exclude_pattern function."""
146-
147-
@pytest.mark.parametrize(["empty_pattern"], [param([], id="empty list"), param(None, id="None")])
148-
def test_empty_list_returns_empty_string_pattern(self, empty_pattern: Any):
149-
"""When passed an empty list, it should return a pattern that only matches an empty string."""
150-
assert glob_exclude_pattern(empty_pattern).pattern == r"^$"
152+
assert len(resolved_files) == expected_number
151153

152154
@pytest.mark.parametrize(
153-
["patterns", "expected"],
155+
["glob_pattern", "expected_number"],
154156
[
155-
param(["foo.txt", ""], "(?s:foo\\.txt)\\Z", id="empty string"),
156-
param(["foo.txt", None], "(?s:foo\\.txt)\\Z", id="None value"),
157-
param(["foo.txt", "", "bar.txt"], "(?s:foo\\.txt)\\Z|(?s:bar\\.txt)\\Z", id="Empty string in the middle"),
157+
param("**/*.cfg|*.txt", 3, id="recursive cfg and text finds 3"),
158+
param("*.cfg|*.txt", 2, id="top level cfg and txt finds 2"),
159+
param("subdir/adir/*.cfg|*.toml", 0, id="non-matching finds 0"),
160+
param("subdir/|*.txt", 2, id="raw subdir and text finds 2"),
158161
],
159162
)
160-
def test_empty_values_are_excluded(self, patterns: List, expected: str):
161-
"""Empty values are excluded from the compiled pattern."""
162-
assert glob_exclude_pattern(patterns).pattern == expected
163-
164-
def test_list_of_empty_patterns_return_empty_string_pattern(self):
165-
"""When passed a list of empty strings, it should return a pattern that only matches an empty string."""
166-
assert glob_exclude_pattern(["", "", None]).pattern == r"^$"
167-
168-
def test_trailing_slash_appends_stars(self):
169-
"""
170-
When a string has a trailing slash, two asterisks are appended.
171-
172-
`fnmatch.translate` converts `**` to `.*`
173-
"""
174-
assert glob_exclude_pattern(["foo/"]).pattern == "(?s:foo/.*)\\Z"
163+
def test_combination_glob_patterns(self, tmp_path: Path, glob_pattern: str, expected_number: int):
164+
"""Test that excludes configured patterns work."""
165+
file1 = tmp_path.joinpath("setup.cfg")
166+
file2 = tmp_path.joinpath("subdir/setup.cfg")
167+
file3 = tmp_path.joinpath("config.txt")
168+
file1.touch()
169+
file2.parent.mkdir()
170+
file2.touch()
171+
file3.touch()
175172

176-
@pytest.mark.parametrize(
177-
["file_path", "excluded"],
178-
[param("node_modules/foo/file.js", True), param("build/foo/file.js", True), param("code/file.js", False)],
179-
)
180-
def test_output_pattern_matches_files(self, file_path: str, excluded: bool):
181-
"""The output pattern should match file paths appropriately."""
182-
exclude_matcher = glob_exclude_pattern(["node_modules/", "build/"])
183-
184-
if excluded:
185-
assert exclude_matcher.match(file_path)
186-
else:
187-
assert not exclude_matcher.match(file_path)
173+
file_cfg = FileChange(
174+
filename=None,
175+
glob=glob_pattern,
176+
glob_exclude=[],
177+
key_path=None,
178+
parse=r"v(?P<major>\d+)",
179+
serialize=("v{major}",),
180+
search="v{current_version}",
181+
replace="v{new_version}",
182+
ignore_missing_version=True,
183+
ignore_missing_file=True,
184+
regex=True,
185+
)
186+
with inside_dir(tmp_path):
187+
resolved_files = resolve_glob_files(file_cfg)
188+
print([x.filename for x in resolved_files])
189+
assert len(resolved_files) == expected_number

0 commit comments

Comments
 (0)