pythonwrench 0.5.0__tar.gz → 0.5.2__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.2}/PKG-INFO +6 -2
  2. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/README.md +3 -0
  3. pythonwrench-0.5.2/docs/requirements.txt +4 -0
  4. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/pyproject.toml +3 -1
  5. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/__init__.py +13 -1
  6. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/_core.py +1 -1
  7. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/argparse.py +0 -2
  8. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/cast.py +4 -2
  9. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/checksum.py +98 -35
  10. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/collections.py +57 -1
  11. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/disk_cache.py +5 -9
  12. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/functools.py +21 -1
  13. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/inspect.py +1 -20
  14. pythonwrench-0.5.2/src/pythonwrench/time.py +31 -0
  15. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/__init__.py +8 -0
  16. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/checks.py +97 -0
  17. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/classes.py +2 -0
  18. {pythonwrench-0.5.0 → pythonwrench-0.5.2/src/pythonwrench.egg-info}/PKG-INFO +6 -2
  19. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/SOURCES.txt +2 -0
  20. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/requires.txt +3 -1
  21. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_cast.py +12 -4
  22. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_checksum.py +20 -0
  23. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_collections.py +14 -0
  24. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_typing.py +23 -0
  25. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/LICENSE +0 -0
  26. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/setup.cfg +0 -0
  27. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/setup.py +0 -0
  28. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/__main__.py +0 -0
  29. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/abc.py +0 -0
  30. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/__init__.py +0 -0
  31. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/prop.py +0 -0
  32. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/reducers.py +0 -0
  33. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/concurrent.py +0 -0
  34. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/csv.py +0 -0
  35. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/dataclasses.py +0 -0
  36. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/datetime.py +0 -0
  37. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/difflib.py +0 -0
  38. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/entries.py +0 -0
  39. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/enum.py +0 -0
  40. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/hashlib.py +0 -0
  41. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/importlib.py +0 -0
  42. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/json.py +0 -0
  43. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/jsonl.py +0 -0
  44. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/logging.py +0 -0
  45. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/math.py +0 -0
  46. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/os.py +0 -0
  47. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/pickle.py +0 -0
  48. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/random.py +0 -0
  49. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/re.py +0 -0
  50. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/semver.py +0 -0
  51. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/warnings.py +0 -0
  52. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  53. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/entry_points.txt +0 -0
  54. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
  55. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_abc.py +0 -0
  56. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_argparse.py +0 -0
  57. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_csv.py +0 -0
  58. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_dataclasses.py +0 -0
  59. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_difflib.py +0 -0
  60. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_disk_cache.py +0 -0
  61. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_entries.py +0 -0
  62. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_enum.py +0 -0
  63. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_functools.py +0 -0
  64. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_hashlib.py +0 -0
  65. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_importlib.py +0 -0
  66. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_inspect.py +0 -0
  67. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_json.py +0 -0
  68. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_jsonl.py +0 -0
  69. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_logging.py +0 -0
  70. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_math.py +0 -0
  71. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_os.py +0 -0
  72. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_random.py +0 -0
  73. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_readme.py +0 -0
  74. {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_semver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -50,9 +50,10 @@ Classifier: Operating System :: POSIX
50
50
  Requires-Python: <3.15,>=3.8
51
51
  Description-Content-Type: text/markdown
52
52
  License-File: LICENSE
53
- Requires-Dist: lazy-loader>=0.4
54
53
  Requires-Dist: typing-extensions>=4.10.0
55
54
  Provides-Extra: dev
55
+ Provides-Extra: lazy
56
+ Requires-Dist: lazy-loader>=0.4; extra == "lazy"
56
57
  Dynamic: license-file
57
58
 
58
59
  # pythonwrench
@@ -218,6 +219,9 @@ Easely converts common python structures like list of dicts to dict of lists :
218
219
  ... True
219
220
  ```
220
221
 
222
+ ## Lazy loading
223
+ To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
224
+
221
225
  ## Contact
222
226
  Maintainer:
223
227
  - [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
@@ -161,6 +161,9 @@ Easely converts common python structures like list of dicts to dict of lists :
161
161
  ... True
162
162
  ```
163
163
 
164
+ ## Lazy loading
165
+ To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
166
+
164
167
  ## Contact
165
168
  Maintainer:
166
169
  - [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ sphinx-immaterial>=0.11.14
4
+ sphinx<9.0.0
@@ -31,7 +31,6 @@ maintainers = [
31
31
  ]
32
32
  dynamic = ["version"]
33
33
  dependencies = [
34
- "lazy-loader>=0.4",
35
34
  "typing-extensions>=4.10.0",
36
35
  ]
37
36
 
@@ -52,6 +51,9 @@ pyw-safe-rmdir = "pythonwrench.entries:main_safe_rmdir"
52
51
 
53
52
  [project.optional-dependencies]
54
53
  dev = []
54
+ lazy = [
55
+ "lazy-loader>=0.4",
56
+ ]
55
57
 
56
58
  [build-system]
57
59
  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.2"
13
13
 
14
14
 
15
15
  from typing import TYPE_CHECKING
@@ -114,10 +114,12 @@ if TYPE_CHECKING or lazy is None:
114
114
  )
115
115
  from .hashlib import hash_file
116
116
  from .importlib import (
117
+ Placeholder,
117
118
  is_available_package,
118
119
  is_editable_package,
119
120
  reload_editable_packages,
120
121
  reload_submodules,
122
+ requires_packages,
121
123
  search_submodules,
122
124
  )
123
125
  from .inspect import get_argnames, get_current_fn_name, get_fullname
@@ -187,6 +189,7 @@ if TYPE_CHECKING or lazy is None:
187
189
  SupportsGetitemIterLen2,
188
190
  SupportsGetitemLen,
189
191
  SupportsGetitemLen2,
192
+ SupportsIter,
190
193
  SupportsIterLen,
191
194
  SupportsLen,
192
195
  SupportsMatmul,
@@ -199,6 +202,7 @@ if TYPE_CHECKING or lazy is None:
199
202
  is_builtin_number,
200
203
  is_builtin_obj,
201
204
  is_builtin_scalar,
205
+ is_collection_alias,
202
206
  is_dataclass_instance,
203
207
  is_iterable_bool,
204
208
  is_iterable_bytes_or_list,
@@ -207,7 +211,9 @@ if TYPE_CHECKING or lazy is None:
207
211
  is_iterable_integral,
208
212
  is_iterable_str,
209
213
  is_namedtuple_instance,
214
+ is_parameterized,
210
215
  is_sequence_str,
216
+ is_special_form,
211
217
  is_typed_dict,
212
218
  isinstance_generic,
213
219
  )
