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,71 @@
1
+ from typing import Any
2
+ from simplibs.exception import ValidationError
3
+ # Outers
4
+ from ...logic.checks._helpers import to_path
5
+ # Inners
6
+ from .build_not_a_path_exception import build_not_a_path_exception
7
+
8
+
9
+ def build_path_rule_exception(
10
+ *,
11
+ error_name: str,
12
+ expected: str,
13
+ value: Any,
14
+ value_name: str | None,
15
+ context: str | None,
16
+ problem: str,
17
+ how_to_fix: str,
18
+ exception: type[Exception] = ValueError,
19
+ ) -> Exception:
20
+ """Build the failure card of a path rule, covering both failure branches.
21
+
22
+ Args:
23
+ error_name: The calling rule's `error_name`.
24
+ expected: The calling rule's `expected` sentence.
25
+ value: The rejected value.
26
+ value_name: The label passed to `build_exception`.
27
+ context: The context passed to `build_exception`.
28
+ problem: What is wrong with a value that IS a usable path.
29
+ how_to_fix: How to fix that.
30
+ exception: The exception type wrapped for that case.
31
+
32
+ Returns:
33
+ A `ValidationError`. If `value` is not a usable path at all, the
34
+ generic "not a path" card is returned instead of `problem`/`how_to_fix`.
35
+ """
36
+
37
+ # 1. Not a usable path — the shared card
38
+ if to_path(value) is None:
39
+ return build_not_a_path_exception(
40
+ error_name=error_name,
41
+ expected=expected,
42
+ value=value,
43
+ value_name=value_name,
44
+ context=context,
45
+ )
46
+
47
+ # 2. A usable path that fails the rule's own condition
48
+ return ValidationError(
49
+ error_name=error_name,
50
+ label=value_name,
51
+ expected=expected,
52
+ value=value,
53
+ problem=problem,
54
+ context=context,
55
+ how_to_fix=how_to_fix,
56
+ exception=exception,
57
+ )
58
+
59
+
60
+ _DESIGN_NOTES = """
61
+ # build_path_rule_exception — the Whole `build_exception` of a Path Rule
62
+
63
+ ## Purpose
64
+ Reduces a path rule's `build_exception` to its own three sentences
65
+ (`expected`, `problem`, `how_to_fix`): the type-or-blank branch is decided
66
+ here, identically for all of them.
67
+
68
+ ## Keyword-Only
69
+ Seven values of the same kind (`str`) travel together; naming each at the
70
+ call site is what keeps `problem` and `how_to_fix` from being swapped.
71
+ """
@@ -0,0 +1,48 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+ from typing import Any
4
+ # Outers
5
+ from ...logic.checks._helpers import to_path
6
+
7
+
8
+ def check_path(
9
+ value: Any,
10
+ check: Callable[[Path], bool],
11
+ ) -> bool:
12
+ """Run a filesystem check on a value, answering False instead of raising.
13
+
14
+ Args:
15
+ value: Any value; only a usable `str`/`Path` is checked.
16
+ check: A callable taking the `Path` — e.g. `Path.is_file`.
17
+
18
+ Returns:
19
+ The strict `bool` answer of `check`, or `False` if `value` is not a
20
+ usable path or the operating system refuses to answer.
21
+ """
22
+
23
+ # 1. Anything that cannot be turned into a path fails the rule
24
+ path = to_path(value)
25
+ if path is None:
26
+ return False
27
+
28
+ # 2. Ask the filesystem; a refusal (permissions, bad name) is a "no"
29
+ try:
30
+ return bool(check(path))
31
+ except (OSError, ValueError):
32
+ return False
33
+
34
+
35
+ _DESIGN_NOTES = """
36
+ # check_path — The Never-Raising Wrapper Every Filesystem Rule Needs
37
+
38
+ ## Purpose
39
+ `Rule.is_valid` must return a strict `bool` and never raise. The pathlib
40
+ predicates mostly honor that for "no such file", but not for everything:
41
+ `Path.is_file()` re-raises `PermissionError` for a path behind a folder you
42
+ may not enter. One wrapper turns every such case into `False`, so no rule
43
+ has to remember the `try`.
44
+
45
+ ## Strict `bool`
46
+ `bool(check(path))` — the contract tests compare with `is True`/`is False`,
47
+ so a truthy non-bool (an `int` from a custom check) is normalized here.
48
+ """
@@ -0,0 +1,24 @@
1
+ from .raise_has_suffix_invalid_suffix_error import raise_has_suffix_invalid_suffix_error
2
+ from .raise_has_suffix_no_suffixes_error import raise_has_suffix_no_suffixes_error
3
+ from .raise_rule_param_not_bool_error import raise_rule_param_not_bool_error
4
+ from .raise_rule_param_not_callable_error import raise_rule_param_not_callable_error
5
+ from .raise_rule_param_not_path_error import raise_rule_param_not_path_error
6
+
7
+
8
+ _DESIGN_NOTES = """
9
+ # Path Rules Diagnostic Helpers Sub-Package
10
+
11
+ ## Purpose
12
+ Internal diagnostic helpers raising `ParamError` when a rule is constructed
13
+ with invalid arguments.
14
+
15
+ ## Internal Components Registry
16
+
17
+ | Component | Type | Description |
18
+ | :--------------------------------------- | :------- | :---------------------------------------------------------------- |
19
+ | `raise_has_suffix_invalid_suffix_error` | Function | A `HasSuffix` suffix has the wrong shape. |
20
+ | `raise_has_suffix_no_suffixes_error` | Function | `HasSuffix()` was created without any suffix. |
21
+ | `raise_rule_param_not_bool_error` | Function | An on/off option is not a `bool`. |
22
+ | `raise_rule_param_not_callable_error` | Function | A wrapped rule/predicate is not callable. |
23
+ | `raise_rule_param_not_path_error` | Function | A root/path option is not a usable path. |
24
+ """
@@ -0,0 +1,35 @@
1
+ from typing import Any, NoReturn
2
+ from simplibs.exception import ParamError
3
+
4
+
5
+ def raise_has_suffix_invalid_suffix_error(suffix: Any) -> NoReturn:
6
+ """Raise ParamError when a HasSuffix suffix has an invalid shape."""
7
+
8
+ # 1. Raise structured ParamError wrapping a ValueError
9
+ raise ParamError(
10
+ error_name="HAS_SUFFIX_INVALID_SUFFIX_ERROR",
11
+ label="HasSuffix.suffixes",
12
+ value=suffix,
13
+ problem=(
14
+ f"{suffix!r} is not a valid file suffix.",
15
+ "A suffix must be an empty string (no suffix) or a string such as '.txt', "
16
+ "without any path separator.",
17
+ ),
18
+ expected="Strings such as '.py', '.tar.gz', or '' for a name without a suffix.",
19
+ how_to_fix=(
20
+ "Start each suffix with a dot, e.g. '.py' instead of 'py'.",
21
+ "Remove path separators from the suffix.",
22
+ ),
23
+ exception=ValueError,
24
+ )
25
+
26
+
27
+ _DESIGN_NOTES = """
28
+ # raise_has_suffix_invalid_suffix_error — Suffix Shape Guard
29
+
30
+ ## Purpose
31
+ The most common mistake is `HasSuffix("py")` — no dot, so it would match
32
+ names merely *ending* in "py". Rejecting the shape up front prevents a rule
33
+ that silently selects the wrong files. The shape rule is the library-wide
34
+ `is_valid_suffix`, shared with `change_suffix`.
35
+ """
@@ -0,0 +1,31 @@
1
+ from typing import NoReturn
2
+ from simplibs.exception import ParamError
3
+
4
+
5
+ def raise_has_suffix_no_suffixes_error() -> NoReturn:
6
+ """Raise ParamError when HasSuffix is constructed without any suffix."""
7
+
8
+ # 1. Raise structured ParamError wrapping a ValueError
9
+ raise ParamError(
10
+ error_name="HAS_SUFFIX_NO_SUFFIXES_ERROR",
11
+ label="HasSuffix.suffixes",
12
+ value=(),
13
+ problem=(
14
+ "HasSuffix() was created without any suffix.",
15
+ "A rule without a suffix to look for could never accept or reject anything.",
16
+ ),
17
+ expected="At least one suffix, e.g. HasSuffix('.py') or HasSuffix('.md', '.rst').",
18
+ how_to_fix=(
19
+ "Pass one or more suffixes: HasSuffix('.py').",
20
+ ),
21
+ exception=ValueError,
22
+ )
23
+
24
+
25
+ _DESIGN_NOTES = """
26
+ # raise_has_suffix_no_suffixes_error — Empty Rule Guard
27
+
28
+ ## Purpose
29
+ Mirrors the "at least one rule" guard of `AllOf`/`AnyOf`: `HasSuffix()` with
30
+ nothing to look for is almost certainly a forgotten argument.
31
+ """
@@ -0,0 +1,37 @@
1
+ from typing import Any, NoReturn
2
+ from simplibs.exception import ParamError
3
+
4
+
5
+ def raise_rule_param_not_bool_error(
6
+ rule_name: str,
7
+ param_name: str,
8
+ value: Any,
9
+ ) -> NoReturn:
10
+ """Raise ParamError when a rule's boolean constructor parameter is not a bool."""
11
+
12
+ # 1. Raise structured ParamError wrapping a TypeError
13
+ raise ParamError(
14
+ error_name="RULE_PARAM_NOT_BOOL_ERROR",
15
+ label=f"{rule_name}.{param_name}",
16
+ value=value,
17
+ problem=(
18
+ f"Parameter '{param_name}' of {rule_name}() must be a bool, "
19
+ f"got {value!r} of type '{type(value).__name__}'.",
20
+ ),
21
+ expected="True or False.",
22
+ how_to_fix=(
23
+ f"Pass True or False as '{param_name}'.",
24
+ ),
25
+ exception=TypeError,
26
+ )
27
+
28
+
29
+ _DESIGN_NOTES = """
30
+ # raise_rule_param_not_bool_error — Boolean Option Guard
31
+
32
+ ## Purpose
33
+ `ignore_case`, `resolve_symlinks`, `allow_equal`, `portable` and
34
+ `check_attributes` are on/off switches. A truthy string such as `"yes"` would
35
+ be accepted by Python but almost certainly signals a mistake, so the rules
36
+ require a real `bool` and report anything else at construction time.
37
+ """
@@ -0,0 +1,36 @@
1
+ from typing import Any, NoReturn
2
+ from simplibs.exception import ParamError
3
+
4
+
5
+ def raise_rule_param_not_callable_error(
6
+ rule_name: str,
7
+ param_name: str,
8
+ value: Any,
9
+ ) -> NoReturn:
10
+ """Raise ParamError when a rule's rule-or-callable parameter is not callable."""
11
+
12
+ # 1. Raise structured ParamError wrapping a TypeError
13
+ raise ParamError(
14
+ error_name="RULE_PARAM_NOT_CALLABLE_ERROR",
15
+ label=f"{rule_name}.{param_name}",
16
+ value=value,
17
+ problem=(
18
+ f"Parameter '{param_name}' of {rule_name}() must be a Rule or a callable, "
19
+ f"got {value!r} of type '{type(value).__name__}'.",
20
+ ),
21
+ expected="A Rule instance or a callable taking one value and returning a bool.",
22
+ how_to_fix=(
23
+ f"Pass a Rule (e.g. StartsWith('test_')) or a function as '{param_name}'.",
24
+ ),
25
+ exception=TypeError,
26
+ )
27
+
28
+
29
+ _DESIGN_NOTES = """
30
+ # raise_rule_param_not_callable_error — Operand Guard
31
+
32
+ ## Purpose
33
+ `NameIs`, `StemIs` and `ParentNameIs` wrap another rule. A value that is not
34
+ callable (a string, `None`) could never be evaluated, so it is reported when
35
+ the rule is built, not on first use.
36
+ """
@@ -0,0 +1,37 @@
1
+ from typing import Any, NoReturn
2
+ from simplibs.exception import ParamError
3
+
4
+
5
+ def raise_rule_param_not_path_error(
6
+ rule_name: str,
7
+ param_name: str,
8
+ value: Any,
9
+ ) -> NoReturn:
10
+ """Raise ParamError when a rule's path constructor parameter is not a usable path."""
11
+
12
+ # 1. Raise structured ParamError wrapping a TypeError
13
+ raise ParamError(
14
+ error_name="RULE_PARAM_NOT_PATH_ERROR",
15
+ label=f"{rule_name}.{param_name}",
16
+ value=value,
17
+ problem=(
18
+ f"Parameter '{param_name}' of {rule_name}() must be a non-blank str or a Path "
19
+ f"without a null byte, got {value!r} of type '{type(value).__name__}'.",
20
+ ),
21
+ expected="A non-blank string or a Path instance.",
22
+ how_to_fix=(
23
+ f"Pass a directory path as '{param_name}', e.g. Path('src') or 'src'.",
24
+ ),
25
+ exception=TypeError,
26
+ )
27
+
28
+
29
+ _DESIGN_NOTES = """
30
+ # raise_rule_param_not_path_error — Path Option Guard
31
+
32
+ ## Purpose
33
+ `IsWithin(root)` is meaningless with a blank or non-path root (it would
34
+ quietly mean "the current directory" or never match). The same `to_path`
35
+ conversion the rule uses at evaluation time decides here, so "accepted at
36
+ construction" and "usable at evaluation" cannot disagree.
37
+ """
@@ -0,0 +1,70 @@
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 IsDir(Rule):
9
+ """Path must be an existing directory (or a symlink to one).
10
+
11
+ Rule:
12
+ Path(value).is_dir()
13
+
14
+ Example:
15
+ validate(root, IsDir()) # or: IsDir() >> CollectFiles()
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_dir)
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Description definition
30
+ # ----------------------------------------------------------------------
31
+ def describe(self) -> str:
32
+
33
+ # 1. Fixed noun phrase
34
+ return "existing directory"
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_DIR_ERROR",
49
+ expected="an existing directory",
50
+ value=value,
51
+ value_name=value_name,
52
+ context=context,
53
+ problem=f"Path {value!r} is not an existing directory.",
54
+ how_to_fix="Provide a path to an existing directory.",
55
+ )
56
+
57
+
58
+ _DESIGN_NOTES = """
59
+ # IsDir — Existing directory
60
+
61
+ ## Purpose
62
+ The natural head of a pipeline: `IsDir() >> CollectFiles()` — check the
63
+ input, then pass it on unchanged (`Action` turns the rule into a `guard`).
64
+
65
+ ## Follows Symlinks
66
+ A symlink to a directory is a directory here, as for every pathlib call.
67
+
68
+ ## Description
69
+ `describe()` returns "existing directory" — a short noun phrase containers compose into larger texts ("not existing directory", "... & existing directory"); the failure card keeps its own `expected` sentence.
70
+ """
@@ -0,0 +1,76 @@
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_empty_dir
6
+ from .._helpers import build_path_rule_exception
7
+
8
+
9
+ class IsEmptyDir(Rule):
10
+ """Path must be an existing directory with no entries.
11
+
12
+ Rule:
13
+ is_empty_dir(value)
14
+
15
+ Example:
16
+ validate(output_dir, IsEmptyDir())
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_empty_dir(path, _validate_call=False)
33
+
34
+ # ----------------------------------------------------------------------
35
+ # Description definition
36
+ # ----------------------------------------------------------------------
37
+ def describe(self) -> str:
38
+
39
+ # 1. Fixed noun phrase
40
+ return "empty directory"
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_EMPTY_DIR_ERROR",
55
+ expected="an existing empty directory",
56
+ value=value,
57
+ value_name=value_name,
58
+ context=context,
59
+ problem=f"Path {value!r} is not an existing, empty directory.",
60
+ how_to_fix="Provide a path to an existing directory that contains nothing.",
61
+ )
62
+
63
+
64
+ _DESIGN_NOTES = """
65
+ # IsEmptyDir — Empty directory
66
+
67
+ ## Purpose
68
+ "Only write into an empty folder", "this scan found nothing to clean up".
69
+
70
+ ## Delegates to `is_empty_dir`
71
+ Reads the first entry only and treats an unlistable directory as *not* empty
72
+ (emptiness unknown) — see the function's notes.
73
+
74
+ ## Description
75
+ `describe()` returns "empty directory" — a short noun phrase containers compose into larger texts ("not empty directory", "... & empty directory"); the failure card keeps its own `expected` sentence.
76
+ """
@@ -0,0 +1,76 @@
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
+ def _is_empty_file(path: Path) -> bool:
9
+ """True for a regular file of size zero."""
10
+ return path.is_file() and path.stat().st_size == 0
11
+
12
+
13
+ class IsEmptyFile(Rule):
14
+ """Path must be an existing regular file of size zero.
15
+
16
+ Rule:
17
+ Path(value).is_file() and Path(value).stat().st_size == 0
18
+
19
+ Example:
20
+ exclude_filter = IsEmptyFile()
21
+ """
22
+
23
+ __slots__ = ()
24
+
25
+ # ----------------------------------------------------------------------
26
+ # Rule definition
27
+ # ----------------------------------------------------------------------
28
+ def is_valid(self, value: Any) -> bool:
29
+
30
+ # 1. Ask the filesystem — a refusal or a non-path is simply a failed check
31
+ return check_path(value, _is_empty_file)
32
+
33
+ # ----------------------------------------------------------------------
34
+ # Description definition
35
+ # ----------------------------------------------------------------------
36
+ def describe(self) -> str:
37
+
38
+ # 1. Fixed noun phrase
39
+ return "empty file"
40
+
41
+ # ----------------------------------------------------------------------
42
+ # Exception definition
43
+ # ----------------------------------------------------------------------
44
+ def build_exception(
45
+ self,
46
+ value: Any,
47
+ value_name: str | None = None,
48
+ context: str | None = None,
49
+ ) -> Exception:
50
+
51
+ # 1. Build the exception (the not-a-path branch is handled by the helper)
52
+ return build_path_rule_exception(
53
+ error_name="IS_EMPTY_FILE_ERROR",
54
+ expected="an existing empty file",
55
+ value=value,
56
+ value_name=value_name,
57
+ context=context,
58
+ problem=f"Path {value!r} is not an existing, empty file.",
59
+ how_to_fix="Provide a path to an existing file whose size is zero bytes.",
60
+ )
61
+
62
+
63
+ _DESIGN_NOTES = """
64
+ # IsEmptyFile — Empty file
65
+
66
+ ## Purpose
67
+ Skipping files that carry no content (`exclude_filter=IsEmptyFile()`), or
68
+ asserting that a file was just created.
69
+
70
+ ## Size, Not Content
71
+ A file with a single space is not empty. Whitespace-only content is a
72
+ question for the file library, which reads content.
73
+
74
+ ## Description
75
+ `describe()` returns "empty file" — a short noun phrase containers compose into larger texts ("not empty file", "... & empty file"); the failure card keeps its own `expected` sentence.
76
+ """
@@ -0,0 +1,76 @@
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 IsFile(Rule):
9
+ """Path must be an existing regular file (or a symlink to one).
10
+
11
+ Rule:
12
+ Path(value).is_file()
13
+
14
+ Example:
15
+ include_filter = IsFile() & HasSuffix(".py")
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_file)
27
+
28
+ # ----------------------------------------------------------------------
29
+ # Description definition
30
+ # ----------------------------------------------------------------------
31
+ def describe(self) -> str:
32
+
33
+ # 1. Fixed noun phrase
34
+ return "existing file"
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_FILE_ERROR",
49
+ expected="an existing file",
50
+ value=value,
51
+ value_name=value_name,
52
+ context=context,
53
+ problem=f"Path {value!r} is not an existing file.",
54
+ how_to_fix="Provide a path to an existing regular file.",
55
+ )
56
+
57
+
58
+ _DESIGN_NOTES = """
59
+ # IsFile — Existing file
60
+
61
+ ## Purpose
62
+ "Is this a file I can treat as one?" — typically the first gate of a
63
+ pipeline step that reads content.
64
+
65
+ ## Follows Symlinks
66
+ A symlink to a file is a file; a broken symlink, a directory, a FIFO or a
67
+ device is not.
68
+
69
+ ## Never Raises
70
+ `Path.is_file()` itself re-raises `PermissionError` for a path behind a folder
71
+ you may not enter; `check_path` turns that into `False`, as `Rule.is_valid`
72
+ requires.
73
+
74
+ ## Description
75
+ `describe()` returns "existing file" — a short noun phrase containers compose into larger texts ("not existing file", "... & existing file"); the failure card keeps its own `expected` sentence.
76
+ """