panda-pytest-assertions 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panda_pytest_assertions/__init__.py +0 -0
- panda_pytest_assertions/assert_context/__init__.py +15 -0
- panda_pytest_assertions/assert_context/_assert_context.py +128 -0
- panda_pytest_assertions/assert_object/__init__.py +41 -0
- panda_pytest_assertions/assert_object/_assert_object.py +36 -0
- panda_pytest_assertions/assert_object/_asserter_factories/__init__.py +8 -0
- panda_pytest_assertions/assert_object/_asserter_factories/asserter_factory.py +66 -0
- panda_pytest_assertions/assert_object/_asserter_factories/built_in_asserter_factory.py +29 -0
- panda_pytest_assertions/assert_object/_converters/__init__.py +10 -0
- panda_pytest_assertions/assert_object/_converters/expectation_from_yaml.py +114 -0
- panda_pytest_assertions/assert_object/_converters/expectation_to_yaml.py +78 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/__init__.py +15 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/mapping_subset.py +15 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/object_attibutes.py +15 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/stringified.py +11 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/unordered.py +18 -0
- panda_pytest_assertions/assert_object/_expectation_modificators/with_type.py +135 -0
- panda_pytest_assertions/assert_object/_objects_asserters/__init__.py +36 -0
- panda_pytest_assertions/assert_object/_objects_asserters/asserter.py +35 -0
- panda_pytest_assertions/assert_object/_objects_asserters/equality_asserter.py +33 -0
- panda_pytest_assertions/assert_object/_objects_asserters/mapping_asserter.py +55 -0
- panda_pytest_assertions/assert_object/_objects_asserters/mapping_subset_asserter.py +53 -0
- panda_pytest_assertions/assert_object/_objects_asserters/object_attributes_asserter.py +52 -0
- panda_pytest_assertions/assert_object/_objects_asserters/ordered_elements_asserter.py +55 -0
- panda_pytest_assertions/assert_object/_objects_asserters/stringified_asserter.py +36 -0
- panda_pytest_assertions/assert_object/_objects_asserters/unordered_elements_asserter.py +77 -0
- panda_pytest_assertions/assert_object/_objects_asserters/with_type_asserter.py +57 -0
- panda_pytest_assertions/assert_object/_protocols.py +93 -0
- panda_pytest_assertions/assert_object/generators/__init__.py +41 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/__init__.py +30 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/equality.py +6 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/mapping.py +14 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/mapping_subset.py +14 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/object_attributes.py +12 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/of_type.py +13 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/stringified.py +4 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/uniform_mapping.py +13 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/uniform_mapping_subset.py +13 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/uniform_ordered.py +39 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/uniform_unordered.py +11 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/union.py +13 -0
- panda_pytest_assertions/assert_object/generators/_expectation_definitions/with_type.py +30 -0
- panda_pytest_assertions/assert_object/generators/_generate_expectation.py +23 -0
- panda_pytest_assertions/assert_object/generators/_generator_factories/__init__.py +8 -0
- panda_pytest_assertions/assert_object/generators/_generator_factories/built_in_generator_factory.py +39 -0
- panda_pytest_assertions/assert_object/generators/_generator_factories/generator_factory.py +66 -0
- panda_pytest_assertions/assert_object/generators/_generators/__init__.py +34 -0
- panda_pytest_assertions/assert_object/generators/_generators/equality_generator.py +35 -0
- panda_pytest_assertions/assert_object/generators/_generators/equality_key_mapping_generator.py +54 -0
- panda_pytest_assertions/assert_object/generators/_generators/exceptions.py +1 -0
- panda_pytest_assertions/assert_object/generators/_generators/generator.py +35 -0
- panda_pytest_assertions/assert_object/generators/_generators/mapping_generator.py +60 -0
- panda_pytest_assertions/assert_object/generators/_generators/mapping_subset_generator.py +57 -0
- panda_pytest_assertions/assert_object/generators/_generators/object_attributes_generator.py +53 -0
- panda_pytest_assertions/assert_object/generators/_generators/of_type_generator.py +47 -0
- panda_pytest_assertions/assert_object/generators/_generators/ordered_generator.py +55 -0
- panda_pytest_assertions/assert_object/generators/_generators/stringified_generator.py +35 -0
- panda_pytest_assertions/assert_object/generators/_generators/uniform_mapping_generator.py +50 -0
- panda_pytest_assertions/assert_object/generators/_generators/uniform_mapping_subset_generator.py +52 -0
- panda_pytest_assertions/assert_object/generators/_generators/uniform_ordered_generator.py +49 -0
- panda_pytest_assertions/assert_object/generators/_generators/union_generator.py +51 -0
- panda_pytest_assertions/assert_object/generators/_generators/unordered_generator.py +46 -0
- panda_pytest_assertions/assert_object/generators/_generators/with_type_generator.py +46 -0
- panda_pytest_assertions/py.typed +0 -0
- panda_pytest_assertions-0.1.0.dist-info/LICENSE +201 -0
- panda_pytest_assertions-0.1.0.dist-info/METADATA +32 -0
- panda_pytest_assertions-0.1.0.dist-info/RECORD +68 -0
- panda_pytest_assertions-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
# isort: off
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
pytest.register_assert_rewrite(__name__ + '._assert_context')
|
|
6
|
+
# isort: on
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from ._assert_context import assert_context, AssertContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'assert_context',
|
|
14
|
+
'AssertContext',
|
|
15
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from contextlib import contextmanager, ExitStack, nullcontext
|
|
3
|
+
from typing import Any, overload
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Unset:
|
|
9
|
+
__instance: '_Unset | None' = None
|
|
10
|
+
|
|
11
|
+
def __new__(cls) -> '_Unset':
|
|
12
|
+
if cls.__instance is None:
|
|
13
|
+
cls.__instance = object.__new__(cls)
|
|
14
|
+
return cls.__instance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_UNSET = _Unset()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AssertContext:
|
|
21
|
+
__slots__ = ('_result',)
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._result: Any = _UNSET
|
|
25
|
+
|
|
26
|
+
def set(self, value: Any) -> None: # noqa: ANN401
|
|
27
|
+
self._result = value
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def assert_result_equal(self, expected_result: Any) -> Generator[None, None, None]: # noqa: ANN401
|
|
31
|
+
yield
|
|
32
|
+
assert expected_result == self._result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
@contextmanager
|
|
37
|
+
def assert_context( # noqa: D418
|
|
38
|
+
*,
|
|
39
|
+
exception: type[Exception] | _Unset = ...,
|
|
40
|
+
result: Any | _Unset = ..., # noqa: ANN401
|
|
41
|
+
) -> Generator[AssertContext, None, None]:
|
|
42
|
+
"""
|
|
43
|
+
Context manager that makes sure `with` block behaved according to the arguments.
|
|
44
|
+
|
|
45
|
+
If the `exception` parameter is set, the `with` block is expected to raise an exception of provided type.
|
|
46
|
+
If it is NOT set, the `with` block is expected NOT TO raise any exceptions.
|
|
47
|
+
|
|
48
|
+
If the `result` parameter is set, its value is expected to be equal to `with` block result. The `with`
|
|
49
|
+
block result is set with the `set` method of an object yielded by the context manager. The comparison
|
|
50
|
+
is used with `==` operator, where `result` argument is on the left side.
|
|
51
|
+
If the `result` parameter is NOT set, the `set` method of yielded object is expected
|
|
52
|
+
not to be called at all.
|
|
53
|
+
|
|
54
|
+
:param exception: type of exception, expected to be raised
|
|
55
|
+
:param result: value expected to be set with `set` method of yielded object
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
@contextmanager
|
|
61
|
+
def assert_context( # noqa: D418
|
|
62
|
+
*,
|
|
63
|
+
behaviour: type[Exception] | Any | _Unset = ..., # noqa: ANN401
|
|
64
|
+
) -> Generator[AssertContext, None, None]:
|
|
65
|
+
"""
|
|
66
|
+
Context manager that makes sure `with` block behaved according to the arguments.
|
|
67
|
+
|
|
68
|
+
Context manager yields a single object used that provides `set` method used to set the `with`
|
|
69
|
+
block result that is to be asserted.
|
|
70
|
+
|
|
71
|
+
If the `behaviour` parameter is set to an exception type, the `with` block is expected
|
|
72
|
+
to raise an exception of provided type and the `set` method of yielded object is expected
|
|
73
|
+
not to be called at all.
|
|
74
|
+
|
|
75
|
+
If the `behaviour` parameter is set to anything else, its value is expected to be equal to `with`
|
|
76
|
+
block result. The comparison is used with `==` operator, where `result` argument is on the left side and
|
|
77
|
+
the `with` block is expected to not raise any exceptions.
|
|
78
|
+
|
|
79
|
+
If the `behaviour` parameter is NOT set, the `with` block is expected NOT TO raise any exceptions and
|
|
80
|
+
the `set` method of yielded object is expected not to be called at all.
|
|
81
|
+
|
|
82
|
+
:param behaviour: describes expected behaviour of the `with` block
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@contextmanager
|
|
87
|
+
def assert_context(
|
|
88
|
+
*,
|
|
89
|
+
exception: type[Exception] | _Unset = _UNSET,
|
|
90
|
+
result: Any | _Unset = _UNSET,
|
|
91
|
+
behaviour: type[Exception] | Any | _Unset = _UNSET,
|
|
92
|
+
) -> Generator[AssertContext, None, None]:
|
|
93
|
+
"""
|
|
94
|
+
Context manager that makes sure `with` block behaved according to the arguments.
|
|
95
|
+
|
|
96
|
+
Pair of parameters `exception` and `result` are exclusive with parameter `behaviour`. If `behaviour`
|
|
97
|
+
is set to an exception type, it is the same as if `exception` would be set to this value and `result`
|
|
98
|
+
would not be set. If `behaviour` is set to any other value, its treated as the value of `result` and the
|
|
99
|
+
`exception` remains unset.
|
|
100
|
+
|
|
101
|
+
If the `exception` parameter is set, the `with` block is expected to raise an exception of provided type.
|
|
102
|
+
If it is NOT set, the `with` block is expected NOT TO raise any exceptions.
|
|
103
|
+
|
|
104
|
+
If the `result` parameter is set, its value is expected to be equal to `with` block result. The `with`
|
|
105
|
+
block result is set with the `set` method of an object yielded by the context manager. The comparison
|
|
106
|
+
is used with `==` operator, where `result` argument is on the left side.
|
|
107
|
+
If the `result` parameter is NOT set, the `set` method of yielded object is expected
|
|
108
|
+
not to be called at all.
|
|
109
|
+
|
|
110
|
+
:param exception: type of exception, expected to be raised
|
|
111
|
+
:param result: value expected to be set with `set` method of yielded object
|
|
112
|
+
:param behaviour: a common name for `exception` and `result` parameters, exclusive to them
|
|
113
|
+
:yield: object used to set the result of a `with` block
|
|
114
|
+
"""
|
|
115
|
+
if behaviour != _UNSET:
|
|
116
|
+
assert exception == _UNSET
|
|
117
|
+
assert result == _UNSET
|
|
118
|
+
if isinstance(behaviour, type) and issubclass(behaviour, Exception):
|
|
119
|
+
exception = behaviour
|
|
120
|
+
else:
|
|
121
|
+
result = behaviour
|
|
122
|
+
context = AssertContext()
|
|
123
|
+
exception_behaviour = nullcontext() if isinstance(exception, _Unset) else pytest.raises(exception)
|
|
124
|
+
|
|
125
|
+
with ExitStack() as behaviours_stack:
|
|
126
|
+
behaviours_stack.enter_context(context.assert_result_equal(result))
|
|
127
|
+
behaviours_stack.enter_context(exception_behaviour)
|
|
128
|
+
yield context
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from ._assert_object import assert_object, Expectation
|
|
2
|
+
from ._asserter_factories import AsserterFactory, BuiltInAsserterFactory
|
|
3
|
+
from ._converters import (
|
|
4
|
+
expectation_from_yaml,
|
|
5
|
+
expectation_to_yaml,
|
|
6
|
+
ExpectationYamlDumper,
|
|
7
|
+
ExpectationYamlLoader,
|
|
8
|
+
)
|
|
9
|
+
from ._expectation_modificators import (
|
|
10
|
+
MappingSubset,
|
|
11
|
+
ObjectAttributes,
|
|
12
|
+
Stringified,
|
|
13
|
+
Unordered,
|
|
14
|
+
with_type,
|
|
15
|
+
WithType,
|
|
16
|
+
)
|
|
17
|
+
from ._objects_asserters import Asserter
|
|
18
|
+
from ._protocols import AsserterFactoryProtocol, AsserterProtocol, GeneratorFactoryProtocol, GeneratorProtocol
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'assert_object',
|
|
23
|
+
'Expectation',
|
|
24
|
+
'AsserterFactory',
|
|
25
|
+
'BuiltInAsserterFactory',
|
|
26
|
+
'expectation_to_yaml',
|
|
27
|
+
'expectation_from_yaml',
|
|
28
|
+
'ExpectationYamlDumper',
|
|
29
|
+
'ExpectationYamlLoader',
|
|
30
|
+
'MappingSubset',
|
|
31
|
+
'ObjectAttributes',
|
|
32
|
+
'Stringified',
|
|
33
|
+
'Unordered',
|
|
34
|
+
'with_type',
|
|
35
|
+
'WithType',
|
|
36
|
+
'Asserter',
|
|
37
|
+
'AsserterProtocol',
|
|
38
|
+
'AsserterFactoryProtocol',
|
|
39
|
+
'GeneratorFactoryProtocol',
|
|
40
|
+
'GeneratorProtocol',
|
|
41
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ._asserter_factories import BuiltInAsserterFactory
|
|
4
|
+
from ._protocols import AsserterFactoryProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def assert_object(
|
|
8
|
+
expectation: Any, # noqa: ANN401
|
|
9
|
+
object_: Any, # noqa: ANN401
|
|
10
|
+
*,
|
|
11
|
+
asserter_factory: type[AsserterFactoryProtocol] = BuiltInAsserterFactory,
|
|
12
|
+
) -> None:
|
|
13
|
+
asserter = asserter_factory.create(expectation)
|
|
14
|
+
try:
|
|
15
|
+
asserter.assert_object(object_)
|
|
16
|
+
except AssertionError as exc:
|
|
17
|
+
msg = f'Object assertion failed. {exc!s}'
|
|
18
|
+
raise AssertionError(msg) from None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Expectation:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
expectation: Any, # noqa: ANN401
|
|
25
|
+
*,
|
|
26
|
+
asserter_factory: type[AsserterFactoryProtocol] = BuiltInAsserterFactory,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.expectation = expectation
|
|
29
|
+
self.asserter_factory = asserter_factory
|
|
30
|
+
|
|
31
|
+
def __eq__(self, value: object) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
assert_object(self.expectation, value, asserter_factory=self.asserter_factory)
|
|
34
|
+
except AssertionError:
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
4
|
+
from panda_pytest_assertions.assert_object._protocols import AsserterProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsserterFactory:
|
|
8
|
+
"""
|
|
9
|
+
Factory for registered asserters.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
#: list of registered asserter classes
|
|
13
|
+
_ASSERTER_TYPES: ClassVar[deque[type[AsserterProtocol]]] = deque()
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def create(cls, expectation: Any) -> AsserterProtocol: # noqa: ANN401
|
|
17
|
+
"""
|
|
18
|
+
Create asserter object for given expectation.
|
|
19
|
+
|
|
20
|
+
The function goes through all registered asserter types and creates the first one that matches.
|
|
21
|
+
|
|
22
|
+
:param expectation: expectation to create asserter for
|
|
23
|
+
:return: created asserter
|
|
24
|
+
"""
|
|
25
|
+
for asserter_type in cls._ASSERTER_TYPES:
|
|
26
|
+
if asserter_type.matches(expectation):
|
|
27
|
+
return asserter_type(cls, expectation)
|
|
28
|
+
msg = (
|
|
29
|
+
f'None of the registered asserters matches expectation "{expectation}" '
|
|
30
|
+
f'of type "{type(expectation)}".'
|
|
31
|
+
)
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def register_asserter(
|
|
36
|
+
cls,
|
|
37
|
+
asserter: type[AsserterProtocol],
|
|
38
|
+
*,
|
|
39
|
+
after: type[AsserterProtocol] | None = None,
|
|
40
|
+
before: type[AsserterProtocol] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Register the provided asserter to be used.
|
|
44
|
+
|
|
45
|
+
The asserter is registered at the earliest possible position. If `after` and `before`
|
|
46
|
+
parameters are not set, it is registered at the beginning of the list to be matched
|
|
47
|
+
when creating asserter for expectations.
|
|
48
|
+
|
|
49
|
+
:param asserter: asserter to be registered
|
|
50
|
+
:param after: registered asserter must be matched after this one
|
|
51
|
+
:param before: registered asserter must be matched before this one
|
|
52
|
+
"""
|
|
53
|
+
if '_ASSERTER_TYPES' not in cls.__dict__:
|
|
54
|
+
cls._ASSERTER_TYPES = cls._ASSERTER_TYPES.copy()
|
|
55
|
+
|
|
56
|
+
after_position = cls._ASSERTER_TYPES.index(after) if after is not None else -1
|
|
57
|
+
before_position = (
|
|
58
|
+
cls._ASSERTER_TYPES.index(before) if before is not None else len(cls._ASSERTER_TYPES) + 1
|
|
59
|
+
)
|
|
60
|
+
if after_position >= before_position:
|
|
61
|
+
msg = (
|
|
62
|
+
f'Cannot register asserter between "{after}" and "{before}" as their '
|
|
63
|
+
f'respective positions are: "{after_position}" and "{before_position}"'
|
|
64
|
+
)
|
|
65
|
+
raise ValueError(msg)
|
|
66
|
+
cls._ASSERTER_TYPES.insert(after_position + 1, asserter)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from panda_pytest_assertions.assert_object._objects_asserters import (
|
|
2
|
+
EqualityAsserter,
|
|
3
|
+
MappingAsserter,
|
|
4
|
+
MappingSubsetAsserter,
|
|
5
|
+
ObjectAttributesAsserter,
|
|
6
|
+
OrderedElementsAsserter,
|
|
7
|
+
StringifiedAsserter,
|
|
8
|
+
UnorderedElementsAsserter,
|
|
9
|
+
WithTypeAsserter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .asserter_factory import AsserterFactory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BuiltInAsserterFactory(AsserterFactory):
|
|
16
|
+
"""
|
|
17
|
+
Factory that has all built-in asserters registered.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# MUST be the first one as it implements a fallback mechanism and matches everything
|
|
22
|
+
BuiltInAsserterFactory.register_asserter(EqualityAsserter)
|
|
23
|
+
BuiltInAsserterFactory.register_asserter(MappingAsserter)
|
|
24
|
+
BuiltInAsserterFactory.register_asserter(MappingSubsetAsserter)
|
|
25
|
+
BuiltInAsserterFactory.register_asserter(ObjectAttributesAsserter)
|
|
26
|
+
BuiltInAsserterFactory.register_asserter(OrderedElementsAsserter)
|
|
27
|
+
BuiltInAsserterFactory.register_asserter(StringifiedAsserter)
|
|
28
|
+
BuiltInAsserterFactory.register_asserter(UnorderedElementsAsserter)
|
|
29
|
+
BuiltInAsserterFactory.register_asserter(WithTypeAsserter)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .expectation_from_yaml import expectation_from_yaml, ExpectationYamlLoader
|
|
2
|
+
from .expectation_to_yaml import expectation_to_yaml, ExpectationYamlDumper
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'expectation_from_yaml',
|
|
7
|
+
'ExpectationYamlLoader',
|
|
8
|
+
'expectation_to_yaml',
|
|
9
|
+
'ExpectationYamlDumper',
|
|
10
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, cast, TypeVar
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from yaml.constructor import ConstructorError
|
|
6
|
+
|
|
7
|
+
from panda_pytest_assertions.assert_object._expectation_modificators import (
|
|
8
|
+
MappingSubset,
|
|
9
|
+
ObjectAttributes,
|
|
10
|
+
Stringified,
|
|
11
|
+
Unordered,
|
|
12
|
+
WithType,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expectation_from_yaml(yaml_string: str) -> Any: # noqa: ANN401
|
|
17
|
+
"""
|
|
18
|
+
Serialize expectation to YAML format.
|
|
19
|
+
|
|
20
|
+
:param yaml_string: YAML string to create expectation from
|
|
21
|
+
:return: YAML string representing expectation
|
|
22
|
+
"""
|
|
23
|
+
return yaml.load(yaml_string, Loader=ExpectationYamlLoader) # noqa: S506
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_T = TypeVar('_T')
|
|
27
|
+
_Constructor = Callable[['ExpectationYamlLoader', yaml.Node], _T]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExpectationYamlLoader(yaml.SafeLoader):
|
|
31
|
+
@classmethod
|
|
32
|
+
def add_construct(cls, tag: str) -> Callable[[_Constructor[_T]], _Constructor[_T]]:
|
|
33
|
+
def _wrapper(constructor: _Constructor[_T]) -> _Constructor[_T]:
|
|
34
|
+
ExpectationYamlLoader.add_constructor(tag, constructor)
|
|
35
|
+
return constructor
|
|
36
|
+
|
|
37
|
+
return _wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@ExpectationYamlLoader.add_construct('!MappingSubset')
|
|
41
|
+
def _(loader: ExpectationYamlLoader, node: yaml.Node) -> MappingSubset:
|
|
42
|
+
if not isinstance(node, yaml.MappingNode):
|
|
43
|
+
raise ConstructorError(
|
|
44
|
+
problem=f'expected a mapping node, but found {type(node)}',
|
|
45
|
+
problem_mark=node.start_mark,
|
|
46
|
+
)
|
|
47
|
+
return MappingSubset(loader.construct_mapping(node))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@ExpectationYamlLoader.add_construct('!ObjectAttributes')
|
|
51
|
+
def _(loader: ExpectationYamlLoader, node: yaml.Node) -> ObjectAttributes:
|
|
52
|
+
if not isinstance(node, yaml.MappingNode):
|
|
53
|
+
raise ConstructorError(
|
|
54
|
+
problem=f'expected a mapping node, but found {type(node)}',
|
|
55
|
+
problem_mark=node.start_mark,
|
|
56
|
+
)
|
|
57
|
+
node_data = loader.construct_mapping(node)
|
|
58
|
+
if any(not isinstance(key, str) for key in node_data):
|
|
59
|
+
raise ConstructorError(
|
|
60
|
+
problem='all keys in ObjectAttributes must be strings',
|
|
61
|
+
problem_mark=node.start_mark,
|
|
62
|
+
)
|
|
63
|
+
return ObjectAttributes(cast(dict[str, Any], node_data))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@ExpectationYamlLoader.add_construct('!Stringified')
|
|
67
|
+
def _(loader: ExpectationYamlLoader, node: yaml.Node) -> Stringified:
|
|
68
|
+
if not isinstance(node, yaml.ScalarNode):
|
|
69
|
+
raise ConstructorError(
|
|
70
|
+
problem=f'expected a scalar node, but found {type(node)}',
|
|
71
|
+
problem_mark=node.start_mark,
|
|
72
|
+
)
|
|
73
|
+
# I was not able to produce yaml that would return something else than a string
|
|
74
|
+
node_data = cast(str, loader.construct_scalar(node))
|
|
75
|
+
return Stringified(node_data)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@ExpectationYamlLoader.add_construct('!Unordered')
|
|
79
|
+
def _(loader: ExpectationYamlLoader, node: yaml.Node) -> Unordered:
|
|
80
|
+
if not isinstance(node, yaml.SequenceNode):
|
|
81
|
+
raise ConstructorError(
|
|
82
|
+
problem=f'expected a sequence node, but found {type(node)}',
|
|
83
|
+
problem_mark=node.start_mark,
|
|
84
|
+
)
|
|
85
|
+
return Unordered(loader.construct_sequence(node))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_WITH_TYPE_KEYS = {'expectation', 'expected_type_name', 'expected_type_module'}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@ExpectationYamlLoader.add_construct('!WithType')
|
|
92
|
+
def _(loader: ExpectationYamlLoader, node: yaml.Node) -> WithType:
|
|
93
|
+
if not isinstance(node, yaml.MappingNode):
|
|
94
|
+
raise ConstructorError(
|
|
95
|
+
problem=f'expected a mapping node, but found {type(node)}',
|
|
96
|
+
problem_mark=node.start_mark,
|
|
97
|
+
)
|
|
98
|
+
node_data = loader.construct_mapping(node)
|
|
99
|
+
if node_data.keys() != _WITH_TYPE_KEYS:
|
|
100
|
+
raise ConstructorError(
|
|
101
|
+
problem=f'WithType keys must be {_WITH_TYPE_KEYS}',
|
|
102
|
+
problem_mark=node.start_mark,
|
|
103
|
+
)
|
|
104
|
+
if not isinstance(expected_type_name := node_data['expected_type_name'], str):
|
|
105
|
+
raise ConstructorError(
|
|
106
|
+
problem='value of WithType "expected_type_name" must be a string',
|
|
107
|
+
problem_mark=node.start_mark,
|
|
108
|
+
)
|
|
109
|
+
if not isinstance(expected_type_module := node_data['expected_type_module'], str | None):
|
|
110
|
+
raise ConstructorError(
|
|
111
|
+
problem='value of WithType "expected_type_module" must be a string or None',
|
|
112
|
+
problem_mark=node.start_mark,
|
|
113
|
+
)
|
|
114
|
+
return WithType(node_data['expectation'], expected_type_name, expected_type_module)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from panda_pytest_assertions.assert_object._expectation_modificators import (
|
|
8
|
+
MappingSubset,
|
|
9
|
+
ObjectAttributes,
|
|
10
|
+
Stringified,
|
|
11
|
+
Unordered,
|
|
12
|
+
with_type,
|
|
13
|
+
WithType,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def expectation_to_yaml(expectation: Any) -> str: # noqa: ANN401
|
|
18
|
+
"""
|
|
19
|
+
Serialize expectation to YAML format.
|
|
20
|
+
|
|
21
|
+
:param expectation: expectation to be dumped to YAML
|
|
22
|
+
:return: YAML string representing expectation
|
|
23
|
+
"""
|
|
24
|
+
return yaml.dump(expectation, Dumper=ExpectationYamlDumper, width=10000)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_T = TypeVar('_T')
|
|
28
|
+
|
|
29
|
+
_Representer = Callable[['ExpectationYamlDumper', _T], yaml.Node]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExpectationYamlDumper(yaml.SafeDumper):
|
|
33
|
+
@classmethod
|
|
34
|
+
def add_repr(
|
|
35
|
+
cls,
|
|
36
|
+
data_type: type[_T],
|
|
37
|
+
*,
|
|
38
|
+
is_multi: bool = False,
|
|
39
|
+
) -> Callable[[_Representer[_T]], _Representer[_T]]:
|
|
40
|
+
def _wrapper(representer: _Representer[_T]) -> _Representer[_T]:
|
|
41
|
+
if is_multi:
|
|
42
|
+
ExpectationYamlDumper.add_multi_representer(data_type, representer)
|
|
43
|
+
else:
|
|
44
|
+
ExpectationYamlDumper.add_representer(data_type, representer)
|
|
45
|
+
return representer
|
|
46
|
+
|
|
47
|
+
return _wrapper
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@ExpectationYamlDumper.add_repr(MappingSubset)
|
|
51
|
+
def _(dumper: ExpectationYamlDumper, data: MappingSubset) -> yaml.MappingNode:
|
|
52
|
+
return dumper.represent_mapping('!MappingSubset', data.items_expectations)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@ExpectationYamlDumper.add_repr(ObjectAttributes)
|
|
56
|
+
def _(dumper: ExpectationYamlDumper, data: ObjectAttributes) -> yaml.MappingNode:
|
|
57
|
+
return dumper.represent_mapping('!ObjectAttributes', data.attributes_expectations)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@ExpectationYamlDumper.add_repr(Stringified)
|
|
61
|
+
def _(dumper: ExpectationYamlDumper, data: Stringified) -> yaml.ScalarNode:
|
|
62
|
+
return dumper.represent_scalar('!Stringified', data.stringified_value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@ExpectationYamlDumper.add_repr(Unordered)
|
|
66
|
+
def _(dumper: ExpectationYamlDumper, data: Unordered) -> yaml.SequenceNode:
|
|
67
|
+
return dumper.represent_sequence('!Unordered', data.elements_expectations)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@ExpectationYamlDumper.add_repr(WithType)
|
|
71
|
+
def _with_type_representer(dumper: ExpectationYamlDumper, data: WithType) -> yaml.MappingNode:
|
|
72
|
+
return dumper.represent_mapping('!WithType', data.__dict__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@ExpectationYamlDumper.add_repr(Enum, is_multi=True)
|
|
76
|
+
def _(dumper: ExpectationYamlDumper, data: Enum) -> yaml.Node:
|
|
77
|
+
expectation = with_type(type(data))(Stringified(str(data)))
|
|
78
|
+
return _with_type_representer(dumper, expectation)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .mapping_subset import MappingSubset
|
|
2
|
+
from .object_attibutes import ObjectAttributes
|
|
3
|
+
from .stringified import Stringified
|
|
4
|
+
from .unordered import Unordered
|
|
5
|
+
from .with_type import with_type, WithType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'MappingSubset',
|
|
10
|
+
'ObjectAttributes',
|
|
11
|
+
'Stringified',
|
|
12
|
+
'Unordered',
|
|
13
|
+
'WithType',
|
|
14
|
+
'with_type',
|
|
15
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MappingSubset:
|
|
6
|
+
"""
|
|
7
|
+
Wrapper for the expectation that describes a subset of a mapping object.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, items_expectations: Mapping[Any, Any], /) -> None:
|
|
11
|
+
#: mapping of items expectations
|
|
12
|
+
self.items_expectations = items_expectations
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f'MappingSubset({self.items_expectations!r})'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ObjectAttributes:
|
|
6
|
+
"""
|
|
7
|
+
Wrapper for the expectation that describes a object attributes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, attributes_expectations: Mapping[str, Any], /) -> None:
|
|
11
|
+
#: mapping of attributes expectations
|
|
12
|
+
self.attributes_expectations = attributes_expectations
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f'ObjectAttributes({self.attributes_expectations!r})'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class Stringified:
|
|
2
|
+
"""
|
|
3
|
+
Wrapper for the expectation that describes stringified object.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, stringified_value: str, /) -> None:
|
|
7
|
+
#: stringified value expectation
|
|
8
|
+
self.stringified_value = stringified_value
|
|
9
|
+
|
|
10
|
+
def __repr__(self) -> str:
|
|
11
|
+
return f'Stringified({self.stringified_value!r})'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Unordered:
|
|
6
|
+
"""
|
|
7
|
+
Wrapper for the expectation that describes unordered list.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, elements_expectations: list[Any], /) -> None:
|
|
11
|
+
#: list of elements expectations
|
|
12
|
+
self.elements_expectations = elements_expectations
|
|
13
|
+
|
|
14
|
+
def __iter__(self) -> Iterator[Any]:
|
|
15
|
+
return iter(self.elements_expectations)
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f'Unordered({self.elements_expectations!r})'
|