simplibs-path 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simplibs/path/__init__.py +118 -0
- simplibs/path/actions/ChangeSuffix.py +23 -0
- simplibs/path/actions/CollectDirs.py +24 -0
- simplibs/path/actions/CollectFiles.py +25 -0
- simplibs/path/actions/CollectPaths.py +24 -0
- simplibs/path/actions/EnsureDir.py +22 -0
- simplibs/path/actions/EnsureFile.py +19 -0
- simplibs/path/actions/EnsureParentDir.py +20 -0
- simplibs/path/actions/FilterPaths.py +27 -0
- simplibs/path/actions/MakeUniquePath.py +22 -0
- simplibs/path/actions/NormalizeEntryPath.py +20 -0
- simplibs/path/actions/NormalizePath.py +23 -0
- simplibs/path/actions/RebasePath.py +24 -0
- simplibs/path/actions/RelativizePath.py +22 -0
- simplibs/path/actions/__init__.py +68 -0
- simplibs/path/annotations/PathLike.py +43 -0
- simplibs/path/annotations/PathPredicate.py +39 -0
- simplibs/path/annotations/RecursionDepth.py +33 -0
- simplibs/path/annotations/__init__.py +21 -0
- simplibs/path/logic/__init__.py +41 -0
- simplibs/path/logic/checks/__init__.py +29 -0
- simplibs/path/logic/checks/_helpers/__init__.py +21 -0
- simplibs/path/logic/checks/_helpers/_find_length_problem.py +48 -0
- simplibs/path/logic/checks/_helpers/_find_windows_name_problem.py +55 -0
- simplibs/path/logic/checks/_helpers/can_create_inside.py +39 -0
- simplibs/path/logic/checks/_helpers/constants/MAX_NAME_LENGTH.py +18 -0
- simplibs/path/logic/checks/_helpers/constants/WINDOWS_FORBIDDEN_CHARS.py +25 -0
- simplibs/path/logic/checks/_helpers/constants/WINDOWS_RESERVED_NAMES.py +27 -0
- simplibs/path/logic/checks/_helpers/constants/__init__.py +20 -0
- simplibs/path/logic/checks/_helpers/find_filename_problem.py +75 -0
- simplibs/path/logic/checks/_helpers/to_path.py +77 -0
- simplibs/path/logic/checks/is_empty_dir.py +59 -0
- simplibs/path/logic/checks/is_hidden_path.py +78 -0
- simplibs/path/logic/checks/is_path_readable.py +73 -0
- simplibs/path/logic/checks/is_path_writable.py +90 -0
- simplibs/path/logic/checks/is_valid_filename.py +47 -0
- simplibs/path/logic/checks/is_within.py +88 -0
- simplibs/path/logic/ensure/__init__.py +22 -0
- simplibs/path/logic/ensure/_validations/__init__.py +25 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_dir_error.py +40 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_dir_not_a_dir_error.py +36 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_create_error.py +39 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_not_a_file_error.py +36 -0
- simplibs/path/logic/ensure/_validations/raise_ensure_file_not_found_error.py +36 -0
- simplibs/path/logic/ensure/ensure_dir.py +83 -0
- simplibs/path/logic/ensure/ensure_file.py +103 -0
- simplibs/path/logic/ensure/ensure_parent_dir.py +58 -0
- simplibs/path/logic/scanners/__init__.py +35 -0
- simplibs/path/logic/scanners/_helpers/ScanEntry.py +36 -0
- simplibs/path/logic/scanners/_helpers/__init__.py +30 -0
- simplibs/path/logic/scanners/_helpers/as_path_predicate.py +38 -0
- simplibs/path/logic/scanners/_helpers/build_ignore_matcher.py +88 -0
- simplibs/path/logic/scanners/_helpers/generate_paths.py +74 -0
- simplibs/path/logic/scanners/_helpers/is_link_like.py +45 -0
- simplibs/path/logic/scanners/_helpers/resolve_scan_root.py +66 -0
- simplibs/path/logic/scanners/_helpers/scan_paths.py +92 -0
- simplibs/path/logic/scanners/_helpers/walk_tree.py +118 -0
- simplibs/path/logic/scanners/_validations/__init__.py +24 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_not_a_dir_error.py +34 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_not_found_error.py +38 -0
- simplibs/path/logic/scanners/_validations/raise_collect_paths_root_unreadable_error.py +38 -0
- simplibs/path/logic/scanners/_validations/raise_ignore_names_not_names_error.py +36 -0
- simplibs/path/logic/scanners/_validations/raise_nothing_to_collect_error.py +34 -0
- simplibs/path/logic/scanners/collect_dirs.py +62 -0
- simplibs/path/logic/scanners/collect_files.py +64 -0
- simplibs/path/logic/scanners/collect_paths.py +102 -0
- simplibs/path/logic/scanners/filter_paths.py +51 -0
- simplibs/path/logic/scanners/ignore_names/DEFAULT_IGNORE_NAMES.py +36 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_BUILD.py +48 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_ENV.py +31 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_GENERIC.py +30 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_IDE.py +20 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_SYSTEM.py +30 -0
- simplibs/path/logic/scanners/ignore_names/IGNORE_VCS.py +24 -0
- simplibs/path/logic/scanners/ignore_names/__init__.py +30 -0
- simplibs/path/logic/scanners/iter_paths.py +91 -0
- simplibs/path/logic/transformers/__init__.py +27 -0
- simplibs/path/logic/transformers/_helpers/__init__.py +17 -0
- simplibs/path/logic/transformers/_helpers/is_valid_suffix.py +37 -0
- simplibs/path/logic/transformers/_helpers/split_name.py +46 -0
- simplibs/path/logic/transformers/_validations/__init__.py +24 -0
- simplibs/path/logic/transformers/_validations/raise_invalid_suffix_error.py +36 -0
- simplibs/path/logic/transformers/_validations/raise_normalize_path_error.py +44 -0
- simplibs/path/logic/transformers/_validations/raise_path_has_no_name_error.py +36 -0
- simplibs/path/logic/transformers/_validations/raise_path_not_within_root_error.py +43 -0
- simplibs/path/logic/transformers/_validations/raise_unique_path_exhausted_error.py +39 -0
- simplibs/path/logic/transformers/change_suffix.py +77 -0
- simplibs/path/logic/transformers/make_unique_path.py +88 -0
- simplibs/path/logic/transformers/normalize_entry_path.py +67 -0
- simplibs/path/logic/transformers/normalize_path.py +85 -0
- simplibs/path/logic/transformers/rebase_path.py +78 -0
- simplibs/path/logic/transformers/relativize_path.py +56 -0
- simplibs/path/rules/__init__.py +66 -0
- simplibs/path/rules/_helpers/PathPartIs.py +152 -0
- simplibs/path/rules/_helpers/__init__.py +21 -0
- simplibs/path/rules/_helpers/build_not_a_path_exception.py +66 -0
- simplibs/path/rules/_helpers/build_path_rule_exception.py +71 -0
- simplibs/path/rules/_helpers/check_path.py +48 -0
- simplibs/path/rules/_validations/__init__.py +24 -0
- simplibs/path/rules/_validations/raise_has_suffix_invalid_suffix_error.py +35 -0
- simplibs/path/rules/_validations/raise_has_suffix_no_suffixes_error.py +31 -0
- simplibs/path/rules/_validations/raise_rule_param_not_bool_error.py +37 -0
- simplibs/path/rules/_validations/raise_rule_param_not_callable_error.py +36 -0
- simplibs/path/rules/_validations/raise_rule_param_not_path_error.py +37 -0
- simplibs/path/rules/filesystem/IsDir.py +70 -0
- simplibs/path/rules/filesystem/IsEmptyDir.py +76 -0
- simplibs/path/rules/filesystem/IsEmptyFile.py +76 -0
- simplibs/path/rules/filesystem/IsFile.py +76 -0
- simplibs/path/rules/filesystem/IsPathReadable.py +77 -0
- simplibs/path/rules/filesystem/IsPathWritable.py +82 -0
- simplibs/path/rules/filesystem/IsSymlink.py +71 -0
- simplibs/path/rules/filesystem/PathExists.py +75 -0
- simplibs/path/rules/filesystem/__init__.py +31 -0
- simplibs/path/rules/lexical/HasSuffix.py +146 -0
- simplibs/path/rules/lexical/IsAbsolutePath.py +74 -0
- simplibs/path/rules/lexical/IsHidden.py +92 -0
- simplibs/path/rules/lexical/IsRelativePath.py +74 -0
- simplibs/path/rules/lexical/IsValidFilename.py +136 -0
- simplibs/path/rules/lexical/IsWithin.py +116 -0
- simplibs/path/rules/lexical/NameIs.py +37 -0
- simplibs/path/rules/lexical/ParentNameIs.py +37 -0
- simplibs/path/rules/lexical/StemIs.py +36 -0
- simplibs/path/rules/lexical/__init__.py +33 -0
- simplibs_path-0.1.0.dist-info/METADATA +420 -0
- simplibs_path-0.1.0.dist-info/RECORD +128 -0
- simplibs_path-0.1.0.dist-info/WHEEL +5 -0
- simplibs_path-0.1.0.dist-info/licenses/LICENSE +21 -0
- simplibs_path-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
"""
|