assertpy2 2.3.1__tar.gz → 2.3.2__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.3.1 → assertpy2-2.3.2}/.github/workflows/ci.yml +3 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/PKG-INFO +3 -3
- {assertpy2-2.3.1 → assertpy2-2.3.2}/README.md +1 -1
- assertpy2-2.3.2/assertpy2/_mixin_base.py +89 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/assertpy.py +3 -2
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/base.py +2 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/behave_matchers.py +1 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/collection.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/contains.py +2 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/date.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/dict.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/dynamic.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/exception.py +8 -5
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/extracting.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/file.py +4 -7
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/helpers.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/matchers.py +54 -54
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/numeric.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/pytest_plugin.py +1 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/snapshot.py +8 -3
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/string.py +3 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/pyproject.toml +6 -3
- {assertpy2-2.3.1 → assertpy2-2.3.2}/uv.lock +28 -1
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.codecov.yml +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/dependabot.yml +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/.gitignore +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/CONTRIBUTING.md +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/LICENSE +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/SECURITY.md +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/__init__.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/_typing.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/api.md +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/docs/logo.svg +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_async.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_bool.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_class.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_collection.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_core.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_custom_list.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_datetime.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_description.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dict.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_dyn.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_equals.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_errors.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_extensions.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_extracting.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_fail.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_file.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_in.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_list.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_matchers.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_none.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_numbers.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_overloads.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_readme.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_same_as.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_snapshots.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_soft.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_string.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_structural.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_traceback.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_type.py +0 -0
- {assertpy2-2.3.1 → assertpy2-2.3.2}/tests/test_warn.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.3.
|
|
4
|
-
Summary: Fluent assertion library for Python with
|
|
3
|
+
Version: 2.3.2
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
|
|
@@ -52,9 +52,9 @@ Description-Content-Type: text/markdown
|
|
|
52
52
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
53
53
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
54
54
|
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
55
|
-
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
|
|
56
55
|
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
|
|
57
56
|
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
|
|
57
|
+
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/badge/type--checker-ty-blue" alt="ty"></a>
|
|
58
58
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
59
59
|
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
60
60
|
</p>
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
18
18
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
19
19
|
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
20
|
-
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Solganis/assertpy2" alt="License"></a>
|
|
21
20
|
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
|
|
22
21
|
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
|
|
22
|
+
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/badge/type--checker-ty-blue" alt="ty"></a>
|
|
23
23
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
24
24
|
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
25
25
|
</p>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from .errors import DiffResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MixinBase:
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
val: Any
|
|
15
|
+
description: str
|
|
16
|
+
kind: str | None
|
|
17
|
+
expected: type[BaseException] | None
|
|
18
|
+
logger: logging.LoggerAdapter
|
|
19
|
+
_not_expected: bool
|
|
20
|
+
|
|
21
|
+
def error(
|
|
22
|
+
self,
|
|
23
|
+
msg: str,
|
|
24
|
+
*,
|
|
25
|
+
actual: object = ...,
|
|
26
|
+
expected: object = ...,
|
|
27
|
+
diff: DiffResult | None = ...,
|
|
28
|
+
) -> Self: ...
|
|
29
|
+
|
|
30
|
+
def builder(
|
|
31
|
+
self,
|
|
32
|
+
val: object,
|
|
33
|
+
description: str = ...,
|
|
34
|
+
kind: str | None = ...,
|
|
35
|
+
expected: BaseException | None = ...,
|
|
36
|
+
logger: logging.LoggerAdapter | None = ...,
|
|
37
|
+
) -> Self: ...
|
|
38
|
+
|
|
39
|
+
# HelpersMixin
|
|
40
|
+
|
|
41
|
+
def _fmt_items(self, i: object) -> str: ...
|
|
42
|
+
|
|
43
|
+
def _fmt_args_kwargs(self, *some_args: object, **some_kwargs: object) -> str: ...
|
|
44
|
+
|
|
45
|
+
def _validate_between_args(self, val_type: type, low: object, high: object) -> None: ...
|
|
46
|
+
|
|
47
|
+
def _validate_close_to_args(self, val: object, other: object, tolerance: object) -> None: ...
|
|
48
|
+
|
|
49
|
+
def _check_dict_like(
|
|
50
|
+
self,
|
|
51
|
+
d: object,
|
|
52
|
+
check_keys: bool = ...,
|
|
53
|
+
check_values: bool = ...,
|
|
54
|
+
check_getitem: bool = ...,
|
|
55
|
+
name: str = ...,
|
|
56
|
+
return_as_bool: bool = ...,
|
|
57
|
+
) -> bool | None: ...
|
|
58
|
+
|
|
59
|
+
def _check_iterable(self, val: object, check_getitem: bool = ..., name: str = ...) -> None: ...
|
|
60
|
+
|
|
61
|
+
def _dict_not_equal(
|
|
62
|
+
self,
|
|
63
|
+
val: object,
|
|
64
|
+
other: object,
|
|
65
|
+
ignore: object = ...,
|
|
66
|
+
include: object = ...,
|
|
67
|
+
) -> bool: ...
|
|
68
|
+
|
|
69
|
+
def _dict_err(
|
|
70
|
+
self,
|
|
71
|
+
val: object,
|
|
72
|
+
other: object,
|
|
73
|
+
ignore: object = ...,
|
|
74
|
+
include: object = ...,
|
|
75
|
+
) -> None: ...
|
|
76
|
+
|
|
77
|
+
# NumericMixin class attrs used by HelpersMixin._validate_between_args
|
|
78
|
+
_NUMERIC_COMPAREABLE: frozenset[type]
|
|
79
|
+
_NUMERIC_NON_COMPAREABLE: frozenset[type]
|
|
80
|
+
|
|
81
|
+
# Cross-mixin methods
|
|
82
|
+
|
|
83
|
+
def contains(self, *items: object) -> Self: ...
|
|
84
|
+
|
|
85
|
+
def does_not_contain(self, *items: object) -> Self: ...
|
|
86
|
+
|
|
87
|
+
def is_equal_to(self, other: object, **kwargs: object) -> Self: ...
|
|
88
|
+
|
|
89
|
+
def is_not_equal_to(self, other: object) -> Self: ...
|
|
@@ -73,10 +73,10 @@ from .numeric import NumericMixin
|
|
|
73
73
|
from .snapshot import SnapshotMixin
|
|
74
74
|
from .string import StringMixin
|
|
75
75
|
|
|
76
|
-
__version__ = "2.3.
|
|
76
|
+
__version__ = "2.3.2"
|
|
77
77
|
|
|
78
78
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
79
|
-
contextlib.__tracebackhide__ = True #
|
|
79
|
+
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
80
80
|
|
|
81
81
|
# assertpy files
|
|
82
82
|
ASSERTPY_FILES = [
|
|
@@ -475,6 +475,7 @@ class AssertionBuilder(
|
|
|
475
475
|
self.kind = kind
|
|
476
476
|
self.expected = expected
|
|
477
477
|
self.logger = logger if logger else _default_logger
|
|
478
|
+
self._not_expected = False
|
|
478
479
|
|
|
479
480
|
def builder(self, val, description="", kind=None, expected=None, logger=None):
|
|
480
481
|
"""Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
|
|
@@ -31,6 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
import collections.abc
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
34
35
|
from .errors import DiffEntry, DiffResult
|
|
35
36
|
from .matchers import Matcher, StructureMatcher
|
|
36
37
|
|
|
@@ -40,7 +41,7 @@ if TYPE_CHECKING:
|
|
|
40
41
|
__tracebackhide__ = True
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
class BaseMixin:
|
|
44
|
+
class BaseMixin(_MixinBase):
|
|
44
45
|
"""Base mixin."""
|
|
45
46
|
|
|
46
47
|
def described_as(self, description) -> Self:
|
|
@@ -77,7 +77,7 @@ def register_assertpy_types() -> None:
|
|
|
77
77
|
ImportError: if behave is not installed
|
|
78
78
|
"""
|
|
79
79
|
try:
|
|
80
|
-
from behave import register_type
|
|
80
|
+
from behave import register_type # ty: ignore[unresolved-import] # optional dependency
|
|
81
81
|
except ImportError:
|
|
82
82
|
raise ImportError(
|
|
83
83
|
"behave is required for register_assertpy_types(). Install it with: pip install assertpy2[behave]"
|
|
@@ -31,13 +31,15 @@ from __future__ import annotations
|
|
|
31
31
|
import collections
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
35
|
+
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from typing_extensions import Self
|
|
36
38
|
|
|
37
39
|
__tracebackhide__ = True
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
class CollectionMixin:
|
|
42
|
+
class CollectionMixin(_MixinBase):
|
|
41
43
|
"""Collection assertions mixin."""
|
|
42
44
|
|
|
43
45
|
def is_iterable(self) -> Self:
|
|
@@ -31,6 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
34
35
|
from .matchers import Matcher
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -39,7 +40,7 @@ if TYPE_CHECKING:
|
|
|
39
40
|
__tracebackhide__ = True
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
class ContainsMixin:
|
|
43
|
+
class ContainsMixin(_MixinBase):
|
|
43
44
|
"""Containment assertions mixin."""
|
|
44
45
|
|
|
45
46
|
def contains(self, *items) -> Self:
|
|
@@ -31,13 +31,15 @@ from __future__ import annotations
|
|
|
31
31
|
import datetime
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
35
|
+
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from typing_extensions import Self
|
|
36
38
|
|
|
37
39
|
__tracebackhide__ = True
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
class DateMixin:
|
|
42
|
+
class DateMixin(_MixinBase):
|
|
41
43
|
"""Date and time assertions mixin."""
|
|
42
44
|
|
|
43
45
|
def is_before(self, other) -> Self:
|
|
@@ -30,13 +30,15 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
from typing import TYPE_CHECKING
|
|
32
32
|
|
|
33
|
+
from ._mixin_base import _MixinBase
|
|
34
|
+
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
36
|
from typing_extensions import Self
|
|
35
37
|
|
|
36
38
|
__tracebackhide__ = True
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
class DictMixin:
|
|
41
|
+
class DictMixin(_MixinBase):
|
|
40
42
|
"""Dict assertions mixin."""
|
|
41
43
|
|
|
42
44
|
def contains_key(self, *keys) -> Self:
|
|
@@ -30,10 +30,12 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
import collections
|
|
32
32
|
|
|
33
|
+
from ._mixin_base import _MixinBase
|
|
34
|
+
|
|
33
35
|
__tracebackhide__ = True
|
|
34
36
|
|
|
35
37
|
|
|
36
|
-
class DynamicMixin:
|
|
38
|
+
class DynamicMixin(_MixinBase):
|
|
37
39
|
"""Dynamic assertions mixin.
|
|
38
40
|
|
|
39
41
|
When testing attributes of an object (or the contents of a dict), the
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
|
-
from typing import TYPE_CHECKING
|
|
31
|
+
from typing import TYPE_CHECKING, cast
|
|
32
|
+
|
|
33
|
+
from ._mixin_base import _MixinBase
|
|
32
34
|
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
36
|
from typing_extensions import Self
|
|
@@ -46,7 +48,7 @@ class _InertBuilder:
|
|
|
46
48
|
return lambda *args, **kwargs: self
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
class ExceptionMixin:
|
|
51
|
+
class ExceptionMixin(_MixinBase):
|
|
50
52
|
"""Expected exception mixin."""
|
|
51
53
|
|
|
52
54
|
def raises(self, ex) -> Self:
|
|
@@ -116,15 +118,16 @@ class ExceptionMixin:
|
|
|
116
118
|
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
|
|
117
119
|
f" but raised <{type(e).__name__}>."
|
|
118
120
|
)
|
|
119
|
-
return _InertBuilder()
|
|
121
|
+
return cast("Self", _InertBuilder())
|
|
120
122
|
|
|
121
123
|
self.error(
|
|
122
124
|
f"Expected <{self.val.__name__}> to raise <{self.expected.__name__}>"
|
|
123
125
|
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)})."
|
|
124
126
|
)
|
|
125
|
-
return _InertBuilder()
|
|
127
|
+
return cast("Self", _InertBuilder())
|
|
126
128
|
|
|
127
129
|
def _when_called_with_not_expected(self, *some_args, **some_kwargs) -> Self:
|
|
130
|
+
assert self.expected is not None
|
|
128
131
|
try:
|
|
129
132
|
self.val(*some_args, **some_kwargs)
|
|
130
133
|
except BaseException as e:
|
|
@@ -134,7 +137,7 @@ class ExceptionMixin:
|
|
|
134
137
|
f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
|
|
135
138
|
f" but did raise <{type(e).__name__}>."
|
|
136
139
|
)
|
|
137
|
-
return _InertBuilder()
|
|
140
|
+
return cast("Self", _InertBuilder())
|
|
138
141
|
return self
|
|
139
142
|
|
|
140
143
|
def does_not_raise(self, ex) -> Self:
|
|
@@ -31,13 +31,15 @@ from __future__ import annotations
|
|
|
31
31
|
import collections.abc
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
35
|
+
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from typing_extensions import Self
|
|
36
38
|
|
|
37
39
|
__tracebackhide__ = True
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
class ExtractingMixin:
|
|
42
|
+
class ExtractingMixin(_MixinBase):
|
|
41
43
|
"""Collection flattening mixin.
|
|
42
44
|
|
|
43
45
|
It is often necessary to test collections of objects. Use the ``extracting()`` helper to
|
|
@@ -31,6 +31,8 @@ from __future__ import annotations
|
|
|
31
31
|
import os
|
|
32
32
|
from typing import TYPE_CHECKING
|
|
33
33
|
|
|
34
|
+
from ._mixin_base import _MixinBase
|
|
35
|
+
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from typing_extensions import Self
|
|
36
38
|
|
|
@@ -72,17 +74,12 @@ def contents_of(file, encoding="utf-8"):
|
|
|
72
74
|
raise ValueError(f"val must be file or path, but was type <{type(file).__name__}>") from None
|
|
73
75
|
raise
|
|
74
76
|
|
|
75
|
-
if
|
|
76
|
-
return contents.decode(encoding, "replace")
|
|
77
|
-
try:
|
|
77
|
+
if isinstance(contents, (bytes, bytearray)):
|
|
78
78
|
return contents.decode(encoding, "replace")
|
|
79
|
-
except AttributeError:
|
|
80
|
-
pass # contents is already a str, no decode needed
|
|
81
|
-
# if all else fails, just return the contents "as is"
|
|
82
79
|
return contents
|
|
83
80
|
|
|
84
81
|
|
|
85
|
-
class FileMixin:
|
|
82
|
+
class FileMixin(_MixinBase):
|
|
86
83
|
"""File assertions mixin."""
|
|
87
84
|
|
|
88
85
|
def exists(self) -> Self:
|
|
@@ -33,10 +33,12 @@ import numbers
|
|
|
33
33
|
|
|
34
34
|
from assertpy2.errors import DiffEntry, DiffResult
|
|
35
35
|
|
|
36
|
+
from ._mixin_base import _MixinBase
|
|
37
|
+
|
|
36
38
|
__tracebackhide__ = True
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
class HelpersMixin:
|
|
41
|
+
class HelpersMixin(_MixinBase):
|
|
40
42
|
"""Helpers mixin. For internal use only."""
|
|
41
43
|
|
|
42
44
|
def _fmt_items(self, i):
|
|
@@ -2,30 +2,30 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import uuid as _uuid_mod
|
|
5
|
-
from typing import Protocol, runtime_checkable
|
|
5
|
+
from typing import Any, Protocol, runtime_checkable
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@runtime_checkable
|
|
9
9
|
class Matcher(Protocol):
|
|
10
10
|
"""Protocol for composable matcher objects."""
|
|
11
11
|
|
|
12
|
-
def matches(self, value:
|
|
12
|
+
def matches(self, value: Any) -> bool: ...
|
|
13
13
|
|
|
14
14
|
def describe(self) -> str: ...
|
|
15
15
|
|
|
16
|
-
def describe_mismatch(self, value:
|
|
16
|
+
def describe_mismatch(self, value: Any) -> str: ...
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class BaseMatcher:
|
|
20
20
|
"""Abstract base for all matchers with operator support."""
|
|
21
21
|
|
|
22
|
-
def matches(self, value:
|
|
22
|
+
def matches(self, value: Any) -> bool:
|
|
23
23
|
raise NotImplementedError
|
|
24
24
|
|
|
25
25
|
def describe(self) -> str:
|
|
26
26
|
raise NotImplementedError
|
|
27
27
|
|
|
28
|
-
def describe_mismatch(self, value:
|
|
28
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
29
29
|
return f"was <{value}>"
|
|
30
30
|
|
|
31
31
|
def __and__(self, other: Matcher) -> AllOfMatcher:
|
|
@@ -54,13 +54,13 @@ class AllOfMatcher(BaseMatcher):
|
|
|
54
54
|
def __init__(self, *matchers: Matcher):
|
|
55
55
|
self.matchers = matchers
|
|
56
56
|
|
|
57
|
-
def matches(self, value:
|
|
57
|
+
def matches(self, value: Any) -> bool:
|
|
58
58
|
return all(m.matches(value) for m in self.matchers)
|
|
59
59
|
|
|
60
60
|
def describe(self) -> str:
|
|
61
61
|
return f"({' and '.join(m.describe() for m in self.matchers)})"
|
|
62
62
|
|
|
63
|
-
def describe_mismatch(self, value:
|
|
63
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
64
64
|
failed = [m for m in self.matchers if not m.matches(value)]
|
|
65
65
|
return f"<{value}> did not satisfy: {', '.join(m.describe() for m in failed)}"
|
|
66
66
|
|
|
@@ -71,13 +71,13 @@ class AnyOfMatcher(BaseMatcher):
|
|
|
71
71
|
def __init__(self, *matchers: Matcher):
|
|
72
72
|
self.matchers = matchers
|
|
73
73
|
|
|
74
|
-
def matches(self, value:
|
|
74
|
+
def matches(self, value: Any) -> bool:
|
|
75
75
|
return any(m.matches(value) for m in self.matchers)
|
|
76
76
|
|
|
77
77
|
def describe(self) -> str:
|
|
78
78
|
return f"({' or '.join(m.describe() for m in self.matchers)})"
|
|
79
79
|
|
|
80
|
-
def describe_mismatch(self, value:
|
|
80
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
81
81
|
return f"<{value}> satisfied none of: {', '.join(m.describe() for m in self.matchers)}"
|
|
82
82
|
|
|
83
83
|
|
|
@@ -87,13 +87,13 @@ class NotMatcher(BaseMatcher):
|
|
|
87
87
|
def __init__(self, matcher: Matcher):
|
|
88
88
|
self.matcher = matcher
|
|
89
89
|
|
|
90
|
-
def matches(self, value:
|
|
90
|
+
def matches(self, value: Any) -> bool:
|
|
91
91
|
return not self.matcher.matches(value)
|
|
92
92
|
|
|
93
93
|
def describe(self) -> str:
|
|
94
94
|
return f"not {self.matcher.describe()}"
|
|
95
95
|
|
|
96
|
-
def describe_mismatch(self, value:
|
|
96
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
97
97
|
return f"<{value}> unexpectedly matched {self.matcher.describe()}"
|
|
98
98
|
|
|
99
99
|
|
|
@@ -104,7 +104,7 @@ class EqualToMatcher(BaseMatcher):
|
|
|
104
104
|
def __init__(self, expected: object):
|
|
105
105
|
self.expected = expected
|
|
106
106
|
|
|
107
|
-
def matches(self, value:
|
|
107
|
+
def matches(self, value: Any) -> bool:
|
|
108
108
|
return value == self.expected
|
|
109
109
|
|
|
110
110
|
def describe(self) -> str:
|
|
@@ -115,7 +115,7 @@ class GreaterThanMatcher(BaseMatcher):
|
|
|
115
115
|
def __init__(self, boundary: object):
|
|
116
116
|
self.boundary = boundary
|
|
117
117
|
|
|
118
|
-
def matches(self, value:
|
|
118
|
+
def matches(self, value: Any) -> bool:
|
|
119
119
|
return value > self.boundary
|
|
120
120
|
|
|
121
121
|
def describe(self) -> str:
|
|
@@ -126,7 +126,7 @@ class GreaterThanOrEqualToMatcher(BaseMatcher):
|
|
|
126
126
|
def __init__(self, boundary: object):
|
|
127
127
|
self.boundary = boundary
|
|
128
128
|
|
|
129
|
-
def matches(self, value:
|
|
129
|
+
def matches(self, value: Any) -> bool:
|
|
130
130
|
return value >= self.boundary
|
|
131
131
|
|
|
132
132
|
def describe(self) -> str:
|
|
@@ -137,7 +137,7 @@ class LessThanMatcher(BaseMatcher):
|
|
|
137
137
|
def __init__(self, boundary: object):
|
|
138
138
|
self.boundary = boundary
|
|
139
139
|
|
|
140
|
-
def matches(self, value:
|
|
140
|
+
def matches(self, value: Any) -> bool:
|
|
141
141
|
return value < self.boundary
|
|
142
142
|
|
|
143
143
|
def describe(self) -> str:
|
|
@@ -148,7 +148,7 @@ class LessThanOrEqualToMatcher(BaseMatcher):
|
|
|
148
148
|
def __init__(self, boundary: object):
|
|
149
149
|
self.boundary = boundary
|
|
150
150
|
|
|
151
|
-
def matches(self, value:
|
|
151
|
+
def matches(self, value: Any) -> bool:
|
|
152
152
|
return value <= self.boundary
|
|
153
153
|
|
|
154
154
|
def describe(self) -> str:
|
|
@@ -160,7 +160,7 @@ class BetweenMatcher(BaseMatcher):
|
|
|
160
160
|
self.low = low
|
|
161
161
|
self.high = high
|
|
162
162
|
|
|
163
|
-
def matches(self, value:
|
|
163
|
+
def matches(self, value: Any) -> bool:
|
|
164
164
|
return self.low <= value <= self.high
|
|
165
165
|
|
|
166
166
|
def describe(self) -> str:
|
|
@@ -172,7 +172,7 @@ class CloseToMatcher(BaseMatcher):
|
|
|
172
172
|
self.expected = expected
|
|
173
173
|
self.tolerance = tolerance
|
|
174
174
|
|
|
175
|
-
def matches(self, value:
|
|
175
|
+
def matches(self, value: Any) -> bool:
|
|
176
176
|
return abs(value - self.expected) <= self.tolerance
|
|
177
177
|
|
|
178
178
|
def describe(self) -> str:
|
|
@@ -183,7 +183,7 @@ class CloseToMatcher(BaseMatcher):
|
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
class IsNoneMatcher(BaseMatcher):
|
|
186
|
-
def matches(self, value:
|
|
186
|
+
def matches(self, value: Any) -> bool:
|
|
187
187
|
return value is None
|
|
188
188
|
|
|
189
189
|
def describe(self) -> str:
|
|
@@ -191,7 +191,7 @@ class IsNoneMatcher(BaseMatcher):
|
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
class IsNotNoneMatcher(BaseMatcher):
|
|
194
|
-
def matches(self, value:
|
|
194
|
+
def matches(self, value: Any) -> bool:
|
|
195
195
|
return value is not None
|
|
196
196
|
|
|
197
197
|
def describe(self) -> str:
|
|
@@ -202,18 +202,18 @@ class IsInstanceOfMatcher(BaseMatcher):
|
|
|
202
202
|
def __init__(self, expected_type: type):
|
|
203
203
|
self.expected_type = expected_type
|
|
204
204
|
|
|
205
|
-
def matches(self, value:
|
|
205
|
+
def matches(self, value: Any) -> bool:
|
|
206
206
|
return isinstance(value, self.expected_type)
|
|
207
207
|
|
|
208
208
|
def describe(self) -> str:
|
|
209
209
|
return f"an instance of <{self.expected_type.__name__}>"
|
|
210
210
|
|
|
211
|
-
def describe_mismatch(self, value:
|
|
211
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
212
212
|
return f"was <{value}> of type <{type(value).__name__}>"
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
class IsTruthyMatcher(BaseMatcher):
|
|
216
|
-
def matches(self, value:
|
|
216
|
+
def matches(self, value: Any) -> bool:
|
|
217
217
|
return bool(value)
|
|
218
218
|
|
|
219
219
|
def describe(self) -> str:
|
|
@@ -221,7 +221,7 @@ class IsTruthyMatcher(BaseMatcher):
|
|
|
221
221
|
|
|
222
222
|
|
|
223
223
|
class IsFalsyMatcher(BaseMatcher):
|
|
224
|
-
def matches(self, value:
|
|
224
|
+
def matches(self, value: Any) -> bool:
|
|
225
225
|
return not bool(value)
|
|
226
226
|
|
|
227
227
|
def describe(self) -> str:
|
|
@@ -235,18 +235,18 @@ class HasLengthMatcher(BaseMatcher):
|
|
|
235
235
|
def __init__(self, expected_length: int):
|
|
236
236
|
self.expected_length = expected_length
|
|
237
237
|
|
|
238
|
-
def matches(self, value:
|
|
238
|
+
def matches(self, value: Any) -> bool:
|
|
239
239
|
return len(value) == self.expected_length
|
|
240
240
|
|
|
241
241
|
def describe(self) -> str:
|
|
242
242
|
return f"a value of length <{self.expected_length}>"
|
|
243
243
|
|
|
244
|
-
def describe_mismatch(self, value:
|
|
244
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
245
245
|
return f"was <{value}> with length <{len(value)}>"
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
class IsEmptyMatcher(BaseMatcher):
|
|
249
|
-
def matches(self, value:
|
|
249
|
+
def matches(self, value: Any) -> bool:
|
|
250
250
|
return len(value) == 0
|
|
251
251
|
|
|
252
252
|
def describe(self) -> str:
|
|
@@ -254,7 +254,7 @@ class IsEmptyMatcher(BaseMatcher):
|
|
|
254
254
|
|
|
255
255
|
|
|
256
256
|
class IsNotEmptyMatcher(BaseMatcher):
|
|
257
|
-
def matches(self, value:
|
|
257
|
+
def matches(self, value: Any) -> bool:
|
|
258
258
|
return len(value) > 0
|
|
259
259
|
|
|
260
260
|
def describe(self) -> str:
|
|
@@ -265,7 +265,7 @@ class IsNotEmptyMatcher(BaseMatcher):
|
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
class IsPositiveMatcher(BaseMatcher):
|
|
268
|
-
def matches(self, value:
|
|
268
|
+
def matches(self, value: Any) -> bool:
|
|
269
269
|
return value > 0
|
|
270
270
|
|
|
271
271
|
def describe(self) -> str:
|
|
@@ -273,7 +273,7 @@ class IsPositiveMatcher(BaseMatcher):
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
class IsNegativeMatcher(BaseMatcher):
|
|
276
|
-
def matches(self, value:
|
|
276
|
+
def matches(self, value: Any) -> bool:
|
|
277
277
|
return value < 0
|
|
278
278
|
|
|
279
279
|
def describe(self) -> str:
|
|
@@ -281,7 +281,7 @@ class IsNegativeMatcher(BaseMatcher):
|
|
|
281
281
|
|
|
282
282
|
|
|
283
283
|
class IsZeroMatcher(BaseMatcher):
|
|
284
|
-
def matches(self, value:
|
|
284
|
+
def matches(self, value: Any) -> bool:
|
|
285
285
|
return value == 0
|
|
286
286
|
|
|
287
287
|
def describe(self) -> str:
|
|
@@ -289,26 +289,26 @@ class IsZeroMatcher(BaseMatcher):
|
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
class IsEvenMatcher(BaseMatcher):
|
|
292
|
-
def matches(self, value:
|
|
292
|
+
def matches(self, value: Any) -> bool:
|
|
293
293
|
return isinstance(value, int) and not isinstance(value, bool) and value % 2 == 0
|
|
294
294
|
|
|
295
295
|
def describe(self) -> str:
|
|
296
296
|
return "an even integer"
|
|
297
297
|
|
|
298
|
-
def describe_mismatch(self, value:
|
|
298
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
299
299
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
300
300
|
return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
|
|
301
301
|
return f"was <{value}>, which is odd"
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
class IsOddMatcher(BaseMatcher):
|
|
305
|
-
def matches(self, value:
|
|
305
|
+
def matches(self, value: Any) -> bool:
|
|
306
306
|
return isinstance(value, int) and not isinstance(value, bool) and value % 2 != 0
|
|
307
307
|
|
|
308
308
|
def describe(self) -> str:
|
|
309
309
|
return "an odd integer"
|
|
310
310
|
|
|
311
|
-
def describe_mismatch(self, value:
|
|
311
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
312
312
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
313
313
|
return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
|
|
314
314
|
return f"was <{value}>, which is even"
|
|
@@ -318,26 +318,26 @@ class IsDivisibleByMatcher(BaseMatcher):
|
|
|
318
318
|
def __init__(self, divisor: int):
|
|
319
319
|
self.divisor = divisor
|
|
320
320
|
|
|
321
|
-
def matches(self, value:
|
|
321
|
+
def matches(self, value: Any) -> bool:
|
|
322
322
|
return isinstance(value, int) and not isinstance(value, bool) and value % self.divisor == 0
|
|
323
323
|
|
|
324
324
|
def describe(self) -> str:
|
|
325
325
|
return f"an integer divisible by <{self.divisor}>"
|
|
326
326
|
|
|
327
|
-
def describe_mismatch(self, value:
|
|
327
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
328
328
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
329
329
|
return f"was <{value!r}> of type <{type(value).__name__}>, not an integer"
|
|
330
330
|
return f"was <{value}>, which has remainder <{value % self.divisor}>"
|
|
331
331
|
|
|
332
332
|
|
|
333
333
|
class IsCallableMatcher(BaseMatcher):
|
|
334
|
-
def matches(self, value:
|
|
334
|
+
def matches(self, value: Any) -> bool:
|
|
335
335
|
return callable(value)
|
|
336
336
|
|
|
337
337
|
def describe(self) -> str:
|
|
338
338
|
return "a callable"
|
|
339
339
|
|
|
340
|
-
def describe_mismatch(self, value:
|
|
340
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
341
341
|
return f"was <{value!r}> of type <{type(value).__name__}>, which is not callable"
|
|
342
342
|
|
|
343
343
|
|
|
@@ -345,13 +345,13 @@ class IsInMatcher(BaseMatcher):
|
|
|
345
345
|
def __init__(self, *values: object):
|
|
346
346
|
self.values = values
|
|
347
347
|
|
|
348
|
-
def matches(self, value:
|
|
348
|
+
def matches(self, value: Any) -> bool:
|
|
349
349
|
return value in self.values
|
|
350
350
|
|
|
351
351
|
def describe(self) -> str:
|
|
352
352
|
return f"a value in <{self.values}>"
|
|
353
353
|
|
|
354
|
-
def describe_mismatch(self, value:
|
|
354
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
355
355
|
return f"was <{value!r}>, which is not in <{self.values}>"
|
|
356
356
|
|
|
357
357
|
|
|
@@ -360,7 +360,7 @@ class HasPropertyMatcher(BaseMatcher):
|
|
|
360
360
|
self.name = name
|
|
361
361
|
self.matcher = matcher
|
|
362
362
|
|
|
363
|
-
def matches(self, value:
|
|
363
|
+
def matches(self, value: Any) -> bool:
|
|
364
364
|
if not hasattr(value, self.name):
|
|
365
365
|
return False
|
|
366
366
|
if self.matcher is not None:
|
|
@@ -372,7 +372,7 @@ class HasPropertyMatcher(BaseMatcher):
|
|
|
372
372
|
return f"an object with property <{self.name}> matching {self.matcher.describe()}"
|
|
373
373
|
return f"an object with property <{self.name}>"
|
|
374
374
|
|
|
375
|
-
def describe_mismatch(self, value:
|
|
375
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
376
376
|
if not hasattr(value, self.name):
|
|
377
377
|
return f"<{value!r}> has no property <{self.name}>"
|
|
378
378
|
if self.matcher is not None:
|
|
@@ -388,7 +388,7 @@ class ContainsStringMatcher(BaseMatcher):
|
|
|
388
388
|
def __init__(self, substring: str):
|
|
389
389
|
self.substring = substring
|
|
390
390
|
|
|
391
|
-
def matches(self, value:
|
|
391
|
+
def matches(self, value: Any) -> bool:
|
|
392
392
|
return isinstance(value, str) and self.substring in value
|
|
393
393
|
|
|
394
394
|
def describe(self) -> str:
|
|
@@ -399,7 +399,7 @@ class MatchesRegexMatcher(BaseMatcher):
|
|
|
399
399
|
def __init__(self, pattern: str):
|
|
400
400
|
self.pattern = pattern
|
|
401
401
|
|
|
402
|
-
def matches(self, value:
|
|
402
|
+
def matches(self, value: Any) -> bool:
|
|
403
403
|
return isinstance(value, str) and re.search(self.pattern, value) is not None
|
|
404
404
|
|
|
405
405
|
def describe(self) -> str:
|
|
@@ -410,7 +410,7 @@ class StartsWithMatcher(BaseMatcher):
|
|
|
410
410
|
def __init__(self, prefix: str):
|
|
411
411
|
self.prefix = prefix
|
|
412
412
|
|
|
413
|
-
def matches(self, value:
|
|
413
|
+
def matches(self, value: Any) -> bool:
|
|
414
414
|
return isinstance(value, str) and value.startswith(self.prefix)
|
|
415
415
|
|
|
416
416
|
def describe(self) -> str:
|
|
@@ -421,7 +421,7 @@ class EndsWithMatcher(BaseMatcher):
|
|
|
421
421
|
def __init__(self, suffix: str):
|
|
422
422
|
self.suffix = suffix
|
|
423
423
|
|
|
424
|
-
def matches(self, value:
|
|
424
|
+
def matches(self, value: Any) -> bool:
|
|
425
425
|
return isinstance(value, str) and value.endswith(self.suffix)
|
|
426
426
|
|
|
427
427
|
def describe(self) -> str:
|
|
@@ -434,7 +434,7 @@ class EndsWithMatcher(BaseMatcher):
|
|
|
434
434
|
class IgnoreMatcher(BaseMatcher):
|
|
435
435
|
"""Always matches. Used in structure specs to accept any value for a field."""
|
|
436
436
|
|
|
437
|
-
def matches(self, value:
|
|
437
|
+
def matches(self, value: Any) -> bool:
|
|
438
438
|
return True
|
|
439
439
|
|
|
440
440
|
def describe(self) -> str:
|
|
@@ -444,7 +444,7 @@ class IgnoreMatcher(BaseMatcher):
|
|
|
444
444
|
class IsUuidMatcher(BaseMatcher):
|
|
445
445
|
"""Matches valid UUID strings."""
|
|
446
446
|
|
|
447
|
-
def matches(self, value:
|
|
447
|
+
def matches(self, value: Any) -> bool:
|
|
448
448
|
if not isinstance(value, str):
|
|
449
449
|
return False
|
|
450
450
|
try:
|
|
@@ -460,7 +460,7 @@ class IsUuidMatcher(BaseMatcher):
|
|
|
460
460
|
class IsNonEmptyStringMatcher(BaseMatcher):
|
|
461
461
|
"""Matches non-empty strings."""
|
|
462
462
|
|
|
463
|
-
def matches(self, value:
|
|
463
|
+
def matches(self, value: Any) -> bool:
|
|
464
464
|
return isinstance(value, str) and len(value) > 0
|
|
465
465
|
|
|
466
466
|
def describe(self) -> str:
|
|
@@ -473,7 +473,7 @@ class EachMatcher(BaseMatcher):
|
|
|
473
473
|
def __init__(self, matcher: Matcher):
|
|
474
474
|
self.matcher = matcher
|
|
475
475
|
|
|
476
|
-
def matches(self, value:
|
|
476
|
+
def matches(self, value: Any) -> bool:
|
|
477
477
|
try:
|
|
478
478
|
return all(self.matcher.matches(item) for item in value)
|
|
479
479
|
except TypeError:
|
|
@@ -482,7 +482,7 @@ class EachMatcher(BaseMatcher):
|
|
|
482
482
|
def describe(self) -> str:
|
|
483
483
|
return f"each item matching {self.matcher.describe()}"
|
|
484
484
|
|
|
485
|
-
def describe_mismatch(self, value:
|
|
485
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
486
486
|
try:
|
|
487
487
|
for i, item in enumerate(value):
|
|
488
488
|
if not self.matcher.matches(item):
|
|
@@ -498,7 +498,7 @@ class StructureMatcher(BaseMatcher):
|
|
|
498
498
|
def __init__(self, spec: dict):
|
|
499
499
|
self._spec = spec
|
|
500
500
|
|
|
501
|
-
def matches(self, value:
|
|
501
|
+
def matches(self, value: Any) -> bool:
|
|
502
502
|
if not isinstance(value, dict):
|
|
503
503
|
return False
|
|
504
504
|
return self._match_recursive(value, self._spec, "") is None
|
|
@@ -506,7 +506,7 @@ class StructureMatcher(BaseMatcher):
|
|
|
506
506
|
def describe(self) -> str:
|
|
507
507
|
return f"a dict matching structure {self._describe_spec(self._spec)}"
|
|
508
508
|
|
|
509
|
-
def describe_mismatch(self, value:
|
|
509
|
+
def describe_mismatch(self, value: Any) -> str:
|
|
510
510
|
if not isinstance(value, dict):
|
|
511
511
|
return f"was not a dict: <{value}>"
|
|
512
512
|
error = self._match_recursive(value, self._spec, "")
|
|
@@ -33,13 +33,15 @@ import math
|
|
|
33
33
|
import numbers
|
|
34
34
|
from typing import TYPE_CHECKING
|
|
35
35
|
|
|
36
|
+
from ._mixin_base import _MixinBase
|
|
37
|
+
|
|
36
38
|
if TYPE_CHECKING:
|
|
37
39
|
from typing_extensions import Self
|
|
38
40
|
|
|
39
41
|
__tracebackhide__ = True
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
class NumericMixin:
|
|
44
|
+
class NumericMixin(_MixinBase):
|
|
43
45
|
"""Numeric assertions mixin."""
|
|
44
46
|
|
|
45
47
|
_NUMERIC_COMPAREABLE = frozenset({datetime.datetime, datetime.timedelta, datetime.date, datetime.time})
|
|
@@ -35,13 +35,15 @@ import os
|
|
|
35
35
|
import sys
|
|
36
36
|
from typing import TYPE_CHECKING
|
|
37
37
|
|
|
38
|
+
from ._mixin_base import _MixinBase
|
|
39
|
+
|
|
38
40
|
if TYPE_CHECKING:
|
|
39
41
|
from typing_extensions import Self
|
|
40
42
|
|
|
41
43
|
__tracebackhide__ = True
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
class SnapshotMixin:
|
|
46
|
+
class SnapshotMixin(_MixinBase):
|
|
45
47
|
"""Snapshot mixin.
|
|
46
48
|
|
|
47
49
|
Take a snapshot of a python data structure, store it on disk in JSON format, and automatically
|
|
@@ -187,9 +189,12 @@ class SnapshotMixin:
|
|
|
187
189
|
else:
|
|
188
190
|
# make id from filename and line number
|
|
189
191
|
f = inspect.currentframe()
|
|
190
|
-
|
|
192
|
+
caller = f.f_back if f is not None else None
|
|
193
|
+
if caller is None: # pragma: no cover - frame introspection always available in CPython
|
|
194
|
+
raise RuntimeError("cannot determine caller frame")
|
|
195
|
+
fpath = os.path.basename(caller.f_code.co_filename)
|
|
191
196
|
fname = os.path.splitext(fpath)[0]
|
|
192
|
-
lineno = str(
|
|
197
|
+
lineno = str(caller.f_lineno)
|
|
193
198
|
snapname = _name(path, fname)
|
|
194
199
|
|
|
195
200
|
if not os.path.exists(path):
|
|
@@ -32,13 +32,15 @@ import collections.abc
|
|
|
32
32
|
import re
|
|
33
33
|
from typing import TYPE_CHECKING
|
|
34
34
|
|
|
35
|
+
from ._mixin_base import _MixinBase
|
|
36
|
+
|
|
35
37
|
if TYPE_CHECKING:
|
|
36
38
|
from typing_extensions import Self
|
|
37
39
|
|
|
38
40
|
__tracebackhide__ = True
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
class StringMixin:
|
|
43
|
+
class StringMixin(_MixinBase):
|
|
42
44
|
"""String assertions mixin."""
|
|
43
45
|
|
|
44
46
|
def is_equal_to_ignoring_case(self, other) -> Self:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "assertpy2"
|
|
3
|
-
version = "2.3.
|
|
4
|
-
description = "Fluent assertion library for Python with
|
|
3
|
+
version = "2.3.2"
|
|
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"
|
|
7
7
|
requires-python = ">=3.10"
|
|
@@ -55,6 +55,7 @@ dev = [
|
|
|
55
55
|
"pytest>=9.0.3",
|
|
56
56
|
"pytest-cov>=6.1",
|
|
57
57
|
"ruff>=0.15.17",
|
|
58
|
+
"ty>=0.0.49",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
[tool.pytest.ini_options]
|
|
@@ -73,7 +74,9 @@ line-length = 120
|
|
|
73
74
|
target-version = "py310"
|
|
74
75
|
|
|
75
76
|
[tool.ruff.lint]
|
|
76
|
-
|
|
77
|
+
preview = true
|
|
78
|
+
explicit-preview-rules = true
|
|
79
|
+
select = ["E", "E301", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
77
80
|
ignore = ["E501"]
|
|
78
81
|
|
|
79
82
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -30,7 +30,7 @@ wheels = [
|
|
|
30
30
|
|
|
31
31
|
[[package]]
|
|
32
32
|
name = "assertpy2"
|
|
33
|
-
version = "2.
|
|
33
|
+
version = "2.3.1"
|
|
34
34
|
source = { editable = "." }
|
|
35
35
|
dependencies = [
|
|
36
36
|
{ name = "typing-extensions" },
|
|
@@ -49,6 +49,7 @@ dev = [
|
|
|
49
49
|
{ name = "pytest" },
|
|
50
50
|
{ name = "pytest-cov" },
|
|
51
51
|
{ name = "ruff" },
|
|
52
|
+
{ name = "ty" },
|
|
52
53
|
]
|
|
53
54
|
|
|
54
55
|
[package.metadata]
|
|
@@ -64,6 +65,7 @@ dev = [
|
|
|
64
65
|
{ name = "pytest", specifier = ">=9.0.3" },
|
|
65
66
|
{ name = "pytest-cov", specifier = ">=6.1" },
|
|
66
67
|
{ name = "ruff", specifier = ">=0.15.17" },
|
|
68
|
+
{ name = "ty", specifier = ">=0.0.49" },
|
|
67
69
|
]
|
|
68
70
|
|
|
69
71
|
[[package]]
|
|
@@ -428,6 +430,31 @@ wheels = [
|
|
|
428
430
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
|
429
431
|
]
|
|
430
432
|
|
|
433
|
+
[[package]]
|
|
434
|
+
name = "ty"
|
|
435
|
+
version = "0.0.49"
|
|
436
|
+
source = { registry = "https://pypi.org/simple" }
|
|
437
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1d/8d/37cb91808069509d43a2a11743e12f1e854fd808dbef2203309d256718cd/ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145", size = 5884753, upload-time = "2026-06-12T03:08:20.221Z" }
|
|
438
|
+
wheels = [
|
|
439
|
+
{ url = "https://files.pythonhosted.org/packages/ca/de/9237c6a96356612dd0393db1e94cf21f903616adf3a3701bf3da6e4adc92/ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065", size = 11834671, upload-time = "2026-06-12T03:07:53.062Z" },
|
|
440
|
+
{ url = "https://files.pythonhosted.org/packages/8f/15/daf5a14a5e07012277d450c75325c94614e2acfec4c620c881486118c410/ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471", size = 11589570, upload-time = "2026-06-12T03:08:25.779Z" },
|
|
441
|
+
{ url = "https://files.pythonhosted.org/packages/7d/58/30bdf98436488aca25f0763bf7f92a061528d42461b686453029e845e4c5/ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4", size = 10985236, upload-time = "2026-06-12T03:08:36.664Z" },
|
|
442
|
+
{ url = "https://files.pythonhosted.org/packages/22/45/ece503e4a1396e13a1a9a0cde51afe476a6506a1d557eeadf8ad45c83bc0/ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad", size = 11504302, upload-time = "2026-06-12T03:08:01.664Z" },
|
|
443
|
+
{ url = "https://files.pythonhosted.org/packages/17/dc/5d09333d289dfbca1804eaade125c9e8a1a992a2a592a8b80c5e9b589ca9/ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04", size = 11626629, upload-time = "2026-06-12T03:08:06.844Z" },
|
|
444
|
+
{ url = "https://files.pythonhosted.org/packages/f2/36/155f41c9dd7237c4b609211f29f77755a139ee6218605dadc7fe21d5e3c8/ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b", size = 12074481, upload-time = "2026-06-12T03:08:09.643Z" },
|
|
445
|
+
{ url = "https://files.pythonhosted.org/packages/96/4c/998ee13cd5045f1f8b36982de7343163832ac53f27debe01b0de0e8bd968/ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d", size = 12678042, upload-time = "2026-06-12T03:08:39.319Z" },
|
|
446
|
+
{ url = "https://files.pythonhosted.org/packages/85/c9/9a505aba85c41ce54cbcaa14f8d79aa084b86151d2d70df11c4655b92898/ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50", size = 12316194, upload-time = "2026-06-12T03:08:18.023Z" },
|
|
447
|
+
{ url = "https://files.pythonhosted.org/packages/c9/b8/ded37fb93503294abbc83c36470bb1413bea05048b745881d4470b518a06/ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0", size = 12145507, upload-time = "2026-06-12T03:07:56.505Z" },
|
|
448
|
+
{ url = "https://files.pythonhosted.org/packages/2f/07/392e80d78f02445f695b815bb9eb0fffacda68b03faee38c900f7b990815/ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d", size = 12365967, upload-time = "2026-06-12T03:08:12.553Z" },
|
|
449
|
+
{ url = "https://files.pythonhosted.org/packages/50/d3/31b0c2a7fbedd3373e389cb1d81b8d2128f6f868fafb46557736a6f9aca8/ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b", size = 11475283, upload-time = "2026-06-12T03:08:28.334Z" },
|
|
450
|
+
{ url = "https://files.pythonhosted.org/packages/5a/5b/329e101638920b468a3bb63059c9f66ef99b44aac501222c44832a507321/ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d", size = 11645343, upload-time = "2026-06-12T03:08:15.246Z" },
|
|
451
|
+
{ url = "https://files.pythonhosted.org/packages/a9/76/c897e615e32f80ca81c8c1bc49b9a1f72ff9e3cfea0f8345ba505fe28472/ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d", size = 11725585, upload-time = "2026-06-12T03:08:33.915Z" },
|
|
452
|
+
{ url = "https://files.pythonhosted.org/packages/59/e1/fdb42ee239f618800842681af5bb8598117e74512c10974a8b7b9086a898/ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731", size = 12237261, upload-time = "2026-06-12T03:08:31.105Z" },
|
|
453
|
+
{ url = "https://files.pythonhosted.org/packages/98/0f/a2d6a5fc9d0786cbeb3c200786da4e18c203589be3984bb5def83ca92320/ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1", size = 11100789, upload-time = "2026-06-12T03:07:59.092Z" },
|
|
454
|
+
{ url = "https://files.pythonhosted.org/packages/d0/9d/473ac8bc57b5a2d121da893bf9dd74a118efb19a01d711df1a6e397f05cc/ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1", size = 12204644, upload-time = "2026-06-12T03:08:04.327Z" },
|
|
455
|
+
{ url = "https://files.pythonhosted.org/packages/ef/a2/8959249da951ba3977fee20e688d28678b8a1d30a9ed4464228a85d45853/ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f", size = 11558965, upload-time = "2026-06-12T03:08:23.012Z" },
|
|
456
|
+
]
|
|
457
|
+
|
|
431
458
|
[[package]]
|
|
432
459
|
name = "typing-extensions"
|
|
433
460
|
version = "4.15.0"
|
|
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
|