assertpy 2.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assertpy/__init__.py +87 -0
- assertpy/_clustering.py +511 -0
- assertpy/_dangling.py +505 -0
- assertpy/_engine/__init__.py +1 -0
- assertpy/_engine/_builder_check_typing.py +1365 -0
- assertpy/_engine/_capable_typing.py +772 -0
- assertpy/_engine/_check_typing.py +753 -0
- assertpy/_engine/_compare.py +983 -0
- assertpy/_engine/_compat.py +38 -0
- assertpy/_engine/_contract.py +1926 -0
- assertpy/_engine/_diff.py +1058 -0
- assertpy/_engine/_equality.py +843 -0
- assertpy/_engine/_introspection.py +729 -0
- assertpy/_engine/_membership.py +522 -0
- assertpy/_engine/_mixin_base.py +258 -0
- assertpy/_engine/_negated_typing.py +2176 -0
- assertpy/_engine/_operations.py +75 -0
- assertpy/_engine/_ordering.py +884 -0
- assertpy/_engine/_pairing.py +70 -0
- assertpy/_engine/_path.py +158 -0
- assertpy/_engine/_poll_typing.py +6039 -0
- assertpy/_engine/_require.py +312 -0
- assertpy/_engine/_size.py +31 -0
- assertpy/_engine/_text.py +39 -0
- assertpy/_engine/_typing.py +1226 -0
- assertpy/_hints.py +1213 -0
- assertpy/_inline.py +484 -0
- assertpy/_matcher_impls.py +1876 -0
- assertpy/_passes.py +249 -0
- assertpy/_renamed.py +64 -0
- assertpy/_satisfies.py +835 -0
- assertpy/_snapshot_codec.py +290 -0
- assertpy/assertpy.py +2640 -0
- assertpy/async_assertions.py +864 -0
- assertpy/base.py +862 -0
- assertpy/behave_matchers.py +96 -0
- assertpy/bytes_mixin.py +158 -0
- assertpy/collection.py +528 -0
- assertpy/contains.py +1068 -0
- assertpy/dataframe.py +200 -0
- assertpy/date.py +327 -0
- assertpy/dict.py +249 -0
- assertpy/dynamic.py +222 -0
- assertpy/errors.py +782 -0
- assertpy/exception.py +838 -0
- assertpy/extracting.py +266 -0
- assertpy/file.py +324 -0
- assertpy/helpers.py +747 -0
- assertpy/http_mixin.py +218 -0
- assertpy/json_mixin.py +1265 -0
- assertpy/matchers.py +639 -0
- assertpy/numeric.py +696 -0
- assertpy/outcome.py +163 -0
- assertpy/py.typed +0 -0
- assertpy/pytest_plugin.py +1650 -0
- assertpy/snapshot.py +1031 -0
- assertpy/string.py +803 -0
- assertpy/warning.py +137 -0
- assertpy-2.35.0.dist-info/METADATA +324 -0
- assertpy-2.35.0.dist-info/RECORD +63 -0
- assertpy-2.35.0.dist-info/WHEEL +4 -0
- assertpy-2.35.0.dist-info/entry_points.txt +2 -0
- assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
assertpy/_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
|