assertpy2 2.19.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.19.0 → assertpy2-2.20.0}/.github/workflows/ci.yml +51 -3
- assertpy2-2.20.0/.github/workflows/mutation.yml +78 -0
- assertpy2-2.20.0/CONTRIBUTING.md +67 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/PKG-INFO +3 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/README.md +2 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/__init__.py +25 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_compare.py +27 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_contract.py +2 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_diff.py +92 -75
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_introspection.py +51 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_mixin_base.py +5 -0
- assertpy2-2.20.0/assertpy2/_engine/_path.py +100 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_typing.py +25 -3
- assertpy2-2.20.0/assertpy2/_hints.py +325 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_inline.py +7 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_matcher_impls.py +173 -37
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_satisfies.py +58 -26
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/assertpy.py +271 -48
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/async_assertions.py +10 -4
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/base.py +15 -6
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/bytes_mixin.py +15 -6
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/collection.py +7 -3
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/contains.py +92 -49
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/errors.py +168 -9
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/extracting.py +4 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/helpers.py +71 -8
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/matchers.py +80 -5
- assertpy2-2.20.0/assertpy2/outcome.py +118 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/pytest_plugin.py +114 -37
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/snapshot.py +70 -32
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/string.py +19 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/benchmarks/test_perf.py +42 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/concepts/type-safety.md +14 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/extending/integrations.md +43 -3
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/getting-started/comparison.md +4 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/getting-started/quickstart.md +6 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/assertions.md +13 -2
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/errors.md +253 -9
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/fluent.md +8 -3
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/matchers.md +78 -4
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/testing.md +12 -3
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/core.md +5 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/entry-points.md +18 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/errors.md +6 -0
- assertpy2-2.20.0/docs/reference/matchers.md +21 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/pyproject.toml +63 -2
- 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.19.0 → assertpy2-2.20.0}/tests/test_attrs_support.py +1 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_bytes.py +54 -3
- assertpy2-2.20.0/tests/test_check.py +121 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_collection.py +71 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_datetime.py +25 -0
- {assertpy2-2.19.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.19.0 → assertpy2-2.20.0}/tests/test_equals.py +62 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_errors.py +146 -9
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_extensions.py +90 -13
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_extracting.py +21 -0
- assertpy2-2.20.0/tests/test_hints.py +417 -0
- {assertpy2-2.19.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.19.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.19.0 → assertpy2-2.20.0}/tests/test_matcher_registry.py +90 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_matchers.py +136 -0
- assertpy2-2.20.0/tests/test_message_elision.py +118 -0
- {assertpy2-2.19.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.19.0 → assertpy2-2.20.0}/tests/test_pipeline.py +15 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_property_based.py +150 -15
- assertpy2-2.20.0/tests/test_public_surface.py +132 -0
- assertpy2-2.20.0/tests/test_pyright_baseline.py +54 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_pytest_plugin.py +308 -17
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_recursive_assertion.py +26 -17
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_recursive_compare_config.py +92 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_rich_diff.py +183 -27
- assertpy2-2.20.0/tests/test_satisfy.py +288 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_soft.py +64 -1
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_structural.py +260 -13
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_sync_eventually.py +12 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_typing.py +39 -2
- assertpy2-2.20.0/tests/test_typing_claims.py +81 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_vacuity_contract.py +11 -4
- {assertpy2-2.19.0 → assertpy2-2.20.0}/uv.lock +271 -934
- assertpy2-2.19.0/.github/workflows/mutation.yml +0 -66
- assertpy2-2.19.0/CONTRIBUTING.md +0 -50
- assertpy2-2.19.0/docs/reference/matchers.md +0 -8
- assertpy2-2.19.0/tests/test_docs_examples.py +0 -60
- assertpy2-2.19.0/tests/test_docs_typing.py +0 -77
- assertpy2-2.19.0/tests/test_inline_snapshot.py +0 -98
- assertpy2-2.19.0/tests/test_readme.py +0 -472
- assertpy2-2.19.0/tests/test_satisfy.py +0 -115
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.codecov.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.gitattributes +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/codspeed.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/.gitignore +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/LICENSE +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/SECURITY.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/_snapshot_codec.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/dataframe.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/date.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/file.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/extending/custom-assertions.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/guides/data.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/index.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/logo.svg +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/recipes.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/async.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/files.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/json.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/mkdocs.yml +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/conftest.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_async.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_bool.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_callable.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_class.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_core.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_dataframe.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_description.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_dict.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_exception_context.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_fail.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_file.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_in.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_json.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_none.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_not.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_protocol_parity.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_string.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_type.py +0 -0
- {assertpy2-2.19.0 → assertpy2-2.20.0}/tests/test_warn.py +0 -0
|
@@ -72,11 +72,49 @@ jobs:
|
|
|
72
72
|
run: uv run pytest tests/test_docs_examples.py
|
|
73
73
|
|
|
74
74
|
- name: Upload coverage to Codecov
|
|
75
|
-
|
|
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
|
|
76
79
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
77
80
|
with:
|
|
78
81
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
79
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
|
|
80
118
|
|
|
81
119
|
lint:
|
|
82
120
|
timeout-minutes: 10
|
|
@@ -112,14 +150,24 @@ jobs:
|
|
|
112
150
|
- name: Type check (pyright, public typing surface)
|
|
113
151
|
run: uv run pyright tests/test_typing.py
|
|
114
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
|
+
|
|
115
159
|
ci-ok:
|
|
116
160
|
timeout-minutes: 5
|
|
117
|
-
needs: [test, lint]
|
|
161
|
+
needs: [test, optional-integrations, lint]
|
|
118
162
|
if: always()
|
|
119
163
|
runs-on: ubuntu-latest
|
|
120
164
|
steps:
|
|
121
165
|
- name: Verify required jobs succeeded
|
|
122
166
|
env:
|
|
123
167
|
TEST_RESULT: ${{ needs.test.result }}
|
|
168
|
+
INTEGRATIONS_RESULT: ${{ needs.optional-integrations.result }}
|
|
124
169
|
LINT_RESULT: ${{ needs.lint.result }}
|
|
125
|
-
run:
|
|
170
|
+
run: |
|
|
171
|
+
[ "$TEST_RESULT" = "success" ] &&
|
|
172
|
+
[ "$INTEGRATIONS_RESULT" = "success" ] &&
|
|
173
|
+
[ "$LINT_RESULT" = "success" ]
|
|
@@ -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
|
|
@@ -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
|
|
@@ -223,13 +223,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
223
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.
|
|
224
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()`).
|
|
225
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.
|
|
226
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.
|
|
227
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).
|
|
228
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.
|
|
229
230
|
|
|
230
231
|
**Extensibility**
|
|
231
232
|
|
|
232
|
-
- [**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 `&`, `|`, `~`.
|
|
233
234
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
234
235
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
235
236
|
|
|
@@ -171,13 +171,14 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
171
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.
|
|
172
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()`).
|
|
173
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.
|
|
174
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.
|
|
175
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).
|
|
176
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.
|
|
177
178
|
|
|
178
179
|
**Extensibility**
|
|
179
180
|
|
|
180
|
-
- [**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 `&`, `|`, `~`.
|
|
181
182
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
182
183
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
183
184
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .assertpy import (
|
|
2
|
+
CheckBuilder,
|
|
2
3
|
NegatedBuilder,
|
|
3
4
|
SoftAssertionCollector,
|
|
4
5
|
WarningLoggingAdapter,
|
|
@@ -14,9 +15,26 @@ from .assertpy import (
|
|
|
14
15
|
soft_fail,
|
|
15
16
|
)
|
|
16
17
|
from .async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder
|
|
17
|
-
from .errors import
|
|
18
|
+
from .errors import (
|
|
19
|
+
AssertionFailure,
|
|
20
|
+
DiffEntry,
|
|
21
|
+
DiffResult,
|
|
22
|
+
PollSample,
|
|
23
|
+
PollTrace,
|
|
24
|
+
Step,
|
|
25
|
+
VacuousAssertionWarning,
|
|
26
|
+
)
|
|
18
27
|
from .file import contents_of
|
|
19
|
-
from .matchers import
|
|
28
|
+
from .matchers import (
|
|
29
|
+
BaseMatcher,
|
|
30
|
+
Matcher,
|
|
31
|
+
MatchResult,
|
|
32
|
+
clear_custom_matchers,
|
|
33
|
+
match,
|
|
34
|
+
register_matcher,
|
|
35
|
+
unregister_matcher,
|
|
36
|
+
)
|
|
37
|
+
from .outcome import AssertionOutcome
|
|
20
38
|
from .snapshot import (
|
|
21
39
|
SnapshotCreatedWarning,
|
|
22
40
|
SnapshotKeyReusedWarning,
|
|
@@ -26,9 +44,13 @@ from .snapshot import (
|
|
|
26
44
|
|
|
27
45
|
__all__ = [
|
|
28
46
|
"AssertionFailure",
|
|
47
|
+
"AssertionOutcome",
|
|
29
48
|
"AsyncAssertionBuilder",
|
|
49
|
+
"BaseMatcher",
|
|
50
|
+
"CheckBuilder",
|
|
30
51
|
"DiffEntry",
|
|
31
52
|
"DiffResult",
|
|
53
|
+
"MatchResult",
|
|
32
54
|
"Matcher",
|
|
33
55
|
"NegatedBuilder",
|
|
34
56
|
"PollSample",
|
|
@@ -37,6 +59,7 @@ __all__ = [
|
|
|
37
59
|
"SnapshotKeyReusedWarning",
|
|
38
60
|
"SnapshotUpdatedWarning",
|
|
39
61
|
"SoftAssertionCollector",
|
|
62
|
+
"Step",
|
|
40
63
|
"SyncAssertionBuilder",
|
|
41
64
|
"VacuousAssertionWarning",
|
|
42
65
|
"WarningLoggingAdapter",
|
|
@@ -331,3 +331,30 @@ def _spec_matches(key, value, specs) -> bool:
|
|
|
331
331
|
elif spec == key:
|
|
332
332
|
return True
|
|
333
333
|
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _config_note(config: _CompareConfig | None) -> str:
|
|
337
|
+
"""A newline plus an echo of the comparison settings that were in force, or ``""`` when none were.
|
|
338
|
+
|
|
339
|
+
``is_equal_to`` already names ``ignore`` and ``include`` inside its sentence, but nothing reports
|
|
340
|
+
the rest, and they are what a reader is questioning when a field they thought was tolerated still
|
|
341
|
+
failed. Rendered only for a non-default config: `_build_compare_config()` returns ``None`` when
|
|
342
|
+
every setting is at its default, so the check costs nothing and the line never appears on the
|
|
343
|
+
ordinary failure.
|
|
344
|
+
|
|
345
|
+
It goes on its own line rather than into the sentence, so the original message stays a prefix and
|
|
346
|
+
a `match=` or ``startswith`` written against it keeps working.
|
|
347
|
+
"""
|
|
348
|
+
if config is None:
|
|
349
|
+
return ""
|
|
350
|
+
parts = []
|
|
351
|
+
if config.tolerance is not None:
|
|
352
|
+
parts.append(f"tolerance={config.tolerance!r}")
|
|
353
|
+
if config.comparators:
|
|
354
|
+
keys = ", ".join(sorted(key.__name__ if isinstance(key, type) else str(key) for key in config.comparators))
|
|
355
|
+
parts.append(f"comparators for {keys}")
|
|
356
|
+
if config.ignore_null:
|
|
357
|
+
parts.append("ignore_null=True")
|
|
358
|
+
if config.strict_types:
|
|
359
|
+
parts.append("strict_types=True")
|
|
360
|
+
return "\ncompared with " + ", ".join(parts) if parts else ""
|
|
@@ -17,6 +17,8 @@ def _submodel(annotation: object) -> type | None:
|
|
|
17
17
|
"""The nested model class for an annotation, peeling ``Optional`` / ``list`` / ``tuple``; else ``None``."""
|
|
18
18
|
origin = typing.get_origin(annotation)
|
|
19
19
|
args = typing.get_args(annotation)
|
|
20
|
+
# both names are needed below 3.14, where `Optional[X]` and `X | None` had distinct origins; from
|
|
21
|
+
# 3.14 they are the same object and the second test is redundant on the interpreters we gate on
|
|
20
22
|
if origin is typing.Union or origin is types.UnionType:
|
|
21
23
|
non_none = [arg for arg in args if arg is not type(None)]
|
|
22
24
|
return _submodel(non_none[0]) if len(non_none) == 1 else None
|