pythonwrench 0.5.0__tar.gz → 0.5.1__tar.gz

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 (74) hide show
  1. {pythonwrench-0.5.0/src/pythonwrench.egg-info → pythonwrench-0.5.1}/PKG-INFO +4 -3
  2. pythonwrench-0.5.1/docs/requirements.txt +4 -0
  3. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/pyproject.toml +3 -0
  4. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/__init__.py +9 -1
  5. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/_core.py +1 -1
  6. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/checksum.py +98 -35
  7. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/collections/collections.py +57 -1
  8. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/disk_cache.py +5 -9
  9. pythonwrench-0.5.1/src/pythonwrench/time.py +31 -0
  10. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/typing/__init__.py +8 -0
  11. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/typing/checks.py +97 -0
  12. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/typing/classes.py +2 -0
  13. {pythonwrench-0.5.0 → pythonwrench-0.5.1/src/pythonwrench.egg-info}/PKG-INFO +4 -3
  14. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench.egg-info/SOURCES.txt +2 -0
  15. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench.egg-info/requires.txt +3 -0
  16. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_checksum.py +20 -0
  17. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_collections.py +14 -0
  18. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_typing.py +23 -0
  19. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/LICENSE +0 -0
  20. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/README.md +0 -0
  21. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/setup.cfg +0 -0
  22. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/setup.py +0 -0
  23. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/__main__.py +0 -0
  24. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/abc.py +0 -0
  25. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/argparse.py +0 -0
  26. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/cast.py +0 -0
  27. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/collections/__init__.py +0 -0
  28. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/collections/prop.py +0 -0
  29. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/collections/reducers.py +0 -0
  30. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/concurrent.py +0 -0
  31. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/csv.py +0 -0
  32. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/dataclasses.py +0 -0
  33. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/datetime.py +0 -0
  34. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/difflib.py +0 -0
  35. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/entries.py +0 -0
  36. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/enum.py +0 -0
  37. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/functools.py +0 -0
  38. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/hashlib.py +0 -0
  39. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/importlib.py +0 -0
  40. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/inspect.py +0 -0
  41. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/json.py +0 -0
  42. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/jsonl.py +0 -0
  43. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/logging.py +0 -0
  44. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/math.py +0 -0
  45. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/os.py +0 -0
  46. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/pickle.py +0 -0
  47. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/random.py +0 -0
  48. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/re.py +0 -0
  49. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/semver.py +0 -0
  50. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench/warnings.py +0 -0
  51. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  52. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench.egg-info/entry_points.txt +0 -0
  53. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/src/pythonwrench.egg-info/top_level.txt +0 -0
  54. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_abc.py +0 -0
  55. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_argparse.py +0 -0
  56. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_cast.py +0 -0
  57. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_csv.py +0 -0
  58. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_dataclasses.py +0 -0
  59. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_difflib.py +0 -0
  60. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_disk_cache.py +0 -0
  61. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_entries.py +0 -0
  62. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_enum.py +0 -0
  63. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_functools.py +0 -0
  64. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_hashlib.py +0 -0
  65. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_importlib.py +0 -0
  66. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_inspect.py +0 -0
  67. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_json.py +0 -0
  68. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_jsonl.py +0 -0
  69. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_logging.py +0 -0
  70. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_math.py +0 -0
  71. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_os.py +0 -0
  72. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_random.py +0 -0
  73. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_readme.py +0 -0
  74. {pythonwrench-0.5.0 → pythonwrench-0.5.1}/tests/test_semver.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pythonwrench
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Python library with tools for typing, manipulating collections, and more!
5
5
  Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
6
6
  Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
@@ -53,7 +53,8 @@ License-File: LICENSE
53
53
  Requires-Dist: lazy-loader>=0.4
54
54
  Requires-Dist: typing-extensions>=4.10.0
55
55
  Provides-Extra: dev
56
- Dynamic: license-file
56
+ Provides-Extra: lazy
57
+ Requires-Dist: lazy-loader>=0.4; extra == "lazy"
57
58
 
58
59
  # pythonwrench
59
60
 
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ sphinx-immaterial>=0.11.14
4
+ sphinx<9.0.0
@@ -52,6 +52,9 @@ pyw-safe-rmdir = "pythonwrench.entries:main_safe_rmdir"
52
52
 
