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.
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/dependabot.yml +5 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/ci.yml +7 -3
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/codeql.yml +4 -2
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/publish.yml +11 -3
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.github/workflows/scorecard.yml +1 -1
- assertpy2-2.3.8/.github/workflows/zizmor.yml +21 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/PKG-INFO +218 -113
- {assertpy2-2.3.6 → assertpy2-2.3.8}/README.md +213 -111
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/__init__.py +4 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/_typing.py +30 -1
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/assertpy.py +91 -8
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/base.py +117 -1
- assertpy2-2.3.8/assertpy2/bytes_mixin.py +142 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/contains.py +22 -3
- assertpy2-2.3.8/assertpy2/json_mixin.py +163 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/pytest_plugin.py +77 -2
- {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/api.md +769 -454
- {assertpy2-2.3.6 → assertpy2-2.3.8}/pyproject.toml +4 -3
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_behave_matchers.py +23 -20
- assertpy2-2.3.8/tests/test_bytes.py +147 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_errors.py +1 -1
- assertpy2-2.3.8/tests/test_grouped_soft.py +130 -0
- assertpy2-2.3.8/tests/test_json.py +172 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matcher_registry.py +2 -2
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matchers_phase3.py +53 -53
- assertpy2-2.3.8/tests/test_overloads.py +58 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_pytest_plugin.py +149 -63
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_regex_groups.py +1 -5
- assertpy2-2.3.8/tests/test_rich_diff.py +427 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_snapshots.py +3 -3
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_warn.py +1 -1
- assertpy2-2.3.8/uv.lock +792 -0
- assertpy2-2.3.6/tests/test_overloads.py +0 -71
- assertpy2-2.3.6/uv.lock +0 -465
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.codecov.yml +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/.gitignore +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/CONTRIBUTING.md +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/LICENSE +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/SECURITY.md +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/collection.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/date.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/dict.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/exception.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/extracting.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/file.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/helpers.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/matchers.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/numeric.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/assertpy2/string.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/docs/logo.svg +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_async.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_bool.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_class.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_collection.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_core.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_custom_list.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_datetime.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_description.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dict.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_dyn.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_equals.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_extensions.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_extracting.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_fail.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_file.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_in.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_list.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_matchers.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_none.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_not.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_numbers.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_pipeline.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_readme.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_same_as.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_soft.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_string.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_structural.py +0 -0
- {assertpy2-2.3.6 → assertpy2-2.3.8}/tests/test_traceback.py +0 -0
- {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@
|
|
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
|
-
|
|
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@
|
|
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 "$
|
|
49
|
-
cp "$
|
|
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 "$
|
|
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@
|
|
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.
|
|
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
|
-
|
|
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
|
|
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** |
|
|
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()
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Built-in types
|
|
347
|
+
|
|
348
|
+
### Bytes assertions
|
|
349
|
+
|
|
350
|
+
Assert on `bytes` and `bytearray` values:
|
|
383
351
|
|
|
384
352
|
```py
|
|
385
|
-
|
|
353
|
+
data = b"\x89PNG\r\n\x1a\n"
|
|
386
354
|
|
|
387
|
-
assert_that(
|
|
388
|
-
assert_that(
|
|
389
|
-
assert_that(
|
|
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
|
-
##
|
|
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
|
-
|
|
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>
|