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.
Files changed (182) hide show
  1. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/ci.yml +24 -6
  2. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/codeql.yml +2 -2
  3. assertpy2-2.17.0/.github/workflows/codspeed.yml +40 -0
  4. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/docs.yml +1 -1
  5. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/mutation.yml +5 -2
  6. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/publish.yml +11 -2
  7. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/scorecard.yml +1 -1
  8. assertpy2-2.17.0/PKG-INFO +225 -0
  9. assertpy2-2.17.0/README.md +173 -0
  10. assertpy2-2.17.0/assertpy2/_engine/__init__.py +1 -0
  11. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_compare.py +9 -5
  12. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_diff.py +41 -21
  13. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_introspection.py +3 -2
  14. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_mixin_base.py +1 -1
  15. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_typing.py +31 -3
  16. assertpy2-2.17.0/assertpy2/_inline.py +101 -0
  17. assertpy2-2.15.0/assertpy2/matchers.py → assertpy2-2.17.0/assertpy2/_matcher_impls.py +42 -385
  18. assertpy2-2.17.0/assertpy2/_satisfies.py +586 -0
  19. assertpy2-2.17.0/assertpy2/_snapshot_codec.py +168 -0
  20. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/assertpy.py +46 -40
  21. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/async_assertions.py +24 -7
  22. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/base.py +27 -581
  23. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/bytes_mixin.py +2 -2
  24. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/collection.py +12 -10
  25. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/contains.py +61 -31
  26. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dataframe.py +22 -5
  27. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/date.py +15 -2
  28. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dict.py +2 -2
  29. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/dynamic.py +16 -10
  30. assertpy2-2.17.0/assertpy2/errors.py +289 -0
  31. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/exception.py +9 -8
  32. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/extracting.py +65 -23
  33. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/file.py +6 -5
  34. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/helpers.py +134 -36
  35. assertpy2-2.17.0/assertpy2/json_mixin.py +369 -0
  36. assertpy2-2.17.0/assertpy2/matchers.py +421 -0
  37. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/numeric.py +49 -12
  38. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/pytest_plugin.py +34 -63
  39. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/snapshot.py +176 -204
  40. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/string.py +9 -11
  41. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/warning.py +6 -5
  42. assertpy2-2.17.0/benchmarks/test_perf.py +123 -0
  43. assertpy2-2.17.0/docs/assets/diff-string.svg +59 -0
  44. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/concepts}/type-safety.md +111 -59
  45. assertpy2-2.15.0/docs/extending.md → assertpy2-2.17.0/docs/extending/custom-assertions.md +3 -1
  46. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/extending}/integrations.md +25 -17
  47. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/getting-started}/comparison.md +86 -95
  48. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/getting-started}/migration.md +34 -30
  49. assertpy2-2.15.0/docs/getting-started.md → assertpy2-2.17.0/docs/getting-started/quickstart.md +20 -7
  50. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/assertions.md +105 -40
  51. assertpy2-2.17.0/docs/guides/data.md +151 -0
  52. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/errors.md +92 -17
  53. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/fluent.md +12 -7
  54. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/matchers.md +39 -15
  55. {assertpy2-2.15.0/docs → assertpy2-2.17.0/docs/guides}/testing.md +133 -64
  56. assertpy2-2.17.0/docs/index.md +153 -0
  57. assertpy2-2.17.0/docs/recipes.md +213 -0
  58. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/async.md +1 -1
  59. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/entry-points.md +1 -1
  60. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/errors.md +4 -3
  61. assertpy2-2.17.0/docs/reference/matchers.md +8 -0
  62. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/overview.md +12 -8
  63. assertpy2-2.17.0/docs/stylesheets/extra.css +56 -0
  64. {assertpy2-2.15.0 → assertpy2-2.17.0}/mkdocs.yml +33 -12
  65. assertpy2-2.17.0/mkdocs_hooks.py +16 -0
  66. {assertpy2-2.15.0 → assertpy2-2.17.0}/pyproject.toml +14 -4
  67. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/conftest.py +13 -0
  68. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_async.py +62 -6
  69. assertpy2-2.17.0/tests/test_attrs_support.py +155 -0
  70. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_boundary_cases.py +1 -1
  71. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_bytes.py +24 -0
  72. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_custom_dict.py +42 -0
  73. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dataframe.py +27 -0
  74. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_datetime.py +15 -0
  75. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dict.py +3 -1
  76. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dict_compare.py +18 -0
  77. assertpy2-2.17.0/tests/test_docs_examples.py +60 -0
  78. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_dynamic.py +36 -0
  79. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_equals.py +98 -1
  80. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_errors.py +60 -5
  81. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_expected_exception.py +13 -0
  82. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_expected_warning.py +25 -0
  83. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_extracting.py +111 -3
  84. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_file.py +13 -0
  85. assertpy2-2.17.0/tests/test_inline_record.py +126 -0
  86. assertpy2-2.17.0/tests/test_inline_snapshot.py +82 -0
  87. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_list.py +22 -1
  88. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_matchers.py +47 -0
  89. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_namedtuple.py +21 -1
  90. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_not.py +40 -0
  91. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_numbers.py +35 -0
  92. assertpy2-2.17.0/tests/test_openapi_contract.py +374 -0
  93. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_overloads.py +2 -2
  94. assertpy2-2.17.0/tests/test_property_attrs.py +83 -0
  95. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_property_based.py +344 -3
  96. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_protocol_parity.py +3 -3
  97. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_pytest_plugin.py +36 -3
  98. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_assertion.py +1 -1
  99. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare.py +42 -0
  100. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_config.py +161 -0
  101. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_rich_diff.py +87 -3
  102. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_snapshots.py +31 -0
  103. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_string.py +11 -0
  104. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_structural.py +1 -1
  105. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_typing.py +2 -2
  106. assertpy2-2.17.0/tests/test_vacuity_contract.py +97 -0
  107. {assertpy2-2.15.0 → assertpy2-2.17.0}/uv.lock +888 -627
  108. assertpy2-2.15.0/PKG-INFO +0 -222
  109. assertpy2-2.15.0/README.md +0 -174
  110. assertpy2-2.15.0/assertpy2/errors.py +0 -178
  111. assertpy2-2.15.0/assertpy2/json_mixin.py +0 -163
  112. assertpy2-2.15.0/docs/data.md +0 -84
  113. assertpy2-2.15.0/docs/index.md +0 -103
  114. assertpy2-2.15.0/docs/reference/matchers.md +0 -7
  115. assertpy2-2.15.0/docs/stylesheets/extra.css +0 -13
  116. {assertpy2-2.15.0 → assertpy2-2.17.0}/.codecov.yml +0 -0
  117. {assertpy2-2.15.0 → assertpy2-2.17.0}/.gitattributes +0 -0
  118. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/dependabot.yml +0 -0
  119. {assertpy2-2.15.0 → assertpy2-2.17.0}/.github/workflows/zizmor.yml +0 -0
  120. {assertpy2-2.15.0 → assertpy2-2.17.0}/.gitignore +0 -0
  121. {assertpy2-2.15.0 → assertpy2-2.17.0}/CONTRIBUTING.md +0 -0
  122. {assertpy2-2.15.0 → assertpy2-2.17.0}/LICENSE +0 -0
  123. {assertpy2-2.15.0 → assertpy2-2.17.0}/SECURITY.md +0 -0
  124. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/__init__.py +0 -0
  125. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_compat.py +0 -0
  126. {assertpy2-2.15.0/assertpy2 → assertpy2-2.17.0/assertpy2/_engine}/_contract.py +0 -0
  127. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/behave_matchers.py +0 -0
  128. {assertpy2-2.15.0 → assertpy2-2.17.0}/assertpy2/py.typed +0 -0
  129. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-equal.png +0 -0
  130. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-equal.svg +0 -0
  131. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-gallery.png +0 -0
  132. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-match.svg +0 -0
  133. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-sequence.svg +0 -0
  134. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/assets/diff-set.svg +0 -0
  135. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/logo-dark.svg +0 -0
  136. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/logo.svg +0 -0
  137. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/bytes.md +0 -0
  138. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/collections.md +0 -0
  139. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/containment.md +0 -0
  140. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/core.md +0 -0
  141. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dataframes.md +0 -0
  142. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dates.md +0 -0
  143. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dicts.md +0 -0
  144. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/dynamic.md +0 -0
  145. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/exceptions.md +0 -0
  146. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/extracting.md +0 -0
  147. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/files.md +0 -0
  148. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/json.md +0 -0
  149. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/numbers.md +0 -0
  150. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/snapshots.md +0 -0
  151. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/strings.md +0 -0
  152. {assertpy2-2.15.0 → assertpy2-2.17.0}/docs/reference/warnings.md +0 -0
  153. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_behave_matchers.py +0 -0
  154. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_bool.py +0 -0
  155. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_callable.py +0 -0
  156. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_chaining.py +0 -0
  157. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_class.py +0 -0
  158. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_collection.py +0 -0
  159. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_core.py +0 -0
  160. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_custom_list.py +0 -0
  161. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_description.py +0 -0
  162. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_extensions.py +0 -0
  163. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_fail.py +0 -0
  164. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_grouped_soft.py +0 -0
  165. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_in.py +0 -0
  166. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_iterable_cluster.py +0 -0
  167. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_json.py +0 -0
  168. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_matcher_registry.py +0 -0
  169. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_none.py +0 -0
  170. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_pipeline.py +0 -0
  171. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_readme.py +0 -0
  172. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_attrs.py +0 -0
  173. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_recursive_compare_pydantic.py +0 -0
  174. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_regex_groups.py +0 -0
  175. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_same_as.py +0 -0
  176. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_satisfy.py +0 -0
  177. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_soft.py +0 -0
  178. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_soft_fail.py +0 -0
  179. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_sync_eventually.py +0 -0
  180. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_traceback.py +0 -0
  181. {assertpy2-2.15.0 → assertpy2-2.17.0}/tests/test_type.py +0 -0
  182. {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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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
- run: uv run pytest -v --cov=assertpy2 --cov-report=term-missing --cov-report=xml tests
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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
- run: '[ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.lint.result }}" = "success" ]'
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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
23
+ - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
24
24
  with:
25
25
  languages: python
26
- - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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
- timeout = 30.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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: Prepare provenance bundles
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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
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&amp;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").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
146
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
147
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
148
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
149
+ - `assert_that(b"\x89PNG").` &rarr; 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&amp;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").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
94
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
95
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
96
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
97
+ - `assert_that(b"\x89PNG").` &rarr; 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)."""