53
53
  [project.optional-dependencies]
54
54
  dev = []
55
+ lazy = [
56
+ "lazy-loader>=0.4",
57
+ ]
55
58
 
56
59
  [build-system]
57
60
  requires = ["setuptools >= 61.0"]
@@ -9,7 +9,7 @@ __author_email__ = "labbeti.pub@gmail.com"
9
9
  __license__ = "MIT"
10
10
  __maintainer__ = "Étienne Labbé (Labbeti)"
11
11
  __status__ = "Development"
12
- __version__ = "0.5.0"
12
+ __version__ = "0.5.1"
13
13
 
14
14
 
15
15
  from typing import TYPE_CHECKING
@@ -187,6 +187,7 @@ if TYPE_CHECKING or lazy is None:
187
187
  SupportsGetitemIterLen2,
188
188
  SupportsGetitemLen,
189
189
  SupportsGetitemLen2,
190
+ SupportsIter,
190
191
  SupportsIterLen,
191
192
  SupportsLen,
192
193
  SupportsMatmul,
@@ -199,6 +200,7 @@ if TYPE_CHECKING or lazy is None:
199
200
  is_builtin_number,
200
201
  is_builtin_obj,
201
202
  is_builtin_scalar,
203
+ is_collection_alias,
202
204
  is_dataclass_instance,
203
205
  is_iterable_bool,
204
206
  is_iterable_bytes_or_list,
@@ -207,7 +209,9 @@ if TYPE_CHECKING or lazy is None:
207
209
  is_iterable_integral,
208
210
  is_iterable_str,
209
211
  is_namedtuple_instance,
212
+ is_parameterized,
210
213
  is_sequence_str,
214
+ is_special_form,
211
215
  is_typed_dict,
212
216
  isinstance_generic,
213
217
  )
@@ -397,6 +401,7 @@ else:
397
401
  "SupportsGetitemIterLen2",
398
402
  "SupportsGetitemLen",
399
403
  "SupportsGetitemLen2",
404
+ "SupportsIter",
400
405
  "SupportsIterLen",
401
406
  "SupportsLen",
402
407
  "SupportsMatmul",
@@ -409,6 +414,7 @@ else:
409
414
  "is_builtin_number",
410
415
  "is_builtin_obj",
411
416
  "is_builtin_scalar",
417
+ "is_collection_alias",
412
418
  "is_dataclass_instance",
413
419
  "is_iterable_bool",
414
420
  "is_iterable_bytes_or_list",
@@ -417,7 +423,9 @@ else:
417
423
  "is_iterable_integral",
418
424
  "is_iterable_str",
419
425
  "is_namedtuple_instance",
426
+ "is_parameterized",
420
427
  "is_sequence_str",
428
+ "is_special_form",
421
429
  "is_typed_dict",
422
430
  "isinstance_generic",
423
431
  ],
@@ -32,7 +32,7 @@ ClassOrTuple = Union[type, Tuple[type, ...]]
32
32
 
33
33
 
34
34
  class Predicate(Protocol[T_Any]):
35
- def __call__(self, x: T_Any) -> bool: ...
35
+ def __call__(self, /, x: T_Any) -> bool: ...
36
36
 
37
37
 
38
38
  def return_none(*args, **kwargs) -> None:
@@ -6,6 +6,7 @@ import re
6
6
  import struct
7
7
  import zlib
8
8
  from dataclasses import asdict
9
+ from datetime import date, datetime
9
10
  from enum import Enum
10
11
  from functools import lru_cache
11
12
  from pathlib import Path
@@ -13,12 +14,15 @@ from types import FunctionType, MethodType
13
14
  from typing import (
14
15
  Any,
15
16
  Callable,
17
+ Dict,
16
18
  Generator,
17
19
  Iterable,
18
20
  Mapping,
19
21
  Optional,
20
22
  TypeVar,
21
23
  Union,
24
+ get_args,
25
+ get_origin,
22
26
  overload,
23
27
  )
24
28
 
@@ -27,14 +31,19 @@ from pythonwrench.functools import function_alias
27
31
  from pythonwrench.inspect import get_fullname
28
32
  from pythonwrench.typing import (
29
33
  DataclassInstance,
34
+ EllipsisType,
30
35
  NamedTupleInstance,
31
36
  NoneType,
37
+ is_collection_alias,
38
+ is_parameterized,
39
+ is_special_form,
32
40
  )
