assertpy2 2.8.1__tar.gz → 2.9.1__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.1 → assertpy2-2.9.1}/.github/workflows/ci.yml +9 -3
- assertpy2-2.9.1/.github/workflows/mutation.yml +63 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/PKG-INFO +4 -2
- {assertpy2-2.8.1 → assertpy2-2.9.1}/README.md +3 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/_mixin_base.py +10 -3
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/_typing.py +7 -2
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/assertpy.py +5 -27
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/async_assertions.py +3 -4
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/base.py +8 -8
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/collection.py +12 -14
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/contains.py +18 -12
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/date.py +24 -32
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/dict.py +8 -8
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/dynamic.py +1 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/exception.py +1 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/extracting.py +8 -4
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/file.py +8 -4
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/helpers.py +54 -40
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/matchers.py +58 -48
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/numeric.py +49 -61
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/pytest_plugin.py +12 -6
- assertpy2-2.9.1/assertpy2/snapshot.py +232 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/string.py +28 -46
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assertions.md +4 -2
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/comparison.md +1 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/type-safety.md +8 -2
- {assertpy2-2.8.1 → assertpy2-2.9.1}/pyproject.toml +11 -4
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_async.py +7 -0
- assertpy2-2.9.1/tests/test_callable.py +48 -0
- assertpy2-2.9.1/tests/test_chaining.py +22 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_collection.py +7 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_core.py +6 -6
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_custom_dict.py +25 -25
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_datetime.py +133 -2
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_dict.py +1 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_dict_compare.py +29 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_expected_exception.py +50 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_extracting.py +2 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_file.py +9 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_in.py +11 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_list.py +77 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_matchers.py +278 -0
- assertpy2-2.9.1/tests/test_mutation_hardening.py +106 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_namedtuple.py +1 -1
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_numbers.py +30 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_pytest_plugin.py +9 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_recursive_compare.py +5 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_rich_diff.py +7 -2
- assertpy2-2.9.1/tests/test_satisfy.py +93 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_snapshots.py +34 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_structural.py +24 -0
- assertpy2-2.9.1/tests/test_typing.py +50 -0
- assertpy2-2.9.1/uv.lock +2523 -0
- assertpy2-2.8.1/assertpy2/snapshot.py +0 -197
- assertpy2-2.8.1/tests/test_matchers_phase3.py +0 -266
- assertpy2-2.8.1/tests/test_phase2.py +0 -349
- assertpy2-2.8.1/uv.lock +0 -1359
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.codecov.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.gitattributes +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/dependabot.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/.gitignore +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/CONTRIBUTING.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/LICENSE +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/SECURITY.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/__init__.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/_compat.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/errors.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/py.typed +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/assertpy2/warning.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/data.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/errors.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/extending.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/fluent.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/getting-started.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/index.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/integrations.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/logo-dark.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/logo.svg +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/matchers.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/migration.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/docs/testing.md +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/mkdocs.yml +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_bool.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_bytes.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_class.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_custom_list.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_description.py +0 -0
- /assertpy2-2.8.1/tests/test_dyn.py → /assertpy2-2.9.1/tests/test_dynamic.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_equals.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_errors.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_extensions.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_fail.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_json.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_none.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_not.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_overloads.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_pipeline.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_property_based.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_readme.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_same_as.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_soft.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_string.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_traceback.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/tests/test_type.py +0 -0
- {assertpy2-2.8.1 → assertpy2-2.9.1}/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.1
|
|
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
|
|
@@ -62,6 +62,8 @@ Description-Content-Type: text/markdown
|
|
|
62
62
|
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
|
|
63
63
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
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
|
+
<br>
|
|
66
|
+
<a href="https://solganis.github.io/assertpy2/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
|
|
65
67
|
</p>
|
|
66
68
|
|
|
67
69
|
---
|
|
@@ -119,7 +121,7 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
|
|
|
119
121
|
assert_that(response).is_equal_to(expected)
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
<img src="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">
|
|
124
|
+
<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
125
|
|
|
124
126
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
125
127
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
|
|
25
25
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
26
26
|
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
27
|
+
<br>
|
|
28
|
+
<a href="https://solganis.github.io/assertpy2/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
|
|
27
29
|
</p>
|
|
28
30
|
|
|
29
31
|
---
|
|
@@ -81,7 +83,7 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
|
|
|
81
83
|
assert_that(response).is_equal_to(expected)
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
<img src="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">
|
|
86
|
+
<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
87
|
|
|
86
88
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
87
89
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
@@ -58,15 +58,22 @@ class _MixinBase:
|
|
|
58
58
|
|
|
59
59
|
def _validate_close_to_args(self, val: object, other: object, tolerance: object) -> None: ...
|
|
60
60
|
|
|
61
|
-
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(
|
|
62
70
|
self,
|
|
63
71
|
d: object,
|
|
64
72
|
check_keys: bool = ...,
|
|
65
73
|
check_values: bool = ...,
|
|
66
74
|
check_getitem: bool = ...,
|
|
67
75
|
name: str = ...,
|
|
68
|
-
|
|
69
|
-
) -> bool | None: ...
|
|
76
|
+
) -> None: ...
|
|
70
77
|
|
|
71
78
|
def _check_iterable(self, val: object, check_getitem: bool = ..., name: str = ...) -> None: ...
|
|
72
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: ...
|
|
@@ -48,37 +48,15 @@ from .snapshot import SnapshotMixin
|
|
|
48
48
|
from .string import StringMixin
|
|
49
49
|
from .warning import WarningMixin
|
|
50
50
|
|
|
51
|
-
__version__ = "2.
|
|
51
|
+
__version__ = "2.9.1"
|
|
52
52
|
|
|
53
53
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
54
54
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
55
55
|
|
|
56
|
-
#
|
|
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).
|
|
57
58
|
ASSERTPY_FILES: Final = [
|
|
58
|
-
os.path.join("assertpy2",
|
|
59
|
-
for file in [
|
|
60
|
-
"assertpy.py",
|
|
61
|
-
"async_assertions.py",
|
|
62
|
-
"base.py",
|
|
63
|
-
"bytes_mixin.py",
|
|
64
|
-
"collection.py",
|
|
65
|
-
"contains.py",
|
|
66
|
-
"date.py",
|
|
67
|
-
"dict.py",
|
|
68
|
-
"dynamic.py",
|
|
69
|
-
"errors.py",
|
|
70
|
-
"exception.py",
|
|
71
|
-
"extracting.py",
|
|
72
|
-
"file.py",
|
|
73
|
-
"helpers.py",
|
|
74
|
-
"json_mixin.py",
|
|
75
|
-
"matchers.py",
|
|
76
|
-
"numeric.py",
|
|
77
|
-
"pytest_plugin.py",
|
|
78
|
-
"snapshot.py",
|
|
79
|
-
"string.py",
|
|
80
|
-
"warning.py",
|
|
81
|
-
]
|
|
59
|
+
os.path.join("assertpy2", name) for name in os.listdir(os.path.dirname(__file__)) if name.endswith(".py")
|
|
82
60
|
]
|
|
83
61
|
|
|
84
62
|
# soft assertions (contextvars for thread/async safety)
|
|
@@ -619,7 +597,7 @@ class AssertionBuilder(
|
|
|
619
597
|
def error(self, msg, *, actual=None, expected=None, diff=None) -> Self:
|
|
620
598
|
"""Helper to raise an ``AssertionError`` with the given message.
|
|
621
599
|
|
|
622
|
-
If an error description is set by :meth:`~
|
|
600
|
+
If an error description is set by :meth:`~assertpy2.base.BaseMixin.described_as`, then that
|
|
623
601
|
description is prepended to the error message.
|
|
624
602
|
|
|
625
603
|
When structured data (``actual``, ``expected``, or ``diff``) is provided, raises
|
|
@@ -61,10 +61,9 @@ 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)
|
|
@@ -53,8 +53,8 @@ class BaseMixin(_MixinBase):
|
|
|
53
53
|
**kwargs: see below
|
|
54
54
|
|
|
55
55
|
Keyword Args:
|
|
56
|
-
ignore: the key/field (or list of keys/fields) to ignore
|
|
57
|
-
include: the key/field (or list of keys/fields) to include
|
|
56
|
+
ignore: the key/field (or list/set/frozenset of keys/fields) to ignore
|
|
57
|
+
include: the key/field (or list/set/frozenset of keys/fields) to include
|
|
58
58
|
|
|
59
59
|
Examples:
|
|
60
60
|
Usage::
|
|
@@ -114,21 +114,21 @@ class BaseMixin(_MixinBase):
|
|
|
114
114
|
|
|
115
115
|
Raises:
|
|
116
116
|
AssertionError: if actual is **not** equal to expected
|
|
117
|
+
TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
|
|
118
|
+
used on a value that is neither dict-like nor has introspectable fields
|
|
117
119
|
|
|
118
120
|
Tip:
|
|
119
121
|
Using :meth:`is_equal_to` with a ``float`` val is just asking for trouble. Instead, you'll
|
|
120
|
-
always want to use *fuzzy* numeric assertions like :meth:`~
|
|
121
|
-
or :meth:`~
|
|
122
|
+
always want to use *fuzzy* numeric assertions like :meth:`~assertpy2.numeric.NumericMixin.is_close_to`
|
|
123
|
+
or :meth:`~assertpy2.numeric.NumericMixin.is_between`.
|
|
122
124
|
|
|
123
125
|
See Also:
|
|
124
|
-
:meth:`~
|
|
126
|
+
:meth:`~assertpy2.string.StringMixin.is_equal_to_ignoring_case` - for case-insensitive string equality
|
|
125
127
|
"""
|
|
126
128
|
ignore = kwargs.get("ignore")
|
|
127
129
|
include = kwargs.get("include")
|
|
128
130
|
|
|
129
|
-
if self.
|
|
130
|
-
other, check_values=False, return_as_bool=True
|
|
131
|
-
):
|
|
131
|
+
if self._is_dict_like(self.val, check_values=False) and self._is_dict_like(other, check_values=False):
|
|
132
132
|
if self._dict_not_equal(self.val, other, ignore=ignore, include=include):
|
|
133
133
|
self._dict_err(self.val, other, ignore=ignore, include=include)
|
|
134
134
|
elif ignore or include:
|
|
@@ -18,6 +18,12 @@ __tracebackhide__ = True
|
|
|
18
18
|
class CollectionMixin(_MixinBase):
|
|
19
19
|
"""Collection assertions mixin."""
|
|
20
20
|
|
|
21
|
+
def _as_list(self) -> list[Any]:
|
|
22
|
+
"""Returns val as a list, raising TypeError if val is not iterable."""
|
|
23
|
+
if not isinstance(self.val, collections.abc.Iterable):
|
|
24
|
+
raise TypeError("val is not iterable")
|
|
25
|
+
return list(self.val)
|
|
26
|
+
|
|
21
27
|
def is_iterable(self) -> Self:
|
|
22
28
|
"""Asserts that val is iterable.
|
|
23
29
|
|
|
@@ -96,7 +102,7 @@ class CollectionMixin(_MixinBase):
|
|
|
96
102
|
if is_mapping_like(self.val):
|
|
97
103
|
superdict = {}
|
|
98
104
|
for superset_index, superset in enumerate(supersets):
|
|
99
|
-
self.
|
|
105
|
+
self._require_dict_like(superset, check_values=False, name=f"arg #{superset_index + 1}")
|
|
100
106
|
for key in superset:
|
|
101
107
|
superdict.update({key: superset[key]})
|
|
102
108
|
|
|
@@ -107,7 +113,7 @@ class CollectionMixin(_MixinBase):
|
|
|
107
113
|
missing.append({key: self.val[key]}) # bad val
|
|
108
114
|
if missing:
|
|
109
115
|
return self.error(
|
|
110
|
-
f"Expected <{self.val}> to be subset of {
|
|
116
|
+
f"Expected <{self.val}> to be subset of <{superdict}>, "
|
|
111
117
|
f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
|
|
112
118
|
)
|
|
113
119
|
else:
|
|
@@ -253,9 +259,7 @@ class CollectionMixin(_MixinBase):
|
|
|
253
259
|
Raises:
|
|
254
260
|
ValueError: if val is empty
|
|
255
261
|
"""
|
|
256
|
-
|
|
257
|
-
raise TypeError("val is not iterable")
|
|
258
|
-
items = list(self.val)
|
|
262
|
+
items = self._as_list()
|
|
259
263
|
if not items:
|
|
260
264
|
raise ValueError("Expected non-empty iterable, but was empty.")
|
|
261
265
|
return self.builder(items[0], self.description, self.kind)
|
|
@@ -274,9 +278,7 @@ class CollectionMixin(_MixinBase):
|
|
|
274
278
|
Raises:
|
|
275
279
|
ValueError: if val is empty
|
|
276
280
|
"""
|
|
277
|
-
|
|
278
|
-
raise TypeError("val is not iterable")
|
|
279
|
-
items = list(self.val)
|
|
281
|
+
items = self._as_list()
|
|
280
282
|
if not items:
|
|
281
283
|
raise ValueError("Expected non-empty iterable, but was empty.")
|
|
282
284
|
return self.builder(items[-1], self.description, self.kind)
|
|
@@ -298,9 +300,7 @@ class CollectionMixin(_MixinBase):
|
|
|
298
300
|
Raises:
|
|
299
301
|
IndexError: if index is out of range
|
|
300
302
|
"""
|
|
301
|
-
|
|
302
|
-
raise TypeError("val is not iterable")
|
|
303
|
-
items = list(self.val)
|
|
303
|
+
items = self._as_list()
|
|
304
304
|
if index < 0 or index >= len(items):
|
|
305
305
|
raise IndexError(f"Expected index {index} to be in range [0, {len(items)}), but was out of range.")
|
|
306
306
|
return self.builder(items[index], self.description, self.kind)
|
|
@@ -319,9 +319,7 @@ class CollectionMixin(_MixinBase):
|
|
|
319
319
|
Raises:
|
|
320
320
|
ValueError: if val is empty or has more than one element
|
|
321
321
|
"""
|
|
322
|
-
|
|
323
|
-
raise TypeError("val is not iterable")
|
|
324
|
-
items = list(self.val)
|
|
322
|
+
items = self._as_list()
|
|
325
323
|
if not items:
|
|
326
324
|
raise ValueError("Expected iterable with single element, but was empty.")
|
|
327
325
|
if len(items) > 1:
|
|
@@ -42,25 +42,31 @@ class ContainsMixin(_MixinBase):
|
|
|
42
42
|
AssertionError: if val does **not** contain the item or items
|
|
43
43
|
|
|
44
44
|
Tip:
|
|
45
|
-
Use the :meth:`~
|
|
45
|
+
Use the :meth:`~assertpy2.dict.DictMixin.contains_key` alias when working with
|
|
46
46
|
*dict-like* objects to be self-documenting.
|
|
47
47
|
|
|
48
48
|
See Also:
|
|
49
|
-
:meth:`~
|
|
49
|
+
:meth:`~assertpy2.string.StringMixin.contains_ignoring_case` - for case-insensitive string contains
|
|
50
50
|
"""
|
|
51
51
|
if len(items) == 0:
|
|
52
52
|
raise ValueError("one or more args must be given")
|
|
53
53
|
elif len(items) == 1:
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
item = items[0]
|
|
55
|
+
if isinstance(item, Matcher):
|
|
56
|
+
if not any(item.matches(value) for value in self.val):
|
|
57
|
+
diff = DiffResult(
|
|
58
|
+
kind="contains", entries=[DiffEntry(path="missing", actual=None, expected=item.describe())]
|
|
59
|
+
)
|
|
56
60
|
return self.error(
|
|
57
|
-
f"Expected <{self.val}> to contain item matching {
|
|
61
|
+
f"Expected <{self.val}> to contain item matching {item.describe()}, but did not.",
|
|
62
|
+
diff=diff,
|
|
58
63
|
)
|
|
59
|
-
elif
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
elif item not in self.val:
|
|
65
|
+
diff = DiffResult(kind="contains", entries=[DiffEntry(path="missing", actual=None, expected=item)])
|
|
66
|
+
if self._is_dict_like(self.val):
|
|
67
|
+
return self.error(f"Expected <{self.val}> to contain key <{item}>, but did not.", diff=diff)
|
|
62
68
|
else:
|
|
63
|
-
return self.error(f"Expected <{self.val}> to contain item <{
|
|
69
|
+
return self.error(f"Expected <{self.val}> to contain item <{item}>, but did not.", diff=diff)
|
|
64
70
|
else:
|
|
65
71
|
missing = []
|
|
66
72
|
for item in items:
|
|
@@ -80,10 +86,10 @@ class ContainsMixin(_MixinBase):
|
|
|
80
86
|
DiffEntry(path="missing", actual=None, expected=missing_item) for missing_item in missing_desc
|
|
81
87
|
],
|
|
82
88
|
)
|
|
83
|
-
if self.
|
|
89
|
+
if self._is_dict_like(self.val):
|
|
84
90
|
return self.error(
|
|
85
91
|
f"Expected <{self.val}> to contain keys {self._fmt_items(items)}, but did not contain"
|
|
86
|
-
f" key{'' if len(missing) ==
|
|
92
|
+
f" key{'' if len(missing) == 1 else 's'} {self._fmt_items(missing_desc)}.",
|
|
87
93
|
diff=diff,
|
|
88
94
|
)
|
|
89
95
|
else:
|
|
@@ -119,7 +125,7 @@ class ContainsMixin(_MixinBase):
|
|
|
119
125
|
AssertionError: if val **does** contain the item or items
|
|
120
126
|
|
|
121
127
|
Tip:
|
|
122
|
-
Use the :meth:`~
|
|
128
|
+
Use the :meth:`~assertpy2.dict.DictMixin.does_not_contain_key` alias when working with
|
|
123
129
|
*dict-like* objects to be self-documenting.
|
|
124
130
|
"""
|
|
125
131
|
if len(items) == 0:
|
|
@@ -11,6 +11,12 @@ if TYPE_CHECKING:
|
|
|
11
11
|
__tracebackhide__ = True
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _require_datetime(value: object, label: str) -> None:
|
|
15
|
+
"""Raise ``TypeError`` unless *value* is exactly a :class:`datetime.datetime`."""
|
|
16
|
+
if not isinstance(value, datetime.datetime):
|
|
17
|
+
raise TypeError(f"{label} must be datetime, but was type <{type(value).__name__}>")
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
class DateMixin(_MixinBase):
|
|
15
21
|
"""Date and time assertions mixin."""
|
|
16
22
|
|
|
@@ -37,13 +43,11 @@ class DateMixin(_MixinBase):
|
|
|
37
43
|
AssertionError: if val is **not** before the given date
|
|
38
44
|
|
|
39
45
|
See Also:
|
|
40
|
-
:meth:`~
|
|
41
|
-
:meth:`~
|
|
46
|
+
:meth:`~assertpy2.numeric.NumericMixin.is_less_than` - numeric assertion, but also works with datetime
|
|
47
|
+
:meth:`~assertpy2.numeric.NumericMixin.is_less_than_or_equal_to` - also works with datetime
|
|
42
48
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if type(other) is not datetime.datetime:
|
|
46
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
49
|
+
_require_datetime(self.val, "val")
|
|
50
|
+
_require_datetime(other, "given arg")
|
|
47
51
|
if self.val >= other:
|
|
48
52
|
return self.error(
|
|
49
53
|
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be before"
|
|
@@ -74,13 +78,11 @@ class DateMixin(_MixinBase):
|
|
|
74
78
|
AssertionError: if val is **not** after the given date
|
|
75
79
|
|
|
76
80
|
See Also:
|
|
77
|
-
:meth:`~
|
|
78
|
-
:meth:`~
|
|
81
|
+
:meth:`~assertpy2.numeric.NumericMixin.is_greater_than` - numeric assertion, but also works with datetime
|
|
82
|
+
:meth:`~assertpy2.numeric.NumericMixin.is_greater_than_or_equal_to` - also works with datetime
|
|
79
83
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if type(other) is not datetime.datetime:
|
|
83
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
84
|
+
_require_datetime(self.val, "val")
|
|
85
|
+
_require_datetime(other, "given arg")
|
|
84
86
|
if self.val <= other:
|
|
85
87
|
return self.error(
|
|
86
88
|
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be after"
|
|
@@ -111,10 +113,8 @@ class DateMixin(_MixinBase):
|
|
|
111
113
|
Raises:
|
|
112
114
|
AssertionError: if val is **not** before or equal to the given date
|
|
113
115
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if type(other) is not datetime.datetime:
|
|
117
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
116
|
+
_require_datetime(self.val, "val")
|
|
117
|
+
_require_datetime(other, "given arg")
|
|
118
118
|
if self.val > other:
|
|
119
119
|
return self.error(
|
|
120
120
|
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be before or equal to"
|
|
@@ -145,10 +145,8 @@ class DateMixin(_MixinBase):
|
|
|
145
145
|
Raises:
|
|
146
146
|
AssertionError: if val is **not** after or equal to the given date
|
|
147
147
|
"""
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if type(other) is not datetime.datetime:
|
|
151
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
148
|
+
_require_datetime(self.val, "val")
|
|
149
|
+
_require_datetime(other, "given arg")
|
|
152
150
|
if self.val < other:
|
|
153
151
|
return self.error(
|
|
154
152
|
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M:%S')}> to be after or equal to"
|
|
@@ -178,10 +176,8 @@ class DateMixin(_MixinBase):
|
|
|
178
176
|
Raises:
|
|
179
177
|
AssertionError: if val is **not** equal to the given date to the second
|
|
180
178
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if type(other) is not datetime.datetime:
|
|
184
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
179
|
+
_require_datetime(self.val, "val")
|
|
180
|
+
_require_datetime(other, "given arg")
|
|
185
181
|
if (
|
|
186
182
|
self.val.date() != other.date()
|
|
187
183
|
or self.val.hour != other.hour
|
|
@@ -216,10 +212,8 @@ class DateMixin(_MixinBase):
|
|
|
216
212
|
Raises:
|
|
217
213
|
AssertionError: if val is **not** equal to the given date to the minute
|
|
218
214
|
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if type(other) is not datetime.datetime:
|
|
222
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
215
|
+
_require_datetime(self.val, "val")
|
|
216
|
+
_require_datetime(other, "given arg")
|
|
223
217
|
if self.val.date() != other.date() or self.val.hour != other.hour or self.val.minute != other.minute:
|
|
224
218
|
return self.error(
|
|
225
219
|
f"Expected <{self.val.strftime('%Y-%m-%d %H:%M')}> to be equal to"
|
|
@@ -249,10 +243,8 @@ class DateMixin(_MixinBase):
|
|
|
249
243
|
Raises:
|
|
250
244
|
AssertionError: if val is **not** equal to the given date ignoring time
|
|
251
245
|
"""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if type(other) is not datetime.datetime:
|
|
255
|
-
raise TypeError(f"given arg must be datetime, but was type <{type(other).__name__}>")
|
|
246
|
+
_require_datetime(self.val, "val")
|
|
247
|
+
_require_datetime(other, "given arg")
|
|
256
248
|
if self.val.date() != other.date():
|
|
257
249
|
return self.error(
|
|
258
250
|
f"Expected <{self.val.strftime('%Y-%m-%d')}> to be equal to"
|
|
@@ -16,7 +16,7 @@ class DictMixin(_MixinBase):
|
|
|
16
16
|
def contains_key(self, *keys) -> Self:
|
|
17
17
|
"""Asserts the val is a dict and contains the given key or keys.
|
|
18
18
|
|
|
19
|
-
Alias for :meth:`~
|
|
19
|
+
Alias for :meth:`~assertpy2.contains.ContainsMixin.contains`.
|
|
20
20
|
|
|
21
21
|
Checks if the dict contains the given key or keys using ``in`` operator.
|
|
22
22
|
|
|
@@ -35,13 +35,13 @@ class DictMixin(_MixinBase):
|
|
|
35
35
|
Raises:
|
|
36
36
|
AssertionError: if val does **not** contain the key or keys
|
|
37
37
|
"""
|
|
38
|
-
self.
|
|
38
|
+
self._require_dict_like(self.val, check_values=False, check_getitem=False)
|
|
39
39
|
return self.contains(*keys)
|
|
40
40
|
|
|
41
41
|
def does_not_contain_key(self, *keys) -> Self:
|
|
42
42
|
"""Asserts the val is a dict and does not contain the given key or keys.
|
|
43
43
|
|
|
44
|
-
Alias for :meth:`~
|
|
44
|
+
Alias for :meth:`~assertpy2.contains.ContainsMixin.does_not_contain`.
|
|
45
45
|
|
|
46
46
|
Checks if the dict excludes the given key or keys using ``in`` operator.
|
|
47
47
|
|
|
@@ -60,7 +60,7 @@ class DictMixin(_MixinBase):
|
|
|
60
60
|
Raises:
|
|
61
61
|
AssertionError: if val **does** contain the key or keys
|
|
62
62
|
"""
|
|
63
|
-
self.
|
|
63
|
+
self._require_dict_like(self.val, check_values=False, check_getitem=False)
|
|
64
64
|
return self.does_not_contain(*keys)
|
|
65
65
|
|
|
66
66
|
def contains_value(self, *values) -> Self:
|
|
@@ -83,7 +83,7 @@ class DictMixin(_MixinBase):
|
|
|
83
83
|
Raises:
|
|
84
84
|
AssertionError: if val does **not** contain the value or values
|
|
85
85
|
"""
|
|
86
|
-
self.
|
|
86
|
+
self._require_dict_like(self.val, check_getitem=False)
|
|
87
87
|
if len(values) == 0:
|
|
88
88
|
raise ValueError("one or more value args must be given")
|
|
89
89
|
missing = [value for value in values if value not in self.val.values()]
|
|
@@ -114,7 +114,7 @@ class DictMixin(_MixinBase):
|
|
|
114
114
|
Raises:
|
|
115
115
|
AssertionError: if val **does** contain the value or values
|
|
116
116
|
"""
|
|
117
|
-
self.
|
|
117
|
+
self._require_dict_like(self.val, check_getitem=False)
|
|
118
118
|
if len(values) == 0:
|
|
119
119
|
raise ValueError("one or more value args must be given")
|
|
120
120
|
else:
|
|
@@ -158,7 +158,7 @@ class DictMixin(_MixinBase):
|
|
|
158
158
|
Raises:
|
|
159
159
|
AssertionError: if val does **not** contain the entry or entries
|
|
160
160
|
"""
|
|
161
|
-
self.
|
|
161
|
+
self._require_dict_like(self.val, check_values=False)
|
|
162
162
|
entries = list(args) + [{key: value} for key, value in kwargs.items()]
|
|
163
163
|
if len(entries) == 0:
|
|
164
164
|
raise ValueError("one or more entry args must be given")
|
|
@@ -206,7 +206,7 @@ class DictMixin(_MixinBase):
|
|
|
206
206
|
Raises:
|
|
207
207
|
AssertionError: if val **does** contain the entry or entries
|
|
208
208
|
"""
|
|
209
|
-
self.
|
|
209
|
+
self._require_dict_like(self.val, check_values=False)
|
|
210
210
|
entries = list(args) + [{key: value} for key, value in kwargs.items()]
|
|
211
211
|
if len(entries) == 0:
|
|
212
212
|
raise ValueError("one or more entry args must be given")
|
|
@@ -12,7 +12,7 @@ class DynamicMixin(_MixinBase):
|
|
|
12
12
|
"""Dynamic assertions mixin.
|
|
13
13
|
|
|
14
14
|
When testing attributes of an object (or the contents of a dict), the
|
|
15
|
-
:meth:`~
|
|
15
|
+
:meth:`~assertpy2.base.BaseMixin.is_equal_to` assertion can be a bit verbose::
|
|
16
16
|
|
|
17
17
|
fred = Person('Fred', 'Smith')
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ class ExceptionMixin(_MixinBase):
|
|
|
28
28
|
def raises(self, ex) -> Self:
|
|
29
29
|
"""Asserts that val is callable and set the expected exception.
|
|
30
30
|
|
|
31
|
-
Just sets the expected exception, but never calls val, and therefore never
|
|
31
|
+
Just sets the expected exception, but never calls val, and therefore never fails. You must
|
|
32
32
|
chain to :meth:`~when_called_with` to invoke ``val()``.
|
|
33
33
|
|
|
34
34
|
Args:
|