assertpy 2.35.0__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 (63) hide show
  1. assertpy/__init__.py +87 -0
  2. assertpy/_clustering.py +511 -0
  3. assertpy/_dangling.py +505 -0
  4. assertpy/_engine/__init__.py +1 -0
  5. assertpy/_engine/_builder_check_typing.py +1365 -0
  6. assertpy/_engine/_capable_typing.py +772 -0
  7. assertpy/_engine/_check_typing.py +753 -0
  8. assertpy/_engine/_compare.py +983 -0
  9. assertpy/_engine/_compat.py +38 -0
  10. assertpy/_engine/_contract.py +1926 -0
  11. assertpy/_engine/_diff.py +1058 -0
  12. assertpy/_engine/_equality.py +843 -0
  13. assertpy/_engine/_introspection.py +729 -0
  14. assertpy/_engine/_membership.py +522 -0
  15. assertpy/_engine/_mixin_base.py +258 -0
  16. assertpy/_engine/_negated_typing.py +2176 -0
  17. assertpy/_engine/_operations.py +75 -0
  18. assertpy/_engine/_ordering.py +884 -0
  19. assertpy/_engine/_pairing.py +70 -0
  20. assertpy/_engine/_path.py +158 -0
  21. assertpy/_engine/_poll_typing.py +6039 -0
  22. assertpy/_engine/_require.py +312 -0
  23. assertpy/_engine/_size.py +31 -0
  24. assertpy/_engine/_text.py +39 -0
  25. assertpy/_engine/_typing.py +1226 -0
  26. assertpy/_hints.py +1213 -0
  27. assertpy/_inline.py +484 -0
  28. assertpy/_matcher_impls.py +1876 -0
  29. assertpy/_passes.py +249 -0
  30. assertpy/_renamed.py +64 -0
  31. assertpy/_satisfies.py +835 -0
  32. assertpy/_snapshot_codec.py +290 -0
  33. assertpy/assertpy.py +2640 -0
  34. assertpy/async_assertions.py +864 -0
  35. assertpy/base.py +862 -0
  36. assertpy/behave_matchers.py +96 -0
  37. assertpy/bytes_mixin.py +158 -0
  38. assertpy/collection.py +528 -0
  39. assertpy/contains.py +1068 -0
  40. assertpy/dataframe.py +200 -0
  41. assertpy/date.py +327 -0
  42. assertpy/dict.py +249 -0
  43. assertpy/dynamic.py +222 -0
  44. assertpy/errors.py +782 -0
  45. assertpy/exception.py +838 -0
  46. assertpy/extracting.py +266 -0
  47. assertpy/file.py +324 -0
  48. assertpy/helpers.py +747 -0
  49. assertpy/http_mixin.py +218 -0
  50. assertpy/json_mixin.py +1265 -0
  51. assertpy/matchers.py +639 -0
  52. assertpy/numeric.py +696 -0
  53. assertpy/outcome.py +163 -0
  54. assertpy/py.typed +0 -0
  55. assertpy/pytest_plugin.py +1650 -0
  56. assertpy/snapshot.py +1031 -0
  57. assertpy/string.py +803 -0
  58. assertpy/warning.py +137 -0
  59. assertpy-2.35.0.dist-info/METADATA +324 -0
  60. assertpy-2.35.0.dist-info/RECORD +63 -0
  61. assertpy-2.35.0.dist-info/WHEEL +4 -0
  62. assertpy-2.35.0.dist-info/entry_points.txt +2 -0
  63. assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import dataclasses
