assertpy2 2.3.6__tar.gz → 2.3.8__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 (93) hide show
  1. {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/dependabot.yml +5 -0
  2. {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/ci.yml +7 -3
  3. {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/codeql.yml +4 -2
  4. {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/publish.yml +11 -3
  5. {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/scorecard.yml +1 -1
  6. assertpy2-2.3.8/.github/workflows/zizmor.yml +21 -0
  7. {assertpy2-2.3.6 → assertpy2-2.3.8}/PKG-INFO +218 -113
  8. {assertpy2-2.3.6 → assertpy2-2.3.8}/README.md +213 -111
  9. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/__init__.py +4 -0
  10. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/_typing.py +30 -1
  11. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/assertpy.py +91 -8
  12. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/base.py +117 -1
  13. assertpy2-2.3.8/assertpy2/bytes_mixin.py +142 -0
  14. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/contains.py +22 -3
  15. assertpy2-2.3.8/assertpy2/json_mixin.py +163 -0
  16. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/pytest_plugin.py +77 -2
  17. {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/api.md +769 -454
  18. {assertpy2-2.3.6 → assertpy2-2.3.8}/pyproject.toml +4 -3
  19. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_behave_matchers.py +23 -20
  20. assertpy2-2.3.8/tests/test_bytes.py +147 -0
  21. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_errors.py +1 -1
  22. assertpy2-2.3.8/tests/test_grouped_soft.py +130 -0
  23. assertpy2-2.3.8/tests/test_json.py +172 -0
  24. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matcher_registry.py +2 -2
  25. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matchers_phase3.py +53 -53
  26. assertpy2-2.3.8/tests/test_overloads.py +58 -0
  27. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_pytest_plugin.py +149 -63
  28. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_regex_groups.py +1 -5
  29. assertpy2-2.3.8/tests/test_rich_diff.py +427 -0
  30. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_snapshots.py +3 -3
  31. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_warn.py +1 -1
  32. assertpy2-2.3.8/uv.lock +792 -0
  33. assertpy2-2.3.6/tests/test_overloads.py +0 -71
  34. assertpy2-2.3.6/uv.lock +0 -465
  35. {assertpy2-2.3.6 → assertpy2-2.3.8}/.codecov.yml +0 -0
  36. {assertpy2-2.3.6 → assertpy2-2.3.8}/.gitignore +0 -0
  37. {assertpy2-2.3.6 → assertpy2-2.3.8}/CONTRIBUTING.md +0 -0
  38. {assertpy2-2.3.6 → assertpy2-2.3.8}/LICENSE +0 -0
  39. {assertpy2-2.3.6 → assertpy2-2.3.8}/SECURITY.md +0 -0
  40. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/_mixin_base.py +0 -0
  41. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/async_assertions.py +0 -0
  42. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/behave_matchers.py +0 -0
  43. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/collection.py +0 -0
  44. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/date.py +0 -0
  45. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/dict.py +0 -0
  46. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/dynamic.py +0 -0
  47. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/errors.py +0 -0
  48. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/exception.py +0 -0
  49. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/extracting.py +0 -0
  50. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/file.py +0 -0
  51. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/helpers.py +0 -0
  52. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/matchers.py +0 -0
  53. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/numeric.py +0 -0
  54. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/py.typed +0 -0
  55. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/snapshot.py +0 -0
  56. {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/string.py +0 -0
  57. {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/logo-dark.svg +0 -0
  58. {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/logo.svg +0 -0
  59. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_async.py +0 -0
  60. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_bool.py +0 -0
  61. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_class.py +0 -0
  62. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_collection.py +0 -0
  63. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_core.py +0 -0
  64. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_custom_dict.py +0 -0
  65. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_custom_list.py +0 -0
  66. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_datetime.py +0 -0
  67. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_description.py +0 -0
  68. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dict.py +0 -0
  69. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dict_compare.py +0 -0
  70. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dyn.py +0 -0
  71. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_equals.py +0 -0
  72. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_expected_exception.py +0 -0
  73. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_extensions.py +0 -0
  74. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_extracting.py +0 -0
  75. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_fail.py +0 -0
  76. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_file.py +0 -0
  77. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_in.py +0 -0
  78. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_list.py +0 -0
  79. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matchers.py +0 -0
  80. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_namedtuple.py +0 -0
  81. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_none.py +0 -0
  82. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_not.py +0 -0
  83. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_numbers.py +0 -0
  84. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_phase2.py +0 -0
  85. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_pipeline.py +0 -0
  86. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_readme.py +0 -0
  87. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_same_as.py +0 -0
  88. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_soft.py +0 -0
  89. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_soft_fail.py +0 -0
  90. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_string.py +0 -0
  91. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_structural.py +0 -0
  92. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_traceback.py +0 -0
  93. {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_type.py +0 -0
@@ -4,6 +4,9 @@ updates:
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: "weekly"
7
+ cooldown:
8
+ default-days: 7
9
+ semver-major-days: 7
7
10
  commit-message:
8
11
  prefix: "chore"
9
12
 
@@ -11,5 +14,7 @@ updates:
11
14
  directory: "/"
12
15
  schedule:
13
16
  interval: "weekly"
17
+ cooldown:
18
+ default-days: 7
14
19
  commit-message:
15
20
  prefix: "ci"
@@ -17,6 +17,8 @@ jobs:
17
17
  python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
18
18
  steps:
19
19
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
+ with:
21
+ persist-credentials: false
20
22
 
21
23
  - name: Install uv
22
24
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -28,7 +30,7 @@ jobs:
28
30
  allow-prereleases: true
29
31
 
30
32
  - name: Install dependencies
31
- run: uv sync
33
+ run: uv sync --extra json
32
34
 
33
35
  - name: Lint
34
36
  run: uv run ruff check .
@@ -38,7 +40,7 @@ jobs:
38
40
 
39
41
  - name: Upload coverage to Codecov
40
42
  if: matrix.python-version == '3.14'
41
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
43
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
42
44
  with:
43
45
  token: ${{ secrets.CODECOV_TOKEN }}
44
46
  files: coverage.xml
@@ -47,6 +49,8 @@ jobs:
47
49
  runs-on: ubuntu-latest
48
50
  steps:
49
51
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
52
+ with:
53
+ persist-credentials: false
50
54
 
51
55
  - name: Install uv
52
56
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -57,7 +61,7 @@ jobs:
57
61
  python-version: "3.14"
58
62
 
59
63
  - name: Install dependencies
60
- run: uv sync
64
+ run: uv sync --extra json
61
65
 
62
66
  - name: Ruff check
63
67
  run: uv run ruff check .
@@ -18,7 +18,9 @@ jobs:
18
18
  security-events: write
19
19
  steps:
20
20
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
21
- - uses: github/codeql-action/init@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0
21
+ with:
22
+ persist-credentials: false
23
+ - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
22
24
  with:
23
25
  languages: python
24
- - uses: github/codeql-action/analyze@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0
26
+ - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
@@ -17,9 +17,13 @@ jobs:
17
17
  attestations: write
18
18
  steps:
19
19
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
20
+ with:
21
+ persist-credentials: false
20
22
 
21
23
  - name: Install uv
22
24
  uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
25
+ with:
26
+ enable-cache: false
23
27
 
24
28
  - name: Set up Python
25
29
  uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -45,13 +49,17 @@ jobs:
45
49
  - name: Prepare provenance bundles
46
50
  run: |
47
51
  mkdir -p provenance
48
- cp "${{ steps.attest-wheel.outputs.bundle-path }}" "provenance/$(basename dist/*.whl).sigstore.json"
49
- cp "${{ steps.attest-sdist.outputs.bundle-path }}" "provenance/$(basename dist/*.tar.gz).sigstore.json"
52
+ cp "$WHEEL_BUNDLE" "provenance/$(basename dist/*.whl).sigstore.json"
53
+ cp "$SDIST_BUNDLE" "provenance/$(basename dist/*.tar.gz).sigstore.json"
54
+ env:
55
+ WHEEL_BUNDLE: ${{ steps.attest-wheel.outputs.bundle-path }}
56
+ SDIST_BUNDLE: ${{ steps.attest-sdist.outputs.bundle-path }}
50
57
 
51
58
  - name: Publish to PyPI
52
59
  uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
53
60
 
54
61
  - name: Upload to GitHub Release
55
- run: gh release upload "${{ github.event.release.tag_name }}" dist/* provenance/*
62
+ run: gh release upload "$TAG_NAME" dist/* provenance/*
56
63
  env:
57
64
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65
+ TAG_NAME: ${{ github.event.release.tag_name }}
@@ -29,6 +29,6 @@ jobs:
29
29
  name: SARIF file
30
30
  path: results.sarif
31
31
  retention-days: 5
32
- - uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
32
+ - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
33
33
  with:
34
34
  sarif_file: results.sarif
@@ -0,0 +1,21 @@
1
+ name: Zizmor
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ zizmor:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ security-events: write
17
+ steps:
18
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
19
+ with:
20
+ persist-credentials: false
21
+ - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.3.6
3
+ Version: 2.3.8
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">
@@ -61,6 +64,7 @@ Description-Content-Type: text/markdown
61
64
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
62
65
  </p>
63
66
 
67
+ ---
64
68
 
65
69
  ## Quick start
66
70
 
@@ -97,7 +101,7 @@ assert_that(api_response).matches_structure({
97
101
  })
98
102
  ```
99
103
 
100
- Structured errors with rich data:
104
+ Structured errors with rich diffs:
101
105
 
102
106
  ```py
103
107
  try:
@@ -115,8 +119,14 @@ FAILED test_example.py::test_comparison
115
119
  --- AssertionFailure ---
116
120
  actual: {'a': 1, 'b': 2}
117
121
  expected: {'a': 1, 'b': 99}
122
+ --- Structured Diff ---
123
+ diff (dict):
124
+ b:
125
+ - 2
126
+ + 99
118
127
  ```
119
128
 
129
+ ---
120
130
 
121
131
  ## Comparison
122
132
 
@@ -132,7 +142,7 @@ FAILED test_example.py::test_comparison
132
142
  | **Async assertions** | No | No | No | **eventually() with polling** |
133
143
  | **Soft assertions** | No | No | Yes (not thread-safe) | **Yes (thread-safe, async-safe)** |
134
144
  | **Structured errors** | Rewrite only | Mismatch string | String only | **.actual .expected .diff** |
135
- | **Maintained** | N/A | Minimal | 2020 | **Active** |
145
+ | **Maintained** | Built-in | Minimal | 2020 | **Active** |
136
146
 
137
147
  </div>
138
148
 
@@ -157,31 +167,52 @@ assert "admin" in items
157
167
  assert_that(items).is_type_of(list).is_length(3).contains("admin")
158
168
  ```
159
169
 
170
+ ---
160
171
 
161
172
  ## Features
162
173
 
174
+ **Fluent API**
175
+
163
176
  - [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
164
177
  - [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
165
178
  - [**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.
167
- - [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for async and eventual consistency testing.
168
- - [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
169
- - **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
170
- - **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
171
- - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe and async-safe via `contextvars`, collect all failures with `soft_assertions()`.
179
+ - [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
172
180
  - [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
173
- - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
181
+
182
+ **Built-in types**
183
+
184
+ - [Strings](docs/api.md#strings), [numbers](docs/api.md#numbers), [lists](docs/api.md#lists), [tuples](docs/api.md#tuples), [sets](docs/api.md#sets), [dicts](docs/api.md#dicts), [dates](docs/api.md#dates), [booleans](docs/api.md#booleans), [objects](docs/api.md#objects), [bytes](docs/api.md#bytes--bytearray-assertions), [files](docs/api.md#files), [exceptions](docs/api.md#failure).
185
+ - [**Bytes assertions**](docs/api.md#bytes--bytearray-assertions): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
186
+ - [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
187
+ - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method.
174
188
  - [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key matching.
175
189
  - [**Extracting**](docs/api.md#objects): flatten collections on attributes with `filter` and `sort` support.
176
- - [**File assertions**](docs/api.md#files): `exists()`, `is_file()`, `is_readable()`, `is_writable()`, `is_executable()` with `pathlib.Path` support.
177
- - [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format, inspired by Jest.
178
- - [**Allure integration**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to Allure reports.
179
- - [**Behave step matchers**](docs/api.md#behave-step-matchers): ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
180
- - [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): register domain-specific matchers via `register_matcher()`, composable with `&`, `|`, `~`.
181
- - [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` to assert on regex captures fluently.
182
- - [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): add custom assertions via `add_extension()`.
183
- - [Strings](docs/api.md#strings), [numbers](docs/api.md#numbers), [lists](docs/api.md#lists), [tuples](docs/api.md#tuples), [sets](docs/api.md#sets), [dicts](docs/api.md#dicts), [dates](docs/api.md#dates), [booleans](docs/api.md#booleans), [objects](docs/api.md#objects), [exceptions](docs/api.md#failure).
184
190
 
191
+ **Testing**
192
+
193
+ - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe, async-safe via `contextvars`. Group errors with `sa.group()`, or use `assert_all()`.
194
+ - [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for eventual consistency.
195
+ - [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
196
+ - [**Rich pytest diffs**](docs/api.md#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples.
197
+ - [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format.
198
+
199
+ **Type safety**
200
+
201
+ - **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
202
+ - **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
203
+
204
+ **Extensibility**
205
+
206
+ - [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
207
+ - [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
208
+ - [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): `add_extension()` for custom assertion methods.
209
+
210
+ **Integrations**
211
+
212
+ - [**Allure**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
213
+ - [**Behave**](docs/api.md#behave-step-matchers): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
214
+
215
+ ---
185
216
 
186
217
  ## Composable matchers
187
218
 
@@ -237,88 +268,9 @@ assert_that(api_response).matches_structure({
237
268
  })
238
269
  ```
239
270
 
271
+ ---
240
272
 
241
- ## Async assertions
242
-
243
- Poll a callable until the assertion passes or timeout is reached:
244
-
245
- ```py
246
- from assertpy2 import assert_that
247
-
248
- async def test_eventual_consistency():
249
- await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
250
-
251
- # works with async callables
252
- await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
253
- ```
254
-
255
- Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
256
-
257
-
258
- ## Soft assertions
259
-
260
- Collect all failures instead of stopping at the first one:
261
-
262
- ```py
263
- from assertpy2 import assert_that, soft_assertions
264
-
265
- def test_user_profile():
266
- with soft_assertions():
267
- assert_that(user.name).is_equal_to("Alice")
268
- assert_that(user.age).is_greater_than(0)
269
- assert_that(user.email).contains("@")
270
- ```
271
-
272
- All failures are reported at the end of the block:
273
-
274
- ```
275
- AssertionError: soft assertion failures:
276
- 1. Expected <Bob> to be equal to <Alice>, but was not.
277
- 2. Expected <-1> to be greater than <0>, but was not.
278
- 3. Expected <invalid> to contain <@>, but did not.
279
- ```
280
-
281
- Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
282
-
283
- Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
284
-
285
-
286
- ## Structured errors
287
-
288
- When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
289
-
290
- ```py
291
- try:
292
- assert_that(1).is_equal_to(2)
293
- except AssertionError as e:
294
- e.actual # 1
295
- e.expected # 2
296
- ```
297
-
298
- For dict comparisons, a `DiffResult` with per-key diff entries is available:
299
-
300
- ```py
301
- try:
302
- assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
303
- except AssertionError as e:
304
- e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
305
- ```
306
-
307
- `AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
308
-
309
- The pytest plugin (auto-registered, no configuration needed) renders structured data as extra sections in failure reports:
310
-
311
- ```
312
- FAILED test_example.py::test_comparison
313
- --- AssertionFailure ---
314
- actual: {'a': 1, 'b': 2}
315
- expected: {'a': 1, 'b': 99}
316
- --- Structured Diff ---
317
- DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
318
- ```
319
-
320
-
321
- ## More features
273
+ ## Fluent API
322
274
 
323
275
  ### Universal negation
324
276
 
@@ -372,6 +324,16 @@ assert_that(users).extracting("user", filter="active").is_equal_to(["Fred", "Joh
372
324
  assert_that(users).extracting("user", sort="age").is_equal_to(["Johnny", "Fred", "Bob"])
373
325
  ```
374
326
 
327
+ ### Dynamic assertions
328
+
329
+ ```py
330
+ fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
331
+
332
+ assert_that(fred).has_first_name("Fred")
333
+ assert_that(fred).has_last_name("Smith")
334
+ assert_that(fred).has_shoe_size(12)
335
+ ```
336
+
375
337
  ### Expected exceptions
376
338
 
377
339
  ```py
@@ -379,14 +341,37 @@ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
379
341
  .is_length(8).starts_with("some").is_equal_to("some err")
380
342
  ```
381
343
 
382
- ### Dynamic assertions
344
+ ---
345
+
346
+ ## Built-in types
347
+
348
+ ### Bytes assertions
349
+
350
+ Assert on `bytes` and `bytearray` values:
383
351
 
384
352
  ```py
385
- fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
353
+ data = b"\x89PNG\r\n\x1a\n"
386
354
 
387
- assert_that(fred).has_first_name("Fred")
388
- assert_that(fred).has_last_name("Smith")
389
- assert_that(fred).has_shoe_size(12)
355
+ assert_that(data).starts_with_bytes(b"\x89PNG")
356
+ assert_that(data).has_byte_at(0, 0x89)
357
+ assert_that(data).is_hex_equal_to("89504e470d0a1a0a")
358
+ assert_that(b"hello").is_valid_utf8()
359
+ assert_that(b"hello").decoded_as("utf-8").starts_with("hel")
360
+ ```
361
+
362
+ Available methods: `is_valid_utf8()`, `is_valid_encoding()`, `starts_with_bytes()`, `contains_bytes()`, `has_byte_at()`, `is_hex_equal_to()`, `decoded_as()`.
363
+
364
+ ### JSON path and schema validation
365
+
366
+ Requires `pip install assertpy2[json]`.
367
+
368
+ ```py
369
+ data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}
370
+
371
+ assert_that(data).at_json_path("$.users[0].name").is_equal_to("Alice")
372
+ assert_that(data).has_json_path("$.meta.total")
373
+ assert_that(data).does_not_have_json_path("$.error")
374
+ assert_that(data).matches_json_schema({"type": "object", "required": ["users"]})
390
375
  ```
391
376
 
392
377
  ### Snapshot testing
@@ -395,6 +380,123 @@ assert_that(fred).has_shoe_size(12)
395
380
  assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
396
381
  ```
397
382
 
383
+ ---
384
+
385
+ ## Testing
386
+
387
+ ### Soft assertions
388
+
389
+ Collect all failures instead of stopping at the first one:
390
+
391
+ ```py
392
+ from assertpy2 import assert_that, soft_assertions
393
+
394
+ def test_user_profile():
395
+ with soft_assertions():
396
+ assert_that(user.name).is_equal_to("Alice")
397
+ assert_that(user.age).is_greater_than(0)
398
+ assert_that(user.email).contains("@")
399
+ ```
400
+
401
+ All failures are reported at the end of the block:
402
+
403
+ ```
404
+ AssertionError: soft assertion failures:
405
+ 1. Expected <Bob> to be equal to <Alice>, but was not.
406
+ 2. Expected <-1> to be greater than <0>, but was not.
407
+ 3. Expected <invalid> to contain <@>, but did not.
408
+ ```
409
+
410
+ Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
411
+
412
+ Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
413
+
414
+ ### Grouped soft assertions
415
+
416
+ ```py
417
+ with soft_assertions() as sa:
418
+ with sa.group("Headers"):
419
+ assert_that(headers["Content-Type"]).is_equal_to("application/json")
420
+ with sa.group("Body"):
421
+ assert_that(body["status"]).is_equal_to("ok")
422
+ assert_that(body["items"]).is_not_empty()
423
+
424
+ # or inline with assert_all
425
+ assert_all(
426
+ lambda: assert_that(x).is_positive(),
427
+ lambda: assert_that(y).is_not_none(),
428
+ )
429
+ ```
430
+
431
+ ### Async assertions
432
+
433
+ Poll a callable until the assertion passes or timeout is reached:
434
+
435
+ ```py
436
+ from assertpy2 import assert_that
437
+
438
+ async def test_eventual_consistency():
439
+ await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
440
+
441
+ # works with async callables
442
+ await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
443
+ ```
444
+
445
+ Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
446
+
447
+ ### Structured errors
448
+
449
+ When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
450
+
451
+ ```py
452
+ try:
453
+ assert_that(1).is_equal_to(2)
454
+ except AssertionError as e:
455
+ e.actual # 1
456
+ e.expected # 2
457
+ ```
458
+
459
+ For comparisons, a `DiffResult` with structural diff entries is available:
460
+
461
+ ```py
462
+ try:
463
+ assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
464
+ except AssertionError as e:
465
+ e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
466
+ ```
467
+
468
+ `AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
469
+
470
+ ### Rich pytest diffs
471
+
472
+ The pytest plugin (auto-registered, no configuration needed) renders structural diffs with recursive descent:
473
+
474
+ ```
475
+ FAILED test_example.py::test_api
476
+ --- AssertionFailure ---
477
+ actual: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
478
+ expected: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Robert'}]
479
+ --- Structured Diff ---
480
+ diff (sequence):
481
+ [1].name:
482
+ - 'Bob'
483
+ + 'Robert'
484
+ ```
485
+
486
+ Supported types: list/tuple, set/frozenset, str, dict, dataclass, namedtuple. Nested structures are diffed recursively. Colored output when `--color=yes`.
487
+
488
+ Configure via `pyproject.toml`:
489
+
490
+ ```toml
491
+ [tool.pytest.ini_options]
492
+ assertpy2_diff = "off" # disable diff sections
493
+ assertpy2_diff_max_entries = "100" # max entries shown (default 50, 0 = unlimited)
494
+ ```
495
+
496
+ ---
497
+
498
+ ## Extensibility
499
+
398
500
  ### Custom matchers
399
501
 
400
502
  Register domain-specific matchers on the `match` namespace with `register_matcher()`:
@@ -457,8 +559,11 @@ assert_that(5).is_5()
457
559
 
458
560
  See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
459
561
 
562
+ ---
460
563
 
461
- ## Allure integration
564
+ ## Integrations
565
+
566
+ ### Allure
462
567
 
463
568
  When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
464
569
 
@@ -480,8 +585,7 @@ Three modes controlled via `pytest.ini` (or `pyproject.toml`):
480
585
  assertpy2_allure = "full"
481
586
  ```
482
587
 
483
-
484
- ## Behave step matchers
588
+ ### Behave
485
589
 
486
590
  Ready-made parameter types for Behave step definitions:
487
591
 
@@ -505,6 +609,7 @@ def step_impl(context, age):
505
609
 
506
610
  Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
507
611
 
612
+ ---
508
613
 
509
614
  ## Migration from assertpy
510
615
 
@@ -521,8 +626,8 @@ from assertpy2 import assert_that, soft_assertions
521
626
  See the [comparison table](#comparison) above for feature differences with other libraries.
522
627
 
523
628
 
524
- ## Contributing
525
-
526
- Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
527
-
629
+ ---
528
630
 
631
+ <p align="center">
632
+ <a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
633
+ </p>