33
41
 
34
42
  T = TypeVar("T")
35
43
 
36
44
 
37
45
  _CHECKSUM_REGISTRY = _FunctionRegistry[int]()
46
+ _CHECKSUM_PROTOCOLS = False
38
47
 
39
48
 
40
49
  @overload
@@ -130,9 +139,7 @@ def checksum_int(x: int, **kwargs) -> int:
130
139
  # Intermediate functions
131
140
  @register_checksum_fn(bytearray)
132
141
  def checksum_bytearray(x: bytearray, **kwargs) -> int:
133
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
134
- get_fullname(x)
135
- )
142
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
136
143
  return _checksum_bytes_bytearray(x, **kwargs)
137
144
 
138
145
 
@@ -151,25 +158,25 @@ def checksum_complex(x: complex, **kwargs) -> int:
151
158
 
152
159
  @register_checksum_fn(FunctionType)
153
160
  def checksum_function(x: FunctionType, **kwargs) -> int:
154
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
155
- get_fullname(x)
156
- )
161
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
157
162
  return checksum_str(x.__qualname__, **kwargs)
158
163
 
159
164
 
160
165
  @register_checksum_fn(NoneType)
161
166
  def checksum_none(x: None, **kwargs) -> int:
162
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
163
- get_fullname(x)
164
- )
165
- return checksum_type(x.__class__, **kwargs) + kwargs.get("accumulator", 0)
167
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
168
+ return checksum_type(x.__class__, **kwargs)
169
+
170
+
171
+ @register_checksum_fn(EllipsisType)
172
+ def checksum_ellipsis(x: None, **kwargs) -> int:
173
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
174
+ return checksum_type(x.__class__, **kwargs)
166
175
 
167
176
 
168
177
  @register_checksum_fn(str)
169
178
  def checksum_str(x: str, **kwargs) -> int:
170
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
171
- get_fullname(x)
172
- )
179
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
173
180
  return checksum_bytes(x.encode(), **kwargs)
174
181
 
175
182
 
@@ -181,12 +188,34 @@ def checksum_type(x: type, **kwargs) -> int:
181
188
  # Recursive functions
182
189
  @register_checksum_fn(DataclassInstance)
183
190
  def checksum_dataclass(x: DataclassInstance, **kwargs) -> int:
184
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
185
- get_fullname(x)
186
- )
191
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
187
192
  return checksum_dict(asdict(x), **kwargs)
188
193
 
189
194
 
195
+ @register_checksum_fn(datetime)
196
+ def checksum_datetime(x: datetime, **kwargs) -> int:
197
+ return _checksum_iterable(
198
+ [
199
+ x.__class__,
200
+ x.year,
201
+ x.month,
202
+ x.day,
203
+ x.hour,
204
+ x.minute,
205
+ x.second,
206
+ x.microsecond,
207
+ x.tzinfo,
208
+ x.fold,
209
+ ],
210
+ **kwargs,
211
+ )
212
+
213
+
214
+ @register_checksum_fn(date)
215
+ def checksum_date(x: date, **kwargs) -> int:
216
+ return _checksum_iterable([x.__class__, x.year, x.month, x.day], **kwargs)
217
+
218
+
190
219
  @register_checksum_fn(dict)
191
220
  def checksum_dict(x: dict, **kwargs) -> int:
192
221
  return _checksum_mapping(x, **kwargs)
@@ -214,9 +243,7 @@ def checksum_set(x: Union[set, frozenset], **kwargs) -> int:
214
243
 
215
244
  @register_checksum_fn(range)
216
245
  def checksum_range(x: range, **kwargs) -> int:
217
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
218
- get_fullname(x)
219
- )
246
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
220
247
  return _checksum_iterable([x.start, x.stop, x.step], **kwargs)
221
248
 
222
249
 
@@ -238,48 +265,76 @@ def checksum_method(x: MethodType, **kwargs) -> int:
238
265
 
239
266
  @register_checksum_fn(NamedTupleInstance)
240
267
  def checksum_namedtuple(x: NamedTupleInstance, **kwargs) -> int:
241
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
242
- get_fullname(x)
243
- )
268
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
244
269
  return checksum_dict(x._asdict(), **kwargs)
