assertpy2 2.3.7__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.7 → assertpy2-2.3.8}/.github/dependabot.yml +5 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.github/workflows/ci.yml +7 -3
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.github/workflows/codeql.yml +4 -2
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.github/workflows/publish.yml +11 -3
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.github/workflows/scorecard.yml +1 -1
- assertpy2-2.3.8/.github/workflows/zizmor.yml +21 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/PKG-INFO +198 -127
- {assertpy2-2.3.7 → assertpy2-2.3.8}/README.md +197 -126
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/_typing.py +16 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/assertpy.py +9 -1
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/base.py +117 -1
- assertpy2-2.3.8/assertpy2/bytes_mixin.py +142 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/contains.py +22 -3
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/pytest_plugin.py +77 -2
- {assertpy2-2.3.7 → assertpy2-2.3.8}/docs/api.md +746 -543
- {assertpy2-2.3.7 → assertpy2-2.3.8}/pyproject.toml +1 -1
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_behave_matchers.py +23 -20
- assertpy2-2.3.8/tests/test_bytes.py +147 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_errors.py +1 -1
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_json.py +3 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_matcher_registry.py +2 -2
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_matchers_phase3.py +53 -53
- assertpy2-2.3.8/tests/test_overloads.py +58 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_pytest_plugin.py +149 -63
- {assertpy2-2.3.7 → 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.7 → assertpy2-2.3.8}/tests/test_snapshots.py +3 -3
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_warn.py +1 -1
- assertpy2-2.3.7/tests/test_overloads.py +0 -71
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.codecov.yml +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/.gitignore +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/CONTRIBUTING.md +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/LICENSE +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/SECURITY.md +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/__init__.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/collection.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/date.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/exception.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/extracting.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/file.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/helpers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/numeric.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/assertpy2/string.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/docs/logo.svg +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_async.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_bool.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_class.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_collection.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_core.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_custom_list.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_datetime.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_description.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_dyn.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_equals.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_extensions.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_extracting.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_fail.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_file.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_in.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_list.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_none.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_not.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_numbers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_pipeline.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_readme.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_same_as.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_soft.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_string.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_structural.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_traceback.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/tests/test_type.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.3.8}/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@
|
|
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,6 +1,6 @@
|
|
|
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
|
|
@@ -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
|
|
104
|
+
Structured errors with rich diffs:
|
|
104
105
|
|
|
105
106
|
```py
|
|
106
107
|
try:
|
|
@@ -118,8 +119,14 @@ 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
|
+
---
|
|
123
130
|
|
|
124
131
|
## Comparison
|
|
125
132
|
|
|
@@ -135,7 +142,7 @@ FAILED test_example.py::test_comparison
|
|
|
135
142
|
| **Async assertions** | No | No | No | **eventually() with polling** |
|
|
136
143
|
| **Soft assertions** | No | No | Yes (not thread-safe) | **Yes (thread-safe, async-safe)** |
|
|
137
144
|
| **Structured errors** | Rewrite only | Mismatch string | String only | **.actual .expected .diff** |
|
|
138
|
-
| **Maintained** |
|
|
145
|
+
| **Maintained** | Built-in | Minimal | 2020 | **Active** |
|
|
139
146
|
|
|
140
147
|
</div>
|
|
141
148
|
|
|
@@ -160,32 +167,52 @@ assert "admin" in items
|
|
|
160
167
|
assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
161
168
|
```
|
|
162
169
|
|
|
170
|
+
---
|
|
163
171
|
|
|
164
172
|
## Features
|
|
165
173
|
|
|
174
|
+
**Fluent API**
|
|
175
|
+
|
|
166
176
|
- [**Composable matchers**](docs/api.md#composable-matchers): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
167
177
|
- [**Structural matching**](docs/api.md#structural-matching): `matches_structure()` for declarative dict/API response validation.
|
|
168
178
|
- [**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()
|
|
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`).
|
|
179
|
+
- [**Collection pipeline**](docs/api.md#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
176
180
|
- [**Fluent chaining**](docs/api.md#chaining): write assertions as readable one-liners that chain naturally.
|
|
177
|
-
|
|
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.
|
|
178
188
|
- [**Dict comparison**](docs/api.md#dicts): `is_equal_to()` with `ignore` and `include` for selective key matching.
|
|
179
189
|
- [**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
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
|
+
---
|
|
189
216
|
|
|
190
217
|
## Composable matchers
|
|
191
218
|
|
|
@@ -241,120 +268,148 @@ assert_that(api_response).matches_structure({
|
|
|
241
268
|
})
|
|
242
269
|
```
|
|
243
270
|
|
|
271
|
+
---
|
|
244
272
|
|
|
245
|
-
##
|
|
273
|
+
## Fluent API
|
|
246
274
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```py
|
|
250
|
-
from assertpy2 import assert_that
|
|
275
|
+
### Universal negation
|
|
251
276
|
|
|
252
|
-
|
|
253
|
-
await assert_that(get_status).eventually().within(5).every(0.5).is_equal_to("ready")
|
|
277
|
+
Invert any assertion with `.not_`:
|
|
254
278
|
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
```py
|
|
280
|
+
assert_that(5).not_.is_none()
|
|
281
|
+
assert_that("abc123").not_.is_alpha()
|
|
282
|
+
assert_that([3, 1, 2]).not_.is_sorted()
|
|
283
|
+
assert_that(value).described_as("check").not_.is_none().is_positive()
|
|
257
284
|
```
|
|
258
285
|
|
|
259
|
-
|
|
260
|
-
|
|
286
|
+
Works with soft assertions and warn mode.
|
|
261
287
|
|
|
262
|
-
|
|
288
|
+
### Collection pipeline
|
|
263
289
|
|
|
264
|
-
|
|
290
|
+
Transform collections before asserting:
|
|
265
291
|
|
|
266
292
|
```py
|
|
267
|
-
|
|
293
|
+
orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
|
|
268
294
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
assert_that(orders).filtered_on(lambda o: o.status == "FAILED").is_length(1)
|
|
296
|
+
assert_that(orders).mapped(lambda o: o.total).contains(100, 200)
|
|
297
|
+
assert_that(orders).first().has_status("DONE")
|
|
298
|
+
assert_that(orders).element(1).has_status("FAILED")
|
|
299
|
+
assert_that([42]).single().is_equal_to(42)
|
|
300
|
+
|
|
301
|
+
# chaining pipeline steps
|
|
302
|
+
assert_that(items).filtered_on(match.is_positive()).mapped(str).contains("1")
|
|
274
303
|
```
|
|
275
304
|
|
|
276
|
-
|
|
305
|
+
Available methods: `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
277
306
|
|
|
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
|
-
```
|
|
307
|
+
### Dict comparison with ignore/include
|
|
284
308
|
|
|
285
|
-
|
|
309
|
+
```py
|
|
310
|
+
assert_that({"a": 1, "b": 2, "c": 3}).is_equal_to({"a": 1}, ignore=["b", "c"])
|
|
311
|
+
assert_that({"a": 1, "b": {"c": 2, "d": 3}}).is_equal_to({"b": {"d": 3}}, include=("b", "d"))
|
|
312
|
+
```
|
|
286
313
|
|
|
287
|
-
|
|
314
|
+
### Extracting with filter and sort
|
|
288
315
|
|
|
316
|
+
```py
|
|
317
|
+
users = [
|
|
318
|
+
{"user": "Fred", "age": 36, "active": True},
|
|
319
|
+
{"user": "Bob", "age": 40, "active": False},
|
|
320
|
+
{"user": "Johnny", "age": 13, "active": True},
|
|
321
|
+
]
|
|
289
322
|
|
|
290
|
-
|
|
323
|
+
assert_that(users).extracting("user", filter="active").is_equal_to(["Fred", "Johnny"])
|
|
324
|
+
assert_that(users).extracting("user", sort="age").is_equal_to(["Johnny", "Fred", "Bob"])
|
|
325
|
+
```
|
|
291
326
|
|
|
292
|
-
|
|
327
|
+
### Dynamic assertions
|
|
293
328
|
|
|
294
329
|
```py
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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)
|
|
300
335
|
```
|
|
301
336
|
|
|
302
|
-
|
|
337
|
+
### Expected exceptions
|
|
303
338
|
|
|
304
339
|
```py
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
except AssertionError as e:
|
|
308
|
-
e.diff # DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
|
|
340
|
+
assert_that(some_func).raises(RuntimeError).when_called_with("bad_arg")\
|
|
341
|
+
.is_length(8).starts_with("some").is_equal_to("some err")
|
|
309
342
|
```
|
|
310
343
|
|
|
311
|
-
|
|
344
|
+
---
|
|
312
345
|
|
|
313
|
-
|
|
346
|
+
## Built-in types
|
|
314
347
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
348
|
+
### Bytes assertions
|
|
349
|
+
|
|
350
|
+
Assert on `bytes` and `bytearray` values:
|
|
351
|
+
|
|
352
|
+
```py
|
|
353
|
+
data = b"\x89PNG\r\n\x1a\n"
|
|
354
|
+
|
|
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")
|
|
322
360
|
```
|
|
323
361
|
|
|
362
|
+
Available methods: `is_valid_utf8()`, `is_valid_encoding()`, `starts_with_bytes()`, `contains_bytes()`, `has_byte_at()`, `is_hex_equal_to()`, `decoded_as()`.
|
|
324
363
|
|
|
325
|
-
|
|
364
|
+
### JSON path and schema validation
|
|
326
365
|
|
|
327
|
-
|
|
366
|
+
Requires `pip install assertpy2[json]`.
|
|
328
367
|
|
|
329
|
-
|
|
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"]})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Snapshot testing
|
|
330
378
|
|
|
331
379
|
```py
|
|
332
|
-
assert_that(
|
|
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()
|
|
380
|
+
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
336
381
|
```
|
|
337
382
|
|
|
338
|
-
|
|
383
|
+
---
|
|
339
384
|
|
|
340
|
-
|
|
385
|
+
## Testing
|
|
341
386
|
|
|
342
|
-
|
|
387
|
+
### Soft assertions
|
|
388
|
+
|
|
389
|
+
Collect all failures instead of stopping at the first one:
|
|
343
390
|
|
|
344
391
|
```py
|
|
345
|
-
|
|
392
|
+
from assertpy2 import assert_that, soft_assertions
|
|
346
393
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
assert_that(
|
|
350
|
-
assert_that(
|
|
351
|
-
assert_that(
|
|
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:
|
|
352
402
|
|
|
353
|
-
|
|
354
|
-
|
|
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.
|
|
355
408
|
```
|
|
356
409
|
|
|
357
|
-
|
|
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`.
|
|
358
413
|
|
|
359
414
|
### Grouped soft assertions
|
|
360
415
|
|
|
@@ -373,62 +428,75 @@ assert_all(
|
|
|
373
428
|
)
|
|
374
429
|
```
|
|
375
430
|
|
|
376
|
-
###
|
|
431
|
+
### Async assertions
|
|
377
432
|
|
|
378
|
-
|
|
433
|
+
Poll a callable until the assertion passes or timeout is reached:
|
|
379
434
|
|
|
380
435
|
```py
|
|
381
|
-
|
|
436
|
+
from assertpy2 import assert_that
|
|
382
437
|
|
|
383
|
-
|
|
384
|
-
assert_that(
|
|
385
|
-
|
|
386
|
-
|
|
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)
|
|
387
443
|
```
|
|
388
444
|
|
|
389
|
-
|
|
445
|
+
Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
|
|
390
446
|
|
|
391
|
-
|
|
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
|
-
```
|
|
447
|
+
### Structured errors
|
|
395
448
|
|
|
396
|
-
|
|
449
|
+
When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
|
|
397
450
|
|
|
398
451
|
```py
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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"])
|
|
452
|
+
try:
|
|
453
|
+
assert_that(1).is_equal_to(2)
|
|
454
|
+
except AssertionError as e:
|
|
455
|
+
e.actual # 1
|
|
456
|
+
e.expected # 2
|
|
407
457
|
```
|
|
408
458
|
|
|
409
|
-
|
|
459
|
+
For comparisons, a `DiffResult` with structural diff entries is available:
|
|
410
460
|
|
|
411
461
|
```py
|
|
412
|
-
|
|
413
|
-
|
|
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)])
|
|
414
466
|
```
|
|
415
467
|
|
|
416
|
-
|
|
468
|
+
`AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
|
|
417
469
|
|
|
418
|
-
|
|
419
|
-
|
|
470
|
+
### Rich pytest diffs
|
|
471
|
+
|
|
472
|
+
The pytest plugin (auto-registered, no configuration needed) renders structural diffs with recursive descent:
|
|
420
473
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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'
|
|
424
484
|
```
|
|
425
485
|
|
|
426
|
-
|
|
486
|
+
Supported types: list/tuple, set/frozenset, str, dict, dataclass, namedtuple. Nested structures are diffed recursively. Colored output when `--color=yes`.
|
|
427
487
|
|
|
428
|
-
|
|
429
|
-
|
|
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)
|
|
430
494
|
```
|
|
431
495
|
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Extensibility
|
|
499
|
+
|
|
432
500
|
### Custom matchers
|
|
433
501
|
|
|
434
502
|
Register domain-specific matchers on the `match` namespace with `register_matcher()`:
|
|
@@ -491,8 +559,11 @@ assert_that(5).is_5()
|
|
|
491
559
|
|
|
492
560
|
See the [full API reference](docs/api.md) for all assertion methods, examples, and advanced features.
|
|
493
561
|
|
|
562
|
+
---
|
|
494
563
|
|
|
495
|
-
##
|
|
564
|
+
## Integrations
|
|
565
|
+
|
|
566
|
+
### Allure
|
|
496
567
|
|
|
497
568
|
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
498
569
|
|
|
@@ -514,8 +585,7 @@ Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
|
514
585
|
assertpy2_allure = "full"
|
|
515
586
|
```
|
|
516
587
|
|
|
517
|
-
|
|
518
|
-
## Behave step matchers
|
|
588
|
+
### Behave
|
|
519
589
|
|
|
520
590
|
Ready-made parameter types for Behave step definitions:
|
|
521
591
|
|
|
@@ -539,6 +609,7 @@ def step_impl(context, age):
|
|
|
539
609
|
|
|
540
610
|
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
541
611
|
|
|
612
|
+
---
|
|
542
613
|
|
|
543
614
|
## Migration from assertpy
|
|
544
615
|
|
|
@@ -555,8 +626,8 @@ from assertpy2 import assert_that, soft_assertions
|
|
|
555
626
|
See the [comparison table](#comparison) above for feature differences with other libraries.
|
|
556
627
|
|
|
557
628
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
561
|
-
|
|
629
|
+
---
|
|
562
630
|
|
|
631
|
+
<p align="center">
|
|
632
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
|
|
633
|
+
</p>
|