assertpy2 2.6.0__tar.gz → 2.7.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.
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.gitignore +1 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/PKG-INFO +1 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/_mixin_base.py +2 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/_typing.py +13 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/assertpy.py +3 -2
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/exception.py +29 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/warning.py +6 -3
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/errors.md +11 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/pyproject.toml +2 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_expected_exception.py +14 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_expected_warning.py +26 -0
- assertpy2-2.7.0/tests/test_property_based.py +333 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/uv.lock +25 -1
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.codecov.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.gitattributes +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/LICENSE +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/README.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/SECURITY.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/_compat.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/base.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/date.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/file.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/helpers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/matchers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/assertpy2/string.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/assertions.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/comparison.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/data.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/extending.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/fluent.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/getting-started.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/index.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/integrations.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/logo.svg +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/matchers.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/migration.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/docs/testing.md +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/mkdocs.yml +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_async.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_bool.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_class.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_collection.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_core.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_description.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_dict.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_equals.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_errors.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_fail.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_file.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_in.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_json.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_list.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_none.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_not.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_phase2.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_readme.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_soft.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_string.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_structural.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_type.py +0 -0
- {assertpy2-2.6.0 → assertpy2-2.7.0}/tests/test_warn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -17,6 +17,7 @@ class _MixinBase:
|
|
|
17
17
|
logger: logging.LoggerAdapter
|
|
18
18
|
_not_expected: bool
|
|
19
19
|
_expected_warning: type[Warning] | None
|
|
20
|
+
_return_value: object
|
|
20
21
|
|
|
21
22
|
def error(
|
|
22
23
|
self,
|
|
@@ -32,7 +33,7 @@ class _MixinBase:
|
|
|
32
33
|
val: object,
|
|
33
34
|
description: str = ...,
|
|
34
35
|
kind: str | None = ...,
|
|
35
|
-
expected: BaseException | None = ...,
|
|
36
|
+
expected: type[BaseException] | None = ...,
|
|
36
37
|
logger: logging.LoggerAdapter | None = ...,
|
|
37
38
|
) -> Self: ...
|
|
38
39
|
|
|
@@ -215,6 +215,17 @@ if TYPE_CHECKING:
|
|
|
215
215
|
def is_hex_equal_to(self, expected_hex: str) -> Self: ...
|
|
216
216
|
def decoded_as(self, encoding: str = ...) -> _StringAssertion: ...
|
|
217
217
|
|
|
218
|
+
class _InvokedAssertion(_StringAssertion, Protocol):
|
|
219
|
+
"""Assertions available after ``when_called_with()`` captured an exception/warning message.
|
|
220
|
+
|
|
221
|
+
The captured message is a ``str`` (hence the string assertions); :meth:`returned` pivots to
|
|
222
|
+
the value the callable returned. Its static type is unknown, so ``returned()`` exposes the
|
|
223
|
+
type-agnostic core assertions (``is_equal_to``, ``is_instance_of``, ``satisfies``, ...) -
|
|
224
|
+
type-safe by construction, never advertising methods that may not apply.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def returned(self) -> _CoreAssertion: ...
|
|
228
|
+
|
|
218
229
|
class _CallableAssertion(_CoreAssertion, Protocol):
|
|
219
230
|
"""Assertions available for callable values."""
|
|
220
231
|
|
|
@@ -222,5 +233,6 @@ if TYPE_CHECKING:
|
|
|
222
233
|
def does_not_raise(self, ex: type) -> Self: ...
|
|
223
234
|
def warns(self, warning: type[Warning] = ...) -> Self: ...
|
|
224
235
|
def does_not_warn(self, warning: type[Warning] = ...) -> Self: ...
|
|
225
|
-
|
|
236
|
+
# captured value (exception or warning message) is a str; returned() pivots to the call result
|
|
237
|
+
def when_called_with(self, *some_args: object, **some_kwargs: object) -> _InvokedAssertion: ...
|
|
226
238
|
def eventually(self, *, timeout: float = ..., interval: float = ...) -> AsyncAssertionBuilder: ...
|
|
@@ -37,7 +37,7 @@ from .date import DateMixin
|
|
|
37
37
|
from .dict import DictMixin
|
|
38
38
|
from .dynamic import DynamicMixin
|
|
39
39
|
from .errors import AssertionFailure
|
|
40
|
-
from .exception import ExceptionMixin
|
|
40
|
+
from .exception import _UNSET, ExceptionMixin
|
|
41
41
|
from .extracting import ExtractingMixin
|
|
42
42
|
from .file import FileMixin
|
|
43
43
|
from .helpers import HelpersMixin
|
|
@@ -47,7 +47,7 @@ from .snapshot import SnapshotMixin
|
|
|
47
47
|
from .string import StringMixin
|
|
48
48
|
from .warning import WarningMixin
|
|
49
49
|
|
|
50
|
-
__version__ = "2.
|
|
50
|
+
__version__ = "2.7.0"
|
|
51
51
|
|
|
52
52
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
53
53
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
@@ -589,6 +589,7 @@ class AssertionBuilder(
|
|
|
589
589
|
self.logger = logger if logger else _default_logger
|
|
590
590
|
self._not_expected = False
|
|
591
591
|
self._expected_warning = None
|
|
592
|
+
self._return_value = _UNSET
|
|
592
593
|
|
|
593
594
|
@property
|
|
594
595
|
def not_(self) -> NegatedBuilder:
|
|
@@ -9,6 +9,8 @@ if TYPE_CHECKING:
|
|
|
9
9
|
|
|
10
10
|
__tracebackhide__ = True
|
|
11
11
|
|
|
12
|
+
_UNSET = object() # sentinel: no return value captured yet
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class _InertBuilder:
|
|
14
16
|
"""No-op builder returned after a failed raises/when_called_with in soft mode.
|
|
@@ -105,10 +107,34 @@ class ExceptionMixin(_MixinBase):
|
|
|
105
107
|
)
|
|
106
108
|
return cast("Self", _InertBuilder())
|
|
107
109
|
|
|
110
|
+
def returned(self) -> Self:
|
|
111
|
+
"""Pivots the chain to the value ``val()`` returned during :meth:`when_called_with`.
|
|
112
|
+
|
|
113
|
+
Use after a call that completed normally (:meth:`~assertpy2.warning.WarningMixin.warns`,
|
|
114
|
+
:meth:`~assertpy2.warning.WarningMixin.does_not_warn`, or :meth:`does_not_raise`) to assert
|
|
115
|
+
on the return value in the same chain.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
Usage::
|
|
119
|
+
|
|
120
|
+
assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
|
|
121
|
+
assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
AssertionBuilder: a new instance wrapping the captured return value
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
TypeError: if no return value was captured (the call raised, or :meth:`when_called_with`
|
|
128
|
+
was not invoked first)
|
|
129
|
+
"""
|
|
130
|
+
if self._return_value is _UNSET:
|
|
131
|
+
raise TypeError("no return value captured; returned() is only valid after a call that completed normally")
|
|
132
|
+
return self.builder(self._return_value, self.description, self.kind, logger=self.logger)
|
|
133
|
+
|
|
108
134
|
def _when_called_with_not_expected(self, *some_args, **some_kwargs) -> Self:
|
|
109
135
|
assert self.expected is not None
|
|
110
136
|
try:
|
|
111
|
-
self.val(*some_args, **some_kwargs)
|
|
137
|
+
result = self.val(*some_args, **some_kwargs)
|
|
112
138
|
except BaseException as e:
|
|
113
139
|
if issubclass(type(e), self.expected):
|
|
114
140
|
self.error(
|
|
@@ -117,6 +143,8 @@ class ExceptionMixin(_MixinBase):
|
|
|
117
143
|
f" but did raise <{type(e).__name__}>."
|
|
118
144
|
)
|
|
119
145
|
return cast("Self", _InertBuilder())
|
|
146
|
+
return self
|
|
147
|
+
self._return_value = result
|
|
120
148
|
return self
|
|
121
149
|
|
|
122
150
|
def does_not_raise(self, ex) -> Self:
|
|
@@ -89,10 +89,12 @@ class WarningMixin(_MixinBase):
|
|
|
89
89
|
def _when_called_with_warning(self, expected: type[Warning], *some_args, **some_kwargs) -> Self:
|
|
90
90
|
with warnings.catch_warnings(record=True) as caught:
|
|
91
91
|
warnings.simplefilter("always") # bypass __warningregistry__ "show once" dedup and filterwarnings=error
|
|
92
|
-
self.val(*some_args, **some_kwargs)
|
|
92
|
+
result = self.val(*some_args, **some_kwargs)
|
|
93
93
|
matched = [w for w in caught if issubclass(w.category, expected)]
|
|
94
94
|
if matched:
|
|
95
|
-
|
|
95
|
+
captured = self.builder(str(matched[0].message), self.description, self.kind, logger=self.logger)
|
|
96
|
+
captured._return_value = result
|
|
97
|
+
return captured
|
|
96
98
|
if caught:
|
|
97
99
|
seen = ", ".join(sorted({w.category.__name__ for w in caught}))
|
|
98
100
|
self.error(
|
|
@@ -110,7 +112,7 @@ class WarningMixin(_MixinBase):
|
|
|
110
112
|
def _when_called_with_not_warning(self, expected: type[Warning], *some_args, **some_kwargs) -> Self:
|
|
111
113
|
with warnings.catch_warnings(record=True) as caught:
|
|
112
114
|
warnings.simplefilter("always") # bypass __warningregistry__ "show once" dedup and filterwarnings=error
|
|
113
|
-
self.val(*some_args, **some_kwargs)
|
|
115
|
+
result = self.val(*some_args, **some_kwargs)
|
|
114
116
|
matched = [w for w in caught if issubclass(w.category, expected)]
|
|
115
117
|
if matched:
|
|
116
118
|
seen = ", ".join(sorted({w.category.__name__ for w in matched}))
|
|
@@ -120,4 +122,5 @@ class WarningMixin(_MixinBase):
|
|
|
120
122
|
f" but did warn <{seen}>."
|
|
121
123
|
)
|
|
122
124
|
return cast("Self", _InertBuilder())
|
|
125
|
+
self._return_value = result
|
|
123
126
|
return self
|
|
@@ -119,6 +119,17 @@ The category defaults to `Warning` (matches any warning) and matches subclasses.
|
|
|
119
119
|
assert_that(safe_func).does_not_warn(DeprecationWarning).when_called_with("foo")
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
To also assert on the value the call returned (alongside the warning, or after `does_not_warn` /
|
|
123
|
+
`does_not_raise`), pivot with `returned()`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
|
|
127
|
+
assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`returned()` exposes the type-agnostic core assertions (`is_equal_to`, `is_instance_of`, `satisfies`,
|
|
131
|
+
...); it raises `TypeError` if the call raised (there is no return value to inspect).
|
|
132
|
+
|
|
122
133
|
!!! warning "Not thread-safe"
|
|
123
134
|
`warns()` / `does_not_warn()` rely on `warnings.catch_warnings()`, which mutates process-global
|
|
124
135
|
state. They are safe within a single thread (including multiple `asyncio` tasks on one event
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "assertpy2"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.7.0"
|
|
4
4
|
description = "Fluent assertion library for Python with composable matchers, structural matching, and full type safety"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "BSD-3-Clause"
|
|
@@ -53,6 +53,7 @@ build-backend = "hatchling.build"
|
|
|
53
53
|
|
|
54
54
|
[dependency-groups]
|
|
55
55
|
dev = [
|
|
56
|
+
"hypothesis>=6.155.6",
|
|
56
57
|
"pytest>=9.1.1",
|
|
57
58
|
"pytest-cov>=6.1",
|
|
58
59
|
"ruff>=0.15.18",
|
|
@@ -161,3 +161,17 @@ def test_expected_exception_warn_wrong_type_preserves_logger():
|
|
|
161
161
|
class Foo:
|
|
162
162
|
def bar(self):
|
|
163
163
|
raise RuntimeError("method err")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def safe_add(value):
|
|
167
|
+
return value + 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_does_not_raise_returned_pivots_to_return_value():
|
|
171
|
+
assert_that(safe_add).does_not_raise(ValueError).when_called_with(41).returned().is_equal_to(42)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_returned_without_return_value_fails():
|
|
175
|
+
with pytest.raises(TypeError) as exc_info:
|
|
176
|
+
assert_that(func_no_arg).raises(RuntimeError).when_called_with().returned()
|
|
177
|
+
assert_that(str(exc_info.value)).contains("no return value captured")
|
|
@@ -175,3 +175,29 @@ def test_does_not_warn_failure_in_warn_mode_logs():
|
|
|
175
175
|
assert_that(out).contains(
|
|
176
176
|
"Expected <warn_user> to not warn <UserWarning> when called with (), but did warn <UserWarning>."
|
|
177
177
|
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# returned() pivot to the callable's return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def warn_and_return_list(*args, **kwargs):
|
|
184
|
+
warnings.warn("dep since 2.7", DeprecationWarning, stacklevel=2)
|
|
185
|
+
return [1, 2, 3]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def quiet_return_ok(*args, **kwargs):
|
|
189
|
+
return "ok"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_warns_returned_pivots_to_return_value():
|
|
193
|
+
assert_that(warn_and_return_list).warns(DeprecationWarning).when_called_with().returned().is_equal_to([1, 2, 3])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_warns_returned_after_message_assertion():
|
|
197
|
+
assert_that(warn_and_return_list).warns(DeprecationWarning).when_called_with().matches(
|
|
198
|
+
"since 2.7"
|
|
199
|
+
).returned().is_equal_to([1, 2, 3])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_does_not_warn_returned_pivots_to_return_value():
|
|
203
|
+
assert_that(quiet_return_ok).does_not_warn(DeprecationWarning).when_called_with().returned().is_equal_to("ok")
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Property-based tests (Hypothesis) for the riskiest pure-logic surfaces.
|
|
2
|
+
|
|
3
|
+
These complement the example-based suite: 100% line coverage does not exercise the *space* of
|
|
4
|
+
values, which is where recursive equality, ignore/include filtering, and matcher algebra hide bugs.
|
|
5
|
+
Each test states an invariant and lets Hypothesis attack it with generated data; on failure the
|
|
6
|
+
shrunk counterexample plus assertpy2's structured ``AssertionFailure`` pinpoint the mismatch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
from collections import namedtuple
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from hypothesis import given, settings
|
|
15
|
+
from hypothesis import strategies as st
|
|
16
|
+
|
|
17
|
+
from assertpy2 import assert_that, match
|
|
18
|
+
|
|
19
|
+
# JSON-like values: atoms plus nested lists/dicts. NaN is excluded so equality stays reflexive.
|
|
20
|
+
_atoms = st.none() | st.booleans() | st.integers() | st.floats(allow_nan=False) | st.text()
|
|
21
|
+
_values = st.recursive(
|
|
22
|
+
_atoms,
|
|
23
|
+
lambda children: st.lists(children) | st.dictionaries(st.text(), children),
|
|
24
|
+
max_leaves=20,
|
|
25
|
+
)
|
|
26
|
+
_keys = st.text(min_size=1, max_size=5)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- is_equal_to / is_not_equal_to mirror Python equality ---
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@settings(deadline=None)
|
|
33
|
+
@given(value=_values)
|
|
34
|
+
def test_is_equal_to_is_reflexive(value):
|
|
35
|
+
assert_that(value).is_equal_to(copy.deepcopy(value))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@settings(deadline=None)
|
|
39
|
+
@given(left=_values, right=_values)
|
|
40
|
+
def test_is_equal_to_consistent_with_eq(left, right):
|
|
41
|
+
if left == right:
|
|
42
|
+
assert_that(left).is_equal_to(right)
|
|
43
|
+
else:
|
|
44
|
+
with pytest.raises(AssertionError):
|
|
45
|
+
assert_that(left).is_equal_to(right)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@settings(deadline=None)
|
|
49
|
+
@given(left=_values, right=_values)
|
|
50
|
+
def test_is_not_equal_to_is_the_inverse(left, right):
|
|
51
|
+
if left != right:
|
|
52
|
+
assert_that(left).is_not_equal_to(right)
|
|
53
|
+
else:
|
|
54
|
+
with pytest.raises(AssertionError):
|
|
55
|
+
assert_that(left).is_not_equal_to(right)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@given(left=st.sets(st.integers()), right=st.sets(st.integers()))
|
|
59
|
+
def test_is_equal_to_consistent_with_eq_for_sets(left, right):
|
|
60
|
+
if left == right:
|
|
61
|
+
assert_that(left).is_equal_to(right)
|
|
62
|
+
else:
|
|
63
|
+
with pytest.raises(AssertionError):
|
|
64
|
+
assert_that(left).is_equal_to(right)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --- ignore / include selective comparison ---
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@settings(deadline=None)
|
|
71
|
+
@given(base=st.dictionaries(_keys, _atoms, max_size=6), perturbations=st.dictionaries(_keys, _atoms, max_size=4))
|
|
72
|
+
def test_ignore_removes_differences(base, perturbations):
|
|
73
|
+
# left keeps base values; right overlays perturbations. Ignoring exactly the perturbed keys
|
|
74
|
+
# leaves both dicts identical on the remaining (base) keys, so they compare equal.
|
|
75
|
+
left = dict(base)
|
|
76
|
+
right = {**base, **perturbations}
|
|
77
|
+
assert_that(left).is_equal_to(right, ignore=list(perturbations))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@settings(deadline=None)
|
|
81
|
+
@given(
|
|
82
|
+
common=st.dictionaries(_keys, _atoms, min_size=1, max_size=4),
|
|
83
|
+
left_extra=st.dictionaries(_keys, _atoms, max_size=3),
|
|
84
|
+
right_extra=st.dictionaries(_keys, _atoms, max_size=3),
|
|
85
|
+
)
|
|
86
|
+
def test_include_compares_only_listed_keys(common, left_extra, right_extra):
|
|
87
|
+
# both dicts agree on every common key; extras (outside the common set) differ but are excluded.
|
|
88
|
+
included = list(common)
|
|
89
|
+
left = {**{k: v for k, v in left_extra.items() if k not in common}, **common}
|
|
90
|
+
right = {**{k: v for k, v in right_extra.items() if k not in common}, **common}
|
|
91
|
+
assert_that(left).is_equal_to(right, include=included)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# --- matcher predicates mirror their Python semantics ---
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@given(value=st.integers(), low=st.integers(), high=st.integers())
|
|
98
|
+
def test_between_matcher_matches_python_semantics(value, low, high):
|
|
99
|
+
assert match.between(low, high).matches(value) == (low <= value <= high)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@given(value=st.integers(), boundary=st.integers())
|
|
103
|
+
def test_greater_than_matcher_matches_python_semantics(value, boundary):
|
|
104
|
+
assert match.greater_than(boundary).matches(value) == (value > boundary)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@given(value=st.integers(), low=st.integers(), high=st.integers())
|
|
108
|
+
def test_satisfies_between_consistent_with_semantics(value, low, high):
|
|
109
|
+
if low <= value <= high:
|
|
110
|
+
assert_that(value).satisfies(match.between(low, high))
|
|
111
|
+
else:
|
|
112
|
+
with pytest.raises(AssertionError):
|
|
113
|
+
assert_that(value).satisfies(match.between(low, high))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --- matcher boolean algebra (& | ~) ---
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@given(value=st.integers(), boundary_low=st.integers(), boundary_high=st.integers())
|
|
120
|
+
def test_matcher_combinators_follow_boolean_logic(value, boundary_low, boundary_high):
|
|
121
|
+
matcher_a = match.greater_than(boundary_low)
|
|
122
|
+
matcher_b = match.less_than(boundary_high)
|
|
123
|
+
result_a = matcher_a.matches(value)
|
|
124
|
+
result_b = matcher_b.matches(value)
|
|
125
|
+
|
|
126
|
+
assert (matcher_a & matcher_b).matches(value) == (result_a and result_b)
|
|
127
|
+
assert (matcher_a | matcher_b).matches(value) == (result_a or result_b)
|
|
128
|
+
assert (~matcher_a).matches(value) == (not result_a)
|
|
129
|
+
# de Morgan: ~(a & b) == (~a | ~b)
|
|
130
|
+
assert (~(matcher_a & matcher_b)).matches(value) == ((~matcher_a) | (~matcher_b)).matches(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --- collection roundtrips ---
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@given(items=st.lists(st.integers()))
|
|
137
|
+
def test_is_length_matches_len(items):
|
|
138
|
+
assert_that(items).is_length(len(items))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@given(items=st.lists(st.integers(), min_size=1))
|
|
142
|
+
def test_contains_every_element(items):
|
|
143
|
+
assert_that(items).contains(*items)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@given(items=st.lists(st.integers()), candidate=st.integers())
|
|
147
|
+
def test_contains_present_iff_member(items, candidate):
|
|
148
|
+
if candidate in items:
|
|
149
|
+
assert_that(items).contains(candidate)
|
|
150
|
+
else:
|
|
151
|
+
with pytest.raises(AssertionError):
|
|
152
|
+
assert_that(items).contains(candidate)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# === Point 1: recursive comparison over dataclasses / namedtuples ===
|
|
156
|
+
# Exercises the riskiest code: _build_equality_diff / _sub_diff_entries (nesting, models, lists).
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class _Inner:
|
|
161
|
+
a: int
|
|
162
|
+
b: str
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class _Outer:
|
|
167
|
+
inner: _Inner
|
|
168
|
+
items: list[int]
|
|
169
|
+
name: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
_Pair = namedtuple("_Pair", ["first", "second"])
|
|
173
|
+
|
|
174
|
+
_inners = st.builds(_Inner, a=st.integers(), b=st.text(max_size=5))
|
|
175
|
+
_outers = st.builds(_Outer, inner=_inners, items=st.lists(st.integers(), max_size=4), name=st.text(max_size=5))
|
|
176
|
+
_pairs = st.builds(_Pair, first=st.integers(), second=st.text(max_size=5))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@settings(deadline=None)
|
|
180
|
+
@given(left=_outers, right=_outers)
|
|
181
|
+
def test_dataclass_is_equal_to_consistent_with_eq(left, right):
|
|
182
|
+
if left == right:
|
|
183
|
+
assert_that(left).is_equal_to(right)
|
|
184
|
+
else:
|
|
185
|
+
with pytest.raises(AssertionError):
|
|
186
|
+
assert_that(left).is_equal_to(right)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@settings(deadline=None)
|
|
190
|
+
@given(value=_outers)
|
|
191
|
+
def test_dataclass_is_equal_to_reflexive(value):
|
|
192
|
+
assert_that(value).is_equal_to(copy.deepcopy(value))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@settings(deadline=None)
|
|
196
|
+
@given(value=_outers, new_name=st.text(max_size=5))
|
|
197
|
+
def test_dataclass_ignore_removes_field_difference(value, new_name):
|
|
198
|
+
other = replace(value, name=new_name)
|
|
199
|
+
assert_that(value).is_equal_to(other, ignore="name")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@given(left=_pairs, right=_pairs)
|
|
203
|
+
def test_namedtuple_is_equal_to_consistent_with_eq(left, right):
|
|
204
|
+
if left == right:
|
|
205
|
+
assert_that(left).is_equal_to(right)
|
|
206
|
+
else:
|
|
207
|
+
with pytest.raises(AssertionError):
|
|
208
|
+
assert_that(left).is_equal_to(right)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@given(value=_pairs)
|
|
212
|
+
def test_namedtuple_is_equal_to_reflexive(value):
|
|
213
|
+
assert_that(value).is_equal_to(copy.deepcopy(value))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# === Point 2: multiset / ordering semantics of collection assertions ===
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _is_subsequence(sub, seq):
|
|
220
|
+
# shared iterator advances across the any() calls - the classic subsequence check
|
|
221
|
+
iterator = iter(seq)
|
|
222
|
+
return all(any(candidate == element for candidate in iterator) for element in sub)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@given(val=st.lists(st.integers(), max_size=6), expected=st.lists(st.integers(), min_size=1, max_size=6))
|
|
226
|
+
def test_contains_exactly_matches_list_equality(val, expected):
|
|
227
|
+
if val == expected:
|
|
228
|
+
assert_that(val).contains_exactly(*expected)
|
|
229
|
+
else:
|
|
230
|
+
with pytest.raises(AssertionError):
|
|
231
|
+
assert_that(val).contains_exactly(*expected)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@given(val=st.lists(st.integers(), max_size=6), items=st.lists(st.integers(), min_size=1, max_size=6))
|
|
235
|
+
def test_contains_only_matches_set_equality(val, items):
|
|
236
|
+
if set(val) == set(items):
|
|
237
|
+
assert_that(val).contains_only(*items)
|
|
238
|
+
else:
|
|
239
|
+
with pytest.raises(AssertionError):
|
|
240
|
+
assert_that(val).contains_only(*items)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@given(val=st.lists(st.integers(), max_size=8), items=st.lists(st.integers(), min_size=1, max_size=4))
|
|
244
|
+
def test_contains_sequence_matches_contiguous_slice(val, items):
|
|
245
|
+
window = len(items)
|
|
246
|
+
is_contiguous = any(val[index : index + window] == items for index in range(len(val) - window + 1))
|
|
247
|
+
if is_contiguous:
|
|
248
|
+
assert_that(val).contains_sequence(*items)
|
|
249
|
+
else:
|
|
250
|
+
with pytest.raises(AssertionError):
|
|
251
|
+
assert_that(val).contains_sequence(*items)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@given(val=st.lists(st.integers(), max_size=8), items=st.lists(st.integers(), min_size=1, max_size=4))
|
|
255
|
+
def test_contains_in_order_matches_subsequence(val, items):
|
|
256
|
+
if _is_subsequence(items, val):
|
|
257
|
+
assert_that(val).contains_in_order(*items)
|
|
258
|
+
else:
|
|
259
|
+
with pytest.raises(AssertionError):
|
|
260
|
+
assert_that(val).contains_in_order(*items)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@given(items=st.lists(st.integers(), max_size=10))
|
|
264
|
+
def test_does_not_contain_duplicates_iff_unique(items):
|
|
265
|
+
if len(items) == len(set(items)):
|
|
266
|
+
assert_that(items).does_not_contain_duplicates()
|
|
267
|
+
else:
|
|
268
|
+
with pytest.raises(AssertionError):
|
|
269
|
+
assert_that(items).does_not_contain_duplicates()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@given(items=st.lists(st.integers(), max_size=10))
|
|
273
|
+
def test_is_sorted_matches_python_sorted(items):
|
|
274
|
+
if items == sorted(items):
|
|
275
|
+
assert_that(items).is_sorted()
|
|
276
|
+
else:
|
|
277
|
+
with pytest.raises(AssertionError):
|
|
278
|
+
assert_that(items).is_sorted()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@given(items=st.lists(st.integers(), max_size=10))
|
|
282
|
+
def test_is_sorted_reverse_matches_python_sorted(items):
|
|
283
|
+
if items == sorted(items, reverse=True):
|
|
284
|
+
assert_that(items).is_sorted(reverse=True)
|
|
285
|
+
else:
|
|
286
|
+
with pytest.raises(AssertionError):
|
|
287
|
+
assert_that(items).is_sorted(reverse=True)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@given(items=st.lists(st.integers(), max_size=8), superset=st.lists(st.integers(), max_size=10))
|
|
291
|
+
def test_is_subset_of_matches_set_subset(items, superset):
|
|
292
|
+
if set(items) <= set(superset):
|
|
293
|
+
assert_that(items).is_subset_of(superset)
|
|
294
|
+
else:
|
|
295
|
+
with pytest.raises(AssertionError):
|
|
296
|
+
assert_that(items).is_subset_of(superset)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# === Point 3: nested ignore via tuple key-paths (recursion in _dict_not_equal) ===
|
|
300
|
+
|
|
301
|
+
_two_level = st.dictionaries(
|
|
302
|
+
st.text(min_size=1, max_size=3),
|
|
303
|
+
st.dictionaries(st.text(min_size=1, max_size=3), st.integers(), max_size=3),
|
|
304
|
+
max_size=3,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@settings(deadline=None)
|
|
309
|
+
@given(base=_two_level, data=st.data())
|
|
310
|
+
def test_nested_ignore_removes_leaf_differences(base, data):
|
|
311
|
+
paths = [(outer, leaf) for outer, inner in base.items() for leaf in inner]
|
|
312
|
+
perturbed = data.draw(st.lists(st.sampled_from(paths), unique=True)) if paths else []
|
|
313
|
+
right = {outer: dict(inner) for outer, inner in base.items()}
|
|
314
|
+
for outer, leaf in perturbed:
|
|
315
|
+
right[outer][leaf] += 1000 # ints, so always a real difference
|
|
316
|
+
# ignoring exactly the perturbed leaf-paths leaves both dicts identical elsewhere
|
|
317
|
+
assert_that(base).is_equal_to(right, ignore=list(perturbed))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@settings(deadline=None)
|
|
321
|
+
@given(base=_two_level, data=st.data())
|
|
322
|
+
def test_nested_include_compares_only_listed_leaves(base, data):
|
|
323
|
+
paths = [(outer, leaf) for outer, inner in base.items() for leaf in inner]
|
|
324
|
+
if not paths:
|
|
325
|
+
return # include needs at least one referenced path; nothing to compare on an empty dict
|
|
326
|
+
included = data.draw(st.lists(st.sampled_from(paths), unique=True, min_size=1))
|
|
327
|
+
included_set = set(included)
|
|
328
|
+
right = {outer: dict(inner) for outer, inner in base.items()}
|
|
329
|
+
for outer, leaf in paths:
|
|
330
|
+
if (outer, leaf) not in included_set:
|
|
331
|
+
right[outer][leaf] += 1000 # perturb only NON-included leaves (keys untouched)
|
|
332
|
+
# comparing only the included leaf-paths -> equal, since those leaves are left intact
|
|
333
|
+
assert_that(base).is_equal_to(right, include=list(included))
|
|
@@ -34,7 +34,7 @@ wheels = [
|
|
|
34
34
|
|
|
35
35
|
[[package]]
|
|
36
36
|
name = "assertpy2"
|
|
37
|
-
version = "2.
|
|
37
|
+
version = "2.7.0"
|
|
38
38
|
source = { editable = "." }
|
|
39
39
|
dependencies = [
|
|
40
40
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
|
@@ -54,6 +54,7 @@ json = [
|
|
|
54
54
|
|
|
55
55
|
[package.dev-dependencies]
|
|
56
56
|
dev = [
|
|
57
|
+
{ name = "hypothesis" },
|
|
57
58
|
{ name = "pytest" },
|
|
58
59
|
{ name = "pytest-cov" },
|
|
59
60
|
{ name = "ruff" },
|
|
@@ -76,6 +77,7 @@ provides-extras = ["allure", "behave", "json"]
|
|
|
76
77
|
|
|
77
78
|
[package.metadata.requires-dev]
|
|
78
79
|
dev = [
|
|
80
|
+
{ name = "hypothesis", specifier = ">=6.155.6" },
|
|
79
81
|
{ name = "pytest", specifier = ">=9.1.1" },
|
|
80
82
|
{ name = "pytest-cov", specifier = ">=6.1" },
|
|
81
83
|
{ name = "ruff", specifier = ">=0.15.18" },
|
|
@@ -428,6 +430,19 @@ wheels = [
|
|
|
428
430
|
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
|
|
429
431
|
]
|
|
430
432
|
|
|
433
|
+
[[package]]
|
|
434
|
+
name = "hypothesis"
|
|
435
|
+
version = "6.155.6"
|
|
436
|
+
source = { registry = "https://pypi.org/simple" }
|
|
437
|
+
dependencies = [
|
|
438
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
439
|
+
{ name = "sortedcontainers" },
|
|
440
|
+
]
|
|
441
|
+
sdist = { url = "https://files.pythonhosted.org/packages/10/aa/9a91a4addf285702a98713da44b3581799539426436617bfb8914478c166/hypothesis-6.155.6.tar.gz", hash = "sha256:7569e1897690336c85d49d8391b49ec6ab83d951009515bfc29faebbac286cf5", size = 478038, upload-time = "2026-06-19T13:21:23.379Z" }
|
|
442
|
+
wheels = [
|
|
443
|
+
{ url = "https://files.pythonhosted.org/packages/9e/a9/4c17e962c2e9cbc314bb579ed2e2b2da45d7b6b942aab6948d14d85abfea/hypothesis-6.155.6-py3-none-any.whl", hash = "sha256:a96d9a29f6bbc8ccac39dd84e140892da76765464929f401a4181b90c20c9ad1", size = 544521, upload-time = "2026-06-19T13:21:20.934Z" },
|
|
444
|
+
]
|
|
445
|
+
|
|
431
446
|
[[package]]
|
|
432
447
|
name = "idna"
|
|
433
448
|
version = "3.18"
|
|
@@ -1205,6 +1220,15 @@ wheels = [
|
|
|
1205
1220
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
|
1206
1221
|
]
|
|
1207
1222
|
|
|
1223
|
+
[[package]]
|
|
1224
|
+
name = "sortedcontainers"
|
|
1225
|
+
version = "2.4.0"
|
|
1226
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1227
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
|
1228
|
+
wheels = [
|
|
1229
|
+
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
|
1230
|
+
]
|
|
1231
|
+
|
|
1208
1232
|
[[package]]
|
|
1209
1233
|
name = "tomli"
|
|
1210
1234
|
version = "2.4.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|