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.
@@ -0,0 +1,233 @@
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.abc
32
+ from typing import TYPE_CHECKING
33
+
34
+ if TYPE_CHECKING:
35
+ from typing_extensions import Self
36
+
37
+ __tracebackhide__ = True
38
+
39
+
40
+ class ExtractingMixin:
41
+ """Collection flattening mixin.
42
+
43
+ It is often necessary to test collections of objects. Use the ``extracting()`` helper to
44
+ reduce the collection on a given attribute. Reduce a list of objects::
45
+
46
+ alice = Person('Alice', 'Alpha')
47
+ bob = Person('Bob', 'Bravo')
48
+ people = [alice, bob]
49
+
50
+ assert_that(people).extracting('first_name').is_equal_to(['Alice', 'Bob'])
51
+ assert_that(people).extracting('first_name').contains('Alice', 'Bob')
52
+ assert_that(people).extracting('first_name').does_not_contain('Charlie')
53
+
54
+ Additionally, the ``extracting()`` helper can accept a list of attributes to be extracted, and
55
+ will flatten them into a list of tuples. Reduce a list of objects on multiple attributes::
56
+
57
+ assert_that(people).extracting('first_name', 'last_name').contains(('Alice', 'Alpha'), ('Bob', 'Bravo'))
58
+
59
+ Also, ``extracting()`` works on not just attributes, but also properties, and even
60
+ zero-argument methods. Reduce a list of object on properties and zero-arg methods::
61
+
62
+ assert_that(people).extracting('name').contains('Alice Alpha', 'Bob Bravo')
63
+ assert_that(people).extracting('say_hello').contains('Hello, Alice!', 'Hello, Bob!')
64
+
65
+ And ``extracting()`` even works on *dict-like* objects. Reduce a list of dicts on key::
66
+
67
+ alice = {'first_name': 'Alice', 'last_name': 'Alpha'}
68
+ bob = {'first_name': 'Bob', 'last_name': 'Bravo'}
69
+ people = [alice, bob]
70
+
71
+ assert_that(people).extracting('first_name').contains('Alice', 'Bob')
72
+
73
+ **Filtering**
74
+
75
+ The ``extracting()`` helper can include a *filter* to keep only those items for which the given
76
+ *filter* is truthy. For example::
77
+
78
+ users = [
79
+ {'user': 'Alice', 'age': 36, 'active': True},
80
+ {'user': 'Bob', 'age': 40, 'active': False},
81
+ {'user': 'Charlie', 'age': 13, 'active': True}
82
+ ]
83
+
84
+ # filter the active users
85
+ assert_that(users).extracting('user', filter='active').is_equal_to(['Alice', 'Charlie'])
86
+
87
+ The *filter* can be a *dict-like* object and the extracted items are kept if and only if all
88
+ corresponding key-value pairs are equal::
89
+
90
+ assert_that(users).extracting('user', filter={'active': False}).is_equal_to(['Bob'])
91
+ assert_that(users).extracting('user', filter={'age': 36, 'active': True}).is_equal_to(['Alice'])
92
+
93
+ Or a *filter* can be any function (including an in-line ``lambda``) that accepts as its single
94
+ argument each item in the collection, and the extracted items are kept if the function
95
+ evaluates to ``True``::
96
+
97
+ assert_that(users).extracting('user', filter=lambda x: x['age'] > 20)
98
+ .is_equal_to(['Alice', 'Bob'])
99
+
100
+ **Sorting**
101
+
102
+ The ``extracting()`` helper can include a *sort* to enforce order on the extracted items.
103
+
104
+ The *sort* can be the name of a key (or attribute, or property, or zero-argument method) and
105
+ the extracted items are ordered by the corresponding values::
106
+
107
+ assert_that(users).extracting('user', sort='age').is_equal_to(['Charlie', 'Alice', 'Bob'])
108
+
109
+ The *sort* can be an iterable of names and the extracted items are ordered by
110
+ corresponding value of the first name, ties are broken by the corresponding values of the
111
+ second name, and so on::
112
+
113
+ assert_that(users).extracting('user', sort=['active', 'age']).is_equal_to(['Bob', 'Charlie', 'Alice'])
114
+
115
+ The *sort* can be any function (including an in-line ``lambda``) that accepts as its single
116
+ argument each item in the collection, and the extracted items are ordered by the corresponding
117
+ function return values::
118
+
119
+ assert_that(users).extracting('user', sort=lambda x: -x['age']).is_equal_to(['Bob', 'Alice', 'Charlie'])
120
+ """
121
+
122
+ def extracting(self, *names, **kwargs) -> Self:
123
+ """Asserts that val is iterable, then extracts the named attributes, properties, or
124
+ zero-arg methods into a list (or list of tuples if multiple names are given).
125
+
126
+ Args:
127
+ *names: the attribute to be extracted (or property or zero-arg method)
128
+ **kwargs: see below
129
+
130
+ Keyword Args:
131
+ filter: extract only those items where filter is truthy
132
+ sort: order the extracted items by the sort key
133
+
134
+ Examples:
135
+ Usage::
136
+
137
+ alice = User('Alice', 20, True)
138
+ bob = User('Bob', 30, False)
139
+ charlie = User('Charlie', 10, True)
140
+ users = [alice, bob, charlie]
141
+
142
+ assert_that(users).extracting('user').contains('Alice', 'Bob', 'Charlie')
143
+
144
+ Works with *dict-like* objects too::
145
+
146
+ users = [
147
+ {'user': 'Alice', 'age': 20, 'active': True},
148
+ {'user': 'Bob', 'age': 30, 'active': False},
149
+ {'user': 'Charlie', 'age': 10, 'active': True}
150
+ ]
151
+
152
+ assert_that(people).extracting('user').contains('Alice', 'Bob', 'Charlie')
153
+
154
+ Filter::
155
+
156
+ assert_that(users).extracting('user', filter='active').is_equal_to(['Alice', 'Charlie'])
157
+
158
+ Sort::
159
+
160
+ assert_that(users).extracting('user', sort='age').is_equal_to(['Charlie', 'Alice', 'Bob'])
161
+
162
+ Returns:
163
+ AssertionBuilder: returns a new instance (now with the extracted list as the val) to chain to the next assertion
164
+ """
165
+ if not isinstance(self.val, collections.abc.Iterable):
166
+ raise TypeError("val is not iterable")
167
+ if isinstance(self.val, str):
168
+ raise TypeError("val must not be string")
169
+ if len(names) == 0:
170
+ raise ValueError("one or more name args must be given")
171
+
172
+ def _extract(x, name):
173
+ if self._check_dict_like(x, check_values=False, return_as_bool=True):
174
+ if name in x:
175
+ return x[name]
176
+ else:
177
+ raise ValueError("item keys %s did not contain key <%s>" % (list(x.keys()), name))
178
+ elif isinstance(x, tuple) and hasattr(x, "_fields") and type(name) is str:
179
+ if name in x._fields:
180
+ return getattr(x, name)
181
+ else: # val has no attribute <foo>
182
+ raise ValueError("item attributes %s did no contain attribute <%s>" % (x._fields, name))
183
+ elif isinstance(x, collections.abc.Iterable): # FIXME, this does __getitem__, but doesn't check for it...
184
+ self._check_iterable(x, name="item")
185
+ return x[name]
186
+ elif hasattr(x, name):
187
+ attr = getattr(x, name)
188
+ if callable(attr):
189
+ try:
190
+ return attr()
191
+ except TypeError:
192
+ raise ValueError("item method <%s()> exists, but is not zero-arg method" % name) from None
193
+ else:
194
+ return attr
195
+ else:
196
+ raise ValueError("item does not have property or zero-arg method <%s>" % name)
197
+
198
+ def _filter(x):
199
+ if "filter" in kwargs:
200
+ if isinstance(kwargs["filter"], str):
201
+ return bool(_extract(x, kwargs["filter"]))
202
+ elif self._check_dict_like(kwargs["filter"], check_values=False, return_as_bool=True):
203
+ for k in kwargs["filter"]:
204
+ if isinstance(k, str) and _extract(x, k) != kwargs["filter"][k]:
205
+ return False
206
+ return True
207
+ elif callable(kwargs["filter"]):
208
+ return kwargs["filter"](x)
209
+ return kwargs["filter"] is None
210
+ return True
211
+
212
+ def _sort(x):
213
+ if "sort" in kwargs:
214
+ if isinstance(kwargs["sort"], str):
215
+ return _extract(x, kwargs["sort"])
216
+ elif isinstance(kwargs["sort"], collections.abc.Iterable):
217
+ items = []
218
+ for k in kwargs["sort"]:
219
+ if isinstance(k, str):
220
+ items.append(_extract(x, k))
221
+ return tuple(items)
222
+ elif callable(kwargs["sort"]):
223
+ return kwargs["sort"](x)
224
+ return 0
225
+
226
+ extracted = []
227
+ for i in sorted(self.val, key=lambda x: _sort(x)):
228
+ if _filter(i):
229
+ items = [_extract(i, name) for name in names]
230
+ extracted.append(tuple(items) if len(items) > 1 else items[0])
231
+
232
+ # chain on with _extracted_ list (don't chain to self!)
233
+ return self.builder(extracted, self.description, self.kind)
assertpy2/file.py ADDED
@@ -0,0 +1,276 @@
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 os
32
+ from typing import TYPE_CHECKING
33
+
34
+ if TYPE_CHECKING:
35
+ from typing_extensions import Self
36
+
37
+ __tracebackhide__ = True
38
+
39
+
40
+ def contents_of(file, encoding="utf-8"):
41
+ """Helper to read the contents of the given file or path into a string with the given encoding.
42
+
43
+ Args:
44
+ file: a *path-like object* (aka a file name) or a *file-like object* (aka a file)
45
+ encoding (str): the target encoding. Defaults to ``utf-8``, other useful encodings are ``ascii`` and ``latin-1``.
46
+
47
+ Examples:
48
+ Usage::
49
+
50
+ from assertpy2 import assert_that, contents_of
51
+
52
+ contents = contents_of('foo.txt')
53
+ assert_that(contents).starts_with('foo').ends_with('bar').contains('oob')
54
+
55
+ Returns:
56
+ str: returns the file contents as a string
57
+
58
+ Raises:
59
+ IOError: if file not found
60
+ TypeError: if file is not a *path-like object* or a *file-like object*
61
+ """
62
+ try:
63
+ contents = file.read()
64
+ except AttributeError:
65
+ try:
66
+ with open(file) as fp:
67
+ contents = fp.read()
68
+ except TypeError:
69
+ raise ValueError("val must be file or path, but was type <%s>" % type(file).__name__) from None
70
+ except OSError:
71
+ if not isinstance(file, (str, os.PathLike)):
72
+ raise ValueError("val must be file or path, but was type <%s>" % type(file).__name__) from None
73
+ raise
74
+
75
+ if type(contents) is bytes:
76
+ return contents.decode(encoding, "replace")
77
+ try:
78
+ return contents.decode(encoding, "replace")
79
+ except AttributeError:
80
+ pass
81
+ # if all else fails, just return the contents "as is"
82
+ return contents
83
+
84
+
85
+ class FileMixin:
86
+ """File assertions mixin."""
87
+
88
+ def exists(self) -> Self:
89
+ """Asserts that val is a path and that it exists.
90
+
91
+ Examples:
92
+ Usage::
93
+
94
+ assert_that('myfile.txt').exists()
95
+ assert_that('mydir').exists()
96
+
97
+ Returns:
98
+ AssertionBuilder: returns this instance to chain to the next assertion
99
+
100
+ Raises:
101
+ AssertionError: if val does **not** exist
102
+ """
103
+ if not isinstance(self.val, (str, os.PathLike)):
104
+ raise TypeError("val is not a path")
105
+ if not os.path.exists(self.val):
106
+ return self.error("Expected <%s> to exist, but was not found." % self.val)
107
+ return self
108
+
109
+ def does_not_exist(self) -> Self:
110
+ """Asserts that val is a path and that it does *not* exist.
111
+
112
+ Examples:
113
+ Usage::
114
+
115
+ assert_that('missing.txt').does_not_exist()
116
+ assert_that('missing_dir').does_not_exist()
117
+
118
+ Returns:
119
+ AssertionBuilder: returns this instance to chain to the next assertion
120
+
121
+ Raises:
122
+ AssertionError: if val **does** exist
123
+ """
124
+ if not isinstance(self.val, (str, os.PathLike)):
125
+ raise TypeError("val is not a path")
126
+ if os.path.exists(self.val):
127
+ return self.error("Expected <%s> to not exist, but was found." % self.val)
128
+ return self
129
+
130
+ def is_file(self) -> Self:
131
+ """Asserts that val is a *file* and that it exists.
132
+
133
+ Examples:
134
+ Usage::
135
+
136
+ assert_that('myfile.txt').is_file()
137
+
138
+ Returns:
139
+ AssertionBuilder: returns this instance to chain to the next assertion
140
+
141
+ Raises:
142
+ AssertionError: if val does **not** exist, or is **not** a file
143
+ """
144
+ self.exists()
145
+ if not os.path.isfile(self.val):
146
+ return self.error("Expected <%s> to be a file, but was not." % self.val)
147
+ return self
148
+
149
+ def is_directory(self) -> Self:
150
+ """Asserts that val is a *directory* and that it exists.
151
+
152
+ Examples:
153
+ Usage::
154
+
155
+ assert_that('mydir').is_directory()
156
+
157
+ Returns:
158
+ AssertionBuilder: returns this instance to chain to the next assertion
159
+
160
+ Raises:
161
+ AssertionError: if val does **not** exist, or is **not** a directory
162
+ """
163
+ self.exists()
164
+ if not os.path.isdir(self.val):
165
+ return self.error("Expected <%s> to be a directory, but was not." % self.val)
166
+ return self
167
+
168
+ def is_named(self, filename) -> Self:
169
+ """Asserts that val is an existing path to a file and that file is named filename.
170
+
171
+ Args:
172
+ filename: the expected filename
173
+
174
+ Examples:
175
+ Usage::
176
+
177
+ assert_that('/path/to/mydir/myfile.txt').is_named('myfile.txt')
178
+
179
+ Returns:
180
+ AssertionBuilder: returns this instance to chain to the next assertion
181
+
182
+ Raises:
183
+ AssertionError: if val does **not** exist, or is **not** a file, or is **not** named the given filename
184
+ """
185
+ self.is_file()
186
+ if not isinstance(filename, (str, os.PathLike)):
187
+ raise TypeError("given filename arg must be a path")
188
+ val_filename = os.path.basename(os.path.abspath(self.val))
189
+ if val_filename != filename:
190
+ return self.error("Expected filename <%s> to be equal to <%s>, but was not." % (val_filename, filename))
191
+ return self
192
+
193
+ def is_child_of(self, parent) -> Self:
194
+ """Asserts that val is an existing path to a file and that file is a child of parent.
195
+
196
+ Args:
197
+ parent: the expected parent directory
198
+
199
+ Examples:
200
+ Usage::
201
+
202
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('mydir')
203
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('to')
204
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('path')
205
+
206
+ Returns:
207
+ AssertionBuilder: returns this instance to chain to the next assertion
208
+
209
+ Raises:
210
+ AssertionError: if val does **not** exist, or is **not** a file, or is **not** a child of the given directory
211
+ """
212
+ self.is_file()
213
+ if not isinstance(parent, (str, os.PathLike)):
214
+ raise TypeError("given parent directory arg must be a path")
215
+ val_abspath = os.path.abspath(self.val)
216
+ parent_abspath = os.path.abspath(parent)
217
+ if not val_abspath.startswith(parent_abspath):
218
+ return self.error("Expected file <%s> to be a child of <%s>, but was not." % (val_abspath, parent_abspath))
219
+ return self
220
+
221
+ def is_readable(self) -> Self:
222
+ """Asserts that val is an existing path and is readable.
223
+
224
+ Examples:
225
+ Usage::
226
+
227
+ assert_that('/path/to/file.txt').is_readable()
228
+
229
+ Returns:
230
+ AssertionBuilder: returns this instance to chain to the next assertion
231
+
232
+ Raises:
233
+ AssertionError: if val does **not** exist, or is **not** readable
234
+ """
235
+ self.exists()
236
+ if not os.access(self.val, os.R_OK):
237
+ return self.error("Expected <%s> to be readable, but was not." % self.val)
238
+ return self
239
+
240
+ def is_writable(self) -> Self:
241
+ """Asserts that val is an existing path and is writable.
242
+
243
+ Examples:
244
+ Usage::
245
+
246
+ assert_that('/path/to/file.txt').is_writable()
247
+
248
+ Returns:
249
+ AssertionBuilder: returns this instance to chain to the next assertion
250
+
251
+ Raises:
252
+ AssertionError: if val does **not** exist, or is **not** writable
253
+ """
254
+ self.exists()
255
+ if not os.access(self.val, os.W_OK):
256
+ return self.error("Expected <%s> to be writable, but was not." % self.val)
257
+ return self
258
+
259
+ def is_executable(self) -> Self:
260
+ """Asserts that val is an existing path and is executable.
261
+
262
+ Examples:
263
+ Usage::
264
+
265
+ assert_that('/path/to/script.sh').is_executable()
266
+
267
+ Returns:
268
+ AssertionBuilder: returns this instance to chain to the next assertion
269
+
270
+ Raises:
271
+ AssertionError: if val does **not** exist, or is **not** executable
272
+ """
273
+ self.exists()
274
+ if not os.access(self.val, os.X_OK):
275
+ return self.error("Expected <%s> to be executable, but was not." % self.val)
276
+ return self