assertpy2 2.3.7__tar.gz → 2.4.0__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 (92) hide show
  1. {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/dependabot.yml +5 -0
  2. {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/ci.yml +7 -3
  3. {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/codeql.yml +4 -2
  4. {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/publish.yml +11 -3
  5. {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/scorecard.yml +1 -1
  6. assertpy2-2.4.0/.github/workflows/zizmor.yml +21 -0
  7. {assertpy2-2.3.7 → assertpy2-2.4.0}/PKG-INFO +212 -128
  8. {assertpy2-2.3.7 → assertpy2-2.4.0}/README.md +211 -127
  9. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/_typing.py +16 -0
  10. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/assertpy.py +9 -1
  11. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/base.py +117 -1
  12. assertpy2-2.4.0/assertpy2/bytes_mixin.py +142 -0
  13. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/contains.py +22 -3
  14. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/pytest_plugin.py +77 -2
  15. {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/api.md +746 -543
  16. {assertpy2-2.3.7 → assertpy2-2.4.0}/pyproject.toml +1 -1
  17. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_behave_matchers.py +23 -20
  18. assertpy2-2.4.0/tests/test_bytes.py +147 -0
  19. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_errors.py +1 -1
  20. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_json.py +3 -0
  21. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matcher_registry.py +2 -2
  22. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matchers_phase3.py +53 -53
  23. assertpy2-2.4.0/tests/test_overloads.py +58 -0
  24. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_pytest_plugin.py +149 -63
  25. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_regex_groups.py +1 -5
  26. assertpy2-2.4.0/tests/test_rich_diff.py +427 -0
  27. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_snapshots.py +3 -3
  28. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_warn.py +1 -1
  29. assertpy2-2.3.7/tests/test_overloads.py +0 -71
  30. {assertpy2-2.3.7 → assertpy2-2.4.0}/.codecov.yml +0 -0
  31. {assertpy2-2.3.7 → assertpy2-2.4.0}/.gitignore +0 -0
  32. {assertpy2-2.3.7 → assertpy2-2.4.0}/CONTRIBUTING.md +0 -0
  33. {assertpy2-2.3.7 → assertpy2-2.4.0}/LICENSE +0 -0
  34. {assertpy2-2.3.7 → assertpy2-2.4.0}/SECURITY.md +0 -0
  35. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/__init__.py +0 -0
  36. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/_mixin_base.py +0 -0
  37. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/async_assertions.py +0 -0
  38. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/behave_matchers.py +0 -0
  39. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/collection.py +0 -0
  40. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/date.py +0 -0
  41. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/dict.py +0 -0
  42. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/dynamic.py +0 -0
  43. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/errors.py +0 -0
  44. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/exception.py +0 -0
  45. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/extracting.py +0 -0
  46. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/file.py +0 -0
  47. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/helpers.py +0 -0
  48. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/json_mixin.py +0 -0
  49. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/matchers.py +0 -0
  50. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/numeric.py +0 -0
  51. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/py.typed +0 -0
  52. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/snapshot.py +0 -0
  53. {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/string.py +0 -0
  54. {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/logo-dark.svg +0 -0
  55. {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/logo.svg +0 -0
  56. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_async.py +0 -0
  57. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_bool.py +0 -0
  58. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_class.py +0 -0
  59. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_collection.py +0 -0
  60. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_core.py +0 -0
  61. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_custom_dict.py +0 -0
  62. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_custom_list.py +0 -0
  63. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_datetime.py +0 -0
  64. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_description.py +0 -0
  65. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dict.py +0 -0
  66. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dict_compare.py +0 -0
  67. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dyn.py +0 -0
  68. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_equals.py +0 -0
  69. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_expected_exception.py +0 -0
  70. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_extensions.py +0 -0
  71. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_extracting.py +0 -0
  72. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_fail.py +0 -0
  73. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_file.py +0 -0
  74. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_grouped_soft.py +0 -0
  75. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_in.py +0 -0
  76. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_list.py +0 -0
  77. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matchers.py +0 -0
  78. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_namedtuple.py +0 -0
  79. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_none.py +0 -0
  80. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_not.py +0 -0
  81. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_numbers.py +0 -0
  82. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_phase2.py +0 -0
  83. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_pipeline.py +0 -0
  84. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_readme.py +0 -0
  85. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_same_as.py +0 -0
  86. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_soft.py +0 -0
  87. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_soft_fail.py +0 -0
  88. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_string.py +0 -0
  89. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_structural.py +0 -0
  90. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_traceback.py +0 -0
  91. {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_type.py +0 -0
  92. {assertpy2-2.3.7 → assertpy2-2.4.0}/uv.lock +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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.3.7
3
+ Version: 2.4.0
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
@@ -64,6 +64,7 @@ Description-Content-Type: text/markdown
64
64
  <a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
65
65
  </p>
66
66
 
67
+ ---
67
68
 
68
69
  ## Quick start
69
70
 
@@ -100,7 +101,7 @@ assert_that(api_response).matches_structure({
100
101
  })
101
102
  ```
102
103
 
103
- Structured errors with rich data:
104
+ Structured errors with rich diffs:
104
105
 
105
106
  ```py
106
107
  try:
@@ -118,8 +119,27 @@ FAILED test_example.py::test_comparison
118
119
  --- AssertionFailure ---
119
120
  actual: {'a': 1, 'b': 2}
120
121
  expected: {'a': 1, 'b': 99}
122
+ --- Structured Diff ---
123
+ diff (dict):
124
+ b:
125
+ - 2
126
+ + 99
121
127
  ```
122
128
 
129
+ ## Type-aware autocomplete
130
+
131
+ `assert_that()` uses `@overload` to return type-specific Protocols.
132
+ Your IDE shows only methods relevant to the value you're testing, not all 100+:
133
+
134
+ - `assert_that("hello").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
135
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
136
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
137
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
138
+ - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
139
+
140
+ 9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
141
+
142
+ ---
123
143
 
124
144
  ## Comparison
125
145
 
@@ -128,14 +148,14 @@ FAILED test_example.py::test_comparison
128
148
  | | pytest assert | PyHamcrest | assertpy | **assertpy2** |
129
149
  |---|:---:|:---:|:---:|:---:|
130
150
  | **Type safety** | Partial (mypy plugin) | No | No | **py.typed, @overload, Self** |
131
- | **IDE autocomplete** | Generic | Generic | Generic | **Type-specific per value** |
151
+ | **IDE autocomplete** | Generic | Generic | Generic | **[Filtered by type](#type-aware-autocomplete)** |
132
152
  | **Fluent chaining** | No | No | Yes | **Yes** |
133
153
  | **Composable matchers** | No | Yes (functions) | No | **Yes (`&` `\|` `~` operators)** |
134
154
  | **Structural matching** | No | Flat (has_entries) | No | **Recursive with matchers** |
135
155
  | **Async assertions** | No | No | No | **eventually() with polling** |
136
156
  | **Soft assertions** | No | No | Yes (not thread-safe) | **Yes (thread-safe, async-safe)** |
137
157
  | **Structured errors** | Rewrite only | Mismatch string | String only | **.actual .expected .diff** |
138
- | **Maintained** | N/A | Minimal | 2020 | **Active** |
158
+ | **Maintained** | Built-in | Minimal | 2020 | **Active** |
139
159
 
140
160
  </div>
141
161
 
@@ -160,32 +180,52 @@ assert "admin" in items
160
180
  assert_that(items).is_type_of(list).is_length(3).contains("admin")
161
181
  ```
162
182
 
183
+ ---
163
184
 
164
185
  ## Features
165
186
 
187
+ **Fluent API**
188
+
166
189
  - [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
167
190
  - [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
168
191
  - [**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.
170
- - [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for async and eventual consistency testing.
171
- - [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes, pytest plugin with rich diff output.
172
- - **Typed overloads**: `assert_that()` returns type-specific Protocols, IDE shows only relevant methods per type.
173
- - **Type safety**: `Self` return types, `py.typed` ([PEP 561](https://peps.python.org/pep-0561/)).
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`).
192
+ - [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
176
193
  - [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
177
- - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method on objects and dicts.
194
+
195
+ **Built-in types**
196
+
197
+ - [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).
198
+ - [**Bytes assertions**](docs/api.md#bytes--bytearray-assertions): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
199
+ - [**JSON assertions**](docs/api.md#json-path--schema-validation): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
200
+ - [**Dynamic assertions**](docs/api.md#objects): `has_<name>()` for any attribute, property, or zero-argument method.
178
201
  - [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key matching.
179
202
  - [**Extracting**](docs/api.md#objects): flatten collections on attributes with `filter` and `sort` support.
180
- - [**File assertions**](docs/api.md#files): `exists()`, `is_file()`, `is_readable()`, `is_writable()`, `is_executable()` with `pathlib.Path` support.
181
- - [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format, inspired by Jest.
182
- - [**Allure integration**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to Allure reports.
183
- - [**Behave step matchers**](docs/api.md#behave-step-matchers): ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
184
- - [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): register domain-specific matchers via `register_matcher()`, composable with `&`, `|`, `~`.
185
- - [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` to assert on regex captures fluently.
186
- - [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): add custom assertions via `add_extension()`.
187
- - [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).
188
203
 
204
+ **Testing**
205
+
206
+ - [**Soft assertions**](docs/api.md#soft-assertions): thread-safe, async-safe via `contextvars`. Group errors with `sa.group()`, or use `assert_all()`.
207
+ - [**Async assertions**](docs/api.md#async-assertions): `eventually()` with polling/retry for eventual consistency.
208
+ - [**Structured errors**](docs/api.md#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
209
+ - [**Rich pytest diffs**](docs/api.md#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples.
210
+ - [**Snapshot testing**](docs/api.md#snapshot-testing): store and compare data structures in JSON format.
211
+
212
+ **Type safety**
213
+
214
+ - [**Type-aware autocomplete**](#type-aware-autocomplete): 9 Protocols, IDE shows only relevant methods per type.
215
+ - **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
216
+
217
+ **Extensibility**
218
+
219
+ - [**Custom matchers**](docs/api.md#custom-matchers---registering-domain-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
220
+ - [**Regex group extraction**](docs/api.md#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
221
+ - [**Extensions**](docs/api.md#extension-system---adding-custom-assertions): `add_extension()` for custom assertion methods.
222
+
223
+ **Integrations**
224
+
225
+ - [**Allure**](docs/api.md#allure-integration): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
226
+ - [**Behave**](docs/api.md#behave-step-matchers): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
227
+
228
+ ---
189
229
 
190
230
  ## Composable matchers
191
231
 
@@ -241,120 +281,148 @@ assert_that(api_response).matches_structure({
241
281
  })
242
282
  ```
243
283
 
284
+ ---
244
285
 
245
- ## Async assertions
246
-
247
- Poll a callable until the assertion passes or timeout is reached:
286
+ ## Fluent API
248
287
 
249
- ```py
250
- from assertpy2 import assert_that
288
+ ### Universal negation
251
289
 
252
- async def test_eventual_consistency():
253
- await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
290
+ Invert any assertion with `.not_`:
254
291
 
255
- # works with async callables
256
- await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
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()
257
297
  ```
258
298
 
259
- Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
260
-
299
+ Works with soft assertions and warn mode.
261
300
 
262
- ## Soft assertions
301
+ ### Collection pipeline
263
302
 
264
- Collect all failures instead of stopping at the first one:
303
+ Transform collections before asserting:
265
304
 
266
305
  ```py
267
- from assertpy2 import assert_that, soft_assertions
306
+ orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
268
307
 
269
- def test_user_profile():
270
- with soft_assertions():
271
- assert_that(user.name).is_equal_to("Alice")
272
- assert_that(user.age).is_greater_than(0)
273
- assert_that(user.email).contains("@")
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")
274
316
  ```
275
317
 
276
- All failures are reported at the end of the block:
318
+ Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
277
319
 
278
- ```
279
- AssertionError: soft assertion failures:
280
- 1. Expected <Bob> to be equal to <Alice>, but was not.
281
- 2. Expected <-1> to be greater than <0>, but was not.
282
- 3. Expected <invalid> to contain <@>, but did not.
283
- ```
320
+ ### Dict comparison with ignore/include
284
321
 
285
- Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
322
+ ```py
323
+ assert_that({"a": 1, "b": 2, "c": 3}).is_equal_to({"a": 1}, ignore=["b", "c"])
324
+ assert_that({"a": 1, "b": {"c": 2, "d": 3}}).is_equal_to({"b": {"d": 3}}, include=("b", "d"))
325
+ ```
286
326
 
287
- Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
327
+ ### Extracting with filter and sort
288
328
 
329
+ ```py
330
+ users = [
331
+ {"user": "Fred", "age": 36, "active": True},
332
+ {"user": "Bob", "age": 40, "active": False},
333
+ {"user": "Johnny", "age": 13, "active": True},
334
+ ]
289
335
 
290
- ## Structured errors
336
+ assert_that(users).extracting("user", filter="active").is_equal_to(["Fred", "Johnny"])
337
+ assert_that(users).extracting("user", sort="age").is_equal_to(["Johnny", "Fred", "Bob"])
338
+ ```
291
339
 
292
- When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
340
+ ### Dynamic assertions
293
341
 
294
342
  ```py
295
- try:
296
- assert_that(1).is_equal_to(2)
297
- except AssertionError as e:
298
- e.actual # 1
299
- e.expected # 2
343
+ fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
344
+
345
+ assert_that(fred).has_first_name("Fred")
346
+ assert_that(fred).has_last_name("Smith")
347
+ assert_that(fred).has_shoe_size(12)
300
348
  ```
301
349
 
302
- For dict comparisons, a `DiffResult` with per-key diff entries is available:
350
+ ### Expected exceptions
303
351
 
304
352
  ```py
305
- try:
306
- assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
307
- except AssertionError as e:
308
- e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
353
+ assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
354
+ .is_length(8).starts_with("some").is_equal_to("some err")
309
355
  ```
310
356
 
311
- `AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
357
+ ---
312
358
 
313
- The pytest plugin (auto-registered, no configuration needed) renders structured data as extra sections in failure reports:
359
+ ## Built-in types
314
360
 
315
- ```
316
- FAILED test_example.py::test_comparison
317
- --- AssertionFailure ---
318
- actual: {'a': 1, 'b': 2}
319
- expected: {'a': 1, 'b': 99}
320
- --- Structured Diff ---
321
- DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
361
+ ### Bytes assertions
362
+
363
+ Assert on `bytes` and `bytearray` values:
364
+
365
+ ```py
366
+ data = b"\x89PNG\r\n\x1a\n"
367
+
368
+ assert_that(data).starts_with_bytes(b"\x89PNG")
369
+ assert_that(data).has_byte_at(0, 0x89)
370
+ assert_that(data).is_hex_equal_to("89504e470d0a1a0a")
371
+ assert_that(b"hello").is_valid_utf8()
372
+ assert_that(b"hello").decoded_as("utf-8").starts_with("hel")
322
373
  ```
323
374
 
375
+ Available methods: `is_valid_utf8()`, `is_valid_encoding()`, `starts_with_bytes()`, `contains_bytes()`, `has_byte_at()`, `is_hex_equal_to()`, `decoded_as()`.
324
376
 
325
- ## More features
377
+ ### JSON path and schema validation
326
378
 
327
- ### Universal negation
379
+ Requires `pip install assertpy2[json]`.
328
380
 
329
- Invert any assertion with `.not_`:
381
+ ```py
382
+ data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}
383
+
384
+ assert_that(data).at_json_path("$.users[0].name").is_equal_to("Alice")
385
+ assert_that(data).has_json_path("$.meta.total")
386
+ assert_that(data).does_not_have_json_path("$.error")
387
+ assert_that(data).matches_json_schema({"type": "object", "required": ["users"]})
388
+ ```
389
+
390
+ ### Snapshot testing
330
391
 
331
392
  ```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()
393
+ assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
336
394
  ```
337
395
 
338
- Works with soft assertions and warn mode.
396
+ ---
339
397
 
340
- ### Collection pipeline
398
+ ## Testing
341
399
 
342
- Transform collections before asserting:
400
+ ### Soft assertions
401
+
402
+ Collect all failures instead of stopping at the first one:
343
403
 
344
404
  ```py
345
- orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
405
+ from assertpy2 import assert_that, soft_assertions
346
406
 
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)
407
+ def test_user_profile():
408
+ with soft_assertions():
409
+ assert_that(user.name).is_equal_to("Alice")
410
+ assert_that(user.age).is_greater_than(0)
411
+ assert_that(user.email).contains("@")
412
+ ```
413
+
414
+ All failures are reported at the end of the block:
352
415
 
353
- # chaining pipeline steps
354
- assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
416
+ ```
417
+ AssertionError: soft assertion failures:
418
+ 1. Expected <Bob> to be equal to <Alice>, but was not.
419
+ 2. Expected <-1> to be greater than <0>, but was not.
420
+ 3. Expected <invalid> to contain <@>, but did not.
355
421
  ```
356
422
 
357
- Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
423
+ Use `soft_fail("message")` inside the block for non-halting explicit failures (unlike `fail()`, which stops immediately).
424
+
425
+ Soft assertions are thread-safe and async-safe: each thread and each `asyncio` task gets independent state via `contextvars`.
358
426
 
359
427
  ### Grouped soft assertions
360
428
 
@@ -373,62 +441,75 @@ assert_all(
373
441
  )
374
442
  ```
375
443
 
376
- ### JSON path and schema validation
444
+ ### Async assertions
377
445
 
378
- Requires `pip install assertpy2[json]`.
446
+ Poll a callable until the assertion passes or timeout is reached:
379
447
 
380
448
  ```py
381
- data = {"users": [{"name": "Alice"}, {"name": "Bob"}], "meta": {"total": 2}}
449
+ from assertpy2 import assert_that
382
450
 
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"]})
451
+ async def test_eventual_consistency():
452
+ await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
453
+
454
+ # works with async callables
455
+ await assert_that(async_get_count).eventually().within(10).is_greater_than(100)
387
456
  ```
388
457
 
389
- ### Dict comparison with ignore/include
458
+ Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
390
459
 
391
- ```py
392
- assert_that({"a": 1, "b": 2, "c": 3}).is_equal_to({"a": 1}, ignore=["b", "c"])
393
- assert_that({"a": 1, "b": {"c": 2, "d": 3}}).is_equal_to({"b": {"d": 3}}, include=("b", "d"))
394
- ```
460
+ ### Structured errors
395
461
 
396
- ### Extracting with filter and sort
462
+ When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
397
463
 
398
464
  ```py
399
- users = [
400
- {"user": "Fred", "age": 36, "active": True},
401
- {"user": "Bob", "age": 40, "active": False},
402
- {"user": "Johnny", "age": 13, "active": True},
403
- ]
404
-
405
- assert_that(users).extracting("user", filter="active").is_equal_to(["Fred", "Johnny"])
406
- assert_that(users).extracting("user", sort="age").is_equal_to(["Johnny", "Fred", "Bob"])
465
+ try:
466
+ assert_that(1).is_equal_to(2)
467
+ except AssertionError as e:
468
+ e.actual # 1
469
+ e.expected # 2
407
470
  ```
408
471
 
409
- ### Expected exceptions
472
+ For comparisons, a `DiffResult` with structural diff entries is available:
410
473
 
411
474
  ```py
412
- assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
413
- .is_length(8).starts_with("some").is_equal_to("some err")
475
+ try:
476
+ assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
477
+ except AssertionError as e:
478
+ e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
414
479
  ```
415
480
 
416
- ### Dynamic assertions
481
+ `AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
417
482
 
418
- ```py
419
- fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
483
+ ### Rich pytest diffs
420
484
 
421
- assert_that(fred).has_first_name("Fred")
422
- assert_that(fred).has_last_name("Smith")
423
- assert_that(fred).has_shoe_size(12)
485
+ The pytest plugin (auto-registered, no configuration needed) renders structural diffs with recursive descent:
486
+
487
+ ```
488
+ FAILED test_example.py::test_api
489
+ --- AssertionFailure ---
490
+ actual: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
491
+ expected: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Robert'}]
492
+ --- Structured Diff ---
493
+ diff (sequence):
494
+ [1].name:
495
+ - 'Bob'
496
+ + 'Robert'
424
497
  ```
425
498
 
426
- ### Snapshot testing
499
+ Supported types: list/tuple, set/frozenset, str, dict, dataclass, namedtuple. Nested structures are diffed recursively. Colored output when `--color=yes`.
427
500
 
428
- ```py
429
- assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
501
+ Configure via `pyproject.toml`:
502
+
503
+ ```toml
504
+ [tool.pytest.ini_options]
505
+ assertpy2_diff = "off" # disable diff sections
506
+ assertpy2_diff_max_entries = "100" # max entries shown (default 50, 0 = unlimited)
430
507
  ```
431
508
 
509
+ ---
510
+
511
+ ## Extensibility
512
+
432
513
  ### Custom matchers
433
514
 
434
515
  Register domain-specific matchers on the `match` namespace with `register_matcher()`:
@@ -491,8 +572,11 @@ assert_that(5).is_5()
491
572
 
492
573
  See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
493
574
 
575
+ ---
576
+
577
+ ## Integrations
494
578
 
495
- ## Allure integration
579
+ ### Allure
496
580
 
497
581
  When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
498
582
 
@@ -514,8 +598,7 @@ Three modes controlled via `pytest.ini` (or `pyproject.toml`):
514
598
  assertpy2_allure = "full"
515
599
  ```
516
600
 
517
-
518
- ## Behave step matchers
601
+ ### Behave
519
602
 
520
603
  Ready-made parameter types for Behave step definitions:
521
604
 
@@ -539,6 +622,7 @@ def step_impl(context, age):
539
622
 
540
623
  Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
541
624
 
625
+ ---
542
626
 
543
627
  ## Migration from assertpy
544
628
 
@@ -555,8 +639,8 @@ from assertpy2 import assert_that, soft_assertions
555
639
  See the [comparison table](#comparison) above for feature differences with other libraries.
556
640
 
557
641
 
558
- ## Contributing
559
-
560
- Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
561
-
642
+ ---
562
643
 
644
+ <p align="center">
645
+ <a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
646
+ </p>