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/_passes.py ADDED
@@ -0,0 +1,249 @@
1
+ """What a `log_passes()` block reports, and the state that tells a pass from a failure delivered quietly.
2
+
3
+ A built-in assertion does no bookkeeping when it holds, and outside every block that stays true: `_builder()`
4
+ makes `_ExtendedBuilder` itself until a block opens somewhere. A builder made inside a block is of a class whose
5
+ assertions are observed, and an observed call reports itself once it has run without a failure being delivered.
6
+
7
+ Returning is not the verdict. A soft block collects a failure and the chain goes on, a warn chain logs one and
8
+ goes on, and `check()` hands a failure back as a value, so an observed call holds an `Observed` that
9
+ `mark_failed()` flips where a failure is delivered without raising. Only the outermost observed call reports:
10
+ `is_positive()` asks `is_greater_than(0)` of the same builder, and the reader wrote the first.
11
+
12
+ Every marker here has a lifetime of its own, `alive`, and not only a place in a context. A task started inside an
13
+ observed call, a poll attempt or a block keeps a copy of the context it started in, and without the flag it went
14
+ on reading a call that had returned as one still running.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import contextvars
20
+ import itertools
21
+ import logging
22
+ import threading
23
+ from dataclasses import dataclass, field
24
+ from typing import TYPE_CHECKING, Any, Protocol
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+ from .outcome import AssertionOutcome
30
+
31
+
32
+ class PassLogger(Protocol):
33
+ """What a block asks of a logger: whether a level is on, and one line at it, as `Logger` and `LoggerAdapter` do."""
34
+
35
+ def isEnabledFor(self, level: int) -> bool: ... # noqa: N802 - the name `logging` gives it
36
+
37
+ def log(self, level: int, msg: str) -> object: ...
38
+
39
+
40
+ open_blocks = 0
41
+ """How many `log_passes()` blocks are open in any thread."""
42
+
43
+ _counting = threading.Lock()
44
+
45
+ logged_class: type[Any] | None = None
46
+ """The builder class a block hands out, made when the first block opens: its wrappers cost the import nothing."""
47
+
48
+ _births = itertools.count()
49
+ """The order entries and poll attempts began in, so a pass can tell an entry made during its attempt."""
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class PassScope:
54
+ """Where the passes of one entry into a block go: to *on_pass* when given, else to *logger* at *level*.
55
+
56
+ Made afresh at each entry, and ``active`` goes off at its exit for good: a task that outlived the entry holds
57
+ this in its own context copy, and a later entry of the same block is not its. *previous* is the scope the
58
+ entry's context had before it, put back at the exit by the context that exits, which needs no token: two tasks
59
+ entering one block each restore their own.
60
+ """
61
+
62
+ logger: PassLogger
63
+ level: int
64
+ on_pass: Callable[[AssertionOutcome], object] | None
65
+ previous: PassScope | None = None
66
+ active: bool = True
67
+ born: int = field(default_factory=lambda: next(_births))
68
+
69
+ def wanted(self) -> bool:
70
+ """Whether anything would read a pass, so one nobody reads is never rendered. Asked of an active scope."""
71
+ return self.on_pass is not None or self.logger.isEnabledFor(self.level)
72
+
73
+
74
+ class Observed:
75
+ """An assertion call being observed, whether a failure was delivered while it ran, and whether it still runs."""
76
+
77
+ __slots__ = ("alive", "failed")
78
+
79
+ def __init__(self) -> None:
80
+ self.failed = False
81
+ self.alive = True
82
+
83
+
84
+ class Held:
85
+ """The passes of one poll attempt, kept until the attempt is known to have passed as a whole.
86
+
87
+ *wanted* is the entry's logger asked once before the attempt, outside the retry, so a pass into it renders
88
+ nothing a refusing logger would drop. *scope* is ``None`` for a poll in a context outside every block, which
89
+ still holds what its probe asserts through builders of blocks open elsewhere. A poll inside another's attempt
90
+ takes the outer one's entry and answer, and hands what it held to that attempt.
91
+ """
92
+
93
+ __slots__ = ("alive", "born", "records", "scope", "wanted")
94
+
95
+ def __init__(self, scope: PassScope | None, wanted: bool) -> None:
96
+ self.records: list[tuple[PassScope, AssertionOutcome, Callable[[], str], bool]] = []
97
+ self.scope, self.wanted = scope, wanted
98
+ self.alive = True
99
+ self.born = next(_births)
100
+
101
+
102
+ scope: contextvars.ContextVar[PassScope | None] = contextvars.ContextVar("assertpy_pass_scope", default=None)
103
+ observed: contextvars.ContextVar[Observed | None] = contextvars.ContextVar("assertpy_observed", default=None)
104
+ held: contextvars.ContextVar[Held | None] = contextvars.ContextVar("assertpy_held_passes", default=None)
105
+
106
+
107
+ def open_block(make_class: Callable[[], type[Any]], counted: Callable[[int], object]) -> None:
108
+ """Count a block in, making the class it hands out on the first, and tell *counted* the count under the lock."""
109
+ global open_blocks, logged_class
110
+ with _counting:
111
+ if logged_class is None:
112
+ logged_class = make_class()
113
+ open_blocks += 1
114
+ counted(open_blocks)
115
+
116
+
117
+ def close_block(counted: Callable[[int], object]) -> None:
118
+ """Count a block out, telling *counted* the count under the lock."""
119
+ global open_blocks
120
+ with _counting:
121
+ open_blocks -= 1
122
+ counted(open_blocks)
123
+
124
+
125
+ def active_scope() -> PassScope | None:
126
+ """The entry this context reports into, or ``None`` outside every one and after it closed."""
127
+ found = scope.get()
128
+ return found if found is not None and found.active else None
129
+
130
+
131
+ def current_call() -> Observed | None:
132
+ """The observed call still running in this context, if any."""
133
+ call = observed.get()
134
+ return call if call is not None and call.alive else None
135
+
136
+
137
+ def mark_failed() -> None:
138
+ """Note on the observed call that a failure was delivered without raising, so it reports no pass."""
139
+ if open_blocks and (call := current_call()) is not None:
140
+ call.failed = True
141
+
142
+
143
+ def wants(found: PassScope) -> bool:
144
+ """Whether a pass into *found* is to be made, asked of the attempt when one is held and of the entry otherwise.
145
+
146
+ A pass into another entry than the attempt's is kept and decided at the flush. An entry that closed while the
147
+ assertion ran, which a predicate leaving the block's `ExitStack` does, wants nothing and its logger is not asked.
148
+ """
149
+ if not found.active:
150
+ return False
151
+ kept = _holding(found)
152
+ if kept is not None:
153
+ return kept.wanted if found is kept.scope else True
154
+ return found.wanted()
155
+
156
+
157
+ def _holding(found: PassScope) -> Held | None:
158
+ """The attempt a pass into *found* waits in, or ``None`` where it is handed on as it comes.
159
+
160
+ A block entered after the attempt began, by its probe or beside it, belongs to the attempt: its passes go as they
161
+ come, and what its callback or logger raises is the attempt's to retry. Told by when it was entered and not by
162
+ what it was entered inside, since a context copied in one block can run a poll after another block was entered.
163
+ """
164
+ kept = held.get()
165
+ if kept is None or not kept.alive or found.born > kept.born:
166
+ return None
167
+ return kept
168
+
169
+
170
+ def emit(found: PassScope, outcome: AssertionOutcome, line: Callable[[], str]) -> None:
171
+ """Hand one pass on, or keep it for the poll attempt it came from, rendered now while its values are as checked.
172
+
173
+ A pass into another entry than the attempt's has not asked that entry's logger whether it wants the level: that
174
+ is user code, and asked inside an attempt an `AssertionError` it raised was retried as the assertion's own. The
175
+ flush asks, past the retry.
176
+ """
177
+ kept = _holding(found)
178
+ if kept is not None:
179
+ rendered = "" if found.on_pass is not None else line()
180
+ kept.records.append((found, outcome, lambda: rendered, found is kept.scope))
181
+ return
182
+ deliver(found, outcome, line, asked=True)
183
+
184
+
185
+ def deliver(found: PassScope, outcome: AssertionOutcome, line: Callable[[], str], *, asked: bool) -> None:
186
+ """To the callback, or as one line to the logger, with any assertion they make themselves left unreported.
187
+
188
+ *asked* says whether the logger has already said it wants the level. An entry that closed in the meantime,
189
+ which a predicate leaving the block's `ExitStack` does, takes nothing.
190
+ """
191
+ if not found.active:
192
+ return
193
+ quiet = Observed()
194
+ token = observed.set(quiet)
195
+ try:
196
+ if found.on_pass is not None:
197
+ found.on_pass(outcome)
198
+ elif asked or found.logger.isEnabledFor(found.level):
199
+ found.logger.log(found.level, line())
200
+ finally:
201
+ quiet.alive = False
202
+ observed.reset(token)
203
+
204
+
205
+ def holding() -> Held | None:
206
+ """A place for one poll attempt's passes, or ``None`` inside an observed call, where none is made.
207
+
208
+ Made before the attempt and outside its retry, which is where the logger is asked whether it wants the level,
209
+ with any assertion it makes left unreported. Inside another attempt that is the outer retry, so nothing is asked.
210
+ """
211
+ if current_call() is not None:
212
+ return None
213
+ outer = held.get()
214
+ if outer is not None and outer.alive:
215
+ return Held(outer.scope, outer.wanted)
216
+ found = active_scope()
217
+ if found is None:
218
+ return Held(None, False)
219
+ quiet = Observed()
220
+ token = observed.set(quiet)
221
+ try:
222
+ return Held(found, found.wanted())
223
+ finally:
224
+ quiet.alive = False
225
+ observed.reset(token)
226
+
227
+
228
+ def release(kept: Held, token: contextvars.Token[Held | None]) -> None:
229
+ """Take *kept* out of place once the attempt is over, however it ended, for anything that outlives it."""
230
+ held.reset(token)
231
+ kept.alive = False
232
+
233
+
234
+ def flush(kept: Held) -> None:
235
+ """Hand on what a poll's passing attempt held, in the order it ran, to the attempt it ran inside if any.
236
+
237
+ A pass into a block entered during that outer attempt goes now: the block is the outer probe's.
238
+ """
239
+ kept.alive = False
240
+ outer = held.get()
241
+ for record in kept.records:
242
+ if outer is not None and outer.alive and record[0].born < outer.born:
243
+ outer.records.append(record)
244
+ else:
245
+ found, outcome, line, asked = record
246
+ deliver(found, outcome, line, asked=asked)
247
+
248
+
249
+ default_logger: logging.Logger = logging.getLogger("assertpy.passes")
assertpy/_renamed.py ADDED
@@ -0,0 +1,64 @@
1
+ """The names this package had as assertpy2, still read through the 2.x line.
2
+
3
+ Each is a deprecated synonym of its current name: it works, warns once per process with a
4
+ `FutureWarning`, and goes in 3.0. The old name is read only where the current one is blank.
5
+
6
+ A `FutureWarning` rather than a `DeprecationWarning`, since the reader is whoever wrote the config, and
7
+ Python hides the other category outside code run as `__main__`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import warnings
14
+
15
+ _told: set[str] = set()
16
+
17
+
18
+ def old_name(name: str) -> str:
19
+ """The name a flag, setting, variable or marker had before the rename.
20
+
21
+ Args:
22
+ name: the current name, in any of the spellings the package uses.
23
+
24
+ Returns:
25
+ The same name with ``assertpy`` spelled ``assertpy2``, in the case it was written in.
26
+ """
27
+ return name.replace("assertpy", "assertpy2", 1).replace("ASSERTPY", "ASSERTPY2", 1)
28
+
29
+
30
+ def tell(old: str, new: str) -> None:
31
+ """Warn, once per process, that a name from assertpy2 was used.
32
+
33
+ Args:
34
+ old: the name as it was given.
35
+ new: the name to give instead.
36
+ """
37
+ if old in _told:
38
+ return
39
+ warnings.warn(
40
+ f"{old} is the assertpy2 name of {new}. It keeps working until assertpy 3.0, use {new} instead.",
41
+ FutureWarning,
42
+ stacklevel=3,
43
+ )
44
+ _told.add(old)
45
+
46
+
47
+ def environ(name: str) -> str:
48
+ """An environment variable, read by its assertpy2 name where its own is blank.
49
+
50
+ Args:
51
+ name: the variable's current name.
52
+
53
+ Returns:
54
+ Its value, else the value of its old name, else an empty string.
55
+ """
56
+ value = os.environ.get(name, "")
57
+ if value.strip():
58
+ return value
59
+ old = old_name(name)
60
+ before = os.environ.get(old, "")
61
+ if not before.strip():
62
+ return value
63
+ tell(old, name)
64
+ return before