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.
Files changed (94) hide show
  1. assertpy2-2.3.6/CONTRIBUTING.md +50 -0
  2. {assertpy2-2.3.4 → assertpy2-2.3.6}/PKG-INFO +45 -18
  3. {assertpy2-2.3.4 → assertpy2-2.3.6}/README.md +44 -17
  4. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/__init__.py +2 -0
  5. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/_typing.py +12 -0
  6. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/assertpy.py +66 -34
  7. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/base.py +0 -28
  8. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/collection.py +151 -31
  9. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/contains.py +0 -29
  10. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/date.py +0 -28
  11. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/dict.py +0 -28
  12. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/dynamic.py +0 -28
  13. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/exception.py +0 -29
  14. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/extracting.py +0 -28
  15. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/file.py +0 -28
  16. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/helpers.py +0 -34
  17. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/matchers.py +6 -6
  18. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/numeric.py +0 -28
  19. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/snapshot.py +0 -28
  20. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/string.py +0 -28
  21. {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/api.md +157 -0
  22. {assertpy2-2.3.4 → assertpy2-2.3.6}/pyproject.toml +2 -2
  23. assertpy2-2.3.6/tests/test_bool.py +35 -0
  24. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_class.py +0 -28
  25. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_collection.py +0 -29
  26. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_core.py +0 -28
  27. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_custom_dict.py +0 -28
  28. assertpy2-2.3.6/tests/test_custom_list.py +52 -0
  29. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_datetime.py +0 -28
  30. assertpy2-2.3.6/tests/test_description.py +35 -0
  31. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dict.py +0 -28
  32. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dict_compare.py +0 -28
  33. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_dyn.py +0 -28
  34. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_equals.py +0 -28
  35. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_expected_exception.py +0 -28
  36. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_extensions.py +0 -28
  37. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_extracting.py +0 -28
  38. assertpy2-2.3.6/tests/test_fail.py +17 -0
  39. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_file.py +0 -28
  40. assertpy2-2.3.6/tests/test_in.py +47 -0
  41. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_list.py +0 -28
  42. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matchers.py +67 -0
  43. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_namedtuple.py +0 -28
  44. assertpy2-2.3.6/tests/test_none.py +29 -0
  45. assertpy2-2.3.6/tests/test_not.py +150 -0
  46. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_numbers.py +0 -28
  47. assertpy2-2.3.6/tests/test_pipeline.py +173 -0
  48. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_readme.py +0 -28
  49. assertpy2-2.3.6/tests/test_same_as.py +44 -0
  50. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_snapshots.py +0 -28
  51. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_soft.py +0 -28
  52. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_soft_fail.py +0 -28
  53. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_string.py +0 -29
  54. assertpy2-2.3.6/tests/test_traceback.py +36 -0
  55. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_type.py +0 -28
  56. assertpy2-2.3.6/tests/test_warn.py +108 -0
  57. {assertpy2-2.3.4 → assertpy2-2.3.6}/uv.lock +1 -1
  58. assertpy2-2.3.4/CONTRIBUTING.md +0 -22
  59. assertpy2-2.3.4/tests/test_bool.py +0 -63
  60. assertpy2-2.3.4/tests/test_custom_list.py +0 -80
  61. assertpy2-2.3.4/tests/test_description.py +0 -63
  62. assertpy2-2.3.4/tests/test_fail.py +0 -45
  63. assertpy2-2.3.4/tests/test_in.py +0 -75
  64. assertpy2-2.3.4/tests/test_none.py +0 -57
  65. assertpy2-2.3.4/tests/test_same_as.py +0 -72
  66. assertpy2-2.3.4/tests/test_traceback.py +0 -64
  67. assertpy2-2.3.4/tests/test_warn.py +0 -136
  68. {assertpy2-2.3.4 → assertpy2-2.3.6}/.codecov.yml +0 -0
  69. {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/dependabot.yml +0 -0
  70. {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/ci.yml +0 -0
  71. {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/codeql.yml +0 -0
  72. {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/publish.yml +0 -0
  73. {assertpy2-2.3.4 → assertpy2-2.3.6}/.github/workflows/scorecard.yml +0 -0
  74. {assertpy2-2.3.4 → assertpy2-2.3.6}/.gitignore +0 -0
  75. {assertpy2-2.3.4 → assertpy2-2.3.6}/LICENSE +0 -0
  76. {assertpy2-2.3.4 → assertpy2-2.3.6}/SECURITY.md +0 -0
  77. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/_mixin_base.py +0 -0
  78. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/async_assertions.py +0 -0
  79. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/behave_matchers.py +0 -0
  80. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/errors.py +0 -0
  81. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/py.typed +0 -0
  82. {assertpy2-2.3.4 → assertpy2-2.3.6}/assertpy2/pytest_plugin.py +0 -0
  83. {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/logo-dark.svg +0 -0
  84. {assertpy2-2.3.4 → assertpy2-2.3.6}/docs/logo.svg +0 -0
  85. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_async.py +0 -0
  86. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_behave_matchers.py +0 -0
  87. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_errors.py +0 -0
  88. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matcher_registry.py +0 -0
  89. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_matchers_phase3.py +0 -0
  90. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_overloads.py +0 -0
  91. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_phase2.py +0 -0
  92. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_pytest_plugin.py +0 -0
  93. {assertpy2-2.3.4 → assertpy2-2.3.6}/tests/test_regex_groups.py +0 -0
  94. {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.4
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 `&`, `|`, `~` operators.
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 `&`, `|`, `~` operators.
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.4"
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 typing import TYPE_CHECKING
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)