245
270
 
246
271
 
247
272
  @register_checksum_fn(functools.partial)
248
273
  def checksum_partial(x: functools.partial, **kwargs) -> int:
249
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
250
- get_fullname(x)
251
- )
274
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
252
275
  return checksum_list_tuple((x.func, x.args, x.keywords), **kwargs)
253
276
 
254
277
 
255
278
  @register_checksum_fn(re.Pattern)
256
279
  def checksum_pattern(x: re.Pattern, **kwargs) -> int:
257
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
258
- get_fullname(x)
259
- )
280
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
260
281
  return checksum_str(str(x), **kwargs)
261
282
 
262
283
 
263
284
  @register_checksum_fn(Path)
264
285
  def checksum_path(x: Path, **kwargs) -> int:
265
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
266
- get_fullname(x)
267
- )
286
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
287
+
268
288
  resolve_path = kwargs.get("resolve_path", False)
269
289
  if isinstance(resolve_path, bool) and resolve_path:
270
290
  x = x.expanduser().resolve()
271
-
272
291
  return checksum_str(str(x), **kwargs)
273
292
 
274
293
 
275
294
  @register_checksum_fn(slice)
276
295
  def checksum_slice(x: slice, **kwargs) -> int:
277
- kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
278
- get_fullname(x)
279
- )
296
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
280
297
  return checksum_list_tuple((x.start, x.stop, x.step), **kwargs)
281
298
 
282
299
 
300
+ @register_checksum_fn(custom_predicate=is_parameterized)
301
+ def checksum_parametrized(x: Any, **kwargs) -> int:
302
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
303
+ return checksum_list_tuple((get_origin(x),) + get_args(x), **kwargs)
304
+
305
+
306
+ @register_checksum_fn(custom_predicate=is_collection_alias)
307
+ def checksum_collection_alias(x: Any, **kwargs) -> int:
308
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
309
+ return checksum_str(x._name, **kwargs)
310
+
311
+
312
+ @register_checksum_fn(custom_predicate=is_special_form)
313
+ def checksum_special_form(x: Any, **kwargs) -> int:
314
+ kwargs = _add_type_checksum_to_accumulator(x, kwargs)
315
+
316
+ if hasattr(x, "_name"):
317
+ name = x._name
318
+ elif hasattr(x, "__name__"):
319
+ name = x.__name__
320
+ else:
321
+ msg = f"Unsupported argument {x=} in checksum_special_form."
322
+ raise ValueError(msg)
323
+
324
+ return checksum_str(name, **kwargs)
325
+
326
+
327
+ if _CHECKSUM_PROTOCOLS:
328
+
329
+ @register_checksum_fn(Mapping, priority=-100)
330
+ def checksum_mapping(x: Mapping, **kwargs) -> int:
331
+ return _checksum_mapping(x, **kwargs)
332
+
333
+ @register_checksum_fn(Iterable, priority=-200)
334
+ def checksum_iterable(x: Iterable, **kwargs) -> int:
335
+ return _checksum_iterable(x, **kwargs)
336
+
337
+
283
338
  # Private functions
284
339
  def _checksum_bytes_bytearray(x: Union[bytes, bytearray], **kwargs) -> int:
285
340
  xint = zlib.crc32(x) % (1 << 32)
@@ -307,9 +362,17 @@ def _checksum_mapping(x: Mapping, **kwargs) -> int:
307
362
 
308
363
 
309
364
  def _terminate_checksum(x: int, fullname: str, **kwargs) -> int:
365
+ """Returns checksum for final value + name + accumulator."""
310
366
  return x + _cached_checksum_str(fullname) + kwargs.get("accumulator", 0)
311
367
 
312
368
 
369
+ def _add_type_checksum_to_accumulator(x: Any, kwargs: Dict[str, Any]) -> Dict[str, Any]:
370
+ kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
371
+ get_fullname(x)
372
+ )
373
+ return kwargs
374
+
375
+
313
376
  @lru_cache(maxsize=None)
314
377
  def _cached_checksum_str(x: str) -> int:
315
378
  return zlib.crc32(x.encode()) % (1 << 32)
@@ -597,14 +597,70 @@ def recursive_generator(x: Any) -> Generator[Tuple[Any, int, int], None, None]:
597
597
  return recursive_generator_impl(x, 0, 0)
