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.
Files changed (128) hide show
  1. simplibs/path/__init__.py +118 -0
  2. simplibs/path/actions/ChangeSuffix.py +23 -0
  3. simplibs/path/actions/CollectDirs.py +24 -0
  4. simplibs/path/actions/CollectFiles.py +25 -0
  5. simplibs/path/actions/CollectPaths.py +24 -0
  6. simplibs/path/actions/EnsureDir.py +22 -0
  7. simplibs/path/actions/EnsureFile.py +19 -0
  8. simplibs/path/actions/EnsureParentDir.py +20 -0
  9. simplibs/path/actions/FilterPaths.py +27 -0
  10. simplibs/path/actions/MakeUniquePath.py +22 -0
  11. simplibs/path/actions/NormalizeEntryPath.py +20 -0
  12. simplibs/path/actions/NormalizePath.py +23 -0
  13. simplibs/path/actions/RebasePath.py +24 -0
  14. simplibs/path/actions/RelativizePath.py +22 -0
  15. simplibs/path/actions/__init__.py +68 -0
  16. simplibs/path/annotations/PathLike.py +43 -0
  17. simplibs/path/annotations/PathPredicate.py +39 -0
  18. simplibs/path/annotations/RecursionDepth.py +33 -0
  19. simplibs/path/annotations/__init__.py +21 -0
  20. simplibs/path/logic/__init__.py +41 -0
  21. simplibs/path/logic/checks/__init__.py +29 -0
  22. simplibs/path/logic/checks/_helpers/__init__.py +21 -0
  23. simplibs/path/logic/checks/_helpers/_find_length_problem.py +48 -0
  24. simplibs/path/logic/checks/_helpers/_find_windows_name_problem.py +55 -0
  25. simplibs/path/logic/checks/_helpers/can_create_inside.py +39 -0
  26. simplibs/path/logic/checks/_helpers/constants/MAX_NAME_LENGTH.py +18 -0
  27. simplibs/path/logic/checks/_helpers/constants/WINDOWS_FORBIDDEN_CHARS.py +25 -0
  28. simplibs/path/logic/checks/_helpers/constants/WINDOWS_RESERVED_NAMES.py +27 -0
  29. simplibs/path/logic/checks/_helpers/constants/__init__.py +20 -0
  30. simplibs/path/logic/checks/_helpers/find_filename_problem.py +75 -0
  31. simplibs/path/logic/checks/_helpers/to_path.py +77 -0
  32. simplibs/path/logic/checks/is_empty_dir.py +59 -0
  33. simplibs/path/logic/checks/is_hidden_path.py +78 -0
  34. simplibs/path/logic/checks/is_path_readable.py +73 -0
  35. simplibs/path/logic/checks/is_path_writable.py +90 -0
  36. simplibs/path/logic/checks/is_valid_filename.py +47 -0
  37. simplibs/path/logic/checks/is_within.py +88 -0
  38. simplibs/path/logic/ensure/__init__.py +22 -0
  39. simplibs/path/logic/ensure/_validations/__init__.py +25 -0
  40. simplibs/path/logic/ensure/_validations/raise_ensure_dir_error.py +40 -0
  41. simplibs/path/logic/ensure/_validations/raise_ensure_dir_not_a_dir_error.py +36 -0
  42. simplibs/path/logic/ensure/_validations/raise_ensure_file_create_error.py +39 -0
  43. simplibs/path/logic/ensure/_validations/raise_ensure_file_not_a_file_error.py +36 -0
  44. simplibs/path/logic/ensure/_validations/raise_ensure_file_not_found_error.py +36 -0
  45. simplibs/path/logic/ensure/ensure_dir.py +83 -0
  46. simplibs/path/logic/ensure/ensure_file.py +103 -0
  47. simplibs/path/logic/ensure/ensure_parent_dir.py +58 -0
  48. simplibs/path/logic/scanners/__init__.py +35 -0
  49. simplibs/path/logic/scanners/_helpers/ScanEntry.py +36 -0
  50. simplibs/path/logic/scanners/_helpers/__init__.py +30 -0
  51. simplibs/path/logic/scanners/_helpers/as_path_predicate.py +38 -0
  52. simplibs/path/logic/scanners/_helpers/build_ignore_matcher.py +88 -0
  53. simplibs/path/logic/scanners/_helpers/generate_paths.py +74 -0
  54. simplibs/path/logic/scanners/_helpers/is_link_like.py +45 -0
  55. simplibs/path/logic/scanners/_helpers/resolve_scan_root.py +66 -0
  56. simplibs/path/logic/scanners/_helpers/scan_paths.py +92 -0
  57. simplibs/path/logic/scanners/_helpers/walk_tree.py +118 -0
  58. simplibs/path/logic/scanners/_validations/__init__.py +24 -0
  59. simplibs/path/logic/scanners/_validations/raise_collect_paths_not_a_dir_error.py +34 -0
  60. simplibs/path/logic/scanners/_validations/raise_collect_paths_not_found_error.py +38 -0
  61. simplibs/path/logic/scanners/_validations/raise_collect_paths_root_unreadable_error.py +38 -0
  62. simplibs/path/logic/scanners/_validations/raise_ignore_names_not_names_error.py +36 -0
  63. simplibs/path/logic/scanners/_validations/raise_nothing_to_collect_error.py +34 -0
  64. simplibs/path/logic/scanners/collect_dirs.py +62 -0
  65. simplibs/path/logic/scanners/collect_files.py +64 -0
  66. simplibs/path/logic/scanners/collect_paths.py +102 -0
  67. simplibs/path/logic/scanners/filter_paths.py +51 -0
  68. simplibs/path/logic/scanners/ignore_names/DEFAULT_IGNORE_NAMES.py +36 -0
  69. simplibs/path/logic/scanners/ignore_names/IGNORE_BUILD.py +48 -0
  70. simplibs/path/logic/scanners/ignore_names/IGNORE_ENV.py +31 -0
  71. simplibs/path/logic/scanners/ignore_names/IGNORE_GENERIC.py +30 -0
  72. simplibs/path/logic/scanners/ignore_names/IGNORE_IDE.py +20 -0
  73. simplibs/path/logic/scanners/ignore_names/IGNORE_SYSTEM.py +30 -0
  74. simplibs/path/logic/scanners/ignore_names/IGNORE_VCS.py +24 -0
  75. simplibs/path/logic/scanners/ignore_names/__init__.py +30 -0
  76. simplibs/path/logic/scanners/iter_paths.py +91 -0
  77. simplibs/path/logic/transformers/__init__.py +27 -0
  78. simplibs/path/logic/transformers/_helpers/__init__.py +17 -0
  79. simplibs/path/logic/transformers/_helpers/is_valid_suffix.py +37 -0
  80. simplibs/path/logic/transformers/_helpers/split_name.py +46 -0
  81. simplibs/path/logic/transformers/_validations/__init__.py +24 -0
  82. simplibs/path/logic/transformers/_validations/raise_invalid_suffix_error.py +36 -0
  83. simplibs/path/logic/transformers/_validations/raise_normalize_path_error.py +44 -0
  84. simplibs/path/logic/transformers/_validations/raise_path_has_no_name_error.py +36 -0
  85. simplibs/path/logic/transformers/_validations/raise_path_not_within_root_error.py +43 -0
  86. simplibs/path/logic/transformers/_validations/raise_unique_path_exhausted_error.py +39 -0
  87. simplibs/path/logic/transformers/change_suffix.py +77 -0
  88. simplibs/path/logic/transformers/make_unique_path.py +88 -0
  89. simplibs/path/logic/transformers/normalize_entry_path.py +67 -0
  90. simplibs/path/logic/transformers/normalize_path.py +85 -0
  91. simplibs/path/logic/transformers/rebase_path.py +78 -0
  92. simplibs/path/logic/transformers/relativize_path.py +56 -0
  93. simplibs/path/rules/__init__.py +66 -0
  94. simplibs/path/rules/_helpers/PathPartIs.py +152 -0
  95. simplibs/path/rules/_helpers/__init__.py +21 -0
  96. simplibs/path/rules/_helpers/build_not_a_path_exception.py +66 -0
  97. simplibs/path/rules/_helpers/build_path_rule_exception.py +71 -0
  98. simplibs/path/rules/_helpers/check_path.py +48 -0
  99. simplibs/path/rules/_validations/__init__.py +24 -0
  100. simplibs/path/rules/_validations/raise_has_suffix_invalid_suffix_error.py +35 -0
  101. simplibs/path/rules/_validations/raise_has_suffix_no_suffixes_error.py +31 -0
  102. simplibs/path/rules/_validations/raise_rule_param_not_bool_error.py +37 -0
  103. simplibs/path/rules/_validations/raise_rule_param_not_callable_error.py +36 -0
  104. simplibs/path/rules/_validations/raise_rule_param_not_path_error.py +37 -0
  105. simplibs/path/rules/filesystem/IsDir.py +70 -0
  106. simplibs/path/rules/filesystem/IsEmptyDir.py +76 -0
  107. simplibs/path/rules/filesystem/IsEmptyFile.py +76 -0
  108. simplibs/path/rules/filesystem/IsFile.py +76 -0
  109. simplibs/path/rules/filesystem/IsPathReadable.py +77 -0
  110. simplibs/path/rules/filesystem/IsPathWritable.py +82 -0
  111. simplibs/path/rules/filesystem/IsSymlink.py +71 -0
  112. simplibs/path/rules/filesystem/PathExists.py +75 -0
  113. simplibs/path/rules/filesystem/__init__.py +31 -0
  114. simplibs/path/rules/lexical/HasSuffix.py +146 -0
  115. simplibs/path/rules/lexical/IsAbsolutePath.py +74 -0
  116. simplibs/path/rules/lexical/IsHidden.py +92 -0
  117. simplibs/path/rules/lexical/IsRelativePath.py +74 -0
  118. simplibs/path/rules/lexical/IsValidFilename.py +136 -0
  119. simplibs/path/rules/lexical/IsWithin.py +116 -0
  120. simplibs/path/rules/lexical/NameIs.py +37 -0
  121. simplibs/path/rules/lexical/ParentNameIs.py +37 -0
  122. simplibs/path/rules/lexical/StemIs.py +36 -0
  123. simplibs/path/rules/lexical/__init__.py +33 -0
  124. simplibs_path-0.1.0.dist-info/METADATA +420 -0
  125. simplibs_path-0.1.0.dist-info/RECORD +128 -0
  126. simplibs_path-0.1.0.dist-info/WHEEL +5 -0
  127. simplibs_path-0.1.0.dist-info/licenses/LICENSE +21 -0
  128. 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
+ """