assertpy2 2.12.0__tar.gz → 2.13.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.13.0/.github/dependabot.yml +31 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/ci.yml +22 -4
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/codeql.yml +1 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/docs.yml +2 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/mutation.yml +2 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/publish.yml +2 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/scorecard.yml +1 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.github/workflows/zizmor.yml +2 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/PKG-INFO +2 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/__init__.py +2 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_compare.py +87 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_typing.py +17 -2
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/assertpy.py +26 -4
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/async_assertions.py +35 -4
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/base.py +13 -34
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/helpers.py +3 -3
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/matchers.py +10 -5
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/snapshot.py +62 -3
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assertions.md +5 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/integrations.md +2 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/matchers.md +6 -1
- assertpy2-2.13.0/docs/reference/snapshots.md +11 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/testing.md +35 -6
- {assertpy2-2.12.0 → assertpy2-2.13.0}/mkdocs.yml +1 -1
- assertpy2-2.13.0/pyproject.toml +158 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_async.py +107 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_equals.py +129 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_matchers.py +24 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_snapshots.py +77 -1
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_structural.py +25 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/uv.lock +237 -69
- assertpy2-2.12.0/.github/dependabot.yml +0 -20
- assertpy2-2.12.0/docs/reference/snapshots.md +0 -5
- assertpy2-2.12.0/pyproject.toml +0 -104
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.codecov.yml +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.gitattributes +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/.gitignore +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/LICENSE +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/README.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/SECURITY.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_compat.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/_mixin_base.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dataframe.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/date.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/file.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/string.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/comparison.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/data.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/errors.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/extending.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/fluent.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/getting-started.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/index.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/logo.svg +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/migration.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/async.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/core.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/files.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/json.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/docs/type-safety.md +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_bool.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_callable.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_class.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_collection.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_core.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dataframe.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_description.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dict.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_errors.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_fail.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_file.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_in.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_json.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_list.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_mutation_hardening.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_none.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_not.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_readme.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_config.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_soft.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_string.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_type.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_typing.py +0 -0
- {assertpy2-2.12.0 → assertpy2-2.13.0}/tests/test_warn.py +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Python dependencies via uv: the native `uv` ecosystem updates pyproject.toml
|
|
4
|
+
# AND keeps uv.lock in sync (the older `pip` ecosystem left the lock untouched).
|
|
5
|
+
- package-ecosystem: uv
|
|
6
|
+
directory: /
|
|
7
|
+
schedule:
|
|
8
|
+
interval: weekly
|
|
9
|
+
cooldown:
|
|
10
|
+
default-days: 7
|
|
11
|
+
semver-major-days: 7
|
|
12
|
+
commit-message:
|
|
13
|
+
prefix: "chore(deps)"
|
|
14
|
+
groups:
|
|
15
|
+
# One PR/week for all minor+patch bumps; majors stay individual for review.
|
|
16
|
+
python-minor-patch:
|
|
17
|
+
patterns: ["*"]
|
|
18
|
+
update-types: ["minor", "patch"]
|
|
19
|
+
|
|
20
|
+
- package-ecosystem: github-actions
|
|
21
|
+
directory: /
|
|
22
|
+
schedule:
|
|
23
|
+
interval: weekly
|
|
24
|
+
cooldown:
|
|
25
|
+
default-days: 7
|
|
26
|
+
commit-message:
|
|
27
|
+
prefix: "ci(deps)"
|
|
28
|
+
groups:
|
|
29
|
+
actions-minor-patch:
|
|
30
|
+
patterns: ["*"]
|
|
31
|
+
update-types: ["minor", "patch"]
|
|
@@ -21,12 +21,17 @@ jobs:
|
|
|
21
21
|
os: [ubuntu-latest]
|
|
22
22
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
23
23
|
include:
|
|
24
|
+
# the Codecov cell also installs every optional integration (pandas/polars/numpy extras plus
|
|
25
|
+
# pydantic/attrs/requests), so the tests the other minimal-env cells skip run here for real
|
|
26
|
+
- os: ubuntu-latest
|
|
27
|
+
python-version: "3.14"
|
|
28
|
+
full-deps: true
|
|
24
29
|
- os: windows-latest
|
|
25
30
|
python-version: "3.14"
|
|
26
31
|
- os: macos-latest
|
|
27
32
|
python-version: "3.14"
|
|
28
33
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
34
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
30
35
|
with:
|
|
31
36
|
persist-credentials: false
|
|
32
37
|
|
|
@@ -34,14 +39,19 @@ jobs:
|
|
|
34
39
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
35
40
|
|
|
36
41
|
- name: Set up Python ${{ matrix.python-version }}
|
|
37
|
-
uses: actions/setup-python@
|
|
42
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
38
43
|
with:
|
|
39
44
|
python-version: ${{ matrix.python-version }}
|
|
40
45
|
allow-prereleases: true
|
|
41
46
|
|
|
42
47
|
- name: Install dependencies
|
|
48
|
+
if: '!matrix.full-deps'
|
|
43
49
|
run: uv sync --extra json
|
|
44
50
|
|
|
51
|
+
- name: Install dependencies (all integrations)
|
|
52
|
+
if: matrix.full-deps
|
|
53
|
+
run: uv sync --extra json --extra data --group integrations
|
|
54
|
+
|
|
45
55
|
- name: Test with coverage
|
|
46
56
|
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml tests
|
|
47
57
|
|
|
@@ -55,7 +65,7 @@ jobs:
|
|
|
55
65
|
lint:
|
|
56
66
|
runs-on: ubuntu-latest
|
|
57
67
|
steps:
|
|
58
|
-
- uses: actions/checkout@
|
|
68
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
59
69
|
with:
|
|
60
70
|
persist-credentials: false
|
|
61
71
|
|
|
@@ -63,7 +73,7 @@ jobs:
|
|
|
63
73
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
64
74
|
|
|
65
75
|
- name: Set up Python
|
|
66
|
-
uses: actions/setup-python@
|
|
76
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
67
77
|
with:
|
|
68
78
|
python-version: "3.14"
|
|
69
79
|
|
|
@@ -84,3 +94,11 @@ jobs:
|
|
|
84
94
|
|
|
85
95
|
- name: Type check (pyright, public typing surface)
|
|
86
96
|
run: uv run pyright tests/test_typing.py
|
|
97
|
+
|
|
98
|
+
ci-ok:
|
|
99
|
+
needs: [test, lint]
|
|
100
|
+
if: always()
|
|
101
|
+
runs-on: ubuntu-latest
|
|
102
|
+
steps:
|
|
103
|
+
- name: Verify required jobs succeeded
|
|
104
|
+
run: '[ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.lint.result }}" = "success" ]'
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
permissions:
|
|
18
18
|
security-events: write
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
21
21
|
with:
|
|
22
22
|
persist-credentials: false
|
|
23
23
|
- uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
build:
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
21
21
|
with:
|
|
22
22
|
persist-credentials: false
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
26
26
|
|
|
27
27
|
- name: Set up Python
|
|
28
|
-
uses: actions/setup-python@
|
|
28
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
29
29
|
with:
|
|
30
30
|
python-version: "3.14"
|
|
31
31
|
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
matrix:
|
|
22
22
|
module: [base, numeric, string, contains, collection, dict, date, matchers, helpers, _compare, dataframe, json_mixin, snapshot, bytes_mixin, dynamic, extracting, warning, exception, async_assertions, file, behave_matchers]
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
25
25
|
with:
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
30
|
|
|
31
31
|
- name: Set up Python
|
|
32
|
-
uses: actions/setup-python@
|
|
32
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
33
33
|
with:
|
|
34
34
|
python-version: "3.14"
|
|
35
35
|
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
contents: write
|
|
17
17
|
attestations: write
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
20
20
|
with:
|
|
21
21
|
persist-credentials: false
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
enable-cache: false
|
|
27
27
|
|
|
28
28
|
- name: Set up Python
|
|
29
|
-
uses: actions/setup-python@
|
|
29
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
30
30
|
with:
|
|
31
31
|
python-version: "3.15"
|
|
32
32
|
allow-prereleases: true
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
security-events: write
|
|
17
17
|
id-token: write
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
20
20
|
with:
|
|
21
21
|
persist-credentials: false
|
|
22
22
|
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
permissions:
|
|
20
20
|
security-events: write
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
23
23
|
with:
|
|
24
24
|
persist-credentials: false
|
|
25
|
-
- uses: zizmorcore/zizmor-action@
|
|
25
|
+
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.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
|
|
@@ -9,7 +9,7 @@ Author-email: Justin Shacklette <justin@saturnboy.com>
|
|
|
9
9
|
Maintainer-email: Solganis <solganis.dev@gmail.com>
|
|
10
10
|
License-Expression: BSD-3-Clause
|
|
11
11
|
License-File: LICENSE
|
|
12
|
-
Keywords: assert,assert_that,assertion,assertthat,pytest,test,testing,unittest
|
|
12
|
+
Keywords: assert,assert_that,assertion,assertpy,assertthat,fluent,matchers,pytest,test,testing,type-safe,unittest
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -16,6 +16,7 @@ from .async_assertions import AsyncAssertionBuilder
|
|
|
16
16
|
from .errors import AssertionFailure, DiffEntry, DiffResult
|
|
17
17
|
from .file import contents_of
|
|
18
18
|
from .matchers import Matcher, clear_custom_matchers, match, register_matcher, unregister_matcher
|
|
19
|
+
from .snapshot import SnapshotCreatedWarning
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"AssertionFailure",
|
|
@@ -24,6 +25,7 @@ __all__ = [
|
|
|
24
25
|
"DiffResult",
|
|
25
26
|
"Matcher",
|
|
26
27
|
"NegatedBuilder",
|
|
28
|
+
"SnapshotCreatedWarning",
|
|
27
29
|
"SoftAssertionCollector",
|
|
28
30
|
"WarningLoggingAdapter",
|
|
29
31
|
"__version__",
|
|
@@ -13,12 +13,15 @@ express to the type checker.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import dataclasses
|
|
16
17
|
import math
|
|
17
18
|
import numbers
|
|
18
19
|
import re
|
|
19
20
|
from dataclasses import dataclass
|
|
20
21
|
from typing import TYPE_CHECKING
|
|
21
22
|
|
|
23
|
+
from ._introspection import is_mapping_like, is_model_dump_object
|
|
24
|
+
|
|
22
25
|
if TYPE_CHECKING:
|
|
23
26
|
from collections.abc import Callable
|
|
24
27
|
|
|
@@ -59,6 +62,89 @@ def _build_compare_config(tolerance, comparators) -> _CompareConfig | None:
|
|
|
59
62
|
return _CompareConfig(tolerance=tolerance, comparators=comparators)
|
|
60
63
|
|
|
61
64
|
|
|
65
|
+
def _ambiguous_array_operand(value: object, other: object) -> object | None:
|
|
66
|
+
"""Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
|
|
67
|
+
|
|
68
|
+
numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
|
|
69
|
+
raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
|
|
70
|
+
mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
|
|
71
|
+
truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
|
|
72
|
+
through unchanged.
|
|
73
|
+
"""
|
|
74
|
+
for candidate, counterpart in ((value, other), (other, value)):
|
|
75
|
+
if hasattr(candidate, "__array__"):
|
|
76
|
+
try:
|
|
77
|
+
bool(candidate == counterpart)
|
|
78
|
+
except (ValueError, TypeError):
|
|
79
|
+
return candidate
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _array_equality_error(method: str, operand: object) -> TypeError:
|
|
84
|
+
"""Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
|
|
85
|
+
return TypeError(
|
|
86
|
+
f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
|
|
87
|
+
" no single truth value. Compare the value's own equality (e.g."
|
|
88
|
+
" assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
|
|
89
|
+
" length), or use satisfies(...) with an explicit predicate."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _find_ambiguous_operand(actual, expected, _seen=None):
|
|
94
|
+
"""Locate the array/frame-like member that broke a comparison, walking the diff engine's containers.
|
|
95
|
+
|
|
96
|
+
Cold error-path only; ``None`` means the error was not array-caused and must be re-raised unchanged.
|
|
97
|
+
"""
|
|
98
|
+
if _seen is None:
|
|
99
|
+
_seen = set()
|
|
100
|
+
pair = (id(actual), id(expected))
|
|
101
|
+
if pair in _seen:
|
|
102
|
+
return None
|
|
103
|
+
_seen = _seen | {pair}
|
|
104
|
+
operand = _ambiguous_array_operand(actual, expected)
|
|
105
|
+
if operand is not None:
|
|
106
|
+
return operand
|
|
107
|
+
if is_mapping_like(actual) and is_mapping_like(expected):
|
|
108
|
+
expected_keys = set(expected)
|
|
109
|
+
for key in actual:
|
|
110
|
+
if key in expected_keys:
|
|
111
|
+
found = _find_ambiguous_operand(actual[key], expected[key], _seen)
|
|
112
|
+
if found is not None:
|
|
113
|
+
return found
|
|
114
|
+
return None
|
|
115
|
+
if (
|
|
116
|
+
dataclasses.is_dataclass(actual)
|
|
117
|
+
and not isinstance(actual, type)
|
|
118
|
+
and dataclasses.is_dataclass(expected)
|
|
119
|
+
and not isinstance(expected, type)
|
|
120
|
+
):
|
|
121
|
+
for field in dataclasses.fields(actual):
|
|
122
|
+
found = _find_ambiguous_operand(getattr(actual, field.name), getattr(expected, field.name, None), _seen)
|
|
123
|
+
if found is not None:
|
|
124
|
+
return found
|
|
125
|
+
return None
|
|
126
|
+
if is_model_dump_object(actual) and is_model_dump_object(expected):
|
|
127
|
+
return _find_ambiguous_operand(actual.model_dump(), expected.model_dump(), _seen)
|
|
128
|
+
if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
|
|
129
|
+
for actual_item, expected_item in zip(actual, expected, strict=False):
|
|
130
|
+
found = _find_ambiguous_operand(actual_item, expected_item, _seen)
|
|
131
|
+
if found is not None:
|
|
132
|
+
return found
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _guarded_not_equal(actual, expected) -> bool:
|
|
137
|
+
"""``bool(actual != expected)``, converting the ambiguity raised from *inside* a container's ``==``
|
|
138
|
+
(where the top-level operand gate cannot see the array member) into the actionable ``TypeError``."""
|
|
139
|
+
try:
|
|
140
|
+
return bool(actual != expected)
|
|
141
|
+
except (ValueError, TypeError) as error:
|
|
142
|
+
operand = _find_ambiguous_operand(actual, expected)
|
|
143
|
+
if operand is None:
|
|
144
|
+
raise
|
|
145
|
+
raise _array_equality_error("is_equal_to", operand) from error
|
|
146
|
+
|
|
147
|
+
|
|
62
148
|
def _is_real_number(value) -> bool:
|
|
63
149
|
"""Return whether ``value`` is a real number eligible for tolerance (excludes ``bool`` and ``complex``).
|
|
64
150
|
|
|
@@ -114,7 +200,7 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
|
|
|
114
200
|
return "equal" if comparator(actual, expected) else "leaf"
|
|
115
201
|
if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
|
|
116
202
|
return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
|
|
117
|
-
return "recurse" if actual
|
|
203
|
+
return "recurse" if _guarded_not_equal(actual, expected) else "equal"
|
|
118
204
|
|
|
119
205
|
|
|
120
206
|
def _spec_matches(key, value, specs) -> bool:
|
|
@@ -48,7 +48,16 @@ if TYPE_CHECKING:
|
|
|
48
48
|
def is_in(self, *items: object) -> Self: ...
|
|
49
49
|
def is_not_in(self, *items: object) -> Self: ...
|
|
50
50
|
# SnapshotMixin
|
|
51
|
-
def snapshot(
|
|
51
|
+
def snapshot(
|
|
52
|
+
self,
|
|
53
|
+
id: str | None = ..., # noqa: A002 # mirrors public snapshot() parameter
|
|
54
|
+
path: str = ...,
|
|
55
|
+
*,
|
|
56
|
+
ignore: _KeySpecs | None = ...,
|
|
57
|
+
include: _KeySpecs | None = ...,
|
|
58
|
+
tolerance: float | None = ...,
|
|
59
|
+
comparators: dict[object, Callable[..., bool]] | None = ...,
|
|
60
|
+
) -> Self: ...
|
|
52
61
|
# NegatedBuilder
|
|
53
62
|
@property
|
|
54
63
|
def not_(self) -> NegatedBuilder: ...
|
|
@@ -258,4 +267,10 @@ if TYPE_CHECKING:
|
|
|
258
267
|
def does_not_warn(self, warning: type[Warning] = ...) -> Self: ...
|
|
259
268
|
# captured value (exception or warning message) is a str; returned() pivots to the call result
|
|
260
269
|
def when_called_with(self, *some_args: object, **some_kwargs: object) -> _InvokedAssertion: ...
|
|
261
|
-
def eventually(
|
|
270
|
+
def eventually(
|
|
271
|
+
self,
|
|
272
|
+
*,
|
|
273
|
+
timeout: float = ...,
|
|
274
|
+
interval: float = ...,
|
|
275
|
+
ignoring: type[Exception] | tuple[type[Exception], ...] = ...,
|
|
276
|
+
) -> AsyncAssertionBuilder: ...
|
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
_StringAssertion,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
from .async_assertions import AsyncAssertionBuilder
|
|
32
|
+
from .async_assertions import AsyncAssertionBuilder, _normalize_ignoring
|
|
33
33
|
from .base import BaseMixin
|
|
34
34
|
from .bytes_mixin import BytesMixin
|
|
35
35
|
from .collection import CollectionMixin
|
|
@@ -49,7 +49,7 @@ from .snapshot import SnapshotMixin
|
|
|
49
49
|
from .string import StringMixin
|
|
50
50
|
from .warning import WarningMixin
|
|
51
51
|
|
|
52
|
-
__version__ = "2.
|
|
52
|
+
__version__ = "2.13.0"
|
|
53
53
|
|
|
54
54
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
55
55
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
@@ -631,7 +631,13 @@ class AssertionBuilder(
|
|
|
631
631
|
raise AssertionFailure(out, actual=actual, expected=expected, diff=diff)
|
|
632
632
|
raise AssertionError(out)
|
|
633
633
|
|
|
634
|
-
def eventually(
|
|
634
|
+
def eventually(
|
|
635
|
+
self,
|
|
636
|
+
*,
|
|
637
|
+
timeout: float = 5.0,
|
|
638
|
+
interval: float = 0.5,
|
|
639
|
+
ignoring: type[Exception] | tuple[type[Exception], ...] = (),
|
|
640
|
+
) -> AsyncAssertionBuilder:
|
|
635
641
|
"""Switch to async polling mode for eventual-consistency assertions.
|
|
636
642
|
|
|
637
643
|
The current ``val`` must be a callable (sync or async). Returns an
|
|
@@ -639,9 +645,16 @@ class AssertionBuilder(
|
|
|
639
645
|
methods are coroutines that poll ``val()`` until the assertion passes or
|
|
640
646
|
``timeout`` expires.
|
|
641
647
|
|
|
648
|
+
By default only a failing assertion is retried: any exception raised by ``val()`` itself
|
|
649
|
+
propagates immediately. A probe that signals "not ready yet" by raising (a connection refused
|
|
650
|
+
while a service boots, a record not yet visible) can be retried too by listing those exception
|
|
651
|
+
types in ``ignoring``.
|
|
652
|
+
|
|
642
653
|
Args:
|
|
643
654
|
timeout: maximum seconds to keep retrying (default ``5.0``)
|
|
644
655
|
interval: seconds between retries (default ``0.5``)
|
|
656
|
+
ignoring: an ``Exception`` subclass (or tuple of them) the polling loop retries instead of
|
|
657
|
+
propagating (default: none)
|
|
645
658
|
|
|
646
659
|
Examples:
|
|
647
660
|
Usage:
|
|
@@ -659,11 +672,19 @@ class AssertionBuilder(
|
|
|
659
672
|
assert_that(get_count).eventually(timeout=2).is_equal_to(3)
|
|
660
673
|
)
|
|
661
674
|
|
|
675
|
+
Retry a probe that raises while the system under test is not ready yet:
|
|
676
|
+
|
|
677
|
+
await assert_that(get_order).eventually(timeout=10, ignoring=ConnectionError).has_status("PAID")
|
|
678
|
+
|
|
679
|
+
# or configure fluently on the returned builder
|
|
680
|
+
await assert_that(get_order).eventually().within(10).ignoring(ConnectionError).has_status("PAID")
|
|
681
|
+
|
|
662
682
|
Returns:
|
|
663
683
|
AsyncAssertionBuilder: an async builder whose assertion methods are awaitable
|
|
664
684
|
|
|
665
685
|
Raises:
|
|
666
|
-
TypeError: if ``val`` is not callable
|
|
686
|
+
TypeError: if ``val`` is not callable, or if ``ignoring`` contains anything that is not an
|
|
687
|
+
``Exception`` subclass
|
|
667
688
|
"""
|
|
668
689
|
if not callable(self.val):
|
|
669
690
|
raise TypeError("val must be callable when using eventually()")
|
|
@@ -673,4 +694,5 @@ class AssertionBuilder(
|
|
|
673
694
|
description=self.description,
|
|
674
695
|
timeout=timeout,
|
|
675
696
|
interval=interval,
|
|
697
|
+
ignoring=_normalize_ignoring(ignoring),
|
|
676
698
|
)
|
|
@@ -12,6 +12,19 @@ if TYPE_CHECKING:
|
|
|
12
12
|
__tracebackhide__ = True
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _normalize_ignoring(ignoring) -> tuple[type[Exception], ...]:
|
|
16
|
+
"""Normalize an ``ignoring`` spec (one exception type or a tuple of them) to a validated tuple.
|
|
17
|
+
|
|
18
|
+
Only ``Exception`` subclasses are accepted, so ``BaseException``-only classes such as
|
|
19
|
+
``KeyboardInterrupt`` can never be swallowed by the polling loop.
|
|
20
|
+
"""
|
|
21
|
+
exceptions = ignoring if isinstance(ignoring, tuple) else (ignoring,)
|
|
22
|
+
for exception_type in exceptions:
|
|
23
|
+
if not (isinstance(exception_type, type) and issubclass(exception_type, Exception)):
|
|
24
|
+
raise TypeError("given ignoring arg must be an Exception subclass or a tuple of Exception subclasses")
|
|
25
|
+
return exceptions
|
|
26
|
+
|
|
27
|
+
|
|
15
28
|
class AsyncAssertionBuilder:
|
|
16
29
|
"""Async assertion builder that polls a callable until an assertion passes or timeout expires.
|
|
17
30
|
|
|
@@ -23,6 +36,7 @@ class AsyncAssertionBuilder:
|
|
|
23
36
|
description: optional error description forwarded to the builder
|
|
24
37
|
timeout: maximum seconds to keep retrying
|
|
25
38
|
interval: seconds between retries
|
|
39
|
+
ignoring: exception types the polling loop retries instead of propagating
|
|
26
40
|
"""
|
|
27
41
|
|
|
28
42
|
def __init__(
|
|
@@ -33,12 +47,14 @@ class AsyncAssertionBuilder:
|
|
|
33
47
|
description: str = "",
|
|
34
48
|
timeout: float = 5.0,
|
|
35
49
|
interval: float = 0.5,
|
|
50
|
+
ignoring: tuple[type[Exception], ...] = (),
|
|
36
51
|
):
|
|
37
52
|
self._func = func
|
|
38
53
|
self._builder_func = builder_func
|
|
39
54
|
self._description = description
|
|
40
55
|
self._timeout = timeout
|
|
41
56
|
self._interval = interval
|
|
57
|
+
self._ignoring = ignoring
|
|
42
58
|
|
|
43
59
|
def within(self, timeout: float) -> Self:
|
|
44
60
|
"""Override the timeout (in seconds)."""
|
|
@@ -50,6 +66,20 @@ class AsyncAssertionBuilder:
|
|
|
50
66
|
self._interval = interval
|
|
51
67
|
return self
|
|
52
68
|
|
|
69
|
+
def ignoring(self, *exceptions: type[Exception]) -> Self:
|
|
70
|
+
"""Replace the exception types the polling loop retries instead of propagating.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
Usage:
|
|
74
|
+
|
|
75
|
+
await assert_that(get_order).eventually().within(10).ignoring(ConnectionError).has_status("PAID")
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
TypeError: if any argument is not an ``Exception`` subclass
|
|
79
|
+
"""
|
|
80
|
+
self._ignoring = _normalize_ignoring(exceptions)
|
|
81
|
+
return self
|
|
82
|
+
|
|
53
83
|
def __getattr__(self, name: str):
|
|
54
84
|
if name.startswith("_"):
|
|
55
85
|
raise AttributeError(name)
|
|
@@ -58,7 +88,7 @@ class AsyncAssertionBuilder:
|
|
|
58
88
|
async def _poll():
|
|
59
89
|
loop = asyncio.get_running_loop()
|
|
60
90
|
deadline = loop.time() + self._timeout
|
|
61
|
-
last_error:
|
|
91
|
+
last_error: Exception | None = None
|
|
62
92
|
while True:
|
|
63
93
|
try:
|
|
64
94
|
val = self._func()
|
|
@@ -68,12 +98,13 @@ class AsyncAssertionBuilder:
|
|
|
68
98
|
method = getattr(builder, name)
|
|
69
99
|
method(*args, **kwargs)
|
|
70
100
|
return builder
|
|
71
|
-
except AssertionError as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
|
|
101
|
+
except (AssertionError, *self._ignoring) as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
|
|
72
102
|
last_error = exc
|
|
73
103
|
if loop.time() >= deadline:
|
|
104
|
+
# repr for ignored exceptions: their type name is the diagnostic, str() may be empty
|
|
105
|
+
failure = str(last_error) if isinstance(last_error, AssertionError) else repr(last_error)
|
|
74
106
|
raise AssertionError(
|
|
75
|
-
f"Expected condition not met after {self._timeout:.1f} seconds."
|
|
76
|
-
f" Last failure: {last_error}"
|
|
107
|
+
f"Expected condition not met after {self._timeout:.1f} seconds. Last failure: {failure}"
|
|
77
108
|
) from last_error
|
|
78
109
|
await asyncio.sleep(self._interval)
|
|
79
110
|
|
|
@@ -4,7 +4,13 @@ import collections.abc
|
|
|
4
4
|
import dataclasses
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Final
|
|
6
6
|
|
|
7
|
-
from ._compare import
|
|
7
|
+
from ._compare import (
|
|
8
|
+
_ambiguous_array_operand,
|
|
9
|
+
_array_equality_error,
|
|
10
|
+
_build_compare_config,
|
|
11
|
+
_guarded_not_equal,
|
|
12
|
+
_node_decision,
|
|
13
|
+
)
|
|
8
14
|
from ._introspection import is_mapping_like, is_model_dump_object, is_namedtuple
|
|
9
15
|
from ._mixin_base import _MixinBase
|
|
10
16
|
from .errors import DiffEntry, DiffResult
|
|
@@ -20,34 +26,6 @@ __tracebackhide__ = True
|
|
|
20
26
|
_SENTINEL: Final = object()
|
|
21
27
|
|
|
22
28
|
|
|
23
|
-
def _ambiguous_array_operand(value: object, other: object) -> object | None:
|
|
24
|
-
"""Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
|
|
25
|
-
|
|
26
|
-
numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
|
|
27
|
-
raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
|
|
28
|
-
mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
|
|
29
|
-
truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
|
|
30
|
-
through unchanged.
|
|
31
|
-
"""
|
|
32
|
-
for candidate, counterpart in ((value, other), (other, value)):
|
|
33
|
-
if hasattr(candidate, "__array__"):
|
|
34
|
-
try:
|
|
35
|
-
bool(candidate == counterpart)
|
|
36
|
-
except (ValueError, TypeError):
|
|
37
|
-
return candidate
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _array_equality_error(method: str, operand: object) -> TypeError:
|
|
42
|
-
"""Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
|
|
43
|
-
return TypeError(
|
|
44
|
-
f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
|
|
45
|
-
" no single truth value. Compare the value's own equality (e.g."
|
|
46
|
-
" assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
|
|
47
|
-
" length), or use satisfies(...) with an explicit predicate."
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
29
|
class BaseMixin(_MixinBase):
|
|
52
30
|
"""Base mixin."""
|
|
53
31
|
|
|
@@ -149,7 +127,7 @@ class BaseMixin(_MixinBase):
|
|
|
149
127
|
|
|
150
128
|
# by type, or by field name (field name wins over type)
|
|
151
129
|
assert_that(actual).is_equal_to(expected, comparators={float: lambda a, e: round(a, 2) == round(e, 2)})
|
|
152
|
-
assert_that(actual).is_equal_to(expected, comparators={"
|
|
130
|
+
assert_that(actual).is_equal_to(expected, comparators={"name": lambda a, e: a.lower() == e.lower()})
|
|
153
131
|
|
|
154
132
|
Ignore fields by regex or by type:
|
|
155
133
|
|
|
@@ -170,9 +148,10 @@ class BaseMixin(_MixinBase):
|
|
|
170
148
|
AssertionError: if actual is **not** equal to expected
|
|
171
149
|
TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
|
|
172
150
|
used on a value that is neither dict-like nor has introspectable fields; if ``tolerance`` is
|
|
173
|
-
not a real number or ``comparators`` is not a dict of callables; or if val or other is
|
|
174
|
-
element-wise array/frame-like (numpy/pandas/polars) whose
|
|
175
|
-
(compare the value's own equality, e.g.
|
|
151
|
+
not a real number or ``comparators`` is not a dict of callables; or if val or other is (or
|
|
152
|
+
contains, at any nesting depth) an element-wise array/frame-like (numpy/pandas/polars) whose
|
|
153
|
+
``==`` has no single truth value (compare the value's own equality, e.g.
|
|
154
|
+
``actual.equals(expected)``, instead)
|
|
176
155
|
ValueError: if ``tolerance`` is ``NaN`` or negative
|
|
177
156
|
|
|
178
157
|
Tip:
|
|
@@ -219,7 +198,7 @@ class BaseMixin(_MixinBase):
|
|
|
219
198
|
diff=diff,
|
|
220
199
|
)
|
|
221
200
|
else:
|
|
222
|
-
if self.val
|
|
201
|
+
if _guarded_not_equal(self.val, other):
|
|
223
202
|
diff = self._build_equality_diff(self.val, other)
|
|
224
203
|
return self.error(
|
|
225
204
|
f"Expected <{self.val}> to be equal to <{other}>, but was not.",
|
|
@@ -7,7 +7,7 @@ import re
|
|
|
7
7
|
|
|
8
8
|
from assertpy2.errors import DiffResult
|
|
9
9
|
|
|
10
|
-
from ._compare import _CompareConfig, _node_decision, _spec_matches
|
|
10
|
+
from ._compare import _CompareConfig, _guarded_not_equal, _node_decision, _spec_matches
|
|
11
11
|
from ._introspection import is_attrs_instance, is_model_dump_object, is_namedtuple
|
|
12
12
|
from ._mixin_base import _MixinBase
|
|
13
13
|
|
|
@@ -138,7 +138,7 @@ class HelpersMixin(_MixinBase):
|
|
|
138
138
|
_seen = _seen | {pair}
|
|
139
139
|
|
|
140
140
|
if not (ignore or include or config is not None):
|
|
141
|
-
return val
|
|
141
|
+
return _guarded_not_equal(val, other)
|
|
142
142
|
|
|
143
143
|
ignores = [] # bound for the nested-recursion use below; only read when ``ignore`` is set
|
|
144
144
|
if ignore or include:
|
|
@@ -210,7 +210,7 @@ class HelpersMixin(_MixinBase):
|
|
|
210
210
|
)
|
|
211
211
|
if subdicts_not_equal:
|
|
212
212
|
return True
|
|
213
|
-
elif val[key]
|
|
213
|
+
elif _guarded_not_equal(val[key], other[key]):
|
|
214
214
|
return True
|
|
215
215
|
return False
|
|
216
216
|
|