assertpy2 2.8.0__tar.gz → 2.9.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.8.0 → assertpy2-2.9.0}/.github/workflows/ci.yml +9 -3
- assertpy2-2.9.0/.github/workflows/mutation.yml +63 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/PKG-INFO +18 -17
- {assertpy2-2.8.0 → assertpy2-2.9.0}/README.md +17 -16
- assertpy2-2.9.0/assertpy2/_introspection.py +66 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_mixin_base.py +12 -4
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_typing.py +7 -2
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/assertpy.py +15 -31
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/async_assertions.py +4 -5
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/base.py +42 -49
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/behave_matchers.py +14 -9
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/collection.py +19 -21
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/contains.py +29 -29
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/date.py +24 -32
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/dict.py +14 -16
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/dynamic.py +7 -6
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/errors.py +3 -4
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/exception.py +4 -4
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/extracting.py +12 -10
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/file.py +10 -6
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/helpers.py +66 -52
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/matchers.py +50 -27
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/numeric.py +49 -61
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/pytest_plugin.py +27 -20
- assertpy2-2.9.0/assertpy2/snapshot.py +232 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/string.py +35 -51
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/assertions.md +19 -3
- assertpy2-2.9.0/docs/assets/diff-equal.png +0 -0
- assertpy2-2.9.0/docs/assets/diff-equal.svg +64 -0
- assertpy2-2.9.0/docs/assets/diff-match.svg +51 -0
- assertpy2-2.9.0/docs/assets/diff-sequence.svg +52 -0
- assertpy2-2.9.0/docs/assets/diff-set.svg +48 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/comparison.md +27 -21
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/errors.md +21 -18
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/extending.md +9 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/getting-started.md +5 -7
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/index.md +13 -8
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/matchers.md +15 -6
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/migration.md +1 -1
- assertpy2-2.9.0/docs/type-safety.md +62 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/mkdocs.yml +1 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/pyproject.toml +14 -7
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_async.py +7 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_collection.py +7 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_core.py +6 -6
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_custom_dict.py +25 -25
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_datetime.py +78 -10
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dict.py +1 -1
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dict_compare.py +39 -5
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_expected_exception.py +4 -2
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_extracting.py +2 -1
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_file.py +9 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_in.py +11 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_list.py +7 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matchers.py +10 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matchers_phase3.py +3 -0
- assertpy2-2.9.0/tests/test_mutation_hardening.py +106 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_namedtuple.py +1 -1
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_numbers.py +30 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_pytest_plugin.py +28 -1
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare.py +5 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_rich_diff.py +7 -2
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_snapshots.py +34 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_structural.py +14 -0
- assertpy2-2.9.0/tests/test_typing.py +50 -0
- assertpy2-2.9.0/uv.lock +2523 -0
- assertpy2-2.8.0/assertpy2/snapshot.py +0 -194
- assertpy2-2.8.0/uv.lock +0 -1359
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.codecov.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.gitattributes +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/.gitignore +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/LICENSE +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/SECURITY.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_compat.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/data.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/fluent.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/integrations.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/logo.svg +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/testing.md +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_bool.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_class.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_description.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dyn.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_equals.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_errors.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_fail.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_json.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_none.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_not.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_phase2.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_readme.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_soft.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_string.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_type.py +0 -0
- {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_warn.py +0 -0
|
@@ -68,7 +68,7 @@ jobs:
|
|
|
68
68
|
python-version: "3.14"
|
|
69
69
|
|
|
70
70
|
- name: Install dependencies
|
|
71
|
-
run: uv sync --extra json
|
|
71
|
+
run: uv sync --extra json --group typecheck
|
|
72
72
|
|
|
73
73
|
- name: Ruff check
|
|
74
74
|
run: uv run ruff check .
|
|
@@ -76,5 +76,11 @@ jobs:
|
|
|
76
76
|
- name: Ruff format check
|
|
77
77
|
run: uv run ruff format --check .
|
|
78
78
|
|
|
79
|
-
- name: Type check
|
|
80
|
-
run: uv run ty check assertpy2/
|
|
79
|
+
- name: Type check (ty)
|
|
80
|
+
run: uv run ty check assertpy2/ tests/test_typing.py
|
|
81
|
+
|
|
82
|
+
- name: Type check (mypy --strict, public typing surface)
|
|
83
|
+
run: uv run mypy --strict --follow-imports=silent tests/test_typing.py
|
|
84
|
+
|
|
85
|
+
- name: Type check (pyright, public typing surface)
|
|
86
|
+
run: uv run pyright tests/test_typing.py
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Mutation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 4 * * 1" # weekly, Monday 04:00 UTC
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
mutation:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 120
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
module: [base, numeric, string, contains, collection, dict, date, matchers, helpers]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
|
+
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.14"
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: uv sync --extra json --group mutation
|
|
38
|
+
|
|
39
|
+
- name: Run mutation testing
|
|
40
|
+
env:
|
|
41
|
+
MODULE: ${{ matrix.module }}
|
|
42
|
+
run: |
|
|
43
|
+
cat > cosmic-ray.toml <<EOF
|
|
44
|
+
[cosmic-ray]
|
|
45
|
+
module-path = "assertpy2/${MODULE}.py"
|
|
46
|
+
timeout = 30.0
|
|
47
|
+
excluded-modules = []
|
|
48
|
+
test-command = ".venv/bin/python -m pytest tests/ -x -q -p no:cacheprovider"
|
|
49
|
+
|
|
50
|
+
[cosmic-ray.distributor]
|
|
51
|
+
name = "local"
|
|
52
|
+
EOF
|
|
53
|
+
uv run cosmic-ray baseline cosmic-ray.toml
|
|
54
|
+
uv run cosmic-ray init cosmic-ray.toml session.sqlite
|
|
55
|
+
uv run cosmic-ray exec cosmic-ray.toml session.sqlite
|
|
56
|
+
{
|
|
57
|
+
echo "### Mutation: ${MODULE}.py"
|
|
58
|
+
echo ""
|
|
59
|
+
echo "Surviving mutants: $(uv run cr-rate session.sqlite)%"
|
|
60
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
61
|
+
echo "::group::Surviving mutants (${MODULE}.py)"
|
|
62
|
+
uv run cr-report session.sqlite
|
|
63
|
+
echo "::endgroup::"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.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
|
|
@@ -66,7 +66,7 @@ Description-Content-Type: text/markdown
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
## Quick start
|
|
69
|
+
## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -84,10 +84,12 @@ def test_user():
|
|
|
84
84
|
assert_that(user).has_name("Alice")
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
88
|
+
|
|
89
|
+
## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
|
|
88
90
|
|
|
89
91
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
90
|
-
offers only the methods that fit the value's type:
|
|
92
|
+
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
91
93
|
|
|
92
94
|
```py
|
|
93
95
|
# bare - three statements, no autocomplete help
|
|
@@ -111,25 +113,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
111
113
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
assertpy2 reports the exact path to every difference:
|
|
116
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
|
|
115
117
|
|
|
116
|
-
```
|
|
118
|
+
```python
|
|
117
119
|
assert_that(response).is_equal_to(expected)
|
|
118
|
-
--- Structured Diff ---
|
|
119
|
-
diff (dict):
|
|
120
|
-
status:
|
|
121
|
-
- 'active'
|
|
122
|
-
+ 'disabled'
|
|
123
|
-
user.role:
|
|
124
|
-
- 'superadmin'
|
|
125
|
-
+ 'admin'
|
|
126
120
|
```
|
|
127
121
|
|
|
122
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
|
|
123
|
+
|
|
128
124
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
129
125
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
130
|
-
`matches_structure()` instead of exact equality.
|
|
126
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
131
127
|
|
|
132
|
-
## Type-aware autocomplete
|
|
128
|
+
## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
|
|
133
129
|
|
|
134
130
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
135
131
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -142,6 +138,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
142
138
|
|
|
143
139
|
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
144
140
|
|
|
141
|
+
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
142
|
+
|
|
145
143
|
---
|
|
146
144
|
|
|
147
145
|
## Features
|
|
@@ -170,6 +168,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
170
168
|
- [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
171
169
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
|
|
172
170
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
|
|
171
|
+
- **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
|
|
173
172
|
|
|
174
173
|
**Type safety**
|
|
175
174
|
|
|
@@ -191,7 +190,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
|
|
|
191
190
|
|
|
192
191
|
---
|
|
193
192
|
|
|
194
|
-
## Integrations
|
|
193
|
+
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
195
194
|
|
|
196
195
|
### Allure
|
|
197
196
|
|
|
@@ -239,6 +238,8 @@ def step_impl(context, age):
|
|
|
239
238
|
|
|
240
239
|
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
241
240
|
|
|
241
|
+
See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
|
|
242
|
+
|
|
242
243
|
---
|
|
243
244
|
|
|
244
245
|
<p align="center">
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
## Quick start
|
|
31
|
+
## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -46,10 +46,12 @@ def test_user():
|
|
|
46
46
|
assert_that(user).has_name("Alice")
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
50
|
+
|
|
51
|
+
## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
|
|
50
52
|
|
|
51
53
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
52
|
-
offers only the methods that fit the value's type:
|
|
54
|
+
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
53
55
|
|
|
54
56
|
```py
|
|
55
57
|
# bare - three statements, no autocomplete help
|
|
@@ -73,25 +75,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
73
75
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
assertpy2 reports the exact path to every difference:
|
|
78
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
|
|
77
79
|
|
|
78
|
-
```
|
|
80
|
+
```python
|
|
79
81
|
assert_that(response).is_equal_to(expected)
|
|
80
|
-
--- Structured Diff ---
|
|
81
|
-
diff (dict):
|
|
82
|
-
status:
|
|
83
|
-
- 'active'
|
|
84
|
-
+ 'disabled'
|
|
85
|
-
user.role:
|
|
86
|
-
- 'superadmin'
|
|
87
|
-
+ 'admin'
|
|
88
82
|
```
|
|
89
83
|
|
|
84
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
|
|
85
|
+
|
|
90
86
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
91
87
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
92
|
-
`matches_structure()` instead of exact equality.
|
|
88
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
93
89
|
|
|
94
|
-
## Type-aware autocomplete
|
|
90
|
+
## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
|
|
95
91
|
|
|
96
92
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
97
93
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -104,6 +100,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
104
100
|
|
|
105
101
|
9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
106
102
|
|
|
103
|
+
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
104
|
+
|
|
107
105
|
---
|
|
108
106
|
|
|
109
107
|
## Features
|
|
@@ -132,6 +130,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
132
130
|
- [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
|
|
133
131
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
|
|
134
132
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
|
|
133
|
+
- **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
|
|
135
134
|
|
|
136
135
|
**Type safety**
|
|
137
136
|
|
|
@@ -153,7 +152,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
|
|
|
153
152
|
|
|
154
153
|
---
|
|
155
154
|
|
|
156
|
-
## Integrations
|
|
155
|
+
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
157
156
|
|
|
158
157
|
### Allure
|
|
159
158
|
|
|
@@ -201,6 +200,8 @@ def step_impl(context, age):
|
|
|
201
200
|
|
|
202
201
|
Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
|
|
203
202
|
|
|
203
|
+
See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
|
|
204
|
+
|
|
204
205
|
---
|
|
205
206
|
|
|
206
207
|
<p align="center">
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Runtime-checkable protocols for duck-typed object introspection.
|
|
2
|
+
|
|
3
|
+
The comparison and diff code accepts arbitrary user objects, so it inspects them structurally: pydantic
|
|
4
|
+
models (``model_dump``), ``attrs`` classes (``__attrs_attrs__``) and namedtuples (``_fields`` /
|
|
5
|
+
``_asdict``). Expressing those shapes as ``@runtime_checkable`` protocols lets the diff code use
|
|
6
|
+
``isinstance`` checks the type checker can follow - narrowing the value instead of probing it with
|
|
7
|
+
``hasattr`` and then suppressing the attribute access.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Protocol, TypeGuard, runtime_checkable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class SupportsModelDump(Protocol):
|
|
17
|
+
"""A pydantic-style model exposing ``model_dump()``."""
|
|
18
|
+
|
|
19
|
+
def model_dump(self) -> dict[str, Any]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class NamedTupleLike(Protocol):
|
|
24
|
+
"""A ``collections.namedtuple`` / ``typing.NamedTuple`` instance."""
|
|
25
|
+
|
|
26
|
+
_fields: tuple[str, ...]
|
|
27
|
+
|
|
28
|
+
def _asdict(self) -> dict[str, Any]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class AttrsInstance(Protocol):
|
|
33
|
+
"""An ``attrs``-decorated instance exposing ``__attrs_attrs__``."""
|
|
34
|
+
|
|
35
|
+
__attrs_attrs__: tuple[Any, ...]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class MappingLike(Protocol):
|
|
40
|
+
"""A dict-like object that can be iterated over and subscripted by key."""
|
|
41
|
+
|
|
42
|
+
def keys(self) -> Any: ...
|
|
43
|
+
|
|
44
|
+
def __iter__(self) -> Any: ...
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: Any) -> Any: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_model_dump_object(obj: object) -> TypeGuard[SupportsModelDump]:
|
|
50
|
+
"""Return whether ``obj`` exposes a callable ``model_dump()`` (e.g. a pydantic model)."""
|
|
51
|
+
return isinstance(obj, SupportsModelDump) and callable(obj.model_dump)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_namedtuple(obj: object) -> TypeGuard[NamedTupleLike]:
|
|
55
|
+
"""Return whether ``obj`` is a namedtuple instance (a ``tuple`` carrying ``_fields``/``_asdict``)."""
|
|
56
|
+
return isinstance(obj, tuple) and isinstance(obj, NamedTupleLike)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_attrs_instance(obj: object) -> TypeGuard[AttrsInstance]:
|
|
60
|
+
"""Return whether ``obj`` is an ``attrs``-decorated instance."""
|
|
61
|
+
return isinstance(obj, AttrsInstance)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_mapping_like(obj: object) -> TypeGuard[MappingLike]:
|
|
65
|
+
"""Return whether ``obj`` is dict-like: iterable with ``keys()`` and ``[]`` access."""
|
|
66
|
+
return isinstance(obj, MappingLike) and callable(obj.keys)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
from typing import TYPE_CHECKING, Any
|
|
5
4
|
|
|
6
5
|
if TYPE_CHECKING:
|
|
6
|
+
import logging
|
|
7
|
+
|
|
7
8
|
from ._compat import Self
|
|
8
9
|
from .errors import DiffResult
|
|
9
10
|
|
|
@@ -57,15 +58,22 @@ class _MixinBase:
|
|
|
57
58
|
|
|
58
59
|
def _validate_close_to_args(self, val: object, other: object, tolerance: object) -> None: ...
|
|
59
60
|
|
|
60
|
-
def
|
|
61
|
+
def _is_dict_like(
|
|
62
|
+
self,
|
|
63
|
+
d: object,
|
|
64
|
+
check_keys: bool = ...,
|
|
65
|
+
check_values: bool = ...,
|
|
66
|
+
check_getitem: bool = ...,
|
|
67
|
+
) -> bool: ...
|
|
68
|
+
|
|
69
|
+
def _require_dict_like(
|
|
61
70
|
self,
|
|
62
71
|
d: object,
|
|
63
72
|
check_keys: bool = ...,
|
|
64
73
|
check_values: bool = ...,
|
|
65
74
|
check_getitem: bool = ...,
|
|
66
75
|
name: str = ...,
|
|
67
|
-
|
|
68
|
-
) -> bool | None: ...
|
|
76
|
+
) -> None: ...
|
|
69
77
|
|
|
70
78
|
def _check_iterable(self, val: object, check_getitem: bool = ..., name: str = ...) -> None: ...
|
|
71
79
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Hashable
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Protocol
|
|
9
9
|
|
|
@@ -12,12 +12,17 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from .async_assertions import AsyncAssertionBuilder
|
|
13
13
|
from .matchers import Matcher
|
|
14
14
|
|
|
15
|
+
# ``ignore``/``include`` accept a single key, a nested-path tuple, or a list/set/frozenset of them.
|
|
16
|
+
_KeySpecs = Hashable | list[Hashable] | set[Hashable] | frozenset[Hashable]
|
|
17
|
+
|
|
15
18
|
class _CoreAssertion(Protocol):
|
|
16
19
|
"""Base protocol with assertions available for all types."""
|
|
17
20
|
|
|
18
21
|
# BaseMixin
|
|
19
22
|
def described_as(self, description: str) -> Self: ...
|
|
20
|
-
def is_equal_to(
|
|
23
|
+
def is_equal_to(
|
|
24
|
+
self, other: object, *, ignore: _KeySpecs | None = ..., include: _KeySpecs | None = ...
|
|
25
|
+
) -> Self: ...
|
|
21
26
|
def is_not_equal_to(self, other: object) -> Self: ...
|
|
22
27
|
def is_same_as(self, other: object) -> Self: ...
|
|
23
28
|
def is_not_same_as(self, other: object) -> Self: ...
|
|
@@ -9,17 +9,18 @@ import logging
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
import types
|
|
12
|
-
from
|
|
13
|
-
from typing import TYPE_CHECKING, overload
|
|
12
|
+
from typing import TYPE_CHECKING, Final, overload
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
15
|
import datetime
|
|
17
16
|
import pathlib
|
|
17
|
+
from collections.abc import Callable, Iterator
|
|
18
18
|
|
|
19
19
|
from ._compat import Self
|
|
20
20
|
from ._typing import (
|
|
21
21
|
_BytesAssertion,
|
|
22
22
|
_CallableAssertion,
|
|
23
|
+
_CoreAssertion,
|
|
23
24
|
_DateAssertion,
|
|
24
25
|
_DictAssertion,
|
|
25
26
|
_IterableAssertion,
|
|
@@ -47,37 +48,15 @@ from .snapshot import SnapshotMixin
|
|
|
47
48
|
from .string import StringMixin
|
|
48
49
|
from .warning import WarningMixin
|
|
49
50
|
|
|
50
|
-
__version__ = "2.
|
|
51
|
+
__version__ = "2.9.0"
|
|
51
52
|
|
|
52
53
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
53
54
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
54
55
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for
|
|
59
|
-
"assertpy.py",
|
|
60
|
-
"async_assertions.py",
|
|
61
|
-
"base.py",
|
|
62
|
-
"bytes_mixin.py",
|
|
63
|
-
"collection.py",
|
|
64
|
-
"contains.py",
|
|
65
|
-
"date.py",
|
|
66
|
-
"dict.py",
|
|
67
|
-
"dynamic.py",
|
|
68
|
-
"errors.py",
|
|
69
|
-
"exception.py",
|
|
70
|
-
"extracting.py",
|
|
71
|
-
"file.py",
|
|
72
|
-
"helpers.py",
|
|
73
|
-
"json_mixin.py",
|
|
74
|
-
"matchers.py",
|
|
75
|
-
"numeric.py",
|
|
76
|
-
"pytest_plugin.py",
|
|
77
|
-
"snapshot.py",
|
|
78
|
-
"string.py",
|
|
79
|
-
"warning.py",
|
|
80
|
-
]
|
|
56
|
+
# assertpy2 source files, used to strip internal frames when locating the caller for warn-mode messages.
|
|
57
|
+
# Derived from the package directory so new modules are covered automatically (no hand-maintained list).
|
|
58
|
+
ASSERTPY_FILES: Final = [
|
|
59
|
+
os.path.join("assertpy2", name) for name in os.listdir(os.path.dirname(__file__)) if name.endswith(".py")
|
|
81
60
|
]
|
|
82
61
|
|
|
83
62
|
# soft assertions (contextvars for thread/async safety)
|
|
@@ -253,11 +232,16 @@ def assert_that(val: bytes | bytearray, description: str = "") -> _BytesAssertio
|
|
|
253
232
|
def assert_that(val: Callable[..., object], description: str = "") -> _CallableAssertion: ...
|
|
254
233
|
|
|
255
234
|
|
|
235
|
+
# Fallback returns the concrete AssertionBuilder so object- and union-typed values keep the full API.
|
|
236
|
+
# The specific protocols are not assignable to AssertionBuilder, so mypy --strict reports overload-overlap
|
|
237
|
+
# for each specific overload and pyright one reportOverlappingOverload; ty (the gate) does not flag it. Kept
|
|
238
|
+
# intentionally - returning _CoreAssertion here would strip type-specific assertions from object/union values.
|
|
256
239
|
@overload
|
|
257
240
|
def assert_that(val: object, description: str = "") -> AssertionBuilder: ...
|
|
258
241
|
|
|
259
242
|
|
|
260
|
-
|
|
243
|
+
# Return the common base protocol so each overload stays consistent with the impl (no reportInconsistentOverload).
|
|
244
|
+
def assert_that(val, description="") -> _CoreAssertion:
|
|
261
245
|
"""Set the value to be tested, plus an optional description, and allow assertions to be called.
|
|
262
246
|
|
|
263
247
|
This is a factory method for the :class:`AssertionBuilder`, and the single most important
|
|
@@ -613,7 +597,7 @@ class AssertionBuilder(
|
|
|
613
597
|
def error(self, msg, *, actual=None, expected=None, diff=None) -> Self:
|
|
614
598
|
"""Helper to raise an ``AssertionError`` with the given message.
|
|
615
599
|
|
|
616
|
-
If an error description is set by :meth:`~
|
|
600
|
+
If an error description is set by :meth:`~assertpy2.base.BaseMixin.described_as`, then that
|
|
617
601
|
description is prepended to the error message.
|
|
618
602
|
|
|
619
603
|
When structured data (``actual``, ``expected``, or ``diff``) is provided, raises
|
|
@@ -61,15 +61,14 @@ class AsyncAssertionBuilder:
|
|
|
61
61
|
last_error: AssertionError | None = None
|
|
62
62
|
while True:
|
|
63
63
|
try:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
val = self._func()
|
|
64
|
+
val = self._func()
|
|
65
|
+
if inspect.isawaitable(val):
|
|
66
|
+
val = await val
|
|
68
67
|
builder = self._builder_func(val, self._description)
|
|
69
68
|
method = getattr(builder, name)
|
|
70
69
|
method(*args, **kwargs)
|
|
71
70
|
return builder
|
|
72
|
-
except AssertionError as exc:
|
|
71
|
+
except AssertionError as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
|
|
73
72
|
last_error = exc
|
|
74
73
|
if loop.time() >= deadline:
|
|
75
74
|
raise AssertionError(
|