assertpy2 2.20.1__tar.gz → 2.22.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 (222) hide show
  1. assertpy2-2.22.0/CONTRIBUTING.md +83 -0
  2. {assertpy2-2.20.1 → assertpy2-2.22.0}/PKG-INFO +107 -42
  3. {assertpy2-2.20.1 → assertpy2-2.22.0}/README.md +105 -40
  4. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/__init__.py +2 -0
  5. assertpy2-2.22.0/assertpy2/_clustering.py +499 -0
  6. assertpy2-2.22.0/assertpy2/_dangling.py +292 -0
  7. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_compare.py +31 -10
  8. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_diff.py +76 -15
  9. assertpy2-2.22.0/assertpy2/_engine/_equality.py +205 -0
  10. assertpy2-2.22.0/assertpy2/_engine/_membership.py +312 -0
  11. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_mixin_base.py +16 -0
  12. assertpy2-2.22.0/assertpy2/_engine/_ordering.py +128 -0
  13. assertpy2-2.22.0/assertpy2/_engine/_require.py +142 -0
  14. assertpy2-2.22.0/assertpy2/_engine/_size.py +33 -0
  15. assertpy2-2.22.0/assertpy2/_engine/_text.py +39 -0
  16. assertpy2-2.22.0/assertpy2/_engine/_typing.py +632 -0
  17. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_hints.py +90 -31
  18. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_matcher_impls.py +377 -87
  19. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_satisfies.py +44 -36
  20. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/assertpy.py +226 -89
  21. assertpy2-2.22.0/assertpy2/async_assertions.py +756 -0
  22. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/base.py +33 -16
  23. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/bytes_mixin.py +4 -3
  24. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/collection.py +66 -56
  25. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/contains.py +43 -49
  26. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dataframe.py +3 -6
  27. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/date.py +9 -9
  28. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dict.py +3 -2
  29. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/errors.py +18 -2
  30. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/exception.py +181 -13
  31. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/extracting.py +15 -12
  32. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/file.py +5 -8
  33. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/helpers.py +75 -196
  34. assertpy2-2.22.0/assertpy2/http_mixin.py +224 -0
  35. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/json_mixin.py +21 -1
  36. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/matchers.py +95 -23
  37. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/numeric.py +27 -22
  38. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/pytest_plugin.py +413 -18
  39. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/snapshot.py +4 -8
  40. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/string.py +43 -71
  41. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/warning.py +6 -3
  42. {assertpy2-2.20.1 → assertpy2-2.22.0}/benchmarks/test_perf.py +137 -10
  43. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/concepts/stability.md +31 -7
  44. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/concepts/type-safety.md +36 -5
  45. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/extending/custom-assertions.md +3 -1
  46. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/extending/integrations.md +21 -10
  47. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/comparison.md +3 -3
  48. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/migration.md +4 -2
  49. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/assertions.md +113 -10
  50. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/data.md +18 -1
  51. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/errors.md +163 -8
  52. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/fluent.md +15 -5
  53. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/matchers.md +9 -4
  54. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/testing.md +121 -3
  55. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/index.md +18 -0
  56. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/recipes.md +32 -6
  57. assertpy2-2.22.0/docs/reference/http.md +6 -0
  58. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/overview.md +2 -1
  59. {assertpy2-2.20.1 → assertpy2-2.22.0}/mkdocs.yml +1 -0
  60. {assertpy2-2.20.1 → assertpy2-2.22.0}/pyproject.toml +48 -6
  61. assertpy2-2.22.0/tests/api_snapshot.json +3724 -0
  62. assertpy2-2.22.0/tests/api_surface.py +381 -0
  63. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/docs_fixtures.py +1 -0
  64. assertpy2-2.22.0/tests/group_compat.py +31 -0
  65. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/pyright_baseline.py +12 -13
  66. assertpy2-2.22.0/tests/test_api_compatibility.py +386 -0
  67. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_api_vocabulary.py +2 -0
  68. assertpy2-2.22.0/tests/test_async.py +1490 -0
  69. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_attrs_support.py +15 -0
  70. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_bytes.py +7 -7
  71. assertpy2-2.22.0/tests/test_clustering.py +1578 -0
  72. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_collection.py +49 -4
  73. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_core.py +3 -3
  74. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_custom_dict.py +5 -5
  75. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_custom_list.py +2 -2
  76. assertpy2-2.22.0/tests/test_dangling.py +906 -0
  77. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_datetime.py +57 -33
  78. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dict.py +52 -8
  79. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_docs_typing.py +4 -2
  80. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_equals.py +23 -0
  81. assertpy2-2.22.0/tests/test_evaluation_core.py +1211 -0
  82. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_exception_context.py +33 -15
  83. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_expected_exception.py +189 -10
  84. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_expected_warning.py +1 -1
  85. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_extensions.py +66 -5
  86. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_extracting.py +5 -5
  87. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_file.py +2 -2
  88. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_hints.py +372 -1
  89. assertpy2-2.22.0/tests/test_http.py +599 -0
  90. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_inline_record.py +72 -0
  91. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_inline_snapshot.py +51 -1
  92. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_iterable_cluster.py +17 -13
  93. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_json.py +2 -2
  94. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_list.py +11 -11
  95. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matcher_registry.py +2 -2
  96. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matchers.py +46 -5
  97. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_not.py +187 -2
  98. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_numbers.py +78 -47
  99. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_pipeline.py +7 -7
  100. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_property_based.py +981 -23
  101. assertpy2-2.22.0/tests/test_protocol_parity.py +603 -0
  102. assertpy2-2.22.0/tests/test_public_surface.py +325 -0
  103. assertpy2-2.22.0/tests/test_pyright_baseline.py +102 -0
  104. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_pytest_plugin.py +635 -26
  105. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_assertion.py +6 -2
  106. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_config.py +221 -5
  107. assertpy2-2.22.0/tests/test_refusals.py +425 -0
  108. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_regex_groups.py +2 -2
  109. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_rich_diff.py +778 -0
  110. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_satisfy.py +183 -5
  111. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_snapshots.py +411 -0
  112. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_soft.py +261 -0
  113. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_string.py +40 -36
  114. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_structural.py +10 -5
  115. assertpy2-2.22.0/tests/test_surface_conformance.py +396 -0
  116. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_sync_eventually.py +19 -1
  117. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_type.py +8 -5
  118. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_typing.py +88 -16
  119. assertpy2-2.22.0/tests/test_typing_negative.py +204 -0
  120. assertpy2-2.22.0/tests/typing_cases.py +370 -0
  121. assertpy2-2.22.0/tests/typing_negative_baseline.py +257 -0
  122. assertpy2-2.20.1/.codecov.yml +0 -10
  123. assertpy2-2.20.1/.gitattributes +0 -2
  124. assertpy2-2.20.1/.github/dependabot.yml +0 -31
  125. assertpy2-2.20.1/.github/workflows/ci.yml +0 -181
  126. assertpy2-2.20.1/.github/workflows/codeql.yml +0 -27
  127. assertpy2-2.20.1/.github/workflows/codspeed.yml +0 -41
  128. assertpy2-2.20.1/.github/workflows/docs.yml +0 -59
  129. assertpy2-2.20.1/.github/workflows/mutation.yml +0 -78
  130. assertpy2-2.20.1/.github/workflows/publish.yml +0 -75
  131. assertpy2-2.20.1/.github/workflows/scorecard.yml +0 -35
  132. assertpy2-2.20.1/.github/workflows/zizmor.yml +0 -26
  133. assertpy2-2.20.1/CONTRIBUTING.md +0 -67
  134. assertpy2-2.20.1/assertpy2/_engine/_typing.py +0 -430
  135. assertpy2-2.20.1/assertpy2/async_assertions.py +0 -388
  136. assertpy2-2.20.1/scripts/mutation_report.py +0 -142
  137. assertpy2-2.20.1/tests/test_async.py +0 -628
  138. assertpy2-2.20.1/tests/test_protocol_parity.py +0 -219
  139. assertpy2-2.20.1/tests/test_public_surface.py +0 -168
  140. assertpy2-2.20.1/tests/test_pyright_baseline.py +0 -54
  141. assertpy2-2.20.1/uv.lock +0 -2785
  142. {assertpy2-2.20.1 → assertpy2-2.22.0}/.gitignore +0 -0
  143. {assertpy2-2.20.1 → assertpy2-2.22.0}/LICENSE +0 -0
  144. {assertpy2-2.20.1 → assertpy2-2.22.0}/SECURITY.md +0 -0
  145. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/__init__.py +0 -0
  146. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_compat.py +0 -0
  147. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_contract.py +0 -0
  148. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_introspection.py +0 -0
  149. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_path.py +0 -0
  150. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_inline.py +0 -0
  151. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_snapshot_codec.py +0 -0
  152. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/behave_matchers.py +0 -0
  153. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dynamic.py +0 -0
  154. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/outcome.py +0 -0
  155. {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/py.typed +0 -0
  156. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-equal.png +0 -0
  157. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-equal.svg +0 -0
  158. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-gallery.png +0 -0
  159. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-match.svg +0 -0
  160. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-sequence.svg +0 -0
  161. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-set.svg +0 -0
  162. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-string.svg +0 -0
  163. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/quickstart.md +0 -0
  164. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/logo-dark.svg +0 -0
  165. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/logo.svg +0 -0
  166. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/async.md +0 -0
  167. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/bytes.md +0 -0
  168. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/collections.md +0 -0
  169. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/containment.md +0 -0
  170. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/core.md +0 -0
  171. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dataframes.md +0 -0
  172. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dates.md +0 -0
  173. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dicts.md +0 -0
  174. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dynamic.md +0 -0
  175. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/entry-points.md +0 -0
  176. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/errors.md +0 -0
  177. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/exceptions.md +0 -0
  178. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/extracting.md +0 -0
  179. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/files.md +0 -0
  180. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/json.md +0 -0
  181. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/matchers.md +0 -0
  182. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/numbers.md +0 -0
  183. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/snapshots.md +0 -0
  184. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/strings.md +0 -0
  185. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/warnings.md +0 -0
  186. {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/stylesheets/extra.css +0 -0
  187. {assertpy2-2.20.1 → assertpy2-2.22.0}/mkdocs_hooks.py +0 -0
  188. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/conftest.py +0 -0
  189. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_behave_matchers.py +0 -0
  190. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_bool.py +0 -0
  191. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_boundary_cases.py +0 -0
  192. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_callable.py +0 -0
  193. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_chaining.py +0 -0
  194. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_check.py +0 -0
  195. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_class.py +0 -0
  196. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dataframe.py +0 -0
  197. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_description.py +0 -0
  198. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dict_compare.py +0 -0
  199. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_docs_examples.py +0 -0
  200. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dynamic.py +0 -0
  201. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_errors.py +0 -0
  202. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_fail.py +0 -0
  203. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_grouped_soft.py +0 -0
  204. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_in.py +0 -0
  205. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_match_result.py +0 -0
  206. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matcher_parity.py +0 -0
  207. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_message_elision.py +0 -0
  208. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_namedtuple.py +0 -0
  209. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_none.py +0 -0
  210. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_openapi_contract.py +0 -0
  211. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_optional_integration_contracts.py +0 -0
  212. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_overloads.py +0 -0
  213. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_property_attrs.py +0 -0
  214. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare.py +0 -0
  215. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_attrs.py +0 -0
  216. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_pydantic.py +0 -0
  217. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_same_as.py +0 -0
  218. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_soft_fail.py +0 -0
  219. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_traceback.py +0 -0
  220. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_typing_claims.py +0 -0
  221. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_vacuity_contract.py +0 -0
  222. {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_warn.py +0 -0
@@ -0,0 +1,83 @@
1
+ # Contributing
2
+
3
+ Contributions of docs, tests, or code are welcome.
4
+
5
+ ## Workflow
6
+
7
+ 1. For a new assertion or matcher, open an issue first. The API grows from demand, and a working
8
+ implementation on its own is not enough to land one
9
+ 2. Fork the repo
10
+ 3. Clone your fork (`git clone <your_fork_url>`)
11
+ 4. Create a branch (`git checkout -b my_branch`)
12
+ 5. Install dependencies with the [one sync command](#verification-pipeline) below, not a bare `uv sync`
13
+ 6. Make your changes
14
+ 7. Run the [verification pipeline](#verification-pipeline) and fix any issues
15
+ 8. Commit using [Conventional Commits](#commit-style)
16
+ 9. Push your branch (`git push origin my_branch`)
17
+ 10. Open a [Pull Request](http://github.com/Solganis/assertpy2/pulls)
18
+
19
+ Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
20
+
21
+ ## Requirements
22
+
23
+ - Python 3.10+
24
+ - [uv](https://docs.astral.sh/uv/) as the package manager
25
+
26
+ ## Verification pipeline
27
+
28
+ Run all checks before submitting a PR. Every step must pass.
29
+
30
+ Install exactly this set. A bare `uv sync` leaves out the optional libraries several tests import, and
31
+ those tests fail rather than skip:
32
+
33
+ ```bash
34
+ uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
35
+ ```
36
+
37
+ ```bash
38
+ uv run ruff check .
39
+ uv run ruff format --check .
40
+ uv run ty check assertpy2/ tests/test_typing.py
41
+ uv run pytest --cov=assertpy2 --cov-fail-under=100 --ignore=tests/test_docs_examples.py tests
42
+ uv run pytest tests/test_docs_examples.py tests/test_typing_claims.py
43
+ uv run --group docs mkdocs build --strict
44
+ ```
45
+
46
+ The public typing surface is checked by three checkers. Run them after the block above, not before:
47
+ `uv run --group typecheck` adds the checkers to the same `.venv` and leaves them there, and a coverage
48
+ run with them installed collects a different set:
49
+
50
+ ```bash
51
+ uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
52
+ uv run --group typecheck pyright tests/test_typing.py
53
+ uv run --group typecheck pytest tests/test_pyright_baseline.py
54
+ ```
55
+
56
+ Three details that cost time if you meet them the hard way:
57
+
58
+ - `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
59
+ `tests/`, which is full of deliberate negative typing cases: calls that must not type-check.
60
+ - `--all-extras` is not a shortcut for the sync line. Installing `allure` or `behave` makes 100%
61
+ coverage unreachable by construction, because the `except ImportError` branch guarding them stops
62
+ being executed. CI never installs them for the coverage job either.
63
+ - `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
64
+ needs the `docs-examples` group and a different collection.
65
+
66
+ CI requires 100% code coverage.
67
+
68
+ ## Commit style
69
+
70
+ Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
71
+
72
+ ## Tests
73
+
74
+ Write tests for every new feature or bug fix. Use `assertpy2` assertions in tests.
75
+
76
+ ## Documentation examples
77
+
78
+ Guide code blocks are executed and type-checked in CI, so an example you add has to run. A block that
79
+ cannot (pseudo-context, a deliberate failure, a rejected counter-example) is marked with an HTML comment
80
+ above the fence: `tests/test_docs_examples.py` lists the markers and what each one exempts.
81
+
82
+ Setup a page assumes (a domain class, a repository, an HTTP response) goes in
83
+ `tests/docs_fixtures.py`, not into an extra block on the page.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.20.1
3
+ Version: 2.22.0
4
4
  Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
@@ -57,7 +57,7 @@ Description-Content-Type: text/markdown
57
57
  <img src="docs/logo.svg" alt="assertpy2" width="280">
58
58
  </picture>
59
59
  <br>
60
- <b>The fully-typed fluent assertion library for Python</b><br>
60
+ <b>A fully typed fluent assertion library for Python</b><br>
61
61
  A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
62
62
  </p>
63
63
 
@@ -95,23 +95,44 @@ def test_user():
95
95
 
96
96
  The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
97
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/):
98
+ <table>
99
+ <tr>
100
+ <td valign="top" width="50%">
101
+ <a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
102
+ A recursive diff names the exact path that differs, in color, instead of dumping both structures.
103
+ </td>
104
+ <td valign="top" width="50%">
105
+ <a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
106
+ <code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit.
107
+ </td>
108
+ </tr>
109
+ <tr>
110
+ <td valign="top">
111
+ <a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
112
+ An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>.
113
+ </td>
114
+ <td valign="top">
115
+ <a href="#features"><b>Composable matchers</b></a><br>
116
+ 45 matchers that combine with <code>&amp;</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself.
117
+ </td>
118
+ </tr>
119
+ <tr>
120
+ <td valign="top">
121
+ <a href="#features"><b>Built for test suites</b></a><br>
122
+ Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
123
+ </td>
124
+ <td valign="top">
125
+ <a href="#integrations"><b>Integrations</b></a><br>
126
+ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.
127
+ </td>
128
+ </tr>
129
+ </table>
102
130
 
103
- <!-- docs-guard: skip -->
104
- ```python
105
- # bare - three statements, no autocomplete help
106
- assert isinstance(items, list)
107
- assert len(items) == 3
108
- assert "admin" in items
131
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
109
132
 
110
- # assertpy2 - one chain, type-aware autocomplete
111
- assert_that(items).is_instance_of(list).is_length(3).contains("admin")
112
- ```
133
+ `assert` states a condition well, and pytest reports it well.
113
134
 
114
- The real difference shows up on failure. Plain `assert` dumps both structures and leaves you to find the two wrong fields:
135
+ What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
115
136
 
116
137
  ```text
117
138
  assert response == expected
@@ -122,7 +143,7 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
122
143
  E {'status': 'active'} != {'status': 'disabled'}
123
144
  ```
124
145
 
125
- assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
146
+ assertpy2 names the [exact path](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
126
147
 
127
148
  <!-- docs-guard: skip -->
128
149
  ```python
@@ -133,24 +154,45 @@ assert_that(response).is_equal_to(expected)
133
154
  <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">
134
155
  </p>
135
156
 
136
- The diff recurses through nested containers, and matcher predicates get the same path-level treatment. For dynamic fields like IDs or timestamps, assert a subset with [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
157
+ It recurses through nested containers, and matcher predicates get the same treatment.
137
158
 
138
- Matchers are ordinary values, so they also compose inside the expected structure itself, at any depth,
139
- with or without the fluent chain:
159
+ For dynamic fields like IDs, assert a subset with
160
+ [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
161
+
162
+ The chain is the other half: one statement carries the whole intent, and your IDE offers only the
163
+ [methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
164
+
165
+ <!-- docs-guard: skip -->
166
+ ```python
167
+ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
168
+ ```
169
+
170
+ Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
171
+
172
+ Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
140
173
 
141
174
  ```python
142
175
  response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
143
176
 
144
177
  assert_that(response).is_equal_to(
145
- {"id": match.greater_than(0), "user": {"name": "Alice", "age": match.between(18, 120)}, "tags": ["a", "b"]}
178
+ {
179
+ "id": match.greater_than(0),
180
+ "user": {"name": "Alice", "age": match.between(18, 120)},
181
+ "tags": ["a", "b"],
182
+ }
146
183
  )
147
184
 
148
185
  # or keep the bare `assert`, and pytest's own rewriting reports it
149
- assert response == {"id": match.greater_than(0), "user": match.ignore(), "tags": ["a", "b"]}
186
+ assert response == {
187
+ "id": match.greater_than(0),
188
+ "user": match.ignore(),
189
+ "tags": ["a", "b"],
190
+ }
150
191
  ```
151
192
 
152
- The fluent form keeps the path-level diff, the bare form keeps pytest's. There are
153
- [39 matchers](https://solganis.github.io/assertpy2/guides/matchers/), and they combine with `&`, `|` and `~`.
193
+ The fluent form keeps the path-level diff, the bare form keeps pytest's.
194
+
195
+ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/), combining with `&`, `|` and `~`.
154
196
 
155
197
  <p align="center">
156
198
  <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">
@@ -167,13 +209,14 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
167
209
  - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
168
210
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
169
211
 
170
- 9 type-specific Protocols instead of one `Any`.<br>
212
+ 11 type-specific Protocols instead of one `Any`.<br>
171
213
  Works in PyCharm, VS Code, and any LSP-compatible editor.
172
214
 
173
215
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
174
216
 
175
- An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
176
- `is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
217
+ An assertion hands the value back, statically narrowed.
218
+
219
+ `is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:
177
220
 
178
221
  <!-- docs-guard: skip -->
179
222
  ```python
@@ -181,31 +224,52 @@ order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
181
224
  order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
182
225
  ```
183
226
 
184
- For API tests, [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms) validates a raw payload against a Pydantic model and narrows the chain to it,<br>
185
- with `exact=True` catching silent contract drift:
227
+ For API tests,
228
+ [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms)
229
+ validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:
186
230
 
187
231
  <!-- docs-guard: skip -->
188
232
  ```python
189
233
  data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
190
234
  ```
191
235
 
236
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
237
+
238
+ An exception is the right default, and a dead end for anything that wants to read the result.
239
+
240
+ `check()` runs the next assertion for its verdict instead:
241
+
242
+ ```python
243
+ response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
244
+ expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}
245
+
246
+ outcome = assert_that(response).check().is_equal_to(expected)
247
+
248
+ if not outcome and outcome.diff:
249
+ print(outcome.diff.entries[0].path) # user.role
250
+ ```
251
+
252
+ It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
253
+ and a walkable `.diff`, and so does `AssertionFailure`.
254
+
255
+ So a reporter reads structure instead of parsing a string. That is how the
256
+ [Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works, and it
257
+ is open to anything else you build.
258
+
192
259
  <h2 align="center">Features</h2>
193
260
 
194
261
  **Fluent API**
195
262
 
196
- - [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
197
263
  - [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
198
264
  - [**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.
199
265
  - [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
200
266
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
201
267
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
202
268
  - [**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.
203
- - [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
204
269
 
205
270
  **Type safety**
206
271
 
207
- - [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/concepts/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
208
- - [**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).
272
+ - [**Refinement predicates**](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too.
209
273
  - [**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.
210
274
 
211
275
  **Built-in types**
@@ -221,18 +285,19 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
221
285
 
222
286
  - [**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()`.
223
287
  - [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
224
- - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
225
- - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
226
- - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
227
- - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
228
- - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
229
- - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
288
+ - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
289
+ - [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
290
+ - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
291
+ - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
292
+ - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
293
+ - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
294
+ - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
230
295
 
231
296
  **Extensibility**
232
297
 
233
- - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
298
+ - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
299
+ - [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
234
300
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
235
- - [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
236
301
 
237
302
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
238
303
 
@@ -5,7 +5,7 @@
5
5
  <img src="docs/logo.svg" alt="assertpy2" width="280">
6
6
  </picture>
7
7
  <br>
8
- <b>The fully-typed fluent assertion library for Python</b><br>
8
+ <b>A fully typed fluent assertion library for Python</b><br>
9
9
  A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
10
10
  </p>
11
11
 
@@ -43,23 +43,44 @@ def test_user():
43
43
 
44
44
  The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
45
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/):
46
+ <table>
47
+ <tr>
48
+ <td valign="top" width="50%">
49
+ <a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
50
+ A recursive diff names the exact path that differs, in color, instead of dumping both structures.
51
+ </td>
52
+ <td valign="top" width="50%">
53
+ <a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
54
+ <code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit.
55
+ </td>
56
+ </tr>
57
+ <tr>
58
+ <td valign="top">
59
+ <a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
60
+ An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>.
61
+ </td>
62
+ <td valign="top">
63
+ <a href="#features"><b>Composable matchers</b></a><br>
64
+ 45 matchers that combine with <code>&amp;</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself.
65
+ </td>
66
+ </tr>
67
+ <tr>
68
+ <td valign="top">
69
+ <a href="#features"><b>Built for test suites</b></a><br>
70
+ Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
71
+ </td>
72
+ <td valign="top">
73
+ <a href="#integrations"><b>Integrations</b></a><br>
74
+ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.
75
+ </td>
76
+ </tr>
77
+ </table>
50
78
 
51
- <!-- docs-guard: skip -->
52
- ```python
53
- # bare - three statements, no autocomplete help
54
- assert isinstance(items, list)
55
- assert len(items) == 3
56
- assert "admin" in items
79
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
57
80
 
58
- # assertpy2 - one chain, type-aware autocomplete
59
- assert_that(items).is_instance_of(list).is_length(3).contains("admin")
60
- ```
81
+ `assert` states a condition well, and pytest reports it well.
61
82
 
62
- The real difference shows up on failure. Plain `assert` dumps both structures and leaves you to find the two wrong fields:
83
+ What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
63
84
 
64
85
  ```text
65
86
  assert response == expected
@@ -70,7 +91,7 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
70
91
  E {'status': 'active'} != {'status': 'disabled'}
71
92
  ```
72
93
 
73
- assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
94
+ assertpy2 names the [exact path](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
74
95
 
75
96
  <!-- docs-guard: skip -->
76
97
  ```python
@@ -81,24 +102,45 @@ assert_that(response).is_equal_to(expected)
81
102
  <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">
82
103
  </p>
83
104
 
84
- The diff recurses through nested containers, and matcher predicates get the same path-level treatment. For dynamic fields like IDs or timestamps, assert a subset with [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
105
+ It recurses through nested containers, and matcher predicates get the same treatment.
85
106
 
86
- Matchers are ordinary values, so they also compose inside the expected structure itself, at any depth,
87
- with or without the fluent chain:
107
+ For dynamic fields like IDs, assert a subset with
108
+ [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
109
+
110
+ The chain is the other half: one statement carries the whole intent, and your IDE offers only the
111
+ [methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
112
+
113
+ <!-- docs-guard: skip -->
114
+ ```python
115
+ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
116
+ ```
117
+
118
+ Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
119
+
120
+ Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
88
121
 
89
122
  ```python
90
123
  response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
91
124
 
92
125
  assert_that(response).is_equal_to(
93
- {"id": match.greater_than(0), "user": {"name": "Alice", "age": match.between(18, 120)}, "tags": ["a", "b"]}
126
+ {
127
+ "id": match.greater_than(0),
128
+ "user": {"name": "Alice", "age": match.between(18, 120)},
129
+ "tags": ["a", "b"],
130
+ }
94
131
  )
95
132
 
96
133
  # or keep the bare `assert`, and pytest's own rewriting reports it
97
- assert response == {"id": match.greater_than(0), "user": match.ignore(), "tags": ["a", "b"]}
134
+ assert response == {
135
+ "id": match.greater_than(0),
136
+ "user": match.ignore(),
137
+ "tags": ["a", "b"],
138
+ }
98
139
  ```
99
140
 
100
- The fluent form keeps the path-level diff, the bare form keeps pytest's. There are
101
- [39 matchers](https://solganis.github.io/assertpy2/guides/matchers/), and they combine with `&`, `|` and `~`.
141
+ The fluent form keeps the path-level diff, the bare form keeps pytest's.
142
+
143
+ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/), combining with `&`, `|` and `~`.
102
144
 
103
145
  <p align="center">
104
146
  <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">
@@ -115,13 +157,14 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
115
157
  - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
116
158
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
117
159
 
118
- 9 type-specific Protocols instead of one `Any`.<br>
160
+ 11 type-specific Protocols instead of one `Any`.<br>
119
161
  Works in PyCharm, VS Code, and any LSP-compatible editor.
120
162
 
121
163
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
122
164
 
123
- An assertion hands the value back, statically narrowed. `is_not_none()` strips `None`,
124
- `is_instance_of()` narrows to the class, and `.value` returns it with no `cast` and no bare `assert`:
165
+ An assertion hands the value back, statically narrowed.
166
+
167
+ `is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:
125
168
 
126
169
  <!-- docs-guard: skip -->
127
170
  ```python
@@ -129,31 +172,52 @@ order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
129
172
  order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
130
173
  ```
131
174
 
132
- For API tests, [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms) validates a raw payload against a Pydantic model and narrows the chain to it,<br>
133
- with `exact=True` catching silent contract drift:
175
+ For API tests,
176
+ [`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms)
177
+ validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:
134
178
 
135
179
  <!-- docs-guard: skip -->
136
180
  ```python
137
181
  data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
138
182
  ```
139
183
 
184
+ <h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
185
+
186
+ An exception is the right default, and a dead end for anything that wants to read the result.
187
+
188
+ `check()` runs the next assertion for its verdict instead:
189
+
190
+ ```python
191
+ response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
192
+ expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}
193
+
194
+ outcome = assert_that(response).check().is_equal_to(expected)
195
+
196
+ if not outcome and outcome.diff:
197
+ print(outcome.diff.entries[0].path) # user.role
198
+ ```
199
+
200
+ It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
201
+ and a walkable `.diff`, and so does `AssertionFailure`.
202
+
203
+ So a reporter reads structure instead of parsing a string. That is how the
204
+ [Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works, and it
205
+ is open to anything else you build.
206
+
140
207
  <h2 align="center">Features</h2>
141
208
 
142
209
  **Fluent API**
143
210
 
144
- - [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
145
211
  - [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
146
212
  - [**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.
147
213
  - [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
148
214
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
149
215
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
150
216
  - [**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.
151
- - [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
152
217
 
153
218
  **Type safety**
154
219
 
155
- - [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/concepts/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
156
- - [**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).
220
+ - [**Refinement predicates**](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too.
157
221
  - [**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.
158
222
 
159
223
  **Built-in types**
@@ -169,18 +233,19 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
169
233
 
170
234
  - [**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()`.
171
235
  - [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
172
- - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), match `ExceptionGroup` (`contains_error()`), or pivot to the object (`raised()`).
173
- - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected`, `.diff`, and the diff renders into the message itself, so it shows off pytest too.
174
- - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, returning an `AssertionOutcome` that is truthy when it held and carries the message, values and diff when it did not.
175
- - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
176
- - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): three modes under one typed API, all updated with `--assertpy2-snapshot-update`: `snapshot()` (external JSON file), [`matches_inline()`](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) (recorded into the test source), and [`matches_contract_snapshot()`](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots) (value-tolerant structural regressions).
177
- - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi(spec, path, method)` validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1 and Swagger 2.0), reporting every violation with its JSON path.
236
+ - [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
237
+ - [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
238
+ - [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
239
+ - [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
240
+ - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
241
+ - [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
242
+ - [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
178
243
 
179
244
  **Extensibility**
180
245
 
181
- - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` to compose existing ones, or subclass `BaseMatcher` for a rule that needs its own predicate. Both compose with `&`, `|`, `~`.
246
+ - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
247
+ - [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
182
248
  - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
183
- - [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
184
249
 
185
250
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
186
251
 
@@ -17,6 +17,7 @@ from .assertpy import (
17
17
  from .async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder
18
18
  from .errors import (
19
19
  AssertionFailure,
20
+ DanglingAssertionWarning,
20
21
  DiffEntry,
21
22
  DiffResult,
22
23
  PollSample,
@@ -48,6 +49,7 @@ __all__ = [
48
49
  "AsyncAssertionBuilder",
49
50
  "BaseMatcher",
50
51
  "CheckBuilder",
52
+ "DanglingAssertionWarning",
51
53
  "DiffEntry",
52
54
  "DiffResult",
53
55
  "MatchResult",