pythonwrench 0.6.4__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.
- pythonwrench/__init__.py +490 -0
- pythonwrench/__main__.py +7 -0
- pythonwrench/_core.py +192 -0
- pythonwrench/abc.py +30 -0
- pythonwrench/argparse/__init__.py +81 -0
- pythonwrench/argparse/dataclass_.py +284 -0
- pythonwrench/argparse/parsers.py +619 -0
- pythonwrench/cast.py +247 -0
- pythonwrench/checksum.py +427 -0
- pythonwrench/collections/__init__.py +104 -0
- pythonwrench/collections/collections.py +900 -0
- pythonwrench/collections/prop.py +104 -0
- pythonwrench/collections/reducers.py +330 -0
- pythonwrench/concurrent.py +73 -0
- pythonwrench/csv.py +12 -0
- pythonwrench/dataclasses.py +117 -0
- pythonwrench/datetime.py +17 -0
- pythonwrench/difflib.py +39 -0
- pythonwrench/disk_cache.py +615 -0
- pythonwrench/entrypoints/info.py +44 -0
- pythonwrench/entrypoints/safe_rmdir.py +98 -0
- pythonwrench/entrypoints/tree.py +113 -0
- pythonwrench/enum.py +55 -0
- pythonwrench/functools.py +234 -0
- pythonwrench/hashlib.py +95 -0
- pythonwrench/importlib.py +243 -0
- pythonwrench/inspect.py +69 -0
- pythonwrench/json.py +12 -0
- pythonwrench/jsonl.py +12 -0
- pythonwrench/logging.py +252 -0
- pythonwrench/math.py +107 -0
- pythonwrench/os.py +226 -0
- pythonwrench/pickle.py +12 -0
- pythonwrench/random.py +60 -0
- pythonwrench/re.py +139 -0
- pythonwrench/semver.py +406 -0
- pythonwrench/serialization/__init__.py +70 -0
- pythonwrench/serialization/_core.py +70 -0
- pythonwrench/serialization/csv.py +493 -0
- pythonwrench/serialization/json.py +178 -0
- pythonwrench/serialization/jsonl.py +215 -0
- pythonwrench/serialization/pickle.py +186 -0
- pythonwrench/time.py +34 -0
- pythonwrench/typing/__init__.py +125 -0
- pythonwrench/typing/checks.py +551 -0
- pythonwrench/typing/classes.py +251 -0
- pythonwrench/warnings.py +118 -0
- pythonwrench-0.6.4.dist-info/METADATA +242 -0
- pythonwrench-0.6.4.dist-info/RECORD +52 -0
- pythonwrench-0.6.4.dist-info/WHEEL +4 -0
- pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
- pythonwrench-0.6.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from pythonwrench.argparse import str_to_bool
|
|
10
|
+
from pythonwrench.os import safe_rmdir
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_safe_rmdir(
|
|
16
|
+
root: Union[str, Path],
|
|
17
|
+
*,
|
|
18
|
+
rm_root: bool = True,
|
|
19
|
+
error_on_non_empty_dir: bool = True,
|
|
20
|
+
followlinks: bool = False,
|
|
21
|
+
dry_run: bool = False,
|
|
22
|
+
verbose: int = 0,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Perform the print safe rmdir operation."""
|
|
25
|
+
deleted, reviewed = safe_rmdir(
|
|
26
|
+
root=root,
|
|
27
|
+
rm_root=rm_root,
|
|
28
|
+
error_on_non_empty_dir=error_on_non_empty_dir,
|
|
29
|
+
followlinks=followlinks,
|
|
30
|
+
dry_run=dry_run,
|
|
31
|
+
verbose=verbose,
|
|
32
|
+
)
|
|
33
|
+
if dry_run:
|
|
34
|
+
msg = f"Dry run mode enabled. Here is the list of directories to delete ({len(deleted)}/{len(reviewed)}):"
|
|
35
|
+
print(msg)
|
|
36
|
+
for path in deleted:
|
|
37
|
+
print(f" - {path}")
|
|
38
|
+
|
|
39
|
+
elif verbose >= 1:
|
|
40
|
+
print(f"{len(deleted)} directories has been deleted.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main_safe_rmdir() -> None:
|
|
44
|
+
"""Perform the main safe rmdir operation."""
|
|
45
|
+
parser = ArgumentParser()
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"root",
|
|
48
|
+
type=str,
|
|
49
|
+
help="Root directory path.",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--rm_root",
|
|
53
|
+
"--rm-root",
|
|
54
|
+
type=str_to_bool,
|
|
55
|
+
default=True,
|
|
56
|
+
help="If True, remove the root directory too if it is empty at the end. defaults to True.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--error_on_non_empty_dir",
|
|
60
|
+
"--error-on-non-empty-dir",
|
|
61
|
+
type=str_to_bool,
|
|
62
|
+
default=True,
|
|
63
|
+
help="If True, raises a RuntimeError if a subdirectory contains at least 1 file. Otherwise it will ignore non-empty directories. defaults to True.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--followlinks",
|
|
67
|
+
"--follow_links",
|
|
68
|
+
"--follow-links",
|
|
69
|
+
type=str_to_bool,
|
|
70
|
+
default=False,
|
|
71
|
+
help="Indicates whether or not symbolic links shound be followed. defaults to False.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--dry_run",
|
|
75
|
+
"--dry-run",
|
|
76
|
+
type=str_to_bool,
|
|
77
|
+
default=False,
|
|
78
|
+
help="If True, does not remove any directory and just output the list of directories which could be deleted. defaults to False.",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--verbose",
|
|
82
|
+
type=int,
|
|
83
|
+
default=0,
|
|
84
|
+
help="Verbose level. defaults to 0.",
|
|
85
|
+
)
|
|
86
|
+
args = parser.parse_args()
|
|
87
|
+
print_safe_rmdir(
|
|
88
|
+
root=args.root,
|
|
89
|
+
rm_root=args.rm_root,
|
|
90
|
+
error_on_non_empty_dir=args.error_on_non_empty_dir,
|
|
91
|
+
followlinks=args.followlinks,
|
|
92
|
+
dry_run=args.dry_run,
|
|
93
|
+
verbose=args.verbose,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main_safe_rmdir()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from pythonwrench.argparse import str_to_bool
|
|
11
|
+
from pythonwrench.os import tree_iter
|
|
12
|
+
from pythonwrench.re import PatternListLike
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main_tree() -> None:
|
|
18
|
+
"""Perform the main tree operation."""
|
|
19
|
+
parser = ArgumentParser()
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"root",
|
|
22
|
+
type=str,
|
|
23
|
+
help="Root directory path.",
|
|
24
|
+
default=".",
|
|
25
|
+
nargs="?", # for optional positional argument
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--include",
|
|
29
|
+
type=str,
|
|
30
|
+
help="Include file/dir patterns.",
|
|
31
|
+
default=".*",
|
|
32
|
+
nargs="*",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--exclude",
|
|
36
|
+
type=str,
|
|
37
|
+
help="Exclude file/dir patterns.",
|
|
38
|
+
default=(),
|
|
39
|
+
nargs="*",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--max_depth",
|
|
43
|
+
type=int,
|
|
44
|
+
help="Max directory tree depth.",
|
|
45
|
+
default=sys.maxsize,
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--followlinks",
|
|
49
|
+
type=str_to_bool,
|
|
50
|
+
help="Indicates whether or not symbolic links should be followed. defaults to True.",
|
|
51
|
+
default=True,
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--skipfiles",
|
|
55
|
+
type=str_to_bool,
|
|
56
|
+
help="Indicates whether or not symbolic files should be shown. defaults to False.",
|
|
57
|
+
default=False,
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--sort",
|
|
61
|
+
type=str_to_bool,
|
|
62
|
+
help="Sort element by name. defaults to False.",
|
|
63
|
+
default=False,
|
|
64
|
+
)
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
print_tree(
|
|
68
|
+
root=args.root,
|
|
69
|
+
include=args.include,
|
|
70
|
+
exclude=args.exclude,
|
|
71
|
+
max_depth=args.max_depth,
|
|
72
|
+
followlinks=args.followlinks,
|
|
73
|
+
skipfiles=args.skipfiles,
|
|
74
|
+
sort=args.sort,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def print_tree(
|
|
79
|
+
root: Union[str, Path],
|
|
80
|
+
*,
|
|
81
|
+
include: PatternListLike = ".*",
|
|
82
|
+
exclude: PatternListLike = (),
|
|
83
|
+
max_depth: int = sys.maxsize,
|
|
84
|
+
followlinks: bool = False,
|
|
85
|
+
skipfiles: bool = False,
|
|
86
|
+
sort: bool = False,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Print directory tree to stdout."""
|
|
89
|
+
num_dirs = 0
|
|
90
|
+
num_files = 0
|
|
91
|
+
|
|
92
|
+
iterable = tree_iter(
|
|
93
|
+
root=root,
|
|
94
|
+
include=include,
|
|
95
|
+
exclude=exclude,
|
|
96
|
+
max_depth=max_depth,
|
|
97
|
+
followlinks=followlinks,
|
|
98
|
+
skipfiles=skipfiles,
|
|
99
|
+
sort=sort,
|
|
100
|
+
)
|
|
101
|
+
for line in iterable:
|
|
102
|
+
print(f"{line}")
|
|
103
|
+
|
|
104
|
+
if line.endswith("/"):
|
|
105
|
+
num_dirs += 1
|
|
106
|
+
else:
|
|
107
|
+
num_files += 1
|
|
108
|
+
|
|
109
|
+
print(f"\n{num_dirs} directories, {num_files} files")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main_tree()
|
pythonwrench/enum.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StrEnum(str, Enum):
|
|
10
|
+
"""StrEnum is the same as Enum, but its members are also strings and can be used in most of the same places that a string can be used.
|
|
11
|
+
|
|
12
|
+
Note: when used as keys of dicts, enums are considered different from strings keys.
|
|
13
|
+
|
|
14
|
+
This class has the same objective than https://docs.python.org/3/library/enum.html#enum.StrEnum, which was introduced in Python 3.11.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_str(
|
|
19
|
+
cls,
|
|
20
|
+
value: str,
|
|
21
|
+
case_sensitive: bool = False,
|
|
22
|
+
) -> Self:
|
|
23
|
+
"""Create an instance from str."""
|
|
24
|
+
members = cls.__members__.keys()
|
|
25
|
+
for member in members:
|
|
26
|
+
if member == value or (
|
|
27
|
+
not case_sensitive and member.lower() == value.lower()
|
|
28
|
+
):
|
|
29
|
+
return cls[member]
|
|
30
|
+
|
|
31
|
+
msg = f"Invalid argument {value=}. (expected one of {tuple(members)})"
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _generate_next_value_(name, start, count, last_values) -> str:
|
|
36
|
+
"""Perform the generate next value operation."""
|
|
37
|
+
return name
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def value(self) -> str:
|
|
41
|
+
"""Perform the value operation."""
|
|
42
|
+
return self._value_
|
|
43
|
+
|
|
44
|
+
def __eq__(self, other: object) -> bool:
|
|
45
|
+
"""Return whether this instance equals another object."""
|
|
46
|
+
other = other.value if isinstance(other, Enum) else str(other)
|
|
47
|
+
return self.value == other # type: ignore
|
|
48
|
+
|
|
49
|
+
def __hash__(self) -> int:
|
|
50
|
+
"""Return the hash of the instance."""
|
|
51
|
+
return hash(self.value)
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
"""Return the string representation of the instance."""
|
|
55
|
+
return self.name
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from types import CodeType
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Callable,
|
|
9
|
+
Generic,
|
|
10
|
+
Iterable,
|
|
11
|
+
Optional,
|
|
12
|
+
Tuple,
|
|
13
|
+
TypeVar,
|
|
14
|
+
overload,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from typing_extensions import ParamSpec
|
|
18
|
+
|
|
19
|
+
from pythonwrench._core import T_Function, _decorator_factory, return_none # noqa: F401
|
|
20
|
+
from pythonwrench.inspect import get_argnames, get_fullname
|
|
21
|
+
from pythonwrench.typing import isinstance_generic
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
U = TypeVar("U")
|
|
25
|
+
P = ParamSpec("P")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Compose(Generic[T, U]):
|
|
29
|
+
"""Compose callables to chain calls sequentially."""
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
"""Initialize the instance."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@overload
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
fn0: Iterable[Callable[[T], T]],
|
|
40
|
+
/,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize the instance."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
fn0: Callable[[T], U],
|
|
49
|
+
/,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Initialize the instance."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
fn0: Callable[[T], Any],
|
|
58
|
+
fn1: Callable[[Any], U],
|
|
59
|
+
/,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize the instance."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
fn0: Callable[[T], Any],
|
|
68
|
+
fn1: Callable[[Any], Any],
|
|
69
|
+
fn2: Callable[[Any], U],
|
|
70
|
+
/,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialize the instance."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
fn0: Callable[[T], Any],
|
|
79
|
+
fn1: Callable[[Any], Any],
|
|
80
|
+
fn2: Callable[[Any], Any],
|
|
81
|
+
fn3: Callable[[Any], U],
|
|
82
|
+
/,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Initialize the instance."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
fn0: Callable[[T], Any],
|
|
91
|
+
fn1: Callable[[Any], Any],
|
|
92
|
+
fn2: Callable[[Any], Any],
|
|
93
|
+
fn3: Callable[[Any], Any],
|
|
94
|
+
fn4: Callable[[Any], U],
|
|
95
|
+
/,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Initialize the instance."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
def __init__(self, *fns: Callable) -> None:
|
|
102
|
+
"""Initialize the instance."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def __init__(self, *fns) -> None:
|
|
106
|
+
"""Initialize the instance."""
|
|
107
|
+
if isinstance_generic(fns, Tuple[Iterable[Callable]]):
|
|
108
|
+
fns = fns[0]
|
|
109
|
+
elif isinstance_generic(fns, Tuple[Callable, ...]):
|
|
110
|
+
pass
|
|
111
|
+
else:
|
|
112
|
+
msg = f"Invalid argument types {type(fns)=}. (with {fns=})"
|
|
113
|
+
raise TypeError(msg)
|
|
114
|
+
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.fns = fns
|
|
117
|
+
|
|
118
|
+
def __call__(self, x: T) -> U:
|
|
119
|
+
"""Call the instance."""
|
|
120
|
+
for fn in self.fns:
|
|
121
|
+
x = fn(x)
|
|
122
|
+
return x # type: ignore
|
|
123
|
+
|
|
124
|
+
def __getitem__(self, idx: int, /) -> Callable[[Any], Any]:
|
|
125
|
+
"""Return the item at the requested index or key."""
|
|
126
|
+
return self.fns[idx]
|
|
127
|
+
|
|
128
|
+
def __len__(self) -> int:
|
|
129
|
+
"""Return the number of items in the instance."""
|
|
130
|
+
return len(self.fns)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
compose = Compose # type: ignore
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def filter_and_call(
|
|
137
|
+
fn: Callable[..., T],
|
|
138
|
+
_fill_all_arguments: bool = False,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> T:
|
|
141
|
+
"""Call object only with the valid keyword arguments. Non-valid arguments are ignored.
|
|
142
|
+
|
|
143
|
+
Arguments:
|
|
144
|
+
fn: Callable to call.
|
|
145
|
+
_fill_all_arguments: If True, all arguments of fn must be provided in kwargs. defaults to False.
|
|
146
|
+
**kwargs: Superset of arguments to pass to fn. Name that does not match any argument of fn are ignored.
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
---------
|
|
150
|
+
>>> def f(x, y):
|
|
151
|
+
>>> return x + y
|
|
152
|
+
>>> filter_and_call(f, y=2, x=1)
|
|
153
|
+
... 3
|
|
154
|
+
>>> filter_and_call(f, y=2, x=1, z=0) # z is ignored
|
|
155
|
+
... 3
|
|
156
|
+
"""
|
|
157
|
+
argnames = get_argnames(fn)
|
|
158
|
+
code, _start = _get_code_and_start(fn)
|
|
159
|
+
|
|
160
|
+
if "_fill_all_arguments" in argnames:
|
|
161
|
+
msg = f"Invalid argument {get_fullname(fn)}, because it has argument '_fill_all_arguments'."
|
|
162
|
+
raise RuntimeError(msg)
|
|
163
|
+
|
|
164
|
+
if _fill_all_arguments:
|
|
165
|
+
missing = set(argnames).difference(kwargs)
|
|
166
|
+
if len(missing) > 0:
|
|
167
|
+
msg = f"Missing {len(missing)}/{len(argnames)} arguments: {tuple(missing)}. (with {_fill_all_arguments=})"
|
|
168
|
+
raise ValueError(msg)
|
|
169
|
+
|
|
170
|
+
pos_argnames = argnames[: code.co_posonlyargcount]
|
|
171
|
+
other_argnames = argnames[code.co_posonlyargcount :]
|
|
172
|
+
|
|
173
|
+
posonly_args = {
|
|
174
|
+
name: value for name, value in kwargs.items() if name in pos_argnames
|
|
175
|
+
}
|
|
176
|
+
other_kwds = {
|
|
177
|
+
name: value for name, value in kwargs.items() if name in other_argnames
|
|
178
|
+
}
|
|
179
|
+
result = fn(*posonly_args.values(), **other_kwds)
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def function_alias(
|
|
184
|
+
alternative: T_Function,
|
|
185
|
+
*,
|
|
186
|
+
pre_fn: Optional[Callable[..., Any]] = None,
|
|
187
|
+
post_fn: Optional[Callable[..., Any]] = None,
|
|
188
|
+
) -> Callable[..., T_Function]:
|
|
189
|
+
"""Decorator to wrap function aliases.
|
|
190
|
+
|
|
191
|
+
Unlike setting directly an alias, this preserve the nature of the alias for language server.
|
|
192
|
+
|
|
193
|
+
Example
|
|
194
|
+
-------
|
|
195
|
+
>>> def f(a: int, b: str) -> str:
|
|
196
|
+
>>> return a * b
|
|
197
|
+
>>> @function_alias(f)
|
|
198
|
+
>>> def g(*args, **kwargs): ...
|
|
199
|
+
>>> f(2, "a")
|
|
200
|
+
... "aa"
|
|
201
|
+
>>> g(3, "b") # calls function f() internally.
|
|
202
|
+
... "bbb"
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
return _decorator_factory(alternative, pre_fn=pre_fn, post_fn=post_fn)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def identity(x: T, **kwargs) -> T:
|
|
209
|
+
"""Identity function placeholder. Returns the first argument. Other keywords arguments are ignored."""
|
|
210
|
+
return x
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def repeat_fn(f: Callable[[T], T], n: int) -> Callable[[T], T]:
|
|
214
|
+
"""Creates wrapper which call a function n items."""
|
|
215
|
+
return Compose([f] * n)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]:
|
|
219
|
+
"""Perform the get code and start operation."""
|
|
220
|
+
if inspect.isfunction(fn):
|
|
221
|
+
code = fn.__code__
|
|
222
|
+
start = 0
|
|
223
|
+
elif inspect.ismethod(fn):
|
|
224
|
+
code = fn.__code__
|
|
225
|
+
start = 1 # If method, remove 'self' arg
|
|
226
|
+
elif inspect.isclass(fn):
|
|
227
|
+
# If init, remove 'self' arg
|
|
228
|
+
code = fn.__init__.__code__
|
|
229
|
+
start = 1 # If init, remove 'self' arg
|
|
230
|
+
else:
|
|
231
|
+
code = fn.__call__.__code__
|
|
232
|
+
start = 0
|
|
233
|
+
|
|
234
|
+
return code, start
|
pythonwrench/hashlib.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
from io import BufferedReader
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal, Optional, Protocol, Union, get_args, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Buffer
|
|
11
|
+
|
|
12
|
+
HasherName = Literal["sha256", "md5"]
|
|
13
|
+
HashName = HasherName # alias
|
|
14
|
+
|
|
15
|
+
DEFAULT_CHUNK_SIZE = 256 * 1024**2 # 256 MiB
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class Hasher(Protocol):
|
|
22
|
+
"""Hasher protocol class."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def digest_size(self) -> int: ...
|
|
26
|
+
@property
|
|
27
|
+
def block_size(self) -> int: ...
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str: ...
|
|
30
|
+
|
|
31
|
+
def digest(self) -> bytes:
|
|
32
|
+
"""Perform the digest operation."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def update(self, data: Buffer, /) -> None:
|
|
36
|
+
"""Perform the update operation."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def hash_file(
|
|
41
|
+
fpath: Union[str, Path, BufferedReader],
|
|
42
|
+
hash_type: Union[HasherName, Hasher] = "md5",
|
|
43
|
+
chunk_size: Optional[int] = DEFAULT_CHUNK_SIZE,
|
|
44
|
+
*,
|
|
45
|
+
init_bytes: bytes = b"",
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Return the hash value for a file.
|
|
48
|
+
|
|
49
|
+
Based on https://github.com/pytorch/audio/blob/v0.13.0/torchaudio/datasets/utils.py#L110
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
fpath: Path to existing file.
|
|
53
|
+
hash_type: Hash name or custom Hasher algorithm.
|
|
54
|
+
chunk_size: Max chunk size in bytes. defaults to 268435456 (256 MiB).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Hash value as string.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(fpath, (str, Path)):
|
|
60
|
+
with open(fpath, "rb") as file:
|
|
61
|
+
return hash_file(file, hash_type, chunk_size)
|
|
62
|
+
else:
|
|
63
|
+
file = fpath
|
|
64
|
+
del fpath
|
|
65
|
+
|
|
66
|
+
if isinstance(hash_type, str):
|
|
67
|
+
hasher = _get_hasher(hash_type, init_bytes)
|
|
68
|
+
elif isinstance(hash_type, Hasher):
|
|
69
|
+
hasher = hash_type
|
|
70
|
+
else:
|
|
71
|
+
msg = f"Invalid argument {hash_type=}. (expected one of {get_args(HasherName)} or custom Hasher type)"
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
del hash_type
|
|
74
|
+
|
|
75
|
+
while True:
|
|
76
|
+
chunk = file.read(chunk_size)
|
|
77
|
+
if not chunk:
|
|
78
|
+
break
|
|
79
|
+
hasher.update(chunk)
|
|
80
|
+
|
|
81
|
+
hash_bytes = hasher.digest()
|
|
82
|
+
return hash_bytes.hex()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_hasher(hasher_name: HasherName, init_bytes: bytes = b"") -> Hasher:
|
|
86
|
+
if hasher_name == "sha256":
|
|
87
|
+
hasher = hashlib.sha256(init_bytes)
|
|
88
|
+
elif hasher_name == "md5":
|
|
89
|
+
hasher = hashlib.md5(init_bytes)
|
|
90
|
+
else:
|
|
91
|
+
msg = (
|
|
92
|
+
f"Invalid argument {hasher_name=}. (expected one of {get_args(HasherName)})"
|
|
93
|
+
)
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
return hasher
|