assertpy2 2.3.5__tar.gz → 2.3.7__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 (98) hide show
  1. assertpy2-2.3.7/CONTRIBUTING.md +50 -0
  2. {assertpy2-2.3.5 → assertpy2-2.3.7}/PKG-INFO +72 -20
  3. {assertpy2-2.3.5 → assertpy2-2.3.7}/README.md +67 -18
  4. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/__init__.py +6 -0
  5. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/_typing.py +26 -1
  6. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/assertpy.py +147 -37
  7. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/base.py +0 -28
  8. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/collection.py +151 -29
  9. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/contains.py +0 -29
  10. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/date.py +0 -28
  11. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/dict.py +0 -28
  12. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/dynamic.py +0 -28
  13. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/exception.py +0 -28
  14. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/extracting.py +0 -28
  15. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/file.py +0 -28
  16. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/helpers.py +0 -28
  17. assertpy2-2.3.7/assertpy2/json_mixin.py +163 -0
  18. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/numeric.py +0 -28
  19. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/snapshot.py +0 -28
  20. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/string.py +0 -28
  21. {assertpy2-2.3.5 → assertpy2-2.3.7}/docs/api.md +237 -0
  22. {assertpy2-2.3.5 → assertpy2-2.3.7}/pyproject.toml +4 -3
  23. assertpy2-2.3.7/tests/test_bool.py +35 -0
  24. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_class.py +0 -28
  25. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_collection.py +0 -29
  26. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_core.py +0 -28
  27. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_custom_dict.py +0 -28
  28. assertpy2-2.3.7/tests/test_custom_list.py +52 -0
  29. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_datetime.py +0 -28
  30. assertpy2-2.3.7/tests/test_description.py +35 -0
  31. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_dict.py +0 -28
  32. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_dict_compare.py +0 -28
  33. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_dyn.py +0 -28
  34. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_equals.py +0 -28
  35. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_expected_exception.py +0 -28
  36. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_extensions.py +0 -28
  37. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_extracting.py +0 -28
  38. assertpy2-2.3.7/tests/test_fail.py +17 -0
  39. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_file.py +0 -28
  40. assertpy2-2.3.7/tests/test_grouped_soft.py +130 -0
  41. assertpy2-2.3.7/tests/test_in.py +47 -0
  42. assertpy2-2.3.7/tests/test_json.py +169 -0
  43. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_list.py +0 -28
  44. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_namedtuple.py +0 -28
  45. assertpy2-2.3.7/tests/test_none.py +29 -0
  46. assertpy2-2.3.7/tests/test_not.py +150 -0
  47. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_numbers.py +0 -28
  48. assertpy2-2.3.7/tests/test_pipeline.py +173 -0
  49. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_readme.py +0 -28
  50. assertpy2-2.3.7/tests/test_same_as.py +44 -0
  51. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_snapshots.py +0 -28
  52. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_soft.py +0 -28
  53. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_soft_fail.py +0 -28
  54. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_string.py +0 -29
  55. assertpy2-2.3.7/tests/test_traceback.py +36 -0
  56. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_type.py +0 -28
  57. assertpy2-2.3.7/tests/test_warn.py +108 -0
  58. assertpy2-2.3.7/uv.lock +792 -0
  59. assertpy2-2.3.5/CONTRIBUTING.md +0 -22
  60. assertpy2-2.3.5/tests/test_bool.py +0 -63
  61. assertpy2-2.3.5/tests/test_custom_list.py +0 -80
  62. assertpy2-2.3.5/tests/test_description.py +0 -63
  63. assertpy2-2.3.5/tests/test_fail.py +0 -45
  64. assertpy2-2.3.5/tests/test_in.py +0 -75
  65. assertpy2-2.3.5/tests/test_none.py +0 -57
  66. assertpy2-2.3.5/tests/test_same_as.py +0 -72
  67. assertpy2-2.3.5/tests/test_traceback.py +0 -64
  68. assertpy2-2.3.5/tests/test_warn.py +0 -136
  69. assertpy2-2.3.5/uv.lock +0 -465
  70. {assertpy2-2.3.5 → assertpy2-2.3.7}/.codecov.yml +0 -0
  71. {assertpy2-2.3.5 → assertpy2-2.3.7}/.github/dependabot.yml +0 -0
  72. {assertpy2-2.3.5 → assertpy2-2.3.7}/.github/workflows/ci.yml +0 -0
  73. {assertpy2-2.3.5 → assertpy2-2.3.7}/.github/workflows/codeql.yml +0 -0
  74. {assertpy2-2.3.5 → assertpy2-2.3.7}/.github/workflows/publish.yml +0 -0
  75. {assertpy2-2.3.5 → assertpy2-2.3.7}/.github/workflows/scorecard.yml +0 -0
  76. {assertpy2-2.3.5 → assertpy2-2.3.7}/.gitignore +0 -0
  77. {assertpy2-2.3.5 → assertpy2-2.3.7}/LICENSE +0 -0
  78. {assertpy2-2.3.5 → assertpy2-2.3.7}/SECURITY.md +0 -0
  79. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/_mixin_base.py +0 -0
  80. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/async_assertions.py +0 -0
  81. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/behave_matchers.py +0 -0
  82. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/errors.py +0 -0
  83. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/matchers.py +0 -0
  84. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/py.typed +0 -0
  85. {assertpy2-2.3.5 → assertpy2-2.3.7}/assertpy2/pytest_plugin.py +0 -0
  86. {assertpy2-2.3.5 → assertpy2-2.3.7}/docs/logo-dark.svg +0 -0
  87. {assertpy2-2.3.5 → assertpy2-2.3.7}/docs/logo.svg +0 -0
  88. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_async.py +0 -0
  89. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_behave_matchers.py +0 -0
  90. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_errors.py +0 -0
  91. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_matcher_registry.py +0 -0
  92. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_matchers.py +0 -0
  93. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_matchers_phase3.py +0 -0
  94. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_overloads.py +0 -0
  95. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_phase2.py +0 -0
  96. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_pytest_plugin.py +0 -0
  97. {assertpy2-2.3.5 → assertpy2-2.3.7}/tests/test_regex_groups.py +0 -0
  98. {assertpy2-2.3.5 → assertpy2-2.3.7}/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,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.3.5
