assertpy2 2.15.0__tar.gz → 2.17.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.15.0 → assertpy2-2.17.0}/.github/workflows/ci.yml +24 -6
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/codeql.yml +2 -2
- assertpy2-2.17.0/.github/workflows/codspeed.yml +40 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/docs.yml +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/mutation.yml +5 -2
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/publish.yml +11 -2
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/scorecard.yml +1 -1
- assertpy2-2.17.0/PKG-INFO +225 -0
- assertpy2-2.17.0/README.md +173 -0
- assertpy2-2.17.0/assertpy2/_engine/__init__.py +1 -0
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_compare.py +9 -5
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_diff.py +41 -21
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_introspection.py +3 -2
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_mixin_base.py +1 -1
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_typing.py +31 -3
- assertpy2-2.17.0/assertpy2/_inline.py +101 -0
- assertpy2-2.15.0/assertpy2/matchers.py → assertpy2-2.17.0/assertpy2/_matcher_impls.py +42 -385
- assertpy2-2.17.0/assertpy2/_satisfies.py +586 -0
- assertpy2-2.17.0/assertpy2/_snapshot_codec.py +168 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/assertpy.py +46 -40
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/async_assertions.py +24 -7
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/base.py +27 -581
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/bytes_mixin.py +2 -2
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/collection.py +12 -10
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/contains.py +61 -31
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dataframe.py +22 -5
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/date.py +15 -2
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dict.py +2 -2
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dynamic.py +16 -10
- assertpy2-2.17.0/assertpy2/errors.py +289 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/exception.py +9 -8
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/extracting.py +65 -23
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/file.py +6 -5
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/helpers.py +134 -36
- assertpy2-2.17.0/assertpy2/json_mixin.py +369 -0
- assertpy2-2.17.0/assertpy2/matchers.py +421 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/numeric.py +49 -12
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/pytest_plugin.py +34 -63
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/snapshot.py +176 -204
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/string.py +9 -11
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/warning.py +6 -5
- assertpy2-2.17.0/benchmarks/test_perf.py +123 -0
- assertpy2-2.17.0/docs/assets/diff-string.svg +59 -0
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/concepts}/type-safety.md +111 -59
- assertpy2-2.15.0/docs/extending.md → assertpy2-2.17.0/docs/extending/custom-assertions.md +3 -1
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/extending}/integrations.md +25 -17
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/getting-started}/comparison.md +86 -95
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/getting-started}/migration.md +34 -30
- assertpy2-2.15.0/docs/getting-started.md → assertpy2-2.17.0/docs/getting-started/quickstart.md +20 -7
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/assertions.md +105 -40
- assertpy2-2.17.0/docs/guides/data.md +151 -0
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/errors.md +92 -17
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/fluent.md +12 -7
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/matchers.md +39 -15
- {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/testing.md +133 -64
- assertpy2-2.17.0/docs/index.md +153 -0
- assertpy2-2.17.0/docs/recipes.md +213 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/async.md +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/entry-points.md +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/errors.md +4 -3
- assertpy2-2.17.0/docs/reference/matchers.md +8 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/overview.md +12 -8
- assertpy2-2.17.0/docs/stylesheets/extra.css +56 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/mkdocs.yml +33 -12
- assertpy2-2.17.0/mkdocs_hooks.py +16 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/pyproject.toml +14 -4
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/conftest.py +13 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_async.py +62 -6
- assertpy2-2.17.0/tests/test_attrs_support.py +155 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_boundary_cases.py +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_bytes.py +24 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_custom_dict.py +42 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dataframe.py +27 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_datetime.py +15 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dict.py +3 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dict_compare.py +18 -0
- assertpy2-2.17.0/tests/test_docs_examples.py +60 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dynamic.py +36 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_equals.py +98 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_errors.py +60 -5
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_expected_exception.py +13 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_expected_warning.py +25 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_extracting.py +111 -3
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_file.py +13 -0
- assertpy2-2.17.0/tests/test_inline_record.py +126 -0
- assertpy2-2.17.0/tests/test_inline_snapshot.py +82 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_list.py +22 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_matchers.py +47 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_namedtuple.py +21 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_not.py +40 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_numbers.py +35 -0
- assertpy2-2.17.0/tests/test_openapi_contract.py +374 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_overloads.py +2 -2
- assertpy2-2.17.0/tests/test_property_attrs.py +83 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_property_based.py +344 -3
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_protocol_parity.py +3 -3
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_pytest_plugin.py +36 -3
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_assertion.py +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare.py +42 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_config.py +161 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_rich_diff.py +87 -3
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_snapshots.py +31 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_string.py +11 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_structural.py +1 -1
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_typing.py +2 -2
- assertpy2-2.17.0/tests/test_vacuity_contract.py +97 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/uv.lock +888 -627
- assertpy2-2.15.0/PKG-INFO +0 -222
- assertpy2-2.15.0/README.md +0 -174
- assertpy2-2.15.0/assertpy2/errors.py +0 -178
- assertpy2-2.15.0/assertpy2/json_mixin.py +0 -163
- assertpy2-2.15.0/docs/data.md +0 -84
- assertpy2-2.15.0/docs/index.md +0 -103
- assertpy2-2.15.0/docs/reference/matchers.md +0 -7
- assertpy2-2.15.0/docs/stylesheets/extra.css +0 -13
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.codecov.yml +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.gitattributes +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/.gitignore +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/LICENSE +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/SECURITY.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_compat.py +0 -0
- {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_contract.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/logo.svg +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/core.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/files.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/json.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_bool.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_callable.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_class.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_collection.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_core.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_description.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_fail.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_in.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_json.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_none.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_readme.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_soft.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_type.py +0 -0
- {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_warn.py +0 -0
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
persist-credentials: false
|
|
37
37
|
|
|
38
38
|
- name: Install uv
|
|
39
|
-
uses: astral-sh/setup-uv@
|
|
39
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
40
40
|
|
|
41
41
|
- name: Set up Python ${{ matrix.python-version }}
|
|
42
42
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
@@ -50,10 +50,25 @@ jobs:
|
|
|
50
50
|
|
|
51
51
|
- name: Install dependencies (all integrations)
|
|
52
52
|
if: matrix.full-deps
|
|
53
|
-
run: uv sync --extra json --extra data --group integrations
|
|
53
|
+
run: uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
|
|
54
54
|
|
|
55
55
|
- name: Test with coverage
|
|
56
|
-
|
|
56
|
+
if: '!matrix.full-deps'
|
|
57
|
+
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml --ignore=tests/test_docs_examples.py tests
|
|
58
|
+
|
|
59
|
+
- name: Test with coverage (enforce 100%)
|
|
60
|
+
# Only the full-deps cell has every integration installed and 0 skips, so it is
|
|
61
|
+
# the only cell that can reach 100%. This is the hard local gate; Codecov's status
|
|
62
|
+
# is reporting on top, not the sole guard (it uploads async and can fail open).
|
|
63
|
+
# The doc-example guard is excluded here so its runs never pad the coverage number.
|
|
64
|
+
if: matrix.full-deps
|
|
65
|
+
run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml --cov-fail-under=100 --ignore=tests/test_docs_examples.py tests
|
|
66
|
+
|
|
67
|
+
- name: Guard doc examples
|
|
68
|
+
# Runs once (docs are platform-independent) and without coverage, so executing the
|
|
69
|
+
# README/guide snippets can't mask a real coverage gap in the test suite.
|
|
70
|
+
if: matrix.full-deps
|
|
71
|
+
run: uv run pytest tests/test_docs_examples.py
|
|
57
72
|
|
|
58
73
|
- name: Upload coverage to Codecov
|
|
59
74
|
if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
|
|
@@ -70,7 +85,7 @@ jobs:
|
|
|
70
85
|
persist-credentials: false
|
|
71
86
|
|
|
72
87
|
- name: Install uv
|
|
73
|
-
uses: astral-sh/setup-uv@
|
|
88
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
74
89
|
|
|
75
90
|
- name: Set up Python
|
|
76
91
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
@@ -78,7 +93,7 @@ jobs:
|
|
|
78
93
|
python-version: "3.14"
|
|
79
94
|
|
|
80
95
|
- name: Install dependencies
|
|
81
|
-
run: uv sync --extra json --group typecheck
|
|
96
|
+
run: uv sync --extra json --extra inline --group typecheck
|
|
82
97
|
|
|
83
98
|
- name: Ruff check
|
|
84
99
|
run: uv run ruff check .
|
|
@@ -101,4 +116,7 @@ jobs:
|
|
|
101
116
|
runs-on: ubuntu-latest
|
|
102
117
|
steps:
|
|
103
118
|
- name: Verify required jobs succeeded
|
|
104
|
-
|
|
119
|
+
env:
|
|
120
|
+
TEST_RESULT: ${{ needs.test.result }}
|
|
121
|
+
LINT_RESULT: ${{ needs.lint.result }}
|
|
122
|
+
run: '[ "$TEST_RESULT" = "success" ] && [ "$LINT_RESULT" = "success" ]'
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
21
21
|
with:
|
|
22
22
|
persist-credentials: false
|
|
23
|
-
- uses: github/codeql-action/init@
|
|
23
|
+
- uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
|
24
24
|
with:
|
|
25
25
|
languages: python
|
|
26
|
-
- uses: github/codeql-action/analyze@
|
|
26
|
+
- uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CodSpeed
|
|
2
|
+
|
|
3
|
+
# Continuous performance gate: run the micro-benchmarks (benchmarks/test_perf.py) under CodSpeed's CPU
|
|
4
|
+
# simulation, which measures a deterministic instruction count rather than wall time, so a runner's noise
|
|
5
|
+
# never flakes it. CodSpeed compares each commit to the base and reports a pass/fail on the PR. A public
|
|
6
|
+
# repo uploads over OIDC (id-token), so no token secret is needed - only the CodSpeed GitHub App installed.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: [main]
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
benchmarks:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
30
|
+
with:
|
|
31
|
+
persist-credentials: false
|
|
32
|
+
|
|
33
|
+
- name: Install uv
|
|
34
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
35
|
+
|
|
36
|
+
- name: Run benchmarks under CodSpeed
|
|
37
|
+
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
|
|
38
|
+
with:
|
|
39
|
+
mode: simulation
|
|
40
|
+
run: uv run --locked --group benchmark pytest benchmarks/ --codspeed --no-cov
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
persist-credentials: false
|
|
23
23
|
|
|
24
24
|
- name: Install uv
|
|
25
|
-
uses: astral-sh/setup-uv@
|
|
25
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
26
26
|
|
|
27
27
|
- name: Set up Python
|
|
28
28
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
|
|
28
28
|
- name: Install uv
|
|
29
|
-
uses: astral-sh/setup-uv@
|
|
29
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
30
30
|
|
|
31
31
|
- name: Set up Python
|
|
32
32
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
@@ -43,7 +43,10 @@ jobs:
|
|
|
43
43
|
cat > cosmic-ray.toml <<EOF
|
|
44
44
|
[cosmic-ray]
|
|
45
45
|
module-path = "assertpy2/${MODULE}.py"
|
|
46
|
-
|
|
46
|
+
# Bounds each test run, including the baseline that runs the whole suite once.
|
|
47
|
+
# Must exceed the full-suite runtime under 21-way matrix contention, or the baseline
|
|
48
|
+
# spuriously times out and the module's job dies before mutating anything.
|
|
49
|
+
timeout = 120.0
|
|
47
50
|
excluded-modules = []
|
|
48
51
|
test-command = ".venv/bin/python -m pytest tests/ -x -q -p no:cacheprovider"
|
|
49
52
|
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
persist-credentials: false
|
|
22
22
|
|
|
23
23
|
- name: Install uv
|
|
24
|
-
uses: astral-sh/setup-uv@
|
|
24
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
25
25
|
with:
|
|
26
26
|
enable-cache: false
|
|
27
27
|
|
|
@@ -46,15 +46,24 @@ jobs:
|
|
|
46
46
|
with:
|
|
47
47
|
subject-path: dist/*.tar.gz
|
|
48
48
|
|
|
49
|
-
- name:
|
|
49
|
+
- name: Collect provenance (sigstore bundles + in-toto)
|
|
50
50
|
run: |
|
|
51
51
|
mkdir -p provenance
|
|
52
52
|
cp "$WHEEL_BUNDLE" "provenance/$(basename dist/*.whl).sigstore.json"
|
|
53
53
|
cp "$SDIST_BUNDLE" "provenance/$(basename dist/*.tar.gz).sigstore.json"
|
|
54
|
+
jq -c '.dsseEnvelope' "$WHEEL_BUNDLE" > provenance/provenance.intoto.jsonl
|
|
55
|
+
jq -c '.dsseEnvelope' "$SDIST_BUNDLE" >> provenance/provenance.intoto.jsonl
|
|
56
|
+
test -s provenance/provenance.intoto.jsonl
|
|
54
57
|
env:
|
|
55
58
|
WHEEL_BUNDLE: ${{ steps.attest-wheel.outputs.bundle-path }}
|
|
56
59
|
SDIST_BUNDLE: ${{ steps.attest-sdist.outputs.bundle-path }}
|
|
57
60
|
|
|
61
|
+
- name: Install syft
|
|
62
|
+
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
63
|
+
|
|
64
|
+
- name: Generate SBOM (CycloneDX)
|
|
65
|
+
run: syft scan dir:dist -o "cyclonedx-json=provenance/assertpy2.sbom.cdx.json"
|
|
66
|
+
|
|
58
67
|
- name: Publish to PyPI
|
|
59
68
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
60
69
|
|
|
@@ -29,6 +29,6 @@ jobs:
|
|
|
29
29
|
name: SARIF file
|
|
30
30
|
path: results.sarif
|
|
31
31
|
retention-days: 5
|
|
32
|
-
- uses: github/codeql-action/upload-sarif@
|
|
32
|
+
- uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
|
33
33
|
with:
|
|
34
34
|
sarif_file: results.sarif
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assertpy2
|
|
3
|
+
Version: 2.17.0
|
|
4
|
+
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
|
+
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
|
+
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
7
|
+
Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
|
|
8
|
+
Author-email: Justin Shacklette <justin@saturnboy.com>
|
|
9
|
+
Maintainer-email: Solganis <solganis.dev@gmail.com>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: assert,assert_that,assertion,assertpy,assertthat,fluent,matchers,pytest,test,testing,type-safe,unittest
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
26
|
+
Classifier: Topic :: Software Development
|
|
27
|
+
Classifier: Topic :: Software Development :: Testing
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
|
30
|
+
Provides-Extra: allure
|
|
31
|
+
Requires-Dist: allure-pytest>=2.13; extra == 'allure'
|
|
32
|
+
Provides-Extra: behave
|
|
33
|
+
Requires-Dist: behave>=1.2.6; extra == 'behave'
|
|
34
|
+
Provides-Extra: data
|
|
35
|
+
Requires-Dist: numpy>=1.26; extra == 'data'
|
|
36
|
+
Requires-Dist: pandas>=2.0; extra == 'data'
|
|
37
|
+
Requires-Dist: polars>=1.0; extra == 'data'
|
|
38
|
+
Provides-Extra: inline
|
|
39
|
+
Requires-Dist: asttokens>=2.0; extra == 'inline'
|
|
40
|
+
Requires-Dist: executing>=2.2; extra == 'inline'
|
|
41
|
+
Provides-Extra: json
|
|
42
|
+
Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
|
|
43
|
+
Requires-Dist: jsonschema>=4.18; extra == 'json'
|
|
44
|
+
Requires-Dist: referencing>=0.28.4; extra == 'json'
|
|
45
|
+
Provides-Extra: numpy
|
|
46
|
+
Requires-Dist: numpy>=1.26; extra == 'numpy'
|
|
47
|
+
Provides-Extra: pandas
|
|
48
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
49
|
+
Provides-Extra: polars
|
|
50
|
+
Requires-Dist: polars>=1.0; extra == 'polars'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<picture>
|
|
55
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
56
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
|
|
57
|
+
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
58
|
+
</picture>
|
|
59
|
+
<br>
|
|
60
|
+
<b>The fully-typed fluent assertion library for Python</b><br>
|
|
61
|
+
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Solganis/assertpy2/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
66
|
+
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
67
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/v/assertpy2" alt="PyPI version"></a>
|
|
68
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
69
|
+
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
70
|
+
<br>
|
|
71
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
|
|
72
|
+
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
73
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/quickstart/">Quick start</a></h2>
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from assertpy2 import assert_that
|
|
86
|
+
|
|
87
|
+
def test_user():
|
|
88
|
+
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
89
|
+
|
|
90
|
+
assert_that(user).contains_key("name", "age")
|
|
91
|
+
assert_that(user["age"]).is_between(18, 120)
|
|
92
|
+
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
93
|
+
assert_that(user).has_name("Alice")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
97
|
+
|
|
98
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
99
|
+
|
|
100
|
+
A fluent chain reads as one intent and replaces several bare asserts -<br>
|
|
101
|
+
and your IDE offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/concepts/type-safety/):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# bare - three statements, no autocomplete help
|
|
105
|
+
assert isinstance(items, list)
|
|
106
|
+
assert len(items) == 3
|
|
107
|
+
assert "admin" in items
|
|
108
|
+
|
|
109
|
+
# assertpy2 - one chain, type-aware autocomplete
|
|
110
|
+
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The real difference shows up on failure. Plain `assert` dumps both structures and leaves you to find the two wrong fields:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
assert response == expected
|
|
117
|
+
E AssertionError: assert {'id': 1, ...} == {'id': 1, ...}
|
|
118
|
+
E Omitting 1 identical items, use -vv to show
|
|
119
|
+
E Differing items:
|
|
120
|
+
E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Alice', 'role': 'admin'}}
|
|
121
|
+
E {'status': 'active'} != {'status': 'disabled'}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
assert_that(response).is_equal_to(expected)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
<p align="center">
|
|
131
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
|
|
132
|
+
</p>
|
|
133
|
+
|
|
134
|
+
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
|
+
|
|
136
|
+
<p align="center">
|
|
137
|
+
<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
|
+
</p>
|
|
139
|
+
|
|
140
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
141
|
+
|
|
142
|
+
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
143
|
+
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
144
|
+
|
|
145
|
+
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
146
|
+
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
147
|
+
- `assert_that(Path("/tmp")).` → path methods: `exists`, `is_file`, `is_readable`, ...
|
|
148
|
+
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
149
|
+
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
150
|
+
|
|
151
|
+
9 type-specific Protocols instead of one `Any`.<br>
|
|
152
|
+
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
153
|
+
|
|
154
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
155
|
+
|
|
156
|
+
An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
|
|
157
|
+
`is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
161
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
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
|
+
with `exact=True` catching silent contract drift:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
<h2 align="center">Features</h2>
|
|
172
|
+
|
|
173
|
+
**Fluent API**
|
|
174
|
+
|
|
175
|
+
- [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
|
|
176
|
+
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
177
|
+
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
178
|
+
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
179
|
+
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
180
|
+
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
181
|
+
- [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
|
|
182
|
+
|
|
183
|
+
**Type safety**
|
|
184
|
+
|
|
185
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/concepts/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
186
|
+
- [**Typed narrowing**](https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value): `.value` hands the checked value back, statically narrowed by `is_not_none()`, `is_instance_of()`, and a [`satisfies()` `TypeIs` predicate](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced).
|
|
187
|
+
- [**Contract testing**](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints.
|
|
188
|
+
|
|
189
|
+
**Built-in types**
|
|
190
|
+
|
|
191
|
+
- [Strings](https://solganis.github.io/assertpy2/guides/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/guides/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/guides/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/guides/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/guides/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/guides/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/guides/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/guides/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/guides/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/guides/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions).
|
|
192
|
+
- [**Bytes assertions**](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
193
|
+
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/guides/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
194
|
+
- [**Dict comparison**](https://solganis.github.io/assertpy2/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type.
|
|
195
|
+
- [**Recursive comparison**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures.
|
|
196
|
+
- [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
197
|
+
|
|
198
|
+
**Testing**
|
|
199
|
+
|
|
200
|
+
- [**Soft assertions**](https://solganis.github.io/assertpy2/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`.
|
|
201
|
+
- [**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.
|
|
202
|
+
- [**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()`).
|
|
203
|
+
- [**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.
|
|
204
|
+
- [**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.
|
|
205
|
+
- [**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).
|
|
206
|
+
- [**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.
|
|
207
|
+
|
|
208
|
+
**Extensibility**
|
|
209
|
+
|
|
210
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
|
|
211
|
+
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
212
|
+
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
213
|
+
|
|
214
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
215
|
+
|
|
216
|
+
- [**Allure**](https://solganis.github.io/assertpy2/extending/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
|
|
217
|
+
- [**Behave**](https://solganis.github.io/assertpy2/extending/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
218
|
+
- [**JSON**](https://solganis.github.io/assertpy2/guides/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
|
|
219
|
+
- [**Data frames**](https://solganis.github.io/assertpy2/extending/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
<p align="center">
|
|
224
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
|
|
225
|
+
</p>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
|
|
5
|
+
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
6
|
+
</picture>
|
|
7
|
+
<br>
|
|
8
|
+
<b>The fully-typed fluent assertion library for Python</b><br>
|
|
9
|
+
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://github.com/Solganis/assertpy2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Solganis/assertpy2/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
14
|
+
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
15
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/v/assertpy2" alt="PyPI version"></a>
|
|
16
|
+
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
17
|
+
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
18
|
+
<br>
|
|
19
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
|
|
20
|
+
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
21
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/quickstart/">Quick start</a></h2>
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from assertpy2 import assert_that
|
|
34
|
+
|
|
35
|
+
def test_user():
|
|
36
|
+
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
|
|
37
|
+
|
|
38
|
+
assert_that(user).contains_key("name", "age")
|
|
39
|
+
assert_that(user["age"]).is_between(18, 120)
|
|
40
|
+
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
|
|
41
|
+
assert_that(user).has_name("Alice")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
45
|
+
|
|
46
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
47
|
+
|
|
48
|
+
A fluent chain reads as one intent and replaces several bare asserts -<br>
|
|
49
|
+
and your IDE offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/concepts/type-safety/):
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# bare - three statements, no autocomplete help
|
|
53
|
+
assert isinstance(items, list)
|
|
54
|
+
assert len(items) == 3
|
|
55
|
+
assert "admin" in items
|
|
56
|
+
|
|
57
|
+
# assertpy2 - one chain, type-aware autocomplete
|
|
58
|
+
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The real difference shows up on failure. Plain `assert` dumps both structures and leaves you to find the two wrong fields:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
assert response == expected
|
|
65
|
+
E AssertionError: assert {'id': 1, ...} == {'id': 1, ...}
|
|
66
|
+
E Omitting 1 identical items, use -vv to show
|
|
67
|
+
E Differing items:
|
|
68
|
+
E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Alice', 'role': 'admin'}}
|
|
69
|
+
E {'status': 'active'} != {'status': 'disabled'}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
assert_that(response).is_equal_to(expected)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
<p align="center">
|
|
79
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
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
|
+
|
|
84
|
+
<p align="center">
|
|
85
|
+
<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
|
+
</p>
|
|
87
|
+
|
|
88
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
89
|
+
|
|
90
|
+
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
91
|
+
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
92
|
+
|
|
93
|
+
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
94
|
+
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
95
|
+
- `assert_that(Path("/tmp")).` → path methods: `exists`, `is_file`, `is_readable`, ...
|
|
96
|
+
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
97
|
+
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
98
|
+
|
|
99
|
+
9 type-specific Protocols instead of one `Any`.<br>
|
|
100
|
+
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
101
|
+
|
|
102
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
103
|
+
|
|
104
|
+
An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
|
|
105
|
+
`is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
109
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
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
|
+
with `exact=True` catching silent contract drift:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
<h2 align="center">Features</h2>
|
|
120
|
+
|
|
121
|
+
**Fluent API**
|
|
122
|
+
|
|
123
|
+
- [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
|
|
124
|
+
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
125
|
+
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
126
|
+
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
127
|
+
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
128
|
+
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
129
|
+
- [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
|
|
130
|
+
|
|
131
|
+
**Type safety**
|
|
132
|
+
|
|
133
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/concepts/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
134
|
+
- [**Typed narrowing**](https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value): `.value` hands the checked value back, statically narrowed by `is_not_none()`, `is_instance_of()`, and a [`satisfies()` `TypeIs` predicate](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced).
|
|
135
|
+
- [**Contract testing**](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints.
|
|
136
|
+
|
|
137
|
+
**Built-in types**
|
|
138
|
+
|
|
139
|
+
- [Strings](https://solganis.github.io/assertpy2/guides/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/guides/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/guides/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/guides/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/guides/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/guides/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/guides/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/guides/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/guides/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/guides/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions).
|
|
140
|
+
- [**Bytes assertions**](https://solganis.github.io/assertpy2/guides/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
141
|
+
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/guides/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
142
|
+
- [**Dict comparison**](https://solganis.github.io/assertpy2/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type.
|
|
143
|
+
- [**Recursive comparison**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures.
|
|
144
|
+
- [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
145
|
+
|
|
146
|
+
**Testing**
|
|
147
|
+
|
|
148
|
+
- [**Soft assertions**](https://solganis.github.io/assertpy2/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`.
|
|
149
|
+
- [**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.
|
|
150
|
+
- [**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()`).
|
|
151
|
+
- [**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.
|
|
152
|
+
- [**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.
|
|
153
|
+
- [**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).
|
|
154
|
+
- [**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.
|
|
155
|
+
|
|
156
|
+
**Extensibility**
|
|
157
|
+
|
|
158
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` for domain-specific matchers, composable with `&`, `|`, `~`.
|
|
159
|
+
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
160
|
+
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
161
|
+
|
|
162
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
163
|
+
|
|
164
|
+
- [**Allure**](https://solganis.github.io/assertpy2/extending/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
|
|
165
|
+
- [**Behave**](https://solganis.github.io/assertpy2/extending/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
166
|
+
- [**JSON**](https://solganis.github.io/assertpy2/guides/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
|
|
167
|
+
- [**Data frames**](https://solganis.github.io/assertpy2/extending/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
<p align="center">
|
|
172
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/LICENSE">BSD 3-Clause License</a>
|
|
173
|
+
</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal comparison, diff, introspection and typing engine (private, no public API)."""
|