simplibs-path 0.1.0__py3-none-any.whl
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.
- simplibs/path/__init__.py +118 -0
- simplibs/path/actions/ChangeSuffix.py +23 -0
- simplibs/path/actions/CollectDirs.py +24 -0
- simplibs/path/actions/CollectFiles.py +25 -0
- simplibs/path/actions/CollectPaths.py +24 -0
- simplibs/path/actions/EnsureDir.py +22 -0
- simplibs/path/actions/EnsureFile.py +19 -0
- simplibs/path/actions/EnsureParentDir.py +20 -0
- simplibs/path/actions/FilterPaths.py +27 -0
- simplibs/path/actions/MakeUniquePath.py +22 -0
- simplibs/path/actions/NormalizeEntryPath.py +20 -0
- simplibs/path/actions/NormalizePath.py +23 -0
- simplibs/path/actions/RebasePath.py +24 -0
- simplibs/path/actions/RelativizePath.py +22 -0
- simplibs/path/actions/__init__.py +68 -0
- simplibs/path/annotations/PathLike.py +43 -0
- simplibs/path/annotations/PathPredicate.py +39 -0
- simplibs/path/annotations/RecursionDepth.py +33 -0
- simplibs/path/annotations/__init__.py +21 -0
- simplibs/path/logic/__init__.py +41 -0
- simplibs/path/logic/checks/__init__.py +29 -0
- simplibs/path/logic/checks/_helpers/__init__.py +21 -0
- simplibs/path/logic/checks/_helpers/_find_length_problem.py +48 -0
- simplibs/path/logic/checks/_helpers/_find_windows_name_problem.py +55 -0
- simplibs/path/logic/checks/_helpers/can_create_inside.py +39 -0
- simplibs/path/logic/checks/_helpers/constants/MAX_NAME_LENGTH.py +18 -0
- simplibs/path/logic/checks/_helpers/constants/WINDOWS_FORBIDDEN_CHARS.py +25 -0
- simplibs/path/logic/checks/_helpers/constants/WINDOWS_RESERVED_NAMES.py +27 -0
- simplibs/path/logic/checks/_helpers/constants/__init__.py +20 -0
- simplibs/path/logic/checks/_helpers/find_filename_problem.py +75 -0
- simplibs/path/logic/checks/_helpers/to_path.py +77 -0
- simplibs/path/logic/checks/is_empty_dir.py +59 -0
- simplibs/path/logic/checks/is_hidden_path.py +78 -0
- simplibs/path/logic/checks/is_path_readable.py +73 -0
- simplibs/path/logic/checks/is_path_writable.py +90 -0
- simplibs/path/logic/checks/is_valid_filename.py +47 -0
- simplibs/path/logic/checks/is_within.py +88 -0
- simplibs/path/logic/ensure/__init__.py +22 -0
- simplibs/path/logic/ensure/_validations/__init__.py +25 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_dir_error.py +40 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_dir_not_a_dir_error.py +36 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_create_error.py +39 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_not_a_file_error.py +36 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_not_found_error.py +36 -0
- simplibs/path/logic/ensure/ensure_dir.py +83 -0
- simplibs/path/logic/ensure/ensure_file.py +103 -0
- simplibs/path/logic/ensure/ensure_parent_dir.py +58 -0
- simplibs/path/logic/scanners/__init__.py +35 -0
- simplibs/path/logic/scanners/_helpers/ScanEntry.py +36 -0
- simplibs/path/logic/scanners/_helpers/__init__.py +30 -0
- simplibs/path/logic/scanners/_helpers/as_path_predicate.py +38 -0
- simplibs/path/logic/scanners/_helpers/build_ignore_matcher.py +88 -0
- simplibs/path/logic/scanners/_helpers/generate_paths.py +74 -0
- simplibs/path/logic/scanners/_helpers/is_link_like.py +45 -0
- simplibs/path/logic/scanners/_helpers/resolve_scan_root.py +66 -0
- simplibs/path/logic/scanners/_helpers/scan_paths.py +92 -0
- simplibs/path/logic/scanners/_helpers/walk_tree.py +118 -0
- simplibs/path/logic/scanners/_validations/__init__.py +24 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_not_a_dir_error.py +34 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_not_found_error.py +38 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_root_unreadable_error.py +38 -0
- simplibs/path/logic/scanners/_validations/raise_ignore_names_not_names_error.py +36 -0
- simplibs/path/logic/scanners/_validations/raise_nothing_to_collect_error.py +34 -0
- simplibs/path/logic/scanners/collect_dirs.py +62 -0
- simplibs/path/logic/scanners/collect_files.py +64 -0
- simplibs/path/logic/scanners/collect_paths.py +102 -0
- simplibs/path/logic/scanners/filter_paths.py +51 -0
- simplibs/path/logic/scanners/ignore_names/DEFAULT_IGNORE_NAMES.py +36 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_BUILD.py +48 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_ENV.py +31 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_GENERIC.py +30 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_IDE.py +20 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_SYSTEM.py +30 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_VCS.py +24 -0
- simplibs/path/logic/scanners/ignore_names/__init__.py +30 -0
- simplibs/path/logic/scanners/iter_paths.py +91 -0
- simplibs/path/logic/transformers/__init__.py +27 -0
- simplibs/path/logic/transformers/_helpers/__init__.py +17 -0
- simplibs/path/logic/transformers/_helpers/is_valid_suffix.py +37 -0
- simplibs/path/logic/transformers/_helpers/split_name.py +46 -0
- simplibs/path/logic/transformers/_validations/__init__.py +24 -0
- simplibs/path/logic/transformers/_validations/raise_invalid_suffix_error.py +36 -0
- simplibs/path/logic/transformers/_validations/raise_normalize_path_error.py +44 -0
- simplibs/path/logic/transformers/_validations/raise_path_has_no_name_error.py +36 -0
- simplibs/path/logic/transformers/_validations/raise_path_not_within_root_error.py +43 -0
- simplibs/path/logic/transformers/_validations/raise_unique_path_exhausted_error.py +39 -0
- simplibs/path/logic/transformers/change_suffix.py +77 -0
- simplibs/path/logic/transformers/make_unique_path.py +88 -0
- simplibs/path/logic/transformers/normalize_entry_path.py +67 -0
- simplibs/path/logic/transformers/normalize_path.py +85 -0
- simplibs/path/logic/transformers/rebase_path.py +78 -0
- simplibs/path/logic/transformers/relativize_path.py +56 -0
- simplibs/path/rules/__init__.py +66 -0
- simplibs/path/rules/_helpers/PathPartIs.py +152 -0
- simplibs/path/rules/_helpers/__init__.py +21 -0
- simplibs/path/rules/_helpers/build_not_a_path_exception.py +66 -0
- simplibs/path/rules/_helpers/build_path_rule_exception.py +71 -0
- simplibs/path/rules/_helpers/check_path.py +48 -0
- simplibs/path/rules/_validations/__init__.py +24 -0
- simplibs/path/rules/_validations/raise_has_suffix_invalid_suffix_error.py +35 -0
- simplibs/path/rules/_validations/raise_has_suffix_no_suffixes_error.py +31 -0
- simplibs/path/rules/_validations/raise_rule_param_not_bool_error.py +37 -0
- simplibs/path/rules/_validations/raise_rule_param_not_callable_error.py +36 -0
- simplibs/path/rules/_validations/raise_rule_param_not_path_error.py +37 -0
- simplibs/path/rules/filesystem/IsDir.py +70 -0
- simplibs/path/rules/filesystem/IsEmptyDir.py +76 -0
- simplibs/path/rules/filesystem/IsEmptyFile.py +76 -0
- simplibs/path/rules/filesystem/IsFile.py +76 -0
- simplibs/path/rules/filesystem/IsPathReadable.py +77 -0
- simplibs/path/rules/filesystem/IsPathWritable.py +82 -0
- simplibs/path/rules/filesystem/IsSymlink.py +71 -0
- simplibs/path/rules/filesystem/PathExists.py +75 -0
- simplibs/path/rules/filesystem/__init__.py +31 -0
- simplibs/path/rules/lexical/HasSuffix.py +146 -0
- simplibs/path/rules/lexical/IsAbsolutePath.py +74 -0
- simplibs/path/rules/lexical/IsHidden.py +92 -0
- simplibs/path/rules/lexical/IsRelativePath.py +74 -0
- simplibs/path/rules/lexical/IsValidFilename.py +136 -0
- simplibs/path/rules/lexical/IsWithin.py +116 -0
- simplibs/path/rules/lexical/NameIs.py +37 -0
- simplibs/path/rules/lexical/ParentNameIs.py +37 -0
- simplibs/path/rules/lexical/StemIs.py +36 -0
- simplibs/path/rules/lexical/__init__.py +33 -0
- simplibs_path-0.1.0.dist-info/METADATA +420 -0
- simplibs_path-0.1.0.dist-info/RECORD +128 -0
- simplibs_path-0.1.0.dist-info/WHEEL +5 -0
- simplibs_path-0.1.0.dist-info/licenses/LICENSE +21 -0
- simplibs_path-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .raise_collect_paths_not_a_dir_error import raise_collect_paths_not_a_dir_error
|
|
2
|
+
from .raise_collect_paths_not_found_error import raise_collect_paths_not_found_error
|
|
3
|
+
from .raise_collect_paths_root_unreadable_error import raise_collect_paths_root_unreadable_error
|
|
4
|
+
from .raise_ignore_names_not_names_error import raise_ignore_names_not_names_error
|
|
5
|
+
from .raise_nothing_to_collect_error import raise_nothing_to_collect_error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_DESIGN_NOTES = """
|
|
9
|
+
# Path Scanners Diagnostic Helpers Sub-Package
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
Internal diagnostic helpers raising structured exceptions for the tree
|
|
13
|
+
scanners in `scanners/`.
|
|
14
|
+
|
|
15
|
+
## Internal Components Registry
|
|
16
|
+
|
|
17
|
+
| Component | Type | Description |
|
|
18
|
+
| :-------------------------------------------- | :------- | :----------------------------------------------------------------- |
|
|
19
|
+
| `raise_collect_paths_not_a_dir_error` | Function | Raises `StateError` when the scan root is a file. |
|
|
20
|
+
| `raise_collect_paths_not_found_error` | Function | Raises `NotFoundError` when the scan root does not exist. |
|
|
21
|
+
| `raise_collect_paths_root_unreadable_error` | Function | Raises `AccessError` when the scan root is unreadable. |
|
|
22
|
+
| `raise_ignore_names_not_names_error` | Function | Raises `ParamError` when `ignore_names` is a single string. |
|
|
23
|
+
| `raise_nothing_to_collect_error` | Function | Raises `ParamError` when both collect flags are False. |
|
|
24
|
+
"""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
from simplibs.exception import StateError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def raise_collect_paths_not_a_dir_error(path: Path) -> NoReturn:
|
|
7
|
+
"""Raise StateError when a scan root is an existing file instead of a directory."""
|
|
8
|
+
|
|
9
|
+
# 1. Raise structured StateError wrapping a NotADirectoryError exception
|
|
10
|
+
raise StateError(
|
|
11
|
+
error_name="COLLECT_PATHS_NOT_A_DIRECTORY",
|
|
12
|
+
label=f"root directory '{path}'",
|
|
13
|
+
value=str(path),
|
|
14
|
+
problem=(
|
|
15
|
+
f"Cannot collect paths from '{path}'.",
|
|
16
|
+
"Expected a root directory, but an existing regular file was found at this location.",
|
|
17
|
+
),
|
|
18
|
+
expected="A path pointing to an existing directory that can be scanned.",
|
|
19
|
+
how_to_fix=(
|
|
20
|
+
"Provide a path to a directory instead of a regular file.",
|
|
21
|
+
f"Check if '{path}' was mistakenly passed instead of its parent directory.",
|
|
22
|
+
),
|
|
23
|
+
exception=NotADirectoryError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DESIGN_NOTES = """
|
|
28
|
+
# raise_collect_paths_not_a_dir_error — Scan Root Is Not a Directory
|
|
29
|
+
|
|
30
|
+
## Purpose
|
|
31
|
+
Counterpart of `raise_collect_paths_not_found_error`: the root exists, but
|
|
32
|
+
is a file. Renamed from `raise_ensure_collect_paths_not_a_dir_error`; the
|
|
33
|
+
card itself is unchanged.
|
|
34
|
+
"""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
from simplibs.exception import NotFoundError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def raise_collect_paths_not_found_error(path: Path) -> NoReturn:
|
|
7
|
+
"""Raise NotFoundError when a scan targets a non-existent root directory."""
|
|
8
|
+
|
|
9
|
+
# 1. Raise structured NotFoundError wrapping a FileNotFoundError exception
|
|
10
|
+
raise NotFoundError(
|
|
11
|
+
error_name="COLLECT_PATHS_ROOT_NOT_FOUND",
|
|
12
|
+
label=f"root directory '{path}'",
|
|
13
|
+
value=str(path),
|
|
14
|
+
problem=(
|
|
15
|
+
f"Cannot scan directory tree at '{path}'.",
|
|
16
|
+
"The specified root directory does not exist on disk.",
|
|
17
|
+
),
|
|
18
|
+
expected="An existing directory path to use as the root for path collection.",
|
|
19
|
+
how_to_fix=(
|
|
20
|
+
f"Verify that the path '{path}' exists before collecting paths from it.",
|
|
21
|
+
"Ensure parent directories are correctly spelled and accessible.",
|
|
22
|
+
),
|
|
23
|
+
exception=FileNotFoundError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DESIGN_NOTES = """
|
|
28
|
+
# raise_collect_paths_not_found_error — Missing Scan Root Diagnostic
|
|
29
|
+
|
|
30
|
+
## Purpose
|
|
31
|
+
Shared by every scanner that walks a tree (`collect_paths`, `collect_files`,
|
|
32
|
+
`collect_dirs`, `iter_paths`).
|
|
33
|
+
|
|
34
|
+
## Renamed From `raise_ensure_collect_paths_not_found_error`
|
|
35
|
+
Nothing is ensured here — the `ensure` prefix was a leftover. The how-to-fix
|
|
36
|
+
text no longer names `collect_paths` specifically, since four functions
|
|
37
|
+
share this card.
|
|
38
|
+
"""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
from simplibs.exception import AccessError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def raise_collect_paths_root_unreadable_error(
|
|
7
|
+
path: Path,
|
|
8
|
+
caught_exc: Exception,
|
|
9
|
+
) -> NoReturn:
|
|
10
|
+
"""Raise AccessError when the scan root exists but its entries cannot be listed."""
|
|
11
|
+
|
|
12
|
+
# 1. Raise structured AccessError wrapping the caught OS exception
|
|
13
|
+
raise AccessError(
|
|
14
|
+
error_name="COLLECT_PATHS_ROOT_UNREADABLE",
|
|
15
|
+
label=f"root directory '{path}'",
|
|
16
|
+
value=str(path),
|
|
17
|
+
problem=(
|
|
18
|
+
f"Cannot list the entries of '{path}'.",
|
|
19
|
+
f"OS reported: {caught_exc}",
|
|
20
|
+
),
|
|
21
|
+
expected="A root directory the current user is allowed to read.",
|
|
22
|
+
how_to_fix=(
|
|
23
|
+
f"Check read and execute permissions of '{path}'.",
|
|
24
|
+
"Scan a directory you have access to, or run with sufficient permissions.",
|
|
25
|
+
),
|
|
26
|
+
exception=type(caught_exc),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_DESIGN_NOTES = """
|
|
31
|
+
# raise_collect_paths_root_unreadable_error — Unreadable Scan Root
|
|
32
|
+
|
|
33
|
+
## Purpose
|
|
34
|
+
Unreadable *sub*directories are skipped silently while walking — a normal
|
|
35
|
+
tree always contains a few. An unreadable *root* is different: skipping it
|
|
36
|
+
would return an empty result that looks exactly like "nothing found". The
|
|
37
|
+
scanners check the root up front and report this card instead.
|
|
38
|
+
"""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import NoReturn
|
|
2
|
+
from simplibs.exception import ParamError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def raise_ignore_names_not_names_error(value: str) -> NoReturn:
|
|
6
|
+
"""Raise ParamError when `ignore_names` is a single string instead of a collection."""
|
|
7
|
+
|
|
8
|
+
# 1. Raise structured ParamError wrapping a TypeError
|
|
9
|
+
raise ParamError(
|
|
10
|
+
error_name="IGNORE_NAMES_NOT_A_COLLECTION",
|
|
11
|
+
label="ignore_names",
|
|
12
|
+
value=value,
|
|
13
|
+
problem=(
|
|
14
|
+
f"A single string {value!r} was passed instead of a collection of names.",
|
|
15
|
+
"A string is iterable, so it would be silently split into single characters.",
|
|
16
|
+
),
|
|
17
|
+
expected="A collection of entry names, e.g. a tuple, list, set or frozenset of strings.",
|
|
18
|
+
how_to_fix=(
|
|
19
|
+
f"Wrap the name in a collection: ignore_names=({value!r},).",
|
|
20
|
+
"Extend the defaults with a union: DEFAULT_IGNORE_NAMES | {'my_dir'}.",
|
|
21
|
+
),
|
|
22
|
+
exception=TypeError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DESIGN_NOTES = """
|
|
27
|
+
# raise_ignore_names_not_names_error — The Single-String Trap
|
|
28
|
+
|
|
29
|
+
## Purpose
|
|
30
|
+
`str` is a `Collection[str]`, so the `@validate_call` annotation alone
|
|
31
|
+
cannot tell `ignore_names=".git"` from `ignore_names=(".git",)`. Without
|
|
32
|
+
this explicit check the first form would be read as the names
|
|
33
|
+
`{".", "g", "i", "t"}` — ignoring nothing useful and hiding every entry
|
|
34
|
+
named `g`, `i` or `t`. The scanners check for `str` in their body and
|
|
35
|
+
raise this card instead.
|
|
36
|
+
"""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import NoReturn
|
|
2
|
+
from simplibs.exception import ParamError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def raise_nothing_to_collect_error() -> NoReturn:
|
|
6
|
+
"""Raise ParamError when both `collect_files` and `collect_dirs` are False."""
|
|
7
|
+
|
|
8
|
+
# 1. Raise structured ParamError wrapping a ValueError
|
|
9
|
+
raise ParamError(
|
|
10
|
+
error_name="NOTHING_TO_COLLECT",
|
|
11
|
+
label="collect_files, collect_dirs",
|
|
12
|
+
value="collect_files=False, collect_dirs=False",
|
|
13
|
+
problem=(
|
|
14
|
+
"Both collect_files and collect_dirs are False.",
|
|
15
|
+
"Nothing could ever be collected, so the scan would be pointless.",
|
|
16
|
+
),
|
|
17
|
+
expected="At least one of collect_files or collect_dirs set to True.",
|
|
18
|
+
how_to_fix=(
|
|
19
|
+
"Set collect_files=True to collect files.",
|
|
20
|
+
"Set collect_dirs=True to collect directories.",
|
|
21
|
+
),
|
|
22
|
+
exception=ValueError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DESIGN_NOTES = """
|
|
27
|
+
# raise_nothing_to_collect_error — Contradictory Collect Flags
|
|
28
|
+
|
|
29
|
+
## Purpose
|
|
30
|
+
An earlier `collect_paths` returned `[]` for this combination before even
|
|
31
|
+
checking the root — silently, and even for a root that does not exist. Both
|
|
32
|
+
flags off is almost certainly a caller mistake, so it is now reported
|
|
33
|
+
immediately instead of producing an empty result that looks like success.
|
|
34
|
+
"""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from simplibs.validate import validate_call
|
|
4
|
+
# Outers
|
|
5
|
+
from ...annotations import PathLike, PathPredicate, RecursionDepth
|
|
6
|
+
# Inners
|
|
7
|
+
from ._helpers import scan_paths
|
|
8
|
+
from .ignore_names import DEFAULT_IGNORE_NAMES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@validate_call
|
|
12
|
+
def collect_dirs(
|
|
13
|
+
root_dir: PathLike,
|
|
14
|
+
*,
|
|
15
|
+
recursive: RecursionDepth = True,
|
|
16
|
+
include_filter: PathPredicate | None = None,
|
|
17
|
+
exclude_filter: PathPredicate | None = None,
|
|
18
|
+
ignore_names: Collection[str] | None = DEFAULT_IGNORE_NAMES,
|
|
19
|
+
) -> list[Path]:
|
|
20
|
+
"""Collect only the directories of a directory tree into a sorted list.
|
|
21
|
+
|
|
22
|
+
The same as `collect_paths(..., collect_files=False, collect_dirs=True)`,
|
|
23
|
+
with the type flags fixed. See `collect_paths` for the meaning of every
|
|
24
|
+
parameter, the sorting and the raised errors.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
root_dir: The root directory to scan (the root itself is not included).
|
|
28
|
+
recursive: `True` for unlimited depth, `False` for the root's own
|
|
29
|
+
entries only, or an `int` for a maximum depth.
|
|
30
|
+
include_filter: A `Rule` or `(Path) -> bool` callable a directory must satisfy.
|
|
31
|
+
exclude_filter: A `Rule` or `(Path) -> bool` callable that skips a directory.
|
|
32
|
+
ignore_names: Entry names or glob patterns to skip entirely.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The matching absolute directory `Path`s, sorted.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# 1. Run the shared scan with the type flags fixed, then sort
|
|
39
|
+
return sorted(
|
|
40
|
+
scan_paths(
|
|
41
|
+
root_dir,
|
|
42
|
+
recursive=recursive,
|
|
43
|
+
include_filter=include_filter,
|
|
44
|
+
exclude_filter=exclude_filter,
|
|
45
|
+
ignore_names=ignore_names,
|
|
46
|
+
collect_files=False,
|
|
47
|
+
collect_dirs=True,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_DESIGN_NOTES = """
|
|
53
|
+
# collect_dirs — Directories Only
|
|
54
|
+
|
|
55
|
+
## Purpose
|
|
56
|
+
The directory counterpart of `collect_files` — for "which folders exist
|
|
57
|
+
under here" steps (mirroring a structure, picking project roots).
|
|
58
|
+
|
|
59
|
+
## The Root Is Not Included
|
|
60
|
+
Like every scanner, it lists what is *inside* `root_dir`. A caller who wants
|
|
61
|
+
the root as well adds it explicitly.
|
|
62
|
+
"""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from simplibs.validate import validate_call
|
|
4
|
+
# Outers
|
|
5
|
+
from ...annotations import PathLike, PathPredicate, RecursionDepth
|
|
6
|
+
# Inners
|
|
7
|
+
from ._helpers import scan_paths
|
|
8
|
+
from .ignore_names import DEFAULT_IGNORE_NAMES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@validate_call
|
|
12
|
+
def collect_files(
|
|
13
|
+
root_dir: PathLike,
|
|
14
|
+
*,
|
|
15
|
+
recursive: RecursionDepth = True,
|
|
16
|
+
include_filter: PathPredicate | None = None,
|
|
17
|
+
exclude_filter: PathPredicate | None = None,
|
|
18
|
+
ignore_names: Collection[str] | None = DEFAULT_IGNORE_NAMES,
|
|
19
|
+
) -> list[Path]:
|
|
20
|
+
"""Collect only the files of a directory tree into a sorted list.
|
|
21
|
+
|
|
22
|
+
The same as `collect_paths(..., collect_files=True, collect_dirs=False)`,
|
|
23
|
+
with the type flags fixed. See `collect_paths` for the meaning of every
|
|
24
|
+
parameter, the sorting and the raised errors.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
root_dir: The root directory to scan.
|
|
28
|
+
recursive: `True` for unlimited depth, `False` for the root's own
|
|
29
|
+
entries only, or an `int` for a maximum depth.
|
|
30
|
+
include_filter: A `Rule` or `(Path) -> bool` callable a file must satisfy.
|
|
31
|
+
exclude_filter: A `Rule` or `(Path) -> bool` callable that skips a file.
|
|
32
|
+
ignore_names: Entry names or glob patterns to skip entirely.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The matching absolute file `Path`s, sorted.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# 1. Run the shared scan with the type flags fixed, then sort
|
|
39
|
+
return sorted(
|
|
40
|
+
scan_paths(
|
|
41
|
+
root_dir,
|
|
42
|
+
recursive=recursive,
|
|
43
|
+
include_filter=include_filter,
|
|
44
|
+
exclude_filter=exclude_filter,
|
|
45
|
+
ignore_names=ignore_names,
|
|
46
|
+
collect_files=True,
|
|
47
|
+
collect_dirs=False,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_DESIGN_NOTES = """
|
|
53
|
+
# collect_files — Files Only
|
|
54
|
+
|
|
55
|
+
## Why a Real Function, Not a `functools.partial`
|
|
56
|
+
The `CollectFiles` action used to be built from
|
|
57
|
+
`partial(collect_paths, collect_files=True, collect_dirs=False)`. A partial
|
|
58
|
+
only changes the *defaults* — the flags stayed in the signature, so
|
|
59
|
+
`CollectFiles(collect_dirs=True)` quietly broke the "locked" promise, and
|
|
60
|
+
`create_action` needs a `__name__` that a partial does not have.
|
|
61
|
+
|
|
62
|
+
A function with the flags removed from its signature cannot be mis-used,
|
|
63
|
+
has a truthful docstring, and wraps cleanly into an action.
|
|
64
|
+
"""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from simplibs.validate import validate_call
|
|
4
|
+
# Outers
|
|
5
|
+
from ...annotations import PathLike, PathPredicate, RecursionDepth
|
|
6
|
+
# Inners
|
|
7
|
+
from ._helpers import scan_paths
|
|
8
|
+
from .ignore_names import DEFAULT_IGNORE_NAMES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@validate_call
|
|
12
|
+
def collect_paths(
|
|
13
|
+
root_dir: PathLike,
|
|
14
|
+
*,
|
|
15
|
+
recursive: RecursionDepth = True,
|
|
16
|
+
include_filter: PathPredicate | None = None,
|
|
17
|
+
exclude_filter: PathPredicate | None = None,
|
|
18
|
+
ignore_names: Collection[str] | None = DEFAULT_IGNORE_NAMES,
|
|
19
|
+
collect_files: bool = True,
|
|
20
|
+
collect_dirs: bool = False,
|
|
21
|
+
) -> list[Path]:
|
|
22
|
+
"""Scan a directory tree and collect the matching paths into a sorted list.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
root_dir: The root directory to scan.
|
|
26
|
+
recursive: `True` for unlimited depth, `False` for the root's own
|
|
27
|
+
entries only, or an `int` for a maximum depth (0 = root's own
|
|
28
|
+
entries, 1 = one level deeper, ...).
|
|
29
|
+
include_filter: A `Rule` or `(Path) -> bool` callable. A path must
|
|
30
|
+
satisfy it to be collected.
|
|
31
|
+
exclude_filter: A `Rule` or `(Path) -> bool` callable. A path that
|
|
32
|
+
satisfies it is skipped (checked before `include_filter`).
|
|
33
|
+
Excluding a directory does not hide what is inside it — only
|
|
34
|
+
`ignore_names` prunes a subtree.
|
|
35
|
+
ignore_names: Entry names (files and directories) or glob patterns
|
|
36
|
+
(`*.egg-info`) to skip entirely — an ignored directory is not
|
|
37
|
+
entered. Defaults to `DEFAULT_IGNORE_NAMES`; extend it with
|
|
38
|
+
`DEFAULT_IGNORE_NAMES | {"my_dir"}`. Pass a collection, never a
|
|
39
|
+
bare string; `None` ignores nothing.
|
|
40
|
+
collect_files: Whether files are collected (default True).
|
|
41
|
+
collect_dirs: Whether directories are collected (default False).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The matching absolute `Path`s, sorted by path parts (a directory's
|
|
45
|
+
contents sort right after the directory itself). Symlinks to files
|
|
46
|
+
and directories are collected as what they point to; broken
|
|
47
|
+
symlinks and special files never are.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ParamError: If both collect flags are False, or `ignore_names` is a string.
|
|
51
|
+
NotFoundError: If `root_dir` does not exist.
|
|
52
|
+
StateError: If `root_dir` is not a directory.
|
|
53
|
+
AccessError: If `root_dir` cannot be listed.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# 1. Run the shared scan and sort what it finds
|
|
57
|
+
return sorted(
|
|
58
|
+
scan_paths(
|
|
59
|
+
root_dir,
|
|
60
|
+
recursive=recursive,
|
|
61
|
+
include_filter=include_filter,
|
|
62
|
+
exclude_filter=exclude_filter,
|
|
63
|
+
ignore_names=ignore_names,
|
|
64
|
+
collect_files=collect_files,
|
|
65
|
+
collect_dirs=collect_dirs,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_DESIGN_NOTES = """
|
|
71
|
+
# collect_paths — the General Scanner
|
|
72
|
+
|
|
73
|
+
## Purpose
|
|
74
|
+
The list-returning scanner every extraction pipeline starts from, with both
|
|
75
|
+
type flags exposed. `collect_files` and `collect_dirs` are its fixed-flag
|
|
76
|
+
presets.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 1. What Changed From the Previous Version
|
|
81
|
+
* **`ignore_dirs` → `ignore_names`**, applied to files too, with glob
|
|
82
|
+
patterns, and a narrower default (no more silently hidden `build/`, `dist/`).
|
|
83
|
+
* **Broken symlinks and special files are no longer collected.** A positive
|
|
84
|
+
type check replaces two negative ones that an entry of neither kind slipped
|
|
85
|
+
through.
|
|
86
|
+
* **Both flags off raises** instead of returning `[]` before even checking
|
|
87
|
+
the root.
|
|
88
|
+
* **An unreadable root raises** instead of looking like an empty tree.
|
|
89
|
+
* **One `scandir` per directory** instead of up to five `stat` calls per entry.
|
|
90
|
+
* **`Collection`, not `Iterable`, for `ignore_names`.** `@validate_call`
|
|
91
|
+
checks `Iterable[...]` by iterating it — which would exhaust a generator
|
|
92
|
+
argument before the function ever saw it.
|
|
93
|
+
|
|
94
|
+
## 2. Sorted by Path Parts
|
|
95
|
+
`Path` objects compare part by part, so `a/b.txt` sorts after the directory
|
|
96
|
+
`a` and before `a.txt`. That is tree order, and on Windows it is
|
|
97
|
+
case-insensitive, matching how Explorer lists a folder.
|
|
98
|
+
|
|
99
|
+
## 3. Absolute Paths
|
|
100
|
+
Entries are the normalized root joined with their relative part, so the
|
|
101
|
+
result needs no further `normalize_path`.
|
|
102
|
+
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from simplibs.validate import validate_call
|
|
4
|
+
# Outers
|
|
5
|
+
from ...annotations import PathPredicate
|
|
6
|
+
# Inners
|
|
7
|
+
from ._helpers import as_path_predicate
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@validate_call
|
|
11
|
+
def filter_paths(
|
|
12
|
+
paths: Collection[Path],
|
|
13
|
+
rule: PathPredicate,
|
|
14
|
+
) -> list[Path]:
|
|
15
|
+
"""Keep only the paths that satisfy a Rule or callable predicate.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
paths: The paths to filter — a list, tuple or set (not a generator,
|
|
19
|
+
which cannot be validated without being consumed; wrap it in
|
|
20
|
+
`list(...)` first).
|
|
21
|
+
rule: A `Rule` or `(Path) -> bool` callable. A path is kept when it
|
|
22
|
+
returns a truthy value.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A new list with the paths satisfying `rule`, in their original order.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# 1. Resolve the predicate once, outside the loop
|
|
29
|
+
predicate = as_path_predicate(rule)
|
|
30
|
+
|
|
31
|
+
# 2. Keep only the paths satisfying it
|
|
32
|
+
return [path for path in paths if predicate(path)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_DESIGN_NOTES = """
|
|
36
|
+
# filter_paths — Filter an Existing List of Paths
|
|
37
|
+
|
|
38
|
+
## Purpose
|
|
39
|
+
`collect_paths` filters while scanning. `filter_paths` filters a list you
|
|
40
|
+
already have — the result of an earlier step, or paths from elsewhere —
|
|
41
|
+
which is what `CollectFiles() >> FilterPaths(rule=...)` needs.
|
|
42
|
+
|
|
43
|
+
## What Changed
|
|
44
|
+
* **`rule` is required.** With `rule=None` the old version just copied the
|
|
45
|
+
list — a no-op that, as an action, could only be a mistake.
|
|
46
|
+
* **`Collection[Path]` instead of `Iterable[Path]`.** `@validate_call`
|
|
47
|
+
checks `Iterable[...]` by iterating it, which would silently exhaust a
|
|
48
|
+
generator argument and then filter an empty list. A collection can be
|
|
49
|
+
checked and still used.
|
|
50
|
+
* **`as_path_predicate`** replaces `getattr(rule, "is_valid", rule)`.
|
|
51
|
+
"""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
# Inners
|
|
3
|
+
from .IGNORE_BUILD import IGNORE_BUILD
|
|
4
|
+
from .IGNORE_ENV import IGNORE_ENV
|
|
5
|
+
from .IGNORE_IDE import IGNORE_IDE
|
|
6
|
+
from .IGNORE_SYSTEM import IGNORE_SYSTEM
|
|
7
|
+
from .IGNORE_VCS import IGNORE_VCS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_IGNORE_NAMES: Final[frozenset[str]] = (
|
|
11
|
+
IGNORE_VCS
|
|
12
|
+
| IGNORE_ENV
|
|
13
|
+
| IGNORE_BUILD
|
|
14
|
+
| IGNORE_IDE
|
|
15
|
+
| IGNORE_SYSTEM
|
|
16
|
+
)
|
|
17
|
+
"""The names every scan skips unless told otherwise.
|
|
18
|
+
|
|
19
|
+
Extend it with a union: `DEFAULT_IGNORE_NAMES | {"my_dir"}`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_DESIGN_NOTES = """
|
|
24
|
+
# DEFAULT_IGNORE_NAMES — the Scanners' Default
|
|
25
|
+
|
|
26
|
+
## Purpose
|
|
27
|
+
The union of every group that is safe to hide without surprising anyone:
|
|
28
|
+
version control, environments, generated artifacts with unmistakable names,
|
|
29
|
+
IDE settings and OS housekeeping. `IGNORE_GENERIC` is deliberately not
|
|
30
|
+
included (see its notes).
|
|
31
|
+
|
|
32
|
+
## A `frozenset` So the Default Is Safe
|
|
33
|
+
A mutable default shared by every call could be changed by accident
|
|
34
|
+
(`DEFAULT_IGNORE_NAMES.add(...)`) and affect all later scans. A frozenset
|
|
35
|
+
cannot, and `|` still builds a new, extended one.
|
|
36
|
+
"""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
IGNORE_BUILD: Final[frozenset[str]] = frozenset({
|
|
5
|
+
"*.egg-info",
|
|
6
|
+
".cache",
|
|
7
|
+
".coverage",
|
|
8
|
+
".coverage.*",
|
|
9
|
+
".eggs",
|
|
10
|
+
".gradle",
|
|
11
|
+
".hypothesis",
|
|
12
|
+
".ipynb_checkpoints",
|
|
13
|
+
".mypy_cache",
|
|
14
|
+
".next",
|
|
15
|
+
".nox",
|
|
16
|
+
".nuxt",
|
|
17
|
+
".pytest_cache",
|
|
18
|
+
".pytype",
|
|
19
|
+
".ruff_cache",
|
|
20
|
+
".serverless",
|
|
21
|
+
".terraform",
|
|
22
|
+
".tox",
|
|
23
|
+
"__pycache__",
|
|
24
|
+
"htmlcov",
|
|
25
|
+
})
|
|
26
|
+
"""Caches, coverage data and tool output that are unambiguous by name.
|
|
27
|
+
|
|
28
|
+
Entries containing `*`, `?` or `[` are glob patterns (`*.egg-info`).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_DESIGN_NOTES = """
|
|
33
|
+
# IGNORE_BUILD — Generated Artifacts With Unmistakable Names
|
|
34
|
+
|
|
35
|
+
## Purpose
|
|
36
|
+
Everything here is produced by a tool, never written by hand, and its name
|
|
37
|
+
says so (`__pycache__`, `.mypy_cache`, `htmlcov`).
|
|
38
|
+
|
|
39
|
+
## What Changed
|
|
40
|
+
* **`build`, `dist`, `site`, `_build` moved to `IGNORE_GENERIC`.** They are
|
|
41
|
+
real output folders in many projects but also ordinary source folders in
|
|
42
|
+
others; hiding them by default silently dropped legitimate content.
|
|
43
|
+
* **`.coverage` now works** (it is a file) and `.coverage.*` covers the
|
|
44
|
+
parallel-run files; `*.egg-info` is a glob, which the exact-name matching
|
|
45
|
+
of the old version could not express.
|
|
46
|
+
* Added `.cache`, `.eggs`, `.gradle`, `.hypothesis`, `.ipynb_checkpoints`,
|
|
47
|
+
`.pytype`, `.terraform`.
|
|
48
|
+
"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
IGNORE_ENV: Final[frozenset[str]] = frozenset({
|
|
5
|
+
".direnv",
|
|
6
|
+
".env",
|
|
7
|
+
".poetry",
|
|
8
|
+
".venv",
|
|
9
|
+
".virtualenv",
|
|
10
|
+
"__pypackages__",
|
|
11
|
+
"node_modules",
|
|
12
|
+
"venv",
|
|
13
|
+
})
|
|
14
|
+
"""Virtual environments and installed dependencies — plus the `.env` secrets file."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_DESIGN_NOTES = """
|
|
18
|
+
# IGNORE_ENV — Environments and Dependencies
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
Installed third-party code (`venv`, `node_modules`, `__pypackages__`) dwarfs
|
|
22
|
+
the project itself and is never part of its sources.
|
|
23
|
+
|
|
24
|
+
## What Changed
|
|
25
|
+
* **`env` moved to `IGNORE_GENERIC`.** Too common as an ordinary folder name
|
|
26
|
+
(`src/env/`, `config/env/`) to be hidden by default.
|
|
27
|
+
* **`.env` stays, and now works.** It is usually a *file* (dotenv secrets).
|
|
28
|
+
The old directory-only ignore never matched it; names now apply to files
|
|
29
|
+
too, so a scan no longer picks up secrets by accident.
|
|
30
|
+
* Added `.direnv` and `__pypackages__` (PEP 582).
|
|
31
|
+
"""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
IGNORE_GENERIC: Final[frozenset[str]] = frozenset({
|
|
5
|
+
"_build",
|
|
6
|
+
"build",
|
|
7
|
+
"dist",
|
|
8
|
+
"env",
|
|
9
|
+
"out",
|
|
10
|
+
"site",
|
|
11
|
+
"target",
|
|
12
|
+
})
|
|
13
|
+
"""Common output-folder names that can also be ordinary source folders.
|
|
14
|
+
|
|
15
|
+
NOT part of `DEFAULT_IGNORE_NAMES` — opt in explicitly:
|
|
16
|
+
`ignore_names=DEFAULT_IGNORE_NAMES | IGNORE_GENERIC`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_DESIGN_NOTES = """
|
|
21
|
+
# IGNORE_GENERIC — Opt-In, Because the Names Are Ambiguous
|
|
22
|
+
|
|
23
|
+
## Purpose
|
|
24
|
+
`build/`, `dist/`, `site/`, `target/` are the output folders of setuptools,
|
|
25
|
+
MkDocs, Cargo and Maven — but `src/site/`, `docs/build-notes/`-style
|
|
26
|
+
folders and a package literally named `env` exist too. A scan that hides
|
|
27
|
+
them by default loses content without saying so, which is worse than a scan
|
|
28
|
+
that is a bit noisy. This set exists so that dropping them is a conscious,
|
|
29
|
+
one-expression choice.
|
|
30
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
IGNORE_IDE: Final[frozenset[str]] = frozenset({
|
|
5
|
+
".fleet", # JetBrains Fleet
|
|
6
|
+
".idea", # PyCharm / JetBrains IDEs
|
|
7
|
+
".settings", # Eclipse
|
|
8
|
+
".vs", # Visual Studio
|
|
9
|
+
".vscode", # Visual Studio Code
|
|
10
|
+
})
|
|
11
|
+
"""Editor and IDE project settings."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_DESIGN_NOTES = """
|
|
15
|
+
# IGNORE_IDE — Editor Settings
|
|
16
|
+
|
|
17
|
+
## Purpose
|
|
18
|
+
Per-project IDE state: personal, machine-specific and not part of the
|
|
19
|
+
sources. Only `.fleet` is new compared to the former version.
|
|
20
|
+
"""
|