3
+ Version: 2.3.7
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
7
7
  Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
8
8
  Author-email: Justin Shacklette <justin@saturnboy.com>
9
- Maintainer: Solganis
9
+ Maintainer-email: Solganis <solganis.dev@gmail.com>
10
10
  License-Expression: BSD-3-Clause
11
11
  License-File: LICENSE
12
12
  Keywords: assert,assert_that,assertion,assertthat,pytest,test,testing,unittest
@@ -31,6 +31,9 @@ Provides-Extra: allure
31
31
  Requires-Dist: allure-pytest>=2.13; extra == 'allure'
32
32
  Provides-Extra: behave
33
33
  Requires-Dist: behave>=1.2.6; extra == 'behave'
34
+ Provides-Extra: json
35
+ Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
36
+ Requires-Dist: jsonschema>=4.0; extra == 'json'
34
37
  Description-Content-Type: text/markdown
35
38
 
36
39
  <p align="center">
@@ -43,7 +46,7 @@ Description-Content-Type: text/markdown
43
46
 
44
47
  <p align="center">
45
48
  <b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
46
- Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
49
+ A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
47
50
  </p>
48
51
 
49
52
  <p align="center">
@@ -162,11 +165,14 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
162
165
 
163
166
  - [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
164
167
  - [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
168
+ - [**Universal negation**](docs/api.md#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
169
+ - [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()` for transforming collections before assertions.
165
170
  - [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for async and eventual consistency testing.
166
171
  - [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
167
172
  - **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
168
173
  - **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
169
- - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`.
174
+ - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`. Group errors with `sa.group()`, or use `assert_all()` for inline checks.
175
+ - [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation (`at_json_path`, `has_json_path`) and JSON Schema validation (`matches_json_schema`).
170
176
  - [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
171
177
  - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
172
178
  - [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key matching.
@@ -318,6 +324,68 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
318
324
 
319
325
  ## More features
320
326
 
327
+ ### Universal negation
328
+
329
+ Invert any assertion with `.not_`:
330
+
331
+ ```py
332
+ assert_that(5).not_.is_none()
333
+ assert_that("abc123").not_.is_alpha()
334
+ assert_that([3, 1, 2]).not_.is_sorted()
335
+ assert_that(value).described_as("check").not_.is_none().is_positive()
336
+ ```
337
+
338
+ Works with soft assertions and warn mode.
339
+
340
+ ### Collection pipeline
341
+
342
+ Transform collections before asserting:
343
+
344
+ ```py
345
+ orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
346
+
347
+ assert_that(orders).filtered_on(lambda o: o.status == "FAILED").is_length(1)
348
+ assert_that(orders).mapped(lambda o: o.total).contains(100, 200)
349
+ assert_that(orders).first().has_status("DONE")
350
+ assert_that(orders).element(1).has_status("FAILED")
351
+ assert_that([42]).single().is_equal_to(42)
352
+
353
+ # chaining pipeline steps
354
+ assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
355
+ ```
356
+
357
+ Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
358
+
359
+ ### Grouped soft assertions
360
+
361
+ ```py
362
+ with soft_assertions() as sa:
363
+ with sa.group("Headers"):
364
+ assert_that(headers["Content-Type"]).is_equal_to("application/json")
365
+ with sa.group("Body"):
366
+ assert_that(body["status"]).is_equal_to("ok")
367
+ assert_that(body["items"]).is_not_empty()
368
+
369
+ # or inline with assert_all
370
+ assert_all(
371
+ lambda: assert_that(x).is_positive(),
372
+ lambda: assert_that(y).is_not_none(),
373
+ )
374
+ ```
375
+
376
+ ### JSON path and schema validation
377
+
378
+ Requires `pip install assertpy2[json]`.
379
+
380
+ ```py
381
+ data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}
382
+
383
+ assert_that(data).at_json_path("$.users[0].name").is_equal_to("Alice")
384
+ assert_that(data).has_json_path("$.meta.total")
385
+ assert_that(data).does_not_have_json_path("$.error")
386
+ assert_that(data).matches_json_schema({"type": "object", "required": ["users"]})
387
+ ```
388
+
321
389
  ### Dict comparison with ignore/include
322
390
 
323
391
  ```py
@@ -492,19 +560,3 @@ See the [comparison table](#comparison) above for feature differences with other
492
560
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
493
561
 
494
562
 
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.
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center">
10
10
  <b>Fluent assertion library for Python with composable matchers, structural matching, and full type safety.</b><br>
11
- Maintained fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
11
+ A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>.
12
12
  </p>
13
13
 
14
14
  <p align="center">
@@ -127,11 +127,14 @@ 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.
133
135
  - **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
134
- - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`.
136
+ - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`. Group errors with `sa.group()`, or use `assert_all()` for inline checks.
137
+ - [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation (`at_json_path`, `has_json_path`) and JSON Schema validation (`matches_json_schema`).
135
138
  - [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
136
139
  - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
137
140
  - [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key matching.
@@ -283,6 +286,68 @@ DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
283
286
 
284
287
  ## More features
285
288
 
289
+ ### Universal negation
290
+
291
+ Invert any assertion with `.not_`:
292
+
293
+ ```py
294
+ assert_that(5).not_.is_none()
295
+ assert_that("abc123").not_.is_alpha()
296
+ assert_that([3, 1, 2]).not_.is_sorted()
297
+ assert_that(value).described_as("check").not_.is_none().is_positive()
298
+ ```
299
+
300
+ Works with soft assertions and warn mode.
301
+
302
+ ### Collection pipeline
303
+
304
+ Transform collections before asserting:
305
+
306
+ ```py
307
+ orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
308
+
309
+ assert_that(orders).filtered_on(lambda o: o.status == "FAILED").is_length(1)
310
+ assert_that(orders).mapped(lambda o: o.total).contains(100, 200)
311
+ assert_that(orders).first().has_status("DONE")
312
+ assert_that(orders).element(1).has_status("FAILED")
313
+ assert_that([42]).single().is_equal_to(42)
314
+
315
+ # chaining pipeline steps
316
+ assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
317
+ ```
318
+
319
+ Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
320
+
321
+ ### Grouped soft assertions
322
+
323
+ ```py
324
+ with soft_assertions() as sa:
325
+ with sa.group("Headers"):
326
+ assert_that(headers["Content-Type"]).is_equal_to("application/json")
327
+ with sa.group("Body"):
328
+ assert_that(body["status"]).is_equal_to("ok")
329
+ assert_that(body["items"]).is_not_empty()
330
+
331
+ # or inline with assert_all
332
+ assert_all(
333
+ lambda: assert_that(x).is_positive(),
334
+ lambda: assert_that(y).is_not_none(),
335
+ )
336
+ ```
337
+
338
+ ### JSON path and schema validation
339
+
340
+ Requires `pip install assertpy2[json]`.
341
+
342
+ ```py
343
+ data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}
344
+
345
+ assert_that(data).at_json_path("$.users[0].name").is_equal_to("Alice")
346
+ assert_that(data).has_json_path("$.meta.total")
347
+ assert_that(data).does_not_have_json_path("$.error")
348
+ assert_that(data).matches_json_schema({"type": "object", "required": ["users"]})
349
+ ```
350
+
286
351
  ### Dict comparison with ignore/include
287
352
 
288
353
  ```py
@@ -457,19 +522,3 @@ See the [comparison table](#comparison) above for feature differences with other
457
522
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
458
523
 
459
524
 
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,7 +1,10 @@
1
1
  from .assertpy import (
2
+ NegatedBuilder,
3
+ SoftAssertionCollector,
2
4
  WarningLoggingAdapter,
3
5
  __version__,
4
6
  add_extension,
7
+ assert_all,
5
8
  assert_that,
6
9
  assert_warn,
7
10
  fail,
@@ -20,9 +23,12 @@ __all__ = [
20
23
  "DiffEntry",
21
24
  "DiffResult",
22
25
  "Matcher",
26
+ "NegatedBuilder",
27
+ "SoftAssertionCollector",
23
28
  "WarningLoggingAdapter",
24
29
  "__version__",
25
30
  "add_extension",
31
+ "assert_all",
26
32
  "assert_that",
27
33
  "assert_warn",
28
34
  "clear_custom_matchers",
@@ -4,10 +4,12 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from collections.abc import Callable
7
- from typing import Protocol
7
+ from pathlib import Path
8
+ from typing import Any, Protocol
8
9
 
9
10
  from typing_extensions import Self
10
11
 
12
+ from .assertpy import NegatedBuilder
11
13
  from .async_assertions import AsyncAssertionBuilder
12
14
  from .matchers import Matcher
13
15
 
@@ -35,6 +37,9 @@ if TYPE_CHECKING:
35
37
  def is_not_in(self, *items: object) -> Self: ...
36
38
  # SnapshotMixin
37
39
  def snapshot(self, id: str | None = ..., path: str = ...) -> Self: ...
40
+ # NegatedBuilder
41
+ @property
42
+ def not_(self) -> NegatedBuilder: ...
38
43
 
39
44
  class _StringAssertion(_CoreAssertion, Protocol):
40
45
  """Assertions available for ``str`` values."""
@@ -129,6 +134,20 @@ if TYPE_CHECKING:
129
134
  def any_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
130
135
  def all_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
131
136
  def none_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
137
+ # CollectionMixin - pipeline
138
+ def filtered_on(self, predicate: Callable[..., bool]) -> Self: ...
139
+ def mapped(self, func: Callable[..., object]) -> Self: ...
140
+ def flat_mapped(self, func: Callable[..., object]) -> Self: ...
141
+ def first(self) -> Self: ...
142
+ def last(self) -> Self: ...
143
+ def element(self, index: int) -> Self: ...
144
+ def single(self) -> Self: ...
145
+ # JsonMixin
146
+ def at_json_path(self, path: str) -> Self: ...
147
+ def has_json_path(self, path: str) -> Self: ...
148
+ def does_not_have_json_path(self, path: str) -> Self: ...
149
+ def matches_json_schema(self, schema: dict[str, Any]) -> Self: ...
150
+ def matches_json_schema_from_file(self, path: str | Path) -> Self: ...
132
151
 
133
152
  class _DictAssertion(_CoreAssertion, Protocol):
134
153
  """Assertions available for ``dict`` values."""
@@ -150,6 +169,12 @@ if TYPE_CHECKING:
150
169
  def each(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
151
170
  # ExtractingMixin
152
171
  def extracting(self, *names: object, **kwargs: object) -> Self: ...
172
+ # JsonMixin
173
+ def at_json_path(self, path: str) -> Self: ...
174
+ def has_json_path(self, path: str) -> Self: ...
175
+ def does_not_have_json_path(self, path: str) -> Self: ...
176
+ def matches_json_schema(self, schema: dict[str, Any]) -> Self: ...
177
+ def matches_json_schema_from_file(self, path: str | Path) -> Self: ...
153
178
 
154
179
  class _DateAssertion(_CoreAssertion, Protocol):
155
180
  """Assertions available for ``datetime.date`` and ``datetime.datetime`` 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
 
@@ -69,11 +40,12 @@ from .exception import ExceptionMixin
69
40
  from .extracting import ExtractingMixin
70
41
  from .file import FileMixin
71
42
  from .helpers import HelpersMixin
43
+ from .json_mixin import JsonMixin
72
44
  from .numeric import NumericMixin
73
45
  from .snapshot import SnapshotMixin
74
46
  from .string import StringMixin
75
47
 
76
- __version__ = "2.3.5"
48
+ __version__ = "2.3.7"
77
49
 
78
50
  __tracebackhide__ = True # clean tracebacks via py.test integration
79
51
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
@@ -95,6 +67,7 @@ ASSERTPY_FILES = [
95
67
  "extracting.py",
96
68
  "file.py",
97
69
  "helpers.py",
70
+ "json_mixin.py",
98
71
  "matchers.py",
99
72
  "numeric.py",
100
73
  "pytest_plugin.py",
@@ -105,11 +78,52 @@ ASSERTPY_FILES = [
105
78
 
106
79
  # soft assertions (contextvars for thread/async safety)
107
80
  _soft_ctx: contextvars.ContextVar[int] = contextvars.ContextVar("assertpy2_soft_ctx", default=0)
108
- _soft_err: contextvars.ContextVar[list[str]] = contextvars.ContextVar("assertpy2_soft_err")
81
+ _soft_err: contextvars.ContextVar[list[tuple[str | None, str]]] = contextvars.ContextVar("assertpy2_soft_err")
82
+ _soft_group: contextvars.ContextVar[str | None] = contextvars.ContextVar("assertpy2_soft_group", default=None)
83
+
84
+
85
+ class SoftAssertionCollector:
86
+ """Collector returned by :func:`soft_assertions` for grouping errors hierarchically."""
87
+
88
+ @contextlib.contextmanager
89
+ def group(self, label: str) -> Iterator[None]:
90
+ """Group subsequent assertion failures under *label*.
91
+
92
+ Examples:
93
+ Usage::
94
+
95
+ with soft_assertions() as sa:
96
+ with sa.group("Headers"):
97
+ assert_that(headers).contains_key("Content-Type")
98
+ with sa.group("Body"):
99
+ assert_that(body["status"]).is_equal_to("ok")
100
+ """
101
+ token = _soft_group.set(label)
102
+ try:
103
+ yield
104
+ finally:
105
+ _soft_group.reset(token)
106
+
107
+
108
+ def _format_soft_errors(errs: list[tuple[str | None, str]]) -> str:
109
+ has_groups = any(group is not None for group, _ in errs)
110
+ if not has_groups:
111
+ return "soft assertion failures:\n" + "\n".join(f"{i + 1}. {msg}" for i, (_, msg) in enumerate(errs))
112
+
113
+ lines = ["soft assertion failures:"]
114
+ current_group: str | None = object() # ty: ignore[invalid-assignment] # sentinel
115
+ for counter, (group, msg) in enumerate(errs, 1):
116
+ if group != current_group:
117
+ current_group = group
118
+ if group is not None:
119
+ lines.append(f" [{group}]")
120
+ indent = " " if group is not None else " "
121
+ lines.append(f"{indent}{counter}. {msg}")
122
+ return "\n".join(lines)
109
123
 
110
124
 
111
125
  @contextlib.contextmanager
112
- def soft_assertions() -> Iterator[None]:
126
+ def soft_assertions() -> Iterator[SoftAssertionCollector]:
113
127
  """Create a soft assertion context.
114
128
 
115
129
  Normally, any assertion failure will halt test execution immediately by raising an error.
@@ -141,6 +155,14 @@ def soft_assertions() -> Iterator[None]:
141
155
  4. Expected <foo> to contain only digits, but did not.
142
156
  5. Expected <123> to contain only alphabetic chars, but did not.
143
157
 
158
+ Group errors by section::
159
+
160
+ with soft_assertions() as sa:
161
+ with sa.group("Headers"):
162
+ assert_that(headers["Content-Type"]).is_equal_to("application/json")
163
+ with sa.group("Body"):
164
+ assert_that(body["status"]).is_equal_to("ok")
165
+
144
166
  Note:
145
167
  The soft assertion context only collects *assertion* failures, other errors such as
146
168
  ``TypeError`` or ``ValueError`` are always raised immediately. Triggering an explicit test
@@ -154,17 +176,40 @@ def soft_assertions() -> Iterator[None]:
154
176
  _soft_ctx.set(ctx + 1)
155
177
 
156
178
  try:
157
- yield
179
+ yield SoftAssertionCollector()
158
180
  finally:
159
181
  _soft_ctx.set(_soft_ctx.get() - 1)
160
182
 
161
183
  errs = _soft_err.get([])
162
184
  if errs and _soft_ctx.get() == 0:
163
- out = "soft assertion failures:\n" + "\n".join(f"{i + 1}. {msg}" for i, msg in enumerate(errs))
185
+ out = _format_soft_errors(errs)
164
186
  _soft_err.set([])
165
187
  raise AssertionError(out)
166
188
 
167
189
 
190
+ def assert_all(*callables: Callable[[], object]) -> None:
191
+ """Run all callables inside a soft assertion context.
192
+
193
+ A convenience wrapper around :func:`soft_assertions` for inline use.
194
+
195
+ Examples:
196
+ Usage::
197
+
198
+ from assertpy2 import assert_all, assert_that
199
+
200
+ assert_all(
201
+ lambda: assert_that(x).is_positive(),
202
+ lambda: assert_that(y).is_not_none(),
203
+ )
204
+
205
+ Raises:
206
+ AssertionError: if any of the callables produce assertion failures
207
+ """
208
+ with soft_assertions():
209
+ for fn in callables:
210
+ fn()
211
+
212
+
168
213
  # factory methods
169
214
 
170
215
 
@@ -332,7 +377,7 @@ def soft_fail(msg=""):
332
377
 
333
378
  """
334
379
  if _soft_ctx.get():
335
- _soft_err.get().append(f"Fail: {msg}!" if msg else "Fail!")
380
+ _soft_err.get().append((_soft_group.get(), f"Fail: {msg}!" if msg else "Fail!"))
336
381
  return
337
382
  fail(msg)
338
383
 
@@ -436,10 +481,70 @@ _logger.addHandler(_handler)
436
481
  _default_logger = WarningLoggingAdapter(_logger, None)
437
482
 
438
483
 
484
+ class NegatedBuilder:
485
+ """Proxy that inverts the next assertion. Created by ``assert_that(val).not_``."""
486
+
487
+ def __init__(self, builder: AssertionBuilder) -> None:
488
+ self._builder = builder
489
+
490
+ def __getattr__(self, name: str) -> object:
491
+ attr = getattr(self._builder, name)
492
+ if not callable(attr):
493
+ return attr
494
+
495
+ def _negated(*args: object, **kwargs: object) -> AssertionBuilder:
496
+ if self._builder.kind == "soft":
497
+ return self._negated_soft(name, attr, *args, **kwargs)
498
+ if self._builder.kind == "warn":
499
+ return self._negated_warn(name, attr, *args, **kwargs)
500
+ return self._negated_strict(name, attr, *args, **kwargs)
501
+
502
+ return _negated
503
+
504
+ def _make_msg(self, name: str) -> str:
505
+ desc = f"[{self._builder.description}] " if self._builder.description else ""
506
+ return f"{desc}Expected <{self._builder.val}> to NOT satisfy: {name}()"
507
+
508
+ def _negated_strict(
509
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
510
+ ) -> AssertionBuilder:
511
+ try:
512
+ attr(*args, **kwargs)
513
+ except (AssertionError, AssertionFailure):
514
+ return self._builder
515
+ raise AssertionError(self._make_msg(name))
516
+
517
+ def _negated_soft(
518
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
519
+ ) -> AssertionBuilder:
520
+ err_list = _soft_err.get()
521
+ before = len(err_list)
522
+ attr(*args, **kwargs)
523
+ if len(err_list) > before:
524
+ del err_list[before:]
525
+ return self._builder
526
+ err_list.append((_soft_group.get(), self._make_msg(name)))
527
+ return self._builder
528
+
529
+ def _negated_warn(
530
+ self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
531
+ ) -> AssertionBuilder:
532
+ self._builder.kind = None
533
+ try:
534
+ attr(*args, **kwargs)
535
+ except (AssertionError, AssertionFailure):
536
+ return self._builder
537
+ finally:
538
+ self._builder.kind = "warn"
539
+ self._builder.logger.warning(self._make_msg(name))
540
+ return self._builder
541
+
542
+
439
543
  class AssertionBuilder(
440
544
  StringMixin,
441
545
  SnapshotMixin,
442
546
  NumericMixin,
547
+ JsonMixin,
443
548
  HelpersMixin,
444
549
  FileMixin,
445
550
  ExtractingMixin,
@@ -474,6 +579,11 @@ class AssertionBuilder(
474
579
  self.logger = logger if logger else _default_logger
475
580
  self._not_expected = False
476
581
 
582
+ @property
583
+ def not_(self) -> NegatedBuilder:
584
+ """Invert the next assertion in the chain."""
585
+ return NegatedBuilder(self)
586
+
477
587
  def builder(self, val, description="", kind=None, expected=None, logger=None):
478
588
  """Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
479
589
 
@@ -515,7 +625,7 @@ class AssertionBuilder(
515
625
  self.logger.warning(out)
516
626
  return self
517
627
  elif self.kind == "soft":
518
- _soft_err.get().append(out)
628
+ _soft_err.get().append((_soft_group.get(), out))
519
629
  return self
520
630
  else:
521
631
  if expected is not None or diff is not None: