punit 1.9.2__tar.gz → 1.10.0__tar.gz

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 (54) hide show
  1. {punit-1.9.2/src/pUnit.egg-info → punit-1.10.0}/PKG-INFO +1 -2
  2. {punit-1.9.2 → punit-1.10.0}/pyproject.toml +1 -2
  3. {punit-1.9.2 → punit-1.10.0/src/pUnit.egg-info}/PKG-INFO +1 -2
  4. {punit-1.9.2 → punit-1.10.0}/src/pUnit.egg-info/SOURCES.txt +4 -0
  5. {punit-1.9.2 → punit-1.10.0}/src/pUnit.egg-info/requires.txt +0 -1
  6. {punit-1.9.2 → punit-1.10.0}/src/punit/__init__.py +2 -2
  7. punit-1.10.0/src/punit/mocks/__init__.py +75 -0
  8. punit-1.10.0/src/punit/mocks/matcher.py +318 -0
  9. punit-1.10.0/src/punit/mocks/mock.py +691 -0
  10. punit-1.10.0/src/punit/mocks/patch.py +167 -0
  11. {punit-1.9.2 → punit-1.10.0}/LICENSE +0 -0
  12. {punit-1.9.2 → punit-1.10.0}/README.md +0 -0
  13. {punit-1.9.2 → punit-1.10.0}/setup.cfg +0 -0
  14. {punit-1.9.2 → punit-1.10.0}/src/pUnit.egg-info/dependency_links.txt +0 -0
  15. {punit-1.9.2 → punit-1.10.0}/src/pUnit.egg-info/entry_points.txt +0 -0
  16. {punit-1.9.2 → punit-1.10.0}/src/pUnit.egg-info/top_level.txt +0 -0
  17. {punit-1.9.2 → punit-1.10.0}/src/punit/TestResult.py +0 -0
  18. {punit-1.9.2 → punit-1.10.0}/src/punit/__main__.py +0 -0
  19. {punit-1.9.2 → punit-1.10.0}/src/punit/assertions/__init__.py +0 -0
  20. {punit-1.9.2 → punit-1.10.0}/src/punit/assertions/collections.py +0 -0
  21. {punit-1.9.2 → punit-1.10.0}/src/punit/assertions/exceptions.py +0 -0
  22. {punit-1.9.2 → punit-1.10.0}/src/punit/assertions/numeric.py +0 -0
  23. {punit-1.9.2 → punit-1.10.0}/src/punit/assertions/strings.py +0 -0
  24. {punit-1.9.2 → punit-1.10.0}/src/punit/cli.py +0 -0
  25. {punit-1.9.2 → punit-1.10.0}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  26. {punit-1.9.2 → punit-1.10.0}/src/punit/discovery/__init__.py +0 -0
  27. {punit-1.9.2 → punit-1.10.0}/src/punit/facts/Fact.py +0 -0
  28. {punit-1.9.2 → punit-1.10.0}/src/punit/facts/FactManager.py +0 -0
  29. {punit-1.9.2 → punit-1.10.0}/src/punit/facts/__init__.py +0 -0
  30. {punit-1.9.2 → punit-1.10.0}/src/punit/filters/Filter.py +0 -0
  31. {punit-1.9.2 → punit-1.10.0}/src/punit/filters/FilterManager.py +0 -0
  32. {punit-1.9.2 → punit-1.10.0}/src/punit/filters/__init__.py +0 -0
  33. {punit-1.9.2 → punit-1.10.0}/src/punit/metadata/CallableMetadata.py +0 -0
  34. {punit-1.9.2 → punit-1.10.0}/src/punit/metadata/__init__.py +0 -0
  35. {punit-1.9.2 → punit-1.10.0}/src/punit/py.typed +0 -0
  36. {punit-1.9.2 → punit-1.10.0}/src/punit/reports/HtmlReportGenerator.py +0 -0
  37. {punit-1.9.2 → punit-1.10.0}/src/punit/reports/JUnitReportGenerator.py +0 -0
  38. {punit-1.9.2 → punit-1.10.0}/src/punit/reports/JsonReportGenerator.py +0 -0
  39. {punit-1.9.2 → punit-1.10.0}/src/punit/reports/__init__.py +0 -0
  40. {punit-1.9.2 → punit-1.10.0}/src/punit/results/__init__.py +0 -0
  41. {punit-1.9.2 → punit-1.10.0}/src/punit/results/fails.py +0 -0
  42. {punit-1.9.2 → punit-1.10.0}/src/punit/runner.py +0 -0
  43. {punit-1.9.2 → punit-1.10.0}/src/punit/setups/Setup.py +0 -0
  44. {punit-1.9.2 → punit-1.10.0}/src/punit/setups/SetupManager.py +0 -0
  45. {punit-1.9.2 → punit-1.10.0}/src/punit/setups/__init__.py +0 -0
  46. {punit-1.9.2 → punit-1.10.0}/src/punit/teardowns/Teardown.py +0 -0
  47. {punit-1.9.2 → punit-1.10.0}/src/punit/teardowns/TeardownManager.py +0 -0
  48. {punit-1.9.2 → punit-1.10.0}/src/punit/teardowns/__init__.py +0 -0
  49. {punit-1.9.2 → punit-1.10.0}/src/punit/theories/Theory.py +0 -0
  50. {punit-1.9.2 → punit-1.10.0}/src/punit/theories/TheoryManager.py +0 -0
  51. {punit-1.9.2 → punit-1.10.0}/src/punit/theories/__init__.py +0 -0
  52. {punit-1.9.2 → punit-1.10.0}/src/punit/traits/Trait.py +0 -0
  53. {punit-1.9.2 → punit-1.10.0}/src/punit/traits/TraitManager.py +0 -0
  54. {punit-1.9.2 → punit-1.10.0}/src/punit/traits/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.9.2
3
+ Version: 1.10.0
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -36,7 +36,6 @@ Classifier: Intended Audience :: Developers
36
36
  Requires-Python: >=3.11
37
37
  Description-Content-Type: text/markdown
38
38
  License-File: LICENSE
39
- Requires-Dist: hazrakah
40
39
  Provides-Extra: dev
41
40
  Requires-Dist: coverage; extra == "dev"
42
41
  Requires-Dist: mypy; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pUnit"
3
- version = "1.9.2"
3
+ version = "1.10.0"
4
4
  description = "A modernized unit-test framework for Python."
5
5
  keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest"]
6
6
  authors = [
@@ -16,7 +16,6 @@ classifiers = [
16
16
  "Intended Audience :: Developers"
17
17
  ]
18
18
  dependencies = [
19
- "hazrakah"
20
19
  ]
21
20
 
22
21
  [project.optional-dependencies]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.9.2
3
+ Version: 1.10.0
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -36,7 +36,6 @@ Classifier: Intended Audience :: Developers
36
36
  Requires-Python: >=3.11
37
37
  Description-Content-Type: text/markdown
38
38
  License-File: LICENSE
39
- Requires-Dist: hazrakah
40
39
  Provides-Extra: dev
41
40
  Requires-Dist: coverage; extra == "dev"
42
41
  Requires-Dist: mypy; extra == "dev"
@@ -28,6 +28,10 @@ src/punit/filters/FilterManager.py
28
28
  src/punit/filters/__init__.py
29
29
  src/punit/metadata/CallableMetadata.py
30
30
  src/punit/metadata/__init__.py
31
+ src/punit/mocks/__init__.py
32
+ src/punit/mocks/matcher.py
33
+ src/punit/mocks/mock.py
34
+ src/punit/mocks/patch.py
31
35
  src/punit/reports/HtmlReportGenerator.py
32
36
  src/punit/reports/JUnitReportGenerator.py
33
37
  src/punit/reports/JsonReportGenerator.py
@@ -1,4 +1,3 @@
1
- hazrakah
2
1
 
3
2
  [dev]
4
3
  coverage
@@ -15,8 +15,8 @@ from .assertions.exceptions import raises
15
15
  from .assertions.numeric import approx
16
16
 
17
17
 
18
- __version__ = '1.9.2'
19
- __commit__ = '4f7d92b'
18
+ __version__ = '1.10.0'
19
+ __commit__ = '4e4c2b6'
20
20
  __all__ = [
21
21
  '__version__', '__commit__',
22
22
  'assertions',
@@ -0,0 +1,75 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Provides :class:`Mock`, :class:`Matcher` argument matchers, a ``patch`` mechanism,
6
+ and convenience functions.
7
+
8
+ Usage::
9
+
10
+ from punit.mocks import Mock, mock, is_any, contains, patch
11
+
12
+ # Fluent syntax
13
+ mock = (
14
+ Mock()
15
+ .do_stuff.
16
+ returns(42)
17
+ )
18
+
19
+ # Constructor kwargs for fixture-style initialization
20
+ user = Mock(first_name='Alice', email='alice@example.com')
21
+ assert user.first_name == 'Alice'
22
+ assert user.email == 'alice@example.com'
23
+
24
+ # Matcher-based verification
25
+ assert mock.called_with(
26
+ is_any(),
27
+ neg(contains('foo')),
28
+ is_in(1, 2, 3))
29
+
30
+ # Module-level patching
31
+ with patch('some.module.ClassName') as m:
32
+ m.method.returns('result')
33
+ """
34
+
35
+ from .matcher import (
36
+ Matcher,
37
+ neg,
38
+ contains,
39
+ is_any,
40
+ is_gte,
41
+ is_gt,
42
+ is_in,
43
+ is_lte,
44
+ is_lt,
45
+ is_type,
46
+ )
47
+ from .mock import (
48
+ CallDetail,
49
+ Call,
50
+ CallList,
51
+ CallRecord,
52
+ Mock,
53
+ MockError
54
+ )
55
+ from .patch import patch
56
+
57
+ __all__ = [
58
+ 'CallDetail',
59
+ 'Call',
60
+ 'CallList',
61
+ 'CallRecord',
62
+ 'Matcher',
63
+ 'Mock',
64
+ 'MockError',
65
+ 'neg',
66
+ 'contains',
67
+ 'is_any',
68
+ 'is_gte',
69
+ 'is_gt',
70
+ 'is_in',
71
+ 'is_lte',
72
+ 'is_lt',
73
+ 'is_type',
74
+ 'patch',
75
+ ]
@@ -0,0 +1,318 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Argument matchers for Mock call verification.
6
+
7
+ Matchers are lightweight, duck-typed objects that implement ``__eq__`` to dispatch
8
+ matching logic. No :class:`typing.Protocol` is used -- any object with a working
9
+ ``__eq__`` method satisfies the matcher interface. This avoids needing a separate
10
+ ``.matches()`` API -- Python's equality operator dispatches naturally.
11
+
12
+ Usage::
13
+
14
+ from punit.mocks import Mock, is_any, is_gt, is_in
15
+
16
+ mock = Mock()
17
+ mock(42, 'hello', ['a', 'b'])
18
+
19
+ assert mock.called_with(
20
+ is_gt(10), # first arg > 10
21
+ is_any(), # second arg can be anything
22
+ is_in('a', 'b'), # third arg is 'a' or 'b'
23
+ )
24
+
25
+ Custom matchers: subclass :class:`Matcher` and implement ``__eq__``::
26
+
27
+ class GreaterThan(Matcher):
28
+ def __init__(self, threshold: int) -> None:
29
+ self._threshold = threshold
30
+
31
+ def __eq__(self, other: object) -> bool:
32
+ return isinstance(other, (int, float)) and other > self._threshold
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import Any
38
+
39
+
40
+ class Matcher:
41
+ """Base class for argument matchers.
42
+
43
+ Subclasses must implement ``__eq__(self, other: Any) -> bool`` and return
44
+ ``True`` when *other* matches the expected pattern. This allows :meth:`Mock.called_with`
45
+ to dispatch via Python's equality operator naturally.
46
+ """
47
+
48
+
49
+ class _IsAny(Matcher):
50
+ """Singleton matcher that matches any value."""
51
+
52
+ __slots__ = ()
53
+ _instance: '_IsAny | None' = None
54
+
55
+ def __new__(cls) -> '_IsAny':
56
+ if _IsAny._instance is None:
57
+ _IsAny._instance = super().__new__(cls)
58
+ return _IsAny._instance
59
+
60
+ def __eq__(self, other: Any) -> bool:
61
+ return True
62
+
63
+ def __repr__(self) -> str:
64
+ return 'is_any'
65
+
66
+
67
+ def is_any() -> Matcher:
68
+ """Return a singleton matcher that matches any single value.
69
+
70
+ :returns: A :class:`Matcher` instance.
71
+ """
72
+ return _IsAny()
73
+
74
+
75
+ class _Contains(Matcher):
76
+ """Matcher that checks membership via the ``in`` operator."""
77
+
78
+ __slots__ = ('_value',)
79
+
80
+ def __init__(self, value: Any) -> None:
81
+ object.__setattr__(self, '_value', value)
82
+
83
+ @property
84
+ def value(self) -> Any:
85
+ return object.__getattribute__(self, '_value')
86
+
87
+ def __eq__(self, other: Any) -> bool:
88
+ if isinstance(other, str):
89
+ return self._value in other # type: ignore[operator, attr-defined]
90
+ try:
91
+ return self._value in other # type: ignore[operator, attr-defined]
92
+ except TypeError:
93
+ return False
94
+
95
+ def __repr__(self) -> str:
96
+ return f'contains({self._value!r})' # type: ignore[attr-defined]
97
+
98
+
99
+ def contains(value: Any) -> Matcher:
100
+ """Return a matcher that checks if *value* is a substring of a string or
101
+ an element of a container (list, tuple, set, dict keys).
102
+
103
+ :param value: The value to search for.
104
+ :returns: A :class:`Matcher` instance.
105
+ """
106
+ return _Contains(value)
107
+
108
+
109
+ class _IsGt(Matcher):
110
+ """Value is strictly greater than *n*."""
111
+
112
+ __slots__ = ('_value',)
113
+
114
+ def __init__(self, value: float) -> None:
115
+ object.__setattr__(self, '_value', value)
116
+
117
+ @property
118
+ def value(self) -> float:
119
+ return object.__getattribute__(self, '_value')
120
+
121
+ def __eq__(self, other: Any) -> bool:
122
+ return isinstance(other, (int, float)) and other > self._value # type: ignore[attr-defined]
123
+
124
+ def __repr__(self) -> str:
125
+ return f'is_gt({self._value!r})' # type: ignore[attr-defined]
126
+
127
+
128
+ def is_gt(n: float) -> Matcher:
129
+ """Return a matcher that checks if the value is strictly greater than *n*.
130
+
131
+ :param n: The threshold to compare against.
132
+ :returns: A :class:`Matcher` instance.
133
+ """
134
+ return _IsGt(n)
135
+
136
+
137
+ class _IsGte(Matcher):
138
+ """Value is greater than or equal to *n*."""
139
+
140
+ __slots__ = ('_value',)
141
+
142
+ def __init__(self, value: float) -> None:
143
+ object.__setattr__(self, '_value', value)
144
+
145
+ @property
146
+ def value(self) -> float:
147
+ return object.__getattribute__(self, '_value')
148
+
149
+ def __eq__(self, other: Any) -> bool:
150
+ return isinstance(other, (int, float)) and other >= self._value # type: ignore[attr-defined]
151
+
152
+ def __repr__(self) -> str:
153
+ return f'is_gte({self._value!r})' # type: ignore[attr-defined]
154
+
155
+
156
+ def is_gte(n: float) -> Matcher:
157
+ """Return a matcher that checks if the value is greater than or equal to *n*.
158
+
159
+ :param n: The threshold to compare against.
160
+ :returns: A :class:`Matcher` instance.
161
+ """
162
+ return _IsGte(n)
163
+
164
+
165
+ class _IsLt(Matcher):
166
+ """Value is strictly less than *n*."""
167
+
168
+ __slots__ = ('_value',)
169
+
170
+ def __init__(self, value: float) -> None:
171
+ object.__setattr__(self, '_value', value)
172
+
173
+ @property
174
+ def value(self) -> float:
175
+ return object.__getattribute__(self, '_value')
176
+
177
+ def __eq__(self, other: Any) -> bool:
178
+ return isinstance(other, (int, float)) and other < self._value # type: ignore[attr-defined]
179
+
180
+ def __repr__(self) -> str:
181
+ return f'is_lt({self._value!r})' # type: ignore[attr-defined]
182
+
183
+
184
+ def is_lt(n: float) -> Matcher:
185
+ """Return a matcher that checks if the value is strictly less than *n*.
186
+
187
+ :param n: The threshold to compare against.
188
+ :returns: A :class:`Matcher` instance.
189
+ """
190
+ return _IsLt(n)
191
+
192
+
193
+ class _IsLte(Matcher):
194
+ """Value is less than or equal to *n*."""
195
+
196
+ __slots__ = ('_value',)
197
+
198
+ def __init__(self, value: float) -> None:
199
+ object.__setattr__(self, '_value', value)
200
+
201
+ @property
202
+ def value(self) -> float:
203
+ return object.__getattribute__(self, '_value')
204
+
205
+ def __eq__(self, other: Any) -> bool:
206
+ return isinstance(other, (int, float)) and other <= self._value # type: ignore[attr-defined]
207
+
208
+ def __repr__(self) -> str:
209
+ return f'is_lte({self._value!r})' # type: ignore[attr-defined]
210
+
211
+
212
+ def is_lte(n: float) -> Matcher:
213
+ """Return a matcher that checks if the value is less than or equal to *n*.
214
+
215
+ :param n: The threshold to compare against.
216
+ :returns: A :class:`Matcher` instance.
217
+ """
218
+ return _IsLte(n)
219
+
220
+
221
+ class _IsIn(Matcher):
222
+ """Value equals one of the provided values."""
223
+
224
+ __slots__ = ('_values',)
225
+
226
+ def __init__(self, *values: Any) -> None:
227
+ object.__setattr__(self, '_values', tuple(values))
228
+
229
+ @property
230
+ def values(self) -> tuple[Any, ...]:
231
+ return object.__getattribute__(self, '_values')
232
+
233
+ def __eq__(self, other: Any) -> bool:
234
+ return other in self._values # type: ignore[attr-defined]
235
+
236
+ def __repr__(self) -> str:
237
+ return f'is_in({self._values!r})' # type: ignore[attr-defined]
238
+
239
+
240
+ def is_in(*values: Any) -> Matcher:
241
+ """Return a matcher that checks if the value equals one of the provided values.
242
+
243
+ This is the inverse of :func:`contains` -- ``contains(x)`` checks if x is in the arg,
244
+ while ``is_in(a, b, c)`` checks if the arg is one of a, b, or c.
245
+
246
+ :param values: The candidate values to check against.
247
+ :returns: A :class:`Matcher` instance.
248
+ """
249
+ return _IsIn(*values)
250
+
251
+
252
+ class _IsType(Matcher):
253
+ """Value is an instance of any given type(s)."""
254
+
255
+ __slots__ = ('_types',)
256
+
257
+ def __init__(self, *types: type) -> None:
258
+ object.__setattr__(self, '_types', tuple(types))
259
+
260
+ @property
261
+ def types(self) -> tuple[type, ...]:
262
+ return object.__getattribute__(self, '_types')
263
+
264
+ def __eq__(self, other: Any) -> bool:
265
+ return isinstance(other, self._types) # type: ignore[attr-defined]
266
+
267
+ def __repr__(self) -> str:
268
+ return f'is_type({self._types!r})' # type: ignore[attr-defined]
269
+
270
+
271
+ def is_type(*types: type) -> Matcher:
272
+ """Return a matcher that checks if the value is an instance of any given type(s).
273
+
274
+ :param types: One or more types to check against.
275
+ :returns: A :class:`Matcher` instance.
276
+ """
277
+ return _IsType(*types)
278
+
279
+
280
+ class neg(Matcher):
281
+ """
282
+ Wraps another matcher and negates its result.
283
+
284
+ This enables composing matchers freely -- e.g. ``neg(is_in('admin', 'root'))``
285
+ matches any value that is not 'admin' or 'root'.
286
+
287
+ :param inner: The matcher to negate.
288
+ :returns: A :class:`Matcher` instance.
289
+ """
290
+
291
+ __slots__ = ('_inner',)
292
+
293
+ def __init__(self, inner: Matcher) -> None:
294
+ object.__setattr__(self, '_inner', inner)
295
+
296
+ @property
297
+ def inner(self) -> Matcher:
298
+ return object.__getattribute__(self, '_inner')
299
+
300
+ def __eq__(self, other: Any) -> bool:
301
+ return not self._inner.__eq__(other) # type: ignore[attr-defined]
302
+
303
+ def __repr__(self) -> str:
304
+ return f'neg({self._inner!r})' # type: ignore[attr-defined]
305
+
306
+
307
+ __all__ = [
308
+ 'Matcher',
309
+ 'neg',
310
+ 'is_any',
311
+ 'contains',
312
+ 'is_gt',
313
+ 'is_gte',
314
+ 'is_lt',
315
+ 'is_lte',
316
+ 'is_in',
317
+ 'is_type',
318
+ ]