598
598
 
599
599
 
600
+ @overload
600
601
  def sorted_dict(
601
602
  x: Mapping[K, V],
602
603
  /,
603
604
  *,
605
+ by: Literal["key"] = "key",
604
606
  key: Optional[Callable[[K], Any]] = None,
605
607
  reverse: bool = False,
608
+ ) -> Dict[K, V]: ...
609
+
610
+
611
+ @overload
612
+ def sorted_dict(
613
+ x: Mapping[K, V],
614
+ /,
615
+ *,
616
+ by: Literal["value"],
617
+ key: Optional[Callable[[V], Any]] = None,
618
+ reverse: bool = False,
619
+ ) -> Dict[K, V]: ...
620
+
621
+
622
+ @overload
623
+ def sorted_dict(
624
+ x: Mapping[K, V],
625
+ /,
626
+ *,
627
+ by: Literal["item"],
628
+ key: Optional[Callable[[Tuple[K, V]], Any]] = None,
629
+ reverse: bool = False,
630
+ ) -> Dict[K, V]: ...
631
+
632
+
633
+ def sorted_dict(
634
+ x: Mapping[K, V],
635
+ /,
636
+ *,
637
+ by: Literal["key", "value", "item"] = "key",
638
+ key: Optional[Callable[[Any], Any]] = None,
639
+ reverse: bool = False,
606
640
  ) -> Dict[K, V]:
607
- return {k: x[k] for k in sorted(x.keys(), key=key, reverse=reverse)} # type: ignore
641
+ """Sort a dictionnary by key, value or item."""
642
+ if key is None or by == "item":
643
+ impl_key = key
644
+
645
+ elif by == "key":
646
+
647
+ def by_key_fn(x: Tuple[K, V]) -> Any:
648
+ return key(x[0])
649
+
650
+ impl_key = by_key_fn
651
+
652
+ elif by == "value":
653
+
654
+ def by_value_fn(x: Tuple[K, V]) -> Any:
655
+ return key(x[1])
656
+
657
+ impl_key = by_value_fn
658
+
659
+ else:
660
+ msg = f"Invalid argument {by=}. (expected one of {('key', 'value', 'item')})"
661
+ raise ValueError(msg)
662
+
663
+ return {k: v for k, v in sorted(x.items(), key=impl_key, reverse=reverse)} # type: ignore
608
664
 
609
665
 
