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,77 @@
1
+ from typing import Any
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ...logic.checks._helpers import to_path
5
+ from ...logic.checks import is_path_readable
6
+ from .._helpers import build_path_rule_exception
7
+
8
+
9
+ class IsPathReadable(Rule):
10
+ """Path must be a file that can be read or a directory that can be listed.
11
+
12
+ Rule:
13
+ is_path_readable(value)
14
+
15
+ Example:
16
+ validate(source, IsPathReadable())
17
+ """
18
+
19
+ __slots__ = ()
20
+
21
+ # ----------------------------------------------------------------------
22
+ # Rule definition
23
+ # ----------------------------------------------------------------------
24
+ def is_valid(self, value: Any) -> bool:
25
+
26
+ # 1. Anything that cannot be turned into a path fails
27
+ path = to_path(value)
28
+ if path is None:
29
+ return False
30
+
31
+ # 2. Delegate to the plain predicate (its own validation is skipped — the input is checked)
32
+ return is_path_readable(path, _validate_call=False)
33
+
34
+ # ----------------------------------------------------------------------
35
+ # Description definition
36
+ # ----------------------------------------------------------------------
37
+ def describe(self) -> str:
38
+
39
+ # 1. Fixed noun phrase
40
+ return "readable path"
41
+
42
+ # ----------------------------------------------------------------------
43
+ # Exception definition
44
+ # ----------------------------------------------------------------------
45
+ def build_exception(
46
+ self,
47
+ value: Any,
48
+ value_name: str | None = None,
49
+ context: str | None = None,
50
+ ) -> Exception:
51
+
52
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
53
+ return build_path_rule_exception(
54
+ error_name="IS_PATH_READABLE_ERROR",
55
+ expected="a readable file or directory",
56
+ value=value,
57
+ value_name=value_name,
58
+ context=context,
59
+ problem=f"Path {value!r} is not readable (it is missing, not a regular file or directory, or access is denied).",
60
+ how_to_fix="Provide a path to a file or directory you have permission to read.",
61
+ )
62
+
63
+
64
+ _DESIGN_NOTES = """
65
+ # IsPathReadable — Readable path
66
+
67
+ ## Purpose
68
+ A precondition for any step that reads: the failure card says *readable*, so
69
+ a permission problem is not mistaken for a missing file.
70
+
71
+ ## Delegates to `is_path_readable`
72
+ It actually opens the file (or lists the directory) — the only check that
73
+ agrees with the real read that follows.
74
+
75
+ ## Description
76
+ `describe()` returns "readable path" — a short noun phrase containers compose into larger texts ("not readable path", "... & readable path"); the failure card keeps its own `expected` sentence.
77
+ """
@@ -0,0 +1,82 @@
1
+ from typing import Any
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ...logic.checks._helpers import to_path
5
+ from ...logic.checks import is_path_writable
6
+ from .._helpers import build_path_rule_exception
7
+
8
+
9
+ class IsPathWritable(Rule):
10
+ """Path must be writable now, without anything being created or changed.
11
+
12
+ Rule:
13
+ is_path_writable(value)
14
+
15
+ Example:
16
+ validate(target, IsPathWritable())
17
+ """
18
+
19
+ __slots__ = ()
20
+
21
+ # ----------------------------------------------------------------------
22
+ # Rule definition
23
+ # ----------------------------------------------------------------------
24
+ def is_valid(self, value: Any) -> bool:
25
+
26
+ # 1. Anything that cannot be turned into a path fails
27
+ path = to_path(value)
28
+ if path is None:
29
+ return False
30
+
31
+ # 2. Delegate to the plain predicate (its own validation is skipped — the input is checked)
32
+ return is_path_writable(path, _validate_call=False)
33
+
34
+ # ----------------------------------------------------------------------
35
+ # Description definition
36
+ # ----------------------------------------------------------------------
37
+ def describe(self) -> str:
38
+
39
+ # 1. Fixed noun phrase
40
+ return "writable path"
41
+
42
+ # ----------------------------------------------------------------------
43
+ # Exception definition
44
+ # ----------------------------------------------------------------------
45
+ def build_exception(
46
+ self,
47
+ value: Any,
48
+ value_name: str | None = None,
49
+ context: str | None = None,
50
+ ) -> Exception:
51
+
52
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
53
+ return build_path_rule_exception(
54
+ error_name="IS_PATH_WRITABLE_ERROR",
55
+ expected="a writable file or directory, or a new file in a writable directory",
56
+ value=value,
57
+ value_name=value_name,
58
+ context=context,
59
+ problem=f"Path {value!r} is not writable (access is denied, its folder is missing, or the name is taken by something else).",
60
+ how_to_fix="Provide a path you have permission to write to, inside an existing directory.",
61
+ )
62
+
63
+
64
+ _DESIGN_NOTES = """
65
+ # IsPathWritable — Writable path
66
+
67
+ ## Moved and Corrected
68
+ This rule existed in `rules/IsPathWritable.py` and delegated to a function
69
+ that **created an empty file** for a path that did not exist. The function
70
+ now probes without a trace (see `is_path_writable`), so the rule is safe to
71
+ use as a pure check. The `error_name` follows the rules library's
72
+ `<NAME>_ERROR` convention (`IS_PATH_WRITABLE_ERROR`, formerly
73
+ `PATH_NOT_WRITABLE_ERROR`).
74
+
75
+ ## Accepted Values
76
+ An existing file, an existing directory, or a path of a not-yet-existing file
77
+ whose parent directory exists and accepts new files. A missing parent is
78
+ `False`: creating it is `ensure_parent_dir`'s job.
79
+
80
+ ## Description
81
+ `describe()` returns "writable path" — a short noun phrase containers compose into larger texts ("not writable path", "... & writable path"); the failure card keeps its own `expected` sentence.
82
+ """
@@ -0,0 +1,71 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ from simplibs.rules import Rule
4
+ # Outers
5
+ from .._helpers import build_path_rule_exception, check_path
6
+
7
+
8
+ class IsSymlink(Rule):
9
+ """Path itself must be a symbolic link (working or broken).
10
+
11
+ Rule:
12
+ Path(value).is_symlink()
13
+
14
+ Example:
15
+ exclude_filter = IsSymlink()
16
+ """
17
+
18
+ __slots__ = ()
19
+
20
+ # ----------------------------------------------------------------------
21
+ # Rule definition
22
+ # ----------------------------------------------------------------------
23
+ def is_valid(self, value: Any) -> bool:
24
+
25
+ # 1. Ask the filesystem — a refusal or a non-path is simply a failed check
26
+ return check_path(value, Path.is_symlink)
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Description definition
30
+ # ----------------------------------------------------------------------
31
+ def describe(self) -> str:
32
+
33
+ # 1. Fixed noun phrase
34
+ return "symbolic link"
35
+
36
+ # ----------------------------------------------------------------------
37
+ # Exception definition
38
+ # ----------------------------------------------------------------------
39
+ def build_exception(
40
+ self,
41
+ value: Any,
42
+ value_name: str | None = None,
43
+ context: str | None = None,
44
+ ) -> Exception:
45
+
46
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
47
+ return build_path_rule_exception(
48
+ error_name="IS_SYMLINK_ERROR",
49
+ expected="a symbolic link",
50
+ value=value,
51
+ value_name=value_name,
52
+ context=context,
53
+ problem=f"Path {value!r} is not a symbolic link.",
54
+ how_to_fix="Provide a path to a symbolic link.",
55
+ )
56
+
57
+
58
+ _DESIGN_NOTES = """
59
+ # IsSymlink — Symbolic link
60
+
61
+ ## Purpose
62
+ The only rule that looks at the *link* instead of its target — which is why
63
+ it also accepts a broken link that `PathExists` rejects.
64
+
65
+ ## Windows
66
+ Creating symlinks needs privileges there, but *detecting* them does not.
67
+ Directory junctions are reparse points, not symlinks, and answer `False`.
68
+
69
+ ## Description
70
+ `describe()` returns "symbolic link" — a short noun phrase containers compose into larger texts ("not symbolic link", "... & symbolic link"); the failure card keeps its own `expected` sentence.
71
+ """
@@ -0,0 +1,75 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ from simplibs.rules import Rule
4
+ # Outers
5
+ from .._helpers import build_path_rule_exception, check_path
6
+
7
+
8
+ class PathExists(Rule):
9
+ """Path must exist (a file, a directory, or a symlink that leads somewhere).
10
+
11
+ Rule:
12
+ Path(value).exists()
13
+
14
+ Example:
15
+ validate(path, PathExists())
16
+ """
17
+
18
+ __slots__ = ()
19
+
20
+ # ----------------------------------------------------------------------
21
+ # Rule definition
22
+ # ----------------------------------------------------------------------
23
+ def is_valid(self, value: Any) -> bool:
24
+
25
+ # 1. Ask the filesystem — a refusal or a non-path is simply a failed check
26
+ return check_path(value, Path.exists)
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Description definition
30
+ # ----------------------------------------------------------------------
31
+ def describe(self) -> str:
32
+
33
+ # 1. Fixed noun phrase
34
+ return "existing path"
35
+
36
+ # ----------------------------------------------------------------------
37
+ # Exception definition
38
+ # ----------------------------------------------------------------------
39
+ def build_exception(
40
+ self,
41
+ value: Any,
42
+ value_name: str | None = None,
43
+ context: str | None = None,
44
+ ) -> Exception:
45
+
46
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
47
+ return build_path_rule_exception(
48
+ error_name="PATH_EXISTS_ERROR",
49
+ expected="an existing path",
50
+ value=value,
51
+ value_name=value_name,
52
+ context=context,
53
+ problem=f"Path {value!r} does not exist.",
54
+ how_to_fix="Provide a path to an existing file or directory.",
55
+ )
56
+
57
+
58
+ _DESIGN_NOTES = """
59
+ # PathExists — Existing path
60
+
61
+ ## Purpose
62
+ The most basic filesystem precondition: "is there anything at this path?".
63
+
64
+ ## Follows Symlinks
65
+ `Path.exists()` follows links, so a *broken* symlink answers `False` — the
66
+ thing it points at is missing. `IsSymlink` is the rule that sees the link
67
+ itself.
68
+
69
+ ## A Check at One Moment
70
+ The answer holds when evaluated; the file can appear or vanish right after.
71
+ Use it as a fail-fast message, not as a guarantee (see `ensure_file`).
72
+
73
+ ## Description
74
+ `describe()` returns "existing path" — a short noun phrase containers compose into larger texts ("not existing path", "... & existing path"); the failure card keeps its own `expected` sentence.
75
+ """
@@ -0,0 +1,31 @@
1
+ from .IsDir import IsDir
2
+ from .IsEmptyDir import IsEmptyDir
3
+ from .IsEmptyFile import IsEmptyFile
4
+ from .IsFile import IsFile
5
+ from .IsPathReadable import IsPathReadable
6
+ from .IsPathWritable import IsPathWritable
7
+ from .IsSymlink import IsSymlink
8
+ from .PathExists import PathExists
9
+
10
+
11
+ _DESIGN_NOTES = """
12
+ # Path Filesystem Rules Sub-Package
13
+
14
+ ## Purpose
15
+ Rules that ask the filesystem about a path. Each is a thin wrapper: pathlib
16
+ (or a function from `checks/`) answers, `check_path` guarantees a strict
17
+ `bool` without exceptions, and a shared helper builds the failure card.
18
+
19
+ ## Internal Components Registry
20
+
21
+ | Component | Type | Description |
22
+ | :--------------- | :--- | :------------------------------------------------------------- |
23
+ | `IsDir` | Rule | An existing directory (or a symlink to one). |
24
+ | `IsEmptyDir` | Rule | An existing directory with no entries. |
25
+ | `IsEmptyFile` | Rule | An existing regular file of size zero. |
26
+ | `IsFile` | Rule | An existing regular file (or a symlink to one). |
27
+ | `IsPathReadable` | Rule | A file that can be read or a directory that can be listed. |
28
+ | `IsPathWritable` | Rule | A path that could be written to now, with no side effects. |
29
+ | `IsSymlink` | Rule | The path itself is a symbolic link. |
30
+ | `PathExists` | Rule | Something exists at the path. |
31
+ """
@@ -0,0 +1,146 @@
1
+ from typing import Any
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ...logic.transformers._helpers import is_valid_suffix
5
+ from ...logic.checks._helpers import to_path
6
+ from .._helpers import build_path_rule_exception
7
+ from .._validations import (
8
+ raise_has_suffix_invalid_suffix_error,
9
+ raise_has_suffix_no_suffixes_error,
10
+ raise_rule_param_not_bool_error,
11
+ )
12
+
13
+
14
+ class HasSuffix(Rule):
15
+ """Path must end with one of the given suffixes.
16
+
17
+ Rule:
18
+ the final name ends with one of `suffixes` (after a non-empty base name)
19
+
20
+ Example:
21
+ validate(path, HasSuffix(".py"))
22
+ include_filter = HasSuffix(".py", ".md")
23
+ HasSuffix("") # names without any suffix
24
+ """
25
+
26
+ __slots__ = ("suffixes", "ignore_case", "_keys")
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Constructor initialization
30
+ # ----------------------------------------------------------------------
31
+ def __init__(self, *suffixes: str, ignore_case: bool = True) -> None:
32
+
33
+ # 1. Parameter validation
34
+ if not suffixes:
35
+ raise_has_suffix_no_suffixes_error()
36
+ for suffix in suffixes:
37
+ if not isinstance(suffix, str) or not is_valid_suffix(suffix):
38
+ raise_has_suffix_invalid_suffix_error(suffix)
39
+ if not isinstance(ignore_case, bool):
40
+ raise_rule_param_not_bool_error("HasSuffix", "ignore_case", ignore_case)
41
+
42
+ # 2. Parameter assignment, with the comparison keys prepared once
43
+ self.suffixes = tuple(suffixes)
44
+ self.ignore_case = ignore_case
45
+ self._keys = tuple(suffix.casefold() for suffix in suffixes) if ignore_case else self.suffixes
46
+
47
+ # ----------------------------------------------------------------------
48
+ # Rule definition
49
+ # ----------------------------------------------------------------------
50
+ def is_valid(self, value: Any) -> bool:
51
+
52
+ # 1. Anything that cannot be turned into a path fails
53
+ path = to_path(value)
54
+ if path is None:
55
+ return False
56
+
57
+ # 2. Compare the final name with each suffix
58
+ name = path.name.casefold() if self.ignore_case else path.name
59
+ for key in self._keys:
60
+
61
+ # 2.1 An empty suffix means "the name has no suffix at all"
62
+ if key == "":
63
+ if path.suffix == "":
64
+ return True
65
+
66
+ # 2.2 Otherwise the name must end with it, after a non-empty base name
67
+ elif len(name) > len(key) and name.endswith(key):
68
+ return True
69
+
70
+ # 3. No suffix matched
71
+ return False
72
+
73
+ # ----------------------------------------------------------------------
74
+ # Description definition
75
+ # ----------------------------------------------------------------------
76
+ def describe(self) -> str:
77
+
78
+ # 1. One suffix reads "suffix '.py'", several "suffix one of '.py', '.md'"
79
+ if len(self.suffixes) == 1:
80
+ text = f"suffix {self.suffixes[0]!r}"
81
+ else:
82
+ text = "suffix one of " + ", ".join(repr(suffix) for suffix in self.suffixes)
83
+
84
+ # 2. Say so when the comparison is case-sensitive
85
+ return text if self.ignore_case else f"{text} (case-sensitive)"
86
+
87
+ # ----------------------------------------------------------------------
88
+ # Exception definition
89
+ # ----------------------------------------------------------------------
90
+ def build_exception(
91
+ self,
92
+ value: Any,
93
+ value_name: str | None = None,
94
+ context: str | None = None,
95
+ ) -> Exception:
96
+
97
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
98
+ return build_path_rule_exception(
99
+ error_name="HAS_SUFFIX_ERROR",
100
+ expected=f"a path with {self.describe()}",
101
+ value=value,
102
+ value_name=value_name,
103
+ context=context,
104
+ problem=f"Path {value!r} does not have {self.describe()}.",
105
+ how_to_fix=f"Provide a path with {self.describe()}.",
106
+ )
107
+
108
+
109
+ _DESIGN_NOTES = """
110
+ # HasSuffix — Select by File Extension
111
+
112
+ ## Purpose
113
+ The most common filter of an extraction pipeline: "only `.py` and `.md`".
114
+ `include_filter=HasSuffix(".py", ".md")`.
115
+
116
+ ---
117
+
118
+ ## 1. Matches the End of the Name, Not `Path.suffix`
119
+ `Path("a.tar.gz").suffix` is only `.gz`, so `HasSuffix(".tar.gz")` could never
120
+ match by comparing it. Checking `name.endswith(suffix)` supports compound
121
+ suffixes. A name that *is* the suffix (`.py` as a whole file name, which is a
122
+ hidden file without an extension) does not match — a suffix needs a base name
123
+ in front of it.
124
+
125
+ ## 2. `""` Means "No Suffix"
126
+ `HasSuffix("")` selects names without an extension (`Makefile`, `.bashrc`).
127
+ It is the one case compared through `Path.suffix`.
128
+
129
+ ## 3. Case-Insensitive by Default
130
+ `README.MD` and `x.PY` are what Windows users produce; a filter that misses
131
+ them is a surprise. `ignore_case=False` opts out. `casefold` is used instead
132
+ of `lower` for correct Unicode comparison.
133
+
134
+ ## 4. Lexical — Works on Names, Not on Files
135
+ `HasSuffix(".py")` is true for a *directory* named `pkg.py` too. Combine
136
+ with `IsFile()` when the type matters: `IsFile() & HasSuffix(".py")`.
137
+
138
+ ## 5. Constructor Guards
139
+ No suffixes, a suffix without a leading dot (`"py"` would match any name
140
+ ending in "py"), and a non-bool `ignore_case` are all `ParamError`s. The
141
+ shape check is `is_valid_suffix`, shared with `change_suffix`.
142
+
143
+ ## 6. Description
144
+ `suffix '.py'` / `suffix one of '.py', '.md'` — no `|` in the text, so the
145
+ description cannot be misread inside `a | b`/`a & b` compositions.
146
+ """
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ from simplibs.rules import Rule
4
+ # Outers
5
+ from .._helpers import build_path_rule_exception, check_path
6
+
7
+
8
+ class IsAbsolutePath(Rule):
9
+ """Path must be absolute.
10
+
11
+ Rule:
12
+ Path(value).is_absolute()
13
+
14
+ Example:
15
+ validate(path, IsAbsolutePath())
16
+ """
17
+
18
+ __slots__ = ()
19
+
20
+ # ----------------------------------------------------------------------
21
+ # Rule definition
22
+ # ----------------------------------------------------------------------
23
+ def is_valid(self, value: Any) -> bool:
24
+
25
+ # 1. Pure path arithmetic — no disk access
26
+ return check_path(value, Path.is_absolute)
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Description definition
30
+ # ----------------------------------------------------------------------
31
+ def describe(self) -> str:
32
+
33
+ # 1. Fixed noun phrase
34
+ return "absolute path"
35
+
36
+ # ----------------------------------------------------------------------
37
+ # Exception definition
38
+ # ----------------------------------------------------------------------
39
+ def build_exception(
40
+ self,
41
+ value: Any,
42
+ value_name: str | None = None,
43
+ context: str | None = None,
44
+ ) -> Exception:
45
+
46
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
47
+ return build_path_rule_exception(
48
+ error_name="IS_ABSOLUTE_PATH_ERROR",
49
+ expected="an absolute path",
50
+ value=value,
51
+ value_name=value_name,
52
+ context=context,
53
+ problem=f"Path {value!r} is not absolute.",
54
+ how_to_fix="Provide an absolute path, or make it absolute with normalize_path().",
55
+ )
56
+
57
+
58
+ _DESIGN_NOTES = """
59
+ # IsAbsolutePath — Anchored, Not Relative
60
+
61
+ ## Purpose
62
+ A precondition for steps that must not depend on the working directory.
63
+
64
+ ## Platform Rule
65
+ `Path.is_absolute()` follows the platform: `/etc` is absolute on POSIX, but on
66
+ Windows it lacks a drive and is *not* (`C:\\\\etc` is). The rule reports what
67
+ the platform means by "absolute".
68
+
69
+ ## `~` Is Expanded First
70
+ `to_path` expands a leading `~`, so `~/work` is absolute (it is the home
71
+ directory). A path that merely *starts with* a tilde character is otherwise
72
+ indistinguishable from a home reference, and the rest of the library treats
73
+ them identically.
74
+ """
@@ -0,0 +1,92 @@
1
+ from typing import Any
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ...logic.checks._helpers import to_path
5
+ from ...logic.checks import is_hidden_path
6
+ from .._helpers import build_path_rule_exception
7
+ from .._validations import raise_rule_param_not_bool_error
8
+
9
+
10
+ class IsHidden(Rule):
11
+ """Path must be hidden — its final name starts with a dot.
12
+
13
+ Rule:
14
+ is_hidden_path(value)
15
+
16
+ Example:
17
+ exclude_filter = IsHidden()
18
+ IsHidden(check_attributes=True) # also the Windows "hidden" attribute
19
+ """
20
+
21
+ __slots__ = ("check_attributes",)
22
+
23
+ # ----------------------------------------------------------------------
24
+ # Constructor initialization
25
+ # ----------------------------------------------------------------------
26
+ def __init__(self, *, check_attributes: bool = False) -> None:
27
+
28
+ # 1. Parameter validation
29
+ if not isinstance(check_attributes, bool):
30
+ raise_rule_param_not_bool_error("IsHidden", "check_attributes", check_attributes)
31
+
32
+ # 2. Parameter assignment
33
+ self.check_attributes = check_attributes
34
+
35
+ # ----------------------------------------------------------------------
36
+ # Rule definition
37
+ # ----------------------------------------------------------------------
38
+ def is_valid(self, value: Any) -> bool:
39
+
40
+ # 1. Anything that cannot be turned into a path fails
41
+ path = to_path(value)
42
+ if path is None:
43
+ return False
44
+
45
+ # 2. Delegate to the plain predicate (its own validation is skipped — the input is checked)
46
+ return is_hidden_path(path, check_attributes=self.check_attributes, _validate_call=False)
47
+
48
+ # ----------------------------------------------------------------------
49
+ # Description definition
50
+ # ----------------------------------------------------------------------
51
+ def describe(self) -> str:
52
+
53
+ # 1. Fixed noun phrase
54
+ return "hidden path"
55
+
56
+ # ----------------------------------------------------------------------
57
+ # Exception definition
58
+ # ----------------------------------------------------------------------
59
+ def build_exception(
60
+ self,
61
+ value: Any,
62
+ value_name: str | None = None,
63
+ context: str | None = None,
64
+ ) -> Exception:
65
+
66
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
67
+ return build_path_rule_exception(
68
+ error_name="IS_HIDDEN_ERROR",
69
+ expected="a hidden path (its name starts with a dot)",
70
+ value=value,
71
+ value_name=value_name,
72
+ context=context,
73
+ problem=f"Path {value!r} is not hidden.",
74
+ how_to_fix="Provide a path whose final name starts with a dot, such as '.git'.",
75
+ )
76
+
77
+
78
+ _DESIGN_NOTES = """
79
+ # IsHidden — Hidden by Name, Optionally by Windows Attribute
80
+
81
+ ## Purpose
82
+ Mostly used negated: `exclude_filter=IsHidden()` or `include_filter=~IsHidden()`.
83
+
84
+ ## Final Name Only
85
+ `/home/me/.config/tool/a.json` is not hidden by this rule — its name is not.
86
+ Skipping whole hidden *folders* is what a scanner's `ignore_names` does.
87
+
88
+ ## Keyword-Only Option
89
+ `IsHidden(True)` would read as a mystery; `IsHidden(check_attributes=True)`
90
+ says what it does. The option costs a `stat` call per path and exists only on
91
+ Windows (a no-op elsewhere), so it is off by default.
92
+ """