@@ -321,7 +327,9 @@ else:
321
327
  "is_editable_package",
322
328
  "reload_editable_packages",
323
329
  "reload_submodules",
330
+ "requires_packages",
324
331
  "search_submodules",
332
+ "Placeholder",
325
333
  ],
326
334
  "inspect": ["get_argnames", "get_current_fn_name", "get_fullname"],
327
335
  "json": [
@@ -397,6 +405,7 @@ else:
397
405
  "SupportsGetitemIterLen2",
398
406
  "SupportsGetitemLen",
399
407
  "SupportsGetitemLen2",
408
+ "SupportsIter",
400
409
  "SupportsIterLen",
401
410
  "SupportsLen",
402
411
  "SupportsMatmul",
@@ -409,6 +418,7 @@ else:
409
418
  "is_builtin_number",
410
419
  "is_builtin_obj",
411
420
  "is_builtin_scalar",
421
+ "is_collection_alias",
412
422
  "is_dataclass_instance",
413
423
  "is_iterable_bool",
414
424
  "is_iterable_bytes_or_list",
@@ -417,7 +427,9 @@ else:
417
427
  "is_iterable_integral",
418
428
  "is_iterable_str",
419
429
  "is_namedtuple_instance",
430
+ "is_parameterized",
420
431
  "is_sequence_str",
432
+ "is_special_form",
421
433
  "is_typed_dict",
422
434
  "isinstance_generic",
423
435
  ],
@@ -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:
@@ -119,8 +119,6 @@ def str_to_optional_bool(
119
119
  x: str,
120
120
  *,
121
121
  case_sensitive: bool = False,
122
- true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
123
- false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
124
122
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
125
123
  ) -> Optional[bool]:
