filefilter 0.2.2__tar.gz → 0.2.3__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.2/src/filefilter.egg-info → filefilter-0.2.3}/PKG-INFO +19 -16
- {filefilter-0.2.2 → filefilter-0.2.3}/README.md +18 -15
- {filefilter-0.2.2 → filefilter-0.2.3}/pyproject.toml +1 -1
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter/__init__.py +1 -1
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter/collector.py +7 -9
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter/ruleset.py +1 -0
- {filefilter-0.2.2 → filefilter-0.2.3/src/filefilter.egg-info}/PKG-INFO +19 -16
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_comprehensive.py +10 -4
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_inclusion_logic.py +19 -6
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_readme_verification.py +12 -2
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_ruleset.py +34 -1
- {filefilter-0.2.2 → filefilter-0.2.3}/LICENSE +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/setup.cfg +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter/utilities.py +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter.egg-info/SOURCES.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter.egg-info/dependency_links.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter.egg-info/requires.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/src/filefilter.egg-info/top_level.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_matcher.py +0 -0
- {filefilter-0.2.2 → filefilter-0.2.3}/tests/test_utilities.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.3
|
|
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
|
|
@@ -161,11 +161,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
161
161
|
- If the file extension matches `exclude.extensions` → **excluded immediately**.
|
|
162
162
|
- This step is not overridden by include patterns.
|
|
163
163
|
|
|
164
|
-
2️⃣ **Path excludes vs.
|
|
164
|
+
2️⃣ **Path excludes vs. override dirs (`include.odirs`)**
|
|
165
165
|
- If the file path matches `exclude.files`, or its parent directory matches `exclude.dirs`, the file is a candidate for exclusion.
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
- If the **best matching include pattern is more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
166
|
+
- **`include.dirs` and `include.files` do not override excludes** — they only gate inclusion later.
|
|
167
|
+
- If a matching pattern in `include.odirs` is **more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
169
168
|
- Otherwise → **excluded**.
|
|
170
169
|
|
|
171
170
|
| Pattern | Relative specificity |
|
|
@@ -174,7 +173,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
174
173
|
| `**/KLM/**` | higher |
|
|
175
174
|
| `**/KLM/ABC/**` | highest |
|
|
176
175
|
|
|
177
|
-
>
|
|
176
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
177
|
+
> Use `include.odirs` only for explicit exceptions to `exclude.dirs` / `exclude.files`.
|
|
178
178
|
|
|
179
179
|
3️⃣ **Include-file fast path**
|
|
180
180
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -201,6 +201,7 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
201
201
|
"filters": {
|
|
202
202
|
"include": {
|
|
203
203
|
"dirs": [],
|
|
204
|
+
"odirs": [],
|
|
204
205
|
"files": [],
|
|
205
206
|
"extensions": []
|
|
206
207
|
},
|
|
@@ -216,8 +217,9 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
216
217
|
| Field | Description |
|
|
217
218
|
|-------|--------------|
|
|
218
219
|
| `root_dir` | Base directory (absolute or relative). |
|
|
219
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
220
|
-
| `filters.include.
|
|
220
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
221
|
+
| `filters.include.odirs` | Optional. Override directory patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies dir gating. |
|
|
222
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
221
223
|
| `filters.include.extensions` | Extension whitelist. |
|
|
222
224
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
223
225
|
|
|
@@ -435,14 +437,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
435
437
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
436
438
|
|
|
437
439
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
438
|
-
|
|
440
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
439
441
|
|
|
440
442
|
```json
|
|
441
443
|
{
|
|
442
444
|
"root_dir": ".",
|
|
443
445
|
"filters": {
|
|
444
446
|
"include": {
|
|
445
|
-
"dirs": ["**"
|
|
447
|
+
"dirs": ["**"],
|
|
448
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
446
449
|
"files": [],
|
|
447
450
|
"extensions": ["c"]
|
|
448
451
|
},
|
|
@@ -458,20 +461,20 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
458
461
|
| File Path | Result | Reason |
|
|
459
462
|
|------------|---------|--------|
|
|
460
463
|
| `src/foo.c` | ✅ | broad `**` include + `.c` extension |
|
|
461
|
-
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude
|
|
462
|
-
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**`
|
|
463
|
-
| `src/x/KLM/ABC/deep/keep.c` | ✅ | exception works at any depth |
|
|
464
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
465
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
466
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
464
467
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
465
468
|
|
|
466
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
469
|
+
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**` via `include.odirs: ["**/ABC/KLM/**"]` alongside `include.dirs: ["src/**"]`.
|
|
467
470
|
|
|
468
471
|
---
|
|
469
472
|
|
|
470
473
|
## Summary of Behavior
|
|
471
474
|
|
|
472
475
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
473
|
-
-
|
|
474
|
-
-
|
|
476
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
477
|
+
- Only `include.odirs` can override a matching exclude, and only when the odir pattern is **more specific**.
|
|
475
478
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
476
479
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
477
480
|
- Extensions act as a **final whitelist** when not bypassed by `include.files`.
|
|
@@ -128,11 +128,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
128
128
|
- If the file extension matches `exclude.extensions` → **excluded immediately**.
|
|
129
129
|
- This step is not overridden by include patterns.
|
|
130
130
|
|
|
131
|
-
2️⃣ **Path excludes vs.
|
|
131
|
+
2️⃣ **Path excludes vs. override dirs (`include.odirs`)**
|
|
132
132
|
- If the file path matches `exclude.files`, or its parent directory matches `exclude.dirs`, the file is a candidate for exclusion.
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
- If the **best matching include pattern is more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
133
|
+
- **`include.dirs` and `include.files` do not override excludes** — they only gate inclusion later.
|
|
134
|
+
- If a matching pattern in `include.odirs` is **more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
136
135
|
- Otherwise → **excluded**.
|
|
137
136
|
|
|
138
137
|
| Pattern | Relative specificity |
|
|
@@ -141,7 +140,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
141
140
|
| `**/KLM/**` | higher |
|
|
142
141
|
| `**/KLM/ABC/**` | highest |
|
|
143
142
|
|
|
144
|
-
>
|
|
143
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
144
|
+
> Use `include.odirs` only for explicit exceptions to `exclude.dirs` / `exclude.files`.
|
|
145
145
|
|
|
146
146
|
3️⃣ **Include-file fast path**
|
|
147
147
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -168,6 +168,7 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
168
168
|
"filters": {
|
|
169
169
|
"include": {
|
|
170
170
|
"dirs": [],
|
|
171
|
+
"odirs": [],
|
|
171
172
|
"files": [],
|
|
172
173
|
"extensions": []
|
|
173
174
|
},
|
|
@@ -183,8 +184,9 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
183
184
|
| Field | Description |
|
|
184
185
|
|-------|--------------|
|
|
185
186
|
| `root_dir` | Base directory (absolute or relative). |
|
|
186
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
187
|
-
| `filters.include.
|
|
187
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
188
|
+
| `filters.include.odirs` | Optional. Override directory patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies dir gating. |
|
|
189
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
188
190
|
| `filters.include.extensions` | Extension whitelist. |
|
|
189
191
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
190
192
|
|
|
@@ -402,14 +404,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
402
404
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
403
405
|
|
|
404
406
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
405
|
-
|
|
407
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
406
408
|
|
|
407
409
|
```json
|
|
408
410
|
{
|
|
409
411
|
"root_dir": ".",
|
|
410
412
|
"filters": {
|
|
411
413
|
"include": {
|
|
412
|
-
"dirs": ["**"
|
|
414
|
+
"dirs": ["**"],
|
|
415
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
413
416
|
"files": [],
|
|
414
417
|
"extensions": ["c"]
|
|
415
418
|
},
|
|
@@ -425,20 +428,20 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
425
428
|
| File Path | Result | Reason |
|
|
426
429
|
|------------|---------|--------|
|
|
427
430
|
| `src/foo.c` | ✅ | broad `**` include + `.c` extension |
|
|
428
|
-
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude
|
|
429
|
-
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**`
|
|
430
|
-
| `src/x/KLM/ABC/deep/keep.c` | ✅ | exception works at any depth |
|
|
431
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
432
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
433
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
431
434
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
432
435
|
|
|
433
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
436
|
+
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**` via `include.odirs: ["**/ABC/KLM/**"]` alongside `include.dirs: ["src/**"]`.
|
|
434
437
|
|
|
435
438
|
---
|
|
436
439
|
|
|
437
440
|
## Summary of Behavior
|
|
438
441
|
|
|
439
442
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
440
|
-
-
|
|
441
|
-
-
|
|
443
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
444
|
+
- Only `include.odirs` can override a matching exclude, and only when the odir pattern is **more specific**.
|
|
442
445
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
443
446
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
444
447
|
- Extensions act as a **final whitelist** when not bypassed by `include.files`.
|
|
@@ -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.3"
|
|
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.3'
|
|
28
28
|
|
|
29
29
|
#########################################################################################
|
|
30
30
|
# IMPORTS #
|
|
@@ -164,12 +164,10 @@ def should_include(full_path: str, cfg: Ruleset) -> bool:
|
|
|
164
164
|
return False
|
|
165
165
|
|
|
166
166
|
inc_dirs = _merge(cfg.inc_dirs_root, cfg.inc_dirs_one, cfg.inc_dirs_any)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
best_matching_specificity(cfg.include_files, match_file, rel),
|
|
172
|
-
)
|
|
167
|
+
odirs = _merge(cfg.inc_odirs_root, cfg.inc_odirs_one, cfg.inc_odirs_any)
|
|
168
|
+
gate_dirs = _merge(inc_dirs, odirs)
|
|
169
|
+
|
|
170
|
+
odir_spec = best_matching_specificity(odirs, match_dir, dir_rel)
|
|
173
171
|
|
|
174
172
|
exc_dirs = _merge(cfg.exc_dirs_root, cfg.exc_dirs_one, cfg.exc_dirs_any)
|
|
175
173
|
exc_path_spec = -1
|
|
@@ -186,15 +184,15 @@ def should_include(full_path: str, cfg: Ruleset) -> bool:
|
|
|
186
184
|
exc_path_spec,
|
|
187
185
|
best_matching_specificity(exc_dirs, match_dir, dir_rel),
|
|
188
186
|
)
|
|
189
|
-
if exclude_path_hit and
|
|
187
|
+
if exclude_path_hit and odir_spec <= exc_path_spec:
|
|
190
188
|
return False
|
|
191
189
|
|
|
192
190
|
files_present = bool(cfg.include_files)
|
|
193
191
|
files_match = files_present and match_file(rel, cfg.include_files)
|
|
194
192
|
if files_match:
|
|
195
193
|
return True
|
|
196
|
-
dirs_present = bool(
|
|
197
|
-
dirs_match = dirs_present and match_dir(dir_rel,
|
|
194
|
+
dirs_present = bool(gate_dirs)
|
|
195
|
+
dirs_match = dirs_present and match_dir(dir_rel, gate_dirs)
|
|
198
196
|
if (files_present or dirs_present) and not (files_match or dirs_match):
|
|
199
197
|
return False
|
|
200
198
|
if cfg.inc_exts and not ext_matches(ext, cfg.inc_exts, name):
|
|
@@ -54,6 +54,7 @@ class Ruleset:
|
|
|
54
54
|
exc = data['filters']['exclude']
|
|
55
55
|
|
|
56
56
|
self.inc_dirs_root, self.inc_dirs_one, self.inc_dirs_any = parse_dir_patterns(inc.get('dirs', []))
|
|
57
|
+
self.inc_odirs_root, self.inc_odirs_one, self.inc_odirs_any = parse_dir_patterns(inc.get('odirs', []))
|
|
57
58
|
self.exc_dirs_root, self.exc_dirs_one, self.exc_dirs_any = parse_dir_patterns(exc.get('dirs', []))
|
|
58
59
|
|
|
59
60
|
self.include_files = parse_file_patterns(inc.get('files', []))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: filefilter
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -161,11 +161,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
161
161
|
- If the file extension matches `exclude.extensions` → **excluded immediately**.
|
|
162
162
|
- This step is not overridden by include patterns.
|
|
163
163
|
|
|
164
|
-
2️⃣ **Path excludes vs.
|
|
164
|
+
2️⃣ **Path excludes vs. override dirs (`include.odirs`)**
|
|
165
165
|
- If the file path matches `exclude.files`, or its parent directory matches `exclude.dirs`, the file is a candidate for exclusion.
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
- If the **best matching include pattern is more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
166
|
+
- **`include.dirs` and `include.files` do not override excludes** — they only gate inclusion later.
|
|
167
|
+
- If a matching pattern in `include.odirs` is **more specific** than the best matching exclude pattern → the exclude is **ignored** for this file.
|
|
169
168
|
- Otherwise → **excluded**.
|
|
170
169
|
|
|
171
170
|
| Pattern | Relative specificity |
|
|
@@ -174,7 +173,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
174
173
|
| `**/KLM/**` | higher |
|
|
175
174
|
| `**/KLM/ABC/**` | highest |
|
|
176
175
|
|
|
177
|
-
>
|
|
176
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
177
|
+
> Use `include.odirs` only for explicit exceptions to `exclude.dirs` / `exclude.files`.
|
|
178
178
|
|
|
179
179
|
3️⃣ **Include-file fast path**
|
|
180
180
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -201,6 +201,7 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
201
201
|
"filters": {
|
|
202
202
|
"include": {
|
|
203
203
|
"dirs": [],
|
|
204
|
+
"odirs": [],
|
|
204
205
|
"files": [],
|
|
205
206
|
"extensions": []
|
|
206
207
|
},
|
|
@@ -216,8 +217,9 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
216
217
|
| Field | Description |
|
|
217
218
|
|-------|--------------|
|
|
218
219
|
| `root_dir` | Base directory (absolute or relative). |
|
|
219
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
220
|
-
| `filters.include.
|
|
220
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
221
|
+
| `filters.include.odirs` | Optional. Override directory patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies dir gating. |
|
|
222
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
221
223
|
| `filters.include.extensions` | Extension whitelist. |
|
|
222
224
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
223
225
|
|
|
@@ -435,14 +437,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
435
437
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
436
438
|
|
|
437
439
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
438
|
-
|
|
440
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
439
441
|
|
|
440
442
|
```json
|
|
441
443
|
{
|
|
442
444
|
"root_dir": ".",
|
|
443
445
|
"filters": {
|
|
444
446
|
"include": {
|
|
445
|
-
"dirs": ["**"
|
|
447
|
+
"dirs": ["**"],
|
|
448
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
446
449
|
"files": [],
|
|
447
450
|
"extensions": ["c"]
|
|
448
451
|
},
|
|
@@ -458,20 +461,20 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
458
461
|
| File Path | Result | Reason |
|
|
459
462
|
|------------|---------|--------|
|
|
460
463
|
| `src/foo.c` | ✅ | broad `**` include + `.c` extension |
|
|
461
|
-
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude
|
|
462
|
-
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**`
|
|
463
|
-
| `src/x/KLM/ABC/deep/keep.c` | ✅ | exception works at any depth |
|
|
464
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
465
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
466
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
464
467
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
465
468
|
|
|
466
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
469
|
+
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**` via `include.odirs: ["**/ABC/KLM/**"]` alongside `include.dirs: ["src/**"]`.
|
|
467
470
|
|
|
468
471
|
---
|
|
469
472
|
|
|
470
473
|
## Summary of Behavior
|
|
471
474
|
|
|
472
475
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
473
|
-
-
|
|
474
|
-
-
|
|
476
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
477
|
+
- Only `include.odirs` can override a matching exclude, and only when the odir pattern is **more specific**.
|
|
475
478
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
476
479
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
477
480
|
- Extensions act as a **final whitelist** when not bypassed by `include.files`.
|
|
@@ -262,7 +262,8 @@ def test_regression_file_matching(path: str, patterns: list[str], expected: bool
|
|
|
262
262
|
|
|
263
263
|
SPECIFICITY_SCENARIOS = [
|
|
264
264
|
pytest.param(
|
|
265
|
-
["**"
|
|
265
|
+
["**"],
|
|
266
|
+
["**/KLM/ABC/**"],
|
|
266
267
|
["**/KLM/**"],
|
|
267
268
|
{
|
|
268
269
|
"src/foo.py": True,
|
|
@@ -273,7 +274,8 @@ SPECIFICITY_SCENARIOS = [
|
|
|
273
274
|
id="klm-abc-exception",
|
|
274
275
|
),
|
|
275
276
|
pytest.param(
|
|
276
|
-
["src/**"
|
|
277
|
+
["src/**"],
|
|
278
|
+
["**/ABC/KLM/**"],
|
|
277
279
|
["**/ABC/**"],
|
|
278
280
|
{
|
|
279
281
|
"src/foo.py": True,
|
|
@@ -286,6 +288,7 @@ SPECIFICITY_SCENARIOS = [
|
|
|
286
288
|
),
|
|
287
289
|
pytest.param(
|
|
288
290
|
["**"],
|
|
291
|
+
[],
|
|
289
292
|
["**/__pycache__/**"],
|
|
290
293
|
{
|
|
291
294
|
"src/app.py": True,
|
|
@@ -294,7 +297,8 @@ SPECIFICITY_SCENARIOS = [
|
|
|
294
297
|
id="pycache-broad-exclude-wins",
|
|
295
298
|
),
|
|
296
299
|
pytest.param(
|
|
297
|
-
["**"
|
|
300
|
+
["**"],
|
|
301
|
+
[],
|
|
298
302
|
["**/skip/**"],
|
|
299
303
|
{
|
|
300
304
|
"skip/only.py": False,
|
|
@@ -306,10 +310,11 @@ SPECIFICITY_SCENARIOS = [
|
|
|
306
310
|
]
|
|
307
311
|
|
|
308
312
|
|
|
309
|
-
@pytest.mark.parametrize("inc_dirs,exc_dirs,files", SPECIFICITY_SCENARIOS)
|
|
313
|
+
@pytest.mark.parametrize("inc_dirs,inc_odirs,exc_dirs,files", SPECIFICITY_SCENARIOS)
|
|
310
314
|
def test_specificity_integration(
|
|
311
315
|
tmp_path: Path,
|
|
312
316
|
inc_dirs: list[str],
|
|
317
|
+
inc_odirs: list[str],
|
|
313
318
|
exc_dirs: list[str],
|
|
314
319
|
files: dict[str, bool],
|
|
315
320
|
):
|
|
@@ -317,6 +322,7 @@ def test_specificity_integration(
|
|
|
317
322
|
touch(tmp_path / rel)
|
|
318
323
|
cfg = make_config(
|
|
319
324
|
include_dirs=inc_dirs,
|
|
325
|
+
include_odirs=inc_odirs,
|
|
320
326
|
include_extensions=["py"],
|
|
321
327
|
exclude_dirs=exc_dirs,
|
|
322
328
|
)
|
|
@@ -179,8 +179,8 @@ def test_dotfile_file_pattern_not_extension_filter(tree: Path):
|
|
|
179
179
|
assert str(tree / "main.py") not in paths
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def
|
|
183
|
-
"""
|
|
182
|
+
def test_specific_odir_beats_broader_exclude(tmp_path: Path):
|
|
183
|
+
"""Only include.odirs override excludes; include.dirs do not."""
|
|
184
184
|
touch(tmp_path / "src" / "foo.c")
|
|
185
185
|
touch(tmp_path / "src" / "KLM" / "skip.c")
|
|
186
186
|
touch(tmp_path / "src" / "KLM" / "ABC" / "keep.c")
|
|
@@ -188,7 +188,8 @@ def test_specific_include_dir_beats_broader_exclude(tmp_path: Path):
|
|
|
188
188
|
touch(tmp_path / "src" / "x" / "KLM" / "ABC" / "keep.c")
|
|
189
189
|
|
|
190
190
|
cfg = make_config(
|
|
191
|
-
include_dirs=["**"
|
|
191
|
+
include_dirs=["**"],
|
|
192
|
+
include_odirs=["**/KLM/ABC/**"],
|
|
192
193
|
include_extensions=["c"],
|
|
193
194
|
exclude_dirs=["**/KLM/**"],
|
|
194
195
|
)
|
|
@@ -201,6 +202,17 @@ def test_specific_include_dir_beats_broader_exclude(tmp_path: Path):
|
|
|
201
202
|
assert str(tmp_path / "src" / "x" / "KLM" / "ABC" / "keep.c") in paths
|
|
202
203
|
|
|
203
204
|
|
|
205
|
+
def test_include_dirs_do_not_override_exclude(tmp_path: Path):
|
|
206
|
+
touch(tmp_path / "src" / "KLM" / "ABC" / "keep.c")
|
|
207
|
+
cfg = make_config(
|
|
208
|
+
include_dirs=["**", "**/KLM/ABC/**"],
|
|
209
|
+
include_extensions=["c"],
|
|
210
|
+
exclude_dirs=["**/KLM/**"],
|
|
211
|
+
)
|
|
212
|
+
paths = select_paths(tmp_path, cfg)
|
|
213
|
+
assert str(tmp_path / "src" / "KLM" / "ABC" / "keep.c") not in paths
|
|
214
|
+
|
|
215
|
+
|
|
204
216
|
def test_broad_include_dir_does_not_beat_specific_exclude(tree: Path):
|
|
205
217
|
cfg = make_config(
|
|
206
218
|
include_dirs=["**"],
|
|
@@ -213,8 +225,8 @@ def test_broad_include_dir_does_not_beat_specific_exclude(tree: Path):
|
|
|
213
225
|
assert str(tree / "src" / "__pycache__" / "cached.py") not in paths
|
|
214
226
|
|
|
215
227
|
|
|
216
|
-
def
|
|
217
|
-
"""
|
|
228
|
+
def test_abc_klm_exception_via_include_odirs(tmp_path: Path):
|
|
229
|
+
"""ABC/KLM carve-out uses include.odirs, not include.dirs."""
|
|
218
230
|
touch(tmp_path / "src" / "foo.c")
|
|
219
231
|
touch(tmp_path / "src" / "ABC" / "only.c")
|
|
220
232
|
touch(tmp_path / "src" / "ABC" / "other" / "skip.c")
|
|
@@ -224,7 +236,8 @@ def test_abc_klm_exception_via_include_dirs_only(tmp_path: Path):
|
|
|
224
236
|
touch(tmp_path / "src" / "x" / "ABC" / "KLM" / "keep.c")
|
|
225
237
|
|
|
226
238
|
cfg = make_config(
|
|
227
|
-
include_dirs=["src/**"
|
|
239
|
+
include_dirs=["src/**"],
|
|
240
|
+
include_odirs=["**/ABC/KLM/**"],
|
|
228
241
|
include_extensions=["c"],
|
|
229
242
|
exclude_dirs=["**/ABC/**"],
|
|
230
243
|
)
|
|
@@ -123,7 +123,12 @@ README_EXAMPLES = [
|
|
|
123
123
|
{
|
|
124
124
|
"root_dir": ".",
|
|
125
125
|
"filters": {
|
|
126
|
-
"include": {
|
|
126
|
+
"include": {
|
|
127
|
+
"dirs": ["**"],
|
|
128
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
129
|
+
"files": [],
|
|
130
|
+
"extensions": ["c"],
|
|
131
|
+
},
|
|
127
132
|
"exclude": {"dirs": ["**/KLM/**"], "files": [], "extensions": []},
|
|
128
133
|
},
|
|
129
134
|
},
|
|
@@ -191,7 +196,12 @@ def test_readme_abc_klm_branch_exception(tmp_path: Path):
|
|
|
191
196
|
cfg = {
|
|
192
197
|
"root_dir": ".",
|
|
193
198
|
"filters": {
|
|
194
|
-
"include": {
|
|
199
|
+
"include": {
|
|
200
|
+
"dirs": ["src/**"],
|
|
201
|
+
"odirs": ["**/ABC/KLM/**"],
|
|
202
|
+
"files": [],
|
|
203
|
+
"extensions": ["c"],
|
|
204
|
+
},
|
|
195
205
|
"exclude": {"dirs": ["**/ABC/**"], "files": [], "extensions": []},
|
|
196
206
|
},
|
|
197
207
|
}
|
|
@@ -6,7 +6,8 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from filefilter import Ruleset, load
|
|
9
|
+
from filefilter import Ruleset, load, matches
|
|
10
|
+
from conftest import touch
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def test_ruleset_resolves_relative_root_against_base(tmp_path: Path):
|
|
@@ -53,6 +54,38 @@ def test_parse_extensions_normalizes_values():
|
|
|
53
54
|
assert rules.exc_exts == [".log"]
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
def test_missing_odirs_defaults_to_empty(tmp_path: Path):
|
|
58
|
+
cfg = {
|
|
59
|
+
"root_dir": ".",
|
|
60
|
+
"filters": {
|
|
61
|
+
"include": {"dirs": ["**"], "extensions": ["py"]},
|
|
62
|
+
"exclude": {"dirs": ["**/build/**"]},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
touch(tmp_path / "src" / "app.py")
|
|
66
|
+
touch(tmp_path / "build" / "app.py")
|
|
67
|
+
rules = load(json.dumps(cfg), base=str(tmp_path))
|
|
68
|
+
assert rules.inc_odirs_root == []
|
|
69
|
+
assert rules.inc_odirs_one == []
|
|
70
|
+
assert rules.inc_odirs_any == []
|
|
71
|
+
assert matches(str(tmp_path / "src" / "app.py"), rules) is True
|
|
72
|
+
assert matches(str(tmp_path / "build" / "app.py"), rules) is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_parse_odirs_buckets_like_dirs():
|
|
76
|
+
cfg = {
|
|
77
|
+
"root_dir": ".",
|
|
78
|
+
"filters": {
|
|
79
|
+
"include": {"dirs": ["src"], "odirs": ["**/keep/**", "*/pkg"]},
|
|
80
|
+
"exclude": {},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
rules = Ruleset(cfg, resolve_base=".")
|
|
84
|
+
assert rules.inc_dirs_root == ["src"]
|
|
85
|
+
assert rules.inc_odirs_any == ["**/keep/**"]
|
|
86
|
+
assert rules.inc_odirs_one == ["*/pkg"]
|
|
87
|
+
|
|
88
|
+
|
|
56
89
|
def test_empty_dir_patterns_are_ignored(tmp_path: Path):
|
|
57
90
|
cfg = {
|
|
58
91
|
"root_dir": ".",
|
|
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
|