assertpy2 2.3.4__tar.gz → 2.3.6__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.6/CONTRIBUTING.md +50 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/PKG-INFO +45 -18
- {assertpy2-2.3.4 → assertpy2-2.3.6}/README.md +44 -17
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/__init__.py +2 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/_typing.py +12 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/assertpy.py +66 -34
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/base.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/collection.py +151 -31
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/contains.py +0 -29
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/date.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/dict.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/dynamic.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/exception.py +0 -29
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/extracting.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/file.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/helpers.py +0 -34
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/matchers.py +6 -6
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/numeric.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/snapshot.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/string.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/api.md +157 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/pyproject.toml +2 -2
- assertpy2-2.3.6/tests/test_bool.py +35 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_class.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_collection.py +0 -29
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_core.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_custom_dict.py +0 -28
- assertpy2-2.3.6/tests/test_custom_list.py +52 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_datetime.py +0 -28
- assertpy2-2.3.6/tests/test_description.py +35 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dict.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dict_compare.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dyn.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_equals.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_expected_exception.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_extensions.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_extracting.py +0 -28
- assertpy2-2.3.6/tests/test_fail.py +17 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_file.py +0 -28
- assertpy2-2.3.6/tests/test_in.py +47 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_list.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matchers.py +67 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_namedtuple.py +0 -28
- assertpy2-2.3.6/tests/test_none.py +29 -0
- assertpy2-2.3.6/tests/test_not.py +150 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_numbers.py +0 -28
- assertpy2-2.3.6/tests/test_pipeline.py +173 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_readme.py +0 -28
- assertpy2-2.3.6/tests/test_same_as.py +44 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_snapshots.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_soft.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_soft_fail.py +0 -28
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_string.py +0 -29
- assertpy2-2.3.6/tests/test_traceback.py +36 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_type.py +0 -28
- assertpy2-2.3.6/tests/test_warn.py +108 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/uv.lock +1 -1
- assertpy2-2.3.4/CONTRIBUTING.md +0 -22
- assertpy2-2.3.4/tests/test_bool.py +0 -63
- assertpy2-2.3.4/tests/test_custom_list.py +0 -80
- assertpy2-2.3.4/tests/test_description.py +0 -63
- assertpy2-2.3.4/tests/test_fail.py +0 -45
- assertpy2-2.3.4/tests/test_in.py +0 -75
- assertpy2-2.3.4/tests/test_none.py +0 -57
- assertpy2-2.3.4/tests/test_same_as.py +0 -72
- assertpy2-2.3.4/tests/test_traceback.py +0 -64
- assertpy2-2.3.4/tests/test_warn.py +0 -136
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.codecov.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/dependabot.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/.gitignore +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/LICENSE +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/SECURITY.md +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/logo.svg +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_async.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_errors.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matchers_phase3.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_overloads.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_structural.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions of docs, tests, or code are welcome.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
1. Fork the repo
|
|
8
|
+
2. Clone your fork (`git clone <your_fork_url>`)
|
|
9
|
+
3. Create a branch (`git checkout -b my_branch`)
|
|
10
|
+
4. Install dependencies: `uv sync`
|
|
11
|
+
5. Make your changes
|
|
12
|
+
6. Run the [verification pipeline](#verification-pipeline) and fix any issues
|
|
13
|
+
7. Commit using [Conventional Commits](#commit-style)
|
|
14
|
+
8. Push your branch (`git push origin my_branch`)
|
|
15
|
+
9. Open a [Pull Request](http://github.com/Solganis/assertpy2/pulls)
|
|
16
|
+
|
|
17
|
+
Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3.10+
|
|
22
|
+
- [uv](https://docs.astral.sh/uv/) as the package manager
|
|
23
|
+
|
|
24
|
+
## Verification pipeline
|
|
25
|
+
|
|
26
|
+
Run all checks before submitting a PR. Every step must pass.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# lint
|
|
30
|
+
uv run ruff check assertpy2/ tests/
|
|
31
|
+
|
|
32
|
+
# format
|
|
33
|
+
uv run ruff format --check assertpy2/ tests/
|
|
34
|
+
|
|
35
|
+
# type check
|
|
36
|
+
uv run ty check
|
|
37
|
+
|
|
38
|
+
# tests with coverage (must be 100%)
|
|
39
|
+
uv run pytest tests/ -v --cov=assertpy2 --cov-report=term-missing
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
CI requires 100% code coverage.
|
|
43
|
+
|
|
44
|
+
## Commit style
|
|
45
|
+
|
|
46
|
+
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
Write tests for every new feature or bug fix. Use `assertpy2` assertions in tests.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.6
|
|
4
4
|
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -160,8 +160,10 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
160
160
|
|
|
161
161
|
## Features
|
|
162
162
|
|
|
163
|
-
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`,
|
|
163
|
+
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
164
164
|
- [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
|
|
165
|
+
- [**Universal negation**](docs/api.md#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
166
|
+
- [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()` for transforming collections before assertions.
|
|
165
167
|
- [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for async and eventual consistency testing.
|
|
166
168
|
- [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
|
|
167
169
|
- **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
|
|
@@ -204,6 +206,15 @@ assert_that("hello").satisfies(~match.equal_to("world"))
|
|
|
204
206
|
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
205
207
|
```
|
|
206
208
|
|
|
209
|
+
Matchers also support `==` directly, so you can use them with plain `assert` or mix into dicts and lists:
|
|
210
|
+
|
|
211
|
+
```py
|
|
212
|
+
from assertpy2 import match
|
|
213
|
+
|
|
214
|
+
assert 42 == match.is_positive()
|
|
215
|
+
assert {"id": 5, "name": "Alice"} == {"id": match.is_positive(), "name": match.is_non_empty_string()}
|
|
216
|
+
```
|
|
217
|
+
|
|
207
218
|
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `is_zero`, `is_even`, `is_odd`, `is_divisible_by`, `is_callable`, `is_in`, `has_property`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
208
219
|
|
|
209
220
|
|
|
@@ -309,6 +320,38 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
|
309
320
|
|
|
310
321
|
## More features
|
|
311
322
|
|
|
323
|
+
### Universal negation
|
|
324
|
+
|
|
325
|
+
Invert any assertion with `.not_`:
|
|
326
|
+
|
|
327
|
+
```py
|
|
328
|
+
assert_that(5).not_.is_none()
|
|
329
|
+
assert_that("abc123").not_.is_alpha()
|
|
330
|
+
assert_that([3, 1, 2]).not_.is_sorted()
|
|
331
|
+
assert_that(value).described_as("check").not_.is_none().is_positive()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Works with soft assertions and warn mode.
|
|
335
|
+
|
|
336
|
+
### Collection pipeline
|
|
337
|
+
|
|
338
|
+
Transform collections before asserting:
|
|
339
|
+
|
|
340
|
+
```py
|
|
341
|
+
orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
|
|
342
|
+
|
|
343
|
+
assert_that(orders).filtered_on(lambda o: o.status == "FAILED").is_length(1)
|
|
344
|
+
assert_that(orders).mapped(lambda o: o.total).contains(100, 200)
|
|
345
|
+
assert_that(orders).first().has_status("DONE")
|
|
346
|
+
assert_that(orders).element(1).has_status("FAILED")
|
|
347
|
+
assert_that([42]).single().is_equal_to(42)
|
|
348
|
+
|
|
349
|
+
# chaining pipeline steps
|
|
350
|
+
assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
354
|
+
|
|
312
355
|
### Dict comparison with ignore/include
|
|
313
356
|
|
|
314
357
|
```py
|
|
@@ -483,19 +526,3 @@ See the [comparison table](#comparison) above for feature differences with other
|
|
|
483
526
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
484
527
|
|
|
485
528
|
|
|
486
|
-
## License
|
|
487
|
-
|
|
488
|
-
All files are licensed under the BSD 3-Clause License as follows:
|
|
489
|
-
|
|
490
|
-
> Copyright (c) 2015-2019, Activision Publishing, Inc.
|
|
491
|
-
> All rights reserved.
|
|
492
|
-
>
|
|
493
|
-
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
494
|
-
>
|
|
495
|
-
> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
496
|
-
>
|
|
497
|
-
> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
498
|
-
>
|
|
499
|
-
> 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
500
|
-
>
|
|
501
|
-
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -125,8 +125,10 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
125
125
|
|
|
126
126
|
## Features
|
|
127
127
|
|
|
128
|
-
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`,
|
|
128
|
+
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
129
129
|
- [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
|
|
130
|
+
- [**Universal negation**](docs/api.md#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
131
|
+
- [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()` for transforming collections before assertions.
|
|
130
132
|
- [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for async and eventual consistency testing.
|
|
131
133
|
- [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
|
|
132
134
|
- **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
|
|
@@ -169,6 +171,15 @@ assert_that("hello").satisfies(~match.equal_to("world"))
|
|
|
169
171
|
assert_that(150).satisfies(match.is_negative() | match.greater_than(100))
|
|
170
172
|
```
|
|
171
173
|
|
|
174
|
+
Matchers also support `==` directly, so you can use them with plain `assert` or mix into dicts and lists:
|
|
175
|
+
|
|
176
|
+
```py
|
|
177
|
+
from assertpy2 import match
|
|
178
|
+
|
|
179
|
+
assert 42 == match.is_positive()
|
|
180
|
+
assert {"id": 5, "name": "Alice"} == {"id": match.is_positive(), "name": match.is_non_empty_string()}
|
|
181
|
+
```
|
|
182
|
+
|
|
172
183
|
Available matchers: `equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `between`, `close_to`, `is_none`, `is_not_none`, `is_instance_of`, `has_length`, `is_empty`, `is_not_empty`, `is_positive`, `is_negative`, `is_zero`, `is_even`, `is_odd`, `is_divisible_by`, `is_callable`, `is_in`, `has_property`, `contains_string`, `matches_regex`, `is_uuid`, `is_non_empty_string`, `ignore`, `each_item`, `structure`.
|
|
173
184
|
|
|
174
185
|
|
|
@@ -274,6 +285,38 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
|
274
285
|
|
|
275
286
|
## More features
|
|
276
287
|
|
|
288
|
+
### Universal negation
|
|
289
|
+
|
|
290
|
+
Invert any assertion with `.not_`:
|
|
291
|
+
|
|
292
|
+
```py
|
|
293
|
+
assert_that(5).not_.is_none()
|
|
294
|
+
assert_that("abc123").not_.is_alpha()
|
|
295
|
+
assert_that([3, 1, 2]).not_.is_sorted()
|
|
296
|
+
assert_that(value).described_as("check").not_.is_none().is_positive()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Works with soft assertions and warn mode.
|
|
300
|
+
|
|
301
|
+
### Collection pipeline
|
|
302
|
+
|
|
303
|
+
Transform collections before asserting:
|
|
304
|
+
|
|
305
|
+
```py
|
|
306
|
+
orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
|
|
307
|
+
|
|
308
|
+
assert_that(orders).filtered_on(lambda o: o.status == "FAILED").is_length(1)
|
|
309
|
+
assert_that(orders).mapped(lambda o: o.total).contains(100, 200)
|
|
310
|
+
assert_that(orders).first().has_status("DONE")
|
|
311
|
+
assert_that(orders).element(1).has_status("FAILED")
|
|
312
|
+
assert_that([42]).single().is_equal_to(42)
|
|
313
|
+
|
|
314
|
+
# chaining pipeline steps
|
|
315
|
+
assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
319
|
+
|
|
277
320
|
### Dict comparison with ignore/include
|
|
278
321
|
|
|
279
322
|
```py
|
|
@@ -448,19 +491,3 @@ See the [comparison table](#comparison) above for feature differences with other
|
|
|
448
491
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
449
492
|
|
|
450
493
|
|
|
451
|
-
## License
|
|
452
|
-
|
|
453
|
-
All files are licensed under the BSD 3-Clause License as follows:
|
|
454
|
-
|
|
455
|
-
> Copyright (c) 2015-2019, Activision Publishing, Inc.
|
|
456
|
-
> All rights reserved.
|
|
457
|
-
>
|
|
458
|
-
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
459
|
-
>
|
|
460
|
-
> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
461
|
-
>
|
|
462
|
-
> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
463
|
-
>
|
|
464
|
-
> 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
465
|
-
>
|
|
466
|
-
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .assertpy import (
|
|
2
|
+
NegatedBuilder,
|
|
2
3
|
WarningLoggingAdapter,
|
|
3
4
|
__version__,
|
|
4
5
|
add_extension,
|
|
@@ -20,6 +21,7 @@ __all__ = [
|
|
|
20
21
|
"DiffEntry",
|
|
21
22
|
"DiffResult",
|
|
22
23
|
"Matcher",
|
|
24
|
+
"NegatedBuilder",
|
|
23
25
|
"WarningLoggingAdapter",
|
|
24
26
|
"__version__",
|
|
25
27
|
"add_extension",
|
|
@@ -8,6 +8,7 @@ if TYPE_CHECKING:
|
|
|
8
8
|
|
|
9
9
|
from typing_extensions import Self
|
|
10
10
|
|
|
11
|
+
from .assertpy import NegatedBuilder
|
|
11
12
|
from .async_assertions import AsyncAssertionBuilder
|
|
12
13
|
from .matchers import Matcher
|
|
13
14
|
|
|
@@ -35,6 +36,9 @@ if TYPE_CHECKING:
|
|
|
35
36
|
def is_not_in(self, *items: object) -> Self: ...
|
|
36
37
|
# SnapshotMixin
|
|
37
38
|
def snapshot(self, id: str | None = ..., path: str = ...) -> Self: ...
|
|
39
|
+
# NegatedBuilder
|
|
40
|
+
@property
|
|
41
|
+
def not_(self) -> NegatedBuilder: ...
|
|
38
42
|
|
|
39
43
|
class _StringAssertion(_CoreAssertion, Protocol):
|
|
40
44
|
"""Assertions available for ``str`` values."""
|
|
@@ -129,6 +133,14 @@ if TYPE_CHECKING:
|
|
|
129
133
|
def any_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
130
134
|
def all_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
131
135
|
def none_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
136
|
+
# CollectionMixin - pipeline
|
|
137
|
+
def filtered_on(self, predicate: Callable[..., bool]) -> Self: ...
|
|
138
|
+
def mapped(self, func: Callable[..., object]) -> Self: ...
|
|
139
|
+
def flat_mapped(self, func: Callable[..., object]) -> Self: ...
|
|
140
|
+
def first(self) -> Self: ...
|
|
141
|
+
def last(self) -> Self: ...
|
|
142
|
+
def element(self, index: int) -> Self: ...
|
|
143
|
+
def single(self) -> Self: ...
|
|
132
144
|
|
|
133
145
|
class _DictAssertion(_CoreAssertion, Protocol):
|
|
134
146
|
"""Assertions available for ``dict`` values."""
|
|
@@ -1,31 +1,3 @@
|
|
|
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
1
|
"""Assertion library for python unit testing with a fluent API"""
|
|
30
2
|
|
|
31
3
|
from __future__ import annotations
|
|
@@ -37,13 +9,12 @@ import logging
|
|
|
37
9
|
import os
|
|
38
10
|
import sys
|
|
39
11
|
import types
|
|
40
|
-
from collections.abc import Iterator
|
|
12
|
+
from collections.abc import Callable, Iterator
|
|
41
13
|
from typing import TYPE_CHECKING, overload
|
|
42
14
|
|
|
43
15
|
if TYPE_CHECKING:
|
|
44
16
|
import datetime
|
|
45
17
|
import pathlib
|
|
46
|
-
from collections.abc import Callable
|
|
47
18
|
|
|
48
19
|
from typing_extensions import Self
|
|
49
20
|
|
|
@@ -73,7 +44,7 @@ from .numeric import NumericMixin
|
|
|
73
44
|
from .snapshot import SnapshotMixin
|
|
74
45
|
from .string import StringMixin
|
|
75
46
|
|
|
76
|
-
__version__ = "2.3.
|
|
47
|
+
__version__ = "2.3.6"
|
|
77
48
|
|
|
78
49
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
79
50
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
@@ -397,7 +368,6 @@ def _builder(val, description="", kind=None, expected=None, logger=None):
|
|
|
397
368
|
"""Internal helper to build a new :class:`AssertionBuilder` instance and glue on any extension methods."""
|
|
398
369
|
ab = AssertionBuilder(val, description, kind, expected, logger)
|
|
399
370
|
if _extensions:
|
|
400
|
-
# glue extension method onto new builder instance
|
|
401
371
|
for name, func in _extensions.items():
|
|
402
372
|
meth = types.MethodType(func, ab)
|
|
403
373
|
setattr(ab, name, meth)
|
|
@@ -410,13 +380,11 @@ class WarningLoggingAdapter(logging.LoggerAdapter):
|
|
|
410
380
|
|
|
411
381
|
def process(self, msg, kwargs):
|
|
412
382
|
def _unwind(frame):
|
|
413
|
-
# walk all the frames
|
|
414
383
|
frames = []
|
|
415
384
|
while frame:
|
|
416
385
|
frames.append((frame.f_code.co_filename, frame.f_lineno))
|
|
417
386
|
frame = frame.f_back
|
|
418
387
|
|
|
419
|
-
# in reverse, find the first assertpy frame (and return the previous one)
|
|
420
388
|
prev = None
|
|
421
389
|
for frame in reversed(
|
|
422
390
|
frames
|
|
@@ -439,6 +407,65 @@ _logger.addHandler(_handler)
|
|
|
439
407
|
_default_logger = WarningLoggingAdapter(_logger, None)
|
|
440
408
|
|
|
441
409
|
|
|
410
|
+
class NegatedBuilder:
|
|
411
|
+
"""Proxy that inverts the next assertion. Created by ``assert_that(val).not_``."""
|
|
412
|
+
|
|
413
|
+
def __init__(self, builder: AssertionBuilder) -> None:
|
|
414
|
+
self._builder = builder
|
|
415
|
+
|
|
416
|
+
def __getattr__(self, name: str) -> object:
|
|
417
|
+
attr = getattr(self._builder, name)
|
|
418
|
+
if not callable(attr):
|
|
419
|
+
return attr
|
|
420
|
+
|
|
421
|
+
def _negated(*args: object, **kwargs: object) -> AssertionBuilder:
|
|
422
|
+
if self._builder.kind == "soft":
|
|
423
|
+
return self._negated_soft(name, attr, *args, **kwargs)
|
|
424
|
+
if self._builder.kind == "warn":
|
|
425
|
+
return self._negated_warn(name, attr, *args, **kwargs)
|
|
426
|
+
return self._negated_strict(name, attr, *args, **kwargs)
|
|
427
|
+
|
|
428
|
+
return _negated
|
|
429
|
+
|
|
430
|
+
def _make_msg(self, name: str) -> str:
|
|
431
|
+
desc = f"[{self._builder.description}] " if self._builder.description else ""
|
|
432
|
+
return f"{desc}Expected <{self._builder.val}> to NOT satisfy: {name}()"
|
|
433
|
+
|
|
434
|
+
def _negated_strict(
|
|
435
|
+
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
436
|
+
) -> AssertionBuilder:
|
|
437
|
+
try:
|
|
438
|
+
attr(*args, **kwargs)
|
|
439
|
+
except (AssertionError, AssertionFailure):
|
|
440
|
+
return self._builder
|
|
441
|
+
raise AssertionError(self._make_msg(name))
|
|
442
|
+
|
|
443
|
+
def _negated_soft(
|
|
444
|
+
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
445
|
+
) -> AssertionBuilder:
|
|
446
|
+
err_list = _soft_err.get()
|
|
447
|
+
before = len(err_list)
|
|
448
|
+
attr(*args, **kwargs)
|
|
449
|
+
if len(err_list) > before:
|
|
450
|
+
del err_list[before:]
|
|
451
|
+
return self._builder
|
|
452
|
+
err_list.append(self._make_msg(name))
|
|
453
|
+
return self._builder
|
|
454
|
+
|
|
455
|
+
def _negated_warn(
|
|
456
|
+
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
457
|
+
) -> AssertionBuilder:
|
|
458
|
+
self._builder.kind = None
|
|
459
|
+
try:
|
|
460
|
+
attr(*args, **kwargs)
|
|
461
|
+
except (AssertionError, AssertionFailure):
|
|
462
|
+
return self._builder
|
|
463
|
+
finally:
|
|
464
|
+
self._builder.kind = "warn"
|
|
465
|
+
self._builder.logger.warning(self._make_msg(name))
|
|
466
|
+
return self._builder
|
|
467
|
+
|
|
468
|
+
|
|
442
469
|
class AssertionBuilder(
|
|
443
470
|
StringMixin,
|
|
444
471
|
SnapshotMixin,
|
|
@@ -477,6 +504,11 @@ class AssertionBuilder(
|
|
|
477
504
|
self.logger = logger if logger else _default_logger
|
|
478
505
|
self._not_expected = False
|
|
479
506
|
|
|
507
|
+
@property
|
|
508
|
+
def not_(self) -> NegatedBuilder:
|
|
509
|
+
"""Invert the next assertion in the chain."""
|
|
510
|
+
return NegatedBuilder(self)
|
|
511
|
+
|
|
480
512
|
def builder(self, val, description="", kind=None, expected=None, logger=None):
|
|
481
513
|
"""Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
|
|
482
514
|
|
|
@@ -1,31 +1,3 @@
|
|
|
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
1
|
from __future__ import annotations
|
|
30
2
|
|
|
31
3
|
import collections.abc
|
|
@@ -1,37 +1,11 @@
|
|
|
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
1
|
from __future__ import annotations
|
|
30
2
|
|
|
31
3
|
import collections
|
|
32
|
-
from
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
33
6
|
|
|
34
7
|
from ._mixin_base import _MixinBase
|
|
8
|
+
from .matchers import BaseMatcher
|
|
35
9
|
|
|
36
10
|
if TYPE_CHECKING:
|
|
37
11
|
from typing_extensions import Self
|
|
@@ -118,7 +92,6 @@ class CollectionMixin(_MixinBase):
|
|
|
118
92
|
|
|
119
93
|
missing = []
|
|
120
94
|
if hasattr(self.val, "keys") and callable(self.val.keys) and hasattr(self.val, "__getitem__"):
|
|
121
|
-
# flatten superset dicts
|
|
122
95
|
superdict = {}
|
|
123
96
|
for idx, j in enumerate(supersets):
|
|
124
97
|
self._check_dict_like(j, check_values=False, name=f"arg #{idx + 1}")
|
|
@@ -136,7 +109,6 @@ class CollectionMixin(_MixinBase):
|
|
|
136
109
|
f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
|
|
137
110
|
)
|
|
138
111
|
else:
|
|
139
|
-
# flatten supersets
|
|
140
112
|
superset = set()
|
|
141
113
|
for j in supersets:
|
|
142
114
|
try:
|
|
@@ -207,3 +179,151 @@ class CollectionMixin(_MixinBase):
|
|
|
207
179
|
prev = x
|
|
208
180
|
|
|
209
181
|
return self
|
|
182
|
+
|
|
183
|
+
def filtered_on(self, predicate: Callable[[Any], bool]) -> Self:
|
|
184
|
+
"""Returns a new builder with elements matching the predicate.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
predicate: callable or Matcher. If a Matcher, uses ``predicate.matches(item)``.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
Usage::
|
|
191
|
+
|
|
192
|
+
assert_that([1, -2, 3]).filtered_on(lambda x: x > 0).is_length(2)
|
|
193
|
+
assert_that(items).filtered_on(match.is_positive()).is_not_empty()
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
AssertionBuilder: returns a new instance with the filtered list as val
|
|
197
|
+
"""
|
|
198
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
199
|
+
raise TypeError("val is not iterable")
|
|
200
|
+
if isinstance(predicate, BaseMatcher):
|
|
201
|
+
filtered = [item for item in self.val if predicate.matches(item)]
|
|
202
|
+
else:
|
|
203
|
+
filtered = [item for item in self.val if predicate(item)]
|
|
204
|
+
return self.builder(filtered, self.description, self.kind)
|
|
205
|
+
|
|
206
|
+
def mapped(self, func: Callable[[Any], Any]) -> Self:
|
|
207
|
+
"""Returns a new builder with each element transformed by func.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
func: callable applied to each element.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
Usage::
|
|
214
|
+
|
|
215
|
+
assert_that(["a", "b"]).mapped(str.upper).contains("A")
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
AssertionBuilder: returns a new instance with the mapped list as val
|
|
219
|
+
"""
|
|
220
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
221
|
+
raise TypeError("val is not iterable")
|
|
222
|
+
return self.builder([func(item) for item in self.val], self.description, self.kind)
|
|
223
|
+
|
|
224
|
+
def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> Self:
|
|
225
|
+
"""Returns a new builder with each element expanded and flattened by func.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
func: callable returning an iterable for each element.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
Usage::
|
|
232
|
+
|
|
233
|
+
assert_that(["ab", "cd"]).flat_mapped(list).contains("a", "c")
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
AssertionBuilder: returns a new instance with the flattened list as val
|
|
237
|
+
"""
|
|
238
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
239
|
+
raise TypeError("val is not iterable")
|
|
240
|
+
return self.builder([x for item in self.val for x in func(item)], self.description, self.kind)
|
|
241
|
+
|
|
242
|
+
def first(self) -> Self:
|
|
243
|
+
"""Returns a new builder with the first element of val.
|
|
244
|
+
|
|
245
|
+
Examples:
|
|
246
|
+
Usage::
|
|
247
|
+
|
|
248
|
+
assert_that([10, 20, 30]).first().is_equal_to(10)
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
AssertionBuilder: returns a new instance with the first element as val
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: if val is empty
|
|
255
|
+
"""
|
|
256
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
257
|
+
raise TypeError("val is not iterable")
|
|
258
|
+
items = list(self.val)
|
|
259
|
+
if not items:
|
|
260
|
+
raise ValueError("Expected non-empty iterable, but was empty.")
|
|
261
|
+
return self.builder(items[0], self.description, self.kind)
|
|
262
|
+
|
|
263
|
+
def last(self) -> Self:
|
|
264
|
+
"""Returns a new builder with the last element of val.
|
|
265
|
+
|
|
266
|
+
Examples:
|
|
267
|
+
Usage::
|
|
268
|
+
|
|
269
|
+
assert_that([10, 20, 30]).last().is_equal_to(30)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
AssertionBuilder: returns a new instance with the last element as val
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: if val is empty
|
|
276
|
+
"""
|
|
277
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
278
|
+
raise TypeError("val is not iterable")
|
|
279
|
+
items = list(self.val)
|
|
280
|
+
if not items:
|
|
281
|
+
raise ValueError("Expected non-empty iterable, but was empty.")
|
|
282
|
+
return self.builder(items[-1], self.description, self.kind)
|
|
283
|
+
|
|
284
|
+
def element(self, index: int) -> Self:
|
|
285
|
+
"""Returns a new builder with the element at the given index.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
index: zero-based index.
|
|
289
|
+
|
|
290
|
+
Examples:
|
|
291
|
+
Usage::
|
|
292
|
+
|
|
293
|
+
assert_that([10, 20, 30]).element(1).is_equal_to(20)
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
AssertionBuilder: returns a new instance with the selected element as val
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
IndexError: if index is out of range
|
|
300
|
+
"""
|
|
301
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
302
|
+
raise TypeError("val is not iterable")
|
|
303
|
+
items = list(self.val)
|
|
304
|
+
if index < 0 or index >= len(items):
|
|
305
|
+
raise IndexError(f"Expected index {index} to be in range [0, {len(items)}), but was out of range.")
|
|
306
|
+
return self.builder(items[index], self.description, self.kind)
|
|
307
|
+
|
|
308
|
+
def single(self) -> Self:
|
|
309
|
+
"""Returns a new builder with the only element of val.
|
|
310
|
+
|
|
311
|
+
Examples:
|
|
312
|
+
Usage::
|
|
313
|
+
|
|
314
|
+
assert_that([42]).single().is_equal_to(42)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
AssertionBuilder: returns a new instance with the single element as val
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
ValueError: if val is empty or has more than one element
|
|
321
|
+
"""
|
|
322
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
323
|
+
raise TypeError("val is not iterable")
|
|
324
|
+
items = list(self.val)
|
|
325
|
+
if not items:
|
|
326
|
+
raise ValueError("Expected iterable with single element, but was empty.")
|
|
327
|
+
if len(items) > 1:
|
|
328
|
+
raise ValueError(f"Expected iterable with single element, but had {len(items)} elements.")
|
|
329
|
+
return self.builder(items[0], self.description, self.kind)
|