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.
Files changed (52) hide show
  1. pythonwrench/__init__.py +490 -0
  2. pythonwrench/__main__.py +7 -0
  3. pythonwrench/_core.py +192 -0
  4. pythonwrench/abc.py +30 -0
  5. pythonwrench/argparse/__init__.py +81 -0
  6. pythonwrench/argparse/dataclass_.py +284 -0
  7. pythonwrench/argparse/parsers.py +619 -0
  8. pythonwrench/cast.py +247 -0
  9. pythonwrench/checksum.py +427 -0
  10. pythonwrench/collections/__init__.py +104 -0
  11. pythonwrench/collections/collections.py +900 -0
  12. pythonwrench/collections/prop.py +104 -0
  13. pythonwrench/collections/reducers.py +330 -0
  14. pythonwrench/concurrent.py +73 -0
  15. pythonwrench/csv.py +12 -0
  16. pythonwrench/dataclasses.py +117 -0
  17. pythonwrench/datetime.py +17 -0
  18. pythonwrench/difflib.py +39 -0
  19. pythonwrench/disk_cache.py +615 -0
  20. pythonwrench/entrypoints/info.py +44 -0
  21. pythonwrench/entrypoints/safe_rmdir.py +98 -0
  22. pythonwrench/entrypoints/tree.py +113 -0
  23. pythonwrench/enum.py +55 -0
  24. pythonwrench/functools.py +234 -0
  25. pythonwrench/hashlib.py +95 -0
  26. pythonwrench/importlib.py +243 -0
  27. pythonwrench/inspect.py +69 -0
  28. pythonwrench/json.py +12 -0
  29. pythonwrench/jsonl.py +12 -0
  30. pythonwrench/logging.py +252 -0
  31. pythonwrench/math.py +107 -0
  32. pythonwrench/os.py +226 -0
  33. pythonwrench/pickle.py +12 -0
  34. pythonwrench/random.py +60 -0
  35. pythonwrench/re.py +139 -0
  36. pythonwrench/semver.py +406 -0
  37. pythonwrench/serialization/__init__.py +70 -0
  38. pythonwrench/serialization/_core.py +70 -0
  39. pythonwrench/serialization/csv.py +493 -0
  40. pythonwrench/serialization/json.py +178 -0
  41. pythonwrench/serialization/jsonl.py +215 -0
  42. pythonwrench/serialization/pickle.py +186 -0
  43. pythonwrench/time.py +34 -0
  44. pythonwrench/typing/__init__.py +125 -0
  45. pythonwrench/typing/checks.py +551 -0
  46. pythonwrench/typing/classes.py +251 -0
  47. pythonwrench/warnings.py +118 -0
  48. pythonwrench-0.6.4.dist-info/METADATA +242 -0
  49. pythonwrench-0.6.4.dist-info/RECORD +52 -0
  50. pythonwrench-0.6.4.dist-info/WHEEL +4 -0
  51. pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
  52. pythonwrench-0.6.4.dist-info/licenses/LICENSE +21 -0