610
666
  def shuffled(
@@ -362,19 +362,15 @@ def _disk_cache_impl(
362
362
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
363
363
  # for backward compatibility
364
364
  if cache_fname_fmt is None:
365
- expected = "{fn_name}_{checksum_hex}{suffix}"
366
- warnings.warn(
367
- f"Deprecated argument value {cache_fname_fmt=}. (use {expected} instead)",
368
- DeprecationWarning,
369
- )
365
+ expected = "{fn_name}_{csum}{suffix}"
366
+ msg = f"Deprecated argument value {cache_fname_fmt=}. (use {expected} instead)"
367
+ warnings.warn(msg, DeprecationWarning)
370
368
  cache_fname_fmt = expected
371
369
 
372
370
  if cache_saving_backend is None:
373
371
  expected = "auto"
374
- warnings.warn(
375
- f"Deprecated argument value {cache_saving_backend=}. (use {expected} instead)",
376
- DeprecationWarning,
377
- )
372
+ msg = f"Deprecated argument value {cache_saving_backend=}. (use {expected} instead)"
373
+ warnings.warn(msg, DeprecationWarning)
378
374
  cache_saving_backend = expected
379
375
 
380
376
  if cache_saving_backend == "auto":
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import time
5
+ from typing import Callable, Optional
6
+
7
+
8
+ class Ticker:
9
+ def __init__(
10
+ self,
11
+ *,
12
+ get_time_fn: Callable[[], float] = time.perf_counter,
13
+ prev_tick: Optional[float] = None,
14
+ ) -> None:
15
+ if prev_tick is None:
16
+ prev_tick = get_time_fn()
17
+
18
+ super().__init__()
19
+ self._get_time_fn = get_time_fn
20
+ self._prev_tick = prev_tick
21
+
22
+ def tick(self) -> float:
23
+ now = self._get_time_fn()
24
+ duration = now - self._prev_tick
25
+ self._prev_tick = now
26
+ return duration
27
+
28
+ def set_prev_tick(self, prev_tick: Optional[float] = None) -> None:
29
+ if prev_tick is None:
30
+ prev_tick = self._get_time_fn()
31
+ self._prev_tick = prev_tick
@@ -16,6 +16,7 @@ if TYPE_CHECKING or lazy is None:
16
16
  is_builtin_number,
17
17
  is_builtin_obj,
18
18
  is_builtin_scalar,
19
+ is_collection_alias,
19
20
  is_dataclass_instance,
20
21
  is_iterable_bool,
21
22
  is_iterable_bytes_or_list,
@@ -24,7 +25,9 @@ if TYPE_CHECKING or lazy is None:
24
25
  is_iterable_integral,
25
26
  is_iterable_str,
26
27
  is_namedtuple_instance,
28
+ is_parameterized,
27
29
  is_sequence_str,
30
+ is_special_form,
28
31
  is_typed_dict,
29
32
  isinstance_generic,
30
33
  )
@@ -47,6 +50,7 @@ if TYPE_CHECKING or lazy is None:
47
50
  SupportsGetitemIterLen2,
48
51
  SupportsGetitemLen,
49
52
  SupportsGetitemLen2,
53
+ SupportsIter,
50
54
  SupportsIterLen,
51
55
  SupportsLen,
52
56
  SupportsMatmul,
@@ -70,6 +74,7 @@ else:
70
74
  "is_builtin_number",
71
75
  "is_builtin_obj",
72
76
  "is_builtin_scalar",
77
+ "is_collection_alias",
73
78
  "is_dataclass_instance",
74
79
  "is_iterable_bool",
75
80
  "is_iterable_bytes_or_list",
@@ -78,7 +83,9 @@ else:
78
83
  "is_iterable_integral",
79
84
  "is_iterable_str",
80
85
  "is_namedtuple_instance",
86
+ "is_parameterized",
81
87
  "is_sequence_str",
88
+ "is_special_form",
82
89
  "is_typed_dict",
83
90
  "isinstance_generic",
84
91
  ],
@@ -101,6 +108,7 @@ else:
101
108
  "SupportsGetitemIterLen2",
102
109
  "SupportsGetitemLen",
103
110
  "SupportsGetitemLen2",
111
+ "SupportsIter",
104
112
  "SupportsIterLen",
105
113
  "SupportsLen",
106
114
  "SupportsMatmul",
@@ -4,6 +4,7 @@
4
4
  import inspect
5
5
  import logging
6
6
  import sys
7
+ import typing
7
8
  from numbers import Integral
8
9
  from types import FunctionType, MethodType
