assertpy2 2.18.0__tar.gz → 2.20.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.18.0 → assertpy2-2.20.0}/.github/workflows/ci.yml +60 -9
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/codeql.yml +4 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/codspeed.yml +4 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/docs.yml +5 -3
- assertpy2-2.20.0/.github/workflows/mutation.yml +78 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/publish.yml +5 -4
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/scorecard.yml +4 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/workflows/zizmor.yml +3 -2
- assertpy2-2.20.0/CONTRIBUTING.md +67 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/PKG-INFO +25 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/README.md +24 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/__init__.py +32 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_compare.py +121 -7
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_contract.py +2 -0
- assertpy2-2.20.0/assertpy2/_engine/_diff.py +584 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_introspection.py +51 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_mixin_base.py +6 -0
- assertpy2-2.20.0/assertpy2/_engine/_path.py +100 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_typing.py +36 -5
- assertpy2-2.20.0/assertpy2/_hints.py +325 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_inline.py +7 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_matcher_impls.py +221 -38
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_satisfies.py +58 -26
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/assertpy.py +303 -50
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/async_assertions.py +15 -5
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/base.py +59 -14
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/bytes_mixin.py +15 -6
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/collection.py +7 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/contains.py +92 -49
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/dataframe.py +5 -3
- assertpy2-2.20.0/assertpy2/errors.py +502 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/extracting.py +4 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/file.py +1 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/helpers.py +85 -10
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/json_mixin.py +26 -7
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/matchers.py +94 -7
- assertpy2-2.20.0/assertpy2/outcome.py +118 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/pytest_plugin.py +152 -22
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/snapshot.py +187 -32
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/string.py +23 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/benchmarks/test_perf.py +107 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/concepts/type-safety.md +40 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/extending/custom-assertions.md +23 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/extending/integrations.md +43 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/getting-started/comparison.md +4 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/getting-started/quickstart.md +6 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/assertions.md +96 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/errors.md +275 -9
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/fluent.md +17 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/matchers.md +78 -4
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/testing.md +47 -3
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/core.md +5 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/entry-points.md +18 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/errors.md +6 -0
- assertpy2-2.20.0/docs/reference/matchers.md +21 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/snapshots.md +6 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/pyproject.toml +80 -6
- assertpy2-2.20.0/scripts/mutation_report.py +142 -0
- assertpy2-2.20.0/tests/docs_fixtures.py +150 -0
- assertpy2-2.20.0/tests/pyright_baseline.py +69 -0
- assertpy2-2.20.0/tests/test_api_vocabulary.py +153 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_async.py +45 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_attrs_support.py +1 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_bytes.py +54 -3
- assertpy2-2.20.0/tests/test_check.py +121 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_collection.py +71 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_dataframe.py +21 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_datetime.py +25 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_dict_compare.py +100 -10
- assertpy2-2.20.0/tests/test_docs_examples.py +92 -0
- assertpy2-2.20.0/tests/test_docs_typing.py +105 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_equals.py +88 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_errors.py +146 -9
- assertpy2-2.20.0/tests/test_exception_context.py +73 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_extensions.py +90 -13
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_extracting.py +21 -0
- assertpy2-2.20.0/tests/test_hints.py +417 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_inline_record.py +58 -0
- assertpy2-2.20.0/tests/test_inline_snapshot.py +199 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_json.py +27 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_list.py +114 -0
- assertpy2-2.20.0/tests/test_match_result.py +134 -0
- assertpy2-2.20.0/tests/test_matcher_parity.py +157 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_matcher_registry.py +90 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_matchers.py +212 -0
- assertpy2-2.20.0/tests/test_message_elision.py +118 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_openapi_contract.py +136 -1
- assertpy2-2.20.0/tests/test_optional_integration_contracts.py +87 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_pipeline.py +15 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_property_based.py +282 -15
- assertpy2-2.20.0/tests/test_protocol_parity.py +219 -0
- assertpy2-2.20.0/tests/test_public_surface.py +132 -0
- assertpy2-2.20.0/tests/test_pyright_baseline.py +54 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_pytest_plugin.py +471 -8
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_recursive_assertion.py +26 -17
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_recursive_compare_config.py +378 -2
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_rich_diff.py +431 -27
- assertpy2-2.20.0/tests/test_satisfy.py +288 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_snapshots.py +39 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_soft.py +64 -1
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_structural.py +276 -13
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_sync_eventually.py +12 -0
- assertpy2-2.20.0/tests/test_traceback.py +86 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_typing.py +58 -1
- assertpy2-2.20.0/tests/test_typing_claims.py +81 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_vacuity_contract.py +11 -4
- {assertpy2-2.18.0 → assertpy2-2.20.0}/uv.lock +561 -1226
- assertpy2-2.18.0/.github/workflows/mutation.yml +0 -66
- assertpy2-2.18.0/CONTRIBUTING.md +0 -50
- assertpy2-2.18.0/assertpy2/_engine/_diff.py +0 -366
- assertpy2-2.18.0/assertpy2/errors.py +0 -299
- assertpy2-2.18.0/docs/reference/matchers.md +0 -8
- assertpy2-2.18.0/tests/test_docs_examples.py +0 -60
- assertpy2-2.18.0/tests/test_inline_snapshot.py +0 -98
- assertpy2-2.18.0/tests/test_protocol_parity.py +0 -86
- assertpy2-2.18.0/tests/test_readme.py +0 -472
- assertpy2-2.18.0/tests/test_satisfy.py +0 -115
- assertpy2-2.18.0/tests/test_traceback.py +0 -38
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.codecov.yml +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.gitattributes +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/.gitignore +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/LICENSE +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/SECURITY.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/_snapshot_codec.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/date.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/guides/data.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/index.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/logo.svg +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/recipes.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/async.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/files.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/json.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/mkdocs.yml +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/conftest.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_bool.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_callable.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_class.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_core.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_description.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_dict.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_fail.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_file.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_in.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_none.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_not.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_string.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_type.py +0 -0
- {assertpy2-2.18.0 → assertpy2-2.20.0}/tests/test_warn.py +0 -0
|
@@ -15,6 +15,7 @@ permissions:
|
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
17
|
test:
|
|
18
|
+
timeout-minutes: 15
|
|
18
19
|
runs-on: ${{ matrix.os }}
|
|
19
20
|
strategy:
|
|
20
21
|
matrix:
|
|
@@ -31,15 +32,15 @@ jobs:
|
|
|
31
32
|
- os: macos-latest
|
|
32
33
|
python-version: "3.14"
|
|
33
34
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
35
36
|
with:
|
|
36
37
|
persist-credentials: false
|
|
37
38
|
|
|
38
39
|
- name: Install uv
|
|
39
|
-
uses: astral-sh/setup-uv@
|
|
40
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
40
41
|
|
|
41
42
|
- name: Set up Python ${{ matrix.python-version }}
|
|
42
|
-
uses: actions/setup-python@
|
|
43
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
43
44
|
with:
|
|
44
45
|
python-version: ${{ matrix.python-version }}
|
|
45
46
|
allow-prereleases: true
|
|
@@ -71,24 +72,63 @@ jobs:
|
|
|
71
72
|
run: uv run pytest tests/test_docs_examples.py
|
|
72
73
|
|
|
73
74
|
- name: Upload coverage to Codecov
|
|
74
|
-
|
|
75
|
+
# Only the full-deps cell. The bare 3.14/ubuntu cell skips every integration test and used
|
|
76
|
+
# to upload too: Codecov merges reports so the number stayed right, but one of the two was
|
|
77
|
+
# always a partial report sent at a 100% target.
|
|
78
|
+
if: matrix.full-deps
|
|
75
79
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
76
80
|
with:
|
|
77
81
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
78
82
|
files: coverage.xml
|
|
83
|
+
# A failed upload is a broken report, not a passing build. The 100% gate above is the hard
|
|
84
|
+
# guard either way, so this cannot turn a real coverage drop into a green run.
|
|
85
|
+
fail_ci_if_error: true
|
|
86
|
+
|
|
87
|
+
optional-integrations:
|
|
88
|
+
# allure and behave get their own job because installing them is incompatible with the coverage
|
|
89
|
+
# gate: with allure present the `except ImportError` fallback in pytest_plugin.py never runs, and
|
|
90
|
+
# without it the import itself never runs. The gating cell keeps the second state, this one holds
|
|
91
|
+
# the first. Everywhere else the two are tested against mocks, which accept any call and would
|
|
92
|
+
# wave through the renamed parameter or moved attribute a Dependabot bump can bring.
|
|
93
|
+
timeout-minutes: 10
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
97
|
+
with:
|
|
98
|
+
persist-credentials: false
|
|
99
|
+
|
|
100
|
+
- name: Install uv
|
|
101
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
102
|
+
|
|
103
|
+
- name: Set up Python
|
|
104
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
105
|
+
with:
|
|
106
|
+
python-version: "3.14"
|
|
107
|
+
|
|
108
|
+
- name: Install dependencies
|
|
109
|
+
run: uv sync --extra json --extra allure --extra behave
|
|
110
|
+
|
|
111
|
+
- name: Contract tests against the real libraries
|
|
112
|
+
run: uv run pytest -v tests/test_optional_integration_contracts.py
|
|
113
|
+
|
|
114
|
+
- name: The rest of the suite must also pass with both installed
|
|
115
|
+
# Installing allure flips `_HAS_ALLURE`, so every failing assertion in the suite takes the
|
|
116
|
+
# real attach path. That must not change any other test's outcome.
|
|
117
|
+
run: uv run pytest -q --ignore=tests/test_docs_examples.py tests
|
|
79
118
|
|
|
80
119
|
lint:
|
|
120
|
+
timeout-minutes: 10
|
|
81
121
|
runs-on: ubuntu-latest
|
|
82
122
|
steps:
|
|
83
|
-
- uses: actions/checkout@
|
|
123
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
84
124
|
with:
|
|
85
125
|
persist-credentials: false
|
|
86
126
|
|
|
87
127
|
- name: Install uv
|
|
88
|
-
uses: astral-sh/setup-uv@
|
|
128
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
89
129
|
|
|
90
130
|
- name: Set up Python
|
|
91
|
-
uses: actions/setup-python@
|
|
131
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
92
132
|
with:
|
|
93
133
|
python-version: "3.14"
|
|
94
134
|
|
|
@@ -110,13 +150,24 @@ jobs:
|
|
|
110
150
|
- name: Type check (pyright, public typing surface)
|
|
111
151
|
run: uv run pyright tests/test_typing.py
|
|
112
152
|
|
|
153
|
+
# the package itself carries diagnostics that are not defects (values typed `object`, guarded
|
|
154
|
+
# optional imports, deliberate overloads). They are recorded per rule with a reason, so a new
|
|
155
|
+
# one fails here instead of hiding in a count nobody reads.
|
|
156
|
+
- name: Type check (pyright, package baseline)
|
|
157
|
+
run: uv run pytest tests/test_pyright_baseline.py -q
|
|
158
|
+
|
|
113
159
|
ci-ok:
|
|
114
|
-
|
|
160
|
+
timeout-minutes: 5
|
|
161
|
+
needs: [test, optional-integrations, lint]
|
|
115
162
|
if: always()
|
|
116
163
|
runs-on: ubuntu-latest
|
|
117
164
|
steps:
|
|
118
165
|
- name: Verify required jobs succeeded
|
|
119
166
|
env:
|
|
120
167
|
TEST_RESULT: ${{ needs.test.result }}
|
|
168
|
+
INTEGRATIONS_RESULT: ${{ needs.optional-integrations.result }}
|
|
121
169
|
LINT_RESULT: ${{ needs.lint.result }}
|
|
122
|
-
run:
|
|
170
|
+
run: |
|
|
171
|
+
[ "$TEST_RESULT" = "success" ] &&
|
|
172
|
+
[ "$INTEGRATIONS_RESULT" = "success" ] &&
|
|
173
|
+
[ "$LINT_RESULT" = "success" ]
|
|
@@ -13,14 +13,15 @@ permissions:
|
|
|
13
13
|
|
|
14
14
|
jobs:
|
|
15
15
|
analyze:
|
|
16
|
+
timeout-minutes: 15
|
|
16
17
|
runs-on: ubuntu-latest
|
|
17
18
|
permissions:
|
|
18
19
|
security-events: write
|
|
19
20
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
22
|
with:
|
|
22
23
|
persist-credentials: false
|
|
23
|
-
- uses: github/codeql-action/init@
|
|
24
|
+
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
|
24
25
|
with:
|
|
25
26
|
languages: python
|
|
26
|
-
- uses: github/codeql-action/analyze@
|
|
27
|
+
- uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
|
@@ -21,20 +21,21 @@ concurrency:
|
|
|
21
21
|
|
|
22
22
|
jobs:
|
|
23
23
|
benchmarks:
|
|
24
|
+
timeout-minutes: 20
|
|
24
25
|
runs-on: ubuntu-latest
|
|
25
26
|
permissions:
|
|
26
27
|
contents: read
|
|
27
28
|
id-token: write
|
|
28
29
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
30
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
30
31
|
with:
|
|
31
32
|
persist-credentials: false
|
|
32
33
|
|
|
33
34
|
- name: Install uv
|
|
34
|
-
uses: astral-sh/setup-uv@
|
|
35
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
35
36
|
|
|
36
37
|
- name: Run benchmarks under CodSpeed
|
|
37
|
-
uses: CodSpeedHQ/action@
|
|
38
|
+
uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1
|
|
38
39
|
with:
|
|
39
40
|
mode: simulation
|
|
40
41
|
run: uv run --locked --group benchmark pytest benchmarks/ --codspeed --no-cov
|
|
@@ -15,17 +15,18 @@ concurrency:
|
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
17
|
build:
|
|
18
|
+
timeout-minutes: 10
|
|
18
19
|
runs-on: ubuntu-latest
|
|
19
20
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
22
|
with:
|
|
22
23
|
persist-credentials: false
|
|
23
24
|
|
|
24
25
|
- name: Install uv
|
|
25
|
-
uses: astral-sh/setup-uv@
|
|
26
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
26
27
|
|
|
27
28
|
- name: Set up Python
|
|
28
|
-
uses: actions/setup-python@
|
|
29
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
29
30
|
with:
|
|
30
31
|
python-version: "3.14"
|
|
31
32
|
|
|
@@ -42,6 +43,7 @@ jobs:
|
|
|
42
43
|
path: site
|
|
43
44
|
|
|
44
45
|
deploy:
|
|
46
|
+
timeout-minutes: 10
|
|
45
47
|
if: github.ref == 'refs/heads/main'
|
|
46
48
|
needs: build
|
|
47
49
|
runs-on: ubuntu-latest
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Mutation
|
|
2
|
+
|
|
3
|
+
# Coverage says a line ran; mutation testing asks whether anything would have NOTICED had it been
|
|
4
|
+
# wrong. It is a review tool, not a gate: most survivors are equivalent mutants (message text no test
|
|
5
|
+
# asserts on, formatting, `__repr__`), so the job stays green on survivors and publishes the list to
|
|
6
|
+
# triage. Weekly, not nightly - the score only moves when assertpy2/ or tests/ move, and a recurring
|
|
7
|
+
# red that means nothing is a red people learn to ignore.
|
|
8
|
+
#
|
|
9
|
+
# It does go red when the RUN itself is broken. mutmut runs the unmutated suite once to learn which
|
|
10
|
+
# tests cover which function, and exits 1 if that fails, so a test the trampoline breaks stops the
|
|
11
|
+
# whole run instead of quietly leaving mutants unjudged. That red is never routine: it means the
|
|
12
|
+
# config rotted, which is the failure the previous cosmic-ray setup hid for five weeks behind a
|
|
13
|
+
# stale module path.
|
|
14
|
+
#
|
|
15
|
+
# Linux only: mutmut forks and calls os.wait, and does not run on Windows.
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
schedule:
|
|
19
|
+
- cron: "0 4 * * 1" # weekly, Monday 04:00 UTC
|
|
20
|
+
workflow_dispatch: {}
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
25
|
+
concurrency:
|
|
26
|
+
group: ${{ github.workflow }}
|
|
27
|
+
cancel-in-progress: true
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
mutation:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
# 10 060 mutants over the whole package. Measured on four pinned cores with `--max-children 4`,
|
|
33
|
+
# which is this runner's shape: 41m15s. Slower runner cores put the real figure somewhere
|
|
34
|
+
# around an hour, so this leaves comfortable headroom without letting a hang burn two hours.
|
|
35
|
+
timeout-minutes: 120
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
38
|
+
with:
|
|
39
|
+
persist-credentials: false
|
|
40
|
+
|
|
41
|
+
- name: Install uv
|
|
42
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
43
|
+
|
|
44
|
+
- name: Set up Python
|
|
45
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
46
|
+
with:
|
|
47
|
+
python-version: "3.14"
|
|
48
|
+
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
# Every optional integration, matching the full-deps cell of ci.yml. A skipped test cannot
|
|
51
|
+
# kill a mutant, so mutating dataframe.py or json_mixin.py without pandas/polars/jsonschema
|
|
52
|
+
# installed reports its whole surface as untested.
|
|
53
|
+
run: uv sync --extra json --extra data --extra inline --group integrations --group mutation
|
|
54
|
+
|
|
55
|
+
- name: Run the mutants
|
|
56
|
+
# Exits non-zero when it cannot collect stats, which is what a baseline failure looks like.
|
|
57
|
+
# Survivors alone do not fail it.
|
|
58
|
+
#
|
|
59
|
+
# `--no-sync` on every `uv run` below: without it uv re-resolves the environment to the
|
|
60
|
+
# project's default groups and uninstalls the extras the step above just installed, so the
|
|
61
|
+
# mutants would be judged by a suite that skips pandas, polars and jsonschema.
|
|
62
|
+
run: uv run --no-sync mutmut run
|
|
63
|
+
|
|
64
|
+
- name: Report
|
|
65
|
+
if: always()
|
|
66
|
+
run: |
|
|
67
|
+
uv run --no-sync python scripts/mutation_report.py >> "$GITHUB_STEP_SUMMARY"
|
|
68
|
+
uv run --no-sync mutmut results > not-killed.txt
|
|
69
|
+
|
|
70
|
+
- name: Upload the mutants that lived
|
|
71
|
+
# `mutmut results` lists every mutant that was not killed, which is survivors plus timeouts
|
|
72
|
+
# plus the ones no test covered. Each line carries its own status, so the file is named for
|
|
73
|
+
# what it holds rather than for survivors alone - reading it as a survivor list overcounts.
|
|
74
|
+
if: always()
|
|
75
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
76
|
+
with:
|
|
77
|
+
name: mutants-not-killed
|
|
78
|
+
path: not-killed.txt
|
|
@@ -9,6 +9,7 @@ permissions:
|
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
11
|
publish:
|
|
12
|
+
timeout-minutes: 20
|
|
12
13
|
runs-on: ubuntu-latest
|
|
13
14
|
environment: pypi
|
|
14
15
|
permissions:
|
|
@@ -16,17 +17,17 @@ jobs:
|
|
|
16
17
|
contents: write
|
|
17
18
|
attestations: write
|
|
18
19
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
21
|
with:
|
|
21
22
|
persist-credentials: false
|
|
22
23
|
|
|
23
24
|
- name: Install uv
|
|
24
|
-
uses: astral-sh/setup-uv@
|
|
25
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
25
26
|
with:
|
|
26
27
|
enable-cache: false
|
|
27
28
|
|
|
28
29
|
- name: Set up Python
|
|
29
|
-
uses: actions/setup-python@
|
|
30
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
30
31
|
with:
|
|
31
32
|
python-version: "3.15"
|
|
32
33
|
allow-prereleases: true
|
|
@@ -65,7 +66,7 @@ jobs:
|
|
|
65
66
|
run: syft scan dir:dist -o "cyclonedx-json=provenance/assertpy2.sbom.cdx.json"
|
|
66
67
|
|
|
67
68
|
- name: Publish to PyPI
|
|
68
|
-
uses: pypa/gh-action-pypi-publish@
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
|
69
70
|
|
|
70
71
|
- name: Upload to GitHub Release
|
|
71
72
|
run: gh release upload "$TAG_NAME" dist/* provenance/*
|
|
@@ -11,15 +11,16 @@ permissions: read-all
|
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
13
|
analysis:
|
|
14
|
+
timeout-minutes: 10
|
|
14
15
|
runs-on: ubuntu-latest
|
|
15
16
|
permissions:
|
|
16
17
|
security-events: write
|
|
17
18
|
id-token: write
|
|
18
19
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
21
|
with:
|
|
21
22
|
persist-credentials: false
|
|
22
|
-
- uses: ossf/scorecard-action@
|
|
23
|
+
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
|
23
24
|
with:
|
|
24
25
|
results_file: results.sarif
|
|
25
26
|
results_format: sarif
|
|
@@ -29,6 +30,6 @@ jobs:
|
|
|
29
30
|
name: SARIF file
|
|
30
31
|
path: results.sarif
|
|
31
32
|
retention-days: 5
|
|
32
|
-
- uses: github/codeql-action/upload-sarif@
|
|
33
|
+
- uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
|
33
34
|
with:
|
|
34
35
|
sarif_file: results.sarif
|
|
@@ -15,11 +15,12 @@ permissions:
|
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
17
|
zizmor:
|
|
18
|
+
timeout-minutes: 10
|
|
18
19
|
runs-on: ubuntu-latest
|
|
19
20
|
permissions:
|
|
20
21
|
security-events: write
|
|
21
22
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
24
|
with:
|
|
24
25
|
persist-credentials: false
|
|
25
|
-
- uses: zizmorcore/zizmor-action@
|
|
26
|
+
- uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions of docs, tests, or code are welcome.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
1. For a new assertion or matcher, open an issue first. The API grows from demand, and a working
|
|
8
|
+
implementation on its own is not enough to land one
|
|
9
|
+
2. Fork the repo
|
|
10
|
+
3. Clone your fork (`git clone <your_fork_url>`)
|
|
11
|
+
4. Create a branch (`git checkout -b my_branch`)
|
|
12
|
+
5. Install dependencies: `uv sync`
|
|
13
|
+
6. Make your changes
|
|
14
|
+
7. Run the [verification pipeline](#verification-pipeline) and fix any issues
|
|
15
|
+
8. Commit using [Conventional Commits](#commit-style)
|
|
16
|
+
9. Push your branch (`git push origin my_branch`)
|
|
17
|
+
10. Open a [Pull Request](http://github.com/Solganis/assertpy2/pulls)
|
|
18
|
+
|
|
19
|
+
Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python 3.10+
|
|
24
|
+
- [uv](https://docs.astral.sh/uv/) as the package manager
|
|
25
|
+
|
|
26
|
+
## Verification pipeline
|
|
27
|
+
|
|
28
|
+
Run all checks before submitting a PR. Every step must pass.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# lint
|
|
32
|
+
uv run ruff check assertpy2/ tests/
|
|
33
|
+
|
|
34
|
+
# format
|
|
35
|
+
uv run ruff format --check assertpy2/ tests/
|
|
36
|
+
|
|
37
|
+
# type check
|
|
38
|
+
uv run ty check
|
|
39
|
+
|
|
40
|
+
# tests with coverage (must be 100%)
|
|
41
|
+
uv run pytest tests/ -v --cov=assertpy2 --cov-report=term-missing
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CI requires 100% code coverage.
|
|
45
|
+
|
|
46
|
+
## Commit style
|
|
47
|
+
|
|
48
|
+
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
Write tests for every new feature or bug fix. Use `assertpy2` assertions in tests.
|
|
53
|
+
|
|
54
|
+
## Documentation examples
|
|
55
|
+
|
|
56
|
+
Guide code blocks are executed (`tests/test_docs_examples.py`) and type-checked
|
|
57
|
+
(`tests/test_docs_typing.py`). To skip one, put an HTML comment above the fence:
|
|
58
|
+
|
|
59
|
+
| Marker | Executed | Type-checked | Use it for |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `<!-- docs-guard: skip -->` | no | no | pseudo-context, neither runnable nor checkable |
|
|
62
|
+
| `<!-- docs-guard: untyped -->` | yes | no | a dynamic assertion (`has_<attr>()`) |
|
|
63
|
+
| `<!-- docs-guard: raises -->` | no | yes | a block showing what a failure looks like |
|
|
64
|
+
| `<!-- docs-guard: type-error -->` | no | yes, and it **must** fail | a counter-example the page presents as rejected |
|
|
65
|
+
|
|
66
|
+
Setup a page assumes (a domain class, a repository, an HTTP response) goes in
|
|
67
|
+
`tests/docs_fixtures.py`, not into an extra block on the page.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.20.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
|
|
@@ -88,9 +88,9 @@ def test_user():
|
|
|
88
88
|
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
89
89
|
|
|
90
90
|
assert_that(user).contains_key("name", "age")
|
|
91
|
+
assert_that(user).contains_entry({"name": "Alice"})
|
|
91
92
|
assert_that(user["age"]).is_between(18, 120)
|
|
92
93
|
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
93
|
-
assert_that(user).has_name("Alice")
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
@@ -100,6 +100,7 @@ The [full documentation](https://solganis.github.io/assertpy2/) covers every ass
|
|
|
100
100
|
A fluent chain reads as one intent and replaces several bare asserts -<br>
|
|
101
101
|
and your IDE offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/concepts/type-safety/):
|
|
102
102
|
|
|
103
|
+
<!-- docs-guard: skip -->
|
|
103
104
|
```python
|
|
104
105
|
# bare - three statements, no autocomplete help
|
|
105
106
|
assert isinstance(items, list)
|
|
@@ -123,6 +124,7 @@ E {'status': 'active'} != {'status': 'disabled'}
|
|
|
123
124
|
|
|
124
125
|
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
125
126
|
|
|
127
|
+
<!-- docs-guard: skip -->
|
|
126
128
|
```python
|
|
127
129
|
assert_that(response).is_equal_to(expected)
|
|
128
130
|
```
|
|
@@ -133,6 +135,23 @@ assert_that(response).is_equal_to(expected)
|
|
|
133
135
|
|
|
134
136
|
The diff recurses through nested containers, and matcher predicates get the same path-level treatment. For dynamic fields like IDs or timestamps, assert a subset with [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
135
137
|
|
|
138
|
+
Matchers are ordinary values, so they also compose inside the expected structure itself, at any depth,
|
|
139
|
+
with or without the fluent chain:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
143
|
+
|
|
144
|
+
assert_that(response).is_equal_to(
|
|
145
|
+
{"id": match.greater_than(0), "user": {"name": "Alice", "age": match.between(18, 120)}, "tags": ["a", "b"]}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# or keep the bare `assert`, and pytest's own rewriting reports it
|
|
149
|
+
assert response == {"id": match.greater_than(0), "user": match.ignore(), "tags": ["a", "b"]}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The fluent form keeps the path-level diff, the bare form keeps pytest's. There are
|
|
153
|
+
[39 matchers](https://solganis.github.io/assertpy2/guides/matchers/), and they combine with `&`, `|` and `~`.
|
|
154
|
+
|
|
136
155
|
<p align="center">
|
|
137
156
|
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
|
|
138
157
|
</p>
|
|
@@ -156,6 +175,7 @@ Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
|
156
175
|
An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
|
|
157
176
|
`is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
|
|
158
177
|
|
|
178
|
+
<!-- docs-guard: skip -->
|
|
159
179
|
```python
|
|
160
180
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
161
181
|
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
@@ -164,6 +184,7 @@ order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
|
164
184
|
For API tests, [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms) validates a raw payload against a Pydantic model and narrows the chain to it,<br>
|
|
165
185
|
with `exact=True` catching silent contract drift:
|
|
166
186
|
|
|
187
|
+
<!-- docs-guard: skip -->
|
|
167
188
|
```python
|
|
168
189
|
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
169
190
|
```
|
|
@@ -202,13 +223,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
202
223
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
203
224
|
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
|
|
204
225
|
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
|
|
226
|
+
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
|
|
205
227
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
|
|
206
228
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
|
|
207
229
|
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
|
|
208
230
|
|
|
209
231
|
**Extensibility**
|
|
210
232
|
|
|
211
|
-
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()`
|
|
233
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
|
|
212
234
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
213
235
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
214
236
|
|
|
@@ -36,9 +36,9 @@ def test_user():
|
|
|
36
36
|
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
37
37
|
|
|
38
38
|
assert_that(user).contains_key("name", "age")
|
|
39
|
+
assert_that(user).contains_entry({"name": "Alice"})
|
|
39
40
|
assert_that(user["age"]).is_between(18, 120)
|
|
40
41
|
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
41
|
-
assert_that(user).has_name("Alice")
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
@@ -48,6 +48,7 @@ The [full documentation](https://solganis.github.io/assertpy2/) covers every ass
|
|
|
48
48
|
A fluent chain reads as one intent and replaces several bare asserts -<br>
|
|
49
49
|
and your IDE offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/concepts/type-safety/):
|
|
50
50
|
|
|
51
|
+
<!-- docs-guard: skip -->
|
|
51
52
|
```python
|
|
52
53
|
# bare - three statements, no autocomplete help
|
|
53
54
|
assert isinstance(items, list)
|
|
@@ -71,6 +72,7 @@ E {'status': 'active'} != {'status': 'disabled'}
|
|
|
71
72
|
|
|
72
73
|
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
73
74
|
|
|
75
|
+
<!-- docs-guard: skip -->
|
|
74
76
|
```python
|
|
75
77
|
assert_that(response).is_equal_to(expected)
|
|
76
78
|
```
|
|
@@ -81,6 +83,23 @@ assert_that(response).is_equal_to(expected)
|
|
|
81
83
|
|
|
82
84
|
The diff recurses through nested containers, and matcher predicates get the same path-level treatment. For dynamic fields like IDs or timestamps, assert a subset with [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
83
85
|
|
|
86
|
+
Matchers are ordinary values, so they also compose inside the expected structure itself, at any depth,
|
|
87
|
+
with or without the fluent chain:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
91
|
+
|
|
92
|
+
assert_that(response).is_equal_to(
|
|
93
|
+
{"id": match.greater_than(0), "user": {"name": "Alice", "age": match.between(18, 120)}, "tags": ["a", "b"]}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# or keep the bare `assert`, and pytest's own rewriting reports it
|
|
97
|
+
assert response == {"id": match.greater_than(0), "user": match.ignore(), "tags": ["a", "b"]}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The fluent form keeps the path-level diff, the bare form keeps pytest's. There are
|
|
101
|
+
[39 matchers](https://solganis.github.io/assertpy2/guides/matchers/), and they combine with `&`, `|` and `~`.
|
|
102
|
+
|
|
84
103
|
<p align="center">
|
|
85
104
|
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
|
|
86
105
|
</p>
|
|
@@ -104,6 +123,7 @@ Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
|
104
123
|
An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
|
|
105
124
|
`is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
|
|
106
125
|
|
|
126
|
+
<!-- docs-guard: skip -->
|
|
107
127
|
```python
|
|
108
128
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
109
129
|
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
@@ -112,6 +132,7 @@ order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
|
112
132
|
For API tests, [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms) validates a raw payload against a Pydantic model and narrows the chain to it,<br>
|
|
113
133
|
with `exact=True` catching silent contract drift:
|
|
114
134
|
|
|
135
|
+
<!-- docs-guard: skip -->
|
|
115
136
|
```python
|
|
116
137
|
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
117
138
|
```
|
|
@@ -150,13 +171,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
150
171
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
151
172
|
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
|
|
152
173
|
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
|
|
174
|
+
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
|
|
153
175
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
|
|
154
176
|
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
|
|
155
177
|
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
|
|
156
178
|
|
|
157
179
|
**Extensibility**
|
|
158
180
|
|
|
159
|
-
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()`
|
|
181
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
|
|
160
182
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
161
183
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
162
184
|
|