assertpy2 2.3.5__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.5 → assertpy2-2.3.6}/PKG-INFO +35 -17
  3. {assertpy2-2.3.5 → assertpy2-2.3.6}/README.md +34 -16
  4. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/__init__.py +2 -0
  5. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/_typing.py +12 -0
  6. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/assertpy.py +66 -31
  7. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/base.py +0 -28
  8. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/collection.py +151 -29
  9. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/contains.py +0 -29
  10. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/date.py +0 -28
  11. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/dict.py +0 -28
  12. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/dynamic.py +0 -28
  13. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/exception.py +0 -28
  14. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/extracting.py +0 -28
  15. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/file.py +0 -28
  16. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/helpers.py +0 -28
  17. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/numeric.py +0 -28
  18. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/snapshot.py +0 -28
  19. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/string.py +0 -28
  20. {assertpy2-2.3.5 → assertpy2-2.3.6}/docs/api.md +125 -0
  21. {assertpy2-2.3.5 → assertpy2-2.3.6}/pyproject.toml +1 -1
  22. assertpy2-2.3.6/tests/test_bool.py +35 -0
  23. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_class.py +0 -28
  24. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_collection.py +0 -29
  25. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_core.py +0 -28
  26. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_custom_dict.py +0 -28
  27. assertpy2-2.3.6/tests/test_custom_list.py +52 -0
  28. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_datetime.py +0 -28
  29. assertpy2-2.3.6/tests/test_description.py +35 -0
  30. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_dict.py +0 -28
  31. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_dict_compare.py +0 -28
  32. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_dyn.py +0 -28
  33. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_equals.py +0 -28
  34. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_expected_exception.py +0 -28
  35. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_extensions.py +0 -28
  36. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_extracting.py +0 -28
  37. assertpy2-2.3.6/tests/test_fail.py +17 -0
  38. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_file.py +0 -28
  39. assertpy2-2.3.6/tests/test_in.py +47 -0
  40. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_list.py +0 -28
  41. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_namedtuple.py +0 -28
  42. assertpy2-2.3.6/tests/test_none.py +29 -0
  43. assertpy2-2.3.6/tests/test_not.py +150 -0
  44. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_numbers.py +0 -28
  45. assertpy2-2.3.6/tests/test_pipeline.py +173 -0
  46. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_readme.py +0 -28
  47. assertpy2-2.3.6/tests/test_same_as.py +44 -0
  48. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_snapshots.py +0 -28
  49. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_soft.py +0 -28
  50. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_soft_fail.py +0 -28
  51. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_string.py +0 -29
  52. assertpy2-2.3.6/tests/test_traceback.py +36 -0
  53. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_type.py +0 -28
  54. assertpy2-2.3.6/tests/test_warn.py +108 -0
  55. {assertpy2-2.3.5 → assertpy2-2.3.6}/uv.lock +1 -1
  56. assertpy2-2.3.5/CONTRIBUTING.md +0 -22
  57. assertpy2-2.3.5/tests/test_bool.py +0 -63
  58. assertpy2-2.3.5/tests/test_custom_list.py +0 -80
  59. assertpy2-2.3.5/tests/test_description.py +0 -63
  60. assertpy2-2.3.5/tests/test_fail.py +0 -45
  61. assertpy2-2.3.5/tests/test_in.py +0 -75
  62. assertpy2-2.3.5/tests/test_none.py +0 -57
  63. assertpy2-2.3.5/tests/test_same_as.py +0 -72
  64. assertpy2-2.3.5/tests/test_traceback.py +0 -64
  65. assertpy2-2.3.5/tests/test_warn.py +0 -136
  66. {assertpy2-2.3.5 → assertpy2-2.3.6}/.codecov.yml +0 -0
  67. {assertpy2-2.3.5 → assertpy2-2.3.6}/.github/dependabot.yml +0 -0
  68. {assertpy2-2.3.5 → assertpy2-2.3.6}/.github/workflows/ci.yml +0 -0
  69. {assertpy2-2.3.5 → assertpy2-2.3.6}/.github/workflows/codeql.yml +0 -0
  70. {assertpy2-2.3.5 → assertpy2-2.3.6}/.github/workflows/publish.yml +0 -0
  71. {assertpy2-2.3.5 → assertpy2-2.3.6}/.github/workflows/scorecard.yml +0 -0
  72. {assertpy2-2.3.5 → assertpy2-2.3.6}/.gitignore +0 -0
  73. {assertpy2-2.3.5 → assertpy2-2.3.6}/LICENSE +0 -0
  74. {assertpy2-2.3.5 → assertpy2-2.3.6}/SECURITY.md +0 -0
  75. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/_mixin_base.py +0 -0
  76. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/async_assertions.py +0 -0
  77. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/behave_matchers.py +0 -0
  78. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/errors.py +0 -0
  79. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/matchers.py +0 -0
  80. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/py.typed +0 -0
  81. {assertpy2-2.3.5 → assertpy2-2.3.6}/assertpy2/pytest_plugin.py +0 -0
  82. {assertpy2-2.3.5 → assertpy2-2.3.6}/docs/logo-dark.svg +0 -0
  83. {assertpy2-2.3.5 → assertpy2-2.3.6}/docs/logo.svg +0 -0
  84. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_async.py +0 -0
  85. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_behave_matchers.py +0 -0
  86. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_errors.py +0 -0
  87. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_matcher_registry.py +0 -0
  88. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_matchers.py +0 -0
  89. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_matchers_phase3.py +0 -0
  90. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_overloads.py +0 -0
  91. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_phase2.py +0 -0
  92. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_pytest_plugin.py +0 -0
  93. {assertpy2-2.3.5 → assertpy2-2.3.6}/tests/test_regex_groups.py +0 -0
  94. {assertpy2-2.3.5 → 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.5
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
@@ -162,6 +162,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
162
162
 
163
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.
@@ -318,6 +320,38 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
318
320
 
319
321
  ## More features
320
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
+
321
355
  ### Dict comparison with ignore/include
322
356
 
323
357
  ```py
@@ -492,19 +526,3 @@ See the [comparison table](#comparison) above for feature differences with other
492
526
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
493
527
 
494
528
 
495
- ## License
496
-
497
- All files are licensed under the BSD 3-Clause License as follows:
498
-
499
- > Copyright (c) 2015-2019, Activision Publishing, Inc.
500
- > All rights reserved.
501
- >
502
- > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
503
- >
504
- > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
505
- >
506
- > 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.
507
- >
508
- > 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.
509
- >
510
- > 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.
@@ -127,6 +127,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
127
127
 
128
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.
@@ -283,6 +285,38 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
283
285
 
284
286
  ## More features
285
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
+
286
320
  ### Dict comparison with ignore/include
287
321
 
288
322
  ```py
@@ -457,19 +491,3 @@ See the [comparison table](#comparison) above for feature differences with other
457
491
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
458
492
 
459
493
 
460
- ## License
461
-
462
- All files are licensed under the BSD 3-Clause License as follows:
463
-
464
- > Copyright (c) 2015-2019, Activision Publishing, Inc.
465
- > All rights reserved.
466
- >
467
- > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
468
- >
469
- > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
470
- >
471
- > 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.
472
- >
473
- > 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.
474
- >
475
- > 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.5"
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
@@ -436,6 +407,65 @@ _logger.addHandler(_handler)
436
407
  _default_logger = WarningLoggingAdapter(_logger, None)
437
408
 
438
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
+
439
469
  class AssertionBuilder(
440
470
  StringMixin,
441
471
  SnapshotMixin,
@@ -474,6 +504,11 @@ class AssertionBuilder(
474
504
  self.logger = logger if logger else _default_logger
475
505
  self._not_expected = False
476
506
 
507
+ @property
508
+ def not_(self) -> NegatedBuilder:
509
+ """Invert the next assertion in the chain."""
510
+ return NegatedBuilder(self)
511
+
477
512
  def builder(self, val, description="", kind=None, expected=None, logger=None):
478
513
  """Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
479
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
@@ -205,3 +179,151 @@ class CollectionMixin(_MixinBase):
205
179
  prev = x
206
180
 
207
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)
@@ -1,32 +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
-
30
1
  from __future__ import annotations
31
2
 
32
3
  from typing import TYPE_CHECKING
@@ -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 datetime