lovely-assertions 0.1.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.
- lovely_assertions/__init__.py +177 -0
- lovely_assertions/_bool.py +127 -0
- lovely_assertions/_callable.py +881 -0
- lovely_assertions/_collection.py +1606 -0
- lovely_assertions/_core.py +1277 -0
- lovely_assertions/_datetime.py +1186 -0
- lovely_assertions/_diff.py +1127 -0
- lovely_assertions/_enum.py +404 -0
- lovely_assertions/_equivalence.py +2613 -0
- lovely_assertions/_exceptions.py +56 -0
- lovely_assertions/_formatters.py +539 -0
- lovely_assertions/_formatting.py +425 -0
- lovely_assertions/_mapping.py +803 -0
- lovely_assertions/_matching.py +1054 -0
- lovely_assertions/_mock.py +748 -0
- lovely_assertions/_names.py +438 -0
- lovely_assertions/_numeric.py +459 -0
- lovely_assertions/_occurrence.py +379 -0
- lovely_assertions/_ordered.py +338 -0
- lovely_assertions/_path.py +1126 -0
- lovely_assertions/_reflection.py +251 -0
- lovely_assertions/_sequence.py +816 -0
- lovely_assertions/_string.py +1323 -0
- lovely_assertions/_subjects.py +607 -0
- lovely_assertions/_text.py +290 -0
- lovely_assertions/_type.py +662 -0
- lovely_assertions/_warnings.py +683 -0
- lovely_assertions/py.typed +0 -0
- lovely_assertions-0.1.0.dist-info/METADATA +221 -0
- lovely_assertions-0.1.0.dist-info/RECORD +32 -0
- lovely_assertions-0.1.0.dist-info/WHEEL +4 -0
- lovely_assertions-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Fluent, strictly-typed assertions for Python tests.
|
|
2
|
+
|
|
3
|
+
``lovely-assertions`` competes with pytest's ``assert`` rewriting on the two axes
|
|
4
|
+
where it has nothing to offer: **typed discoverability** -- ``expect(x).`` proposes
|
|
5
|
+
only the assertions that are valid for ``x`` -- and **real narrowing** --
|
|
6
|
+
``expect(raw).is_not_none().subject`` is a ``str`` as far as the type checker is
|
|
7
|
+
concerned.
|
|
8
|
+
|
|
9
|
+
::
|
|
10
|
+
|
|
11
|
+
from lovely_assertions import expect
|
|
12
|
+
|
|
13
|
+
expect(order.total).is_greater_than(0, because="an empty basket cannot ship")
|
|
14
|
+
expect(response.headers).contains_key("etag")
|
|
15
|
+
|
|
16
|
+
:func:`expect` chooses the subject type from the value, so only the assertions
|
|
17
|
+
that make sense for it are offered; every assertion returns a subject, so they
|
|
18
|
+
chain; every failure is a sentence naming the bug rather than a diff to interpret.
|
|
19
|
+
``because=`` is appended to that sentence and is never interpolated into it.
|
|
20
|
+
|
|
21
|
+
Around that: :func:`soft_assertions` gathers several failures into one report
|
|
22
|
+
instead of stopping at the first, :func:`expect_raises` and :func:`expect_warns`
|
|
23
|
+
put an exception or a warning under the same fluent API, :func:`formatting` widens
|
|
24
|
+
how much a message may print for one block, and :func:`register_formatter` decides
|
|
25
|
+
how a domain type reads inside one. :func:`register` attaches a subject class to
|
|
26
|
+
a type of your own, and :func:`custom_assertion` marks an assertion you wrote so
|
|
27
|
+
its failures still name the caller's variable rather than a local of yours.
|
|
28
|
+
|
|
29
|
+
Zero runtime dependencies, Python 3.13+, ``py.typed``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
#: The distribution's version, and the only place it is written down.
|
|
33
|
+
#:
|
|
34
|
+
#: A string literal rather than a call to ``importlib.metadata.version()``: that
|
|
35
|
+
#: reads the installed distribution's metadata off disk, which costs more than
|
|
36
|
+
#: everything else this package does at import time and would be paid by every
|
|
37
|
+
#: program that imports it to run one assertion. The build backend reads this very
|
|
38
|
+
#: line to version the wheel, so the string here and the distribution's recorded
|
|
39
|
+
#: version cannot disagree.
|
|
40
|
+
#:
|
|
41
|
+
#: The trailing marker is not decoration: release-please's generic updater rewrites
|
|
42
|
+
#: the line carrying it, which is how the version derived from the commit log lands
|
|
43
|
+
#: here rather than being typed by hand. Remove it and releases silently stop
|
|
44
|
+
#: bumping this file while still tagging.
|
|
45
|
+
#:
|
|
46
|
+
#: It reads ``0.0.0`` because nothing has been published to PyPI yet, and this
|
|
47
|
+
#: line records the last *released* version rather than the next one. The release
|
|
48
|
+
#: pull request is where the next one is proposed.
|
|
49
|
+
__version__ = "0.1.0" # x-release-please-version
|
|
50
|
+
|
|
51
|
+
from lovely_assertions._core import Expect, Found, SoftScope, soft_assertions
|
|
52
|
+
from lovely_assertions._equivalence import Equivalency, close_within, equivalency
|
|
53
|
+
from lovely_assertions._exceptions import AssertionFailure
|
|
54
|
+
from lovely_assertions._formatters import (
|
|
55
|
+
IterableFormatter,
|
|
56
|
+
ObjectFormatter,
|
|
57
|
+
ValueFormatter,
|
|
58
|
+
format_value,
|
|
59
|
+
register_formatter,
|
|
60
|
+
)
|
|
61
|
+
from lovely_assertions._formatting import FormattingOptions, current_formatting, formatting
|
|
62
|
+
|
|
63
|
+
# `_matching` wires itself up at import time: a subject that refuses
|
|
64
|
+
# `expect(<a matcher>)` and says where the matcher belongs instead, and a formatter
|
|
65
|
+
# that renders matchers inside a failure message. Neither happens unless something
|
|
66
|
+
# imports the module, so re-exporting the matchers here is what installs both for
|
|
67
|
+
# every user.
|
|
68
|
+
from lovely_assertions._matching import (
|
|
69
|
+
any_instance_of,
|
|
70
|
+
anything,
|
|
71
|
+
close_to,
|
|
72
|
+
containing,
|
|
73
|
+
is_matcher,
|
|
74
|
+
matching,
|
|
75
|
+
one_of,
|
|
76
|
+
string_containing,
|
|
77
|
+
string_matching,
|
|
78
|
+
)
|
|
79
|
+
from lovely_assertions._names import custom_assertion
|
|
80
|
+
from lovely_assertions._occurrence import (
|
|
81
|
+
Occurrence,
|
|
82
|
+
at_least,
|
|
83
|
+
at_most,
|
|
84
|
+
exactly,
|
|
85
|
+
less_than,
|
|
86
|
+
more_than,
|
|
87
|
+
once,
|
|
88
|
+
twice,
|
|
89
|
+
)
|
|
90
|
+
from lovely_assertions._subjects import (
|
|
91
|
+
BoolExpect,
|
|
92
|
+
CallableExpect,
|
|
93
|
+
CollectionExpect,
|
|
94
|
+
DateExpect,
|
|
95
|
+
DateTimeExpect,
|
|
96
|
+
EnumExpect,
|
|
97
|
+
MappingExpect,
|
|
98
|
+
MockExpect,
|
|
99
|
+
NumericExpect,
|
|
100
|
+
OrderedExpect,
|
|
101
|
+
PathExpect,
|
|
102
|
+
PurePathExpect,
|
|
103
|
+
RaisedExpect,
|
|
104
|
+
SequenceExpect,
|
|
105
|
+
StringExpect,
|
|
106
|
+
TimeDeltaExpect,
|
|
107
|
+
TimeExpect,
|
|
108
|
+
TypeExpect,
|
|
109
|
+
WithinDelta,
|
|
110
|
+
expect,
|
|
111
|
+
expect_raises,
|
|
112
|
+
is_mock,
|
|
113
|
+
register,
|
|
114
|
+
)
|
|
115
|
+
from lovely_assertions._warnings import WarnedExpect, expect_warns
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"AssertionFailure",
|
|
119
|
+
"BoolExpect",
|
|
120
|
+
"CallableExpect",
|
|
121
|
+
"CollectionExpect",
|
|
122
|
+
"DateExpect",
|
|
123
|
+
"DateTimeExpect",
|
|
124
|
+
"EnumExpect",
|
|
125
|
+
"Equivalency",
|
|
126
|
+
"Expect",
|
|
127
|
+
"FormattingOptions",
|
|
128
|
+
"Found",
|
|
129
|
+
"IterableFormatter",
|
|
130
|
+
"MappingExpect",
|
|
131
|
+
"MockExpect",
|
|
132
|
+
"NumericExpect",
|
|
133
|
+
"ObjectFormatter",
|
|
134
|
+
"Occurrence",
|
|
135
|
+
"OrderedExpect",
|
|
136
|
+
"PathExpect",
|
|
137
|
+
"PurePathExpect",
|
|
138
|
+
"RaisedExpect",
|
|
139
|
+
"SequenceExpect",
|
|
140
|
+
"SoftScope",
|
|
141
|
+
"StringExpect",
|
|
142
|
+
"TimeDeltaExpect",
|
|
143
|
+
"TimeExpect",
|
|
144
|
+
"TypeExpect",
|
|
145
|
+
"ValueFormatter",
|
|
146
|
+
"WarnedExpect",
|
|
147
|
+
"WithinDelta",
|
|
148
|
+
"any_instance_of",
|
|
149
|
+
"anything",
|
|
150
|
+
"at_least",
|
|
151
|
+
"at_most",
|
|
152
|
+
"close_to",
|
|
153
|
+
"close_within",
|
|
154
|
+
"containing",
|
|
155
|
+
"current_formatting",
|
|
156
|
+
"custom_assertion",
|
|
157
|
+
"equivalency",
|
|
158
|
+
"exactly",
|
|
159
|
+
"expect",
|
|
160
|
+
"expect_raises",
|
|
161
|
+
"expect_warns",
|
|
162
|
+
"format_value",
|
|
163
|
+
"formatting",
|
|
164
|
+
"is_matcher",
|
|
165
|
+
"is_mock",
|
|
166
|
+
"less_than",
|
|
167
|
+
"matching",
|
|
168
|
+
"more_than",
|
|
169
|
+
"once",
|
|
170
|
+
"one_of",
|
|
171
|
+
"register",
|
|
172
|
+
"register_formatter",
|
|
173
|
+
"soft_assertions",
|
|
174
|
+
"string_containing",
|
|
175
|
+
"string_matching",
|
|
176
|
+
"twice",
|
|
177
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Assertions for ``bool``.
|
|
2
|
+
|
|
3
|
+
The shortest catalogue in the library, and the only one where the value in a
|
|
4
|
+
failure message tells the reader nothing new: there are two of them, and the
|
|
5
|
+
assertion already named the one it wanted. So the effort goes elsewhere.
|
|
6
|
+
``implies`` can fail exactly one way -- the subject held and the consequent did
|
|
7
|
+
not -- and its message spells both sides out rather than leaving the reader to
|
|
8
|
+
recall the truth table.
|
|
9
|
+
|
|
10
|
+
The four value checks are identity, not truthiness: ``is_true`` asks for ``True``
|
|
11
|
+
itself, never for something merely truthy. ``expect()`` routes only an exact
|
|
12
|
+
``bool`` here, so the distinction stays invisible in normal use; it surfaces when
|
|
13
|
+
a subject is built by hand around a ``1`` or a NumPy scalar, and there the strict
|
|
14
|
+
reading reports the bug instead of hiding it. ``implies`` is the one method that
|
|
15
|
+
must read the subject for *truth* rather than for identity -- an implication is
|
|
16
|
+
defined over truth values -- so it reports the value it actually saw instead of
|
|
17
|
+
asserting that it was ``True``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Self
|
|
21
|
+
|
|
22
|
+
from lovely_assertions._core import Expect
|
|
23
|
+
from lovely_assertions._diff import render_operand
|
|
24
|
+
from lovely_assertions._exceptions import hide_internal_frames
|
|
25
|
+
|
|
26
|
+
#: pytest reads ``__tracebackhide__`` from a frame's globals, so this one
|
|
27
|
+
#: assignment folds every frame of this module out of an assertion failure's
|
|
28
|
+
#: traceback while leaving them in place for a genuine error. See
|
|
29
|
+
#: :func:`lovely_assertions._exceptions.hide_internal_frames`.
|
|
30
|
+
__tracebackhide__ = hide_internal_frames
|
|
31
|
+
|
|
32
|
+
__all__ = ["BoolExpect"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BoolExpect(Expect[bool]):
|
|
36
|
+
"""The subject ``expect()`` hands back for a ``bool``.
|
|
37
|
+
|
|
38
|
+
Adds the two value checks, their negations and :meth:`implies` to the generic
|
|
39
|
+
catalogue; ``is_equal_to``, ``is_not_equal_to`` and the rest are inherited
|
|
40
|
+
from :class:`Expect`. Every method returns the subject, so calls chain.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
__slots__ = ()
|
|
44
|
+
|
|
45
|
+
# -- the two values ----------------------------------------------------
|
|
46
|
+
def is_true(self, *, because: str = "") -> Self:
|
|
47
|
+
"""Assert the subject is ``True``.
|
|
48
|
+
|
|
49
|
+
Identity, not truthiness: a subject that is merely truthy -- a ``1``, a
|
|
50
|
+
non-empty string, a NumPy scalar -- fails, and the failure prints what was
|
|
51
|
+
actually there. Returns the subject, so the call chains. For a plain
|
|
52
|
+
``bool`` this and :meth:`is_not_false` make the same claim; reach for this
|
|
53
|
+
one to state what the value is.
|
|
54
|
+
"""
|
|
55
|
+
if self._subject is True:
|
|
56
|
+
return self
|
|
57
|
+
return self._fail(f"to be True, but was {render_operand(self._subject)}", because)
|
|
58
|
+
|
|
59
|
+
def is_false(self, *, because: str = "") -> Self:
|
|
60
|
+
"""Assert the subject is ``False``.
|
|
61
|
+
|
|
62
|
+
Identity, not falsiness: a ``0``, a ``None`` or an empty container -- which
|
|
63
|
+
reaches here only through a hand-built subject -- fails rather than passing
|
|
64
|
+
quietly. Returns the subject, so the call chains. For a plain ``bool`` this
|
|
65
|
+
and :meth:`is_not_true` make the same claim; reach for this one to state
|
|
66
|
+
what the value is.
|
|
67
|
+
"""
|
|
68
|
+
if self._subject is False:
|
|
69
|
+
return self
|
|
70
|
+
return self._fail(f"to be False, but was {render_operand(self._subject)}", because)
|
|
71
|
+
|
|
72
|
+
def is_not_true(self, *, because: str = "") -> Self:
|
|
73
|
+
"""Assert the subject is not ``True``.
|
|
74
|
+
|
|
75
|
+
For a plain ``bool`` this is :meth:`is_false` wearing another name, and
|
|
76
|
+
both are kept because they read differently at the call site: one states
|
|
77
|
+
what the value is, the other what it must not have become -- the shape a
|
|
78
|
+
"the flag was never set" check wants. It is also where FluentAssertions'
|
|
79
|
+
``NotBeTrue`` lands, where the distinction is real because a nullable
|
|
80
|
+
``bool?`` can be neither. A Python ``bool | None`` falls back to the
|
|
81
|
+
generic subject rather than reaching this class, so that case does not
|
|
82
|
+
survive the trip -- but the name it is looked up under does. Returns the
|
|
83
|
+
subject, so the call chains.
|
|
84
|
+
"""
|
|
85
|
+
if self._subject is not True:
|
|
86
|
+
return self
|
|
87
|
+
return self._fail("not to be True, but it was", because)
|
|
88
|
+
|
|
89
|
+
def is_not_false(self, *, because: str = "") -> Self:
|
|
90
|
+
"""Assert the subject is not ``False``.
|
|
91
|
+
|
|
92
|
+
The mirror of :meth:`is_not_true`. For a plain ``bool`` it makes the same
|
|
93
|
+
claim as :meth:`is_true`, under a name that reads as "it must not have been
|
|
94
|
+
switched off". Returns the subject, so the call chains.
|
|
95
|
+
"""
|
|
96
|
+
if self._subject is not False:
|
|
97
|
+
return self
|
|
98
|
+
return self._fail("not to be False, but it was", because)
|
|
99
|
+
|
|
100
|
+
# -- logic -------------------------------------------------------------
|
|
101
|
+
def implies(self, consequent: bool, /, *, because: str = "") -> Self:
|
|
102
|
+
"""Assert the material implication ``subject -> consequent``.
|
|
103
|
+
|
|
104
|
+
It holds unless the subject is true and ``consequent`` is not, so three
|
|
105
|
+
of the four rows pass -- including both rows where the subject is false,
|
|
106
|
+
which is the part that surprises people reading a failure.
|
|
107
|
+
|
|
108
|
+
``consequent`` is a value, not a callable: it is evaluated by the caller
|
|
109
|
+
before the assertion runs, so this asserts a relationship between two
|
|
110
|
+
booleans already in hand rather than deferring one of them.
|
|
111
|
+
|
|
112
|
+
Both sides are reported as they were found. The failing row is always
|
|
113
|
+
"the subject held", but *what* held is worth printing: a subject that
|
|
114
|
+
reached here as a ``1`` or a ``"yes"`` is a finding, and a message that
|
|
115
|
+
answered ``True`` for all three would hide it.
|
|
116
|
+
|
|
117
|
+
This is the one method here that reads the subject for truth rather than
|
|
118
|
+
for identity, since an implication is defined over truth values. Returns
|
|
119
|
+
the subject, so the call chains.
|
|
120
|
+
"""
|
|
121
|
+
if not self._subject or consequent:
|
|
122
|
+
return self
|
|
123
|
+
return self._fail(
|
|
124
|
+
f"to imply the consequent, but was {render_operand(self._subject)} "
|
|
125
|
+
f"while the consequent was {render_operand(consequent)}",
|
|
126
|
+
because,
|
|
127
|
+
)
|