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,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
|
+
"""
|