assertpy 2.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. assertpy/__init__.py +87 -0
  2. assertpy/_clustering.py +511 -0
  3. assertpy/_dangling.py +505 -0
  4. assertpy/_engine/__init__.py +1 -0
  5. assertpy/_engine/_builder_check_typing.py +1365 -0
  6. assertpy/_engine/_capable_typing.py +772 -0
  7. assertpy/_engine/_check_typing.py +753 -0
  8. assertpy/_engine/_compare.py +983 -0
  9. assertpy/_engine/_compat.py +38 -0
  10. assertpy/_engine/_contract.py +1926 -0
  11. assertpy/_engine/_diff.py +1058 -0
  12. assertpy/_engine/_equality.py +843 -0
  13. assertpy/_engine/_introspection.py +729 -0
  14. assertpy/_engine/_membership.py +522 -0
  15. assertpy/_engine/_mixin_base.py +258 -0
  16. assertpy/_engine/_negated_typing.py +2176 -0
  17. assertpy/_engine/_operations.py +75 -0
  18. assertpy/_engine/_ordering.py +884 -0
  19. assertpy/_engine/_pairing.py +70 -0
  20. assertpy/_engine/_path.py +158 -0
  21. assertpy/_engine/_poll_typing.py +6039 -0
  22. assertpy/_engine/_require.py +312 -0
  23. assertpy/_engine/_size.py +31 -0
  24. assertpy/_engine/_text.py +39 -0
  25. assertpy/_engine/_typing.py +1226 -0
  26. assertpy/_hints.py +1213 -0
  27. assertpy/_inline.py +484 -0
  28. assertpy/_matcher_impls.py +1876 -0
  29. assertpy/_passes.py +249 -0
  30. assertpy/_renamed.py +64 -0
  31. assertpy/_satisfies.py +835 -0
  32. assertpy/_snapshot_codec.py +290 -0
  33. assertpy/assertpy.py +2640 -0
  34. assertpy/async_assertions.py +864 -0
  35. assertpy/base.py +862 -0
  36. assertpy/behave_matchers.py +96 -0
  37. assertpy/bytes_mixin.py +158 -0
  38. assertpy/collection.py +528 -0
  39. assertpy/contains.py +1068 -0
  40. assertpy/dataframe.py +200 -0
  41. assertpy/date.py +327 -0
  42. assertpy/dict.py +249 -0
  43. assertpy/dynamic.py +222 -0
  44. assertpy/errors.py +782 -0
  45. assertpy/exception.py +838 -0
  46. assertpy/extracting.py +266 -0
  47. assertpy/file.py +324 -0
  48. assertpy/helpers.py +747 -0
  49. assertpy/http_mixin.py +218 -0
  50. assertpy/json_mixin.py +1265 -0
  51. assertpy/matchers.py +639 -0
  52. assertpy/numeric.py +696 -0
  53. assertpy/outcome.py +163 -0
  54. assertpy/py.typed +0 -0
  55. assertpy/pytest_plugin.py +1650 -0
  56. assertpy/snapshot.py +1031 -0
  57. assertpy/string.py +803 -0
  58. assertpy/warning.py +137 -0
  59. assertpy-2.35.0.dist-info/METADATA +324 -0
  60. assertpy-2.35.0.dist-info/RECORD +63 -0
  61. assertpy-2.35.0.dist-info/WHEEL +4 -0
  62. assertpy-2.35.0.dist-info/entry_points.txt +2 -0
  63. assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,312 @@
