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.
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/dependabot.yml +5 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/ci.yml +7 -3
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/codeql.yml +4 -2
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/publish.yml +11 -3
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.github/workflows/scorecard.yml +1 -1
- assertpy2-2.4.0/.github/workflows/zizmor.yml +21 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/PKG-INFO +212 -128
- {assertpy2-2.3.7 → assertpy2-2.4.0}/README.md +211 -127
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/_typing.py +16 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/assertpy.py +9 -1
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/base.py +117 -1
- assertpy2-2.4.0/assertpy2/bytes_mixin.py +142 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/contains.py +22 -3
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/pytest_plugin.py +77 -2
- {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/api.md +746 -543
- {assertpy2-2.3.7 → assertpy2-2.4.0}/pyproject.toml +1 -1
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_behave_matchers.py +23 -20
- assertpy2-2.4.0/tests/test_bytes.py +147 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_errors.py +1 -1
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_json.py +3 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matcher_registry.py +2 -2
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matchers_phase3.py +53 -53
- assertpy2-2.4.0/tests/test_overloads.py +58 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_pytest_plugin.py +149 -63
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_regex_groups.py +1 -5
- assertpy2-2.4.0/tests/test_rich_diff.py +427 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_snapshots.py +3 -3
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_warn.py +1 -1
- assertpy2-2.3.7/tests/test_overloads.py +0 -71
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.codecov.yml +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/.gitignore +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/LICENSE +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/SECURITY.md +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/date.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/file.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/helpers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/assertpy2/string.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/docs/logo.svg +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_async.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_bool.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_class.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_collection.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_core.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_description.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dict.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_equals.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_fail.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_file.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_in.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_list.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_none.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_not.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_phase2.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_readme.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_soft.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_string.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_structural.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.3.7 → assertpy2-2.4.0}/tests/test_type.py +0 -0
- {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@
|
|
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
|
+
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
|
|
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").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
135
|
+
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
136
|
+
- `assert_that(Path("/tmp")).` → path methods: `exists`, `is_file`, `is_readable`, ...
|
|
137
|
+
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
138
|
+
- `assert_that(b"\x89PNG").` → 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 | **
|
|
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** |
|
|
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()
|
|
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
|
-
|
|
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
|
-
##
|
|
246
|
-
|
|
247
|
-
Poll a callable until the assertion passes or timeout is reached:
|
|
286
|
+
## Fluent API
|
|
248
287
|
|
|
249
|
-
|
|
250
|
-
from assertpy2 import assert_that
|
|
288
|
+
### Universal negation
|
|
251
289
|
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
299
|
+
Works with soft assertions and warn mode.
|
|
261
300
|
|
|
262
|
-
|
|
301
|
+
### Collection pipeline
|
|
263
302
|
|
|
264
|
-
|
|
303
|
+
Transform collections before asserting:
|
|
265
304
|
|
|
266
305
|
```py
|
|
267
|
-
|
|
306
|
+
orders = [Order("DONE", 100), Order("FAILED", 50), Order("DONE", 200)]
|
|
268
307
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
### Dynamic assertions
|
|
293
341
|
|
|
294
342
|
```py
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
350
|
+
### Expected exceptions
|
|
303
351
|
|
|
304
352
|
```py
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
357
|
+
---
|
|
312
358
|
|
|
313
|
-
|
|
359
|
+
## Built-in types
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
377
|
+
### JSON path and schema validation
|
|
326
378
|
|
|
327
|
-
|
|
379
|
+
Requires `pip install assertpy2[json]`.
|
|
328
380
|
|
|
329
|
-
|
|
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(
|
|
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
|
-
|
|
396
|
+
---
|
|
339
397
|
|
|
340
|
-
|
|
398
|
+
## Testing
|
|
341
399
|
|
|
342
|
-
|
|
400
|
+
### Soft assertions
|
|
401
|
+
|
|
402
|
+
Collect all failures instead of stopping at the first one:
|
|
343
403
|
|
|
344
404
|
```py
|
|
345
|
-
|
|
405
|
+
from assertpy2 import assert_that, soft_assertions
|
|
346
406
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
assert_that(
|
|
350
|
-
assert_that(
|
|
351
|
-
assert_that(
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
444
|
+
### Async assertions
|
|
377
445
|
|
|
378
|
-
|
|
446
|
+
Poll a callable until the assertion passes or timeout is reached:
|
|
379
447
|
|
|
380
448
|
```py
|
|
381
|
-
|
|
449
|
+
from assertpy2 import assert_that
|
|
382
450
|
|
|
383
|
-
|
|
384
|
-
assert_that(
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
458
|
+
Any assertion method is available after `eventually()`. Only `AssertionError` is retried, other exceptions propagate immediately.
|
|
390
459
|
|
|
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
|
-
```
|
|
460
|
+
### Structured errors
|
|
395
461
|
|
|
396
|
-
|
|
462
|
+
When assertions fail, `AssertionFailure` carries structured data alongside the human-readable message:
|
|
397
463
|
|
|
398
464
|
```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"])
|
|
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
|
-
|
|
472
|
+
For comparisons, a `DiffResult` with structural diff entries is available:
|
|
410
473
|
|
|
411
474
|
```py
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
481
|
+
`AssertionFailure` is a subclass of `AssertionError`, so all existing `except AssertionError` handlers work unchanged.
|
|
417
482
|
|
|
418
|
-
|
|
419
|
-
fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
|
|
483
|
+
### Rich pytest diffs
|
|
420
484
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
499
|
+
Supported types: list/tuple, set/frozenset, str, dict, dataclass, namedtuple. Nested structures are diffed recursively. Colored output when `--color=yes`.
|
|
427
500
|
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|