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
assertpy/assertpy.py ADDED
@@ -0,0 +1,2640 @@
1
+ """Assertion library for python unit testing with a fluent API"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import contextvars
7
+ import functools
8
+ import inspect
9
+ import logging
10
+ import os
11
+ import sys
12
+ import threading
13
+ import types
14
+ from dataclasses import replace
15
+ from typing import (
16
+ TYPE_CHECKING,
17
+ Any,
18
+ Final,
19
+ Generic,
20
+ Literal,
21
+ NoReturn,
22
+ Protocol,
23
+ SupportsIndex,
24
+ TypeVar,
25
+ cast,
26
+ overload,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ import datetime
31
+ import pathlib
32
+ from collections.abc import Callable, Collection, Mapping
33
+
34
+ from typing_extensions import TypeIs
35
+
36
+ from ._engine._builder_check_typing import _CheckAnyValue
37
+ from ._engine._capable_typing import _CapableAssertion
38
+ from ._engine._compat import Self
39
+ from ._engine._typing import (
40
+ _ArrayAssertion,
41
+ _ArrayT_co,
42
+ _BoolAssertion,
43
+ _BytesAssertion,
44
+ _CallableAssertion,
45
+ _CapableT,
46
+ _ComplexAssertion,
47
+ _DateAssertion,
48
+ _DateTimeAssertion,
49
+ _DictAssertion,
50
+ _FrameAssertion,
51
+ _FrameT_co,
52
+ _IterableAssertion,
53
+ _NumericAssertion,
54
+ _ObjectAssertion,
55
+ _PathAssertion,
56
+ _StringAssertion,
57
+ )
58
+ from ._matcher_impls import ClassInfo, ExceptionInfo, RetriedInfo
59
+ from ._passes import PassLogger
60
+ from .errors import PollTrace
61
+ from .matchers import Matcher
62
+
63
+ from . import _hints, _passes
64
+ from . import errors as _errors
65
+ from ._engine._compat import _LoggerAdapter
66
+ from ._engine._contract import exactness_failure, kept_apart, put_as_sent, put_back, runs_own_code, sent_record
67
+ from ._engine._introspection import ASSERTPY_FILES, WarningLogger, is_same_implementation
68
+ from ._engine._operations import (
69
+ ALSO_ASSERTS,
70
+ CONFIGURES,
71
+ DESCRIBES,
72
+ HANDS_THE_SUBJECT_BACK,
73
+ NOT_AN_OPERATION,
74
+ POLLS,
75
+ TRANSFORMS,
76
+ WHAT_IT_DOES,
77
+ WITHOUT_A_VERDICT,
78
+ )
79
+ from ._engine._path import _ROOT, _Path
80
+ from ._engine._require import argument, refuse, verdict
81
+ from .async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder, _normalize_ignoring
82
+ from .base import BaseMixin
83
+ from .bytes_mixin import BytesMixin
84
+ from .collection import CollectionMixin
85
+ from .contains import ContainsMixin
86
+ from .dataframe import DataFrameMixin
87
+ from .date import DateMixin
88
+ from .dict import DictMixin
89
+ from .dynamic import DynamicMixin, _one_operand
90
+ from .errors import (
91
+ AssertionFailure,
92
+ DiffEntry,
93
+ DiffResult,
94
+ Step,
95
+ _capped,
96
+ _capped_repr,
97
+ _diff_sides,
98
+ _render_diff,
99
+ _safe_str,
100
+ _truncated,
101
+ _windowed,
102
+ )
103
+ from .exception import _UNSET, ExceptionMixin
104
+ from .extracting import ExtractingMixin
105
+ from .file import FileMixin
106
+ from .helpers import HelpersMixin
107
+ from .http_mixin import HttpMixin, response_note, response_of
108
+ from .json_mixin import JsonMixin
109
+ from .numeric import NumericMixin
110
+ from .outcome import MISSING, AssertionOutcome, Requirement
111
+ from .snapshot import SnapshotMixin
112
+ from .string import StringMixin
113
+ from .warning import WarningMixin
114
+
115
+ __version__ = "2.35.0"
116
+
117
+ # the tracked value type of the generic AssertionBuilder fallback (_U appears only in narrowing stubs)
118
+ _T = TypeVar("_T")
119
+ # the assertion type `not_` was reached from, so an inverted step hands the same narrowed type back
120
+ _E_co = TypeVar("_E_co", covariant=True) # the element a pivot hands back
121
+ _S = TypeVar("_S")
122
+ if TYPE_CHECKING:
123
+ _U = TypeVar("_U")
124
+ _U2 = TypeVar("_U2")
125
+ _U3 = TypeVar("_U3")
126
+ _E = TypeVar("_E") # element type of a collection, so first()/element()/... narrow to it
127
+ _R = TypeVar("_R") # result element type after a mapping pivot
128
+ _K = TypeVar("_K") # dict key type, so .value keeps dict[K, V]
129
+ _V = TypeVar("_V") # dict value type
130
+ _P = TypeVar("_P") # what a probe hands back, so a polling chain asserts on that and not on `object`
131
+
132
+ class _Extension(Protocol):
133
+ """A function attached as an extension, which the registry keys on its own name.
134
+
135
+ The name is part of the contract rather than incidental: `add_extension` refuses a value whose
136
+ `__name__` is not an identifier, and `remove_extension` reads it to find what to take off.
137
+ """
138
+
139
+ __name__: str
140
+
141
+ def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
142
+
143
+ class _ElementSource(Protocol[_E_co]):
144
+ """Anything whose value is a collection of `_E_co`, however it was annotated.
145
+
146
+ Structural because `AssertionBuilder` is invariant in `_T`: a builder over a `Sequence[int]`
147
+ is not a builder over a `Collection[int]`, so the nominal self type bound for a mapping and
148
+ missed every other container.
149
+ """
150
+
151
+ @property
152
+ def value(self) -> Collection[_E_co]: ...
153
+
154
+
155
+ __tracebackhide__ = True # clean tracebacks via py.test integration
156
+
157
+
158
+ _run_as_written: contextvars.ContextVar[bool] = contextvars.ContextVar("assertpy_run_as_written", default=False)
159
+ """Whether the assertion being located was written inside a callable we are running.
160
+
161
+ `assert_all()` calls what it was given, so the outermost handover is its own call site and every failure
162
+ it collected was tagged with that one line. A callable passed to it holds the assertions as written, so
163
+ theirs is the line to report.
164
+ """
165
+
166
+
167
+ def _caller_location() -> tuple[str, int] | None:
168
+ """The ``(filename, lineno)`` of the user frame that called into assertpy, skipping internal frames.
169
+
170
+ Used to locate a warn-mode warning and each collected soft-assertion failure.
171
+
172
+ The answer is the frame just outside the *outermost* assertpy frame, which is why the walk cannot
173
+ stop at the first user frame it meets going inwards: a predicate passed to `satisfies()` or `each()`
174
+ runs inside our own call, and the line worth reporting is the assertion in the test rather than the
175
+ lambda we invoked. Walking outwards and keeping the last handover does the same in one pass,
176
+ without building a list of the whole stack.
177
+
178
+ ``None`` where no such handover exists, so a caller that cannot be located logs without the prefix
179
+ instead of crashing on unpacking.
180
+ """
181
+ frame: types.FrameType | None = sys._getframe(1) # CPython accessor; the inspect equivalent is 10x slower here
182
+ innermost = _run_as_written.get()
183
+ location: tuple[str, int] | None = None
184
+ inner_is_internal = False
185
+ while frame:
186
+ filename = frame.f_code.co_filename
187
+ is_internal = filename in ASSERTPY_FILES
188
+ if inner_is_internal and not is_internal:
189
+ location = (filename, frame.f_lineno)
190
+ if innermost:
191
+ return location
192
+ inner_is_internal = is_internal
193
+ frame = frame.f_back
194
+ return location
195
+
196
+
197
+ def _what_was_asked(builder: object, given: Requirement | None = None) -> Requirement | None:
198
+ """What the failure being composed was asked for.
199
+
200
+ A live extension call on this builder first, because it owns what runs under it the way
201
+ `is_positive()` owns the `is_greater_than(0)` it asks. That has to beat *given*, which is what the
202
+ negated proxy and a dynamic ``has_`` assertion say about themselves: an extension delegating to
203
+ either of those was reporting the delegate.
204
+
205
+ The extension answers only for the builder it was called on. An assertion it makes about some
206
+ other value is that assertion's own, exactly as one inside a `satisfies()` predicate is.
207
+ """
208
+ asked = _asked_of.get()
209
+ if asked is not None and asked.answers_for(builder):
210
+ return _extension_requirement(asked)
211
+ return given if given is not None else _requirement_of()
212
+
213
+
214
+ def _requirement_of() -> Requirement | None:
215
+ """Which operation the failure being composed was asked for, read off the stack.
216
+
217
+ The name and the arguments live at the assertion's entry and not at its failure: of the 164 places
218
+ that reach `error()`, 144 are the public method itself and 20 are private helpers it calls. So the
219
+ answer is read here instead of passed down from each of them, which costs a built-in assertion
220
+ nothing at all on the passing path and one microsecond on a failure that is already building a diff
221
+ and a hint. An extension is the exception and pays a wrapper on every call, because a frame cannot
222
+ be tied back to a registration: `_ExtensionCall` says why.
223
+
224
+ The *outermost* assertion still inside the package, for the same reason `_caller_location()` walks
225
+ outwards: `is_positive()` asks `is_greater_than(0)` and `contains_key()` asks `contains()`, so the
226
+ innermost frame answers the delegate rather than the assertion in the test. A predicate passed to
227
+ `satisfies()` is not our code, so the walk leaves the package there and an assertion inside the
228
+ predicate still answers itself. An extension is not read off the stack at all: it says what it is
229
+ before its body runs, because a frame cannot be tied back to a registration. Two instances of one
230
+ callable class, and two functions from one decorator factory, share a code object.
231
+
232
+ ``None`` where no operation was asked: `fail()`, and a bare `error()` with the caller's own message.
233
+ """
234
+ frame: types.FrameType | None = sys._getframe(3) # skips this, `_what_was_asked` and `_compose`
235
+ found: types.FrameType | None = None
236
+ while frame is not None and frame.f_code.co_filename in ASSERTPY_FILES:
237
+ if frame.f_code.co_name in _ASSERTS:
238
+ found = frame
239
+ frame = frame.f_back
240
+ return None if found is None else Requirement(found.f_code.co_name, _bound_parameters(found))
241
+
242
+
243
+ def _bound_parameters(frame: types.FrameType, receivers: int = 1) -> dict[str, object]:
244
+ """Every declared parameter of *frame* with the value it was bound to, minus the receiver.
245
+
246
+ A frame cannot say whether an argument was omitted, passed positionally or passed by keyword, so
247
+ what is read here is what the assertion ran with. `Requirement.parameters` says why that is the
248
+ more useful of the two.
249
+ """
250
+ code = frame.f_code
251
+ local = frame.f_locals
252
+ names = code.co_varnames
253
+ taken = code.co_argcount + code.co_kwonlyargcount
254
+ found: dict[str, object] = {name: local[name] for name in names[receivers:taken] if name in local}
255
+ if code.co_flags & inspect.CO_VARARGS:
256
+ found[names[taken]] = tuple(local.get(names[taken], ()))
257
+ taken += 1
258
+ if code.co_flags & inspect.CO_VARKEYWORDS:
259
+ found[names[taken]] = dict(local.get(names[taken], {}))
260
+ return found
261
+
262
+
263
+ @functools.cache
264
+ def _method_signature(own: Callable[..., object]) -> inspect.Signature:
265
+ """The signature of an assertion as its caller writes it, without the receiver.
266
+
267
+ Cached on the function rather than on the bound method, which is a new object per attribute access.
268
+ `inspect.signature` costs 4.8 microseconds, three times what composing a negated failure costs
269
+ without it, and the set of assertion functions is fixed at import.
270
+ """
271
+ found = inspect.signature(own)
272
+ return found.replace(parameters=list(found.parameters.values())[1:])
273
+
274
+
275
+ def _signature_of(attr: Callable[..., object]) -> inspect.Signature | None:
276
+ """As a caller writes it, for anything callable, or ``None`` where it cannot be read.
277
+
278
+ Only a bound method is cached: a dynamic ``has_`` assertion and a registered extension are closures
279
+ built per access, and holding them in a cache would grow it for the life of the process.
280
+ """
281
+ own = getattr(attr, "__func__", None)
282
+ try:
283
+ # only a function is cached: a callable object need not be hashable, and the key would refuse it
284
+ return _method_signature(own) if isinstance(own, types.FunctionType) else inspect.signature(attr)
285
+ except (TypeError, ValueError):
286
+ return None
287
+
288
+
289
+ class _SoftBlock:
290
+ """The failures one `soft_assertions()` block is collecting, and whether it is still collecting.
291
+
292
+ ``active`` exists because a context variable is copied *by value* into a new task: a child created
293
+ inside the block inherits the depth and the list, and the parent's exit is invisible to it. The
294
+ list is shared, so the flag rides along with it and the child can see that nobody is listening any
295
+ more. Without it, an assertion in a task that outlived the block appended to an orphaned list and
296
+ the test passed having checked nothing.
297
+ """
298
+
299
+ __slots__ = ("active", "compared", "failures")
300
+
301
+ def __init__(self) -> None:
302
+ self.failures: list[AssertionOutcome] = []
303
+ # by the identity of the record, which `failures` keeps alive, the pair its equality compared
304
+ self.compared: dict[int, tuple[object, object]] = {}
305
+ self.active = True
306
+
307
+ def taken(self) -> tuple[list[AssertionOutcome], tuple[tuple[object, object] | None, ...]]:
308
+ """What was collected, with the pair beside each, the block left empty for whatever comes next."""
309
+ errs, compared = self.failures, self.compared
310
+ self.failures, self.compared = [], {}
311
+ return errs, tuple(compared.get(id(outcome)) for outcome in errs)
312
+
313
+
314
+ def _collecting() -> _SoftBlock | None:
315
+ """The block currently collecting in this context, or ``None`` when assertions should raise."""
316
+ if not _soft_ctx.get():
317
+ return None
318
+ block = _soft_err.get(None)
319
+ return block if block is not None and block.active else None
320
+
321
+
322
+ _soft_ctx: contextvars.ContextVar[int] = contextvars.ContextVar("assertpy_soft_ctx", default=0)
323
+ # nothing about a soft failure is flattened until the aggregate message is rendered
324
+ _soft_err: contextvars.ContextVar[_SoftBlock | None] = contextvars.ContextVar("assertpy_soft_err", default=None)
325
+ _soft_group: contextvars.ContextVar[str | None] = contextvars.ContextVar("assertpy_soft_group", default=None)
326
+
327
+
328
+ class _Group:
329
+ """The context `group()` hands back.
330
+
331
+ A class rather than a `@contextlib.contextmanager` generator, and the same goes for the soft block
332
+ itself. A generator-based manager puts `contextlib.__exit__` on the stack, so a failure raised on
333
+ the way out is reported against `contextlib.py` instead of the `with` line in the test. The old
334
+ cure was setting `__tracebackhide__` on the `contextlib` module, which fixed our two managers by
335
+ changing how every third-party context manager in the process is reported.
336
+ """
337
+
338
+ __slots__ = ("_label", "_token")
339
+
340
+ def __init__(self, label: str) -> None:
341
+ self._label = label
342
+
343
+ def __enter__(self) -> None:
344
+ self._token = _soft_group.set(self._label)
345
+
346
+ def __exit__(self, *_exc: object) -> None:
347
+ _soft_group.reset(self._token)
348
+
349
+
350
+ class SoftAssertionCollector:
351
+ """Collector returned by [`soft_assertions()`][assertpy.assertpy.soft_assertions] for grouping
352
+ errors hierarchically."""
353
+
354
+ def group(self, label: str) -> _Group:
355
+ """Group subsequent assertion failures under *label*.
356
+
357
+ Examples:
358
+ Usage:
359
+
360
+ with soft_assertions() as sa:
361
+ with sa.group("Headers"):
362
+ assert_that(headers).contains_key("Content-Type")
363
+ with sa.group("Body"):
364
+ assert_that(body["status"]).is_equal_to("ok")
365
+ """
366
+ return _Group(label)
367
+
368
+
369
+ def _located(location: tuple[str, int] | None, msg: str) -> str:
370
+ """Append ``[file:line]`` so each collected soft failure can be jumped to, matching warn-mode."""
371
+ if location is None: # pragma: no cover - only None when a user file shadows assertpy (see _caller_location)
372
+ return msg
373
+ return f"{msg} [{os.path.basename(location[0])}:{location[1]}]"
374
+
375
+
376
+ # half the block form's window, because the compact form puts both sides on one line
377
+ _SCANNABLE_WIDTH = 60
378
+
379
+
380
+ def _indented_diff(diff: object, indent: str) -> list[str]:
381
+ """Render a collected failure's diff under its entry, or nothing when it carries none."""
382
+ # one line per differing path: a soft run exists to show many of them at once
383
+ lines = []
384
+ entries = getattr(diff, "entries", None) or []
385
+ # a mapping whose only differing key is spelled "." has a step, and used to be dropped here
386
+ only = entries[0] if len(entries) == 1 else None
387
+ if only is not None and not only.steps and only.path == ".":
388
+ return lines
389
+ if only is not None and only.steps == (Step("line", 1),):
390
+ # past this width the change cannot be found by eye, so a window around it is drawn instead
391
+ actual_text, expected_text = _safe_str(only.actual), _safe_str(only.expected)
392
+ if len(actual_text) <= _SCANNABLE_WIDTH and len(expected_text) <= _SCANNABLE_WIDTH:
393
+ return lines
394
+ near_actual, near_expected = _windowed(actual_text, expected_text, _SCANNABLE_WIDTH)
395
+ return [f"{indent}{only.path}: {near_actual} != {near_expected}"]
396
+ shown = entries[:5] # bound once: a slice and a separate threshold would drift apart
397
+ for entry in shown:
398
+ if entry.absent == "expected": # an extra item, which has no counterpart to contrast with
399
+ lines.append(f"{indent}{entry.path}: {_capped_repr(entry.actual)}")
400
+ elif entry.absent == "actual": # a missing one
401
+ lines.append(f"{indent}{entry.path}: {_capped_repr(entry.expected)}")
402
+ else:
403
+ actual_side, expected_side = _diff_sides(entry.actual, entry.expected, 4000)
404
+ lines.append(f"{indent}{entry.path}: {actual_side} != {expected_side}")
405
+ if len(entries) > len(shown):
406
+ lines.append(f"{indent}... and {len(entries) - len(shown)} more")
407
+ return lines
408
+
409
+
410
+ class _Room:
411
+ """What is left of the room for the blocks of collected failures: twenty failures drawn and 60 000 characters.
412
+
413
+ One per message of a soft block where that message is the only place its diffs are read, and one per report
414
+ under pytest, shared there by every block that failed in it. A failure drawn and then too wide for what is left
415
+ still takes one of the twenty: 200 wide failures each drawn and refused cost a report 0.49 s, against 0.05 s.
416
+ """
417
+
418
+ def __init__(self) -> None:
419
+ self.failures = 20
420
+ self.characters = 60_000
421
+
422
+
423
+ def _numbered(numbers: list[int]) -> str:
424
+ """``[3, 21, 22, 23]`` as ``failures 3, 21 to 23``, and ``[3]`` as ``failure 3``: failures named by number."""
425
+ runs: list[list[int]] = []
426
+ for number in numbers:
427
+ if runs and number == runs[-1][-1] + 1:
428
+ runs[-1].append(number)
429
+ else:
430
+ runs.append([number])
431
+ spans = ", ".join(str(run[0]) if len(run) == 1 else f"{run[0]} to {run[-1]}" for run in runs)
432
+ return f"failure {spans}" if len(numbers) == 1 else f"failures {spans}"
433
+
434
+
435
+ def _block_of(diff: object, indent: str, room: _Room) -> list[str] | None:
436
+ """*diff* in block form under an entry, or ``None`` where it has none, the room is spent or it does not fit.
437
+
438
+ A diff that cannot be drawn keeps the compact rows the summary has always printed, and a line saying why.
439
+ """
440
+ if diff is None or room.failures == 0:
441
+ return None
442
+ try:
443
+ rendered = _render_diff(diff)
444
+ except Exception as e:
445
+ room.failures -= 1
446
+ return [*_indented_diff(diff, indent), f"{indent}(no block: drawing it raised {type(e).__name__})"]
447
+ if not rendered:
448
+ return None
449
+ room.failures -= 1
450
+ block = [f"{indent}{line}" for line in rendered.splitlines()]
451
+ size = sum(len(line) + 1 for line in block)
452
+ if size > room.characters:
453
+ return None
454
+ room.characters -= size
455
+ return block
456
+
457
+
458
+ def _format_soft_errors(errs: list[AssertionOutcome], *, blocks: bool = False, kept: bool = False) -> str:
459
+ """The numbered summary a soft block raises, each entry located, with each failure's diff under it.
460
+
461
+ The diff goes in compact rows, one per differing path, where the pytest plugin hangs each failure's block on
462
+ the report anyway. With *blocks*, for a message that is the only place a reader finds them, each diff goes in
463
+ the block form a raised failure prints while a `_Room` lasts. A block that does not fit keeps its compact rows
464
+ and takes one of the twenty but none of the characters, so a smaller block after it still fits. The last line
465
+ names those failures, pointing at `failures` where *kept* says the summary is the message that carries them.
466
+ """
467
+ has_groups = any(outcome.group is not None for outcome in errs)
468
+ lines = ["soft assertion failures:"]
469
+ room = _Room() if blocks else None
470
+ compact: list[int] = []
471
+ current_group: str | None = None
472
+ for counter, outcome in enumerate(errs, 1):
473
+ indent = ""
474
+ # one failure that cannot be drawn, its group's label included, costs its own entry and not the summary
475
+ try:
476
+ group = outcome.group
477
+ indent = (" " if group is not None else " ") if has_groups else ""
478
+ if has_groups and not _same_label(group, current_group):
479
+ header = None if group is None else f" [{group}]"
480
+ current_group = group
481
+ if header is not None:
482
+ lines.append(header)
483
+ entry, squeezed = _entry_of(counter, outcome, indent, room)
484
+ except Exception as e:
485
+ entry, squeezed = [f"{indent}{counter}. (this failure could not be drawn: {type(e).__name__})"], False
486
+ lines.extend(entry)
487
+ if squeezed:
488
+ compact.append(counter)
489
+ if compact:
490
+ pointer = " The diff of each is on `failures`." if kept else ""
491
+ lines.append(f"Not drawn as a block for lack of room: {_numbered(compact)}.{pointer}")
492
+ return "\n".join(lines)
493
+
494
+
495
+ def _same_label(label: object, other: object) -> bool:
496
+ """Whether two group labels are one, a label whose ``==`` raises reading as another."""
497
+ if label is other:
498
+ return True
499
+ try:
500
+ return bool(label == other)
501
+ except Exception: # a label is anyone's object, and the entries after it still have to be drawn
502
+ return False
503
+
504
+
505
+ def _entry_of(counter: int, outcome: AssertionOutcome, indent: str, room: _Room | None) -> tuple[list[str], bool]:
506
+ """One entry of the summary, and whether its diff went in compact rows for lack of room."""
507
+ # written flat, the location landed on the last line and the next entry read as a new section
508
+ headline, *continuation = outcome.message.splitlines() or [""]
509
+ lines = [f"{indent}{counter}. {_located(outcome.location, headline)}"]
510
+ lines.extend(f"{indent} {line}" for line in continuation)
511
+ block = None if room is None else _block_of(outcome.diff, indent + " ", room)
512
+ if block is not None:
513
+ return [*lines, *block], False
514
+ lines.extend(_indented_diff(outcome.diff, indent + " "))
515
+ return lines, room is not None and bool(getattr(outcome.diff, "entries", None))
516
+
517
+
518
+ def _drawn(errs: list[AssertionOutcome], *, blocks: bool, kept: bool = False) -> str:
519
+ """`_format_soft_errors`, or a line saying how many were collected where drawing them raised.
520
+
521
+ The summary is a diagnostic: it must not take the place of the failure it describes, nor of an exception of the
522
+ caller's that it is a note on.
523
+ """
524
+ try:
525
+ return _format_soft_errors(errs, blocks=blocks, kept=kept)
526
+ except Exception as e:
527
+ return f"soft assertion failures: {len(errs)} collected, and drawing them raised {type(e).__name__}"
528
+
529
+
530
+ def _carry(
531
+ exc: BaseException, collected: tuple[AssertionOutcome, ...], pairs: tuple[tuple[object, object] | None, ...]
532
+ ) -> None:
533
+ """Leave what a soft block collected on the exception that escaped it, where the pytest plugin reads it.
534
+
535
+ Through the instance's own dictionary and past any ``__setattr__`` of its own, best-effort as the note is: the
536
+ exception the caller raised is the one that has to come out of the block, whatever this manages. Added to what
537
+ an earlier block left, as its note is added to the notes, since one exception can escape two blocks in turn.
538
+ """
539
+ # an instance without a dictionary keeps the note, which carries the text
540
+ with contextlib.suppress(Exception):
541
+ vars(exc).setdefault("_assertpy_collected", []).append((collected, pairs))
542
+
543
+
544
+ def _attach_soft_note(exc: BaseException, rendered: str) -> None:
545
+ """Put the collected failures on *exc* without ever becoming the reason the test failed.
546
+
547
+ This is a diagnostic hung on somebody else's exception, so every step of it is best-effort. Three
548
+ things can go wrong and each of them used to matter more than the note:
549
+
550
+ * `__notes__` may already be something other than a list. CPython's own `add_note` refuses with
551
+ `TypeError: Cannot add note: __notes__ is not a list`, and that error replaced the exception the
552
+ user raised, leaving theirs in `__context__`. Exactly backwards for a mechanism whose whole job
553
+ is not losing information;
554
+ * a user exception type can override `add_note` with something that raises, or with something that
555
+ is not callable at all. `BaseException.add_note` is called unbound to step around the override;
556
+ * the attribute may not be writable, on a type with `__slots__` or a frozen instance.
557
+
558
+ `add_note` also arrived in 3.11 and this package supports 3.10, which the first branch covers on the
559
+ way past: there the unbound lookup raises `AttributeError` and the assignment takes over.
560
+ """
561
+ try:
562
+ BaseException.add_note(exc, rendered) # ty: ignore[unresolved-attribute] # absent on 3.10
563
+ return
564
+ except Exception: # every failure here falls through to the assignment below
565
+ pass
566
+ try:
567
+ existing = getattr(exc, "__notes__", ())
568
+ notes = [*existing, rendered] if isinstance(existing, (list, tuple)) else [rendered]
569
+ exc.__notes__ = notes # ty: ignore[unresolved-attribute] # 3.10 lacks it until set
570
+ except Exception: # a note is worth nothing next to the exception it annotates
571
+ pass
572
+
573
+
574
+ class _SoftAssertions:
575
+ """The context `soft_assertions()` hands back. See `_Group` for why this is a class."""
576
+
577
+ __slots__ = ()
578
+
579
+ def __enter__(self) -> SoftAssertionCollector:
580
+ ctx = _soft_ctx.get()
581
+ block = _soft_err.get(None)
582
+ if ctx == 0 or block is None or not block.active:
583
+ # a task outliving the block it inherited holds a closed one: collecting into it reaches nobody
584
+ _soft_err.set(_SoftBlock())
585
+ ctx = 0
586
+ _soft_ctx.set(ctx + 1)
587
+ return SoftAssertionCollector()
588
+
589
+ def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, *_rest: object) -> None:
590
+ _soft_ctx.set(_soft_ctx.get() - 1)
591
+ if exc_type is not None:
592
+ # the escaping error still wins, and what was collected before it travels as a note
593
+ if exc is not None and (block := _soft_err.get(None)) is not None and _soft_ctx.get() == 0:
594
+ block.active = False
595
+ errs, pairs = block.taken()
596
+ if errs:
597
+ _carry(exc, tuple(errs), pairs)
598
+ _attach_soft_note(exc, _drawn(errs, blocks=_errors._RENDER_DIFF_IN_MESSAGE))
599
+ return
600
+ block = _soft_err.get(None)
601
+ errs: list[AssertionOutcome] = block.failures if block is not None else []
602
+ pairs: tuple[tuple[object, object] | None, ...] = ()
603
+ if _soft_ctx.get() == 0 and block is not None:
604
+ # a task still running with this block in its own context copy stops collecting
605
+ block.active = False
606
+ errs, pairs = block.taken()
607
+ if errs and _soft_ctx.get() == 0:
608
+ # the same class as a single failure, so one `except AssertionFailure` covers both
609
+ failure = AssertionFailure(_drawn(errs, blocks=False), failures=tuple(errs))
610
+ failure._compared_pairs = pairs
611
+ failure._soft_summary = functools.partial(_drawn, list(errs), blocks=True, kept=True)
612
+ raise failure
613
+
614
+
615
+ def soft_assertions() -> _SoftAssertions:
616
+ """Create a soft assertion context.
617
+
618
+ Normally, any assertion failure will halt test execution immediately by raising an error.
619
+ Soft assertions are way to collect assertion failures (and failure messages) together, to be
620
+ raised all at once at the end, without halting your test.
621
+
622
+ Uses `contextvars` internally, so each thread and each ``asyncio`` task gets its own
623
+ soft-assertion state. A task started inside an open block collects into that block while it is open.
624
+
625
+ Examples:
626
+ Create a soft assertion context, and some failing tests:
627
+
628
+ from assertpy import assert_that, soft_assertions
629
+
630
+ with soft_assertions():
631
+ assert_that('foo').is_length(4)
632
+ assert_that('foo').is_empty()
633
+ assert_that('foo').is_false()
634
+ assert_that('foo').is_digit()
635
+ assert_that('123').is_alpha()
636
+
637
+ When the context ends, any assertion failures are collected together and a single
638
+ ``AssertionError`` is raised, each tagged with the ``file:line`` it came from:
639
+
640
+ AssertionError: soft assertion failures:
641
+ 1. Expected <foo> to be of length <4>, but was <3>. [test_str.py:10]
642
+ 2. Expected <foo> to be empty string, but was not. [test_str.py:11]
643
+ 3. Expected <False>, but was not. [test_str.py:12]
644
+ 4. Expected <foo> to contain only digits, but did not. [test_str.py:13]
645
+ 5. Expected <123> to contain only alphabetic chars, but did not. [test_str.py:14]
646
+
647
+ Group errors by section:
648
+
649
+ with soft_assertions() as sa:
650
+ with sa.group("Headers"):
651
+ assert_that(headers["Content-Type"]).is_equal_to("application/json")
652
+ with sa.group("Body"):
653
+ assert_that(body["status"]).is_equal_to("ok")
654
+
655
+ Note:
656
+ The soft assertion context only collects *assertion* failures, other errors such as
657
+ ``TypeError`` or ``ValueError`` are always raised immediately. Triggering an explicit test
658
+ failure with [`fail()`][assertpy.assertpy.fail] will similarly halt execution immediately.
659
+ If you need more forgiving behavior, use [`soft_fail()`][assertpy.assertpy.soft_fail] to add
660
+ a failure message without halting test execution.
661
+ """
662
+ return _SoftAssertions()
663
+
664
+
665
+ def assert_all(*callables: Callable[[], object]) -> None:
666
+ """Run all callables inside a soft assertion context.
667
+
668
+ A convenience wrapper around [`soft_assertions()`][assertpy.assertpy.soft_assertions] for inline use.
669
+
670
+ Examples:
671
+ Usage:
672
+
673
+ from assertpy import assert_all, assert_that
674
+
675
+ assert_all(
676
+ lambda: assert_that(x).is_positive(),
677
+ lambda: assert_that(y).is_not_none(),
678
+ )
679
+
680
+ Raises:
681
+ AssertionError: if any of the callables produce assertion failures
682
+ TypeError: if a callable hands back a coroutine, which has asserted nothing
683
+ """
684
+ with soft_assertions():
685
+ for call in callables:
686
+ located = _run_as_written.set(True)
687
+ try:
688
+ verdict(call(), subject="the callable")
689
+ finally:
690
+ _run_as_written.reset(located)
691
+
692
+
693
+ def _build_with(open_blocks: int) -> None:
694
+ """Point `_builder()` at `_scoped_builder` while a block is open anywhere, called under the count's lock.
695
+
696
+ A global swapped here rather than a test in `_builder()`, which outside every block cost `assert_that()` 9%.
697
+ """
698
+ global _made_builder
699
+ _made_builder = _scoped_builder if open_blocks else _ExtendedBuilder
700
+
701
+
702
+ def _scoped_builder(val, description="", kind=None, expected=None, logger=None):
703
+ """A builder whose assertions are observed when it is made inside an entry of a block, a plain one otherwise.
704
+
705
+ Asked of the context that makes it, so a builder made in a thread with no block of its own is plain whatever
706
+ another thread has open, and one made before its block opened stays plain inside it. An observed builder
707
+ reports into the entry it was made in, and into no other once that one has closed.
708
+ """
709
+ entered = _passes.active_scope()
710
+ if entered is None or _passes.logged_class is None:
711
+ return _ExtendedBuilder(val, description, kind, expected, logger)
712
+ made = _passes.logged_class(val, description, kind, expected, logger)
713
+ made._reports_to = entered
714
+ return made
715
+
716
+
717
+ class _LogPasses:
718
+ """The context `log_passes()` hands back. Each entry reports into a scope of its own, so it may be reentered."""
719
+
720
+ __slots__ = ("_level", "_logger", "_on_pass")
721
+
722
+ def __init__(self, logger: PassLogger, level: int, on_pass: Callable[[AssertionOutcome], object] | None) -> None:
723
+ self._logger, self._level, self._on_pass = logger, level, on_pass
724
+
725
+ def __enter__(self) -> None:
726
+ _passes.open_block(_logged_builder_class, _build_with)
727
+ _passes.scope.set(_passes.PassScope(self._logger, self._level, self._on_pass, _passes.scope.get()))
728
+
729
+ def __exit__(self, *_exc: object) -> None:
730
+ try:
731
+ # the entry `__enter__` set in this context, which is the one a `with` statement exits in
732
+ entered = cast("_passes.PassScope", _passes.scope.get())
733
+ # a task outliving the entry reads it from its own context copy, and stops here
734
+ entered.active = False
735
+ _passes.scope.set(entered.previous)
736
+ finally:
737
+ _passes.close_block(_build_with)
738
+
739
+
740
+ def log_passes(
741
+ logger: PassLogger | None = None,
742
+ *,
743
+ level: int = logging.INFO,
744
+ on_pass: Callable[[AssertionOutcome], object] | None = None,
745
+ ) -> contextlib.AbstractContextManager[None]:
746
+ """Report every assertion that passes inside the block, with the value it held for.
747
+
748
+ An assertion that holds says nothing by default. Inside this block each one logs a line naming the call, the
749
+ value and the file and line it was asked at, and, for a comparison against a bound, how far the value was from
750
+ it. Nothing else changes: a failure is raised, collected or logged exactly as it would be outside.
751
+
752
+ One line per assertion the test asked. What an assertion asks underneath (`is_positive()` asking
753
+ `is_greater_than(0)`, an assertion inside a `satisfies()` predicate) is part of it and not reported again. A
754
+ soft or warn assertion that failed is not a pass, nor is one run under `check()`. A poll reports the steps of
755
+ the attempt that passed, once.
756
+
757
+ Uses `contextvars`, so each thread and task has its own: a task started inside the block reports into it while
758
+ the block is open. A builder reports into the block it was made in, while that block is open: one made before
759
+ it reports nothing, and one kept past its end reports nothing, in a later block too. A pivot reports where the
760
+ builder it came from reports.
761
+
762
+ Args:
763
+ logger: where the lines go, any object with ``isEnabledFor()`` and ``log()``. Defaults to the
764
+ ``assertpy.passes`` logger
765
+ level: the level of each line. Defaults to ``logging.INFO``
766
+ on_pass: called with each pass's [`AssertionOutcome`][assertpy.outcome.AssertionOutcome] in place of
767
+ logging it: ``passed`` set, the value, the requirement, the ``(file, line)`` and, as ``hint``, how far
768
+ the value was from its bound. Not together with ``logger``
769
+
770
+ Examples:
771
+ Log the passes of a block:
772
+
773
+ from assertpy import assert_that, log_passes
774
+
775
+ with log_passes():
776
+ assert_that(latency).is_less_than(0.5)
777
+ assert_that(status).is_equal_to(200)
778
+
779
+ Each pass is one line at ``INFO`` on the ``assertpy.passes`` logger:
780
+
781
+ [test_api.py:5]: is_less_than(0.5) passed for <0.42>, the value is about 0.08 below the number given
782
+ [test_api.py:6]: is_equal_to(200) passed for <200>
783
+
784
+ Under pytest a passing test shows its log only when asked, so turn on live logging, for one run with
785
+ ``pytest -o log_cli=true --log-cli-level=INFO`` or for good in the configuration:
786
+
787
+ [tool.pytest.ini_options]
788
+ log_cli = true
789
+ log_cli_level = "INFO"
790
+
791
+ Raises:
792
+ TypeError: if ``level`` is not an ``int``, ``logger`` has no ``isEnabledFor()`` or ``log()``, ``on_pass``
793
+ is not callable, or both ``logger`` and ``on_pass`` are given
794
+ """
795
+ if on_pass is not None:
796
+ if logger is not None:
797
+ refuse(logger, "None when on_pass is given, which takes each pass in its place", subject=argument("logger"))
798
+ if not callable(on_pass):
799
+ refuse(on_pass, "callable", subject=argument("on_pass"))
800
+ if logger is not None and not (
801
+ callable(getattr(logger, "isEnabledFor", None)) and callable(getattr(logger, "log", None))
802
+ ):
803
+ refuse(logger, "a logger, with isEnabledFor() and log()", subject=argument("logger"))
804
+ if type(level) is not int:
805
+ refuse(level, "an int logging level", subject=argument("level"))
806
+ return _LogPasses(logger if logger is not None else _passes.default_logger, level, on_pass)
807
+
808
+
809
+ @overload
810
+ def assert_that(val: str, description: str = "") -> _StringAssertion: ...
811
+
812
+
813
+ @overload
814
+ def assert_that(val: bool, description: str = "") -> _BoolAssertion: ...
815
+ @overload
816
+ def assert_that(val: int, description: str = "") -> _NumericAssertion[int]: ...
817
+
818
+
819
+ @overload
820
+ def assert_that(val: float, description: str = "") -> _NumericAssertion[float]: ...
821
+
822
+
823
+ @overload
824
+ def assert_that(val: complex, description: str = "") -> _ComplexAssertion: ...
825
+
826
+
827
+ @overload
828
+ def assert_that(val: dict[_K, _V], description: str = "") -> _DictAssertion[_K, _V]: ...
829
+
830
+
831
+ @overload
832
+ def assert_that(val: list[_E] | tuple[_E, ...], description: str = "") -> _IterableAssertion[_E]: ...
833
+
834
+
835
+ @overload
836
+ def assert_that(val: set[_E] | frozenset[_E], description: str = "") -> _IterableAssertion[_E]: ...
837
+
838
+
839
+ @overload
840
+ # a `datetime` is a `date` and is claimed first: one view for both offered chronology to a type that cannot answer
841
+ def assert_that(val: datetime.datetime, description: str = "") -> _DateTimeAssertion: ...
842
+
843
+
844
+ @overload
845
+ def assert_that(val: datetime.date, description: str = "") -> _DateAssertion: ...
846
+
847
+
848
+ @overload
849
+ def assert_that(val: pathlib.Path, description: str = "") -> _PathAssertion: ...
850
+
851
+
852
+ @overload
853
+ def assert_that(val: bytes, description: str = "") -> _BytesAssertion[bytes]: ...
854
+
855
+
856
+ @overload
857
+ def assert_that(val: bytearray, description: str = "") -> _BytesAssertion[bytearray]: ...
858
+
859
+
860
+ # a frame is assignable to every structural protocol, so it goes first; `tests/test_overload_order.py` holds that
861
+ @overload
862
+ def assert_that(val: _FrameT_co, description: str = "") -> _FrameAssertion[_FrameT_co]: ...
863
+
864
+
865
+ @overload
866
+ def assert_that(val: _ArrayT_co, description: str = "") -> _ArrayAssertion[_ArrayT_co]: ...
867
+
868
+
869
+ # the umbrella: below the frame pair so it cannot claim their values, above the fallback so a capability keeps all
870
+ @overload
871
+ def assert_that(val: _CapableT, description: str = "") -> _CapableAssertion[_CapableT]: ...
872
+
873
+
874
+ # below the umbrella: an ASGI or WSGI response is a callable, and this claimed one before its capability could
875
+ @overload
876
+ def assert_that(val: Callable[..., _P], description: str = "") -> _CallableAssertion[_P]: ...
877
+
878
+
879
+ # the fallback, at the price of an overload overlap mypy and pyright report and ty does not
880
+ @overload
881
+ def assert_that(val: _T, description: str = "") -> _ObjectAssertion[_T]: ...
882
+
883
+
884
+ # `Any` rather than the base protocol, which took pyright past its 4 GB heap; the overloads are what a caller sees
885
+ def assert_that(val: object, description="") -> Any:
886
+ """Set the value to be tested, plus an optional description, and allow assertions to be called.
887
+
888
+ This is a factory method for the `AssertionBuilder`, and the single most important
889
+ method in all of assertpy.
890
+
891
+ Args:
892
+ val: the value to be tested (aka the actual value)
893
+ description (str, optional): the extra error message description. Defaults to ``''``
894
+ (aka empty string)
895
+
896
+ Examples:
897
+ Just import it once at the top of your test file, and away you go...
898
+
899
+ from assertpy import assert_that
900
+
901
+ def test_something():
902
+ assert_that(1 + 2).is_equal_to(3)
903
+ assert_that('foobar').is_length(6).starts_with('foo').ends_with('bar')
904
+ assert_that(['a', 'b', 'c']).contains('a').does_not_contain('x')
905
+ """
906
+ if _collecting() is not None:
907
+ return _builder(val, description, "soft")
908
+ return _builder(val, description)
909
+
910
+
911
+ def _contract_entries(exc: object, prefix: _Path = _ROOT) -> list[DiffEntry]:
912
+ """Turn a pydantic ``ValidationError`` into diff rows, mirroring the OpenAPI path in `json_mixin`.
913
+
914
+ ``errors()`` already carries the location, the message and the offending input, so the structured
915
+ channel costs a reshape rather than a second walk of the payload. Under ``each=True`` the caller
916
+ passes the element's ``[i]`` prefix, the way ``contract_drift`` labels its own paths.
917
+
918
+ ``loc`` is already a tuple of keys and indices, so the machine half of the path comes straight
919
+ across rather than being parsed back out of the text.
920
+ """
921
+ errors = exc.errors() if hasattr(exc, "errors") else [] # ty: ignore[call-non-callable] # duck-typed pydantic
922
+ entries = []
923
+ for error in errors:
924
+ path = prefix
925
+ for segment in error.get("loc", ()):
926
+ path = path.index(segment) if isinstance(segment, int) else path.key(segment)
927
+ entries.append(path.leaf_entry(actual=error.get("input"), expected=error.get("msg", "")))
928
+ return entries
929
+
930
+
931
+ def _exact_failure(val: object, pairs: Any, carrier: str, sent: Any) -> tuple[str, str, DiffResult] | None:
932
+ """What an exact check of *pairs* found, in the words a failure continues with, the payload as it then reads, and
933
+ the same finds as a diff. ``None`` when it found nothing.
934
+
935
+ All are read while the payload is put back as it was *sent*, where validation changed it in place, and it is
936
+ returned to what validation left before this returns, whatever the check does.
937
+ """
938
+ left = put_as_sent(val, sent) if sent is not None else None
939
+ try:
940
+ found = exactness_failure(pairs, carrier=carrier)
941
+ if found is None:
942
+ return None
943
+ entries = kept_apart(found[1]) if left else found[1]
944
+ return found[0], _truncated(_safe_str(val)), DiffResult(kind="match", entries=entries)
945
+ finally:
946
+ if left:
947
+ put_back(left)
948
+
949
+
950
+ @overload
951
+ def assert_conforms(
952
+ val: object, model: type[_U], description: str = ..., *, exact: bool = ..., each: Literal[False] = ...
953
+ ) -> AssertionBuilder[_U]: ...
954
+ @overload
955
+ def assert_conforms(
956
+ val: object, model: type[_U], description: str = ..., *, exact: bool = ..., each: Literal[True]
957
+ ) -> AssertionBuilder[list[_U]]: ...
958
+ def assert_conforms(
959
+ val: object, model: type[_U], description: str = "", *, exact: bool = False, each: bool = False
960
+ ) -> AssertionBuilder[Any]:
961
+ """Validate ``val`` against a pydantic v2 ``model`` and continue over the validated instance.
962
+
963
+ The narrowing-complete companion to [`assert_that()`][assertpy.assertpy.assert_that] for
964
+ contract testing. Runs ``model.model_validate(val)``: on success the returned builder carries the
965
+ validated, coerced instance (so ``.value`` and ``extracting`` see typed fields); on failure it
966
+ fails with pydantic's validation errors.
967
+
968
+ Because the return type is driven by ``model`` rather than by the type of ``val``, the chain
969
+ narrows to ``model`` for **any** input - including the ``Any`` a decoded JSON payload carries.
970
+
971
+ With ``exact=True`` it also asserts **contract drift**: the payload must not carry fields the model
972
+ does not declare. ``model_validate`` silently drops undeclared fields, so a stale model keeps
973
+ passing after the live API grows new ones.
974
+
975
+ ``exact`` catches that drift in the models, dataclasses and ``TypedDict`` values the payload became inside
976
+ lists, tuples, sets, dicts, unions and root models, and reports the exact paths. It is alias-aware, and
977
+ respects a model that opts into extras (``extra="allow"``). A key that spells a field and was not read,
978
+ a name sent beside its alias, is dropped by pydantic like any other, and is named with where the field
979
+ was read from.
980
+
981
+ Raw items that no longer pair one by one with what they became are still checked where they can be:
982
+
983
+ - A set, a filtered list, an object wrapped into a list: validated again by their model class when they
984
+ all became one, which runs that class's validators once more for them. JSON text is read as the
985
+ original JSON input.
986
+ - An emptied list, whose raw items a validator left no built item for: checked against the type their
987
+ field declares. They are validated again as it, which runs the item model's validators but not the
988
+ field's own, so a field validator that would have built them otherwise is not seen.
989
+
990
+ What still cannot be paired fails with ``<path> cannot be checked`` and the reason:
991
+
992
+ - items of mixed classes in a set or a resized list
993
+ - emptied items the declared type does not build, or declares through a union of two or more types
994
+ besides ``None`` or a named type alias
995
+ - merged dict keys
996
+ - a generator consumed during validation, or a lazy ``Iterable``
997
+
998
+ It reads the payload as it was sent: what a validator changed in place in a plain dict or list is put back
999
+ for the check and returned afterwards.
1000
+
1001
+ Two things it does not see:
1002
+
1003
+ - a validator that renames keys, or reorders or rewrites the items of a container without changing its
1004
+ size: those items are read by position
1005
+ - a model inside a container of plain values (``list[Any]``)
1006
+
1007
+ A ``TypedDict`` is read off the schema pydantic built, so text annotations and type variables declare it
1008
+ too. Where several are declared for one value and their ``Literal`` fields do not tell them apart, or a
1009
+ type beside them may build a dict as well, a key the built dict lost fails with ``cannot be checked``.
1010
+
1011
+ An ``exact`` failure carries its finds as a ``diff``: an entry per undeclared field, holding the value the
1012
+ payload sent there and the steps that lead to it, or one entry holding the part that cannot be checked
1013
+ against the reason.
1014
+
1015
+ Args:
1016
+ val: the raw payload to validate (e.g. a decoded JSON response)
1017
+ model: a pydantic v2 model class (anything exposing ``model_validate``)
1018
+ description (str, optional): the extra error message description. Defaults to ``''``
1019
+ exact (bool, optional): also fail if the payload carries fields ``model`` does not declare.
1020
+ Defaults to ``False``
1021
+ each (bool, optional): validate a *list* payload element-by-element against ``model`` (for list
1022
+ endpoints), narrowing the chain to ``list[model]``. ``exact`` then applies per element.
1023
+ Defaults to ``False``
1024
+
1025
+ Examples:
1026
+ Usage:
1027
+
1028
+ from assertpy import assert_conforms, assert_that
1029
+
1030
+ order = assert_conforms(response.json(), OrderModel).value # .value: OrderModel
1031
+ assert_that(order.total).is_greater_than(0)
1032
+
1033
+ # catch silent API growth: fail if the response grew fields the model does not declare
1034
+ assert_conforms(response.json(), OrderModel, exact=True)
1035
+
1036
+ # a list endpoint: validate every item, narrowing to list[OrderModel]
1037
+ orders = assert_conforms(response.json(), OrderModel, each=True).value # .value: list[OrderModel]
1038
+
1039
+ Returns:
1040
+ AssertionBuilder: a builder over the validated model instance, statically typed as ``model``
1041
+
1042
+ Raises:
1043
+ TypeError: if ``model`` is not a pydantic v2 model class
1044
+ AssertionError: if ``val`` does not validate against ``model``, or (with ``exact``) drifts from it or
1045
+ holds a part that cannot be paired with the model built from it
1046
+ """
1047
+ if not (isinstance(model, type) and hasattr(model, "model_validate")):
1048
+ raise TypeError("assert_conforms requires a pydantic v2 model class")
1049
+ kind = "soft" if _collecting() is not None else None
1050
+ builder = _builder(val, description, kind)
1051
+ if type(builder) is _passes.logged_class:
1052
+ asked: dict[str, object] = {"model": model, "description": description, "exact": exact, "each": each}
1053
+ written: dict[str, object] = {name: True for name in ("exact", "each") if asked[name]}
1054
+ return _observe(
1055
+ builder,
1056
+ lambda: _conforming(builder, val, model, description, exact=exact, each=each, kind=kind),
1057
+ lambda: ("assert_conforms", (model,), written, Requirement("assert_conforms", asked)),
1058
+ )
1059
+ return _conforming(builder, val, model, description, exact=exact, each=each, kind=kind)
1060
+
1061
+
1062
+ def _conforming(
1063
+ builder: AssertionBuilder[Any],
1064
+ val: object,
1065
+ model: Any,
1066
+ description: str,
1067
+ *,
1068
+ exact: bool,
1069
+ each: bool,
1070
+ kind: str | None,
1071
+ ) -> AssertionBuilder[Any]:
1072
+ """The body of `assert_conforms()`, run beneath its frame, which is the one a failure reads its requirement off."""
1073
+ pydantic = sys.modules.get("pydantic") # loaded already, since model exposes model_validate
1074
+ catchable: tuple[type[BaseException], ...] = (pydantic.ValidationError,) if pydantic is not None else ()
1075
+ # only a model with code of its own can change the payload it validates; what it is sent is kept for the check
1076
+ sent = sent_record(val) if exact and runs_own_code(model) else None
1077
+ if each:
1078
+ if not isinstance(val, (list, tuple)):
1079
+ raise TypeError("assert_conforms(each=True) requires a list or tuple payload")
1080
+ validated_items = []
1081
+ for index, item in enumerate(val):
1082
+ try:
1083
+ validated_items.append(model.model_validate(item))
1084
+ except catchable as exc: # noqa: PERF203 # per-element catch reports which item failed; ~0 cost on 3.11+
1085
+ return builder.error(
1086
+ f"Expected item [{index}] to conform to <{model.__name__}>, but it did not:\n{exc}",
1087
+ actual=val,
1088
+ expected=model,
1089
+ diff=DiffResult(kind="match", entries=_contract_entries(exc, _ROOT.index(index))),
1090
+ suppress_context=True,
1091
+ )
1092
+ if exact:
1093
+ items = enumerate(zip(val, validated_items, strict=True))
1094
+ found = _exact_failure(val, ((item, it, index) for index, (item, it) in items), "", sent)
1095
+ if found:
1096
+ return builder.error(
1097
+ f"Expected every item to conform exactly to <{model.__name__}>, but {found[0]}",
1098
+ actual=val,
1099
+ expected=model,
1100
+ diff=found[2],
1101
+ )
1102
+ return builder.builder(validated_items, description, kind)
1103
+ try:
1104
+ validated = model.model_validate(val)
1105
+ except catchable as exc:
1106
+ return builder.error(
1107
+ f"Expected <{_truncated(_safe_str(val))}> to conform to <{model.__name__}>, but it did not:\n{exc}",
1108
+ actual=val,
1109
+ expected=model,
1110
+ diff=DiffResult(kind="match", entries=_contract_entries(exc)),
1111
+ suppress_context=True,
1112
+ )
1113
+ if exact:
1114
+ found = _exact_failure(val, [(val, validated, None)], "it carries ", sent)
1115
+ if found:
1116
+ return builder.error(
1117
+ f"Expected <{found[1]}> to conform exactly to <{model.__name__}>, but {found[0]}",
1118
+ actual=val,
1119
+ expected=model,
1120
+ diff=found[2],
1121
+ )
1122
+ return builder.builder(validated, description, kind)
1123
+
1124
+
1125
+ def assert_warn(
1126
+ val: object,
1127
+ description: str = "",
1128
+ logger: WarningLogger | None = None,
1129
+ *,
1130
+ on_failure: Callable[[AssertionOutcome], object] | None = None,
1131
+ ) -> Any:
1132
+ """Set the value to be tested, and optional description and logger, and allow assertions to be
1133
+ called, but never fail, only log warnings or hand each failure to ``on_failure``.
1134
+
1135
+ This is a factory method for the `AssertionBuilder`, but unlike [`assert_that()`][assertpy.assertpy.assert_that] a
1136
+ failed assertion raises no `AssertionError` and does not halt execution. Instead, any assertion failures
1137
+ results in a warning message being logged. Uses the given logger, or defaults to a simple logger that prints
1138
+ warnings to ``stdout``. An exception the logger or an ``on_failure`` callable raises does come out of the
1139
+ assertion that failed, whatever its type.
1140
+
1141
+
1142
+ Args:
1143
+ val: the value to be tested (aka the actual value)
1144
+ description (str, optional): the extra error message description. Defaults to ``''``
1145
+ (aka empty string)
1146
+ logger (Logger, optional): the logger for warning message on assertion failure. Defaults to ``None``
1147
+ (aka use the default simple logger that prints warnings to ``stdout``)
1148
+ on_failure (Callable, optional): called with each failure's
1149
+ [`AssertionOutcome`][assertpy.outcome.AssertionOutcome] in place of logging it: the message, the
1150
+ values, the diff and the ``(file, line)`` it was asked at, for every link of the chain. An exception
1151
+ it raises, an ``AssertionError`` included, comes out of the assertion that failed. Not together with
1152
+ ``logger``.
1153
+
1154
+ Examples:
1155
+ Usage:
1156
+
1157
+ from assertpy import assert_warn
1158
+
1159
+ assert_warn('foo').is_length(4)
1160
+ assert_warn('foo').is_empty()
1161
+ assert_warn('foo').is_false()
1162
+ assert_warn('foo').is_digit()
1163
+ assert_warn('123').is_alpha()
1164
+
1165
+ Even though all of the above assertions fail, ``AssertionError`` is never raised and
1166
+ test execution is never halted. Instead, the failed assertions merely log the following
1167
+ warning messages to ``stdout``, each after the time it was logged, which is left out here:
1168
+
1169
+ WARNING [test.py:23]: Expected <foo> to be of length <4>, but was <3>.
1170
+ WARNING [test.py:24]: Expected <foo> to be empty string, but was not.
1171
+ WARNING [test.py:25]: Expected <False>, but was not.
1172
+ WARNING [test.py:26]: Expected <foo> to contain only digits, but did not.
1173
+ WARNING [test.py:27]: Expected <123> to contain only alphabetic chars, but did not.
1174
+
1175
+ Tip:
1176
+ Use `assert_warn()` if and only if you have a *really* good reason to log assertion
1177
+ failures instead of failing.
1178
+ """
1179
+ if on_failure is not None:
1180
+ if logger is not None:
1181
+ refuse(
1182
+ logger,
1183
+ "None when on_failure is given, which takes each failure in its place",
1184
+ subject=argument("logger"),
1185
+ )
1186
+ if not callable(on_failure):
1187
+ refuse(on_failure, "callable", subject=argument("on_failure"))
1188
+ logger = _Subscribed(on_failure)
1189
+ return _builder(val, description, "warn", logger=logger)
1190
+
1191
+
1192
+ def fail(msg: str = "") -> NoReturn:
1193
+ """Force immediate test failure with the given message.
1194
+
1195
+ Args:
1196
+ msg (str, optional): the failure message. Defaults to ``''``
1197
+
1198
+ Examples:
1199
+ Fail a test:
1200
+
1201
+ from assertpy import assert_that, fail
1202
+
1203
+ def test_fail():
1204
+ fail('forced fail!')
1205
+
1206
+ If you wanted to test for a known failure, here is a useful pattern:
1207
+
1208
+ import operator
1209
+
1210
+ def test_adder_bad_arg():
1211
+ try:
1212
+ operator.add(1, 'bad arg')
1213
+ fail('should have raised error')
1214
+ except TypeError as e:
1215
+ assert_that(str(e)).contains('unsupported operand')
1216
+ """
1217
+ # the same class as every other failure, so a handler does not have to name two of them
1218
+ raise AssertionFailure(f"Fail: {msg}!" if msg else "Fail!")
1219
+
1220
+
1221
+ def soft_fail(msg: str = "") -> None:
1222
+ """Within a [`soft_assertions()`][assertpy.assertpy.soft_assertions] context, append the failure
1223
+ message to the soft error list, but do not halt test execution.
1224
+
1225
+ Otherwise, outside the context, acts identical to [`fail()`][assertpy.assertpy.fail] and forces immediate test
1226
+ failure with the given message.
1227
+
1228
+ Args:
1229
+ msg (str, optional): the failure message. Defaults to ``''``
1230
+
1231
+ Examples:
1232
+ Failing soft assertions:
1233
+
1234
+ from assertpy import assert_that, soft_assertions, soft_fail
1235
+
1236
+ with soft_assertions():
1237
+ assert_that(1).is_equal_to(2)
1238
+ soft_fail('my message')
1239
+ assert_that('foo').is_equal_to('bar')
1240
+
1241
+ Fails, and outputs the following soft error list (each tagged with its ``file:line``):
1242
+
1243
+ AssertionError: soft assertion failures:
1244
+ 1. Expected <1> to be equal to <2>, but was not. [test_add.py:10]
1245
+ 2. Fail: my message! [test_add.py:11]
1246
+ 3. Expected <foo> to be equal to <bar>, but was not. [test_add.py:12]
1247
+
1248
+ """
1249
+ if (block := _collecting()) is not None:
1250
+ if _passes.open_blocks:
1251
+ _passes.mark_failed()
1252
+ block.failures.append(
1253
+ AssertionOutcome(
1254
+ message=f"Fail: {msg}!" if msg else "Fail!",
1255
+ group=_soft_group.get(),
1256
+ location=_caller_location(),
1257
+ )
1258
+ )
1259
+ return
1260
+ fail(msg)
1261
+
1262
+
1263
+ _extensions = {}
1264
+ # guards the check-then-set below against two threads adding the same name
1265
+ _extensions_lock = threading.Lock()
1266
+
1267
+
1268
+ # `not_` is answered inside `__getattr__` rather than declared, so it is named here
1269
+ _POLLING_NAMES: Final = frozenset(
1270
+ name
1271
+ for builder in (AsyncAssertionBuilder, SyncAssertionBuilder)
1272
+ for name in vars(builder)
1273
+ if not name.startswith("_")
1274
+ ) | {"not_"}
1275
+
1276
+
1277
+ def add_extension(func: _Extension, *, override: bool = False) -> None:
1278
+ """Add a new user-defined custom assertion to assertpy.
1279
+
1280
+ Once the assertion is registered with assertpy, use it like any other assertion. Pass val to
1281
+ [`assert_that()`][assertpy.assertpy.assert_that], and then call it.
1282
+
1283
+ A name already in use is refused, so an extension that would quietly replace a built-in assertion
1284
+ or another extension says so instead. Registering the same implementation again is not a clash:
1285
+ a module-scoped ``conftest`` fixture rebuilds its function on every module that requests it.
1286
+
1287
+ Args:
1288
+ func (Callable): the assertion function (to be added)
1289
+ override: replace an assertion of the same name instead of refusing
1290
+
1291
+ Raises:
1292
+ TypeError: if ``func`` is not callable
1293
+ ValueError: if its ``__name__`` is not an identifier, or the name is taken and ``override``
1294
+ is false
1295
+
1296
+ Examples:
1297
+ Usage:
1298
+
1299
+ from assertpy import add_extension
1300
+
1301
+ def is_5(self):
1302
+ if self.val != 5:
1303
+ return self.error(f'{self.val} is NOT 5!')
1304
+ return self
1305
+
1306
+ add_extension(is_5)
1307
+
1308
+ def test_5():
1309
+ assert_that(5).is_5()
1310
+
1311
+ def test_6():
1312
+ assert_that(6).is_5() # fails
1313
+ # 6 is NOT 5!
1314
+ """
1315
+ if not callable(func):
1316
+ refuse(func, "callable", subject=argument("func"))
1317
+ name = getattr(func, "__name__", None)
1318
+ if not isinstance(name, str) or not name.isidentifier():
1319
+ raise ValueError(f"the assertion's __name__ must be a valid Python identifier, got {name!r}")
1320
+ # nothing to override: after `eventually()` the name is out of reach, and `override=True` bought silence
1321
+ if name in _POLLING_NAMES or name.startswith(("_", "cr_", "gi_")):
1322
+ raise ValueError(
1323
+ f"{name!r} is how a polling chain spells one of its own, so an extension of that name "
1324
+ f"would be unreachable after eventually(); give it another name"
1325
+ )
1326
+ with _extensions_lock:
1327
+ # re-adding the same implementation is a no-op, not a clash. Compared against what was
1328
+ # registered rather than against the wrapper naming it, whose code every extension shares
1329
+ same = is_same_implementation(_registered(vars(_ExtendedBuilder).get(name)), func) or is_same_implementation(
1330
+ _registered(_extensions.get(name)), func
1331
+ )
1332
+ if not override and not same:
1333
+ # an extension called `is_equal_to` used to replace the core assertion in silence
1334
+ if name in vars(_ExtendedBuilder) or name in _extensions:
1335
+ raise ValueError(
1336
+ f"an assertion named {name!r} has already been added; pass override=True to "
1337
+ f"replace it, or remove_extension() it first"
1338
+ )
1339
+ if hasattr(AssertionBuilder, name):
1340
+ raise ValueError(
1341
+ f"{name!r} is already defined on the assertion builder; pass override=True to "
1342
+ f"replace it deliberately, or give the extension another name"
1343
+ )
1344
+ named = _named_extension(name, func)
1345
+ # one implementation per name, so an override that changes shape clears the other half too: a
1346
+ # callable grafted onto the instance in `_builder()` shadows a function set on the class
1347
+ if isinstance(func, types.FunctionType):
1348
+ _extensions.pop(name, None)
1349
+ # the descriptor protocol binds once here, and the subclass keeps `AssertionBuilder` pristine on removal
1350
+ setattr(_ExtendedBuilder, name, named)
1351
+ else:
1352
+ if name in vars(_ExtendedBuilder):
1353
+ delattr(_ExtendedBuilder, name)
1354
+ _extensions[name] = named
1355
+
1356
+
1357
+ def remove_extension(func: _Extension) -> None:
1358
+ """Remove a user-defined custom assertion.
1359
+
1360
+ Args:
1361
+ func (Callable): the assertion function (to be removed)
1362
+
1363
+ Examples:
1364
+ Usage:
1365
+
1366
+ from assertpy import remove_extension
1367
+
1368
+ remove_extension(is_5)
1369
+ """
1370
+ if not callable(func):
1371
+ refuse(func, "callable", subject=argument("func"))
1372
+ if func.__name__ in vars(_ExtendedBuilder):
1373
+ delattr(_ExtendedBuilder, func.__name__)
1374
+ _extensions.pop(func.__name__, None)
1375
+ # a signature cached against a function nobody can call any more
1376
+ _method_signature.cache_clear()
1377
+
1378
+
1379
+ class _ExtensionCall:
1380
+ """One running call of one extension: what it was asked, on which builder, and whether it is still on.
1381
+
1382
+ ``alive`` exists for the same reason `_SoftBlock.active` does. A task created inside the call
1383
+ inherits the context variable by value and never sees the reset, so without it a failure raised
1384
+ after the call had returned was still reported under the extension's name.
1385
+ """
1386
+
1387
+ __slots__ = ("alive", "args", "builder", "func", "kwargs", "name")
1388
+
1389
+ def __init__(
1390
+ self,
1391
+ builder: object,
1392
+ name: str,
1393
+ func: Callable[..., object],
1394
+ args: tuple[object, ...],
1395
+ kwargs: dict[str, object],
1396
+ ) -> None:
1397
+ self.builder = builder
1398
+ self.name = name
1399
+ self.func = func
1400
+ self.args = args
1401
+ self.kwargs = kwargs
1402
+ self.alive = True
1403
+
1404
+ def answers_for(self, builder: object) -> bool:
1405
+ """Whether a failure on *builder* is this call's to name."""
1406
+ return self.alive and self.builder is builder
1407
+
1408
+
1409
+ _asked_of: contextvars.ContextVar[_ExtensionCall | None] = contextvars.ContextVar("assertpy_asked_of", default=None)
1410
+ """The extension call now running, set before its body and read only if it fails.
1411
+
1412
+ An extension cannot be recognised from its frame. Two instances of one callable class share
1413
+ ``__call__``'s code, two functions built by one decorator factory share the wrapper's, and a
1414
+ `functools.partial` runs no Python frame of its own at all. Naming it at the call is exact for every
1415
+ shape, and it costs an extension call rather than every assertion.
1416
+
1417
+ Scoped to one builder, because the name belongs to a call and not to everything that runs during it.
1418
+ An extension asserting about some other value has that assertion answer for itself, exactly as one
1419
+ inside a `satisfies()` predicate does. On the same builder the outer call keeps the name, which is
1420
+ what a built-in does: `is_positive()` reports itself rather than the `is_greater_than()` it asks.
1421
+ """
1422
+
1423
+
1424
+ def _registered(installed: object) -> object:
1425
+ """What was handed to `add_extension()`, behind the wrapper that names it."""
1426
+ return getattr(installed, "__wrapped__", installed)
1427
+
1428
+
1429
+ def _named_extension(name: str, func: Callable[..., object]) -> Callable[..., object]:
1430
+ """*func*, saying what it was asked for the length of the call."""
1431
+
1432
+ @functools.wraps(func)
1433
+ def named(self: object, *args: object, **kwargs: object) -> object:
1434
+ outer = _asked_of.get()
1435
+ if outer is not None and outer.answers_for(self):
1436
+ return func(self, *args, **kwargs)
1437
+ call = _ExtensionCall(self, name, func, args, kwargs)
1438
+ token = _asked_of.set(call)
1439
+ try:
1440
+ if _passes.open_blocks and type(self) is _passes.logged_class:
1441
+ return _observe_extension(cast("AssertionBuilder[Any]", self), func, args, kwargs, call)
1442
+ return func(self, *args, **kwargs)
1443
+ finally:
1444
+ call.alive = False
1445
+ _asked_of.reset(token)
1446
+
1447
+ return named
1448
+
1449
+
1450
+ def _observe_extension(
1451
+ builder: AssertionBuilder[Any],
1452
+ func: Callable[..., object],
1453
+ args: tuple[object, ...],
1454
+ kwargs: dict[str, object],
1455
+ call: _ExtensionCall,
1456
+ ) -> object:
1457
+ """An extension's call, observed. Apart from `named`: a closure there made its locals cells on every call."""
1458
+ return _observe(
1459
+ builder,
1460
+ lambda: func(builder, *args, **kwargs),
1461
+ lambda: (call.name, args, kwargs, _extension_requirement(call)),
1462
+ )
1463
+
1464
+
1465
+ def _extension_requirement(asked: _ExtensionCall) -> Requirement:
1466
+ """What the running extension was asked, bound to its own parameter names where they can be read."""
1467
+ name, func, args, kwargs = asked.name, asked.func, asked.args, asked.kwargs
1468
+ found = _signature_of(func)
1469
+ if found is not None:
1470
+ # the receiver is the builder, which the caller did not write
1471
+ caller_writes = found.replace(parameters=list(found.parameters.values())[1:])
1472
+ with contextlib.suppress(TypeError):
1473
+ bound = caller_writes.bind(*args, **kwargs)
1474
+ bound.apply_defaults()
1475
+ return Requirement(name, dict(bound.arguments))
1476
+ # a signature nothing can read, or a call it disagrees with: the operands as the caller passed them
1477
+ return Requirement(name, {"args": args, "kwargs": kwargs})
1478
+
1479
+
1480
+ def _call_text(name: str, args: tuple[object, ...], kwargs: Mapping[str, object]) -> str:
1481
+ """*name* called with *args* and *kwargs* as a call is written: ``key=value``, keywords in the caller's order."""
1482
+ rendered = ", ".join(
1483
+ [_capped_repr(arg) for arg in args] + [f"{key}={_capped_repr(value)}" for key, value in kwargs.items()]
1484
+ )
1485
+ return f"{name}({rendered})"
1486
+
1487
+
1488
+ def _bound_requirement(
1489
+ attr: Callable[..., object],
1490
+ name: str,
1491
+ args: tuple[object, ...],
1492
+ kwargs: dict[str, object],
1493
+ *,
1494
+ negated: bool = False,
1495
+ ) -> Requirement:
1496
+ """What a call asked, bound to the parameter names of *attr*, as the positive path reads them off the frame."""
1497
+ found = _signature_of(attr)
1498
+ if found is None:
1499
+ # a builtin with no introspectable signature, or a call the assertion itself will refuse
1500
+ return Requirement(name, {"args": args, "kwargs": kwargs}, negated=negated)
1501
+ try:
1502
+ bound = found.bind(*args, **kwargs)
1503
+ except TypeError:
1504
+ return Requirement(name, {"args": args, "kwargs": kwargs}, negated=negated)
1505
+ bound.apply_defaults()
1506
+ return Requirement(name, dict(bound.arguments), negated=negated)
1507
+
1508
+
1509
+ _Ran = TypeVar("_Ran")
1510
+
1511
+
1512
+ def _observe(
1513
+ builder: AssertionBuilder[Any],
1514
+ run: Callable[[], _Ran],
1515
+ describe: Callable[[], tuple[str, tuple[object, ...], dict[str, object], Requirement]],
1516
+ ) -> _Ran:
1517
+ """Run one assertion of a builder a `log_passes()` block made, and report it if it held.
1518
+
1519
+ Not reported: one asked inside another being observed, which is the outer one's to report, one run for its
1520
+ verdict (`check()`, and the run `not_` inverts), and one that delivered a failure without raising.
1521
+ *describe* is asked only for a pass that is reported.
1522
+ """
1523
+ if _passes.current_call() is not None:
1524
+ return run()
1525
+ found: _passes.PassScope | None = getattr(builder, "_reports_to", None)
1526
+ if found is None or not found.active:
1527
+ return run()
1528
+ # a verdict asked for is not reported, and neither is anything asked inside it
1529
+ asked_for_a_verdict = builder.kind == "check"
1530
+ call = _passes.Observed()
1531
+ token = _passes.observed.set(call)
1532
+ try:
1533
+ result = run()
1534
+ # still inside the call, so an assertion the logger or the callback makes is its, and reports nothing
1535
+ if not (asked_for_a_verdict or call.failed) and _passes.wants(found):
1536
+ _report_pass(found, builder, *describe())
1537
+ finally:
1538
+ call.alive = False
1539
+ _passes.observed.reset(token)
1540
+ return result
1541
+
1542
+
1543
+ def _report_pass(
1544
+ found: _passes.PassScope,
1545
+ builder: AssertionBuilder[Any],
1546
+ name: str,
1547
+ args: tuple[object, ...],
1548
+ kwargs: dict[str, object],
1549
+ requirement: Requirement,
1550
+ ) -> None:
1551
+ """Hand one pass to the block: the record now, its line only if a logger is to print it."""
1552
+ location = _caller_location()
1553
+ val, description = builder.val, builder.description
1554
+ hint = _hints.passed_by(requirement.operation, val, requirement.parameters, negated=requirement.negated)
1555
+ outcome = AssertionOutcome(passed=True, actual=val, requirement=requirement, location=location, hint=hint)
1556
+ step = f"not_.{name}" if requirement.negated else name
1557
+
1558
+ def line() -> str:
1559
+ where = f"[{os.path.basename(location[0])}:{location[1]}]: " if location is not None else ""
1560
+ desc = f"[{description}] " if description else ""
1561
+ margin = f", {hint}" if hint else ""
1562
+ text = f"{where}{desc}{_call_text(step, args, kwargs)} passed for <{_capped(val)}>{margin}"
1563
+ # one record, one line; and a lone surrogate cannot be encoded, which pytest-xdist does to captured logs
1564
+ text = text.replace("\r", "\\r").replace("\n", "\\n").encode("utf-8", "backslashreplace").decode("utf-8")
1565
+ return _errors._ends_kept(text)
1566
+
1567
+ _passes.emit(found, outcome, line)
1568
+
1569
+
1570
+ def _observing(name: str) -> Callable[..., object]:
1571
+ """The built-in assertion *name*, observed on a builder a `log_passes()` block made."""
1572
+ own = getattr(AssertionBuilder, name)
1573
+
1574
+ @functools.wraps(own)
1575
+ def observed(self: AssertionBuilder[Any], *args: object, **kwargs: object) -> object:
1576
+ taken = vars(_ExtendedBuilder).get(name)
1577
+ if taken is not None:
1578
+ # an extension took the name over, and reports itself as one
1579
+ return taken(self, *args, **kwargs)
1580
+ return _observe(
1581
+ self,
1582
+ lambda: own(self, *args, **kwargs),
1583
+ lambda: (name, args, kwargs, _bound_requirement(types.MethodType(own, self), name, args, kwargs)),
1584
+ )
1585
+
1586
+ return observed
1587
+
1588
+
1589
+ def _observed_dynamic(self: AssertionBuilder[Any], name: str) -> object:
1590
+ """A dynamic ``has_`` assertion, observed as the built-ins are; anything else is refused as it always was."""
1591
+ found = DynamicMixin.__getattr__(self, name)
1592
+
1593
+ # wrapped, so `not_` binds the operand to the name the assertion gives it
1594
+ @functools.wraps(found)
1595
+ def observed(*args: object, **kwargs: object) -> object:
1596
+ return _observe(
1597
+ self,
1598
+ lambda: found(*args, **kwargs),
1599
+ lambda: (name, args, kwargs, Requirement(name, _one_operand(args, kwargs))),
1600
+ )
1601
+
1602
+ return observed
1603
+
1604
+
1605
+ def _logged_builder_class() -> type[Any]:
1606
+ """The class a `log_passes()` block builds with: every operation of `_ASSERTS` observed, nothing else changed."""
1607
+ namespace: dict[str, object] = {name: _observing(name) for name in _ASSERTS if name != "assert_conforms"}
1608
+ namespace["__getattr__"] = _observed_dynamic
1609
+ namespace["_reports_to"] = None
1610
+ namespace["__doc__"] = "A builder made inside a `log_passes()` block, whose assertions report themselves."
1611
+ return type("_LoggedBuilder", (_ExtendedBuilder,), namespace)
1612
+
1613
+
1614
+ def _builder(val, description="", kind=None, expected=None, logger=None):
1615
+ """Internal helper to build a new `AssertionBuilder` instance and glue on any extension methods.
1616
+
1617
+ Function extensions already live on `_ExtendedBuilder`; only non-function callables (which the
1618
+ descriptor protocol cannot bind) still need per-instance grafting here. Grafted in place rather than through
1619
+ `_grafted()`, whose call cost `assert_that()` 3%.
1620
+ """
1621
+ builder = _made_builder(val, description, kind, expected, logger)
1622
+ if _extensions:
1623
+ for name, func in _extensions.items():
1624
+ meth = types.MethodType(func, builder)
1625
+ setattr(builder, name, meth)
1626
+ return builder
1627
+
1628
+
1629
+ def _grafted(builder):
1630
+ """*builder* with every non-function extension bound to it."""
1631
+ if _extensions:
1632
+ for name, func in _extensions.items():
1633
+ meth = types.MethodType(func, builder)
1634
+ setattr(builder, name, meth)
1635
+ return builder
1636
+
1637
+
1638
+ def _builder_like(parent, val, description, kind, expected, logger):
1639
+ """A pivot of *parent* while a block is open: observed exactly when *parent* is, into the entry it reports to.
1640
+
1641
+ A pivot made through `_builder()` asked the context that made it, so a builder made before its block took an
1642
+ observed pivot inside it, and one made in a closed entry handed its pivot to the entry open now.
1643
+ """
1644
+ observed = _passes.logged_class
1645
+ if observed is None or type(parent) is not observed:
1646
+ return _grafted(_ExtendedBuilder(val, description, kind, expected, logger))
1647
+ made = observed(val, description, kind, expected, logger)
1648
+ made._reports_to = parent._reports_to
1649
+ return _grafted(made)
1650
+
1651
+
1652
+ class WarningLoggingAdapter(_LoggerAdapter):
1653
+ """Logging adapter to unwind the stack to get the correct callee filename and line number."""
1654
+
1655
+ def process(self, msg: Any, kwargs: Any) -> tuple[Any, Any]:
1656
+ caller = _caller_location()
1657
+ if caller is None:
1658
+ return msg, kwargs
1659
+ filename, lineno = caller
1660
+ return f"[{os.path.basename(filename)}:{lineno}]: {msg}", kwargs
1661
+
1662
+
1663
+ class _Subscribed:
1664
+ """The callback `assert_warn(on_failure=...)` hands each failure to, carried where the logger is carried.
1665
+
1666
+ In the logger's place because every builder a chain goes on to is built with the logger the chain was given:
1667
+ a pivot, `extracting()` and a poll's own builder carry this too, and six pivots once dropped the logger.
1668
+ """
1669
+
1670
+ __slots__ = ("callback",)
1671
+
1672
+ def __init__(self, callback: Callable[[AssertionOutcome], object]) -> None:
1673
+ self.callback = callback
1674
+
1675
+ def warning(self, msg: str) -> None:
1676
+ """What a code of anyone's that logs through the chain's logger reaches: a record carrying its message."""
1677
+ self.callback(AssertionOutcome(message=msg, location=_caller_location()))
1678
+
1679
+
1680
+ _logger = logging.getLogger("assertpy")
1681
+ _handler = logging.StreamHandler(sys.stdout)
1682
+ _handler.setLevel(logging.WARNING)
1683
+ _format = logging.Formatter("%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
1684
+ _handler.setFormatter(_format)
1685
+ _logger.addHandler(_handler)
1686
+ _default_logger = WarningLoggingAdapter(_logger, None)
1687
+
1688
+
1689
+ # what the register in `_engine/_operations.py` means for the proxy; `{name}` so the reader finds their call
1690
+ _INSTEAD_OF_NEGATING: Final = {
1691
+ CONFIGURES: "negate the assertion that tests the expectation {name}() set instead",
1692
+ TRANSFORMS: "negate the assertion after {name}() instead",
1693
+ DESCRIBES: "call {name}() before not_ instead",
1694
+ POLLS: "assert the inverted condition instead",
1695
+ }
1696
+ _INSTEAD_OF_CHECKING: Final = {
1697
+ CONFIGURES: "ask check() for the verdict of the assertion that tests the expectation {name}() set",
1698
+ TRANSFORMS: "ask check() for the verdict of the assertion after {name}()",
1699
+ DESCRIBES: "call {name}() before check() instead",
1700
+ POLLS: "a poll delivers its own failure, so there is no verdict to hand back",
1701
+ }
1702
+
1703
+
1704
+ # the proxies themselves, reached the same way and not operations at all
1705
+ _PROXY_ENTRIES: Final = {
1706
+ "check": "check() cannot be negated; call check().not_ before the assertion instead",
1707
+ "not_": "two negations cancel; drop both instead of writing not_.not_",
1708
+ }
1709
+
1710
+
1711
+ def _is_still_ours(target: object, name: str) -> bool:
1712
+ """Whether *name* on *target* is this library's own operation rather than an extension's.
1713
+
1714
+ Asked of the object rather than of a global register, because the two ways an extension is applied
1715
+ have different lifetimes. A plain function is set on `_ExtendedBuilder` and every instance sees it
1716
+ at once, but a non-function callable is grafted per instance at construction, so a builder made
1717
+ before the extension was registered keeps the built-in and one made before it was removed keeps the
1718
+ override. Reading the registry instead let the first of those skip the refusal and then fail with
1719
+ `CollectionMixin.mapped() missing 1 required positional argument`.
1720
+ """
1721
+ return name not in vars(target) and name not in vars(_ExtendedBuilder)
1722
+
1723
+
1724
+ def _refuse_without_a_verdict(target: object, name: str, proxy: str, remedies: dict[str, str]) -> None:
1725
+ """Raise for an operation reached through a proxy that has nothing to do with it.
1726
+
1727
+ Both proxies exist to work on a verdict, so an operation that reaches none is a mistake in the
1728
+ call rather than something to invert or report. Silently accepting one is worse than it sounds:
1729
+ `check()` answered `passed=True` for a pivot, which reads as an assertion that ran and held.
1730
+ """
1731
+ category = WITHOUT_A_VERDICT.get(name)
1732
+ # an extension over the name is somebody else's method, so the register's reason to refuse no longer applies
1733
+ if category is None or not _is_still_ours(target, name):
1734
+ return
1735
+ remedy = remedies[category].format(name=name)
1736
+ raise TypeError(f"{name}() {WHAT_IT_DOES[category]}, so it cannot be {proxy}; {remedy}")
1737
+
1738
+
1739
+ class NegatedBuilder(Generic[_S]):
1740
+ """Proxy that inverts the next assertion. Created by ``assert_that(val).not_``.
1741
+
1742
+ Generic over the assertion type it was reached from, so inverting a step returns the view the chain
1743
+ had before ``.not_``: ``assert_that("x").not_.starts_with("y")`` stays a string assertion rather
1744
+ than collapsing to the untyped builder and letting a numeric assertion follow it, and a negated
1745
+ narrowing narrows nothing.
1746
+ """
1747
+
1748
+ if TYPE_CHECKING:
1749
+ # declared because a checker prefers it over `__getattr__`, which would type this as the negated method
1750
+ val: Any
1751
+ description: str
1752
+ kind: str | None
1753
+ expected: type[BaseException] | ExceptionInfo | None
1754
+ logger: WarningLogger
1755
+
1756
+ def __init__(self, builder: AssertionBuilder[Any]) -> None:
1757
+ self._builder = builder
1758
+
1759
+ def __getattr__(self, name: str) -> Callable[..., _S]:
1760
+ _refuse_without_a_verdict(self._builder, name, "negated with not_", _INSTEAD_OF_NEGATING)
1761
+ # an extension over `check` or `not_` is somebody else's method, so it is not ours to refuse
1762
+ if name in _PROXY_ENTRIES and _is_still_ours(self._builder, name):
1763
+ raise TypeError(_PROXY_ENTRIES[name])
1764
+ attr = getattr(self._builder, name)
1765
+ if name in HANDS_THE_SUBJECT_BACK or not callable(attr):
1766
+ return attr
1767
+
1768
+ def _negated(*args: object, **kwargs: object) -> AssertionBuilder:
1769
+ if self._builder.kind == "soft":
1770
+ return self._negated_soft(name, attr, *args, **kwargs)
1771
+ if self._builder.kind == "warn":
1772
+ return self._negated_warn(name, attr, *args, **kwargs)
1773
+ if self._builder.kind == "check":
1774
+ return self._negated_check(name, attr, *args, **kwargs)
1775
+ return self._negated_strict(name, attr, *args, **kwargs)
1776
+
1777
+ if _passes.open_blocks and type(self._builder) is _passes.logged_class:
1778
+ # chosen here, so a negation outside every block pays no frame of its own
1779
+ def _observed(*args: object, **kwargs: object) -> Any:
1780
+ return _observe(
1781
+ self._builder,
1782
+ lambda: _negated(*args, **kwargs),
1783
+ lambda: (name, args, kwargs, self._asked(attr, name, *args, **kwargs)),
1784
+ )
1785
+
1786
+ return _observed
1787
+
1788
+ # every branch hands back the wrapped builder, unsayable without `_builder: _S`, and an unbound TypeVar has none
1789
+ return _negated # ty: ignore[invalid-return-type] # see above
1790
+
1791
+ def _make_msg(self, name: str, *args: object, **kwargs: object) -> str:
1792
+ """The negated failure, naming the call that held when it should not have.
1793
+
1794
+ The arguments are in it because without them the message says which relation was asked for and
1795
+ not what it was asked about, which is the one thing the reader needs.
1796
+
1797
+ Rendered here rather than by `HelpersMixin._fmt_args_kwargs()`, which spells a keyword as
1798
+ ``'key': value``: that is the shape `when_called_with()` has printed since assertpy and its
1799
+ messages are pinned to it, but this line reads as a call and ``key=value`` is what a call looks
1800
+ like. Keyword order is the caller's, which Python preserves, so it is stable without sorting.
1801
+ """
1802
+ desc = f"[{self._builder.description}] " if self._builder.description else ""
1803
+ return f"{desc}Expected <{_capped(self._builder.val)}> to NOT satisfy: {_call_text(name, args, kwargs)}"
1804
+
1805
+ def _asked(self, attr: Callable[..., object], name: str, *args: object, **kwargs: object) -> Requirement:
1806
+ """What the negated call asked for, bound to the underlying assertion's parameter names.
1807
+
1808
+ Named here rather than read off the stack: a negated failure is composed after the assertion
1809
+ it inverted has already returned, so its frame is gone by then. Bound through the signature
1810
+ so the parameters read the same as on the positive path, where they come out of the frame.
1811
+ """
1812
+ return _bound_requirement(attr, name, args, kwargs, negated=True)
1813
+
1814
+ def _verdict(
1815
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
1816
+ ) -> AssertionOutcome | None:
1817
+ """What the underlying assertion decided, or ``None`` when it held.
1818
+
1819
+ Run in check mode, the one mode that hands a verdict back instead of delivering it. That is
1820
+ what separates a verdict from an accident: anything still raised by the call came out of the
1821
+ value or out of an assertion nested inside it, and inverting *that* turns a break into a pass.
1822
+
1823
+ Catching an exception class cannot make the distinction, whatever class is chosen. A comparator
1824
+ that asserts with this library raises this library's own failure, and it is still not the
1825
+ verdict of the assertion being negated. `_negated_check()` has always read the sink instead;
1826
+ the other three modes now do the same.
1827
+
1828
+ A failure on something the question presupposes is not a verdict either, and inverting it turns
1829
+ "the question has no answer" into a pass. When it is the run's first failure it is delivered
1830
+ before this returns, whatever failed after it, and handed back like any failure so no caller
1831
+ inverts it. One after an ordinary failure was never reached by the strict run, which stops at
1832
+ the first, so the answer stands. It keeps the positive assertion's message and values, takes the
1833
+ requirement every negated failure carries, and is recorded in turn for a negation this one runs
1834
+ inside of when it is that run's first failure too.
1835
+
1836
+ Both records are read where `_verdict_holder()` keeps them, so a negation asked of a pivot inside
1837
+ an extension's own verdict run reads and restores that run's records rather than a stray copy.
1838
+ Only a run inside another one records what it passes on: at the top nobody reads it afterwards.
1839
+ """
1840
+ builder = self._builder
1841
+ holder = builder if builder._answers_to is None else builder._answers_to
1842
+ kind, answering = builder.kind, builder._answering_another
1843
+ sink, enclosing = holder._check_sink, holder._unmet_prerequisite
1844
+ builder.kind = "check"
1845
+ builder._answering_another = True
1846
+ holder._check_sink = None
1847
+ holder._unmet_prerequisite = None
1848
+ try:
1849
+ attr(*args, **kwargs)
1850
+ decided, unmet = holder._check_sink, holder._unmet_prerequisite
1851
+ finally:
1852
+ builder.kind, builder._answering_another = kind, answering
1853
+ holder._check_sink, holder._unmet_prerequisite = sink, enclosing
1854
+ if kind != "check" and holder._run_pivots is not None:
1855
+ holder._release_pivots()
1856
+ if unmet is None:
1857
+ return decided
1858
+ passed_on = replace(unmet, requirement=_what_was_asked(builder, self._asked(attr, name, *args, **kwargs)))
1859
+ if kind == "check" and sink is None:
1860
+ holder._unmet_prerequisite = passed_on
1861
+ failure = builder._deliver(passed_on)
1862
+ if failure is not None:
1863
+ raise failure
1864
+ return passed_on
1865
+
1866
+ def _negated_strict(
1867
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
1868
+ ) -> AssertionBuilder:
1869
+ if self._verdict(name, attr, *args, **kwargs) is not None:
1870
+ return self._builder
1871
+ # composed here rather than by `error()`, which would prefix the description twice
1872
+ raise AssertionBuilder._failure(
1873
+ AssertionOutcome(
1874
+ message=self._make_msg(name, *args, **kwargs),
1875
+ actual=self._builder.val,
1876
+ requirement=_what_was_asked(self._builder, self._asked(attr, name, *args, **kwargs)),
1877
+ )
1878
+ )
1879
+
1880
+ def _negated_soft(
1881
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
1882
+ ) -> AssertionBuilder:
1883
+ if self._verdict(name, attr, *args, **kwargs) is not None:
1884
+ return self._builder
1885
+ msg = self._make_msg(name, *args, **kwargs)
1886
+ outcome = AssertionOutcome(
1887
+ message=msg,
1888
+ actual=self._builder.val,
1889
+ group=_soft_group.get(),
1890
+ location=_caller_location(),
1891
+ requirement=_what_was_asked(self._builder, self._asked(attr, name, *args, **kwargs)),
1892
+ )
1893
+ block = _collecting()
1894
+ if block is None:
1895
+ # the block has closed, which an outliving task does: appending puts the failure in a list nobody reads
1896
+ raise self._builder._failure(outcome)
1897
+ if self._builder._value_taint_reason is None:
1898
+ self._builder._value_taint_reason = msg
1899
+ if _passes.open_blocks:
1900
+ _passes.mark_failed()
1901
+ block.failures.append(outcome)
1902
+ return self._builder
1903
+
1904
+ def _negated_check(
1905
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
1906
+ ) -> AssertionBuilder:
1907
+ if self._verdict(name, attr, *args, **kwargs) is not None:
1908
+ return self._builder
1909
+ holder = self._builder._verdict_holder()
1910
+ if holder._check_sink is None:
1911
+ holder._check_sink = AssertionOutcome(
1912
+ message=self._make_msg(name, *args, **kwargs),
1913
+ actual=self._builder.val,
1914
+ requirement=_what_was_asked(self._builder, self._asked(attr, name, *args, **kwargs)),
1915
+ )
1916
+ return self._builder
1917
+
1918
+ def _negated_warn(
1919
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
1920
+ ) -> AssertionBuilder:
1921
+ if self._verdict(name, attr, *args, **kwargs) is not None:
1922
+ return self._builder
1923
+ self._builder._warned(
1924
+ AssertionOutcome(
1925
+ message=self._make_msg(name, *args, **kwargs),
1926
+ actual=self._builder.val,
1927
+ requirement=_what_was_asked(self._builder, self._asked(attr, name, *args, **kwargs)),
1928
+ )
1929
+ )
1930
+ return self._builder
1931
+
1932
+
1933
+ class CheckBuilder:
1934
+ """Proxy returned by [`check()`][assertpy.assertpy.AssertionBuilder.check].
1935
+
1936
+ Runs one assertion with the builder in verdict mode and hands back what it decided. The mode is
1937
+ put on and taken off around the call rather than held, so a builder that is also used normally
1938
+ afterwards is unaffected, and an assertion that raises for a bad argument still leaves it clean.
1939
+
1940
+ ``not_`` is proxied rather than refused, so a negated assertion can be asked for a verdict too.
1941
+ The value is handed straight back under ``val`` and ``value``, callable or not, and so is anything
1942
+ else that is not callable, ``description`` among them.
1943
+ """
1944
+
1945
+ if TYPE_CHECKING:
1946
+ # declared because a checker prefers it over `__getattr__`, which types every name as a verdict call
1947
+ val: Any
1948
+ description: str
1949
+
1950
+ @property
1951
+ def not_(self) -> CheckBuilder: ...
1952
+
1953
+ def __init__(self, target: object, builder: AssertionBuilder[Any]) -> None:
1954
+ # two references: `not_` moves the target to the negation proxy while the mode stays underneath
1955
+ self._target = target
1956
+ self._builder = builder
1957
+
1958
+ def __getattr__(self, name: str) -> Callable[..., AssertionOutcome]:
1959
+ _refuse_without_a_verdict(self._builder, name, "asked for a verdict", _INSTEAD_OF_CHECKING)
1960
+ if name == "check" and _is_still_ours(self._builder, name):
1961
+ raise TypeError("check() is already the verdict proxy; one check() is enough")
1962
+ attr = getattr(self._target, name)
1963
+ if name in HANDS_THE_SUBJECT_BACK:
1964
+ return attr
1965
+ if isinstance(attr, NegatedBuilder):
1966
+ # ty: ignore[invalid-return-type] # declared above as a proxy; this is the path that cannot be described
1967
+ return CheckBuilder(attr, self._builder)
1968
+ if not callable(attr):
1969
+ return attr
1970
+
1971
+ def _checked(*args: object, **kwargs: object) -> AssertionOutcome:
1972
+ # the verdict is answered here, so one this runs inside keeps its own failure and prerequisite
1973
+ builder = self._builder
1974
+ holder = builder if builder._answers_to is None else builder._answers_to
1975
+ kind, sink, unmet = builder.kind, holder._check_sink, holder._unmet_prerequisite
1976
+ builder.kind = "check"
1977
+ holder._check_sink = None
1978
+ try:
1979
+ attr(*args, **kwargs)
1980
+ failure = holder._check_sink
1981
+ finally:
1982
+ builder.kind, holder._check_sink, holder._unmet_prerequisite = kind, sink, unmet
1983
+ if kind != "check" and holder._run_pivots is not None:
1984
+ holder._release_pivots()
1985
+ if failure is not None:
1986
+ return failure
1987
+ return AssertionOutcome(passed=True, actual=builder.val)
1988
+
1989
+ return _checked
1990
+
1991
+
1992
+ class AssertionBuilder(
1993
+ StringMixin,
1994
+ SnapshotMixin,
1995
+ NumericMixin,
1996
+ JsonMixin,
1997
+ HttpMixin,
1998
+ HelpersMixin,
1999
+ FileMixin,
2000
+ ExtractingMixin,
2001
+ ExceptionMixin,
2002
+ WarningMixin,
2003
+ DynamicMixin,
2004
+ DictMixin,
2005
+ DateMixin,
2006
+ ContainsMixin,
2007
+ CollectionMixin,
2008
+ BytesMixin,
2009
+ DataFrameMixin,
2010
+ BaseMixin,
2011
+ Generic[_T],
2012
+ ):
2013
+ """The main assertion class. Never call the constructor directly, always use the
2014
+ [`assert_that()`][assertpy.assertpy.assert_that] helper instead. Or if you just want warning messages, use the
2015
+ `assert_warn()` helper.
2016
+
2017
+ Args:
2018
+ val: the value to be tested (aka the actual value)
2019
+ description (str, optional): the extra error message description. Defaults to ``''``
2020
+ (aka empty string)
2021
+ kind (str, optional): the kind of assertions, one of ``None``, ``soft``, or ``warn``.
2022
+ Defaults to ``None``
2023
+ expected (Error, optional): the expected exception. Defaults to ``None``
2024
+ logger (Logger, optional): the logger for warning messages. Defaults to ``None``
2025
+ """
2026
+
2027
+ def __init__(
2028
+ self,
2029
+ val: Any,
2030
+ description: str = "",
2031
+ kind: str | None = None,
2032
+ expected: type[BaseException] | ExceptionInfo | None = None,
2033
+ logger: WarningLogger | None = None,
2034
+ ) -> None:
2035
+ """Never call this constructor directly."""
2036
+ self.val = val
2037
+ self.description = description
2038
+ self.kind = kind
2039
+ self.expected = expected
2040
+ self.logger: WarningLogger = logger or _default_logger
2041
+ self._not_expected = False
2042
+ self._expected_warning = None
2043
+ self._return_value = _UNSET
2044
+ self._raised_exception = _UNSET
2045
+ # `.value` refuses an unverified value and names the root cause rather than breaking its type in silence
2046
+ self._value_taint_reason: str | None = None
2047
+ self._value_origin: str | None = None
2048
+ # where a failure lands while `check()` has this builder in verdict mode
2049
+ self._check_sink: AssertionOutcome | None = None
2050
+
2051
+ @property
2052
+ def not_(self) -> NegatedBuilder[Self]:
2053
+ """Invert the next assertion in the chain."""
2054
+ return NegatedBuilder(self)
2055
+
2056
+ def check(self) -> _CheckAnyValue[_T]:
2057
+ """Run the next assertion for its verdict instead of for its failure.
2058
+
2059
+ The assertion does not raise, collect or log. It returns an
2060
+ [`AssertionOutcome`][assertpy.outcome.AssertionOutcome], truthy when it held and carrying the
2061
+ failure message, values and diff when it did not.
2062
+
2063
+ For asking a question about a value. An assertion states a requirement, and a test that stops
2064
+ at the first unmet one is the point; this is for the cases that are not that, like branching on
2065
+ a precondition, or reporting a check into a system that is not pytest.
2066
+
2067
+ A bad argument still raises. ``TypeError`` and ``ValueError`` mean the call itself is wrong,
2068
+ which is not a verdict about the value and would be silenced by returning one.
2069
+
2070
+ An assertion or an extension that fails more than once answers with its first failure, the one
2071
+ it raises outside ``check()``.
2072
+
2073
+ Examples:
2074
+ Usage:
2075
+
2076
+ outcome = assert_that(response).check().is_equal_to(expected)
2077
+ if not outcome:
2078
+ logger.warning(outcome.message)
2079
+
2080
+ assert_that(5).check().is_positive().passed # True
2081
+ assert_that(5).check().not_.is_positive().passed # False
2082
+ """
2083
+ # the proxy resolves every name through `__getattr__`: the twin is the shape, `CheckBuilder` is the thing
2084
+ return cast("_CheckAnyValue[_T]", CheckBuilder(self, self))
2085
+
2086
+ @property
2087
+ def value(self) -> _T:
2088
+ """The value under test, returned as-is for typed extract-and-continue.
2089
+
2090
+ Ends a chain by handing the checked value back, so a test can keep using it after the
2091
+ assertions passed. For object- and union-typed values the static type is refined by the
2092
+ narrowing assertions along the way:
2093
+ [`is_not_none()`][assertpy.base.BaseMixin.is_not_none] removes ``None`` and
2094
+ [`is_instance_of()`][assertpy.base.BaseMixin.is_instance_of] narrows to the checked class, so
2095
+ no ``cast()`` or bare ``assert`` is needed to satisfy a type checker.
2096
+
2097
+ ``value`` is a strict-mode extraction: it hands the value back only when *every* assertion on
2098
+ it passed. If one failed under
2099
+ [`soft_assertions()`][assertpy.assertpy.soft_assertions] or
2100
+ [`assert_warn()`][assertpy.assertpy.assert_warn] - where failures are collected, not raised -
2101
+ reading ``value`` raises ``TypeError`` instead of returning an unverified value. Read it in
2102
+ strict mode, or after the soft block has closed.
2103
+
2104
+ The taint is per-value, not per-chain. A value-changing pivot
2105
+ ([`extracting()`][assertpy.extracting.ExtractingMixin.extracting],
2106
+ [`first()`][assertpy.collection.CollectionMixin.first],
2107
+ [`decoded_as()`][assertpy.bytes_mixin.BytesMixin.decoded_as], ...) starts a *new* value with a
2108
+ fresh guard and validates its own input, so a pivot never reaches ``.value`` with a value
2109
+ derived from a failed assertion - it raises in the pivot first.
2110
+
2111
+ Examples:
2112
+ Usage:
2113
+
2114
+ order: Order | None = repo.find_order(42)
2115
+ paid = assert_that(order).is_not_none().is_instance_of(PaidOrder).value
2116
+ paid.refund() # statically typed as PaidOrder
2117
+
2118
+ Returns:
2119
+ object: the original value under test (never a copy)
2120
+
2121
+ Raises:
2122
+ TypeError: if an assertion on this chain failed under ``soft_assertions()`` or
2123
+ ``assert_warn()``, so the value cannot be trusted to match its narrowed type; the
2124
+ message carries the underlying (root) failure so its cause is not lost
2125
+ """
2126
+ if self._value_taint_reason is not None:
2127
+ raise TypeError(
2128
+ "cannot extract .value: the underlying assertion failed under soft or warn mode - "
2129
+ f"{self._value_taint_reason} (read .value in strict mode, or after the soft-assertions block)"
2130
+ )
2131
+ return self.val
2132
+
2133
+ if TYPE_CHECKING:
2134
+ # narrowing declarations for checkers only; the runtime behaviour lives in `BaseMixin`
2135
+
2136
+ @overload
2137
+ def is_not_none(self: AssertionBuilder[_U | None]) -> AssertionBuilder[_U]: ...
2138
+ @overload
2139
+ def is_not_none(self) -> Self: ...
2140
+ def is_not_none(self) -> Any: ...
2141
+
2142
+ # the second rung takes a tuple, and a union under checkers reading it as `UnionType`; it also keeps conformance
2143
+ @overload
2144
+ def is_instance_of(self, some_class: tuple[type[_U], type[_U2]]) -> AssertionBuilder[_U | _U2]: ...
2145
+ @overload
2146
+ def is_instance_of(
2147
+ self, some_class: tuple[type[_U], type[_U2], type[_U3]]
2148
+ ) -> AssertionBuilder[_U | _U2 | _U3]: ...
2149
+ @overload
2150
+ def is_instance_of(self, some_class: type[_U]) -> AssertionBuilder[_U]: ...
2151
+ @overload
2152
+ def is_instance_of(self, some_class: ClassInfo) -> Self: ...
2153
+ def is_instance_of(self, some_class: Any) -> Any: ...
2154
+
2155
+ @overload
2156
+ def is_instance_of_any(self, first: type[_U], second: type[_U2], /) -> AssertionBuilder[_U | _U2]: ...
2157
+ @overload
2158
+ def is_instance_of_any(
2159
+ self, first: type[_U], second: type[_U2], third: type[_U3], /
2160
+ ) -> AssertionBuilder[_U | _U2 | _U3]: ...
2161
+ @overload
2162
+ def is_instance_of_any(self, *some_classes: ClassInfo) -> Self: ...
2163
+ def is_instance_of_any(self, *some_classes: Any) -> Any: ...
2164
+
2165
+ # the pivots return `self.builder(<element>)` where `CollectionMixin` says `-> Self`; `_T` is invariant
2166
+ @overload
2167
+ def first(self: AssertionBuilder[Mapping[_K, _V]]) -> AssertionBuilder[_K]: ...
2168
+ @overload
2169
+ def first(self: _ElementSource[_E]) -> AssertionBuilder[_E]: ...
2170
+ @overload
2171
+ def first(self) -> Self: ...
2172
+ def first(self) -> Any: ...
2173
+
2174
+ @overload
2175
+ def last(self: AssertionBuilder[Mapping[_K, _V]]) -> AssertionBuilder[_K]: ...
2176
+ @overload
2177
+ def last(self: _ElementSource[_E]) -> AssertionBuilder[_E]: ...
2178
+ @overload
2179
+ def last(self) -> Self: ...
2180
+ def last(self) -> Any: ...
2181
+
2182
+ @overload
2183
+ def element(self: AssertionBuilder[Mapping[_K, _V]], index: SupportsIndex) -> AssertionBuilder[_K]: ...
2184
+ @overload
2185
+ def element(self: _ElementSource[_E], index: SupportsIndex) -> AssertionBuilder[_E]: ...
2186
+ @overload
2187
+ def element(self, index: SupportsIndex) -> Self: ...
2188
+ def element(self, index: SupportsIndex) -> Any: ...
2189
+
2190
+ # `mapped()` builds a `list` whatever it was given, so `-> Self` was wrong about the container too and
2191
+ # refused working code. Its boundary: a named function binds `_R` under both checkers, a lambda binds it
2192
+ # under ty and comes back `Any` under mypy, losing precision rather than rejecting. `filtered_on()` and
2193
+ # `flat_mapped()` stay undeclared, measured: against `Matcher[_E] | Callable[[_E], object]` a lambda binds
2194
+ # nothing, so `filtered_on()` was `Unknown` under ty and `flat_mapped()` resolved there, and both stayed
2195
+ # `Any` under mypy. One answer out of three checkers is the bar and neither reaches it
2196
+ @overload
2197
+ def mapped(self: _ElementSource[_E], func: Callable[[_E], _R]) -> AssertionBuilder[list[_R]]: ...
2198
+ @overload
2199
+ def mapped(self, func: Callable[..., Any]) -> Self: ...
2200
+ def mapped(self, func: Any) -> Any: # overload impl stub, never executed
2201
+ ...
2202
+
2203
+ @overload
2204
+ def single(self: AssertionBuilder[Mapping[_K, _V]]) -> AssertionBuilder[_K]: ...
2205
+ @overload
2206
+ def single(self: _ElementSource[_E]) -> AssertionBuilder[_E]: ...
2207
+ @overload
2208
+ def single(self) -> Self: ...
2209
+ def single(self) -> Any: # overload impl stub, never executed
2210
+ ...
2211
+
2212
+ # a `TypeIs` predicate narrows the chain; PyCharm does not solve TypeVars through it yet (PY-89124)
2213
+ @overload
2214
+ def satisfies(self, matcher: Callable[[Any], TypeIs[_U]]) -> AssertionBuilder[_U]: ...
2215
+ @overload
2216
+ def satisfies(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
2217
+ def satisfies(self, matcher: Any) -> Any: # overload impl stub, never executed
2218
+ ...
2219
+
2220
+ def builder(
2221
+ self,
2222
+ val: Any,
2223
+ description: str = "",
2224
+ kind: str | None = None,
2225
+ expected: type[BaseException] | ExceptionInfo | None = None,
2226
+ logger: WarningLogger | None = None,
2227
+ origin: str | None = None,
2228
+ ) -> Any:
2229
+ """Helper to build a new `AssertionBuilder` instance. Use this only if not chaining to ``self``.
2230
+
2231
+ Args:
2232
+ val: the value to be tested (aka the actual value)
2233
+ description (str, optional): the extra error message description. Defaults to ``''``
2234
+ (aka empty string)
2235
+ kind (str, optional): the failure mode of the assertions, one of ``None`` (raise),
2236
+ ``soft`` (collect), or ``warn`` (log). Defaults to ``None``. Unrelated to
2237
+ [`DiffResult.kind`][assertpy.errors.DiffResult], which is a diff category
2238
+ expected (Error, optional): the expected exception. Defaults to ``None``
2239
+ logger (Logger, optional): the logger for warning messages. Defaults to ``None``
2240
+ """
2241
+ pivoted = _builder_like(self, val, description, kind, expected, logger) if _passes.open_blocks else None
2242
+ if pivoted is None:
2243
+ pivoted = _builder(val, description, kind, expected, logger)
2244
+ pivoted._value_origin = origin
2245
+ if kind == "check":
2246
+ holder = self._verdict_holder()
2247
+ pivoted._answers_to = holder
2248
+ pivoted._answering_another = self._answering_another
2249
+ if holder._run_pivots is None:
2250
+ holder._run_pivots = []
2251
+ holder._run_pivots.append(pivoted)
2252
+ # kept across the pivot, and `is not None` not `or`, since a `requests.Response` is falsey for every 4xx
2253
+ pivoted._response = self._response if self._response is not None else response_of(self.val)
2254
+ return pivoted
2255
+
2256
+ def error(
2257
+ self,
2258
+ msg: str,
2259
+ *,
2260
+ actual: Any = MISSING,
2261
+ expected: Any = MISSING,
2262
+ diff: DiffResult | None = None,
2263
+ trace: PollTrace | None = None,
2264
+ requirement: Requirement | None = None,
2265
+ suppress_context: bool = False,
2266
+ ) -> Self:
2267
+ """Helper to raise an ``AssertionError`` with the given message.
2268
+
2269
+ If an error description is set by [`described_as()`][assertpy.base.BaseMixin.described_as], then that
2270
+ description is prepended to the error message.
2271
+
2272
+ Always raises [`AssertionFailure`][assertpy.errors.AssertionFailure], which is an
2273
+ ``AssertionError``. Structured data (``actual``, ``expected``, ``diff``) is carried on it when
2274
+ given, and the class is the same either way, which is why nothing reads the class to learn who
2275
+ raised it: `NegatedBuilder._verdict()` asks in check mode instead.
2276
+
2277
+ Args:
2278
+ msg: the error message
2279
+ actual: the actual value (for structured error reporting)
2280
+ expected: the expected value (for structured error reporting)
2281
+ diff: a [`DiffResult`][assertpy.errors.DiffResult] instance (for structured error reporting)
2282
+ trace: a [`PollTrace`][assertpy.errors.PollTrace] from a poll that timed out
2283
+ requirement: what was asked, for a caller the stack cannot be read from. A negated
2284
+ assertion, a poll that timed out and a dynamic ``has_`` assertion each compose their
2285
+ failure after or beside the frame that named the operation, so they say it here.
2286
+ suppress_context: raise ``from None``, dropping the exception currently being handled from
2287
+ the traceback. Pass it when the caught exception is your own plumbing and its text is
2288
+ already folded into ``msg``, so the reader is not shown the same failure twice. Leave
2289
+ it alone when the caught exception is the caller's, which is context they want.
2290
+
2291
+ Raises:
2292
+ AssertionFailure: unless ``kind`` is ``warn``, ``soft`` or ``check``, which log it, collect
2293
+ it and record it respectively.
2294
+
2295
+ Returns:
2296
+ AssertionBuilder: this instance, to chain the next assertion, whenever the failure was
2297
+ delivered some other way than by raising.
2298
+ """
2299
+ # taken ahead of anything that can raise, so a pair never outlives the failure it was set for
2300
+ compared, self._compared = self._compared, None
2301
+ failure = self._deliver(
2302
+ self._compose(msg, actual=actual, expected=expected, diff=diff, trace=trace, requirement=requirement),
2303
+ compared,
2304
+ )
2305
+ if failure is None:
2306
+ return self
2307
+ failure._compared = compared
2308
+ # the raise stays here: a failure's traceback ends at `error`, three frames deep, pinned in test_traceback.py
2309
+ if suppress_context:
2310
+ raise failure from None
2311
+ raise failure
2312
+
2313
+ def _unmet(
2314
+ self,
2315
+ msg: str,
2316
+ *,
2317
+ expected: Any = MISSING,
2318
+ requirement: Requirement | None = None,
2319
+ suppress_context: bool = False,
2320
+ ) -> None:
2321
+ """Deliver *msg* as a failure of something the question presupposes rather than as its answer.
2322
+
2323
+ Delivered like any failure, with the keywords `error()` takes. What differs is under `not_`, whose
2324
+ verdict run is in check mode: recorded when it is the run's first failure, it is what
2325
+ `NegatedBuilder._verdict()` delivers as it stands.
2326
+ """
2327
+ holder = self._verdict_holder()
2328
+ first = holder._check_sink is None
2329
+ self.error(msg, expected=expected, requirement=requirement, suppress_context=suppress_context)
2330
+ if first:
2331
+ holder._unmet_prerequisite = holder._check_sink
2332
+
2333
+ def _verdict_holder(self) -> AssertionBuilder[Any]:
2334
+ """The builder whose check-mode sink this one's failures land in: itself, unless it was pivoted to
2335
+ during another builder's verdict run, whose failures they then are."""
2336
+ return self if self._answers_to is None else self._answers_to
2337
+
2338
+ def _release_pivots(self) -> None:
2339
+ """Hand the pivots made during the verdict run that just ended the mode this builder is back in.
2340
+
2341
+ Kept in check mode, a pivot an extension held on to after the run swallowed every later failure.
2342
+ Called by the outermost run only, since a run inside another still belongs to that one.
2343
+ """
2344
+ made, self._run_pivots = self._run_pivots, None
2345
+ for pivot in made or ():
2346
+ pivot.kind, pivot._answers_to, pivot._answering_another = self.kind, None, self._answering_another
2347
+
2348
+ def _compose(
2349
+ self,
2350
+ msg: str,
2351
+ *,
2352
+ actual: object,
2353
+ expected: object,
2354
+ diff: DiffResult | None,
2355
+ trace: PollTrace | None,
2356
+ requirement: Requirement | None = None,
2357
+ ) -> AssertionOutcome:
2358
+ """Build the failure record. Decides nothing about what happens to it."""
2359
+ # filled here rather than per call site, which is what puts it on all 163 failures instead of 34
2360
+ provided = actual is not MISSING
2361
+ out = f"{f'[{self.description}] ' if len(self.description) > 0 else ''}{msg}"
2362
+ if self._value_origin and not len(self.val):
2363
+ # an empty derived value carries no context of its own, so name the step that produced it
2364
+ out = f"{out} The value is empty because {self._value_origin}."
2365
+ hint = _hints.diagnose(
2366
+ diff, actual, expected, identity=self._equality_comparison, comparators=self._comparators_took_part
2367
+ )
2368
+ # a message quoting a failure that already carries the hint printed it twice, the poll timeout among them
2369
+ if hint is not None and hint not in out:
2370
+ # on its own line, so the original message stays a prefix and a `match=` written against it keeps working
2371
+ out = f"{out}\n{hint}"
2372
+ places = _hints.placed(diff)
2373
+ if places is not None and places not in out:
2374
+ out = f"{out}\n{places}"
2375
+ response = self._response if self._response is not None else response_of(self.val)
2376
+ note = response_note(response) if response is not None else None
2377
+ if note is not None:
2378
+ # last, under the explanation: it says where the value came from, not why it differs
2379
+ out = f"{out}\n{note}"
2380
+ return AssertionOutcome(
2381
+ message=out,
2382
+ actual=actual if provided else self.val,
2383
+ actual_provided=provided,
2384
+ expected=expected,
2385
+ diff=diff,
2386
+ trace=trace,
2387
+ hint=hint,
2388
+ requirement=_what_was_asked(self, requirement),
2389
+ )
2390
+
2391
+ def _deliver(
2392
+ self, outcome: AssertionOutcome, compared: tuple[object, object] | None = None
2393
+ ) -> AssertionFailure | None:
2394
+ """Act on a composed failure according to the builder's mode.
2395
+
2396
+ Returns the exception the caller should raise, or ``None`` when the failure was collected or
2397
+ logged instead. Raising is left to `error()` so the traceback keeps its shape. *compared* is the pair
2398
+ an equality held against each other, kept on a failure a soft block collects as a raised one keeps it.
2399
+ """
2400
+ if self.kind == "warn":
2401
+ self._warned(outcome)
2402
+ return None
2403
+ if self.kind == "soft":
2404
+ block = _collecting()
2405
+ if block is None:
2406
+ # the block has closed, which an outliving task does: appending puts the failure in a list nobody reads
2407
+ return self._failure(outcome)
2408
+ if self._value_taint_reason is None:
2409
+ self._value_taint_reason = outcome.message
2410
+ collected = replace(outcome, group=_soft_group.get(), location=_caller_location())
2411
+ if compared is not None:
2412
+ block.compared[id(collected)] = compared
2413
+ if _passes.open_blocks:
2414
+ _passes.mark_failed()
2415
+ block.failures.append(collected)
2416
+ return None
2417
+ if self.kind == "check":
2418
+ # no taint, as a verdict was asked for and not asserted, and the first failure is it: strict stops there
2419
+ holder = self._verdict_holder()
2420
+ if holder._check_sink is None:
2421
+ holder._check_sink = outcome
2422
+ return None
2423
+ return self._failure(outcome)
2424
+
2425
+ def _warned(self, outcome: AssertionOutcome) -> None:
2426
+ """Hand a warn-mode failure on: as its record to an `on_failure` callback, or as text to the logger.
2427
+
2428
+ The value is marked as unverified first, and the record is located first, so a callback that raises leaves
2429
+ both as a logger that raises does. Every warn-mode failure comes through here, negations included.
2430
+ """
2431
+ if self._value_taint_reason is None:
2432
+ self._value_taint_reason = outcome.message
2433
+ if _passes.open_blocks:
2434
+ _passes.mark_failed()
2435
+ logger = self.logger
2436
+ if isinstance(logger, _Subscribed):
2437
+ logger.callback(replace(outcome, location=_caller_location()))
2438
+ return
2439
+ detail = _indented_diff(outcome.diff, " ")
2440
+ logger.warning("\n".join([outcome.message, *detail]) if detail else outcome.message)
2441
+
2442
+ @staticmethod
2443
+ def _failure(outcome: AssertionOutcome) -> AssertionFailure:
2444
+ """Build the exception for a composed failure.
2445
+
2446
+ One class for every failure. The older split raised a bare `AssertionError` whenever the
2447
+ assertion had named no expected value and built no diff, which is most of them, so whether a
2448
+ caught failure carried structure at all depended on which assertion had failed.
2449
+
2450
+ Separate from `_deliver` for the one caller that composes its own message: `NegatedBuilder`
2451
+ inverts by catching, so its own failure is the case where nothing was caught.
2452
+ """
2453
+ failure = AssertionFailure(
2454
+ outcome.message,
2455
+ actual=outcome.actual,
2456
+ expected=None if outcome.expected is MISSING else outcome.expected,
2457
+ diff=outcome.diff,
2458
+ requirement=outcome.requirement,
2459
+ )
2460
+ failure._outcome = outcome
2461
+ return failure
2462
+
2463
+ def eventually(
2464
+ self,
2465
+ *,
2466
+ timeout: float = 5.0,
2467
+ interval: float = 0.5,
2468
+ ignoring: type[Exception] | RetriedInfo = (),
2469
+ trace: bool = True,
2470
+ ) -> AsyncAssertionBuilder:
2471
+ """Switch to async polling mode for eventual-consistency assertions.
2472
+
2473
+ The current ``val`` must be a callable (sync or async). Returns an
2474
+ `AsyncAssertionBuilder` whose assertion
2475
+ methods are coroutines that poll ``val()`` until the assertion passes or
2476
+ ``timeout`` expires.
2477
+
2478
+ By default only a failing assertion is retried: any other exception raised by ``val()`` itself
2479
+ propagates immediately. An ``AssertionError`` from ``val()`` reaches the same place a failing
2480
+ assertion does and is retried the same way, which the timeout message names. A probe that
2481
+ signals "not ready yet" by raising something else (a connection refused while a service boots,
2482
+ a record not yet visible) can be retried too by listing those types in ``ignoring``.
2483
+
2484
+ Polling itself is always strict - retrying *requires* hard failures - but the final timeout
2485
+ failure honors the builder's mode: inside
2486
+ [`soft_assertions()`][assertpy.assertpy.soft_assertions] it is collected instead of raised,
2487
+ and under [`assert_warn()`][assertpy.assertpy.assert_warn] it is logged.
2488
+
2489
+ Args:
2490
+ timeout: maximum seconds to keep retrying (default ``5.0``). Infinity, or a number past the
2491
+ float range, retries until the assertion passes
2492
+ interval: seconds between retries (default ``0.5``), finite
2493
+ ignoring: an ``Exception`` subclass, or a union or a tuple of them, the polling loop retries instead of
2494
+ propagating (default: none)
2495
+ trace: record a [`PollTrace`][assertpy.errors.PollTrace] flight recorder attached to the
2496
+ timeout failure (default ``True``); pass ``False`` to skip recording, for tight
2497
+ polling loops where per-poll snapshots of a heavy probed value are too costly
2498
+
2499
+ Examples:
2500
+ Usage:
2501
+
2502
+ import asyncio
2503
+ from assertpy import assert_that
2504
+
2505
+ counter = {"n": 0}
2506
+
2507
+ def get_count():
2508
+ counter["n"] += 1
2509
+ return counter["n"]
2510
+
2511
+ asyncio.run(
2512
+ assert_that(get_count).eventually(timeout=2).is_equal_to(3)
2513
+ )
2514
+
2515
+ Retry a probe that raises while the system under test is not ready yet:
2516
+
2517
+ await assert_that(get_order).eventually(timeout=10, ignoring=ConnectionError).has_status("PAID")
2518
+
2519
+ # or configure fluently on the returned builder
2520
+ await assert_that(get_order).eventually().within(10).ignoring(ConnectionError).has_status("PAID")
2521
+
2522
+ Returns:
2523
+ AsyncAssertionBuilder: an async builder whose assertion methods are awaitable
2524
+
2525
+ Raises:
2526
+ TypeError: if ``val`` is not callable, if ``timeout`` or ``interval`` is not a real number or is
2527
+ a bool, or if ``ignoring`` contains anything that is not an ``Exception`` subclass
2528
+ ValueError: if ``timeout`` or ``interval`` is negative or NaN, or ``interval`` is infinite
2529
+ """
2530
+ if not callable(self.val):
2531
+ refuse(self.val, "callable, since eventually() polls it")
2532
+ return AsyncAssertionBuilder(
2533
+ self.val,
2534
+ builder_func=_builder,
2535
+ description=self.description,
2536
+ timeout=timeout,
2537
+ interval=interval,
2538
+ ignoring=_normalize_ignoring(ignoring),
2539
+ kind=self.kind,
2540
+ logger=self.logger,
2541
+ trace=trace,
2542
+ )
2543
+
2544
+ def eventually_sync(
2545
+ self,
2546
+ *,
2547
+ timeout: float = 5.0,
2548
+ interval: float = 0.5,
2549
+ ignoring: type[Exception] | RetriedInfo = (),
2550
+ trace: bool = True,
2551
+ ) -> SyncAssertionBuilder:
2552
+ """Switch to blocking polling mode for eventual-consistency assertions, without asyncio.
2553
+
2554
+ The synchronous sibling of [`eventually()`][assertpy.assertpy.AssertionBuilder.eventually]:
2555
+ the current ``val`` must be a sync callable, and the returned
2556
+ `SyncAssertionBuilder` exposes assertion methods
2557
+ that block the calling thread (via ``time.sleep``) while polling ``val()`` until the
2558
+ assertion passes or ``timeout`` expires - no event loop and no ``await`` needed. A probe
2559
+ that returns an awaitable raises ``TypeError``; poll async probes with ``eventually()``.
2560
+
2561
+ Retry, failure-mode, and diagnostics semantics are identical to ``eventually()``: only a
2562
+ failing assertion (or an exception type listed in ``ignoring``) is retried, the final
2563
+ timeout failure honors the builder's soft/warn mode, and it carries the same
2564
+ [`PollTrace`][assertpy.errors.PollTrace] flight recorder.
2565
+
2566
+ Args:
2567
+ timeout: maximum seconds to keep retrying (default ``5.0``). Infinity, or a number past the
2568
+ float range, retries until the assertion passes
2569
+ interval: seconds between retries (default ``0.5``), finite
2570
+ ignoring: an ``Exception`` subclass, or a union or a tuple of them, the polling loop retries instead of
2571
+ propagating (default: none)
2572
+ trace: record a [`PollTrace`][assertpy.errors.PollTrace] flight recorder attached to the
2573
+ timeout failure (default ``True``); pass ``False`` to skip recording, for tight
2574
+ polling loops where per-poll snapshots of a heavy probed value are too costly
2575
+
2576
+ Examples:
2577
+ Usage:
2578
+
2579
+ from assertpy import assert_that
2580
+
2581
+ counter = {"n": 0}
2582
+
2583
+ def get_count():
2584
+ counter["n"] += 1
2585
+ return counter["n"]
2586
+
2587
+ assert_that(get_count).eventually_sync(timeout=2, interval=0.1).is_equal_to(3)
2588
+
2589
+ Retry a probe that raises while the system under test is not ready yet:
2590
+
2591
+ assert_that(get_order).eventually_sync(timeout=10, ignoring=ConnectionError).has_status("PAID")
2592
+
2593
+ # or configure fluently on the returned builder
2594
+ assert_that(get_order).eventually_sync().within(10).ignoring(ConnectionError).has_status("PAID")
2595
+
2596
+ Returns:
2597
+ SyncAssertionBuilder: a blocking builder whose assertion methods poll on call
2598
+
2599
+ Raises:
2600
+ TypeError: if ``val`` is not callable, if ``timeout`` or ``interval`` is not a real number or is
2601
+ a bool, or if ``ignoring`` contains anything that is not an ``Exception`` subclass
2602
+ ValueError: if ``timeout`` or ``interval`` is negative or NaN, or ``interval`` is infinite
2603
+ """
2604
+ if not callable(self.val):
2605
+ refuse(self.val, "callable, since eventually_sync() polls it")
2606
+ return SyncAssertionBuilder(
2607
+ self.val,
2608
+ builder_func=_builder,
2609
+ description=self.description,
2610
+ timeout=timeout,
2611
+ interval=interval,
2612
+ ignoring=_normalize_ignoring(ignoring),
2613
+ kind=self.kind,
2614
+ logger=self.logger,
2615
+ trace=trace,
2616
+ )
2617
+
2618
+
2619
+ class _ExtendedBuilder(AssertionBuilder[Any]):
2620
+ """Host for user extensions: `add_extension()` installs plain functions here, so binding happens
2621
+ once at registration and `AssertionBuilder` itself stays pristine when an extension is removed."""
2622
+
2623
+
2624
+ _made_builder: Callable[..., Any] = _ExtendedBuilder
2625
+ """What `_builder()` makes: `_ExtendedBuilder`, or while any `log_passes()` block is open, the class it hands out."""
2626
+
2627
+
2628
+ _ASSERTS: Final = frozenset(
2629
+ name
2630
+ for name in dir(AssertionBuilder)
2631
+ if not name.startswith("_")
2632
+ and name not in NOT_AN_OPERATION
2633
+ and (name not in WITHOUT_A_VERDICT or name in ALSO_ASSERTS)
2634
+ ) | {"assert_conforms"}
2635
+ """The names `_requirement_of()` will answer with, derived rather than listed.
2636
+
2637
+ The complement of the register, plus the one assertion that is a module function and so has no frame
2638
+ with a builder in it. A hybrid such as `when_called_with()` belongs here: the register calls it a
2639
+ configurer because it hands a value back, and it reaches a verdict on the way.
2640
+ """