9
10
  from typing import (
@@ -395,3 +396,99 @@ def is_typed_dict(x: Any) -> TypeGuard[type]:
395
396
  return x.__class__.__name__ == "_TypedDictMeta"
396
397
  else:
397
398
  return hasattr(x, "__orig_bases__") and TypedDict in x.__orig_bases__
399
+
400
+
401
+ def is_parameterized(x: Any) -> bool:
402
+ """Returns True if object is a parametrized object like `Iterable[int]`, `Mapping[str, float]`, etc."""
403
+ return get_origin(x) is not None and len(get_args(x)) > 0
404
+
405
+
406
+ _COLLECTION_ALIASES = [
407
+ typing.Iterable,
408
+ typing.Iterator,
409
+ typing.Reversible,
410
+ typing.Generator,
411
+ typing.AsyncIterable,
412
+ typing.AsyncIterator,
413
+ typing.AsyncGenerator,
414
+ typing.Collection,
415
+ typing.Container,
416
+ typing.Sized,
417
+ typing.Sequence,
418
+ typing.MutableSequence,
419
+ typing.Set,
420
+ typing.MutableSet,
421
+ typing.Mapping,
422
+ typing.MutableMapping,
423
+ typing.MappingView,
424
+ typing.KeysView,
425
+ typing.ItemsView,
426
+ typing.ValuesView,
427
+ typing.Awaitable,
428
+ typing.Coroutine,
429
+ typing.Callable,
430
+ typing.Hashable,
431
+ typing_extensions.Iterable,
432
+ typing_extensions.Iterator,
433
+ typing_extensions.Reversible,
434
+ typing_extensions.Generator,
435
+ typing_extensions.AsyncIterable,
436
+ typing_extensions.AsyncIterator,
437
+ typing_extensions.AsyncGenerator,
438
+ typing_extensions.Collection,
439
+ typing_extensions.Container,
440
+ typing_extensions.Sized,
441
+ typing_extensions.Sequence,
442
+ typing_extensions.MutableSequence,
443
+ typing_extensions.Set,
444
+ typing_extensions.MutableSet,
445
+ typing_extensions.Mapping,
446
+ typing_extensions.MutableMapping,
447
+ typing_extensions.MappingView,
448
+ typing_extensions.KeysView,
449
+ typing_extensions.ItemsView,
450
+ typing_extensions.ValuesView,
451
+ typing_extensions.Awaitable,
452
+ typing_extensions.Coroutine,
453
+ typing_extensions.Callable,
454
+ typing_extensions.Hashable,
455
+ ]
456
+
457
+ _SPECIAL_FORMS = [] + [
458
+ getattr(module, candidate_name, None)
459
+ for module in (typing, typing_extensions)
460
+ for candidate_name in [
461
+ "Any",
462
+ "NoReturn",
463
+ "Union",
464
+ "Optional",
465
+ "Literal",
466
+ "Final",
467
+ "ClassVar",
468
+ "TypeVar",
469
+ "Annotated",
470
+ "Never",
471
+ "Self",
472
+ "Required",
473
+ "NotRequired",
474
+ "TypeGuard",
475
+ "TypeIs",
476
+ "Concatenate",
477
+ "ParamSpec",
478
+ "ParamSpecArgs",
479
+ "ParamSpecKwargs",
480
+ "TypeVarTuple",
481
+ "Unpack",
482
+ ]
483
+ if getattr(module, candidate_name, None) is not None
484
+ ]
485
+
486
+
487
+ def is_collection_alias(x: Any) -> bool:
488
+ """Returns True if object is a non-parameterized collection alias type."""
489
+ return x in _COLLECTION_ALIASES
490
+
491
+
492
+ def is_special_form(x: Any) -> bool:
493
+ """Returns True if object is a typing special form like `Any`."""
494
+ return x in _SPECIAL_FORMS
@@ -5,6 +5,7 @@ from typing import (
5
5
  Any,
6
6
  ClassVar,
7
7
  Dict,
8
+ Iterable,
8
9
  Iterator,
9
10
  List,
10
11
  Protocol,
@@ -41,6 +42,7 @@ T_BuiltinScalar = TypeVar(
41
42
  )
42
43
 
43
44
  ListOrTuple = Union[List[_T_Item], Tuple[_T_Item, ...]]
45
+ SupportsIter = Iterable
44
46
 
45
47
 
46
48
  @runtime_checkable
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pythonwrench
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Python library with tools for typing, manipulating collections, and more!
5
5
  Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
6
6
  Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
@@ -53,7 +53,8 @@ License-File: LICENSE
53
53
  Requires-Dist: lazy-loader>=0.4
54
54
  Requires-Dist: typing-extensions>=4.10.0
55
55
  Provides-Extra: dev
56
- Dynamic: license-file
56
+ Provides-Extra: lazy
57
+ Requires-Dist: lazy-loader>=0.4; extra == "lazy"
57
58
 
58
59
  # pythonwrench
59
60
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  setup.py
5
+ docs/requirements.txt
5
6
  src/pythonwrench/__init__.py
6
7
  src/pythonwrench/__main__.py
7
8
  src/pythonwrench/_core.py
@@ -30,6 +31,7 @@ src/pythonwrench/pickle.py
30
31
  src/pythonwrench/random.py
31
32
  src/pythonwrench/re.py
32
33
  src/pythonwrench/semver.py
34
+ src/pythonwrench/time.py
33
35
  src/pythonwrench/warnings.py
34
36
  src/pythonwrench.egg-info/PKG-INFO
35
37
  src/pythonwrench.egg-info/SOURCES.txt
@@ -2,3 +2,6 @@ lazy-loader>=0.4
2
2
  typing-extensions>=4.10.0
3
3
 
4
4
  [dev]
5
+
6
+ [lazy]
7
+ lazy-loader>=0.4
@@ -4,8 +4,11 @@
4
4
  import math
5
5
  import random
6
6
  import unittest
7
+ from typing import Any, Iterable, Mapping, Tuple
7
8
  from unittest import TestCase
8
9
 
10
+ from typing_extensions import Annotated
11
+
9
12
  from pythonwrench.checksum import checksum_any
10
13
  from pythonwrench.collections import all_ne
11
14
  from pythonwrench.math import nextafter
@@ -74,6 +77,23 @@ class TestChecksum(TestCase):
74
77
  assert Color.RED == Color.ROUGE
75
78
  assert checksum_any(Color.RED) == checksum_any(Color.ROUGE)
76
79
 
80
+ def test_typing_classes(self) -> None:
81
+ x = [
82
+ Any,
83
+ Iterable,
84
+ Iterable[int],
85
+ Annotated,
86
+ Annotated[int, "annotation"],
87
+ Tuple[int, ...],
88
+ Tuple[int],
89
+ Tuple[int, int],
90
+ tuple,
91
+ Mapping,
92
+ Mapping[int, int],
93
+ ]
94
+ csums = [checksum_any(xi) for xi in x]
95
+ assert all_ne(csums), f"{csums=}"
96
+
77
97
 
78
98
  if __name__ == "__main__":
79
99
  unittest.main()
@@ -25,6 +25,7 @@ from pythonwrench.collections import (
25
25
  recursive_generator,
26
26
  reduce_add,
27
27
  shuffled,
28
+ sorted_dict,
28
29
  unflat_dict_of_dict,
29
30
  unflat_list_of_list,
30
31
  union_dicts,
@@ -325,5 +326,18 @@ class TestReduce(TestCase):
325
326
  assert reduce_add(args, args, start=1)
326
327
 
327
328
 
329
+ class TestSortedDict(TestCase):
330
+ def test_example_1(self) -> None:
331
+ x = {"b": 2, "a": 3, "c": 1}
332
+ result = sorted_dict(x)
333
+ assert result == {"a": 3, "b": 2, "c": 1}
334
+
335
+ result = sorted_dict(x, by="value")
336
+ assert result == {"c": 1, "b": 2, "a": 3}
337
+
338
+ result = sorted_dict(x, reverse=True, key=lambda x: x % 3, by="value")
339
+ assert result == {"a": 3, "c": 1, "b": 2}
340
+
341
+
328
342
  if __name__ == "__main__":
329
343
  unittest.main()
@@ -22,14 +22,18 @@ from typing import (
22
22
  )
23
23
  from unittest import TestCase
24
24
 
25
+ import typing_extensions
25
26
  from typing_extensions import NotRequired
26
27
 
27
28
  from pythonwrench.typing import (
28
29
  NoneType,
29
30
  check_args_types,
31
+ is_collection_alias,
30
32
  is_dataclass_instance,
31
33
  is_iterable_str,
32
34
  is_namedtuple_instance,
35
+ is_parameterized,
36
+ is_special_form,
33
37
  is_typed_dict,
34
38
  isinstance_generic,
35
39
  )
@@ -101,6 +105,25 @@ class TestChecks(TestCase):
101
105
  assert not is_dataclass_instance(nt1)
102
106
  assert not is_dataclass_instance(nt2)
103
107
 
108
+ def test_is_parameterized(self) -> None:
109
+ assert is_parameterized(Iterable[int])
110
+ assert is_parameterized(Tuple[int, ...])
111
+ assert not is_parameterized(Iterable)
112
+ assert not is_parameterized(Tuple)
113
+
114
+ def test_is_collection_alias(self) -> None:
115
+ assert not is_collection_alias(Iterable[int])
116
+ assert not is_collection_alias(Tuple[int, ...])
117
+ assert is_collection_alias(Iterable)
118
+ assert not is_collection_alias(Tuple)
119
+ assert not is_collection_alias(list)
120
+
121
+ def test_is_special_form(self) -> None:
122
+ assert not is_special_form(Iterable[int])
123
+ assert not is_special_form(Tuple[int, ...])
124
+ assert is_special_form(Any)
125
+ assert is_special_form(typing_extensions.Any)
126
+
104
127
 
105
128
  class TestIsInstanceGuard(TestCase):
106
129
  def test_docstring_examples(self) -> None:
File without changes
File without changes
File without changes
File without changes