filefilter 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {filefilter-0.2.1/src/filefilter.egg-info → filefilter-0.2.2}/PKG-INFO +4 -1
- {filefilter-0.2.1 → filefilter-0.2.2}/README.md +3 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/pyproject.toml +1 -1
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter/__init__.py +1 -1
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter/collector.py +1 -1
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter/utilities.py +16 -1
- {filefilter-0.2.1 → filefilter-0.2.2/src/filefilter.egg-info}/PKG-INFO +4 -1
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter.egg-info/SOURCES.txt +1 -0
- filefilter-0.2.2/tests/test_comprehensive.py +560 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/tests/test_matcher.py +55 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/tests/test_utilities.py +40 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/LICENSE +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/setup.cfg +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter/ruleset.py +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter.egg-info/dependency_links.txt +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter.egg-info/requires.txt +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/src/filefilter.egg-info/top_level.txt +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/tests/test_inclusion_logic.py +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/tests/test_readme_verification.py +0 -0
- {filefilter-0.2.1 → filefilter-0.2.2}/tests/test_ruleset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: filefilter
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Filter files in a directory tree based on configurable glob rules.
|
|
5
5
|
Author-email: "Ioannis D (devcoons)" <support@devcoons.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -229,9 +229,12 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
229
229
|
|:-------|:--------|
|
|
230
230
|
| `*` | one or more characters (no slashes) |
|
|
231
231
|
| `**` | zero or more characters (can cross dirs) |
|
|
232
|
+
| `*ABC` / `ABC*` | within a **directory name**, `*` = 1+ chars (e.g. `Myhello` matches `*hello`, not bare `hello`) |
|
|
233
|
+
| `**ABC` / `ABC**` | within a **directory name**, `**` = 0+ chars (e.g. `hello` and `Myhello` both match `**hello`) |
|
|
232
234
|
| Leading `**/` | match anywhere in the tree |
|
|
233
235
|
| Leading `*/` | match exactly N levels below root |
|
|
234
236
|
| Trailing `/**` | match directory and all descendants |
|
|
237
|
+
| `***` (3+ asterisks) | literal directory name, not a wildcard |
|
|
235
238
|
| `.ext` or `ext` | file extension match (case-insensitive) |
|
|
236
239
|
| `.*` in extensions | any non-empty extension |
|
|
237
240
|
|
|
@@ -196,9 +196,12 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
196
196
|
|:-------|:--------|
|
|
197
197
|
| `*` | one or more characters (no slashes) |
|
|
198
198
|
| `**` | zero or more characters (can cross dirs) |
|
|
199
|
+
| `*ABC` / `ABC*` | within a **directory name**, `*` = 1+ chars (e.g. `Myhello` matches `*hello`, not bare `hello`) |
|
|
200
|
+
| `**ABC` / `ABC**` | within a **directory name**, `**` = 0+ chars (e.g. `hello` and `Myhello` both match `**hello`) |
|
|
199
201
|
| Leading `**/` | match anywhere in the tree |
|
|
200
202
|
| Leading `*/` | match exactly N levels below root |
|
|
201
203
|
| Trailing `/**` | match directory and all descendants |
|
|
204
|
+
| `***` (3+ asterisks) | literal directory name, not a wildcard |
|
|
202
205
|
| `.ext` or `ext` | file extension match (case-insensitive) |
|
|
203
206
|
| `.*` in extensions | any non-empty extension |
|
|
204
207
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "filefilter"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Filter files in a directory tree based on configurable glob rules."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Ioannis D (devcoons)", email = "support@devcoons.com" }]
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# #
|
|
25
25
|
#########################################################################################
|
|
26
26
|
|
|
27
|
-
__version__ = '0.2.
|
|
27
|
+
__version__ = '0.2.2'
|
|
28
28
|
|
|
29
29
|
#########################################################################################
|
|
30
30
|
# IMPORTS #
|
|
@@ -127,7 +127,7 @@ def match_file(filepath: str, include_files: list[str]) -> bool:
|
|
|
127
127
|
start_positions = range(0, depth + 1)
|
|
128
128
|
for s in start_positions:
|
|
129
129
|
ends = match_doublestar_segments(dir_parts, s, pat_dirs)
|
|
130
|
-
if ends:
|
|
130
|
+
if any(e == depth for e in ends):
|
|
131
131
|
return True
|
|
132
132
|
elif lead_stars == 0:
|
|
133
133
|
s = 0
|
|
@@ -98,6 +98,21 @@ def count_leading_stars(patt: str) -> tuple[int, str]:
|
|
|
98
98
|
|
|
99
99
|
#########################################################################################
|
|
100
100
|
|
|
101
|
+
def segment_matches(token: str, part: str) -> bool:
|
|
102
|
+
"""Match one directory name; * is 1+ chars, ** is 0+ chars within the segment."""
|
|
103
|
+
token = normalize_path(token)
|
|
104
|
+
part = normalize_path(part)
|
|
105
|
+
if len(token) >= 3 and set(token) == {'*'}:
|
|
106
|
+
return part == token
|
|
107
|
+
if '*' not in token:
|
|
108
|
+
return part == token
|
|
109
|
+
rx = re.escape(token)
|
|
110
|
+
rx = rx.replace(r'\*\*', '.*')
|
|
111
|
+
rx = rx.replace(r'\*', '.+')
|
|
112
|
+
return re.match(r'^' + rx + r'$', part) is not None
|
|
113
|
+
|
|
114
|
+
#########################################################################################
|
|
115
|
+
|
|
101
116
|
def match_doublestar_segments(path_parts: list[str], start: int, patt_parts: list[str]) -> list[int]:
|
|
102
117
|
"""Return a list of end indices j such that path_parts[start:j] matches patt_parts where '**' means zero+ segments."""
|
|
103
118
|
results = []
|
|
@@ -113,7 +128,7 @@ def match_doublestar_segments(path_parts: list[str], start: int, patt_parts: lis
|
|
|
113
128
|
if j < len(path_parts):
|
|
114
129
|
rec(i + 1, j + 1)
|
|
115
130
|
else:
|
|
116
|
-
if j < len(path_parts) and path_parts[j]
|
|
131
|
+
if j < len(path_parts) and segment_matches(token, path_parts[j]):
|
|
117
132
|
rec(i + 1, j + 1)
|
|
118
133
|
rec(0, start)
|
|
119
134
|
return results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: filefilter
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Filter files in a directory tree based on configurable glob rules.
|
|
5
5
|
Author-email: "Ioannis D (devcoons)" <support@devcoons.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -229,9 +229,12 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
229
229
|
|:-------|:--------|
|
|
230
230
|
| `*` | one or more characters (no slashes) |
|
|
231
231
|
| `**` | zero or more characters (can cross dirs) |
|
|
232
|
+
| `*ABC` / `ABC*` | within a **directory name**, `*` = 1+ chars (e.g. `Myhello` matches `*hello`, not bare `hello`) |
|
|
233
|
+
| `**ABC` / `ABC**` | within a **directory name**, `**` = 0+ chars (e.g. `hello` and `Myhello` both match `**hello`) |
|
|
232
234
|
| Leading `**/` | match anywhere in the tree |
|
|
233
235
|
| Leading `*/` | match exactly N levels below root |
|
|
234
236
|
| Trailing `/**` | match directory and all descendants |
|
|
237
|
+
| `***` (3+ asterisks) | literal directory name, not a wildcard |
|
|
235
238
|
| `.ext` or `ext` | file extension match (case-insensitive) |
|
|
236
239
|
| `.*` in extensions | any non-empty extension |
|
|
237
240
|
|
|
@@ -10,6 +10,7 @@ src/filefilter.egg-info/SOURCES.txt
|
|
|
10
10
|
src/filefilter.egg-info/dependency_links.txt
|
|
11
11
|
src/filefilter.egg-info/requires.txt
|
|
12
12
|
src/filefilter.egg-info/top_level.txt
|
|
13
|
+
tests/test_comprehensive.py
|
|
13
14
|
tests/test_inclusion_logic.py
|
|
14
15
|
tests/test_matcher.py
|
|
15
16
|
tests/test_readme_verification.py
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exhaustive behavioural tests beyond line coverage — README tables, segment
|
|
3
|
+
globs, specificity, regressions, and end-to-end selection outcomes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from conftest import make_config, select_paths, touch
|
|
13
|
+
from filefilter import match_dir, match_file, matches
|
|
14
|
+
from filefilter.utilities import segment_matches
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Segment glob matrix
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
SEGMENT_GLOB_CASES = [
|
|
21
|
+
# suffix glob *hello
|
|
22
|
+
("*hello", "myhello", True),
|
|
23
|
+
("*hello", "xhello", True),
|
|
24
|
+
("*hello", "hello", False),
|
|
25
|
+
("*hello", "helloworld", False),
|
|
26
|
+
("*hello", "notmatch", False),
|
|
27
|
+
# suffix glob **hello (0+ prefix)
|
|
28
|
+
("**hello", "hello", True),
|
|
29
|
+
("**hello", "myhello", True),
|
|
30
|
+
("**hello", "xhello", True),
|
|
31
|
+
("**hello", "helloworld", False),
|
|
32
|
+
# prefix glob hello*
|
|
33
|
+
("hello*", "helloworld", True),
|
|
34
|
+
("hello*", "hellox", True),
|
|
35
|
+
("hello*", "hello", False),
|
|
36
|
+
# prefix glob hello**
|
|
37
|
+
("hello**", "hello", True),
|
|
38
|
+
("hello**", "helloworld", True),
|
|
39
|
+
("hello**", "myhello", False),
|
|
40
|
+
# prefix *abc / suffix abc*
|
|
41
|
+
("*abc", "xabc", True),
|
|
42
|
+
("*abc", "myabc", True),
|
|
43
|
+
("*abc", "abc", False),
|
|
44
|
+
("abc*", "abcx", True),
|
|
45
|
+
("abc*", "abc123", True),
|
|
46
|
+
("abc*", "abc", False),
|
|
47
|
+
# infix *abc*
|
|
48
|
+
("*abc*", "xabcy", True),
|
|
49
|
+
("*abc*", "dataabcdata", True),
|
|
50
|
+
("*abc*", "abc", False),
|
|
51
|
+
("*abc*", "ab", False),
|
|
52
|
+
# **abc / abc** (0+ on one side)
|
|
53
|
+
("**abc", "abc", True),
|
|
54
|
+
("**abc", "xabc", True),
|
|
55
|
+
("abc**", "abc", True),
|
|
56
|
+
("abc**", "abcd", True),
|
|
57
|
+
("**abc**", "abc", True),
|
|
58
|
+
("**abc**", "xabcy", True),
|
|
59
|
+
("**abc**", "xab", False),
|
|
60
|
+
# literals
|
|
61
|
+
("folder", "folder", True),
|
|
62
|
+
("folder", "Folder", True),
|
|
63
|
+
("folder", "other", False),
|
|
64
|
+
("***", "***", True),
|
|
65
|
+
("***", "**", False),
|
|
66
|
+
("***", "****", False),
|
|
67
|
+
("***", "myhello", False),
|
|
68
|
+
# mixed literal + glob
|
|
69
|
+
("pre*fix", "preabcfix", True),
|
|
70
|
+
("pre*fix", "prefix", False),
|
|
71
|
+
("pre**fix", "prefix", True),
|
|
72
|
+
("pre**fix", "preabcfix", True),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize("token,part,expected", SEGMENT_GLOB_CASES)
|
|
77
|
+
def test_segment_glob_matrix(token: str, part: str, expected: bool):
|
|
78
|
+
assert segment_matches(token, part) is expected
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# README folder-pattern table (match_dir)
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
README_DIR_PATTERNS = [
|
|
86
|
+
("folder", "folder", True),
|
|
87
|
+
("folder", "folder/sub", False),
|
|
88
|
+
("folder/*", "folder/a", True),
|
|
89
|
+
("folder/*", "folder/a/b", False),
|
|
90
|
+
("folder/*/*", "folder/a/b", True),
|
|
91
|
+
("folder/*/*", "folder/a", False),
|
|
92
|
+
("folder/**", "folder/x/y", True),
|
|
93
|
+
("folder/**/sub", "folder/a/b/sub", True),
|
|
94
|
+
("folder/**/sub", "folder/sub", True),
|
|
95
|
+
("*/folder", "src/folder", True),
|
|
96
|
+
("*/folder", "folder", False),
|
|
97
|
+
("*/folder/**", "src/folder/x/y", True),
|
|
98
|
+
("*/*/folder", "apps/web/folder", True),
|
|
99
|
+
("*/*/folder", "apps/folder", False),
|
|
100
|
+
("*/*/*/folder", "a/b/c/folder", True),
|
|
101
|
+
("*/*/*/*/folder/**", "a/b/c/d/folder/x/y", True),
|
|
102
|
+
("**/folder", "a/b/c/folder", True),
|
|
103
|
+
("**/folder", "folder", True),
|
|
104
|
+
("**/folder/**", "x/y/folder/z", True),
|
|
105
|
+
("**/folder/another", "folder/another", True),
|
|
106
|
+
("**/folder/another", "x/folder/another", True),
|
|
107
|
+
("**/pkg/*", "pkg/a", True),
|
|
108
|
+
("**/pkg/*", "x/y/pkg/z", True),
|
|
109
|
+
("**/pkg/*", "pkg/a/b", False),
|
|
110
|
+
("**/pkg/*/*", "pkg/a/b", True),
|
|
111
|
+
("**/pkg/*/*", "x/y/pkg/z/t", True),
|
|
112
|
+
("**/pkg/*/*", "pkg/a", False),
|
|
113
|
+
("modules/*/test", "modules/auth/test", True),
|
|
114
|
+
("modules/*/test", "modules/auth/pay/test", False),
|
|
115
|
+
("apps/**/dist", "apps/web/dist", True),
|
|
116
|
+
("apps/**/dist", "apps/mobile/a/b/dist", True),
|
|
117
|
+
("apps/**/dist", "other/dist", False),
|
|
118
|
+
("*/*/folder/*/another_folder", "a/b/folder/c/another_folder", True),
|
|
119
|
+
("*/*/folder/*/another_folder", "a/b/folder/c/d/another_folder", False),
|
|
120
|
+
("*/*/folder/**/another_folder", "a/b/folder/another_folder", True),
|
|
121
|
+
("*/*/folder/**/another_folder", "a/b/folder/x/another_folder", True),
|
|
122
|
+
("*/*/folder/*/*/another_folder", "a/b/folder/x/y/another_folder", True),
|
|
123
|
+
("*/*/folder/*/*/another_folder", "a/b/folder/x/another_folder", False),
|
|
124
|
+
("**/services/**/migrations/*", "services/migrations/2", True),
|
|
125
|
+
("**/services/**/migrations/*", "x/services/a/migrations/001", True),
|
|
126
|
+
("**/services/**/migrations/*", "services/m/2", False),
|
|
127
|
+
("**/features/*/*", "a/features/x/y", True),
|
|
128
|
+
("**/features/*/*", "features/p/q", True),
|
|
129
|
+
("**/features/*/*", "features/p/q/r", False),
|
|
130
|
+
("packages/*/**/dist", "packages/core/dist", True),
|
|
131
|
+
("packages/*/**/dist", "packages/ui/x/y/dist", True),
|
|
132
|
+
("packages/*/**/dist", "packages/dist", False),
|
|
133
|
+
("**/folder/***/anotherfolder", "folder/***/anotherfolder", True),
|
|
134
|
+
("**/folder/***/anotherfolder", "folder/x/anotherfolder", False),
|
|
135
|
+
# segment globs in dir patterns
|
|
136
|
+
("**/*hello/**", "abc/myhello/efg", True),
|
|
137
|
+
("**/*hello/**", "abc/hello/efg", False),
|
|
138
|
+
("**/**hello/**", "abc/hello/efg", True),
|
|
139
|
+
("**/**hello/**", "abc/myhello/efg", True),
|
|
140
|
+
("**/hello*/**", "abc/helloworld/x", True),
|
|
141
|
+
("**/hello*/**", "abc/hello/x", False),
|
|
142
|
+
("**/hello**/**", "abc/hello/x", True),
|
|
143
|
+
("**/*abc/**", "data/myabc/nested", True),
|
|
144
|
+
("**/abc*/**", "data/abc123/nested", True),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize("pattern,dirpath,expected", README_DIR_PATTERNS)
|
|
149
|
+
def test_readme_folder_pattern_table(pattern: str, dirpath: str, expected: bool):
|
|
150
|
+
assert match_dir(dirpath, [pattern]) is expected
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# README file-pattern table (match_file)
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
README_FILE_PATTERNS = [
|
|
158
|
+
("file.py", "file.py", True),
|
|
159
|
+
("file.py", "src/file.py", False),
|
|
160
|
+
("README.*", "README.md", True),
|
|
161
|
+
("README.*", "README.rst", True),
|
|
162
|
+
("README.*", "README", False),
|
|
163
|
+
("README.**", "README", True),
|
|
164
|
+
("README.**", "README.md", True),
|
|
165
|
+
(".*", ".env", True),
|
|
166
|
+
(".*", ".gitignore", True),
|
|
167
|
+
(".*", "env", False),
|
|
168
|
+
("folder/*.py", "folder/a.py", True),
|
|
169
|
+
("folder/*.py", "folder/sub/a.py", False),
|
|
170
|
+
("folder/**/test_*.py", "folder/test_a.py", True),
|
|
171
|
+
("folder/**/test_*.py", "folder/x/y/test_utils.py", True),
|
|
172
|
+
("Makefile", "Makefile", True),
|
|
173
|
+
("Makefile", "src/Makefile", False),
|
|
174
|
+
("*.toml", "pyproject.toml", True),
|
|
175
|
+
("*.toml", "pkg/a/config.toml", False),
|
|
176
|
+
("**/*.toml", "pyproject.toml", True),
|
|
177
|
+
("**/*.toml", "pkg/a/b/config.toml", True),
|
|
178
|
+
("folder/**/*.md", "folder/README.md", True),
|
|
179
|
+
("folder/**/*.md", "folder/docs/a/b/guide.md", True),
|
|
180
|
+
("*/Dockerfile", "api/Dockerfile", True),
|
|
181
|
+
("*/Dockerfile", "Dockerfile", False),
|
|
182
|
+
("*/LICENSE*", "pkg/LICENSE-MIT", True),
|
|
183
|
+
("*/LICENSE*", "pkg/LICENSE", False),
|
|
184
|
+
("*/*/Makefile", "x/y/Makefile", True),
|
|
185
|
+
("*/*/Makefile", "x/Makefile", False),
|
|
186
|
+
("*/*/*/package.json", "a/b/c/package.json", True),
|
|
187
|
+
("**/Makefile", "Makefile", True),
|
|
188
|
+
("**/Makefile", "a/b/c/Makefile", True),
|
|
189
|
+
("**/*.py", "a.py", True),
|
|
190
|
+
("**/*.py", "pkg/mod/x.py", True),
|
|
191
|
+
("**", "any/depth/file.dat", True),
|
|
192
|
+
("src/**/test_*.py", "src/test_a.py", True),
|
|
193
|
+
("src/**/test_*.py", "src/unit/core/test_utils.py", True),
|
|
194
|
+
("**/migrations/*", "app/migrations/001.sql", True),
|
|
195
|
+
("**/migrations/*", "app/migrations/v1/001.sql", False),
|
|
196
|
+
("**/migrations/*/*", "app/migrations/v1/001.sql", True),
|
|
197
|
+
("**/assets/**/*.png", "assets/logo.png", True),
|
|
198
|
+
("**/assets/**/*.png", "x/assets/img/icons/a.png", True),
|
|
199
|
+
("packages/*/**/dist/*.js", "packages/core/dist/index.js", True),
|
|
200
|
+
("packages/*/**/dist/*.js", "packages/ui/x/y/dist/app.js", True),
|
|
201
|
+
("**/pkg/*/*/index.*", "pkg/a/b/index.js", True),
|
|
202
|
+
("**/pkg/*/*/index.*", "x/y/pkg/z/t/index.html", True),
|
|
203
|
+
("**/file*.py", "fileA.py", True),
|
|
204
|
+
("**/file*.py", "file.py", False),
|
|
205
|
+
("**/file**.py", "file.py", True),
|
|
206
|
+
("**/file**.py", "x/fileA.py", True),
|
|
207
|
+
("**/*test*.py", "x/y/unittest_tools.py", True),
|
|
208
|
+
("**/*test*.py", "test_main.py", False),
|
|
209
|
+
("*/*/folder/*/another.py", "a/b/folder/c/another.py", True),
|
|
210
|
+
("*/*/folder/*/another.py", "a/b/folder/c/d/another.py", False),
|
|
211
|
+
("*/*/folder/**/another.py", "a/b/folder/another.py", True),
|
|
212
|
+
("*/*/folder/**/another.py", "a/b/folder/x/y/another.py", True),
|
|
213
|
+
("*/*/folder/*/*/another.py", "a/b/folder/x/y/another.py", True),
|
|
214
|
+
("**/folder/***/file.txt", "folder/***/file.txt", True),
|
|
215
|
+
("**/folder/***/file.txt", "folder/x/file.txt", False),
|
|
216
|
+
("folder/*/file.txt", "folder/a/file.txt", True),
|
|
217
|
+
("folder/*/file.txt", "folder/a/b/file.txt", False),
|
|
218
|
+
("folder/**/file.txt", "folder/file.txt", True),
|
|
219
|
+
("folder/**/file.txt", "folder/a/b/file.txt", True),
|
|
220
|
+
# file patterns with segment globs in directory part
|
|
221
|
+
("**/*hello/**/target.c", "abc/myhello/target.c", True),
|
|
222
|
+
("**/*hello/**/target.c", "abc/hello/target.c", False),
|
|
223
|
+
("**/**hello/**/target.c", "abc/hello/target.c", True),
|
|
224
|
+
("**/**hello/**/target.c", "abc/myhello/target.c", True),
|
|
225
|
+
("**/pre*fix/**/out.dat", "abc/preXYZfix/out.dat", True),
|
|
226
|
+
("**/pre*fix/**/out.dat", "abc/prefix/out.dat", False),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.mark.parametrize("pattern,filepath,expected", README_FILE_PATTERNS)
|
|
231
|
+
def test_readme_file_pattern_table(pattern: str, filepath: str, expected: bool):
|
|
232
|
+
assert match_file(filepath, [pattern]) is expected
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Regressions (bugs found during development)
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
REGRESSION_FILE_CASES = [
|
|
240
|
+
("file.txt", ["file.txt"], True),
|
|
241
|
+
("x/file.txt", ["file.txt"], False),
|
|
242
|
+
("main.py", ["*.py"], True),
|
|
243
|
+
("src/app.py", ["*.py"], False),
|
|
244
|
+
("any/deep/file.dat", ["**"], True),
|
|
245
|
+
("README", ["README.**"], True),
|
|
246
|
+
("pkg/LICENSE", ["**/LICENSE**"], True),
|
|
247
|
+
# **/migrations/* must match only one level under migrations (not deeper)
|
|
248
|
+
("app/migrations/001.sql", ["**/migrations/*"], True),
|
|
249
|
+
("app/migrations/v1/001.sql", ["**/migrations/*"], False),
|
|
250
|
+
("app/migrations/v1/001.sql", ["**/migrations/*/*"], True),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.parametrize("path,patterns,expected", REGRESSION_FILE_CASES)
|
|
255
|
+
def test_regression_file_matching(path: str, patterns: list[str], expected: bool):
|
|
256
|
+
assert match_file(path, patterns) is expected
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Specificity / precedence integration
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
SPECIFICITY_SCENARIOS = [
|
|
264
|
+
pytest.param(
|
|
265
|
+
["**", "**/KLM/ABC/**"],
|
|
266
|
+
["**/KLM/**"],
|
|
267
|
+
{
|
|
268
|
+
"src/foo.py": True,
|
|
269
|
+
"src/KLM/skip.py": False,
|
|
270
|
+
"src/KLM/ABC/keep.py": True,
|
|
271
|
+
"src/x/KLM/ABC/deep/keep.py": True,
|
|
272
|
+
},
|
|
273
|
+
id="klm-abc-exception",
|
|
274
|
+
),
|
|
275
|
+
pytest.param(
|
|
276
|
+
["src/**", "**/ABC/KLM/**"],
|
|
277
|
+
["**/ABC/**"],
|
|
278
|
+
{
|
|
279
|
+
"src/foo.py": True,
|
|
280
|
+
"src/ABC/only.py": False,
|
|
281
|
+
"src/ABC/other/skip.py": False,
|
|
282
|
+
"src/ABC/KLM/keep.py": True,
|
|
283
|
+
"src/x/ABC/KLM/keep.py": True,
|
|
284
|
+
},
|
|
285
|
+
id="abc-klm-exception",
|
|
286
|
+
),
|
|
287
|
+
pytest.param(
|
|
288
|
+
["**"],
|
|
289
|
+
["**/__pycache__/**"],
|
|
290
|
+
{
|
|
291
|
+
"src/app.py": True,
|
|
292
|
+
"src/__pycache__/cached.py": False,
|
|
293
|
+
},
|
|
294
|
+
id="pycache-broad-exclude-wins",
|
|
295
|
+
),
|
|
296
|
+
pytest.param(
|
|
297
|
+
["**", "**/keep/**"],
|
|
298
|
+
["**/skip/**"],
|
|
299
|
+
{
|
|
300
|
+
"skip/only.py": False,
|
|
301
|
+
"keep/only.py": True,
|
|
302
|
+
"other/only.py": True,
|
|
303
|
+
},
|
|
304
|
+
id="unrelated-exclude-does-not-block",
|
|
305
|
+
),
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@pytest.mark.parametrize("inc_dirs,exc_dirs,files", SPECIFICITY_SCENARIOS)
|
|
310
|
+
def test_specificity_integration(
|
|
311
|
+
tmp_path: Path,
|
|
312
|
+
inc_dirs: list[str],
|
|
313
|
+
exc_dirs: list[str],
|
|
314
|
+
files: dict[str, bool],
|
|
315
|
+
):
|
|
316
|
+
for rel in files:
|
|
317
|
+
touch(tmp_path / rel)
|
|
318
|
+
cfg = make_config(
|
|
319
|
+
include_dirs=inc_dirs,
|
|
320
|
+
include_extensions=["py"],
|
|
321
|
+
exclude_dirs=exc_dirs,
|
|
322
|
+
)
|
|
323
|
+
got = select_paths(tmp_path, cfg)
|
|
324
|
+
for rel, should_include in files.items():
|
|
325
|
+
assert (str(tmp_path / rel) in got) is should_include, rel
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Segment-glob exclude integration (end-to-end)
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
SEGMENT_EXCLUDE_SCENARIOS = [
|
|
333
|
+
pytest.param(
|
|
334
|
+
["**/*hello/**"],
|
|
335
|
+
{
|
|
336
|
+
"ok/file.c": True,
|
|
337
|
+
"ABC/Myhello/EFG/skip.c": False,
|
|
338
|
+
"ABC/xhello/skip.c": False,
|
|
339
|
+
"ABC/hello/EFG/keep.c": True,
|
|
340
|
+
},
|
|
341
|
+
id="star-hello-suffix",
|
|
342
|
+
),
|
|
343
|
+
pytest.param(
|
|
344
|
+
["**/**hello/**"],
|
|
345
|
+
{
|
|
346
|
+
"ok/file.c": True,
|
|
347
|
+
"ABC/hello/EFG/skip.c": False,
|
|
348
|
+
"ABC/Myhello/EFG/skip.c": False,
|
|
349
|
+
"ABC/other/keep.c": True,
|
|
350
|
+
},
|
|
351
|
+
id="doublestar-hello-suffix",
|
|
352
|
+
),
|
|
353
|
+
pytest.param(
|
|
354
|
+
["**/hello*/**"],
|
|
355
|
+
{
|
|
356
|
+
"ABC/helloworld/x/skip.c": False,
|
|
357
|
+
"ABC/hello/x/keep.c": True,
|
|
358
|
+
"ABC/myhello/x/keep.c": True,
|
|
359
|
+
},
|
|
360
|
+
id="hello-prefix-glob",
|
|
361
|
+
),
|
|
362
|
+
pytest.param(
|
|
363
|
+
["**/*abc*/**"],
|
|
364
|
+
{
|
|
365
|
+
"data/xabcy/nested/skip.c": False,
|
|
366
|
+
"data/abc/keep.c": True,
|
|
367
|
+
"data/ab/keep.c": True,
|
|
368
|
+
},
|
|
369
|
+
id="infix-abc-glob",
|
|
370
|
+
),
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@pytest.mark.parametrize("exclude_dirs,files", SEGMENT_EXCLUDE_SCENARIOS)
|
|
375
|
+
def test_segment_glob_exclude_integration(
|
|
376
|
+
tmp_path: Path,
|
|
377
|
+
exclude_dirs: list[str],
|
|
378
|
+
files: dict[str, bool],
|
|
379
|
+
):
|
|
380
|
+
for rel in files:
|
|
381
|
+
touch(tmp_path / rel)
|
|
382
|
+
cfg = make_config(
|
|
383
|
+
include_dirs=["**"],
|
|
384
|
+
include_extensions=["c"],
|
|
385
|
+
exclude_dirs=exclude_dirs,
|
|
386
|
+
)
|
|
387
|
+
got = select_paths(tmp_path, cfg)
|
|
388
|
+
for rel, should_include in files.items():
|
|
389
|
+
assert (str(tmp_path / rel) in got) is should_include, rel
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# Multi-pattern OR semantics
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def test_multiple_include_file_patterns_or(tmp_path: Path):
|
|
397
|
+
touch(tmp_path / "a.py")
|
|
398
|
+
touch(tmp_path / "b.js")
|
|
399
|
+
touch(tmp_path / "c.txt")
|
|
400
|
+
cfg = make_config(include_files=["**/*.py", "**/*.js"])
|
|
401
|
+
paths = select_paths(tmp_path, cfg)
|
|
402
|
+
assert str(tmp_path / "a.py") in paths
|
|
403
|
+
assert str(tmp_path / "b.js") in paths
|
|
404
|
+
assert str(tmp_path / "c.txt") not in paths
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_multiple_exclude_dir_patterns_or(tmp_path: Path):
|
|
408
|
+
touch(tmp_path / "ok/file.c")
|
|
409
|
+
touch(tmp_path / "build/out.c")
|
|
410
|
+
touch(tmp_path / "tmp/out.c")
|
|
411
|
+
touch(tmp_path / "src/out.c")
|
|
412
|
+
cfg = make_config(
|
|
413
|
+
include_dirs=["**"],
|
|
414
|
+
include_extensions=["c"],
|
|
415
|
+
exclude_dirs=["**/build/**", "**/tmp/**"],
|
|
416
|
+
)
|
|
417
|
+
paths = select_paths(tmp_path, cfg)
|
|
418
|
+
assert str(tmp_path / "ok/file.c") in paths
|
|
419
|
+
assert str(tmp_path / "src/out.c") in paths
|
|
420
|
+
assert str(tmp_path / "build/out.c") not in paths
|
|
421
|
+
assert str(tmp_path / "tmp/out.c") not in paths
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Extension + path filter combinations
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
EXTENSION_COMBO_CASES = [
|
|
429
|
+
pytest.param(["py"], [], "main.py", True, id="ext-match"),
|
|
430
|
+
pytest.param(["py"], [], "main.md", False, id="ext-mismatch"),
|
|
431
|
+
pytest.param(["tar.gz"], [], "archive.tar.gz", True, id="compound-ext"),
|
|
432
|
+
pytest.param(["tar.gz"], [], "archive.tar", False, id="compound-ext-partial"),
|
|
433
|
+
pytest.param([".*"], [], "dotted.txt", True, id="dot-star"),
|
|
434
|
+
pytest.param([".*"], [], "noext", False, id="dot-star-no-ext"),
|
|
435
|
+
pytest.param([], [".log"], "app.log", False, id="exclude-ext"),
|
|
436
|
+
pytest.param([], [".log"], "app.py", True, id="exclude-ext-other"),
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@pytest.mark.parametrize(
|
|
441
|
+
"inc_ext,exc_ext,rel,expected", EXTENSION_COMBO_CASES
|
|
442
|
+
)
|
|
443
|
+
def test_extension_combinations(
|
|
444
|
+
tmp_path: Path,
|
|
445
|
+
inc_ext: list[str],
|
|
446
|
+
exc_ext: list[str],
|
|
447
|
+
rel: str,
|
|
448
|
+
expected: bool,
|
|
449
|
+
):
|
|
450
|
+
touch(tmp_path / rel)
|
|
451
|
+
cfg = make_config(
|
|
452
|
+
include_extensions=inc_ext or None,
|
|
453
|
+
exclude_extensions=exc_ext or None,
|
|
454
|
+
)
|
|
455
|
+
paths = select_paths(tmp_path, cfg)
|
|
456
|
+
assert (str(tmp_path / rel) in paths) is expected
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ---------------------------------------------------------------------------
|
|
460
|
+
# Case insensitivity across path, pattern, and extensions
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
@pytest.mark.parametrize(
|
|
464
|
+
"pattern,dirpath",
|
|
465
|
+
[
|
|
466
|
+
("**/myfolder/**", "abc/myfolder/sub"),
|
|
467
|
+
("**/*hello/**", "abc/xhello/efg"),
|
|
468
|
+
("folder/**/sub", "folder/a/sub"),
|
|
469
|
+
],
|
|
470
|
+
)
|
|
471
|
+
def test_case_insensitivity_dir_patterns(pattern: str, dirpath: str):
|
|
472
|
+
assert match_dir(dirpath, [pattern.upper()]) is True
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def test_case_insensitivity_file_pattern():
|
|
476
|
+
assert match_file("src/MODULE.PY", ["**/*.PY"]) is True
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_case_insensitive_full_pipeline(tree: Path):
|
|
480
|
+
touch(tree / "SRC" / "MODULE.PY")
|
|
481
|
+
cfg = make_config(include_extensions=["PY"], include_dirs=["**/src/**"])
|
|
482
|
+
paths = select_paths(tree, cfg)
|
|
483
|
+
assert str(tree / "SRC" / "MODULE.PY") in paths
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# Depth / anchoring edge cases
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
DEPTH_CASES = [
|
|
491
|
+
("folder", "folder", True),
|
|
492
|
+
("folder", "a/folder", False),
|
|
493
|
+
("folder/*", "folder/a", True),
|
|
494
|
+
("folder/*", "a/folder/x", False),
|
|
495
|
+
("folder/*/*", "folder/a/b", True),
|
|
496
|
+
("folder/*/*", "folder/a", False),
|
|
497
|
+
("*/*/x", "a/b/x", True),
|
|
498
|
+
("*/*/x", "a/x", False),
|
|
499
|
+
("*/*/*/x", "a/b/c/x", True),
|
|
500
|
+
("*/*/*/x", "a/b/x", False),
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@pytest.mark.parametrize("pattern,dirpath,expected", DEPTH_CASES)
|
|
505
|
+
def test_dir_depth_anchoring(pattern: str, dirpath: str, expected: bool):
|
|
506
|
+
assert match_dir(dirpath, [pattern]) is expected
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
# matches() API consistency with select()
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
def test_matches_agrees_with_select(tmp_path: Path):
|
|
514
|
+
from conftest import rules_for
|
|
515
|
+
import json
|
|
516
|
+
from filefilter import load
|
|
517
|
+
|
|
518
|
+
files = {
|
|
519
|
+
"a.py": True,
|
|
520
|
+
"build/a.py": False,
|
|
521
|
+
"src/b.py": True,
|
|
522
|
+
}
|
|
523
|
+
for rel in files:
|
|
524
|
+
touch(tmp_path / rel)
|
|
525
|
+
cfg = make_config(
|
|
526
|
+
include_dirs=["src/**", "*.py"],
|
|
527
|
+
include_extensions=["py"],
|
|
528
|
+
exclude_dirs=["build"],
|
|
529
|
+
)
|
|
530
|
+
rules = rules_for(tmp_path, cfg)
|
|
531
|
+
selected = select_paths(tmp_path, cfg)
|
|
532
|
+
for rel, _ in files.items():
|
|
533
|
+
full = str(tmp_path / rel)
|
|
534
|
+
assert matches(full, rules) == (full in selected or str(tmp_path / rel) in selected)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ---------------------------------------------------------------------------
|
|
538
|
+
# Empty / degenerate inputs
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
@pytest.mark.parametrize(
|
|
542
|
+
"pattern,path",
|
|
543
|
+
[
|
|
544
|
+
("", "a/b"),
|
|
545
|
+
("**", ""),
|
|
546
|
+
],
|
|
547
|
+
)
|
|
548
|
+
def test_degenerate_dir_patterns_do_not_crash(pattern: str, path: str):
|
|
549
|
+
assert match_dir(path, [pattern]) in (True, False)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@pytest.mark.parametrize(
|
|
553
|
+
"pattern,path",
|
|
554
|
+
[
|
|
555
|
+
("", "file.txt"),
|
|
556
|
+
("**", "a/b/c"),
|
|
557
|
+
],
|
|
558
|
+
)
|
|
559
|
+
def test_degenerate_file_patterns_do_not_crash(pattern: str, path: str):
|
|
560
|
+
assert match_file(path, [pattern]) in (True, False)
|
|
@@ -106,12 +106,67 @@ def test_dir_root_one_any_semantics(tree: Path):
|
|
|
106
106
|
("folder/**/another", "folder/another", True),
|
|
107
107
|
("folder/***/another", "folder/***/another", True),
|
|
108
108
|
("folder/***/another", "folder/x/another", False),
|
|
109
|
+
("**/*hello/**", "abc/myhello/efg", True),
|
|
110
|
+
("**/*hello/**", "abc/hello/efg", False),
|
|
111
|
+
("**/**hello/**", "abc/hello/efg", True),
|
|
112
|
+
("**/**hello/**", "abc/myhello/efg", True),
|
|
113
|
+
("**/*hello/**", "abc/xhello/efg", True),
|
|
114
|
+
("**/hello*/**", "abc/helloworld/x", True),
|
|
115
|
+
("**/hello*/**", "abc/hello/x", False),
|
|
116
|
+
("**/*abc/**", "xyz/dataabc/nested", True),
|
|
117
|
+
("**/abc*/**", "xyz/abc123/nested", True),
|
|
118
|
+
("**/*abc*", "somewhere/myabcfolder", True),
|
|
109
119
|
],
|
|
110
120
|
)
|
|
111
121
|
def test_match_dir_patterns(pattern: str, dirpath: str, expected: bool):
|
|
112
122
|
assert match_dir(dirpath, [pattern]) is expected
|
|
113
123
|
|
|
114
124
|
|
|
125
|
+
def test_exclude_dirs_with_segment_glob(tmp_path: Path):
|
|
126
|
+
touch(tmp_path / "ok" / "file.c")
|
|
127
|
+
touch(tmp_path / "ABC" / "Myhello" / "EFG" / "skip.c")
|
|
128
|
+
touch(tmp_path / "ABC" / "hello" / "EFG" / "skip.c")
|
|
129
|
+
touch(tmp_path / "ABC" / "xhello" / "keep.c")
|
|
130
|
+
|
|
131
|
+
cfg = make_config(
|
|
132
|
+
include_dirs=["**"],
|
|
133
|
+
include_extensions=["c"],
|
|
134
|
+
exclude_dirs=["**/*hello/**"],
|
|
135
|
+
)
|
|
136
|
+
paths = select_paths(tmp_path, cfg)
|
|
137
|
+
assert str(tmp_path / "ok" / "file.c") in paths
|
|
138
|
+
assert str(tmp_path / "ABC" / "Myhello" / "EFG" / "skip.c") not in paths
|
|
139
|
+
assert str(tmp_path / "ABC" / "xhello" / "keep.c") not in paths
|
|
140
|
+
assert str(tmp_path / "ABC" / "hello" / "EFG" / "skip.c") in paths
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_exclude_dirs_segment_glob_includes_bare_hello_with_doublestar(tmp_path: Path):
|
|
144
|
+
touch(tmp_path / "ABC" / "hello" / "EFG" / "skip.c")
|
|
145
|
+
touch(tmp_path / "ABC" / "Myhello" / "EFG" / "skip.c")
|
|
146
|
+
touch(tmp_path / "ok" / "file.c")
|
|
147
|
+
|
|
148
|
+
cfg = make_config(
|
|
149
|
+
include_dirs=["**"],
|
|
150
|
+
include_extensions=["c"],
|
|
151
|
+
exclude_dirs=["**/**hello/**"],
|
|
152
|
+
)
|
|
153
|
+
paths = select_paths(tmp_path, cfg)
|
|
154
|
+
assert str(tmp_path / "ok" / "file.c") in paths
|
|
155
|
+
assert str(tmp_path / "ABC" / "hello" / "EFG" / "skip.c") not in paths
|
|
156
|
+
assert str(tmp_path / "ABC" / "Myhello" / "EFG" / "skip.c") not in paths
|
|
157
|
+
|
|
158
|
+
def test_match_file_dir_segment_glob(tmp_path: Path):
|
|
159
|
+
touch(tmp_path / "ABC" / "Myhello" / "target.c")
|
|
160
|
+
touch(tmp_path / "ABC" / "hello" / "target.c")
|
|
161
|
+
cfg = make_config(
|
|
162
|
+
include_files=["**/*hello/**/target.c"],
|
|
163
|
+
include_extensions=["c"],
|
|
164
|
+
)
|
|
165
|
+
paths = select_paths(tmp_path, cfg)
|
|
166
|
+
assert str(tmp_path / "ABC" / "Myhello" / "target.c") in paths
|
|
167
|
+
assert str(tmp_path / "ABC" / "hello" / "target.c") not in paths
|
|
168
|
+
|
|
169
|
+
|
|
115
170
|
def test_root_only_dir_pattern_does_not_match_nested_descendants(tree: Path):
|
|
116
171
|
cfg = make_config(include_dirs=["folder"], include_extensions=[".*"])
|
|
117
172
|
paths = select_paths(tree, cfg)
|
|
@@ -11,6 +11,7 @@ from filefilter.utilities import (
|
|
|
11
11
|
parse_dir_patterns,
|
|
12
12
|
parse_extensions,
|
|
13
13
|
pattern_specificity,
|
|
14
|
+
segment_matches,
|
|
14
15
|
split_path,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -58,6 +59,45 @@ def test_match_doublestar_single_segment_wildcard():
|
|
|
58
59
|
assert 2 not in ends
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
@pytest.mark.parametrize(
|
|
63
|
+
"token,part,expected",
|
|
64
|
+
[
|
|
65
|
+
("*hello", "myhello", True),
|
|
66
|
+
("*hello", "hello", False),
|
|
67
|
+
("*hello", "xhello", True),
|
|
68
|
+
("**hello", "hello", True),
|
|
69
|
+
("**hello", "myhello", True),
|
|
70
|
+
("**hello", "xhello", True),
|
|
71
|
+
("hello*", "helloworld", True),
|
|
72
|
+
("hello*", "hello", False),
|
|
73
|
+
("hello**", "hello", True),
|
|
74
|
+
("hello**", "helloworld", True),
|
|
75
|
+
("abc*", "abcx", True),
|
|
76
|
+
("abc*", "abc", False),
|
|
77
|
+
("*abc", "xabc", True),
|
|
78
|
+
("*abc", "abc", False),
|
|
79
|
+
("*abc*", "xabcy", True),
|
|
80
|
+
("*abc*", "abc", False),
|
|
81
|
+
("folder", "folder", True),
|
|
82
|
+
("folder", "other", False),
|
|
83
|
+
("***", "***", True),
|
|
84
|
+
("***", "myhello", False),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
def test_segment_matches_glob(token: str, part: str, expected: bool):
|
|
88
|
+
assert segment_matches(token, part) is expected
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_match_doublestar_segments_glob_in_segment():
|
|
92
|
+
parts = ["abc", "myhello", "efg"]
|
|
93
|
+
assert match_doublestar_segments(parts, 1, ["*hello"]) == [2]
|
|
94
|
+
assert match_doublestar_segments(parts, 0, ["abc", "*hello"]) == [2]
|
|
95
|
+
assert match_doublestar_segments(parts, 0, ["**", "*hello"]) == [2]
|
|
96
|
+
parts2 = ["abc", "helloworld", "efg"]
|
|
97
|
+
assert match_doublestar_segments(parts2, 1, ["hello*"]) == [2]
|
|
98
|
+
assert match_doublestar_segments(parts2, 0, ["abc", "hello*"]) == [2]
|
|
99
|
+
|
|
100
|
+
|
|
61
101
|
def test_parse_dir_patterns_buckets():
|
|
62
102
|
root, one, any_ = parse_dir_patterns(
|
|
63
103
|
["src", "*/pkg", "**/cache", "/", " "]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|