5
+ import datetime
6
+ import decimal
7
+ import enum
8
+ import json
9
+ import os
10
+ import sys
11
+ import types
12
+ import uuid
13
+ from typing import TYPE_CHECKING, Any, NamedTuple
14
+
15
+ from ._engine._introspection import (
16
+ is_attrs_instance,
17
+ is_pydantic_model,
18
+ is_pydantic_model_class,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+ __tracebackhide__ = True
25
+
26
+
27
+ class _Serializer(NamedTuple):
28
+ cls: type
29
+ encode: Callable[[object], object]
30
+ decode: Callable[[object], object]
31
+ tag: str
32
+
33
+
34
+ # user-registered serializers, checked before the built-in codec (last registered wins)
35
+ _SERIALIZERS: list[_Serializer] = []
36
+
37
+ # a user dict carrying any of them is escaped so it is not mistaken for an envelope on load
38
+ _RESERVED_MARKERS = frozenset({"__type__", "__data__", "__tag__", "__class__", "__module__"})
39
+
40
+
41
+ def _prepare(value, _seen: frozenset[int] = frozenset()):
42
+ """Pre-process a value so ``json`` round-trips dicts that it otherwise mangles: dicts with a
43
+ non-string key (which ``json`` coerces to a string) and dicts that collide with the codec's markers
44
+ (which the decoder would misread). Both are wrapped in a ``{"__type__": "dict", "__data__": [[k,
45
+ v], ...]}`` envelope; a plain string-keyed dict without markers is left as an ordinary object so
46
+ existing snapshots stay byte-identical."""
47
+ if isinstance(value, (dict, list, tuple)):
48
+ if id(value) in _seen:
49
+ # json cannot represent a cycle, so naming it beats a thousand frames of RecursionError
50
+ raise ValueError("cannot snapshot a value that contains a circular reference")
51
+ nested = _seen | {id(value)}
52
+ if isinstance(value, dict):
53
+ if all(isinstance(key, str) for key in value) and not _RESERVED_MARKERS.intersection(value):
54
+ return {key: _prepare(item, nested) for key, item in value.items()}
55
+ items = sorted(value.items(), key=lambda pair: repr(pair[0])) # deterministic for mixed keys
56
+ return {
57
+ "__type__": "dict",
58
+ "__data__": [[_prepare(key, nested), _prepare(item, nested)] for key, item in items],
59
+ }
60
+ # a tuple comes back a list: `_rehash_key` restores one only where it was a dict key
61
+ return [_prepare(item, nested) for item in value]
62
+ return value
63
+
64
+
65
+ def _hashable(item):
66
+ """An element as it was written: JSON has no tuple, and a list cannot have been one, since it cannot hash."""
67
+ return tuple(_hashable(inner) for inner in item) if isinstance(item, list) else item
68
+
69
+
70
+ def _rehash_key(key):
71
+ """Restore a hashable dict key from its json form: a list can only be a re-encoded tuple (a list is
72
+ not hashable and so could never have been a key), so convert it back to a tuple recursively."""
73
+ if isinstance(key, list):
74
+ return tuple(_rehash_key(item) for item in key)
75
+ return key
76
+
77
+
78
+ class _Encoder(json.JSONEncoder):
79
+ # `o` is what `JSONEncoder` names it, and pyright reads an override by parameter name
80
+ def default(self, o):
81
+ for entry in _SERIALIZERS:
82
+ if isinstance(o, entry.cls):
83
+ return {"__type__": "custom", "__tag__": entry.tag, "__data__": entry.encode(o)}
84
+ if isinstance(o, set):
85
+ return {"__type__": "set", "__data__": list(o)}
86
+ elif isinstance(o, complex):
87
+ return {"__type__": "complex", "__data__": [o.real, o.imag]}
88
+ elif isinstance(o, datetime.datetime):
89
+ # each suffix is added only when the value carries it, so a snapshot with neither microseconds nor
90
+ # `tzinfo` keeps the historical format and stays readable by an older version
91
+ fmt = "%Y-%m-%d %H:%M:%S.%f" if o.microsecond else "%Y-%m-%d %H:%M:%S"
92
+ if o.tzinfo is not None:
93
+ fmt += "%z"
94
+ return {"__type__": "datetime", "__data__": o.strftime(fmt)}
95
+ elif isinstance(o, datetime.date):
96
+ return {"__type__": "date", "__data__": o.isoformat()}
97
+ elif isinstance(o, datetime.time):
98
+ return {"__type__": "time", "__data__": o.isoformat()}
99
+ elif isinstance(o, decimal.Decimal):
100
+ return {"__type__": "decimal", "__data__": str(o)}
101
+ elif isinstance(o, (bytes, bytearray)):
102
+ return {"__type__": "bytes", "__data__": base64.b64encode(bytes(o)).decode("ascii")}
103
+ elif isinstance(o, uuid.UUID):
104
+ return {"__type__": "uuid", "__data__": str(o)}
105
+ elif isinstance(o, enum.Enum):
106
+ return {
107
+ "__type__": "enum",
108
+ "__class__": o.__class__.__name__,
109
+ "__module__": o.__class__.__module__,
110
+ "__data__": o.value,
111
+ }
112
+ elif _slot_aware(o) or is_pydantic_model(o) or ("__dict__" in dir(o) and type(o) is not type):
113
+ return {
114
+ "__type__": "instance",
115
+ "__class__": o.__class__.__name__,
116
+ "__module__": o.__class__.__module__,
117
+ # prepared like any other mapping: a non-string key inside an attribute came back a string
118
+ "__data__": _prepare(_held(o)),
119
+ **_model_state(o),
120
+ }
121
+ return json.JSONEncoder.default(self, o)
122
+
123
+
124
+ class _Decoder(json.JSONDecoder):
125
+ def __init__(self):
126
+ json.JSONDecoder.__init__(self, object_hook=self._object_hook)
127
+
128
+ def _object_hook(self, decoded):
129
+ if "__type__" in decoded and "__data__" in decoded:
130
+ if decoded["__type__"] == "dict":
131
+ return {_rehash_key(key): item for key, item in decoded["__data__"]}
132
+ elif decoded["__type__"] == "set":
133
+ return {_hashable(item) for item in decoded["__data__"]}
134
+ elif decoded["__type__"] == "complex":
135
+ return complex(decoded["__data__"][0], decoded["__data__"][1])
136
+ elif decoded["__type__"] == "datetime":
137
+ raw = decoded["__data__"]
138
+ tail = raw[len("0000-00-00 00:00:00") :] # the date part contains "-", so probe past the seconds
139
+ fmt = "%Y-%m-%d %H:%M:%S"
140
+ if "." in tail:
141
+ fmt += ".%f"
142
+ if "+" in tail or "-" in tail:
143
+ fmt += "%z"
144
+ return datetime.datetime.strptime(raw, fmt)
145
+ elif decoded["__type__"] == "date":
146
+ return datetime.date.fromisoformat(decoded["__data__"])
147
+ elif decoded["__type__"] == "time":
148
+ return datetime.time.fromisoformat(decoded["__data__"])
149
+ elif decoded["__type__"] == "decimal":
150
+ return decimal.Decimal(decoded["__data__"])
151
+ elif decoded["__type__"] == "bytes":
152
+ return base64.b64decode(decoded["__data__"])
153
+ elif decoded["__type__"] == "uuid":
154
+ return uuid.UUID(decoded["__data__"])
155
+ elif decoded["__type__"] == "custom":
156
+ tag = decoded.get("__tag__")
157
+ for entry in _SERIALIZERS:
158
+ if entry.tag == tag:
159
+ return entry.decode(decoded["__data__"])
160
+ return decoded # no serializer registered for this tag this run - leave the marker as-is
161
+ elif decoded["__type__"] == "enum":
162
+ target_class = _resolve_class(decoded["__module__"], decoded["__class__"])
163
+ if target_class is None:
164
+ return decoded
165
+ raw = decoded["__data__"]
166
+ try:
167
+ return target_class(raw)
168
+ except ValueError:
169
+ if not isinstance(raw, list):
170
+ raise
171
+ return target_class(_hashable(raw))
172
+ elif decoded["__type__"] == "instance":
173
+ target_class = _resolve_class(decoded["__module__"], decoded["__class__"])
174
+ if target_class is None:
175
+ return decoded
176
+ return _rebuilt(target_class, decoded)
177
+ return decoded
178
+
179
+
180
+ def _slot_aware(value: object) -> bool:
181
+ """An attrs instance or a dataclass, the two kinds this reads through slots as well as a ``__dict__``.
182
+
183
+ Slotted by default in attrs and on request in dataclasses, and without a ``__dict__`` either raised
184
+ `Object of type ... is not JSON serializable`. Other slotted objects stay refused: a slot of the
185
+ standard library's is state nobody vouched for rebuilding.
186
+ """
187
+ return is_attrs_instance(value) or (dataclasses.is_dataclass(value) and not isinstance(value, type))
188
+
189
+
190
+ def _slot_names(target_class: type) -> set[str]:
191
+ """Every slot the class and its bases declare, under the name it is stored by, mangled or not."""
192
+ return {
193
+ name
194
+ for klass in target_class.__mro__
195
+ for name, member in vars(klass).items()
196
+ if isinstance(member, types.MemberDescriptorType)
197
+ }
198
+
199
+
200
+ def _held(value: object) -> dict:
201
+ """What an instance holds: its ``__dict__`` and any slot set on it.
202
+
203
+ A slot is read past ``__getattr__``, so one never set stays unwritten: attrs computes a
204
+ ``cached_property`` of a slotted class there, and writing a snapshot would have run it.
205
+ """
206
+ held = dict(vars(value)) if hasattr(value, "__dict__") else {}
207
+ for name in _slot_names(type(value)) if _slot_aware(value) else ():
208
+ try:
209
+ held[name] = object.__getattribute__(value, name)
210
+ except AttributeError: # noqa: PERF203 # a slot never set holds nothing to write
211
+ continue
212
+ return held
213
+
214
+
215
+ def _model_state(value: object) -> dict:
216
+ """What a pydantic model holds beside its ``__dict__``: the extras and the private state its ``==``
217
+ compares, and the fields set, which ``model_dump(exclude_unset=True)`` reads. Nothing for any other value."""
218
+ if not is_pydantic_model(value):
219
+ return {}
220
+ extra, private = getattr(value, "__pydantic_extra__", None), getattr(value, "__pydantic_private__", None)
221
+ return {
222
+ "__extra__": None if extra is None else _prepare(dict(extra)),
223
+ "__private__": None if private is None else _prepare(dict(private)),
224
+ "__fields_set__": sorted(getattr(value, "model_fields_set", ())),
225
+ }
226
+
227
+
228
+ def _rebuilt(target_class: Any, written: dict) -> object:
229
+ """An instance of *target_class* holding what was written, built without running its ``__init__``.
230
+
231
+ A slot is set attribute by attribute, since a ``__dict__`` cannot hold it.
232
+ """
233
+ if is_pydantic_model_class(target_class):
234
+ return _rebuilt_model(target_class, written)
235
+ held = written["__data__"]
236
+ instance = target_class.__new__(target_class)
237
+ slots = _slot_names(target_class)
238
+ if hasattr(instance, "__dict__"):
239
+ instance.__dict__ = {name: value for name, value in held.items() if name not in slots}
240
+ for name in slots & held.keys():
241
+ object.__setattr__(instance, name, held[name])
242
+ return instance
243
+
244
+
245
+ def _rebuilt_model(target_class: Any, written: dict) -> object:
246
+ """A pydantic model in the state it was written in, through pydantic's own ``__setstate__``.
247
+
248
+ Assigned a ``__dict__`` alone it had none of the state its ``==`` reads, and every snapshot of a model
249
+ failed on the run after it was written. `model_construct` runs `model_post_init`, and a subclass may
250
+ override ``__new__`` or ``__setstate__``: reading a snapshot runs none of the caller's code. A file
251
+ written before the rest of the state was stored holds the fields alone, and reads as a model holding
252
+ them and nothing else.
253
+ """
254
+ model = object.__new__(target_class)
255
+ sys.modules["pydantic"].BaseModel.__setstate__(
256
+ model,
257
+ {
258
+ "__dict__": written["__data__"],
259
+ "__pydantic_extra__": written.get("__extra__"),
260
+ "__pydantic_fields_set__": set(written.get("__fields_set__", written["__data__"])),
261
+ "__pydantic_private__": written.get("__private__"),
262
+ },
263
+ )
264
+ return model
265
+
266
+
267
+ def _resolve_class(module_name, class_name):
268
+ """Resolve a class by module+name without importing anything (never runs arbitrary imports, per the
269
+ snapshot security model); returns ``None`` if the module is not already imported or the name is absent."""
270
+ if module_name not in sys.modules:
271
+ return None
272
+ return getattr(sys.modules[module_name], class_name, None)
273
+
274
+
275
+ def _save(name, val):
276
+ # rendered whole first: `json.dump` calls `write` once per fragment where `dumps` joins them once, and
277
+ # with `indent` neither reaches the C encoder. Five thousand records: rendering to a string 1.4 ms
278
+ # against 10.1 ms for the same rendering written fragment by fragment, 20.8 ms against 8.0 ms end to
279
+ # end. A value the encoder refuses now leaves no file behind either
280
+ text = json.dumps(_prepare(val), indent=2, separators=(",", ": "), sort_keys=True, cls=_Encoder)
281
+ tmp = f"{name}.{os.getpid()}.tmp"
282
+ with open(tmp, "w", encoding="utf-8") as file_handle:
283
+ file_handle.write(text)
284
+ os.replace(tmp, name)
285
+
286
+
287
+ def _load(name):
288
+ # as UTF-8 and not as the platform's default: a file a formatter left unescaped read as mojibake under cp1252
289
+ with open(name, encoding="utf-8") as file_handle:
290
+ return json.load(file_handle, cls=_Decoder)