filefilter 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +55 -16
- {filefilter-0.2.2 → filefilter-0.2.4}/README.md +54 -15
- {filefilter-0.2.2 → filefilter-0.2.4}/pyproject.toml +1 -1
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter/__init__.py +1 -1
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter/collector.py +14 -12
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter/ruleset.py +2 -0
- {filefilter-0.2.2 → filefilter-0.2.4/src/filefilter.egg-info}/PKG-INFO +55 -16
- {filefilter-0.2.2 → filefilter-0.2.4}/tests/test_comprehensive.py +10 -4
- {filefilter-0.2.2 → filefilter-0.2.4}/tests/test_inclusion_logic.py +81 -6
- {filefilter-0.2.2 → filefilter-0.2.4}/tests/test_readme_verification.py +12 -2
- {filefilter-0.2.2 → filefilter-0.2.4}/tests/test_ruleset.py +63 -1
- {filefilter-0.2.2 → filefilter-0.2.4}/LICENSE +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/setup.cfg +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter/utilities.py +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter.egg-info/SOURCES.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter.egg-info/dependency_links.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter.egg-info/requires.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/src/filefilter.egg-info/top_level.txt +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/tests/test_matcher.py +0 -0
- {filefilter-0.2.2 → filefilter-0.2.4}/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.4
|
|
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 patterns (`include.odirs`, `include.ofiles`)**
|
|
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` or `include.ofiles` 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 |
|
|
@@ -173,8 +172,11 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
173
172
|
| `**` | very low (broad) |
|
|
174
173
|
| `**/KLM/**` | higher |
|
|
175
174
|
| `**/KLM/ABC/**` | highest |
|
|
175
|
+
| `**/test_*.py` vs `**/test_keep.py` | ofile wins (more specific) |
|
|
176
176
|
|
|
177
|
-
>
|
|
177
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
178
|
+
> Use `include.odirs` for directory carve-outs under `exclude.dirs` / `exclude.files`.
|
|
179
|
+
> Use `include.ofiles` for file carve-outs under `exclude.files` / `exclude.dirs`.
|
|
178
180
|
|
|
179
181
|
3️⃣ **Include-file fast path**
|
|
180
182
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -201,6 +203,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
201
203
|
"filters": {
|
|
202
204
|
"include": {
|
|
203
205
|
"dirs": [],
|
|
206
|
+
"odirs": [],
|
|
207
|
+
"ofiles": [],
|
|
204
208
|
"files": [],
|
|
205
209
|
"extensions": []
|
|
206
210
|
},
|
|
@@ -216,8 +220,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
216
220
|
| Field | Description |
|
|
217
221
|
|-------|--------------|
|
|
218
222
|
| `root_dir` | Base directory (absolute or relative). |
|
|
219
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
220
|
-
| `filters.include.
|
|
223
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
224
|
+
| `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. |
|
|
225
|
+
| `filters.include.ofiles` | Optional. Override file patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies file gating (does not skip extension whitelist). |
|
|
226
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
221
227
|
| `filters.include.extensions` | Extension whitelist. |
|
|
222
228
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
223
229
|
|
|
@@ -435,14 +441,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
435
441
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
436
442
|
|
|
437
443
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
438
|
-
|
|
444
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
439
445
|
|
|
440
446
|
```json
|
|
441
447
|
{
|
|
442
448
|
"root_dir": ".",
|
|
443
449
|
"filters": {
|
|
444
450
|
"include": {
|
|
445
|
-
"dirs": ["**"
|
|
451
|
+
"dirs": ["**"],
|
|
452
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
446
453
|
"files": [],
|
|
447
454
|
"extensions": ["c"]
|
|
448
455
|
},
|
|
@@ -458,20 +465,52 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
458
465
|
| File Path | Result | Reason |
|
|
459
466
|
|------------|---------|--------|
|
|
460
467
|
| `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 |
|
|
468
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
469
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
470
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
464
471
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
465
472
|
|
|
466
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
473
|
+
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/**"]`.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
### Example 8 — Exclude file patterns, except specific files
|
|
478
|
+
|
|
479
|
+
Exclude all `test_*.py` files, but keep `test_keep.py`:
|
|
480
|
+
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"root_dir": ".",
|
|
484
|
+
"filters": {
|
|
485
|
+
"include": {
|
|
486
|
+
"dirs": ["**"],
|
|
487
|
+
"ofiles": ["**/test_keep.py"],
|
|
488
|
+
"files": [],
|
|
489
|
+
"extensions": ["py"]
|
|
490
|
+
},
|
|
491
|
+
"exclude": {
|
|
492
|
+
"dirs": [],
|
|
493
|
+
"files": ["**/test_*.py"],
|
|
494
|
+
"extensions": []
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
| File Path | Result | Reason |
|
|
501
|
+
|------------|---------|--------|
|
|
502
|
+
| `src/app.py` | ✅ | broad include + `.py` extension |
|
|
503
|
+
| `src/test_foo.py` | ❌ | `**/test_*.py` exclude; no matching `ofiles` |
|
|
504
|
+
| `src/test_keep.py` | ✅ | `**/test_keep.py` ofile beats `**/test_*.py` exclude |
|
|
505
|
+
| `nested/test_keep.py` | ✅ | ofile exception works at any depth |
|
|
467
506
|
|
|
468
507
|
---
|
|
469
508
|
|
|
470
509
|
## Summary of Behavior
|
|
471
510
|
|
|
472
511
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
473
|
-
-
|
|
474
|
-
-
|
|
512
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
513
|
+
- Only `include.odirs` and `include.ofiles` can override a matching exclude, and only when the override pattern is **more specific**.
|
|
475
514
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
476
515
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
477
516
|
- 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 patterns (`include.odirs`, `include.ofiles`)**
|
|
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` or `include.ofiles` 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 |
|
|
@@ -140,8 +139,11 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
140
139
|
| `**` | very low (broad) |
|
|
141
140
|
| `**/KLM/**` | higher |
|
|
142
141
|
| `**/KLM/ABC/**` | highest |
|
|
142
|
+
| `**/test_*.py` vs `**/test_keep.py` | ofile wins (more specific) |
|
|
143
143
|
|
|
144
|
-
>
|
|
144
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
145
|
+
> Use `include.odirs` for directory carve-outs under `exclude.dirs` / `exclude.files`.
|
|
146
|
+
> Use `include.ofiles` for file carve-outs under `exclude.files` / `exclude.dirs`.
|
|
145
147
|
|
|
146
148
|
3️⃣ **Include-file fast path**
|
|
147
149
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -168,6 +170,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
168
170
|
"filters": {
|
|
169
171
|
"include": {
|
|
170
172
|
"dirs": [],
|
|
173
|
+
"odirs": [],
|
|
174
|
+
"ofiles": [],
|
|
171
175
|
"files": [],
|
|
172
176
|
"extensions": []
|
|
173
177
|
},
|
|
@@ -183,8 +187,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
183
187
|
| Field | Description |
|
|
184
188
|
|-------|--------------|
|
|
185
189
|
| `root_dir` | Base directory (absolute or relative). |
|
|
186
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
187
|
-
| `filters.include.
|
|
190
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
191
|
+
| `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. |
|
|
192
|
+
| `filters.include.ofiles` | Optional. Override file patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies file gating (does not skip extension whitelist). |
|
|
193
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
188
194
|
| `filters.include.extensions` | Extension whitelist. |
|
|
189
195
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
190
196
|
|
|
@@ -402,14 +408,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
402
408
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
403
409
|
|
|
404
410
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
405
|
-
|
|
411
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
406
412
|
|
|
407
413
|
```json
|
|
408
414
|
{
|
|
409
415
|
"root_dir": ".",
|
|
410
416
|
"filters": {
|
|
411
417
|
"include": {
|
|
412
|
-
"dirs": ["**"
|
|
418
|
+
"dirs": ["**"],
|
|
419
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
413
420
|
"files": [],
|
|
414
421
|
"extensions": ["c"]
|
|
415
422
|
},
|
|
@@ -425,20 +432,52 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
425
432
|
| File Path | Result | Reason |
|
|
426
433
|
|------------|---------|--------|
|
|
427
434
|
| `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 |
|
|
435
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
436
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
437
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
431
438
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
432
439
|
|
|
433
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
440
|
+
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/**"]`.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
### Example 8 — Exclude file patterns, except specific files
|
|
445
|
+
|
|
446
|
+
Exclude all `test_*.py` files, but keep `test_keep.py`:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"root_dir": ".",
|
|
451
|
+
"filters": {
|
|
452
|
+
"include": {
|
|
453
|
+
"dirs": ["**"],
|
|
454
|
+
"ofiles": ["**/test_keep.py"],
|
|
455
|
+
"files": [],
|
|
456
|
+
"extensions": ["py"]
|
|
457
|
+
},
|
|
458
|
+
"exclude": {
|
|
459
|
+
"dirs": [],
|
|
460
|
+
"files": ["**/test_*.py"],
|
|
461
|
+
"extensions": []
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
| File Path | Result | Reason |
|
|
468
|
+
|------------|---------|--------|
|
|
469
|
+
| `src/app.py` | ✅ | broad include + `.py` extension |
|
|
470
|
+
| `src/test_foo.py` | ❌ | `**/test_*.py` exclude; no matching `ofiles` |
|
|
471
|
+
| `src/test_keep.py` | ✅ | `**/test_keep.py` ofile beats `**/test_*.py` exclude |
|
|
472
|
+
| `nested/test_keep.py` | ✅ | ofile exception works at any depth |
|
|
434
473
|
|
|
435
474
|
---
|
|
436
475
|
|
|
437
476
|
## Summary of Behavior
|
|
438
477
|
|
|
439
478
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
440
|
-
-
|
|
441
|
-
-
|
|
479
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
480
|
+
- Only `include.odirs` and `include.ofiles` can override a matching exclude, and only when the override pattern is **more specific**.
|
|
442
481
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
443
482
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
444
483
|
- 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.4"
|
|
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.4'
|
|
28
28
|
|
|
29
29
|
#########################################################################################
|
|
30
30
|
# IMPORTS #
|
|
@@ -164,12 +164,13 @@ 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
|
-
|
|
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)
|
|
171
|
+
ofiles = cfg.include_ofiles or []
|
|
172
|
+
ofile_spec = best_matching_specificity(ofiles, match_file, rel)
|
|
173
|
+
override_spec = max(odir_spec, ofile_spec)
|
|
173
174
|
|
|
174
175
|
exc_dirs = _merge(cfg.exc_dirs_root, cfg.exc_dirs_one, cfg.exc_dirs_any)
|
|
175
176
|
exc_path_spec = -1
|
|
@@ -186,15 +187,16 @@ def should_include(full_path: str, cfg: Ruleset) -> bool:
|
|
|
186
187
|
exc_path_spec,
|
|
187
188
|
best_matching_specificity(exc_dirs, match_dir, dir_rel),
|
|
188
189
|
)
|
|
189
|
-
if exclude_path_hit and
|
|
190
|
+
if exclude_path_hit and override_spec <= exc_path_spec:
|
|
190
191
|
return False
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if files_match:
|
|
193
|
+
gate_files = _merge(cfg.include_files, ofiles)
|
|
194
|
+
if cfg.include_files and match_file(rel, cfg.include_files):
|
|
195
195
|
return True
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
files_present = bool(gate_files)
|
|
197
|
+
files_match = files_present and match_file(rel, gate_files)
|
|
198
|
+
dirs_present = bool(gate_dirs)
|
|
199
|
+
dirs_match = dirs_present and match_dir(dir_rel, gate_dirs)
|
|
198
200
|
if (files_present or dirs_present) and not (files_match or dirs_match):
|
|
199
201
|
return False
|
|
200
202
|
if cfg.inc_exts and not ext_matches(ext, cfg.inc_exts, name):
|
|
@@ -54,9 +54,11 @@ 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', []))
|
|
61
|
+
self.include_ofiles = parse_file_patterns(inc.get('ofiles', []))
|
|
60
62
|
self.exclude_files = parse_file_patterns(exc.get('files', []))
|
|
61
63
|
|
|
62
64
|
self.inc_exts = parse_extensions(inc.get('extensions', []))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: filefilter
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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 patterns (`include.odirs`, `include.ofiles`)**
|
|
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` or `include.ofiles` 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 |
|
|
@@ -173,8 +172,11 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
173
172
|
| `**` | very low (broad) |
|
|
174
173
|
| `**/KLM/**` | higher |
|
|
175
174
|
| `**/KLM/ABC/**` | highest |
|
|
175
|
+
| `**/test_*.py` vs `**/test_keep.py` | ofile wins (more specific) |
|
|
176
176
|
|
|
177
|
-
>
|
|
177
|
+
> Use `include.dirs` for normal scoping (e.g. `**`, `src/**`).
|
|
178
|
+
> Use `include.odirs` for directory carve-outs under `exclude.dirs` / `exclude.files`.
|
|
179
|
+
> Use `include.ofiles` for file carve-outs under `exclude.files` / `exclude.dirs`.
|
|
178
180
|
|
|
179
181
|
3️⃣ **Include-file fast path**
|
|
180
182
|
- If the file matches any `include.files` pattern → **included immediately**,
|
|
@@ -201,6 +203,8 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
201
203
|
"filters": {
|
|
202
204
|
"include": {
|
|
203
205
|
"dirs": [],
|
|
206
|
+
"odirs": [],
|
|
207
|
+
"ofiles": [],
|
|
204
208
|
"files": [],
|
|
205
209
|
"extensions": []
|
|
206
210
|
},
|
|
@@ -216,8 +220,10 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
216
220
|
| Field | Description |
|
|
217
221
|
|-------|--------------|
|
|
218
222
|
| `root_dir` | Base directory (absolute or relative). |
|
|
219
|
-
| `filters.include.dirs` | Directory inclusion patterns
|
|
220
|
-
| `filters.include.
|
|
223
|
+
| `filters.include.dirs` | Directory inclusion patterns for gating (must match when any include filters are set). Never overrides excludes. |
|
|
224
|
+
| `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. |
|
|
225
|
+
| `filters.include.ofiles` | Optional. Override file patterns (defaults to `[]` when omitted). When more specific than a matching exclude, the exclude is ignored for that path. Also satisfies file gating (does not skip extension whitelist). |
|
|
226
|
+
| `filters.include.files` | File inclusion patterns (glob-like). Forces inclusion (skips extension whitelist) once path excludes are resolved. Does not override excludes. |
|
|
221
227
|
| `filters.include.extensions` | Extension whitelist. |
|
|
222
228
|
| `filters.exclude.*` | Same structure, but acts as exclusion filters. |
|
|
223
229
|
|
|
@@ -435,14 +441,15 @@ When scanning files, the library applies filters in this exact sequence:
|
|
|
435
441
|
### Example 7 — Exclude a subtree, except an explicit include path
|
|
436
442
|
|
|
437
443
|
Exclude everything under `KLM`, but still collect files under `KLM/ABC`.
|
|
438
|
-
|
|
444
|
+
Use `include.dirs` for the broad scope and `include.odirs` for the exception:
|
|
439
445
|
|
|
440
446
|
```json
|
|
441
447
|
{
|
|
442
448
|
"root_dir": ".",
|
|
443
449
|
"filters": {
|
|
444
450
|
"include": {
|
|
445
|
-
"dirs": ["**"
|
|
451
|
+
"dirs": ["**"],
|
|
452
|
+
"odirs": ["**/KLM/ABC/**"],
|
|
446
453
|
"files": [],
|
|
447
454
|
"extensions": ["c"]
|
|
448
455
|
},
|
|
@@ -458,20 +465,52 @@ List the exception alongside the broad include in normal `include.dirs`:
|
|
|
458
465
|
| File Path | Result | Reason |
|
|
459
466
|
|------------|---------|--------|
|
|
460
467
|
| `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 |
|
|
468
|
+
| `src/KLM/skip.c` | ❌ | `**/KLM/**` exclude; no matching `odirs` |
|
|
469
|
+
| `src/KLM/ABC/keep.c` | ✅ | `**/KLM/ABC/**` odir beats `**/KLM/**` exclude |
|
|
470
|
+
| `src/x/KLM/ABC/deep/keep.c` | ✅ | odir exception works at any depth |
|
|
464
471
|
| `src/KLM/ABC/keep.txt` | ❌ | wrong extension (still inside the allowed dir) |
|
|
465
472
|
|
|
466
|
-
The same technique applies to other “exclude a path part, except one branch” cases — for example, exclude `**/ABC/**` but keep `**/ABC/KLM/**`
|
|
473
|
+
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/**"]`.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
### Example 8 — Exclude file patterns, except specific files
|
|
478
|
+
|
|
479
|
+
Exclude all `test_*.py` files, but keep `test_keep.py`:
|
|
480
|
+
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"root_dir": ".",
|
|
484
|
+
"filters": {
|
|
485
|
+
"include": {
|
|
486
|
+
"dirs": ["**"],
|
|
487
|
+
"ofiles": ["**/test_keep.py"],
|
|
488
|
+
"files": [],
|
|
489
|
+
"extensions": ["py"]
|
|
490
|
+
},
|
|
491
|
+
"exclude": {
|
|
492
|
+
"dirs": [],
|
|
493
|
+
"files": ["**/test_*.py"],
|
|
494
|
+
"extensions": []
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
| File Path | Result | Reason |
|
|
501
|
+
|------------|---------|--------|
|
|
502
|
+
| `src/app.py` | ✅ | broad include + `.py` extension |
|
|
503
|
+
| `src/test_foo.py` | ❌ | `**/test_*.py` exclude; no matching `ofiles` |
|
|
504
|
+
| `src/test_keep.py` | ✅ | `**/test_keep.py` ofile beats `**/test_*.py` exclude |
|
|
505
|
+
| `nested/test_keep.py` | ✅ | ofile exception works at any depth |
|
|
467
506
|
|
|
468
507
|
---
|
|
469
508
|
|
|
470
509
|
## Summary of Behavior
|
|
471
510
|
|
|
472
511
|
- `exclude.extensions` are always applied first (hard exclude).
|
|
473
|
-
-
|
|
474
|
-
-
|
|
512
|
+
- `include.dirs` and `include.files` **never** override path excludes — they only gate inclusion.
|
|
513
|
+
- Only `include.odirs` and `include.ofiles` can override a matching exclude, and only when the override pattern is **more specific**.
|
|
475
514
|
- A matching `include.files` pattern forces inclusion (skips extension whitelist), once path excludes are resolved.
|
|
476
515
|
- If any include filters exist, at least one must match (inclusion gating).
|
|
477
516
|
- 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
|
)
|
|
@@ -237,3 +250,65 @@ def test_abc_klm_exception_via_include_dirs_only(tmp_path: Path):
|
|
|
237
250
|
assert str(tmp_path / "src" / "ABC" / "KLM" / "nested" / "keep.c") in paths
|
|
238
251
|
assert str(tmp_path / "src" / "x" / "ABC" / "other" / "skip.c") not in paths
|
|
239
252
|
assert str(tmp_path / "src" / "x" / "ABC" / "KLM" / "keep.c") in paths
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_specific_ofile_beats_broader_exclude_file(tmp_path: Path):
|
|
256
|
+
"""Only include.ofiles override excludes; include.files do not."""
|
|
257
|
+
touch(tmp_path / "src" / "app.py")
|
|
258
|
+
touch(tmp_path / "src" / "test_foo.py")
|
|
259
|
+
touch(tmp_path / "src" / "test_keep.py")
|
|
260
|
+
touch(tmp_path / "src" / "nested" / "test_keep.py")
|
|
261
|
+
|
|
262
|
+
cfg = make_config(
|
|
263
|
+
include_dirs=["**"],
|
|
264
|
+
include_ofiles=["**/test_keep.py"],
|
|
265
|
+
include_extensions=["py"],
|
|
266
|
+
exclude_files=["**/test_*.py"],
|
|
267
|
+
)
|
|
268
|
+
paths = select_paths(tmp_path, cfg)
|
|
269
|
+
|
|
270
|
+
assert str(tmp_path / "src" / "app.py") in paths
|
|
271
|
+
assert str(tmp_path / "src" / "test_foo.py") not in paths
|
|
272
|
+
assert str(tmp_path / "src" / "test_keep.py") in paths
|
|
273
|
+
assert str(tmp_path / "src" / "nested" / "test_keep.py") in paths
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_include_files_do_not_override_exclude_file(tmp_path: Path):
|
|
277
|
+
touch(tmp_path / "src" / "test_keep.py")
|
|
278
|
+
cfg = make_config(
|
|
279
|
+
include_dirs=["**"],
|
|
280
|
+
include_files=["**/test_keep.py", "**/test_*.py"],
|
|
281
|
+
include_extensions=["py"],
|
|
282
|
+
exclude_files=["**/test_*.py"],
|
|
283
|
+
)
|
|
284
|
+
paths = select_paths(tmp_path, cfg)
|
|
285
|
+
assert str(tmp_path / "src" / "test_keep.py") not in paths
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_ofile_overrides_exclude_dir_for_single_file(tmp_path: Path):
|
|
289
|
+
touch(tmp_path / "build" / "app.py")
|
|
290
|
+
touch(tmp_path / "build" / "keep.py")
|
|
291
|
+
cfg = make_config(
|
|
292
|
+
include_dirs=["**"],
|
|
293
|
+
include_ofiles=["**/build/keep.py"],
|
|
294
|
+
include_extensions=["py"],
|
|
295
|
+
exclude_dirs=["**/build/**"],
|
|
296
|
+
)
|
|
297
|
+
paths = select_paths(tmp_path, cfg)
|
|
298
|
+
assert str(tmp_path / "build" / "app.py") not in paths
|
|
299
|
+
assert str(tmp_path / "build" / "keep.py") in paths
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_ofile_satisfies_file_gating_without_fast_path(tmp_path: Path):
|
|
303
|
+
"""ofiles satisfy gating but do not skip extension whitelist."""
|
|
304
|
+
touch(tmp_path / "src" / "keep.txt")
|
|
305
|
+
touch(tmp_path / "src" / "skip.txt")
|
|
306
|
+
cfg = make_config(
|
|
307
|
+
include_files=["**/only.py"],
|
|
308
|
+
include_ofiles=["**/keep.txt"],
|
|
309
|
+
include_extensions=["py"],
|
|
310
|
+
exclude_files=["**/*.txt"],
|
|
311
|
+
)
|
|
312
|
+
paths = select_paths(tmp_path, cfg)
|
|
313
|
+
assert str(tmp_path / "src" / "keep.txt") not in paths
|
|
314
|
+
assert str(tmp_path / "src" / "skip.txt") not in paths
|
|
@@ -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,67 @@ 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
|
+
|
|
89
|
+
def test_missing_ofiles_defaults_to_empty(tmp_path: Path):
|
|
90
|
+
cfg = {
|
|
91
|
+
"root_dir": ".",
|
|
92
|
+
"filters": {
|
|
93
|
+
"include": {"dirs": ["**"], "extensions": ["py"]},
|
|
94
|
+
"exclude": {"files": ["**/skip.py"]},
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
touch(tmp_path / "app.py")
|
|
98
|
+
touch(tmp_path / "skip.py")
|
|
99
|
+
rules = load(json.dumps(cfg), base=str(tmp_path))
|
|
100
|
+
assert rules.include_ofiles == []
|
|
101
|
+
assert matches(str(tmp_path / "app.py"), rules) is True
|
|
102
|
+
assert matches(str(tmp_path / "skip.py"), rules) is False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_parse_ofiles_like_files():
|
|
106
|
+
cfg = {
|
|
107
|
+
"root_dir": ".",
|
|
108
|
+
"filters": {
|
|
109
|
+
"include": {"files": ["*.py"], "ofiles": ["**/keep.txt", "README.*"]},
|
|
110
|
+
"exclude": {},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
rules = Ruleset(cfg, resolve_base=".")
|
|
114
|
+
assert rules.include_files == ["*.py"]
|
|
115
|
+
assert rules.include_ofiles == ["**/keep.txt", "readme.*"]
|
|
116
|
+
|
|
117
|
+
|
|
56
118
|
def test_empty_dir_patterns_are_ignored(tmp_path: Path):
|
|
57
119
|
cfg = {
|
|
58
120
|
"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
|