pytest-probatio 0.2.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,7 @@
1
+ """pytest-probatio: use a probatio schema as a pytest assertion matcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pytest_probatio.plugin import Exact, Partial
6
+
7
+ __all__ = ["Exact", "Partial"]
@@ -0,0 +1,149 @@
1
+ """Use a probatio schema as a pytest assertion matcher.
2
+
3
+ ``assert response == Exact({"name": str, "port": Port()})`` validates ``response``
4
+ against the schema. On a mismatch, pytest's assertion rewriting calls the
5
+ ``pytest_assertrepr_compare`` hook below, which renders each probatio error by its
6
+ path, so the failure points at the exact offending value instead of a bare
7
+ ``assert ... == ...``.
8
+
9
+ Two matchers are exposed:
10
+
11
+ - ``Exact(schema)``: extra dict keys make it unequal; ``<=`` relaxes that to a
12
+ partial match (extra keys allowed).
13
+ - ``Partial(schema)``: a partial match under ``==`` (extra keys allowed).
14
+
15
+ This lives outside the ``probatio`` package on purpose: the core library is
16
+ dependency-free, and a pytest plugin is a test-framework concern.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import typing
22
+
23
+ from probatio import ALLOW_EXTRA, PREVENT_EXTRA, Invalid, MultipleInvalid, Schema
24
+
25
+
26
+ def _compile(schema: typing.Any, extra: int) -> Schema:
27
+ """Return ``schema`` as a ``Schema`` with the given extra-key policy.
28
+
29
+ A ready-made ``Schema`` (you can build one once and reuse it across tests) is
30
+ rebuilt with the requested policy, its underlying schema and ``required`` flag
31
+ preserved, so ``Exact`` and ``Partial`` control extra keys the same way whether
32
+ they are given a raw shape or a ``Schema``.
33
+ """
34
+ if isinstance(schema, Schema):
35
+ return Schema(schema.schema, required=schema.required, extra=extra)
36
+ return Schema(schema, extra=extra)
37
+
38
+
39
+ class _Matcher:
40
+ """A schema that compares equal to data validating against it.
41
+
42
+ ``__eq__`` runs the schema and records the errors, so the assertion hook can
43
+ show them. It is not hashable (the recorded errors make it mutable), which also
44
+ keeps it out of places that expect a stable hash.
45
+ """
46
+
47
+ __slots__ = ("_errors", "_loose", "_strict", "_strict_eq")
48
+
49
+ def __init__(self, schema: typing.Any, *, partial: bool) -> None:
50
+ """Build the strict and partial schemas; ``partial`` picks which ``==`` uses."""
51
+ self._strict = _compile(schema, PREVENT_EXTRA)
52
+ self._loose = _compile(schema, ALLOW_EXTRA)
53
+ # ``Partial`` validates loosely under ``==``; ``Exact`` validates strictly.
54
+ self._strict_eq = not partial
55
+ self._errors: list[Invalid] = []
56
+
57
+ @property
58
+ def errors(self) -> list[Invalid]:
59
+ """The probatio errors from the most recent comparison (empty if it matched)."""
60
+ return self._errors
61
+
62
+ def _run(self, data: typing.Any, schema: Schema) -> bool:
63
+ """Validate ``data``, recording any errors, returning whether it matched."""
64
+ try:
65
+ schema(data)
66
+ except MultipleInvalid as exc:
67
+ self._errors = list(exc.errors)
68
+ return False
69
+ except Invalid as exc:
70
+ self._errors = [exc]
71
+ return False
72
+ self._errors = []
73
+ return True
74
+
75
+ def __eq__(self, data: object) -> bool:
76
+ """Whether ``data`` validates (strictly for ``Exact``, loosely for ``Partial``)."""
77
+ return self._run(data, self._strict if self._strict_eq else self._loose)
78
+
79
+ def __ne__(self, data: object) -> bool:
80
+ """Return the negation of ``__eq__``."""
81
+ return not self.__eq__(data)
82
+
83
+ def __le__(self, data: object) -> bool:
84
+ """Return a partial match: ``data`` validates with extra keys allowed."""
85
+ return self._run(data, self._loose)
86
+
87
+ def __ge__(self, data: object) -> bool:
88
+ """Support the reversed ``data <= matcher`` form (also a partial match)."""
89
+ return self._run(data, self._loose)
90
+
91
+ __hash__ = None # type: ignore[assignment]
92
+
93
+ def __repr__(self) -> str:
94
+ """Render with the underlying schema, so a failing assert reads clearly."""
95
+ return f"{type(self).__name__}({self._strict.schema!r})"
96
+
97
+
98
+ class Exact(_Matcher):
99
+ """A schema matcher: ``==`` requires an exact match, ``<=`` a partial one.
100
+
101
+ Exact means an extra dictionary key makes the data unequal; ``<=`` relaxes
102
+ that to allow extra keys, the same as ``Partial`` under ``==``.
103
+ """
104
+
105
+ def __init__(self, schema: typing.Any) -> None:
106
+ """Wrap ``schema`` as an exact-by-equality matcher."""
107
+ super().__init__(schema, partial=False)
108
+
109
+
110
+ class Partial(_Matcher):
111
+ """A schema matcher whose ``==`` allows extra keys (a partial match)."""
112
+
113
+ def __init__(self, schema: typing.Any) -> None:
114
+ """Wrap ``schema`` as a partial-by-equality matcher."""
115
+ super().__init__(schema, partial=True)
116
+
117
+
118
+ def _format_path(path: list[typing.Any]) -> str:
119
+ """Render an error path as ``data['a'][0]``, or ``<root>`` when empty."""
120
+ if not path:
121
+ return "<root>"
122
+ return "data" + "".join(f"[{segment!r}]" for segment in path)
123
+
124
+
125
+ def pytest_assertrepr_compare(
126
+ op: str,
127
+ left: object,
128
+ right: object,
129
+ ) -> list[str] | None:
130
+ """Explain a failed schema comparison by listing each error with its path."""
131
+ if op not in ("==", "!=", "<=", ">="):
132
+ return None
133
+ if isinstance(left, _Matcher):
134
+ matcher: _Matcher = left
135
+ elif isinstance(right, _Matcher):
136
+ matcher = right
137
+ else:
138
+ return None
139
+ if not matcher.errors:
140
+ # A negated comparison (``!=``) that failed because the data *did* match.
141
+ return [
142
+ f"data matches the probatio schema, but the assertion ({op}) required it not to"
143
+ ]
144
+ lines = [f"data does not match the probatio schema ({op}):"]
145
+ lines.extend(
146
+ f" {_format_path(error.path)}: {error.error_message or str(error)}"
147
+ for error in matcher.errors
148
+ )
149
+ return lines
File without changes
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-probatio
3
+ Version: 0.2.0
4
+ Summary: Use a probatio schema as a pytest assertion matcher
5
+ Project-URL: Homepage, https://github.com/frenck/probatio
6
+ Project-URL: Repository, https://github.com/frenck/probatio
7
+ Project-URL: Issues, https://github.com/frenck/probatio/issues
8
+ Author-email: Franck Nijhof <opensource@frenck.dev>
9
+ Maintainer-email: Franck Nijhof <opensource@frenck.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: probatio,pytest,schema,testing,validation
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.13
24
+ Requires-Dist: probatio>=0.1.0
25
+ Requires-Dist: pytest>=8
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pytest-probatio
29
+
30
+ Use a [probatio](https://github.com/frenck/probatio) schema as a pytest assertion
31
+ matcher. A schema reads as the expected shape, and a mismatch is explained by
32
+ probatio's path-precise errors instead of a bare `assert`.
33
+
34
+ ```python
35
+ from pytest_probatio import Exact, Partial
36
+ from probatio import Port
37
+
38
+
39
+ def test_response(response):
40
+ # Exact: extra keys make it unequal.
41
+ assert response == Exact({"name": str, "port": Port()})
42
+
43
+ # Partial: extra keys are allowed.
44
+ assert response == Partial({"name": str})
45
+ assert Exact({"name": str}) <= response
46
+ ```
47
+
48
+ When the data does not match, the failure lists each error by its path:
49
+
50
+ ```
51
+ data does not match the probatio schema (==):
52
+ data['port']: expected a port number between 1 and 65535
53
+ ```
54
+
55
+ Install it alongside pytest; the plugin registers itself:
56
+
57
+ ```bash
58
+ pip install pytest-probatio
59
+ ```
60
+
61
+ This package lives in the probatio monorepo but ships separately, so the core
62
+ `probatio` library stays dependency-free.
@@ -0,0 +1,8 @@
1
+ pytest_probatio/__init__.py,sha256=s-S0nBcMm4wXs81cn9IYHFP6MNJPACmPjKPozAgTxlc,195
2
+ pytest_probatio/plugin.py,sha256=mXNR0lG5OQC_JKhWioUqg7FzCEKgmayL7v1_1yGGyu4,5600
3
+ pytest_probatio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_probatio-0.2.0.dist-info/METADATA,sha256=qKB2-ADAgfIueEMcURO26jYX4-7wQsecIkQ1_qX-UQg,2069
5
+ pytest_probatio-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ pytest_probatio-0.2.0.dist-info/entry_points.txt,sha256=-3C_rZOBSZPp9mqG-exojMsjiZTYDFgqkjn4naPq9oQ,45
7
+ pytest_probatio-0.2.0.dist-info/licenses/LICENSE,sha256=xey_9Ex7UIJ45DSFo_Gzo0HN_fsyUJ6ANF2SKQDwaJA,1070
8
+ pytest_probatio-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ probatio = pytest_probatio.plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Franck Nijhof
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.