pythonwrench/semver.py ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import logging
5
+ import re
6
+ import sys
7
+ from dataclasses import asdict, dataclass
8
+ from typing import Any, List, Mapping, Tuple, TypedDict, Union, overload
9
+
10
+ from typing_extensions import NotRequired, Self, TypeAlias
11
+
12
+ from pythonwrench.typing import NoneType, isinstance_generic
13
+
14
+ PreRelease: TypeAlias = Union[int, str, None, List[Union[int, str]]]
15
+ BuildMetadata: TypeAlias = Union[int, str, None, List[Union[int, str]]]
16
+
17
+ # Pattern of https://semver.org/
18
+ _VERSION_PATTERN = r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
19
+ _VERSION_FORMAT = r"{major}.{minor}.{patch}"
20
+ _VERSION_KEYS = ("major", "minor", "patch", "prerelease", "buildmetadata")
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class VersionDict(TypedDict):
27
+ """TypedDict which represents a Version."""
28
+
29
+ major: int
30
+ minor: int
31
+ patch: int
32
+ prerelease: NotRequired[PreRelease]
33
+ buildmetadata: NotRequired[BuildMetadata]
34
+
35
+
36
+ VersionTuple: TypeAlias = Union[
37
+ Tuple[int, int, int],
38
+ Tuple[int, int, int, PreRelease],
39
+ Tuple[int, int, int, PreRelease, BuildMetadata],
40
+ ]
41
+
42
+ VersionDictLike: TypeAlias = Mapping[str, Union[int, PreRelease, BuildMetadata]]
43
+ VersionTupleLike: TypeAlias = Tuple[Union[int, PreRelease, BuildMetadata], ...]
44
+ VersionLike: TypeAlias = Union["Version", str, VersionDictLike, VersionTupleLike]
45
+
46
+
47
+ @dataclass(init=False, eq=False)
48
+ class Version:
49
+ """Version utility class following Semantic Versioning (SemVer) spec.
50
+
51
+ Version format is: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILDMETADATA]
52
+
53
+ Based on https://semver.org/ version 2.0.0.
54
+ """
55
+
56
+ major: int
57
+ minor: int
58
+ patch: int
59
+ prerelease: PreRelease
60
+ buildmetadata: BuildMetadata
61
+
62
+ @overload
63
+ def __init__(
64
+ self,
65
+ version: Self,
66
+ /,
67
+ ) -> None:
68
+ """Initialize the instance."""
69
+ ...
70
+
71
+ @overload
72
+ def __init__(
73
+ self,
74
+ version_str: str,
75
+ /,
76
+ ) -> None:
77
+ """Initialize the instance."""
78
+ ...
79
+
80
+ @overload
81
+ def __init__(
82
+ self,
83
+ version_dict: VersionDictLike,
84
+ /,
85
+ ) -> None:
86
+ """Initialize the instance."""
87
+ ...
88
+
89
+ @overload
90
+ def __init__(
91
+ self,
92
+ version_tuple: VersionTupleLike,
93
+ /,
94
+ ) -> None:
95
+ """Initialize the instance."""
96
+ ...
97
+
98
+ @overload
99
+ def __init__(
100
+ self,
101
+ major: int,
102
+ minor: int,
103
+ patch: int,
104
+ prerelease: PreRelease = None,
105
+ buildmetadata: BuildMetadata = None,
106
+ ) -> None:
107
+ """Initialize the instance."""
108
+ ...
109
+
110
+ def __init__(self, *args, **kwargs) -> None:
111
+ """Initialize the instance."""
112
+ has_1_pos_arg = len(args) == 1 and len(kwargs) == 0
113
+ # Version
114
+ if has_1_pos_arg and isinstance(args[0], Version):
115
+ version = args[0]
116
+ version_dict = version.to_dict(exclude_none=False)
117
+
118
+ # Version str
119
+ elif has_1_pos_arg and isinstance(args[0], str):
120
+ version_str = args[0]
121
+ version_dict = _parse_version_str(version_str)
122
+
123
+ # Version dict
124
+ elif has_1_pos_arg and isinstance_generic(args[0], VersionDictLike):
125
+ version_dict = args[0]
126
+
127
+ # Version tuple
128
+ elif has_1_pos_arg and isinstance_generic(args[0], VersionTupleLike):
129
+ version_tuple = args[0]
130
+ version_dict = dict(zip(_VERSION_KEYS, version_tuple))
131
+
132
+ # Version args/kwargs
133
+ else:
134
+ version_dict = dict(zip(_VERSION_KEYS, args))
135
+ intersection = tuple(set(version_dict.keys()).intersection(kwargs.keys()))
136
+ if len(intersection) > 0:
137
+ msg = f"Got multiple values for argument(s) {intersection}. (with {args=} and {kwargs=})"
138
+ raise TypeError(msg)
139
+ version_dict.update(kwargs) # type: ignore
140
+
141
+ invalid = tuple(set(version_dict.keys()).difference(_VERSION_KEYS))
142
+ if len(invalid) > 0:
143
+ msg = f"Got an unexpected arguments {invalid=}. (with {args=} and {kwargs=})"
144
+ raise TypeError(msg)
145
+
146
+ if not isinstance_generic(version_dict, VersionDict):
147
+ msg = f"Invalid argument {args=} and {kwargs=}. (invalid argument types, expected (major=int, minor=int, patch=int, prerelease={PreRelease}, buildmetadata={BuildMetadata}))"
148
+ raise ValueError(msg)
149
+
150
+ major = version_dict["major"]
151
+ minor = version_dict["minor"]
152
+ patch = version_dict["patch"]
153
+ prerelease = version_dict.get("prerelease", None)
154
+ buildmetadata = version_dict.get("buildmetadata", None)
155
+
156
+ self.major = major # type: ignore
157
+ self.minor = minor # type: ignore
158
+ self.patch = patch # type: ignore
159
+ self.prerelease = prerelease # type: ignore
160
+ self.buildmetadata = buildmetadata # type: ignore
161
+
162
+ @classmethod
163
+ def from_dict(cls, version_dict: VersionDictLike) -> Self:
164
+ """Create an instance from dict."""
165
+ return cls(version_dict)
166
+
167
+ @classmethod
168
+ def from_str(cls, version_str: str) -> Self:
169
+ """Create an instance from str."""
170
+ return cls(version_str)
171
+
172
+ @classmethod
173
+ def from_tuple(cls, version_tuple: VersionTupleLike) -> Self:
174
+ """Create an instance from tuple."""
175
+ return cls(version_tuple)
176
+
177
+ @classmethod
178
+ def python(cls, releaselevel_in_metadata: bool = False) -> Self:
179
+ """Create an instance of Version with Python version.
180
+
181
+ Note: Python 'micro' value is mapped to 'patch'.
182
+ """
183
+ if releaselevel_in_metadata:
184
+ buildmetadata = sys.version_info.releaselevel
185
+ else:
186
+ buildmetadata = None
187
+
188
+ return cls(
189
+ major=sys.version_info.major,
190
+ minor=sys.version_info.minor,
191
+ patch=sys.version_info.micro,
192
+ buildmetadata=buildmetadata,
193
+ )
194
+
195
+ @property
196
+ def micro(self) -> int:
197
+ """Getter alias of 'patch'."""
198
+ return self.patch
199
+
200
+ @micro.setter
201
+ def micro(self, new_value: int) -> None:
202
+ """Setter alias of 'patch'."""
203
+ self.patch = new_value
204
+
205
+ def without_prerelease(self) -> "Version":
206
+ """Perform the without prerelease operation."""
207
+ return Version(self.major, self.minor, self.patch, None, self.buildmetadata)
208
+
209
+ def without_buildmetadata(self) -> "Version":
210
+ """Perform the without buildmetadata operation."""
211
+ return Version(self.major, self.minor, self.patch, self.prerelease, None)
212
+
213
+ def next_major(
214
+ self,
215
+ keep_prerelease: bool = False,
216
+ keep_buildmetadata: bool = False,
217
+ ) -> "Version":
218
+ """Perform the next major operation."""
219
+ prerelease = self.prerelease if keep_prerelease else None
220
+ buildmetadata = self.buildmetadata if keep_buildmetadata else None
221
+ return Version(
222
+ major=self.major + 1,
223
+ minor=0,
224
+ patch=0,
225
+ prerelease=prerelease,
226
+ buildmetadata=buildmetadata,
227
+ )
228
+
229
+ def next_minor(
230
+ self,
231
+ keep_prerelease: bool = False,
232
+ keep_buildmetadata: bool = False,
233
+ ) -> "Version":
234
+ """Perform the next minor operation."""
235
+ prerelease = self.prerelease if keep_prerelease else None
236
+ buildmetadata = self.buildmetadata if keep_buildmetadata else None
237
+ return Version(
238
+ major=self.major,
239
+ minor=self.minor + 1,
240
+ patch=0,
241
+ prerelease=prerelease,
242
+ buildmetadata=buildmetadata,
243
+ )
244
+
245
+ def next_patch(
246
+ self,
247
+ keep_prerelease: bool = False,
248
+ keep_buildmetadata: bool = False,
249
+ ) -> "Version":
250
+ """Perform the next patch operation."""
251
+ prerelease = self.prerelease if keep_prerelease else None
252
+ buildmetadata = self.buildmetadata if keep_buildmetadata else None
253
+ return Version(
254
+ major=self.major,
255
+ minor=self.minor,
256
+ patch=self.patch + 1,
257
+ prerelease=prerelease,
258
+ buildmetadata=buildmetadata,
259
+ )
260
+
261
+ def to_dict(self, exclude_none: bool = True) -> VersionDict:
262
+ """Convert the value to dict."""
263
+ version_dict = asdict(self)
264
+ if exclude_none:
265
+ version_dict = {k: v for k, v in version_dict.items() if v is not None}
266
+ return version_dict # type: ignore
267
+
268
+ def to_str(self) -> str:
269
+ """Convert the value to str."""
270
+ kwds = dict(
271
+ major=self.major,
272
+ minor=self.minor,
273
+ patch=self.patch,
274
+ )
275
+ version_str = _VERSION_FORMAT.format(**kwds)
276
+ if self.prerelease is not None:
277
+ version_str = f"{version_str}-{self.prerelease}"
278
+ if self.buildmetadata is not None:
279
+ version_str = f"{version_str}+{self.buildmetadata}"
280
+
281
+ return version_str
282
+
283
+ def to_tuple(
284
+ self,
285
+ exclude_none: bool = True,
286
+ ) -> VersionTuple:
287
+ """Convert the value to tuple."""
288
+ version_tuple = tuple(self.to_dict(exclude_none).values())
289
+ return version_tuple # type: ignore
290
+
291
+ def equals(self, other: VersionLike, *, ignore_buildmetadata: bool = False) -> bool:
292
+ """Perform the equals operation."""
293
+ if isinstance(other, (Mapping, tuple, str)):
294
+ other = Version(other)
295
+ # note: use self.__class__ to avoid error cause by 'pytest -v test' collect
296
+ elif not isinstance(other, (Version, self.__class__)):
297
+ return False
298
+
299
+ return (
300
+ self.major == other.major
301
+ and self.minor == other.minor
302
+ and self.patch == other.patch
303
+ and self.prerelease == other.prerelease
304
+ and (ignore_buildmetadata or self.buildmetadata == other.buildmetadata)
305
+ )
306
+
307
+ def __str__(self) -> str:
308
+ """Return the string representation of the instance."""
309
+ return self.to_str()
310
+
311
+ def __eq__(self, other: Any) -> bool:
312
+ """Return whether this instance equals another object."""
313
+ return self.equals(other)
314
+
315
+ def __lt__(self, other: VersionLike) -> bool:
316
+ """Return whether this instance is less than another."""
317
+ return _compare_lt(self, other)
318
+
319
+ def __le__(self, other: VersionLike) -> bool:
320
+ """Return whether this instance is less than or equal to another."""
321
+ return (self == other) or (self < other)
322
+
323
+ def __gt__(self, other: VersionLike) -> bool:
324
+ """Return whether this instance is greater than another."""
325
+ return _compare_lt(other, self)
326
+
327
+ def __ge__(self, other: VersionLike) -> bool:
328
+ """Return whether this instance is greater than or equal to another."""
329
+ return (self == other) or (self > other)
330
+
331
+
332
+ def _compare_lt(
333
+ x: Union[Version, Mapping, tuple, str], y: Union[Version, Mapping, tuple, str]
334
+ ) -> bool:
335
+ """Perform the compare lt operation."""
336
+ if isinstance(x, (Mapping, tuple, str)):
337
+ x = Version(x)
338
+ if isinstance(y, (Mapping, tuple, str)):
339
+ y = Version(y)
340
+
341
+ self_tuple = x.to_tuple(exclude_none=False)
342
+ other_tuple = y.to_tuple(exclude_none=False)
343
+
344
+ self_tuple = self_tuple[:4]
345
+ other_tuple = other_tuple[:4]
346
+
347
+ for self_v, other_v in zip(self_tuple, other_tuple):
348
+ if self_v == other_v:
349
+ continue
350
+ if self_v is None and other_v is not None:
351
+ return False
352
+ if self_v is not None and other_v is None:
353
+ return True
354
+
355
+ if isinstance(self_v, (int, str, NoneType)):
356
+ self_v = [self_v]
357
+ elif not isinstance(self_v, list):
358
+ raise TypeError(f"Invalid argument type {type(self_v)}.")
359
+
360
+ if isinstance(other_v, (int, str, NoneType)):
361
+ other_v = [other_v]
362
+ elif not isinstance(other_v, list):
363
+ raise TypeError(f"Invalid argument type {type(other_v)}.")
364
+
365
+ minlen = min(len(self_v), len(other_v))
366
+ if len(self_v) != len(other_v) and self_v[:minlen] == other_v[:minlen]:
367
+ return len(self_v) < len(other_v)
368
+
369
+ for self_vi, other_vi in zip(self_v, other_v):
370
+ if self_vi == other_vi:
371
+ continue
372
+ if isinstance(self_vi, int) and isinstance(other_vi, int):
373
+ return self_vi < other_vi
374
+ if isinstance(self_vi, int) and isinstance(other_vi, str):
375
+ return True
376
+ if isinstance(self_vi, str) and isinstance(other_vi, int):
377
+ return False
378
+ if isinstance(self_vi, str) and isinstance(other_vi, str):
379
+ return self_vi < other_vi
380
+
381
+ msg = f"Invalid attribute type {self_vi=} and {other_vi=}."
382
+ raise TypeError(msg)
383
+
384
+ return False
385
+
386
+
387
+ def _parse_version_str(version_str: str) -> VersionDict:
388
+ """Parse version str."""
389
+ version_match = re.match(_VERSION_PATTERN, version_str)
390
+ if version_match is None:
391
+ msg = f"Invalid argument {version_str=}. (not a version)"
392
+ raise ValueError(msg)
393
+
394
+ version_dict = version_match.groupdict()
395
+ result = {}
396
+ for k, v in version_dict.items():
397
+ if isinstance(v, str) and "." in v:
398
+ v = v.split(".")
399
+ else:
400
+ v = [v]
401
+
402
+ v = [int(vi) if isinstance(vi, str) and vi.isdigit() else vi for vi in v]
403
+ if len(v) == 1:
404
+ v = v[0]
405
+ result[k] = v
406
+ return result # type: ignore
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ try:
7
+ import lazy_loader as lazy # type: ignore
8
+ except ImportError:
9
+ lazy = None
10
+
11
+
12
+ if TYPE_CHECKING or lazy is None:
13
+ from .csv import dump_csv, dumps_csv, load_csv, loads_csv, read_csv, save_csv
14
+ from .json import dump_json, dumps_json, load_json, loads_json, read_json, save_json
15
+ from .jsonl import (
16
+ dump_jsonl,
17
+ dumps_jsonl,
18
+ load_jsonl,
19
+ loads_jsonl,
20
+ read_jsonl,
21
+ save_jsonl,
22
+ )
23
+ from .pickle import (
24
+ dump_pickle,
25
+ dumps_pickle,
26
+ load_pickle,
27
+ loads_pickle,
28
+ read_pickle,
29
+ save_pickle,
30
+ )
31
+
32
+ else:
33
+ __getattr__, __dir__, __all__ = lazy.attach(
34
+ __name__,
35
+ submodules=["csv", "json", "jsonl", "pickle"],
36
+ submod_attrs={
37
+ "csv": [
38
+ "dump_csv",
39
+ "dumps_csv",
40
+ "load_csv",
41
+ "loads_csv",
42
+ "read_csv",
43
+ "save_csv",
44
+ ],
45
+ "json": [
46
+ "dump_json",
47
+ "dumps_json",
48
+ "load_json",
49
+ "loads_json",
50
+ "read_json",
51
+ "save_json",
52
+ ],
53
+ "jsonl": [
54
+ "dump_jsonl",
55
+ "dumps_jsonl",
56
+ "load_jsonl",
57
+ "loads_jsonl",
58
+ "read_jsonl",
59
+ "save_jsonl",
60
+ ],
61
+ "pickle": [
62
+ "dump_pickle",
63
+ "dumps_pickle",
64
+ "load_pickle",
65
+ "loads_pickle",
66
+ "read_pickle",
67
+ "save_pickle",
68
+ ],
69
+ },
70
+ )
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from io import TextIOWrapper
6
+ from pathlib import Path
7
+ from typing import (
8
+ Union,
9
+ overload,
10
+ )
11
+
12
+
13
+ @overload
14
+ def _setup_output_fpath(
15
+ fpath: Union[str, Path, os.PathLike],
16
+ *,
17
+ overwrite: bool = True,
18
+ make_parents: bool = True,
19
+ absolute: bool = True,
20
+ ) -> Path:
21
+ """Perform the setup output fpath operation."""
22
+ ...
23
+
24
+
25
+ @overload
26
+ def _setup_output_fpath(
27
+ fpath: TextIOWrapper,
28
+ *,
29
+ overwrite: bool = True,
30
+ make_parents: bool = True,
31
+ absolute: bool = True,
32
+ ) -> TextIOWrapper:
33
+ """Perform the setup output fpath operation."""
34
+ ...
35
+
36
+
37
+ @overload
38
+ def _setup_output_fpath(
39
+ fpath: None,
40
+ *,
41
+ overwrite: bool = True,
42
+ make_parents: bool = True,
43
+ absolute: bool = True,
44
+ ) -> None:
45
+ """Perform the setup output fpath operation."""
46
+ ...
47
+
48
+
49
+ def _setup_output_fpath(
50
+ fpath: Union[str, Path, os.PathLike, TextIOWrapper, None],
51
+ *,
52
+ overwrite: bool = True,
53
+ make_parents: bool = True,
54
+ absolute: bool = True,
55
+ ) -> Union[Path, None, TextIOWrapper]:
56
+ """Resolve path, expand path and create intermediate parents."""
57
+ if not isinstance(fpath, (str, Path, os.PathLike)):
58
+ return fpath
59
+
60
+ fpath = Path(fpath)
61
+ if absolute:
62
+ fpath = fpath.resolve().expanduser()
63
+
64
+ if not overwrite and fpath.exists():
65
+ msg = f"File {fpath} already exists."
66
+ raise FileExistsError(msg)
67
+ elif make_parents:
68
+ fpath.parent.mkdir(parents=True, exist_ok=True)
69
+
70
+ return fpath