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
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import inspect
5
+ import logging
6
+ import sys
7
+ import typing
8
+ from collections.abc import Callable as _RuntimeCallable
9
+ from dataclasses import is_dataclass
10
+ from numbers import Integral
11
+ from types import FunctionType, MethodType
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Dict,
16
+ Generator,
17
+ Iterable,
18
+ Literal,
19
+ Mapping,
20
+ Sequence,
21
+ Tuple,
22
+ Type,
23
+ TypedDict,
24
+ Union,
25
+ )
26
+
27
+ import typing_extensions
28
+ from typing_extensions import (
29
+ NotRequired,
30
+ ParamSpec,
31
+ Required,
32
+ TypeGuard,
33
+ TypeIs,
34
+ TypeVar,
35
+ get_args,
36
+ get_origin,
37
+ )
38
+
39
+ from pythonwrench.typing.classes import (
40
+ BuiltinCollection,
41
+ BuiltinNumber,
42
+ BuiltinScalar,
43
+ DataclassInstance,
44
+ NamedTupleInstance,
45
+ NoneType,
46
+ )
47
+
48
+ T = TypeVar("T")
49
+ P = ParamSpec("P")
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ def check_args_types(fn: Callable[P, T]) -> Callable[P, T]:
55
+ """Decorator to check argument types before call to a function.
56
+
57
+ Example
58
+ -------
59
+ >>> import pythonwrench as pw
60
+ >>> @pw.check_args_types
61
+ >>> def f(a: int, b: str) -> str:
62
+ >>> return a * b
63
+ >>> f(1, "a") # pass check
64
+ >>> f(1, 2) # raises TypeError from decorator
65
+ """
66
+ if not isinstance(fn, (FunctionType, MethodType)):
67
+ msg = f"Invalid argument type {type(fn)}. (expected function or method)"
68
+ raise TypeError(msg)
69
+
70
+ parameters = inspect.signature(fn).parameters
71
+ annotations = {k: v.annotation for k, v in parameters.items()}
72
+ argnames = list(annotations.keys())
73
+
74
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
75
+ """Perform the wrapper operation."""
76
+ num_positional = len(args)
77
+ given_kwargs = dict(zip(argnames[:num_positional], args))
78
+ given_kwargs.update(kwargs)
79
+
80
+ msgs = []
81
+ for i, (k, v) in enumerate(given_kwargs.items()):
82
+ if isinstance_generic(v, annotations[k]):
83
+ continue
84
+
85
+ if i < num_positional:
86
+ msg = f" - invalid argument n°{i + 1} with value {v!r}; expected an instance of {annotations[k]}."
87
+ else:
88
+ msg = f" - invalid argument '{k}' with value {v!r}; expected an instance of {annotations[k]}."
89
+ msgs.append(msg)
90
+
91
+ if len(msgs) > 0:
92
+ msgs = [
93
+ f"{fn.__name__}() has {len(msgs)}/{len(given_kwargs)} invalid argument(s):",
94
+ ] + msgs
95
+ msg = "\n".join(msgs)
96
+ raise TypeError(msg)
97
+
98
+ result = fn(*args, **kwargs)
99
+ return result
100
+
101
+ return _wrapper
102
+
103
+
104
+ def isinstance_generic(
105
+ obj: Any,
106
+ class_or_tuple: Union[Type[T], None, Tuple[Type[T], ...], Any],
107
+ *,
108
+ check_only_first: bool = False,
109
+ ) -> TypeIs[T]:
110
+ """Improved isinstance(...) function that supports parametrized Union, TypedDict, Literal, Mapping or Iterable.
111
+
112
+ Args:
113
+ obj: Object to check.
114
+ class_or_tuple: Type to check. Can be a parametrized type from `typing`.
115
+ check_only_first: If True, check only if first element when checking for Iterable[type]. defaults to False.
116
+
117
+ Example 1
118
+ ---------
119
+ >>> isinstance_generic({"a": 1, "b": 2}, dict)
120
+ ... True
121
+ >>> isinstance_generic({"a": 1, "b": 2}, dict[str, int])
122
+ ... True
123
+ >>> isinstance_generic({"a": 1, "b": 2}, dict[str, str])
124
+ ... False
125
+ >>> from typing import Literal
126
+ >>> isinstance_generic({"a": 1, "b": 2}, dict[str, Literal[1, 2]])
127
+ ... True
128
+
129
+ """
130
+ if class_or_tuple is Any or class_or_tuple is typing_extensions.Any:
131
+ return True
132
+ if class_or_tuple is None:
133
+ return obj is None
134
+ if isinstance(class_or_tuple, tuple):
135
+ return any(
136
+ isinstance_generic(obj, target_type_i) for target_type_i in class_or_tuple
137
+ )
138
+
139
+ if is_typed_dict(class_or_tuple):
140
+ return _isinstance_generic_typed_dict(obj, class_or_tuple)
141
+
142
+ origin = get_origin(class_or_tuple)
143
+ if origin is None:
144
+ return isinstance(obj, class_or_tuple) # type: ignore
145
+
146
+ # Special case for empty tuple because get_args(Tuple[()]) returns () and not ((),) in python >= 3.11
147
+ # More info at https://github.com/python/cpython/issues/91137
148
+ if class_or_tuple == Tuple[()]:
149
+ return obj == ()
150
+
151
+ args = get_args(class_or_tuple)
152
+
153
+ if _is_callable_type(origin):
154
+ if not callable(obj):
155
+ return False
156
+ if len(args) == 0:
157
+ return True
158
+ elif len(args) != 2:
159
+ msg = f"Invalid number of parameters in Callable. (found {len(args)} but expected 0 or 2)"
160
+ raise RuntimeError(msg)
161
+
162
+ sign = inspect.signature(obj)
163
+ type_params_annots, type_return_annot = args
164
+ obj_return_annot = sign.return_annotation
165
+
166
+ if obj_return_annot != type_return_annot:
167
+ return False
168
+
169
+ obj_params_annots = [param.annotation for param in sign.parameters.values()]
170
+ if type_params_annots is ...:
171
+ return True
172
+
173
+ if len(obj_params_annots) != len(type_params_annots):
174
+ return False
175
+
176
+ for obj_param_annot, type_param_annot in zip(
177
+ obj_params_annots, type_params_annots
178
+ ):
179
+ if obj_param_annot != type_param_annot:
180
+ return False
181
+
182
+ return True
183
+
184
+ if len(args) == 0:
185
+ return isinstance_generic(obj, origin)
186
+
187
+ if origin is Union:
188
+ return any(isinstance_generic(obj, arg) for arg in args)
189
+
190
+ if origin is Literal:
191
+ return obj in args
192
+
193
+ if isinstance(obj, Generator):
194
+ msg = f"Invalid argument type {type(obj)}. (cannot check elements in generator)"
195
+ raise TypeError(msg)
196
+
197
+ if issubclass(origin, Generator):
198
+ msg = f"Invalid argument type {origin}. (cannot check generator type)"
199
+ raise TypeError(msg)
200
+
201
+ if issubclass(origin, Mapping):
202
+ assert len(args) == 2, f"{args=}"
203
+ if not isinstance_generic(obj, origin):
204
+ return False
205
+
206
+ return all(isinstance_generic(k, args[0]) for k in obj.keys()) and all(
207
+ isinstance_generic(v, args[1]) for v in obj.values()
208
+ )
209
+
210
+ if issubclass(origin, Tuple):
211
+ if not isinstance_generic(obj, origin):
212
+ return False
213
+ elif len(args) == 1 and args[0] == ():
214
+ return len(obj) == 0
215
+ elif len(args) == 2 and args[1] is ...:
216
+ if check_only_first:
217
+ args = (args[0],)
218
+ else:
219
+ args = tuple([args[0]] * len(obj))
220
+ elif len(obj) != len(args):
221
+ return False
222
+ return all(isinstance_generic(xi, ti) for xi, ti in zip(obj, args))
223
+
224
+ if issubclass(origin, Iterable):
225
+ if not isinstance_generic(obj, origin):
226
+ return False
227
+
228
+ if check_only_first:
229
+ try:
230
+ return isinstance_generic(next(iter(obj)), args[0])
231
+ except StopIteration:
232
+ return True
233
+ else:
234
+ return all(isinstance_generic(xi, args[0]) for xi in obj)
235
+
236
+ msg = f"Unsupported type {class_or_tuple}. (expected unparametrized type or parametrized Union, TypedDict, Literal, Mapping or Iterable)"
237
+ raise NotImplementedError(msg)
238
+
239
+
240
+ def _isinstance_generic_typed_dict(x: Any, target_type: type) -> bool:
241
+ """Perform the isinstance generic typed dict operation."""
242
+ if not isinstance_generic(x, Dict[str, Any]):
243
+ return False
244
+
245
+ total: bool = target_type.__total__
246
+ annotations = target_type.__annotations__
247
+
248
+ required_annotations = {}
249
+ optional_annotations = {}
250
+ for k, v in annotations.items():
251
+ origin = get_origin(v)
252
+ if origin is Required:
253
+ required_annotations[k] = v
254
+ elif origin is NotRequired:
255
+ optional_annotations[k] = v
256
+ elif total:
257
+ required_annotations[k] = v
258
+ else:
259
+ optional_annotations[k] = v
260
+
261
+ if not set(required_annotations.keys()).issubset(x.keys()):
262
+ return False
263
+
264
+ annotations_set = set(required_annotations.keys()) | set(
265
+ optional_annotations.keys()
266
+ )
267
+ if not annotations_set.issuperset(x.keys()):
268
+ return False
269
+
270
+ for k, v in required_annotations.items():
271
+ origin = get_origin(v)
272
+ if origin is Required:
273
+ v = get_args(v)[0]
274
+
275
+ if not isinstance_generic(x[k], v):
276
+ return False
277
+
278
+ for k, v in optional_annotations.items():
279
+ if k not in x:
280
+ continue
281
+ origin = get_origin(v)
282
+ if origin is NotRequired:
283
+ v = get_args(v)[0]
284
+ if not isinstance_generic(x[k], v):
285
+ return False
286
+
287
+ return True
288
+
289
+
290
+ def is_builtin_collection(x: Any, *, strict: bool = False) -> TypeIs[BuiltinCollection]:
291
+ """Returns True if x is an instance of a builtin collection type (list, tuple, dict, set, frozenset).
292
+
293
+ Args:
294
+ x: Object to check.
295
+ strict: If True, it will not consider custom subtypes of builtins as builtin collections. defaults to False.
296
+ """
297
+ if strict and not is_builtin_obj(x):
298
+ return False
299
+ return isinstance(x, (list, tuple, dict, set, frozenset))
300
+
301
+
302
+ def is_builtin_number(x: Any, *, strict: bool = False) -> TypeIs[BuiltinNumber]:
303
+ """Returns True if x is an instance of a builtin number type (int, float, bool, complex).
304
+
305
+ Args:
306
+ x: Object to check.
307
+ strict: If True, it will not consider custom subtypes of builtins as builtin numbers. defaults to False.
308
+ """
309
+ if strict and not is_builtin_obj(x):
310
+ return False
311
+ return isinstance(x, (int, float, bool, complex))
312
+
313
+
314
+ def is_builtin_obj(x: Any) -> bool:
315
+ """Returns True if object is an instance of a builtin object.
316
+
317
+ Note: If the object is an instance of a custom subtype of a builtin object, this function returns False.
318
+ """
319
+ return x.__class__.__module__ == "builtins" and not isinstance(x, type)
320
+
321
+
322
+ def is_builtin_scalar(x: Any, *, strict: bool = False) -> TypeIs[BuiltinScalar]:
323
+ """Returns True if x is an instance of a builtin scalar type (int, float, bool, complex, NoneType, str, bytes).
324
+
325
+ Args:
326
+ x: Object to check.
327
+ strict: If True, it will not consider subtypes of builtins as builtin scalars. defaults to False.
328
+ """
329
+ if strict and not is_builtin_obj(x):
330
+ return False
331
+ return isinstance(x, (int, float, bool, complex, NoneType, str, bytes))
332
+
333
+
334
+ def is_dataclass_instance(x: Any) -> TypeIs[DataclassInstance]:
335
+ """Returns True if argument is a dataclass.
336
+
337
+ Unlike function `dataclasses.is_dataclass`, this function returns False for a dataclass type.
338
+ """
339
+ return not isinstance(x, type) and isinstance_generic(x, DataclassInstance)
340
+
341
+
342
+ def is_dataclass_type(x: Any) -> TypeIs[Type[DataclassInstance]]:
343
+ """Return whether dataclass type."""
344
+ return isinstance(x, type) and is_dataclass(x)
345
+
346
+
347
+ def is_iterable_bool(
348
+ x: Any,
349
+ *,
350
+ accept_generator: bool = True,
351
+ ) -> TypeIs[Iterable[bool]]:
352
+ """Return whether iterable bool."""
353
+ if not accept_generator and isinstance(x, Generator):
354
+ return False
355
+ return isinstance_generic(x, Iterable[bool])
356
+
357
+
358
+ def is_iterable_bytes_or_list(
359
+ x: Any,
360
+ *,
361
+ accept_generator: bool = True,
362
+ ) -> TypeIs[Iterable[Union[bytes, list]]]:
363
+ """Return whether iterable bytes or list."""
364
+ if not accept_generator and isinstance(x, Generator):
365
+ return False
366
+ return isinstance_generic(x, Iterable[Union[bytes, list]])
367
+
368
+
369
+ def is_iterable_float(
370
+ x: Any,
371
+ *,
372
+ accept_generator: bool = True,
373
+ ) -> TypeIs[Iterable[float]]:
374
+ """Return whether iterable float."""
375
+ if not accept_generator and isinstance(x, Generator):
376
+ return False
377
+ return isinstance_generic(x, Iterable[float])
378
+
379
+
380
+ def is_iterable_int(
381
+ x: Any,
382
+ *,
383
+ accept_bool: bool = True,
384
+ accept_generator: bool = True,
385
+ ) -> TypeIs[Iterable[int]]:
386
+ """Return whether iterable int."""
387
+ if not accept_generator and isinstance(x, Generator):
388
+ return False
389
+ return isinstance_generic(x, Iterable[int]) and (
390
+ accept_bool or not isinstance_generic(x, Iterable[bool])
391
+ )
392
+
393
+
394
+ def is_iterable_integral(
395
+ x: Any,
396
+ *,
397
+ accept_generator: bool = True,
398
+ ) -> TypeIs[Iterable[Integral]]:
399
+ """Return whether iterable integral."""
400
+ if not accept_generator and isinstance(x, Generator):
401
+ return False
402
+ return isinstance_generic(x, Iterable[Integral])
403
+
404
+
405
+ def is_iterable_str(
406
+ x: Any,
407
+ *,
408
+ accept_str: bool = True,
409
+ accept_generator: bool = True,
410
+ ) -> TypeGuard[Iterable[str]]:
411
+ """Return whether iterable str."""
412
+ if isinstance(x, str):
413
+ return accept_str
414
+ if isinstance(x, Generator):
415
+ return accept_generator and all(isinstance(xi, str) for xi in x)
416
+ return isinstance_generic(x, Iterable[str])
417
+
418
+
419
+ def is_namedtuple_instance(x: Any) -> TypeIs[NamedTupleInstance]:
420
+ """Returns True if argument is a NamedTuple."""
421
+ return not isinstance(x, type) and isinstance_generic(x, NamedTupleInstance)
422
+
423
+
424
+ def is_sequence_str(
425
+ x: Any,
426
+ *,
427
+ accept_str: bool = True,
428
+ ) -> TypeGuard[Sequence[str]]:
429
+ """Return whether sequence str."""
430
+ return (accept_str and isinstance(x, str)) or (
431
+ not isinstance(x, str)
432
+ and isinstance(x, Sequence)
433
+ and all(isinstance(xi, str) for xi in x)
434
+ )
435
+
436
+
437
+ def is_typed_dict(x: Any) -> TypeGuard[type]:
438
+ """Return whether typed dict."""
439
+ if sys.version_info.major == 3 and sys.version_info.minor < 9:
440
+ return x.__class__.__name__ == "_TypedDictMeta"
441
+ else:
442
+ return hasattr(x, "__orig_bases__") and TypedDict in x.__orig_bases__
443
+
444
+
445
+ def is_parameterized(x: Any) -> bool:
446
+ """Returns True if object is a parametrized object like `Iterable[int]`, `Mapping[str, float]`, etc."""
447
+ return get_origin(x) is not None and len(get_args(x)) > 0
448
+
449
+
450
+ _COLLECTION_ALIASES = [
451
+ typing.Iterable,
452
+ typing.Iterator,
453
+ typing.Reversible,
454
+ typing.Generator,
455
+ typing.AsyncIterable,
456
+ typing.AsyncIterator,
457
+ typing.AsyncGenerator,
458
+ typing.Collection,
459
+ typing.Container,
460
+ typing.Sized,
461
+ typing.Sequence,
462
+ typing.MutableSequence,
463
+ typing.Set,
464
+ typing.MutableSet,
465
+ typing.Mapping,
466
+ typing.MutableMapping,
467
+ typing.MappingView,
468
+ typing.KeysView,
469
+ typing.ItemsView,
470
+ typing.ValuesView,
471
+ typing.Awaitable,
472
+ typing.Coroutine,
473
+ typing.Callable,
474
+ typing.Hashable,
475
+ typing_extensions.Iterable,
476
+ typing_extensions.Iterator,
477
+ typing_extensions.Reversible,
478
+ typing_extensions.Generator,
479
+ typing_extensions.AsyncIterable,
480
+ typing_extensions.AsyncIterator,
481
+ typing_extensions.AsyncGenerator,
482
+ typing_extensions.Collection,
483
+ typing_extensions.Container,
484
+ typing_extensions.Sized,
485
+ typing_extensions.Sequence,
486
+ typing_extensions.MutableSequence,
487
+ typing_extensions.Set,
488
+ typing_extensions.MutableSet,
489
+ typing_extensions.Mapping,
490
+ typing_extensions.MutableMapping,
491
+ typing_extensions.MappingView,
492
+ typing_extensions.KeysView,
493
+ typing_extensions.ItemsView,
494
+ typing_extensions.ValuesView,
495
+ typing_extensions.Awaitable,
496
+ typing_extensions.Coroutine,
497
+ typing_extensions.Callable,
498
+ typing_extensions.Hashable,
499
+ ]
500
+
501
+ _SPECIAL_FORMS = [] + [
502
+ getattr(module, candidate_name, None)
503
+ for module in (typing, typing_extensions)
504
+ for candidate_name in [
505
+ "Any",
506
+ "NoReturn",
507
+ "Union",
508
+ "Optional",
509
+ "Literal",
510
+ "Final",
511
+ "ClassVar",
512
+ "TypeVar",
513
+ "Annotated",
514
+ "Never",
515
+ "Self",
516
+ "Required",
517
+ "NotRequired",
518
+ "TypeGuard",
519
+ "TypeIs",
520
+ "Concatenate",
521
+ "ParamSpec",
522
+ "ParamSpecArgs",
523
+ "ParamSpecKwargs",
524
+ "TypeVarTuple",
525
+ "Unpack",
526
+ ]
527
+ if getattr(module, candidate_name, None) is not None
528
+ ]
529
+
530
+
531
+ def is_collection_alias(x: Any) -> bool:
532
+ """Returns True if object is a non-parameterized collection alias type."""
533
+ return _safe_isin(x, _COLLECTION_ALIASES)
534
+
535
+
536
+ def is_special_form(x: Any) -> bool:
537
+ """Returns True if object is a typing special form like `Any`."""
538
+ return _safe_isin(x, _SPECIAL_FORMS)
539
+
540
+
541
+ def _safe_isin(x: Any, targets: Iterable) -> bool:
542
+ """Perform the safe isin operation."""
543
+ for alias in targets:
544
+ if (x == alias) is True:
545
+ return True
546
+ return False
547
+
548
+
549
+ def _is_callable_type(x: Any) -> bool:
550
+ """Perform the is callable type operation."""
551
+ return x in (Callable, _RuntimeCallable)