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.
@@ -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
+ )