126
124
  """Convert string values to optional bool safely. Intended for argparse arguments.
@@ -3,7 +3,6 @@
3
3
 
4
4
  from argparse import Namespace
5
5
  from collections import Counter
6
- from dataclasses import asdict
7
6
  from datetime import date
8
7
  from enum import Enum
9
8
  from functools import partial
@@ -119,7 +118,10 @@ def _namespace_to_builtin(x: Namespace, **kwargs) -> Any:
119
118
 
120
119
  @register_as_builtin_fn(DataclassInstance)
121
120
  def _dataclass_to_builtin(x: DataclassInstance, **kwargs) -> Any:
122
- return as_builtin(asdict(x), **kwargs)
121
+ # IMPORTANT note : we do not use dataclasses.asdict() because it also converts attributes like Counter, but not do dicts
122
+ field_names = x.__dataclass_fields__.keys()
123
+ xdict = {name: as_builtin(getattr(x, name), **kwargs) for name in field_names}
124
+ return xdict
123
125
 
124
126
 
125
127
  @register_as_builtin_fn(NamedTupleInstance)
@@ -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":
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
+ import inspect
5
+ from types import CodeType
4
6
  from typing import (
5
7
  Any,
6
8
  Callable,
@@ -14,7 +16,7 @@ from typing import (
14
16
  from typing_extensions import ParamSpec
15
17
 
16
18
  from pythonwrench._core import _decorator_factory, return_none # noqa: F401
17
- from pythonwrench.inspect import _get_code_and_start, get_argnames
19
+ from pythonwrench.inspect import get_argnames
18
20
  from pythonwrench.typing import isinstance_generic
19
21
 
20
22
  T = TypeVar("T")
@@ -162,3 +164,21 @@ def identity(x: T, **kwargs) -> T:
162
164
  def repeat_fn(f: Callable[[T], T], n: int) -> Callable[[T], T]:
163
165
  """Creates wrapper which call a function n items."""
164
166
  return Compose([f] * n)
167
+
168
+
169
+ def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]:
170
+ if inspect.isfunction(fn):
171
+ code = fn.__code__
172
+ start = 0
173
+ elif inspect.ismethod(fn):
174
+ code = fn.__code__
175
+ start = 1 # If method, remove 'self' arg
176
+ elif inspect.isclass(fn):
177
+ # If init, remove 'self' arg
178
+ code = fn.__init__.__code__
179
+ start = 1 # If init, remove 'self' arg
180
+ else:
181
+ code = fn.__call__.__code__
182
+ start = 0
183
+
184
+ return code, start
@@ -2,8 +2,7 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  import inspect
5
- from types import CodeType
6
- from typing import Any, Callable, List, Tuple, TypeVar, Union, get_args
5
+ from typing import Any, Callable, List, TypeVar, Union, get_args
7
6
 
8
7
  T = TypeVar("T")
9
8
 
@@ -68,21 +67,3 @@ def get_fullname(x: Any, *, inst_suffix: str = "(...)") -> str:
68
67
  name = f"{name}[{argsnames_str}]"
69
68
 
70
69
  return name
71
-
72
-
73
- def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]:
74
- if inspect.isfunction(fn):
75
- code = fn.__code__
76
- start = 0
77
- elif inspect.ismethod(fn):
78
- code = fn.__code__
79
- start = 1 # If method, remove 'self' arg
80
- elif inspect.isclass(fn):
81
- # If init, remove 'self' arg
82
- code = fn.__init__.__code__
83
- start = 1 # If init, remove 'self' arg
84
- else:
85
- code = fn.__call__.__code__
86
- start = 0
87
-
88
- return code, start
@@ -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
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -50,9 +50,10 @@ Classifier: Operating System :: POSIX
50
50
  Requires-Python: <3.15,>=3.8
51
51
  Description-Content-Type: text/markdown
52
52
  License-File: LICENSE
53
- Requires-Dist: lazy-loader>=0.4
54
53
  Requires-Dist: typing-extensions>=4.10.0
55
54
  Provides-Extra: dev
55
+ Provides-Extra: lazy
56
+ Requires-Dist: lazy-loader>=0.4; extra == "lazy"
56
57
  Dynamic: license-file
57
58
 
58
59
  # pythonwrench
@@ -218,6 +219,9 @@ Easely converts common python structures like list of dicts to dict of lists :
218
219
  ... True
219
220
  ```
220
221
 
222
+ ## Lazy loading
223
+ To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
224
+
221
225
  ## Contact
222
226
  Maintainer:
223
227
  - [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
@@ -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
@@ -1,4 +1,6 @@
1
- lazy-loader>=0.4
2
1
  typing-extensions>=4.10.0
3
2
 
4
3
  [dev]
4
+
5
+ [lazy]
6
+ lazy-loader>=0.4
@@ -3,6 +3,7 @@
3
3
 
4
4
  import unittest
5
5
  from collections import Counter
6
+ from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
  from unittest import TestCase
8
9
 
@@ -14,8 +15,17 @@ class CustomClass:
14
15
  b: str = ""
15
16
 
16
17
 
18
+ @dataclass
19
+ class CustomDataclass:
20
+ counts: Counter = field(default_factory=Counter)
21
+
22
+
17
23
  class TestCast(TestCase):
18
24
  def test_example_1(self) -> None:
25
+ @register_as_builtin_fn(CustomClass)
26
+ def customclass_to_builtin(x: CustomClass) -> dict:
27
+ return {"a": x.a, "b": x.b, "added_prop": None}
28
+
19
29
  examples = [
20
30
  ("a", "a"),
21
31
  ((), []),
@@ -25,12 +35,10 @@ class TestCast(TestCase):
25
35
  (CustomClass(), {"a": 0, "b": "", "added_prop": None}),
26
36
  ({"a": (1, 2)}, {"a": [1, 2]}),
27
37
  (Counter(a=2, b=1, c=3), {"a": 2, "b": 1, "c": 3}),
38
+ (Counter(["a", "b", "a"]), {"a": 2, "b": 1}),
39
+ (CustomDataclass(Counter(["a", "b", "a"])), {"counts": {"a": 2, "b": 1}}),
28
40
  ]
29
41
 
30
- @register_as_builtin_fn(CustomClass)
31
- def customclass_to_builtin(x: CustomClass) -> dict:
32
- return {"a": x.a, "b": x.b, "added_prop": None}
33
-
34
42
  for x, expected in examples:
35
43
  result = as_builtin(x)
36
44
  assert result == expected
@@ -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