1
+ """One shape for every refusal of a wrong type, and one place that decides how it reads.
2
+
3
+ An assertion refuses a value or an argument in two situations, and until now each site worded its own:
4
+ ninety-three different phrasings across a hundred and fifty-nine sites, for about ten actual
5
+ situations. Ten of them said "not a number" and eleven said "not a string", differing in whether the
6
+ offending value appeared at all, whether its type did, and in what order. The reader could not learn
7
+ the format once, because there was no format.
8
+
9
+ The shape here is::
10
+
11
+ <subject> must be <expectation>, but was <value> (<type>)
12
+
13
+ `subject` is `val` for the value under assertion and `given <name> arg` for an argument, which is the
14
+ wording the package already used where it named the operand at all. The value is shown because that is
15
+ the question a reader actually has: not *that* the type was wrong, but *what arrived* instead. A
16
+ payload field that came back as the string ``"12"`` rather than the number ``12`` is invisible in
17
+ "val is not numeric" and obvious in ``but was <'12'> (str)``.
18
+
19
+ The type is printed next to the value even when `repr` already implies it. `'12'` and `12` are told
20
+ apart by their quotes, but `Decimal("1")`, a numpy scalar and any class with a hand-written `__repr__`
21
+ are not, and a rule with an exception is a rule nobody can rely on.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import decimal
27
+ import difflib
28
+ import functools
29
+ import inspect
30
+ import re
31
+ import types
32
+ from typing import Any, Final, NoReturn, TypeVar
33
+
34
+ from ..errors import _safe_repr, _truncated
35
+ from ._size import length_of
36
+
37
+ _T = TypeVar("_T")
38
+
39
+
40
+ def raised_on_purpose(excinfo: Any = None) -> bool:
41
+ """Whether pytest hides a frame of the module that names this as its ``__tracebackhide__``.
42
+
43
+ Only where the frame the exception was born in is a refusal of this library: one sets a name of its own,
44
+ ``_assertpy_refusal``, where it raises, and then no frame of the library says anything of the mistake.
45
+ Anything else keeps its frames, what the caller's code or another library raised, hidden by pytest's own
46
+ flag or not, and a defect of this one, since they are what locate it.
47
+ """
48
+ try:
49
+ last = excinfo.tb
50
+ while last.tb_next is not None:
51
+ last = last.tb_next
52
+ return last.tb_frame.f_locals.get("_assertpy_refusal") is True
53
+ except Exception:
54
+ return True
55
+
56
+
57
+ __tracebackhide__ = raised_on_purpose
58
+
59
+ # a refusal is read on one terminal line, so the cap is well below the one a diff row uses
60
+ _SHOWN = 60
61
+
62
+
63
+ # a type name can be as long as its author liked, and the refusal is still one line
64
+ _NAMED = 40
65
+ # `repr` output is arbitrary text: escape sequences repaint the terminal, and newlines turn one refusal into several
66
+ _CONTROL = {code: "\\x{:02x}".format(code) for code in [*range(0x20), 0x7F, 0x85]} | { # noqa: UP032 # an f-string cannot hold this escape
67
+ # the bidi overrides reorder the printed line without changing the string
68
+ code: "\\u{:04x}".format(code) # noqa: UP032 # same
69
+ for code in [0x2028, 0x2029, *range(0x202A, 0x2030), *range(0x2066, 0x206A)]
70
+ }
71
+
72
+
73
+ def _one_line(text: str) -> str:
74
+ """*text* with control characters spelled out, so a refusal stays one readable line."""
75
+ return text.translate(_CONTROL)
76
+
77
+
78
+ def _shown(value: object) -> str:
79
+ """The value and its type, both capped, both safe to print next to a traceback.
80
+
81
+ `_safe_repr` already answers `<unreprable X>` for a `__repr__` that raises, returns a non-string or
82
+ recurses. What it does not do is make the result printable: an object is free to return escape
83
+ sequences or newlines from `__repr__`, and a diagnostic that repaints the terminal or spreads over
84
+ four lines is worse than the mistake it reports.
85
+ """
86
+ # escaped before it is capped: capping first let sixty control characters render as two hundred and forty
87
+ shown = _truncated(_one_line(_safe_repr(value)), _SHOWN)
88
+ return f"<{shown}> ({_truncated(type(value).__name__, _NAMED)})"
89
+
90
+
91
+ def raised_inside(exc: BaseException) -> bool:
92
+ """Whether *exc* came out of somebody else's code rather than from the operation itself.
93
+
94
+ `len(42)` and `1 < "a"` are refusals by the operation: the traceback stops at the frame that tried
95
+ it. A `__len__` or a `__lt__` that raises `TypeError` of its own adds a frame, and that error is a
96
+ bug in the value being tested, not a wrong operand. Answering it with "val must be a sized object"
97
+ is a lie that sends the reader looking in the wrong file, so those are re-raised untouched.
98
+
99
+ A frame of `decimal` itself is the operation's own, where CPython was built without the C accelerator and
100
+ runs it in Python (`pure_decimal_code`): the same refusal raised in C adds no frame at all. Only when the
101
+ method first called runs on a real `Decimal` or `Context`, since a class of anybody's can borrow one.
102
+ """
103
+ traceback = exc.__traceback__
104
+ deeper = traceback.tb_next if traceback is not None else None
105
+ if deeper is None:
106
+ return False
107
+ operation = pure_decimal_code()
108
+ entry = deeper.tb_frame
109
+ if entry.f_code not in operation or not isinstance(entry.f_locals.get("self"), (decimal.Decimal, decimal.Context)):
110
+ return True
111
+ while deeper is not None:
112
+ if deeper.tb_frame.f_code not in operation:
113
+ return True
114
+ deeper = deeper.tb_next
115
+ return False
116
+
117
+
118
+ @functools.cache
119
+ def pure_decimal_code() -> frozenset[types.CodeType]:
120
+ """The code `decimal` runs in Python where CPython was built without its C accelerator, else nothing.
121
+
122
+ Read once from the namespace `Decimal.__add__` was defined in, nested code included: the functions and the
123
+ classes defined there, `Decimal` and `Context` among them, and not what it imported from elsewhere, such as
124
+ `collections.namedtuple`. That namespace is `_pydecimal`'s, whether `decimal` is that module itself, as on
125
+ 3.15, or re-exports its names, as below it, and it must hold the very `Decimal` in use.
126
+ """
127
+ added = inspect.getattr_static(decimal.Decimal, "__add__")
128
+ if not isinstance(added, types.FunctionType) or added.__globals__.get("Decimal") is not decimal.Decimal:
129
+ return frozenset()
130
+ namespace = added.__globals__
131
+ held = list(namespace.values())
132
+ for kind in [one for one in held if isinstance(one, type) and one.__module__ == namespace.get("__name__")]:
133
+ for member in vars(kind).values():
134
+ held += [getattr(member, "__func__", member)]
135
+ if isinstance(member, property):
136
+ held += [member.fget, member.fset, member.fdel]
137
+ pending = [
138
+ function.__code__
139
+ for function in held
140
+ if isinstance(function, types.FunctionType) and function.__globals__ is namespace
141
+ ]
142
+ found: set[types.CodeType] = set()
143
+ while pending:
144
+ code = pending.pop()
145
+ if code not in found:
146
+ found.add(code)
147
+ pending += [constant for constant in code.co_consts if isinstance(constant, types.CodeType)]
148
+ return frozenset(found)
149
+
150
+
151
+ def refuse(value: object, expectation: str, *, subject: str = "val", note: str = "") -> NoReturn:
152
+ """Raise the refusal for *value*, for a check the caller has already made, with *note* after it."""
153
+ message = f"{subject} must be {expectation}, but was {_shown(value)}{note}"
154
+ _assertpy_refusal = True
155
+ raise TypeError(message)
156
+
157
+
158
+ def require_type(
159
+ value: object, types: type[_T] | tuple[type[_T], ...], expectation: str, *, subject: str = "val"
160
+ ) -> _T:
161
+ """Refuse *value* unless it is an instance of *types*, which is what most of these checks are.
162
+
163
+ The value is handed back so a caller can bind it and keep the narrowing: a type checker cannot see
164
+ through a function that only raises, and the alternative at each site was a second `isinstance` or a
165
+ cast written purely for the checker.
166
+ """
167
+ if not isinstance(value, types):
168
+ refuse(value, expectation, subject=subject)
169
+ return value
170
+
171
+
172
+ def argument(name: str) -> str:
173
+ """The subject for an argument of the assertion, as opposed to the value under assertion."""
174
+ return f"given {name} arg"
175
+
176
+
177
+ def require_pattern(pattern: object) -> re.Pattern[str]:
178
+ """*pattern* compiled, refused unless it is a string or a pattern compiled from one, and not empty.
179
+
180
+ A pattern compiled from bytes is never found in a string, and an empty one is found in every string. A
181
+ string goes through ``re.compile`` and its cache, as ``re.search`` would take it. A compiled one comes back
182
+ as it is, flags and all.
183
+ """
184
+ text = pattern.pattern if isinstance(pattern, re.Pattern) else pattern
185
+ if not isinstance(text, str):
186
+ refuse(pattern, "a string or a pattern compiled from one", subject=argument("pattern"))
187
+ if len(text) == 0:
188
+ _assertpy_refusal = True
189
+ raise ValueError("given pattern arg must not be empty")
190
+ return pattern if isinstance(pattern, re.Pattern) else re.compile(text)
191
+
192
+
193
+ def sized_len(value: object, *, subject: str = "val") -> int:
194
+ """The length of *value*, refusing it in the shared shape when it has none.
195
+
196
+ Left to `len()` alone, the refusal reads "object of type 'int' has no len()": true, and about the
197
+ builtin rather than about the assertion, with no mention of which operand was wrong.
198
+ """
199
+ length = length_of(value)
200
+ if length is None:
201
+ refuse(value, "a sized object", subject=subject)
202
+ # a `__len__` that exists and still raises is a bug in the value, and it travels out as its author wrote it
203
+ return length
204
+
205
+
206
+ def reject_unknown_kwargs(kwargs: dict, known: frozenset, method: str) -> None:
207
+ """Raise on a keyword the method does not read, naming the closest one it does.
208
+
209
+ A misspelt option is the worst silent pass there is. ``is_equal_to(other, strict_type=True)``
210
+ returned green with the comparison never tightened, and nothing said so: no error, no warning, and
211
+ no type error either, because a ``**kwargs`` signature makes every spelling legal to a checker.
212
+ The reader is left certain they asserted something they did not.
213
+
214
+ The matcher spelling of the same option already fails loudly, since a real parameter list gets
215
+ this from the interpreter for free. This gives the ``**kwargs`` entry points the same manners.
216
+ """
217
+ unknown = sorted(set(kwargs) - known)
218
+ if not unknown:
219
+ return
220
+ named = []
221
+ for name in unknown:
222
+ # one suggestion, like the extraction hint: measured typos score ~0.9 and wrong neighbours ~0.65
223
+ close = difflib.get_close_matches(str(name), sorted(known), n=1)
224
+ named.append(f"{name!r}" + (f" (did you mean {close[0]!r}?)" if close else ""))
225
+ plural = "" if len(named) == 1 else "s"
226
+ message = f"{method}() got an unexpected keyword argument{plural} {', '.join(named)}"
227
+ _assertpy_refusal = True
228
+ raise TypeError(message)
229
+
230
+
231
+ class VerdictError(TypeError):
232
+ """A predicate handed back something that is not an answer: a mistake in the test, not a non-match.
233
+
234
+ Its own type because several assertions catch `TypeError` from a probe on purpose, reading it as
235
+ "this one does not match", which is right for a predicate that cannot judge some item and wrong for
236
+ one that was never asked: swallowed there, the refusal came back as an ordinary failed assertion.
237
+ """
238
+
239
+
240
+ class CoroutineVerdictError(VerdictError):
241
+ """A predicate handed back a coroutine, so nothing ever ran."""
242
+
243
+
244
+ class MatcherVerdictError(VerdictError):
245
+ """A predicate handed back a matcher, so nothing was ever asked of the value."""
246
+
247
+
248
+ _MATCHER_MEMBERS: Final = ("matches", "describe", "describe_mismatch")
249
+
250
+ NON_MATCHER_TYPES: Final = frozenset(
251
+ {int, float, bool, complex, str, bytes, bytearray, list, tuple, dict, set, frozenset, type(None)}
252
+ )
253
+ """Types no matcher is, which is what an ordinary answer is: one frozenset lookup instead of three reads."""
254
+
255
+
256
+ def _answers_like_a_matcher(answer: object) -> bool:
257
+ """Whether this object is a matcher by the only rule that matters: it has the three members.
258
+
259
+ Read statically, wherever a matcher keeps its members: on the type, in the instance dictionary or in
260
+ slots. Never through `getattr`, which runs a `__getattr__`, a property or a metaclass hook: a `Mock`
261
+ answers every name with something callable, and a hostile object could decide its own verdict.
262
+
263
+ The frozenset first because the static read costs 1.08 us against 0.11 us for a plain `getattr`, and
264
+ the answers that reach here are almost always an ordinary value of a builtin type.
265
+ """
266
+ if type(answer) in NON_MATCHER_TYPES:
267
+ return False
268
+ for member in _MATCHER_MEMBERS:
269
+ held = inspect.getattr_static(answer, member, None)
270
+ if isinstance(held, types.MemberDescriptorType):
271
+ # a slot: its own `__get__` reads the value, which is a C-level read and not their code
272
+ try:
273
+ held = held.__get__(answer)
274
+ except AttributeError:
275
+ return False # the slot was never filled
276
+ if not callable(held):
277
+ return False
278
+ return True
279
+
280
+
281
+ def verdict(answer: object, *, subject: str = "predicate") -> object:
282
+ """The answer a predicate gave, refusing what is not one rather than reading it as truth.
283
+
284
+ A coroutine object is truthy, so an `async def` predicate passed every assertion that reads one
285
+ without ever running: measured, ten of the thirteen places that take a callable went green on a
286
+ predicate that always answers `False`. A matcher is truthy the same way, so a factory that was
287
+ named but never called, or a predicate written in matcher style, passed on any value at all.
288
+
289
+ Asked of the answer rather than of the callable, so a lambda handing one back is caught too, and a
290
+ coroutine is closed before raising so the refusal is not followed by "was never awaited".
291
+ """
292
+ if answer is True or answer is False:
293
+ return answer
294
+ # the exact type, since `inspect.iscoroutine` and even `isinstance` read attributes off the answer,
295
+ # and a hostile `__getattribute__` then decides a failure that is not about it at all. Nothing
296
+ # subclasses a coroutine, so `is` asks the same question
297
+ if type(answer) is types.CoroutineType:
298
+ answer.close()
299
+ message = (
300
+ f"{subject} handed back a coroutine instead of an answer; assertions here are synchronous, "
301
+ "so await the call yourself and assert on what it returned"
302
+ )
303
+ _assertpy_refusal = True
304
+ raise CoroutineVerdictError(message)
305
+ if _answers_like_a_matcher(answer):
306
+ message = (
307
+ f"{subject} handed back a matcher instead of an answer; pass the matcher where one is taken, "
308
+ "or ask it about the value and answer with what it said"
309
+ )
310
+ _assertpy_refusal = True
311
+ raise MatcherVerdictError(message)
312
+ return answer
@@ -0,0 +1,31 @@
1
+ """Size as one decision: how long a value is, or that it has no length at all.
2
+
3
+ `len()` answers both questions at once by raising, and the two callers need them apart. A builder
4
+ refuses a value with no length, because a wrong subject there is a mistake in the test. A matcher
5
+ answers "no match", because it feeds `==` and the combinators, where raising would be wrong. Written
6
+ separately, the two used a different rule for what "has no length" means: one asked the protocol, the
7
+ other caught `TypeError` and so also swallowed a `__len__` of the user's own that raised.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Sized
13
+ from typing import cast
14
+
15
+ _SIZED = frozenset({list, tuple, dict, str, bytes, set, frozenset, bytearray})
16
+
17
+
18
+ def length_of(value: object) -> int | None:
19
+ """The length of *value*, or ``None`` when it has none.
20
+
21
+ Answered by the protocol rather than by catching `TypeError`, so a `__len__` that exists and raises
22
+ travels out as its author wrote it: that is a bug in the value, not a value without a length, and
23
+ reporting it as the latter sends the reader looking in the wrong file. It also does not depend on a
24
+ Python frame being pushed, which a `__len__` implemented in C does not do.
25
+ """
26
+ if type(value) in _SIZED:
27
+ # `isinstance` against an ABC is the whole cost here, and it was paid per element on the extraction path
28
+ return len(cast("Sized", value))
29
+ if not isinstance(value, Sized):
30
+ return None
31
+ return len(value)
@@ -0,0 +1,39 @@
1
+ """Text relations as one decision: containment, prefix and suffix over `str` and `bytes` alike.
2
+
3
+ This is the family that already drifted. The methods grew `bytes` support and the matchers kept
4
+ `isinstance(value, str)`, so `assert_that(b"hello").contains(b"ell")` passed while
5
+ `match.contains_string(b"ell")` said no, and the disagreement reached everything a matcher feeds:
6
+ `satisfies()`, nested specs and the `==` protocol. It was found by measurement rather than by reading,
7
+ which is the argument for the rule living in one place from here on.
8
+
9
+ `str` and `bytes` are neighbours and never each other's operands. Mixing them is answered "no" rather
10
+ than raised on, because that is what a matcher needs; a builder checks the pair before it asks.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+
18
+ def _comparable(value: object, operand: object) -> bool:
19
+ """Whether the two are text of the same family: `str` with `str`, `bytes` with `bytes`."""
20
+ if isinstance(value, str):
21
+ return isinstance(operand, str)
22
+ if isinstance(value, (bytes, bytearray)):
23
+ return isinstance(operand, (bytes, bytearray))
24
+ return False
25
+
26
+
27
+ def contains(value: Any, part: Any) -> bool:
28
+ """Whether *part* appears in *value*, both being text of the same family."""
29
+ return _comparable(value, part) and part in value
30
+
31
+
32
+ def starts_with(value: Any, prefix: Any) -> bool:
33
+ """Whether *value* begins with *prefix*, both being text of the same family."""
34
+ return _comparable(value, prefix) and value.startswith(prefix)
35
+
36
+
37
+ def ends_with(value: Any, suffix: Any) -> bool:
38
+ """Whether *value* ends with *suffix*, both being text of the same family."""
39
+ return _comparable(value, suffix) and value.endswith(suffix)