assertpy2 2.0.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.
- assertpy2/__init__.py +25 -0
- assertpy2/assertpy.py +468 -0
- assertpy2/base.py +435 -0
- assertpy2/collection.py +217 -0
- assertpy2/contains.py +386 -0
- assertpy2/date.py +219 -0
- assertpy2/dict.py +255 -0
- assertpy2/dynamic.py +113 -0
- assertpy2/exception.py +128 -0
- assertpy2/extracting.py +233 -0
- assertpy2/file.py +276 -0
- assertpy2/helpers.py +273 -0
- assertpy2/numeric.py +555 -0
- assertpy2/py.typed +0 -0
- assertpy2/snapshot.py +217 -0
- assertpy2/string.py +413 -0
- assertpy2-2.0.0.dist-info/METADATA +227 -0
- assertpy2-2.0.0.dist-info/RECORD +20 -0
- assertpy2-2.0.0.dist-info/WHEEL +4 -0
- assertpy2-2.0.0.dist-info/licenses/LICENSE +28 -0
assertpy2/dict.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Copyright (c) 2015-2019, Activision Publishing, Inc.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
# are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
# list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
# and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
# may be used to endorse or promote products derived from this software without
|
|
16
|
+
# specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
22
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
25
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from typing_extensions import Self
|
|
35
|
+
|
|
36
|
+
__tracebackhide__ = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DictMixin:
|
|
40
|
+
"""Dict assertions mixin."""
|
|
41
|
+
|
|
42
|
+
def contains_key(self, *keys) -> Self:
|
|
43
|
+
"""Asserts the val is a dict and contains the given key or keys. Alias for :meth:`~assertpy.contains.ContainsMixin.contains`.
|
|
44
|
+
|
|
45
|
+
Checks if the dict contains the given key or keys using ``in`` operator.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
*keys: the key or keys expected to be contained
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
Usage::
|
|
52
|
+
|
|
53
|
+
assert_that({'a': 1, 'b': 2}).contains_key('a')
|
|
54
|
+
assert_that({'a': 1, 'b': 2}).contains_key('a', 'b')
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
AssertionError: if val does **not** contain the key or keys
|
|
61
|
+
"""
|
|
62
|
+
self._check_dict_like(self.val, check_values=False, check_getitem=False)
|
|
63
|
+
return self.contains(*keys)
|
|
64
|
+
|
|
65
|
+
def does_not_contain_key(self, *keys) -> Self:
|
|
66
|
+
"""Asserts the val is a dict and does not contain the given key or keys. Alias for :meth:`~assertpy.contains.ContainsMixin.does_not_contain`.
|
|
67
|
+
|
|
68
|
+
Checks if the dict excludes the given key or keys using ``in`` operator.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
*keys: the key or keys expected to be excluded
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
Usage::
|
|
75
|
+
|
|
76
|
+
assert_that({'a': 1, 'b': 2}).does_not_contain_key('x')
|
|
77
|
+
assert_that({'a': 1, 'b': 2}).does_not_contain_key('x', 'y')
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
AssertionError: if val **does** contain the key or keys
|
|
84
|
+
"""
|
|
85
|
+
self._check_dict_like(self.val, check_values=False, check_getitem=False)
|
|
86
|
+
return self.does_not_contain(*keys)
|
|
87
|
+
|
|
88
|
+
def contains_value(self, *values) -> Self:
|
|
89
|
+
"""Asserts that val is a dict and contains the given value or values.
|
|
90
|
+
|
|
91
|
+
Checks if the dict contains the given value or values in *any* key.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
*values: the value or values expected to be contained
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
Usage::
|
|
98
|
+
|
|
99
|
+
assert_that({'a': 1, 'b': 2}).contains_value(1)
|
|
100
|
+
assert_that({'a': 1, 'b': 2}).contains_value(1, 2)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
AssertionError: if val does **not** contain the value or values
|
|
107
|
+
"""
|
|
108
|
+
self._check_dict_like(self.val, check_getitem=False)
|
|
109
|
+
if len(values) == 0:
|
|
110
|
+
raise ValueError("one or more value args must be given")
|
|
111
|
+
missing = []
|
|
112
|
+
for v in values:
|
|
113
|
+
if v not in self.val.values():
|
|
114
|
+
missing.append(v)
|
|
115
|
+
if missing:
|
|
116
|
+
return self.error(
|
|
117
|
+
"Expected <%s> to contain values %s, but did not contain %s."
|
|
118
|
+
% (self.val, self._fmt_items(values), self._fmt_items(missing))
|
|
119
|
+
)
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def does_not_contain_value(self, *values) -> Self:
|
|
123
|
+
"""Asserts that val is a dict and does not contain the given value or values.
|
|
124
|
+
|
|
125
|
+
Checks if the dict excludes the given value or values across *all* keys.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
*values: the value or values expected to be excluded
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
Usage::
|
|
132
|
+
|
|
133
|
+
assert_that({'a': 1, 'b': 2}).does_not_contain_value(3)
|
|
134
|
+
assert_that({'a': 1, 'b': 2}).does_not_contain_value(3, 4)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
AssertionError: if val **does** contain the value or values
|
|
141
|
+
"""
|
|
142
|
+
self._check_dict_like(self.val, check_getitem=False)
|
|
143
|
+
if len(values) == 0:
|
|
144
|
+
raise ValueError("one or more value args must be given")
|
|
145
|
+
else:
|
|
146
|
+
found = []
|
|
147
|
+
for v in values:
|
|
148
|
+
if v in self.val.values():
|
|
149
|
+
found.append(v)
|
|
150
|
+
if found:
|
|
151
|
+
return self.error(
|
|
152
|
+
"Expected <%s> to not contain values %s, but did contain %s."
|
|
153
|
+
% (self.val, self._fmt_items(values), self._fmt_items(found))
|
|
154
|
+
)
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def contains_entry(self, *args, **kwargs) -> Self:
|
|
158
|
+
"""Asserts that val is a dict and contains the given entry or entries.
|
|
159
|
+
|
|
160
|
+
Checks if the dict contains the given key-value pair or pairs.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
*args: the entry or entries expected to be contained (as ``{k: v}`` args)
|
|
164
|
+
**kwargs: the entry or entries expected to be contained (as ``k=v`` kwargs)
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
Usage::
|
|
168
|
+
|
|
169
|
+
# using args
|
|
170
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1})
|
|
171
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2})
|
|
172
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2}, {'c': 3})
|
|
173
|
+
|
|
174
|
+
# using kwargs
|
|
175
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1)
|
|
176
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2)
|
|
177
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2, c=3)
|
|
178
|
+
|
|
179
|
+
# or args and kwargs
|
|
180
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'c': 3}, a=1, b=2)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
AssertionError: if val does **not** contain the entry or entries
|
|
188
|
+
"""
|
|
189
|
+
self._check_dict_like(self.val, check_values=False)
|
|
190
|
+
entries = list(args) + [{k: v} for k, v in kwargs.items()]
|
|
191
|
+
if len(entries) == 0:
|
|
192
|
+
raise ValueError("one or more entry args must be given")
|
|
193
|
+
missing = []
|
|
194
|
+
for e in entries:
|
|
195
|
+
if type(e) is not dict:
|
|
196
|
+
raise TypeError("given entry arg must be a dict")
|
|
197
|
+
if len(e) != 1:
|
|
198
|
+
raise ValueError("given entry args must contain exactly one key-value pair")
|
|
199
|
+
k = next(iter(e))
|
|
200
|
+
if k not in self.val:
|
|
201
|
+
missing.append(e) # bad key
|
|
202
|
+
elif self.val[k] != e[k]:
|
|
203
|
+
missing.append(e) # bad val
|
|
204
|
+
if missing:
|
|
205
|
+
return self.error(
|
|
206
|
+
"Expected <%s> to contain entries %s, but did not contain %s."
|
|
207
|
+
% (self.val, self._fmt_items(entries), self._fmt_items(missing))
|
|
208
|
+
)
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def does_not_contain_entry(self, *args, **kwargs) -> Self:
|
|
212
|
+
"""Asserts that val is a dict and does not contain the given entry or entries.
|
|
213
|
+
|
|
214
|
+
Checks if the dict excludes the given key-value pair or pairs.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
*args: the entry or entries expected to be excluded (as ``{k: v}`` args)
|
|
218
|
+
**kwargs: the entry or entries expected to be excluded (as ``k=v`` kwargs)
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
Usage::
|
|
222
|
+
|
|
223
|
+
# using args
|
|
224
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2})
|
|
225
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2}, {'x': 4})
|
|
226
|
+
|
|
227
|
+
# using kwargs
|
|
228
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2)
|
|
229
|
+
assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2, x=4)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
AssertionError: if val **does** contain the entry or entries
|
|
236
|
+
"""
|
|
237
|
+
self._check_dict_like(self.val, check_values=False)
|
|
238
|
+
entries = list(args) + [{k: v} for k, v in kwargs.items()]
|
|
239
|
+
if len(entries) == 0:
|
|
240
|
+
raise ValueError("one or more entry args must be given")
|
|
241
|
+
found = []
|
|
242
|
+
for e in entries:
|
|
243
|
+
if type(e) is not dict:
|
|
244
|
+
raise TypeError("given entry arg must be a dict")
|
|
245
|
+
if len(e) != 1:
|
|
246
|
+
raise ValueError("given entry args must contain exactly one key-value pair")
|
|
247
|
+
k = next(iter(e))
|
|
248
|
+
if k in self.val and e[k] == self.val[k]:
|
|
249
|
+
found.append(e)
|
|
250
|
+
if found:
|
|
251
|
+
return self.error(
|
|
252
|
+
"Expected <%s> to not contain entries %s, but did contain %s."
|
|
253
|
+
% (self.val, self._fmt_items(entries), self._fmt_items(found))
|
|
254
|
+
)
|
|
255
|
+
return self
|
assertpy2/dynamic.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright (c) 2015-2019, Activision Publishing, Inc.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
# are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
# list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
# and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
# may be used to endorse or promote products derived from this software without
|
|
16
|
+
# specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
22
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
25
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import collections
|
|
32
|
+
|
|
33
|
+
__tracebackhide__ = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DynamicMixin:
|
|
37
|
+
"""Dynamic assertions mixin.
|
|
38
|
+
|
|
39
|
+
When testing attributes of an object (or the contents of a dict), the
|
|
40
|
+
:meth:`~assertpy.base.BaseMixin.is_equal_to` assertion can be a bit verbose::
|
|
41
|
+
|
|
42
|
+
fred = Person('Fred', 'Smith')
|
|
43
|
+
|
|
44
|
+
assert_that(fred.first_name).is_equal_to('Fred')
|
|
45
|
+
assert_that(fred.name).is_equal_to('Fred Smith')
|
|
46
|
+
assert_that(fred.say_hello()).is_equal_to('Hello, Fred!')
|
|
47
|
+
|
|
48
|
+
Instead, use dynamic assertions in the form of ``has_<name>()`` where ``<name>`` is the name of
|
|
49
|
+
any attribute, property, or zero-argument method on the given object. Dynamic equality
|
|
50
|
+
assertions test if actual is equal to expected using the ``==`` operator. Using dynamic
|
|
51
|
+
assertions, we can rewrite the above example as::
|
|
52
|
+
|
|
53
|
+
assert_that(fred).has_first_name('Fred')
|
|
54
|
+
assert_that(fred).has_name('Fred Smith')
|
|
55
|
+
assert_that(fred).has_say_hello('Hello, Fred!')
|
|
56
|
+
|
|
57
|
+
Similarly, dynamic assertions also work on any *dict-like* object::
|
|
58
|
+
|
|
59
|
+
fred = {
|
|
60
|
+
'first_name': 'Fred',
|
|
61
|
+
'last_name': 'Smith',
|
|
62
|
+
'shoe_size': 12
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assert_that(fred).has_first_name('Fred')
|
|
66
|
+
assert_that(fred).has_last_name('Smith')
|
|
67
|
+
assert_that(fred).has_shoe_size(12)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __getattr__(self, attr):
|
|
71
|
+
"""Asserts that val has attribute attr and that its value is equal to other via a dynamic
|
|
72
|
+
assertion of the form ``has_<attr>()``."""
|
|
73
|
+
if not attr.startswith("has_"):
|
|
74
|
+
raise AttributeError("assertpy has no assertion <%s()>" % attr)
|
|
75
|
+
|
|
76
|
+
attr_name = attr[4:]
|
|
77
|
+
err_msg = False
|
|
78
|
+
is_namedtuple = isinstance(self.val, tuple) and hasattr(self.val, "_fields")
|
|
79
|
+
is_dict = isinstance(self.val, collections.abc.Iterable) and hasattr(self.val, "__getitem__")
|
|
80
|
+
|
|
81
|
+
if not hasattr(self.val, attr_name):
|
|
82
|
+
if is_dict and not is_namedtuple:
|
|
83
|
+
if attr_name not in self.val:
|
|
84
|
+
err_msg = "Expected key <%s>, but val has no key <%s>." % (attr_name, attr_name)
|
|
85
|
+
else:
|
|
86
|
+
err_msg = "Expected attribute <%s>, but val has no attribute <%s>." % (attr_name, attr_name)
|
|
87
|
+
|
|
88
|
+
def _wrapper(*args, **kwargs):
|
|
89
|
+
if err_msg:
|
|
90
|
+
return self.error(err_msg) # ok to raise AssertionError now that we are inside wrapper
|
|
91
|
+
else:
|
|
92
|
+
if len(args) != 1:
|
|
93
|
+
raise TypeError("assertion <%s()> takes exactly 1 argument (%d given)" % (attr, len(args)))
|
|
94
|
+
|
|
95
|
+
val_attr = self.val[attr_name] if is_dict and not is_namedtuple else getattr(self.val, attr_name)
|
|
96
|
+
|
|
97
|
+
if callable(val_attr):
|
|
98
|
+
try:
|
|
99
|
+
actual = val_attr()
|
|
100
|
+
except TypeError:
|
|
101
|
+
raise TypeError("val does not have zero-arg method <%s()>" % attr_name) from None
|
|
102
|
+
else:
|
|
103
|
+
actual = val_attr
|
|
104
|
+
|
|
105
|
+
expected = args[0]
|
|
106
|
+
if actual != expected:
|
|
107
|
+
return self.error(
|
|
108
|
+
"Expected <%s> to be equal to <%s> on %s <%s>, but was not."
|
|
109
|
+
% (actual, expected, "key" if is_dict else "attribute", attr_name)
|
|
110
|
+
)
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
return _wrapper
|
assertpy2/exception.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright (c) 2015-2019, Activision Publishing, Inc.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
# are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
# list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
# and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
# may be used to endorse or promote products derived from this software without
|
|
16
|
+
# specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
22
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
25
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from typing_extensions import Self
|
|
35
|
+
|
|
36
|
+
__tracebackhide__ = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _InertBuilder:
|
|
40
|
+
"""No-op builder returned after a failed raises/when_called_with in soft mode.
|
|
41
|
+
|
|
42
|
+
Silently absorbs all chained assertions so they don't crash on wrong val type.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __getattr__(self, name):
|
|
46
|
+
return lambda *args, **kwargs: self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExceptionMixin:
|
|
50
|
+
"""Expected exception mixin."""
|
|
51
|
+
|
|
52
|
+
def raises(self, ex) -> Self:
|
|
53
|
+
"""Asserts that val is callable and set the expected exception.
|
|
54
|
+
|
|
55
|
+
Just sets the expected exception, but never calls val, and therefore never failes. You must
|
|
56
|
+
chain to :meth:`~when_called_with` to invoke ``val()``.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
ex: the expected exception
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
Usage::
|
|
63
|
+
|
|
64
|
+
assert_that(some_func).raises(RuntimeError).when_called_with('foo')
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
AssertionBuilder: returns a new instance (now with the given expected exception) to chain to the next assertion
|
|
68
|
+
"""
|
|
69
|
+
if not callable(self.val):
|
|
70
|
+
raise TypeError("val must be callable")
|
|
71
|
+
if not issubclass(ex, BaseException):
|
|
72
|
+
raise TypeError("given arg must be exception")
|
|
73
|
+
|
|
74
|
+
# chain on with ex as the expected exception
|
|
75
|
+
return self.builder(self.val, self.description, self.kind, ex, self.logger)
|
|
76
|
+
|
|
77
|
+
def when_called_with(self, *some_args, **some_kwargs) -> Self:
|
|
78
|
+
"""Asserts that val, when invoked with the given args and kwargs, raises the expected exception.
|
|
79
|
+
|
|
80
|
+
Invokes ``val()`` with the given args and kwargs. You must first set the expected
|
|
81
|
+
exception with :meth:`~raises`.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
*some_args: the args to call ``val()``
|
|
85
|
+
**some_kwargs: the kwargs to call ``val()``
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
Usage::
|
|
89
|
+
|
|
90
|
+
def some_func(a):
|
|
91
|
+
raise RuntimeError('some error!')
|
|
92
|
+
|
|
93
|
+
assert_that(some_func).raises(RuntimeError).when_called_with('foo')
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
AssertionBuilder: returns a new instance (now with the captured exception error message as the val) to chain to the next assertion
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
AssertionError: if val does **not** raise the expected exception
|
|
100
|
+
TypeError: if expected exception not set via :meth:`raises`
|
|
101
|
+
"""
|
|
102
|
+
if not self.expected:
|
|
103
|
+
raise TypeError("expected exception not set, raises() must be called first")
|
|
104
|
+
try:
|
|
105
|
+
self.val(*some_args, **some_kwargs)
|
|
106
|
+
except BaseException as e:
|
|
107
|
+
if issubclass(type(e), self.expected):
|
|
108
|
+
# chain on with error message
|
|
109
|
+
return self.builder(str(e), self.description, self.kind, logger=self.logger)
|
|
110
|
+
else:
|
|
111
|
+
# got exception, but wrong type, so raise
|
|
112
|
+
self.error(
|
|
113
|
+
"Expected <%s> to raise <%s> when called with (%s), but raised <%s>."
|
|
114
|
+
% (
|
|
115
|
+
self.val.__name__,
|
|
116
|
+
self.expected.__name__,
|
|
117
|
+
self._fmt_args_kwargs(*some_args, **some_kwargs),
|
|
118
|
+
type(e).__name__,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return _InertBuilder()
|
|
122
|
+
|
|
123
|
+
# didn't fail as expected, so raise
|
|
124
|
+
self.error(
|
|
125
|
+
"Expected <%s> to raise <%s> when called with (%s)."
|
|
126
|
+
% (self.val.__name__, self.expected.__name__, self._fmt_args_kwargs(*some_args, **some_kwargs))
|
|
127
|
+
)
|
|
128
|
+
return _InertBuilder()
|