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.
- assertpy/__init__.py +87 -0
- assertpy/_clustering.py +511 -0
- assertpy/_dangling.py +505 -0
- assertpy/_engine/__init__.py +1 -0
- assertpy/_engine/_builder_check_typing.py +1365 -0
- assertpy/_engine/_capable_typing.py +772 -0
- assertpy/_engine/_check_typing.py +753 -0
- assertpy/_engine/_compare.py +983 -0
- assertpy/_engine/_compat.py +38 -0
- assertpy/_engine/_contract.py +1926 -0
- assertpy/_engine/_diff.py +1058 -0
- assertpy/_engine/_equality.py +843 -0
- assertpy/_engine/_introspection.py +729 -0
- assertpy/_engine/_membership.py +522 -0
- assertpy/_engine/_mixin_base.py +258 -0
- assertpy/_engine/_negated_typing.py +2176 -0
- assertpy/_engine/_operations.py +75 -0
- assertpy/_engine/_ordering.py +884 -0
- assertpy/_engine/_pairing.py +70 -0
- assertpy/_engine/_path.py +158 -0
- assertpy/_engine/_poll_typing.py +6039 -0
- assertpy/_engine/_require.py +312 -0
- assertpy/_engine/_size.py +31 -0
- assertpy/_engine/_text.py +39 -0
- assertpy/_engine/_typing.py +1226 -0
- assertpy/_hints.py +1213 -0
- assertpy/_inline.py +484 -0
- assertpy/_matcher_impls.py +1876 -0
- assertpy/_passes.py +249 -0
- assertpy/_renamed.py +64 -0
- assertpy/_satisfies.py +835 -0
- assertpy/_snapshot_codec.py +290 -0
- assertpy/assertpy.py +2640 -0
- assertpy/async_assertions.py +864 -0
- assertpy/base.py +862 -0
- assertpy/behave_matchers.py +96 -0
- assertpy/bytes_mixin.py +158 -0
- assertpy/collection.py +528 -0
- assertpy/contains.py +1068 -0
- assertpy/dataframe.py +200 -0
- assertpy/date.py +327 -0
- assertpy/dict.py +249 -0
- assertpy/dynamic.py +222 -0
- assertpy/errors.py +782 -0
- assertpy/exception.py +838 -0
- assertpy/extracting.py +266 -0
- assertpy/file.py +324 -0
- assertpy/helpers.py +747 -0
- assertpy/http_mixin.py +218 -0
- assertpy/json_mixin.py +1265 -0
- assertpy/matchers.py +639 -0
- assertpy/numeric.py +696 -0
- assertpy/outcome.py +163 -0
- assertpy/py.typed +0 -0
- assertpy/pytest_plugin.py +1650 -0
- assertpy/snapshot.py +1031 -0
- assertpy/string.py +803 -0
- assertpy/warning.py +137 -0
- assertpy-2.35.0.dist-info/METADATA +324 -0
- assertpy-2.35.0.dist-info/RECORD +63 -0
- assertpy-2.35.0.dist-info/WHEEL +4 -0
- assertpy-2.35.0.dist-info/entry_points.txt +2 -0
- assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
assertpy/exception.py
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import types
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Final, TypeVar, cast, get_origin
|
|
7
|
+
|
|
8
|
+
from ._engine._compat import BaseExceptionGroup
|
|
9
|
+
from ._engine._mixin_base import _MixinBase
|
|
10
|
+
from ._engine._pairing import maximum_pairing
|
|
11
|
+
from ._engine._require import argument, refuse
|
|
12
|
+
from ._matcher_impls import _UNION_ORIGINS, _require_class_info
|
|
13
|
+
from .errors import _callable_name, _safe_str, _type_expression_name
|
|
14
|
+
from .outcome import AssertionOutcome
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
|
|
19
|
+
from ._engine._compat import Self
|
|
20
|
+
from ._matcher_impls import ClassInfo, ExceptionInfo
|
|
21
|
+
|
|
22
|
+
__tracebackhide__ = True
|
|
23
|
+
|
|
24
|
+
_UNSET: Final = object() # sentinel: no return value / exception captured yet
|
|
25
|
+
|
|
26
|
+
_E = TypeVar("_E", bound=BaseException)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _nodes(exc: BaseException) -> Iterator[BaseException]:
|
|
30
|
+
"""Every exception in the tree, groups included, the group itself before its members.
|
|
31
|
+
|
|
32
|
+
What `BaseExceptionGroup.subgroup()` matches against, in the order it meets them.
|
|
33
|
+
|
|
34
|
+
Iterative, and lazy, for the depth: `subgroup()` is written in C and answers on a group nested three
|
|
35
|
+
thousand deep, where a recursive walk in Python gives up around five hundred. Yielding also lets a
|
|
36
|
+
caller looking for one exception stop at it instead of building the whole tree first.
|
|
37
|
+
"""
|
|
38
|
+
pending = [exc]
|
|
39
|
+
while pending:
|
|
40
|
+
node = pending.pop()
|
|
41
|
+
yield node
|
|
42
|
+
if isinstance(node, BaseExceptionGroup):
|
|
43
|
+
pending.extend(reversed(node.exceptions))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _leaves(exc: BaseException) -> list[BaseException]:
|
|
47
|
+
"""The same walk without the groups: what actually failed, rather than what holds it."""
|
|
48
|
+
return [node for node in _nodes(exc) if not isinstance(node, BaseExceptionGroup)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _require_exception_type(ex: object) -> type[BaseException]:
|
|
52
|
+
"""Refuse anything that is not an exception class, in the shape `raises()` already refuses it.
|
|
53
|
+
|
|
54
|
+
`isinstance()` and `issubclass()` each report their own argument position and neither names the
|
|
55
|
+
assertion, and `isinstance(node, str)` does not complain at all: it answers False, so a mistyped
|
|
56
|
+
argument would read as "the group does not contain <str>" rather than as the mistake it is.
|
|
57
|
+
"""
|
|
58
|
+
if not (isinstance(ex, type) and issubclass(ex, BaseException)):
|
|
59
|
+
refuse(ex, "an exception type", subject=argument("exception"))
|
|
60
|
+
return ex
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_AN_EXCEPTION: Final = "an exception type, or a union or a non-empty tuple of them"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def named_classes(
|
|
67
|
+
spec: object, base: type[_E], expectation: str, *, subject: str, allow_empty: bool = False
|
|
68
|
+
) -> tuple[type[_E], ...]:
|
|
69
|
+
"""Every class *spec* names, through tuples and unions to any depth, each refused unless it subclasses *base*.
|
|
70
|
+
|
|
71
|
+
Read whole before anything is called, so a member that is no class, after one that is, is refused where
|
|
72
|
+
it is written rather than once the call raised something it would have been asked about. A tuple is read
|
|
73
|
+
from its storage, as `isinstance` reads it. An empty one names nothing that could ever be met, and is
|
|
74
|
+
refused unless *allow_empty* says nothing is a choice there, as `pytest.raises(())` refuses it.
|
|
75
|
+
"""
|
|
76
|
+
pending: list[object] = [spec]
|
|
77
|
+
found: list[type[_E]] = []
|
|
78
|
+
while pending:
|
|
79
|
+
member = pending.pop()
|
|
80
|
+
if isinstance(member, tuple):
|
|
81
|
+
pending.extend(tuple.__getitem__(member, slice(None, None, -1)))
|
|
82
|
+
elif isinstance(member, types.UnionType) or get_origin(member) in _UNION_ORIGINS:
|
|
83
|
+
pending.extend(cast("Any", member).__args__[::-1])
|
|
84
|
+
elif isinstance(member, type) and issubclass(member, base):
|
|
85
|
+
found.append(member)
|
|
86
|
+
else:
|
|
87
|
+
refuse(spec, expectation, subject=argument(subject))
|
|
88
|
+
if not found and not allow_empty:
|
|
89
|
+
refuse(spec, expectation, subject=argument(subject))
|
|
90
|
+
return tuple(found)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _expected_classes(spec: object) -> type[BaseException] | ExceptionInfo:
|
|
94
|
+
"""*spec* as it was written, once every class it names is read as an exception.
|
|
95
|
+
|
|
96
|
+
Kept as written for the message and for `expected`, as `caused_by` keeps it: `issubclass` and `isinstance`
|
|
97
|
+
read a tuple and either spelling of a union on every Python this package supports.
|
|
98
|
+
"""
|
|
99
|
+
named_classes(spec, BaseException, _AN_EXCEPTION, subject="exception")
|
|
100
|
+
return cast("type[BaseException] | ExceptionInfo", spec)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _shaped(spec: object, at: str) -> object:
|
|
104
|
+
"""Validate one node of a tree spec, returning it, and refuse anything the matcher cannot read.
|
|
105
|
+
|
|
106
|
+
A `list` is a subgroup and anything else has to be an exception type. A `tuple` is refused by name:
|
|
107
|
+
elsewhere in this library a tuple of types means "any of these", and reading it as a subgroup here
|
|
108
|
+
would give one spelling two meanings.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(spec, list):
|
|
111
|
+
if not spec:
|
|
112
|
+
refuse(
|
|
113
|
+
spec,
|
|
114
|
+
f"a non-empty subgroup at {at}, since a group with no members cannot be raised",
|
|
115
|
+
subject=argument("expected"),
|
|
116
|
+
)
|
|
117
|
+
return [_shaped(member, f"{at}[{index}]") for index, member in enumerate(spec)]
|
|
118
|
+
if isinstance(spec, tuple):
|
|
119
|
+
refuse(spec, f"a list for a subgroup at {at}, not a tuple", subject=argument("expected"))
|
|
120
|
+
return _require_exception_type(spec)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _matches_shape(spec: list[Any], exceptions: tuple[BaseException, ...]) -> bool:
|
|
124
|
+
"""Whether every spec entry pairs with a distinct exception, and none is left over.
|
|
125
|
+
|
|
126
|
+
A complete matching rather than a greedy walk, and the difference is observable: pairing in the
|
|
127
|
+
order written makes `(Exception, ValueError)` refuse a group of `ValueError` and `KeyError` while
|
|
128
|
+
`(ValueError, Exception)` accepts it, so the same claim would depend on how it was typed. Measured
|
|
129
|
+
against `pytest.RaisesGroup`, which does pair greedily and refuses the first spelling.
|
|
130
|
+
|
|
131
|
+
Recursion is over the *spec*, not the tree: a type entry stops the descent whatever is under it, so
|
|
132
|
+
the depth is what somebody wrote by hand rather than what a task group nested.
|
|
133
|
+
"""
|
|
134
|
+
if len(spec) != len(exceptions):
|
|
135
|
+
return False
|
|
136
|
+
candidates = [
|
|
137
|
+
{
|
|
138
|
+
index
|
|
139
|
+
for index, exc in enumerate(exceptions)
|
|
140
|
+
if isinstance(exc, BaseExceptionGroup) and _matches_shape(entry, exc.exceptions)
|
|
141
|
+
}
|
|
142
|
+
if isinstance(entry, list)
|
|
143
|
+
else {index for index, exc in enumerate(exceptions) if isinstance(exc, entry)}
|
|
144
|
+
for entry in spec
|
|
145
|
+
]
|
|
146
|
+
return len(maximum_pairing(candidates)) == len(spec)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _naming(spec: list[Any], exc: BaseException) -> dict[int, str]:
|
|
150
|
+
"""A name per class across both halves of the message, distinct however alike the classes are.
|
|
151
|
+
|
|
152
|
+
Two classes called `Error` from different modules otherwise render a mismatch as two identical trees,
|
|
153
|
+
which reads as the assertion contradicting itself. Qualifying is enough for those and is applied only
|
|
154
|
+
where the short names collide, following `_disambiguated`: the common case keeps the short name.
|
|
155
|
+
|
|
156
|
+
Module and qualified name do not settle it either. A class defined inside a function has both fixed,
|
|
157
|
+
so two calls to that function give two classes indistinguishable by name, and a reloaded module gives
|
|
158
|
+
the same. Those get an ordinal, ordered by where they were met rather than by identity, since a set
|
|
159
|
+
of classes iterates differently between runs and a failure message has to read the same twice.
|
|
160
|
+
|
|
161
|
+
Keyed by `id`, because a class is only usually hashable: a metaclass setting `__hash__ = None` turned
|
|
162
|
+
the failure into a `TypeError`, where the other three group assertions reported it and carried on.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def in_spec(entries: list[Any]) -> Iterator[type]:
|
|
166
|
+
for entry in entries:
|
|
167
|
+
yield from in_spec(entry) if isinstance(entry, list) else (cast("type", entry),)
|
|
168
|
+
|
|
169
|
+
kinds: dict[int, type] = {}
|
|
170
|
+
for kind in in_spec(spec):
|
|
171
|
+
kinds.setdefault(id(kind), kind)
|
|
172
|
+
for node in _nodes(exc):
|
|
173
|
+
kinds.setdefault(id(type(node)), type(node))
|
|
174
|
+
short = Counter(kind.__name__ for kind in kinds.values())
|
|
175
|
+
qualified = {
|
|
176
|
+
key: f"{kind.__module__}.{kind.__qualname__}" if short[kind.__name__] > 1 else kind.__name__
|
|
177
|
+
for key, kind in kinds.items()
|
|
178
|
+
}
|
|
179
|
+
still_alike = Counter(qualified.values())
|
|
180
|
+
ordinals: Counter[str] = Counter()
|
|
181
|
+
names = {}
|
|
182
|
+
for key, label in qualified.items():
|
|
183
|
+
ordinals[label] += 1
|
|
184
|
+
names[key] = label if still_alike[label] == 1 else f"{label}#{ordinals[label]}"
|
|
185
|
+
return names
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _shape_of(exc: BaseException, names: dict[int, str]) -> str:
|
|
189
|
+
"""The tree as names, for the message: a group becomes a bracketed list of what it holds.
|
|
190
|
+
|
|
191
|
+
Built as text rather than as a list of strings, because the list would print its names in quotes and
|
|
192
|
+
a reader comparing two trees would be reading around them.
|
|
193
|
+
|
|
194
|
+
Iterative, because this runs on the failure path over a tree the program under test built rather than
|
|
195
|
+
over anything written by hand: a group 3000 deep turned the assertion into a `RecursionError`.
|
|
196
|
+
|
|
197
|
+
Emitted as pieces joined once, rather than as each node's finished text, because the latter has every
|
|
198
|
+
ancestor holding a copy of its child: 16000 deep cost 123 ms and 258 MB of peak for 32 KB of output.
|
|
199
|
+
"""
|
|
200
|
+
pieces: list[str] = []
|
|
201
|
+
pending: list[BaseException | str] = [exc]
|
|
202
|
+
while pending:
|
|
203
|
+
node = pending.pop()
|
|
204
|
+
if isinstance(node, str):
|
|
205
|
+
pieces.append(node)
|
|
206
|
+
elif isinstance(node, BaseExceptionGroup):
|
|
207
|
+
pieces.append("[")
|
|
208
|
+
pending.append("]")
|
|
209
|
+
for position, member in enumerate(reversed(node.exceptions)):
|
|
210
|
+
if position:
|
|
211
|
+
pending.append(", ")
|
|
212
|
+
pending.append(member)
|
|
213
|
+
else:
|
|
214
|
+
pieces.append(names[id(type(node))])
|
|
215
|
+
return "".join(pieces)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _spec_shape(spec: object, names: dict[int, str]) -> str:
|
|
219
|
+
"""The same rendering for a spec, so the two halves of the message line up."""
|
|
220
|
+
if isinstance(spec, list):
|
|
221
|
+
return f"[{', '.join(_spec_shape(entry, names) for entry in spec)}]"
|
|
222
|
+
return names[id(spec)]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _escaped(exc: BaseException) -> bool:
|
|
226
|
+
"""Whether *exc* is the runner's to act on rather than a mismatch to report.
|
|
227
|
+
|
|
228
|
+
A probe interrupted by ``KeyboardInterrupt``, or one that called ``sys.exit()``, did not answer the
|
|
229
|
+
question the assertion asked. Reported as a mismatch, Ctrl+C turned into a red test, and
|
|
230
|
+
``does_not_raise()`` swallowed it whole, so neither reached the runner. Asking for one of them by
|
|
231
|
+
name still works: the caller's own branch catches what it named before this is consulted.
|
|
232
|
+
"""
|
|
233
|
+
return not isinstance(exc, Exception)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _first_of(exc: BaseException, ex: type | ExceptionInfo) -> BaseException | None:
|
|
237
|
+
"""The first exception of type *ex* in the tree, or ``None``.
|
|
238
|
+
|
|
239
|
+
The one place the three group assertions ask their question, so they cannot answer it differently.
|
|
240
|
+
`BaseExceptionGroup.subgroup()` answers the same on a stock group, but it is a method a subclass may
|
|
241
|
+
override, which would split the verdicts between whichever assertions called it.
|
|
242
|
+
"""
|
|
243
|
+
return next((node for node in _nodes(exc) if isinstance(node, ex)), None)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _effective_cause(exc: BaseException) -> BaseException | None:
|
|
247
|
+
"""The exception that chained into *exc*: explicit ``__cause__`` (``raise ... from``), else the
|
|
248
|
+
implicit ``__context__`` (a raise during handling), unless the context was suppressed."""
|
|
249
|
+
if exc.__cause__ is not None:
|
|
250
|
+
return exc.__cause__
|
|
251
|
+
if not exc.__suppress_context__:
|
|
252
|
+
return exc.__context__
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _require_synchronous_result(result: object, func: object) -> None:
|
|
257
|
+
"""Refuse a call that handed back something whose outcome comes later: an awaitable or an async generator.
|
|
258
|
+
|
|
259
|
+
Judged as it stood, `does_not_raise()` passed on an `async def` that raises when awaited, and `raises()`
|
|
260
|
+
failed on it. Awaitable means what `await` accepts, a type with `__await__` or a generator-based
|
|
261
|
+
coroutine, not a class registered with the ABC and lacking one. A plain generator is not refused: the
|
|
262
|
+
call does not run its body either, but it can be consumed synchronously, which assertpy always allowed.
|
|
263
|
+
|
|
264
|
+
A coroutine the call created, from a coroutine function, is closed so it does not warn that it was
|
|
265
|
+
never awaited. One handed back from elsewhere, and a future or a task, is left alone: closing or
|
|
266
|
+
cancelling it would be a side effect on somebody else's work.
|
|
267
|
+
"""
|
|
268
|
+
# the type is asked first: `inspect.isawaitable` on a plain value cost 157 ns, this 38
|
|
269
|
+
if not (hasattr(type(result), "__await__") or isinstance(result, (types.GeneratorType, types.AsyncGeneratorType))):
|
|
270
|
+
return
|
|
271
|
+
if inspect.isasyncgen(result):
|
|
272
|
+
remedy = "iterate it with `async for` to assert on what it does"
|
|
273
|
+
elif inspect.isawaitable(result):
|
|
274
|
+
remedy = "await the call yourself to assert on its result, exception or warning"
|
|
275
|
+
created = inspect.iscoroutinefunction(func) or inspect.iscoroutinefunction(type(func).__call__)
|
|
276
|
+
if created and inspect.iscoroutine(result):
|
|
277
|
+
result.close()
|
|
278
|
+
else:
|
|
279
|
+
return
|
|
280
|
+
raise TypeError(
|
|
281
|
+
f"<{_callable_name(func)}> returned {type(result).__name__}, whose outcome when_called_with() cannot see;"
|
|
282
|
+
f" {remedy}, and use eventually() only to poll an async value"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class _InertBuilder:
|
|
287
|
+
"""No-op builder returned after a failed raises/when_called_with in soft mode.
|
|
288
|
+
|
|
289
|
+
Silently absorbs all chained assertions so they don't crash on wrong val type. What it must not
|
|
290
|
+
absorb is a name the typed surface declares as data: every lambda is truthy, so `passed` read as a
|
|
291
|
+
pass and `.value` read as a value that had been checked.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(self, reason: str | None = None) -> None:
|
|
295
|
+
self._reason = reason
|
|
296
|
+
|
|
297
|
+
def _refusal(self, name: str) -> str:
|
|
298
|
+
"""Why the accessor refuses, carrying the failure that made this builder inert.
|
|
299
|
+
|
|
300
|
+
The reason and not only the rule: the ordinary builder's `.value` names the root failure, and a
|
|
301
|
+
refusal without it sends the reader looking for which assertion went wrong.
|
|
302
|
+
"""
|
|
303
|
+
because = f" - {self._reason}" if self._reason else ""
|
|
304
|
+
return (
|
|
305
|
+
f"cannot extract .{name}: the underlying assertion failed under soft or warn mode{because}"
|
|
306
|
+
f" (read .{name} in strict mode, or after the soft-assertions block)"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def value(self) -> object:
|
|
311
|
+
"""The refusal `.value` promises after a failure under soft or warn mode.
|
|
312
|
+
|
|
313
|
+
Absorbed like everything else, it handed back the absorbing lambda, which is truthy and reads as
|
|
314
|
+
a value that passed.
|
|
315
|
+
"""
|
|
316
|
+
raise TypeError(self._refusal("value"))
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def val(self) -> object:
|
|
320
|
+
"""The same refusal, for the name a polling chain hands the value back under.
|
|
321
|
+
|
|
322
|
+
A chain declares `val` where the builder declares `value`, so absorbing it handed a typed
|
|
323
|
+
accessor the absorbing lambda instead of the value it promises.
|
|
324
|
+
"""
|
|
325
|
+
raise TypeError(self._refusal("val"))
|
|
326
|
+
|
|
327
|
+
def check(self) -> _InertVerdict:
|
|
328
|
+
"""A verdict about a chain that already failed, which is what made this builder inert.
|
|
329
|
+
|
|
330
|
+
Absorbed, it answered with this builder, whose `passed` and `message` are the absorbing lambda:
|
|
331
|
+
`passed` is declared `bool` and every lambda is truthy, so a failed chain read as a pass. It
|
|
332
|
+
answers rather than raising, because reading a verdict is the one thing `check()` promises and
|
|
333
|
+
there is a real failure to report.
|
|
334
|
+
"""
|
|
335
|
+
return _InertVerdict(self._reason or "")
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def not_(self) -> _InertBuilder:
|
|
339
|
+
"""This chain again: nothing after its failure runs, so there is nothing to invert either.
|
|
340
|
+
|
|
341
|
+
Absorbed, it was the absorbing lambda, and the assertion named after it an `AttributeError`.
|
|
342
|
+
"""
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
def __getattr__(self, name):
|
|
346
|
+
return lambda *args, **kwargs: self
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class _InertVerdict:
|
|
350
|
+
"""The verdict of a chain that failed before the assertion being asked about ran.
|
|
351
|
+
|
|
352
|
+
Every assertion on it answers with the same outcome: the failure that made the chain inert is the
|
|
353
|
+
one a reader acts on, and the assertion after it never ran to have a verdict of its own.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def __init__(self, reason: str) -> None:
|
|
357
|
+
self._outcome = AssertionOutcome(passed=False, message=reason)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def not_(self) -> _InertVerdict:
|
|
361
|
+
"""The same verdict, since a negated assertion after the failure never ran either."""
|
|
362
|
+
return self
|
|
363
|
+
|
|
364
|
+
def __getattr__(self, name: str) -> Any:
|
|
365
|
+
return lambda *args, **kwargs: self._outcome
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class ExceptionMixin(_MixinBase):
|
|
369
|
+
"""Expected exception mixin."""
|
|
370
|
+
|
|
371
|
+
def raises(self, ex: type | ExceptionInfo) -> Self:
|
|
372
|
+
"""Asserts that val is callable and set the expected exception.
|
|
373
|
+
|
|
374
|
+
Just sets the expected exception, but never calls val, and therefore never fails. You must
|
|
375
|
+
chain to [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with] to invoke ``val()``.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
ex: the expected exception, or a union or a tuple of them any one of which will do
|
|
379
|
+
|
|
380
|
+
Examples:
|
|
381
|
+
Usage:
|
|
382
|
+
|
|
383
|
+
assert_that(some_func).raises(RuntimeError).when_called_with('foo')
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
AssertionBuilder: returns a new instance (with the expected exception) to chain the next assertion
|
|
387
|
+
"""
|
|
388
|
+
if not callable(self.val):
|
|
389
|
+
refuse(self.val, "callable")
|
|
390
|
+
expected = _expected_classes(ex)
|
|
391
|
+
|
|
392
|
+
return self.builder(self.val, self.description, self.kind, expected, self.logger)
|
|
393
|
+
|
|
394
|
+
def when_called_with(self, *some_args: object, **some_kwargs: object) -> Self:
|
|
395
|
+
"""Asserts that val, when invoked with the given args and kwargs, meets the set expectation.
|
|
396
|
+
|
|
397
|
+
Invokes ``val()`` with the given args and kwargs. You must first set an expectation with
|
|
398
|
+
[`raises()`][assertpy.exception.ExceptionMixin.raises] or
|
|
399
|
+
[`does_not_raise()`][assertpy.exception.ExceptionMixin.does_not_raise] (expected exception),
|
|
400
|
+
or with
|
|
401
|
+
[`warns()`][assertpy.warning.WarningMixin.warns] or
|
|
402
|
+
[`does_not_warn()`][assertpy.warning.WarningMixin.does_not_warn] (expected warning).
|
|
403
|
+
|
|
404
|
+
Only what happens before the call returns is judged. A call that hands back an awaitable or an
|
|
405
|
+
async generator is refused with ``TypeError``, since what it does when awaited or iterated comes
|
|
406
|
+
later. A plain generator is judged as returned: its body runs only when it is consumed.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
*some_args: the args to call ``val()``
|
|
410
|
+
**some_kwargs: the kwargs to call ``val()``
|
|
411
|
+
|
|
412
|
+
Examples:
|
|
413
|
+
Usage:
|
|
414
|
+
|
|
415
|
+
def some_func(a):
|
|
416
|
+
raise RuntimeError('some error!')
|
|
417
|
+
|
|
418
|
+
assert_that(some_func).raises(RuntimeError).when_called_with('foo')
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
AssertionBuilder: returns a new instance (now with the captured exception or warning
|
|
422
|
+
message as the val) to chain to the next assertion
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
AssertionError: if val does **not** meet the set expectation
|
|
426
|
+
TypeError: if no expectation set first
|
|
427
|
+
"""
|
|
428
|
+
if self._expected_warning is not None:
|
|
429
|
+
if self._not_expected:
|
|
430
|
+
return self._when_called_with_not_warning(self._expected_warning, *some_args, **some_kwargs)
|
|
431
|
+
return self._when_called_with_warning(self._expected_warning, *some_args, **some_kwargs)
|
|
432
|
+
|
|
433
|
+
if not self.expected:
|
|
434
|
+
raise TypeError("no expectation set; call raises(), warns() or a does_not_* method first")
|
|
435
|
+
|
|
436
|
+
if getattr(self, "_not_expected", False):
|
|
437
|
+
return self._when_called_with_not_expected(*some_args, **some_kwargs)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
result = self.val(*some_args, **some_kwargs)
|
|
441
|
+
except BaseException as e:
|
|
442
|
+
if issubclass(type(e), self.expected):
|
|
443
|
+
captured = self.builder(_safe_str(e), self.description, self.kind, logger=self.logger)
|
|
444
|
+
captured._raised_exception = e
|
|
445
|
+
return captured
|
|
446
|
+
elif _escaped(e):
|
|
447
|
+
raise
|
|
448
|
+
else:
|
|
449
|
+
self.error(
|
|
450
|
+
f"Expected <{_callable_name(self.val)}> to raise <{_type_expression_name(self.expected)}>"
|
|
451
|
+
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
|
|
452
|
+
f" but raised <{type(e).__name__}>.",
|
|
453
|
+
expected=self.expected,
|
|
454
|
+
)
|
|
455
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
456
|
+
|
|
457
|
+
_require_synchronous_result(result, self.val)
|
|
458
|
+
self.error(
|
|
459
|
+
f"Expected <{_callable_name(self.val)}> to raise <{_type_expression_name(self.expected)}>"
|
|
460
|
+
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}).",
|
|
461
|
+
expected=self.expected,
|
|
462
|
+
)
|
|
463
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
464
|
+
|
|
465
|
+
def returned(self) -> Self:
|
|
466
|
+
"""Pivots the chain to the value ``val()`` returned during
|
|
467
|
+
[`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with].
|
|
468
|
+
|
|
469
|
+
Use after a call that completed normally ([`warns()`][assertpy.warning.WarningMixin.warns],
|
|
470
|
+
[`does_not_warn()`][assertpy.warning.WarningMixin.does_not_warn], or
|
|
471
|
+
[`does_not_raise()`][assertpy.exception.ExceptionMixin.does_not_raise]) to assert
|
|
472
|
+
on the return value in the same chain.
|
|
473
|
+
|
|
474
|
+
Examples:
|
|
475
|
+
Usage:
|
|
476
|
+
|
|
477
|
+
assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
|
|
478
|
+
assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
AssertionBuilder: a new instance wrapping the captured return value
|
|
482
|
+
|
|
483
|
+
Raises:
|
|
484
|
+
TypeError: if no return value was captured (the call raised, or
|
|
485
|
+
[`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with]
|
|
486
|
+
was not invoked first)
|
|
487
|
+
"""
|
|
488
|
+
if self._return_value is _UNSET:
|
|
489
|
+
raise TypeError("no return value captured; returned() is only valid after a call that completed normally")
|
|
490
|
+
return self.builder(self._return_value, self.description, self.kind, logger=self.logger)
|
|
491
|
+
|
|
492
|
+
def raised(self) -> Self:
|
|
493
|
+
"""Pivots the chain to the exception object caught by
|
|
494
|
+
[`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with], to assert on its type,
|
|
495
|
+
``args``, or custom attributes - not only its message string.
|
|
496
|
+
|
|
497
|
+
Examples:
|
|
498
|
+
Usage:
|
|
499
|
+
|
|
500
|
+
err = assert_that(load).raises(ConfigError).when_called_with("bad").raised().value
|
|
501
|
+
assert_that(err.code).is_equal_to(42)
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
AssertionBuilder: a new instance wrapping the caught exception object
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
TypeError: if no exception was captured (the call did not raise, or
|
|
508
|
+
[`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with] was not invoked first)
|
|
509
|
+
"""
|
|
510
|
+
exc = self._require_raised("raised")
|
|
511
|
+
return self.builder(exc, self.description, self.kind, logger=self.logger)
|
|
512
|
+
|
|
513
|
+
def caused_by(self, ex: ClassInfo) -> Self:
|
|
514
|
+
"""Asserts the caught exception was chained from a cause of type ``ex`` (``raise ... from``, or an
|
|
515
|
+
exception raised during handling), then pivots the chain to that cause's message.
|
|
516
|
+
|
|
517
|
+
Examples:
|
|
518
|
+
Usage:
|
|
519
|
+
|
|
520
|
+
assert_that(save).raises(ServiceError).when_called_with(row).caused_by(TimeoutError)
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
ex: the expected cause type, or a union or tuple of them as `isinstance` takes
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
AssertionBuilder: a new instance wrapping the cause's message (chain on it, or walk deeper)
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
TypeError: if ``ex`` holds anything `isinstance` cannot take, checked member by member
|
|
530
|
+
"""
|
|
531
|
+
exc = self._require_raised("caused_by")
|
|
532
|
+
cause = _effective_cause(exc)
|
|
533
|
+
_require_class_info(ex, name="exception", probe=cause)
|
|
534
|
+
if cause is None or not isinstance(cause, ex):
|
|
535
|
+
found = "no cause" if cause is None else f"<{type(cause).__name__}>"
|
|
536
|
+
expected_name = _type_expression_name(ex)
|
|
537
|
+
self.error(
|
|
538
|
+
f"Expected <{type(exc).__name__}> to be caused by <{expected_name}>, but the cause was {found}.",
|
|
539
|
+
expected=ex,
|
|
540
|
+
)
|
|
541
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
542
|
+
pivoted = self.builder(_safe_str(cause), self.description, self.kind, logger=self.logger)
|
|
543
|
+
pivoted._raised_exception = cause
|
|
544
|
+
return pivoted
|
|
545
|
+
|
|
546
|
+
def has_root_cause(self, ex: ClassInfo) -> Self:
|
|
547
|
+
"""Asserts the *root* of the caught exception's cause chain is of type ``ex``, then pivots the chain
|
|
548
|
+
to that root cause's message.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
ex: the expected root-cause type, or a union or tuple of them as `isinstance` takes
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
AssertionBuilder: a new instance wrapping the root cause's message
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
TypeError: if ``ex`` holds anything `isinstance` cannot take, checked member by member
|
|
558
|
+
"""
|
|
559
|
+
exc = self._require_raised("has_root_cause")
|
|
560
|
+
root = exc
|
|
561
|
+
seen = {id(root)}
|
|
562
|
+
while (nxt := _effective_cause(root)) is not None and id(nxt) not in seen:
|
|
563
|
+
root = nxt
|
|
564
|
+
seen.add(id(root))
|
|
565
|
+
_require_class_info(ex, name="exception", probe=root)
|
|
566
|
+
if not isinstance(root, ex):
|
|
567
|
+
self.error(
|
|
568
|
+
f"Expected <{type(exc).__name__}> to have root cause <{_type_expression_name(ex)}>,"
|
|
569
|
+
f" but the root cause was <{type(root).__name__}>.",
|
|
570
|
+
expected=ex,
|
|
571
|
+
)
|
|
572
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
573
|
+
pivoted = self.builder(_safe_str(root), self.description, self.kind, logger=self.logger)
|
|
574
|
+
pivoted._raised_exception = root
|
|
575
|
+
return pivoted
|
|
576
|
+
|
|
577
|
+
def contains_error(self, *ex_types: type | ExceptionInfo) -> Self:
|
|
578
|
+
"""Asserts the caught exception is an exception group that contains, recursively, an exception of
|
|
579
|
+
each given type (for [`raises(ExceptionGroup)`][assertpy.exception.ExceptionMixin.raises]).
|
|
580
|
+
|
|
581
|
+
A caught exception that is not a group fails with ``not_`` as well: negation inverts what the group
|
|
582
|
+
holds, never whether there is a group.
|
|
583
|
+
|
|
584
|
+
Examples:
|
|
585
|
+
Usage:
|
|
586
|
+
|
|
587
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().contains_error(ValueError, KeyError)
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
*ex_types: the exception types the group must contain, each of which may be a union or a tuple
|
|
591
|
+
of them that any one class answers
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
AssertionBuilder: this instance, to chain further assertions on the group
|
|
595
|
+
|
|
596
|
+
Raises:
|
|
597
|
+
ValueError: if called with no types at all, which nothing could fail
|
|
598
|
+
TypeError: if given anything but an exception class
|
|
599
|
+
"""
|
|
600
|
+
if len(ex_types) == 0:
|
|
601
|
+
raise ValueError("one or more args must be given")
|
|
602
|
+
wanted = [_expected_classes(ex) for ex in ex_types]
|
|
603
|
+
exc = self._require_group("contains_error")
|
|
604
|
+
if exc is None:
|
|
605
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
606
|
+
for ex in wanted:
|
|
607
|
+
if _first_of(exc, ex) is None:
|
|
608
|
+
self.error(
|
|
609
|
+
f"Expected the raised exception group to contain <{_type_expression_name(ex)}>, but it did not.",
|
|
610
|
+
expected=ex,
|
|
611
|
+
)
|
|
612
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
613
|
+
return self
|
|
614
|
+
|
|
615
|
+
def does_not_contain_error(self, *ex_types: type | ExceptionInfo) -> Self:
|
|
616
|
+
"""Asserts the caught exception group holds none of the given types, at any depth.
|
|
617
|
+
|
|
618
|
+
The none-of counterpart to [`contains_error()`][assertpy.exception.ExceptionMixin.contains_error]
|
|
619
|
+
rather than its negation: that one asks for every type given, this one refuses every type given.
|
|
620
|
+
With several arguments both can fail on the same group, which is what "some but not all" means.
|
|
621
|
+
A caught exception that is not a group fails it with or without ``not_``.
|
|
622
|
+
|
|
623
|
+
Examples:
|
|
624
|
+
Usage:
|
|
625
|
+
|
|
626
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().does_not_contain_error(KeyError)
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
*ex_types: the exception types the group must not contain, each of which may be a union or a
|
|
630
|
+
tuple of them
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
AssertionBuilder: this instance, to chain further assertions on the group
|
|
634
|
+
|
|
635
|
+
Raises:
|
|
636
|
+
ValueError: if called with no types at all, which nothing could fail
|
|
637
|
+
TypeError: if given anything but an exception class
|
|
638
|
+
"""
|
|
639
|
+
if len(ex_types) == 0:
|
|
640
|
+
raise ValueError("one or more args must be given")
|
|
641
|
+
unwanted = [_expected_classes(ex) for ex in ex_types]
|
|
642
|
+
exc = self._require_group("does_not_contain_error")
|
|
643
|
+
if exc is None:
|
|
644
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
645
|
+
for ex in unwanted:
|
|
646
|
+
if _first_of(exc, ex) is not None:
|
|
647
|
+
self.error(
|
|
648
|
+
f"Expected the raised exception group to not contain <{_type_expression_name(ex)}>, but it did."
|
|
649
|
+
)
|
|
650
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
651
|
+
return self
|
|
652
|
+
|
|
653
|
+
def errors(self) -> Self:
|
|
654
|
+
"""Pivots the chain to the list of leaf exceptions in the caught group, nested ones flattened.
|
|
655
|
+
|
|
656
|
+
Flattened rather than one level deep, because that is what
|
|
657
|
+
[`contains_error()`][assertpy.exception.ExceptionMixin.contains_error] already searches: a group
|
|
658
|
+
holding a group is an implementation detail of whoever raised it, and a suite asking "what
|
|
659
|
+
failed" means the leaves. The view this was reached from still holds the group, so
|
|
660
|
+
[`raised()`][assertpy.exception.ExceptionMixin.raised] on *that* answers with the whole tree when
|
|
661
|
+
the shape itself is the point. It is not offered on the leaves, which are a collection.
|
|
662
|
+
|
|
663
|
+
Examples:
|
|
664
|
+
Usage:
|
|
665
|
+
|
|
666
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().is_length(2)
|
|
667
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().extracting(
|
|
668
|
+
"args"
|
|
669
|
+
).contains(("bad id",))
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
AssertionBuilder: a new instance wrapping the leaves as a list, to ask a collection anything
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
TypeError: if no exception was captured
|
|
676
|
+
"""
|
|
677
|
+
exc = self._require_group("errors")
|
|
678
|
+
if exc is None:
|
|
679
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
680
|
+
return self.builder(_leaves(exc), self.description, self.kind, logger=self.logger)
|
|
681
|
+
|
|
682
|
+
def error_of(self, ex: type) -> Self:
|
|
683
|
+
"""Asserts the caught group holds an exception of type ``ex``, then pivots to that one's message.
|
|
684
|
+
|
|
685
|
+
The step [`contains_error()`][assertpy.exception.ExceptionMixin.contains_error] cannot take: after
|
|
686
|
+
it the chain still holds the *group's* message, so asking what one failure said meant reaching into
|
|
687
|
+
the tree by hand.
|
|
688
|
+
|
|
689
|
+
Both search the same exceptions, groups included, so whatever one finds the other pivots to. Asking
|
|
690
|
+
for a group type therefore answers with that group, and for the outermost one that is the message
|
|
691
|
+
the chain already held. A caught exception that is not a group fails it with or without ``not_``.
|
|
692
|
+
|
|
693
|
+
Examples:
|
|
694
|
+
Usage:
|
|
695
|
+
|
|
696
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().error_of(ValueError).contains("bad id")
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
ex: the type to pivot to, the first one found, the group itself before its members
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
AssertionBuilder: a new instance wrapping that exception's message (chain on it, or
|
|
703
|
+
`raised()` to reach the object itself)
|
|
704
|
+
|
|
705
|
+
Raises:
|
|
706
|
+
TypeError: if given anything but an exception class
|
|
707
|
+
"""
|
|
708
|
+
_require_exception_type(ex)
|
|
709
|
+
exc = self._require_group("error_of")
|
|
710
|
+
if exc is None:
|
|
711
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
712
|
+
found = _first_of(exc, ex)
|
|
713
|
+
if found is None:
|
|
714
|
+
self.error(f"Expected the raised exception group to contain <{ex.__name__}>, but it did not.", expected=ex)
|
|
715
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
716
|
+
pivoted = self.builder(_safe_str(found), self.description, self.kind, logger=self.logger)
|
|
717
|
+
pivoted._raised_exception = found
|
|
718
|
+
return pivoted
|
|
719
|
+
|
|
720
|
+
def matches_error_tree(self, *expected: type | list[Any]) -> Self:
|
|
721
|
+
"""Asserts the caught group holds exactly these exceptions, in exactly this nesting.
|
|
722
|
+
|
|
723
|
+
The question [`contains_error()`][assertpy.exception.ExceptionMixin.contains_error] does not ask.
|
|
724
|
+
That one searches the whole tree for a type and says nothing about how many others there are or
|
|
725
|
+
how deep it sat, so a group that grew an extra failure, or one whose members moved into a
|
|
726
|
+
subgroup, still passes it. This one reads the shape.
|
|
727
|
+
|
|
728
|
+
A `list` is a subgroup and a type is one exception, so `(ValueError, [KeyError])` means a group of
|
|
729
|
+
two members whose second is a subgroup holding one `KeyError`. A type matches by `isinstance`,
|
|
730
|
+
the way the rest of the family does, so `Exception` matches a subgroup node as readily as a leaf,
|
|
731
|
+
and stops there: only a nested list looks inside one, and what a matched subgroup holds is unsaid.
|
|
732
|
+
|
|
733
|
+
Order is not part of the shape. Whoever raised the group usually did not choose it: an
|
|
734
|
+
`asyncio.TaskGroup` reports its failures in the order the tasks finished. A caught exception that
|
|
735
|
+
is not a group fails it with or without ``not_``.
|
|
736
|
+
|
|
737
|
+
Examples:
|
|
738
|
+
Usage:
|
|
739
|
+
|
|
740
|
+
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().matches_error_tree(
|
|
741
|
+
ValueError, [KeyError, KeyError]
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
*expected: one entry per member of the group, a type for an exception and a list for a
|
|
746
|
+
subgroup
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
AssertionBuilder: this instance, to chain further assertions on the group
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
ValueError: if called with no entries at all, as the rest of the family does
|
|
753
|
+
TypeError: if an entry is neither an exception class nor a list, or is an empty list, which
|
|
754
|
+
is a group with no members and cannot be raised
|
|
755
|
+
"""
|
|
756
|
+
if not expected:
|
|
757
|
+
raise ValueError("one or more args must be given")
|
|
758
|
+
spec = [_shaped(entry, f"[{index}]") for index, entry in enumerate(expected)]
|
|
759
|
+
exc = self._require_group("matches_error_tree")
|
|
760
|
+
if exc is None:
|
|
761
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
762
|
+
if not _matches_shape(spec, exc.exceptions):
|
|
763
|
+
names = _naming(spec, exc)
|
|
764
|
+
self.error(
|
|
765
|
+
f"Expected the raised exception group to match <{_spec_shape(spec, names)}>,"
|
|
766
|
+
f" but it was <{_shape_of(exc, names)}>.",
|
|
767
|
+
expected=spec,
|
|
768
|
+
)
|
|
769
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
770
|
+
return self
|
|
771
|
+
|
|
772
|
+
def _require_group(self, method: str) -> Any:
|
|
773
|
+
"""The caught exception as a group, or ``None`` after reporting that it was not one.
|
|
774
|
+
|
|
775
|
+
Reported as a prerequisite, so `not_` fails on it too: the group family asks about what a group
|
|
776
|
+
holds, and a plain exception answers neither that question nor its negation.
|
|
777
|
+
|
|
778
|
+
Returns ``Any`` rather than the group type: on the 3.10 floor the compat name falls back to an
|
|
779
|
+
empty tuple, which is a value and not a type, so an annotation naming it is rejected outright.
|
|
780
|
+
Callers walk `.exceptions` on the result, which is why the check has to happen before they do.
|
|
781
|
+
"""
|
|
782
|
+
exc = self._require_raised(method)
|
|
783
|
+
if not isinstance(exc, BaseExceptionGroup):
|
|
784
|
+
self._unmet(f"Expected the raised <{type(exc).__name__}> to be an exception group, but it was not.")
|
|
785
|
+
return None
|
|
786
|
+
return exc
|
|
787
|
+
|
|
788
|
+
def _require_raised(self, method: str) -> BaseException:
|
|
789
|
+
if self._raised_exception is _UNSET:
|
|
790
|
+
raise TypeError(
|
|
791
|
+
f"no exception captured; {method}() is only valid after raises()...when_called_with() caught one"
|
|
792
|
+
)
|
|
793
|
+
return cast("BaseException", self._raised_exception)
|
|
794
|
+
|
|
795
|
+
def _when_called_with_not_expected(self, *some_args, **some_kwargs) -> Self:
|
|
796
|
+
# when_called_with() rejects an unset expectation
|
|
797
|
+
expected = cast("type[BaseException] | ExceptionInfo", self.expected)
|
|
798
|
+
try:
|
|
799
|
+
result = self.val(*some_args, **some_kwargs)
|
|
800
|
+
except BaseException as e:
|
|
801
|
+
if issubclass(type(e), expected):
|
|
802
|
+
self.error(
|
|
803
|
+
f"Expected <{_callable_name(self.val)}> to not raise <{_type_expression_name(expected)}>"
|
|
804
|
+
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
|
|
805
|
+
f" but did raise <{type(e).__name__}>."
|
|
806
|
+
)
|
|
807
|
+
return cast("Self", _InertBuilder(self._value_taint_reason))
|
|
808
|
+
if _escaped(e):
|
|
809
|
+
raise
|
|
810
|
+
return self
|
|
811
|
+
_require_synchronous_result(result, self.val)
|
|
812
|
+
self._return_value = result
|
|
813
|
+
return self
|
|
814
|
+
|
|
815
|
+
def does_not_raise(self, ex: type | ExceptionInfo) -> Self:
|
|
816
|
+
"""Asserts that val is callable and sets the not-expected exception.
|
|
817
|
+
|
|
818
|
+
Just sets the not-expected exception, but never calls val. You must
|
|
819
|
+
chain to [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with] to invoke ``val()``.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
ex: the exception that should **not** be raised, or a union or a tuple of them
|
|
823
|
+
|
|
824
|
+
Examples:
|
|
825
|
+
Usage:
|
|
826
|
+
|
|
827
|
+
assert_that(some_func).does_not_raise(RuntimeError).when_called_with('foo')
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
AssertionBuilder: returns a new instance to chain to the next assertion
|
|
831
|
+
"""
|
|
832
|
+
if not callable(self.val):
|
|
833
|
+
refuse(self.val, "callable")
|
|
834
|
+
unwanted = _expected_classes(ex)
|
|
835
|
+
|
|
836
|
+
new_builder = self.builder(self.val, self.description, self.kind, unwanted, self.logger)
|
|
837
|
+
new_builder._not_expected = True
|
|
838
|
+
return new_builder
|