assertpy2 2.24.0__tar.gz → 2.25.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 (235) hide show
  1. {assertpy2-2.24.0 → assertpy2-2.25.0}/PKG-INFO +27 -17
  2. {assertpy2-2.24.0 → assertpy2-2.25.0}/README.md +24 -14
  3. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_builder_check_typing.py +11 -7
  4. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_capable_typing.py +37 -6
  5. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_check_typing.py +1 -0
  6. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_diff.py +49 -38
  7. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_equality.py +108 -3
  8. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_introspection.py +44 -7
  9. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_poll_typing.py +19 -13
  10. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_typing.py +21 -0
  11. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_matcher_impls.py +88 -3
  12. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/assertpy.py +1 -1
  13. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/base.py +7 -3
  14. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/exception.py +212 -0
  15. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/helpers.py +82 -37
  16. {assertpy2-2.24.0 → assertpy2-2.25.0}/benchmarks/test_perf.py +31 -0
  17. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/concepts/stability.md +18 -2
  18. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/extending/custom-assertions.md +51 -2
  19. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/assertions.md +10 -1
  20. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/errors.md +26 -0
  21. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/matchers.md +14 -0
  22. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/testing.md +5 -0
  23. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/recipes.md +29 -0
  24. {assertpy2-2.24.0 → assertpy2-2.25.0}/pyproject.toml +22 -7
  25. {assertpy2-2.24.0 → assertpy2-2.25.0}/scripts/generate_check_protocols.py +17 -2
  26. {assertpy2-2.24.0 → assertpy2-2.25.0}/scripts/generate_poll_protocols.py +48 -4
  27. assertpy2-2.25.0/scripts/regenerate.py +45 -0
  28. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/api_snapshot.json +344 -5
  29. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/api_surface.py +30 -5
  30. assertpy2-2.25.0/tests/test_architecture_doc.py +112 -0
  31. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_async_predicates.py +38 -0
  32. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_capable_protocol.py +52 -1
  33. assertpy2-2.25.0/tests/test_complexity.py +120 -0
  34. assertpy2-2.25.0/tests/test_custom_dict.py +1010 -0
  35. assertpy2-2.25.0/tests/test_declared_floors.py +190 -0
  36. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_expected_contract.py +90 -1
  37. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_expected_exception.py +201 -0
  38. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_matchers.py +117 -9
  39. assertpy2-2.25.0/tests/test_pin_coverage.py +480 -0
  40. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_poll_protocols.py +2 -1
  41. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_property_based.py +184 -35
  42. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing.py +204 -1
  43. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_cases.py +16 -0
  44. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_negative_baseline.py +7 -0
  45. assertpy2-2.24.0/tests/test_architecture_doc.py +0 -133
  46. assertpy2-2.24.0/tests/test_custom_dict.py +0 -231
  47. {assertpy2-2.24.0 → assertpy2-2.25.0}/.gitignore +0 -0
  48. {assertpy2-2.24.0 → assertpy2-2.25.0}/CONTRIBUTING.md +0 -0
  49. {assertpy2-2.24.0 → assertpy2-2.25.0}/LICENSE +0 -0
  50. {assertpy2-2.24.0 → assertpy2-2.25.0}/SECURITY.md +0 -0
  51. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/__init__.py +0 -0
  52. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_clustering.py +0 -0
  53. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_dangling.py +0 -0
  54. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/__init__.py +0 -0
  55. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_compare.py +0 -0
  56. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_compat.py +0 -0
  57. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_contract.py +0 -0
  58. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_membership.py +0 -0
  59. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_mixin_base.py +0 -0
  60. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_operations.py +0 -0
  61. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_ordering.py +0 -0
  62. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_path.py +0 -0
  63. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_require.py +0 -0
  64. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_size.py +0 -0
  65. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_engine/_text.py +0 -0
  66. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_hints.py +0 -0
  67. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_inline.py +0 -0
  68. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_satisfies.py +0 -0
  69. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/_snapshot_codec.py +0 -0
  70. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/async_assertions.py +0 -0
  71. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/behave_matchers.py +0 -0
  72. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/bytes_mixin.py +0 -0
  73. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/collection.py +0 -0
  74. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/contains.py +0 -0
  75. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/dataframe.py +0 -0
  76. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/date.py +0 -0
  77. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/dict.py +0 -0
  78. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/dynamic.py +0 -0
  79. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/errors.py +0 -0
  80. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/extracting.py +0 -0
  81. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/file.py +0 -0
  82. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/http_mixin.py +0 -0
  83. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/json_mixin.py +0 -0
  84. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/matchers.py +0 -0
  85. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/numeric.py +0 -0
  86. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/outcome.py +0 -0
  87. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/py.typed +0 -0
  88. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/pytest_plugin.py +0 -0
  89. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/snapshot.py +0 -0
  90. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/string.py +0 -0
  91. {assertpy2-2.24.0 → assertpy2-2.25.0}/assertpy2/warning.py +0 -0
  92. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-equal.png +0 -0
  93. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-equal.svg +0 -0
  94. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-gallery.png +0 -0
  95. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-match.svg +0 -0
  96. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-sequence.svg +0 -0
  97. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-set.svg +0 -0
  98. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/assets/diff-string.svg +0 -0
  99. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/concepts/type-safety.md +0 -0
  100. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/extending/integrations.md +0 -0
  101. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/getting-started/comparison.md +0 -0
  102. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/getting-started/migration.md +0 -0
  103. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/getting-started/quickstart.md +0 -0
  104. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/data.md +0 -0
  105. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/guides/fluent.md +0 -0
  106. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/index.md +0 -0
  107. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/logo-dark.svg +0 -0
  108. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/logo.svg +0 -0
  109. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/async.md +0 -0
  110. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/bytes.md +0 -0
  111. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/collections.md +0 -0
  112. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/containment.md +0 -0
  113. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/core.md +0 -0
  114. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/dataframes.md +0 -0
  115. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/dates.md +0 -0
  116. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/dicts.md +0 -0
  117. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/dynamic.md +0 -0
  118. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/entry-points.md +0 -0
  119. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/errors.md +0 -0
  120. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/exceptions.md +0 -0
  121. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/extracting.md +0 -0
  122. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/files.md +0 -0
  123. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/http.md +0 -0
  124. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/json.md +0 -0
  125. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/matchers.md +0 -0
  126. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/numbers.md +0 -0
  127. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/overview.md +0 -0
  128. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/snapshots.md +0 -0
  129. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/strings.md +0 -0
  130. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/reference/warnings.md +0 -0
  131. {assertpy2-2.24.0 → assertpy2-2.25.0}/docs/stylesheets/extra.css +0 -0
  132. {assertpy2-2.24.0 → assertpy2-2.25.0}/mkdocs.yml +0 -0
  133. {assertpy2-2.24.0 → assertpy2-2.25.0}/mkdocs_hooks.py +0 -0
  134. {assertpy2-2.24.0 → assertpy2-2.25.0}/scripts/mutation_report.py +0 -0
  135. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/conftest.py +0 -0
  136. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/docs_fixtures.py +0 -0
  137. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/group_compat.py +0 -0
  138. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/pyright_baseline.py +0 -0
  139. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_api_compatibility.py +0 -0
  140. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_api_vocabulary.py +0 -0
  141. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_async.py +0 -0
  142. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_attrs_support.py +0 -0
  143. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_behave_matchers.py +0 -0
  144. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_bool.py +0 -0
  145. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_boundary_cases.py +0 -0
  146. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_bytes.py +0 -0
  147. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_callable.py +0 -0
  148. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_chaining.py +0 -0
  149. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_check.py +0 -0
  150. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_check_protocols.py +0 -0
  151. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_class.py +0 -0
  152. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_clustering.py +0 -0
  153. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_collection.py +0 -0
  154. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_core.py +0 -0
  155. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_custom_list.py +0 -0
  156. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_dangling.py +0 -0
  157. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_dataframe.py +0 -0
  158. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_datetime.py +0 -0
  159. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_description.py +0 -0
  160. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_dict.py +0 -0
  161. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_dict_compare.py +0 -0
  162. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_docs_examples.py +0 -0
  163. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_docs_typing.py +0 -0
  164. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_dynamic.py +0 -0
  165. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_equals.py +0 -0
  166. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_errors.py +0 -0
  167. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_evaluation_core.py +0 -0
  168. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_exception_context.py +0 -0
  169. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_expected_warning.py +0 -0
  170. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_extensions.py +0 -0
  171. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_extracting.py +0 -0
  172. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_fail.py +0 -0
  173. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_file.py +0 -0
  174. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_grouped_soft.py +0 -0
  175. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_hints.py +0 -0
  176. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_http.py +0 -0
  177. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_in.py +0 -0
  178. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_inline_record.py +0 -0
  179. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_inline_snapshot.py +0 -0
  180. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_iterable_cluster.py +0 -0
  181. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_json.py +0 -0
  182. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_list.py +0 -0
  183. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_match_result.py +0 -0
  184. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_matcher_parity.py +0 -0
  185. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_matcher_registry.py +0 -0
  186. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_message_elision.py +0 -0
  187. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_namedtuple.py +0 -0
  188. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_none.py +0 -0
  189. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_not.py +0 -0
  190. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_numbers.py +0 -0
  191. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_openapi_contract.py +0 -0
  192. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_operation_contract.py +0 -0
  193. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_optional_integration_contracts.py +0 -0
  194. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_overload_order.py +0 -0
  195. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_overloads.py +0 -0
  196. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_pipeline.py +0 -0
  197. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_property_attrs.py +0 -0
  198. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_protocol_parity.py +0 -0
  199. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_public_surface.py +0 -0
  200. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_pyright_baseline.py +0 -0
  201. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_pytest_plugin.py +0 -0
  202. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_recursive_assertion.py +0 -0
  203. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_recursive_compare.py +0 -0
  204. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_recursive_compare_attrs.py +0 -0
  205. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_recursive_compare_config.py +0 -0
  206. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_recursive_compare_pydantic.py +0 -0
  207. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_refusals.py +0 -0
  208. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_regex_groups.py +0 -0
  209. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_rich_diff.py +0 -0
  210. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_same_as.py +0 -0
  211. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_satisfy.py +0 -0
  212. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_skip_guard.py +0 -0
  213. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_snapshots.py +0 -0
  214. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_soft.py +0 -0
  215. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_soft_fail.py +0 -0
  216. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_string.py +0 -0
  217. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_structural.py +0 -0
  218. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_surface_conformance.py +0 -0
  219. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_sync_eventually.py +0 -0
  220. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_traceback.py +0 -0
  221. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_type.py +0 -0
  222. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_type_expression_failures.py +0 -0
  223. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_claims.py +0 -0
  224. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_completeness.py +0 -0
  225. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_conformance.py +0 -0
  226. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_from_a_wheel.py +0 -0
  227. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_http.py +0 -0
  228. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_integrations.py +0 -0
  229. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_typing_negative.py +0 -0
  230. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_vacuity_contract.py +0 -0
  231. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/test_warn.py +0 -0
  232. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_harness.py +0 -0
  233. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_http.py +0 -0
  234. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_integrations.py +0 -0
  235. {assertpy2-2.24.0 → assertpy2-2.25.0}/tests/typing_integrations_baseline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.24.0
3
+ Version: 2.25.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
@@ -30,13 +30,13 @@ Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
30
30
  Provides-Extra: allure
31
31
  Requires-Dist: allure-pytest>=2.13; extra == 'allure'
32
32
  Provides-Extra: behave
33
- Requires-Dist: behave>=1.2.6; extra == 'behave'
33
+ Requires-Dist: behave>=1.3.0; extra == 'behave'
34
34
  Provides-Extra: data
35
35
  Requires-Dist: numpy>=1.26; extra == 'data'
36
36
  Requires-Dist: pandas>=2.0; extra == 'data'
37
37
  Requires-Dist: polars>=1.0; extra == 'data'
38
38
  Provides-Extra: inline
39
- Requires-Dist: asttokens>=2.0; extra == 'inline'
39
+ Requires-Dist: asttokens>=2.0.4; extra == 'inline'
40
40
  Requires-Dist: executing>=2.2; extra == 'inline'
41
41
  Provides-Extra: json
42
42
  Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
@@ -130,7 +130,7 @@ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI respons
130
130
 
131
131
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
132
132
 
133
- `assert` states a condition well, and pytest reports it well.
133
+ `assert` states a condition well. pytest reports it well.
134
134
 
135
135
  What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
136
136
 
@@ -154,12 +154,12 @@ assert_that(response).is_equal_to(expected)
154
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">
155
155
  </p>
156
156
 
157
- It recurses through nested containers, and matcher predicates get the same treatment.
157
+ It recurses through nested containers. Matcher predicates get the same treatment.
158
158
 
159
159
  For dynamic fields like IDs, assert a subset with
160
160
  [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
161
161
 
162
- The chain is the other half: one statement carries the whole intent, and your IDE offers only the
162
+ The chain is the other half. Your IDE offers only the
163
163
  [methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
164
164
 
165
165
  <!-- docs-guard: skip -->
@@ -169,7 +169,7 @@ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
169
169
 
170
170
  Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
171
171
 
172
- Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
172
+ Nothing is patched, so a matcher can sit inside the expected structure at any depth:
173
173
 
174
174
  ```python
175
175
  response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
@@ -210,7 +210,7 @@ Your IDE shows only methods relevant to the value you're testing, not the whole
210
210
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
211
211
 
212
212
  15 type-specific Protocols instead of one `Any`.<br>
213
- Works in PyCharm, VS Code, and any LSP-compatible editor.
213
+ Works in PyCharm, VS Code, and any editor that runs a type checker.
214
214
 
215
215
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
216
216
 
@@ -235,7 +235,7 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
235
235
 
236
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
237
 
238
- An exception is the right default, and a dead end for anything that wants to read the result.
238
+ An exception is the right default. Reading the result means catching it.
239
239
 
240
240
  `check()` runs the next assertion for its verdict instead:
241
241
 
@@ -250,11 +250,11 @@ if not outcome and outcome.diff:
250
250
  ```
251
251
 
252
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`.
253
+ and a walkable `.diff`. So does `AssertionFailure`.
254
254
 
255
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.
256
+ [Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works.
257
+ Anything else you build can too.
258
258
 
259
259
  <h2 align="center">Features</h2>
260
260
 
@@ -262,8 +262,6 @@ is open to anything else you build.
262
262
 
263
263
  - [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
264
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.
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.
266
- - [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
267
265
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
268
266
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
269
267
  - [**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.
@@ -281,6 +279,7 @@ is open to anything else you build.
281
279
  - [**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.
282
280
  - [**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.
283
281
  - [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
282
+ - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
284
283
 
285
284
  **Testing**
286
285
 
@@ -288,17 +287,28 @@ is open to anything else you build.
288
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.
289
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()`).
290
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
+ - [**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`.
291
+ - [**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.
292
+
293
+ **Failure reporting**
294
+
291
295
  - [**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.
292
296
  - [**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`.
293
297
  - [**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.
294
- - [**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`.
295
- - [**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.
298
+
299
+ **Plugin for pytest**
300
+
301
+ Set from the command line or from `[tool.pytest.ini_options]`, not from a call.
302
+
303
+ - [**Failure clustering**](https://solganis.github.io/assertpy2/guides/errors/#what-the-failures-had-in-common): forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, `assertpy2_failure_clusters = "off"` turns it off.
304
+ - [**Diagnostic profiles**](https://solganis.github.io/assertpy2/guides/errors/#configuration): `assertpy2_profile` turns the guards below on in one line. `compatible` (default) leaves them off, `safe` warns, `strict` fails the tests they find. A setting you name yourself still wins.
305
+ - [**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.
306
+ - [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
296
307
 
297
308
  **Extensibility**
298
309
 
299
310
  - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
300
311
  - [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
301
- - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
302
312
 
303
313
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
304
314
 
@@ -78,7 +78,7 @@ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI respons
78
78
 
79
79
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
80
80
 
81
- `assert` states a condition well, and pytest reports it well.
81
+ `assert` states a condition well. pytest reports it well.
82
82
 
83
83
  What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
84
84
 
@@ -102,12 +102,12 @@ assert_that(response).is_equal_to(expected)
102
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">
103
103
  </p>
104
104
 
105
- It recurses through nested containers, and matcher predicates get the same treatment.
105
+ It recurses through nested containers. Matcher predicates get the same treatment.
106
106
 
107
107
  For dynamic fields like IDs, assert a subset with
108
108
  [`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
109
109
 
110
- The chain is the other half: one statement carries the whole intent, and your IDE offers only the
110
+ The chain is the other half. Your IDE offers only the
111
111
  [methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
112
112
 
113
113
  <!-- docs-guard: skip -->
@@ -117,7 +117,7 @@ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
117
117
 
118
118
  Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
119
119
 
120
- Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
120
+ Nothing is patched, so a matcher can sit inside the expected structure at any depth:
121
121
 
122
122
  ```python
123
123
  response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
@@ -158,7 +158,7 @@ Your IDE shows only methods relevant to the value you're testing, not the whole
158
158
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
159
159
 
160
160
  15 type-specific Protocols instead of one `Any`.<br>
161
- Works in PyCharm, VS Code, and any LSP-compatible editor.
161
+ Works in PyCharm, VS Code, and any editor that runs a type checker.
162
162
 
163
163
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
164
164
 
@@ -183,7 +183,7 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
183
183
 
184
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
185
 
186
- An exception is the right default, and a dead end for anything that wants to read the result.
186
+ An exception is the right default. Reading the result means catching it.
187
187
 
188
188
  `check()` runs the next assertion for its verdict instead:
189
189
 
@@ -198,11 +198,11 @@ if not outcome and outcome.diff:
198
198
  ```
199
199
 
200
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`.
201
+ and a walkable `.diff`. So does `AssertionFailure`.
202
202
 
203
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.
204
+ [Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works.
205
+ Anything else you build can too.
206
206
 
207
207
  <h2 align="center">Features</h2>
208
208
 
@@ -210,8 +210,6 @@ is open to anything else you build.
210
210
 
211
211
  - [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
212
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.
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.
214
- - [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
215
213
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
216
214
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
217
215
  - [**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.
@@ -229,6 +227,7 @@ is open to anything else you build.
229
227
  - [**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.
230
228
  - [**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.
231
229
  - [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
230
+ - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
232
231
 
233
232
  **Testing**
234
233
 
@@ -236,17 +235,28 @@ is open to anything else you build.
236
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.
237
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()`).
238
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
+ - [**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`.
239
+ - [**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.
240
+
241
+ **Failure reporting**
242
+
239
243
  - [**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.
240
244
  - [**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`.
241
245
  - [**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.
242
- - [**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`.
243
- - [**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.
246
+
247
+ **Plugin for pytest**
248
+
249
+ Set from the command line or from `[tool.pytest.ini_options]`, not from a call.
250
+
251
+ - [**Failure clustering**](https://solganis.github.io/assertpy2/guides/errors/#what-the-failures-had-in-common): forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, `assertpy2_failure_clusters = "off"` turns it off.
252
+ - [**Diagnostic profiles**](https://solganis.github.io/assertpy2/guides/errors/#configuration): `assertpy2_profile` turns the guards below on in one line. `compatible` (default) leaves them off, `safe` warns, `strict` fails the tests they find. A setting you name yourself still wins.
253
+ - [**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.
254
+ - [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
244
255
 
245
256
  **Extensibility**
246
257
 
247
258
  - [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
248
259
  - [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
249
- - [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
250
260
 
251
261
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
252
262
 
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
26
26
  from .._matcher_impls import ClassInfo
27
27
  from ..matchers import Matcher
28
28
  from ..outcome import AssertionOutcome
29
- from ._capable_typing import _Callable, _Orderable, _PathLike
29
+ from ._capable_typing import _Callable, _Keyed, _KeyedWithItems, _KeyedWithValues, _Orderable, _PathLike
30
30
  from ._introspection import MappingLike
31
31
  from ._typing import (
32
32
  _U,
@@ -906,29 +906,33 @@ if TYPE_CHECKING:
906
906
  @overload
907
907
  def contains_key(self: _CheckAnyValue[dict[_K, _V]], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
908
908
  @overload
909
- def contains_key(self: _CheckAnyValue[_CapableT], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
909
+ def contains_key(self: _CheckAnyValue[_Keyed], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
910
910
 
911
911
  @overload
912
912
  def does_not_contain_key(self: _CheckAnyValue[dict[_K, _V]], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
913
913
  @overload
914
- def does_not_contain_key(self: _CheckAnyValue[_CapableT], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
914
+ def does_not_contain_key(self: _CheckAnyValue[_Keyed], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
915
915
 
916
916
  @overload
917
917
  def contains_value(self: _CheckAnyValue[dict[_K, _V]], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
918
918
  @overload
919
- def contains_value(self: _CheckAnyValue[_CapableT], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
919
+ def contains_value(self: _CheckAnyValue[_KeyedWithValues], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
920
920
 
921
921
  @overload
922
922
  def does_not_contain_value(
923
923
  self: _CheckAnyValue[dict[_K, _V]], *values: _V | Matcher[_V]
924
924
  ) -> AssertionOutcome: ...
925
925
  @overload
926
- def does_not_contain_value(self: _CheckAnyValue[_CapableT], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
926
+ def does_not_contain_value(
927
+ self: _CheckAnyValue[_KeyedWithValues], *values: _V | Matcher[_V]
928
+ ) -> AssertionOutcome: ...
927
929
 
928
930
  @overload
929
931
  def contains_entry(self: _CheckAnyValue[dict[_K, _V]], *args: object, **kwargs: object) -> AssertionOutcome: ...
930
932
  @overload
931
- def contains_entry(self: _CheckAnyValue[_CapableT], *args: object, **kwargs: object) -> AssertionOutcome: ...
933
+ def contains_entry(
934
+ self: _CheckAnyValue[_KeyedWithItems], *args: object, **kwargs: object
935
+ ) -> AssertionOutcome: ...
932
936
 
933
937
  @overload
934
938
  def does_not_contain_entry(
@@ -936,7 +940,7 @@ if TYPE_CHECKING:
936
940
  ) -> AssertionOutcome: ...
937
941
  @overload
938
942
  def does_not_contain_entry(
939
- self: _CheckAnyValue[_CapableT], *args: object, **kwargs: object
943
+ self: _CheckAnyValue[_KeyedWithItems], *args: object, **kwargs: object
940
944
  ) -> AssertionOutcome: ...
941
945
 
942
946
  @overload
@@ -49,6 +49,28 @@ if TYPE_CHECKING:
49
49
 
50
50
  def __lt__(self, other: Any, /) -> Any: ...
51
51
 
52
+ class _Keyed(Protocol):
53
+ """`keys()`, which the dict gate reads before anything else.
54
+
55
+ Iterability is read too and is deliberately NOT here: the gate asks
56
+ `isinstance(value, collections.abc.Iterable)`, which a class earns by registration, and
57
+ requiring `__iter__` structurally refused a registered value that runs. Same blind spot as
58
+ `numbers.Number` in the numeric family, and the same answer.
59
+ """
60
+
61
+ def keys(self) -> Any: ...
62
+
63
+ class _KeyedWithItems(_Keyed, Protocol):
64
+ """And a lookup, which reading an entry needs and reading a key does not."""
65
+
66
+ def __getitem__(self, key: Any, /) -> Any: ...
67
+
68
+ class _KeyedWithValues(_Keyed, Protocol):
69
+ """And the values, which only the value pair reads. A lookup is not enough: measured on a
70
+ subject carrying `keys` and `__getitem__`, `contains_value` still refuses."""
71
+
72
+ def values(self) -> Any: ...
73
+
52
74
  class _PathLike(Protocol):
53
75
  """A value the filesystem assertions can read a path out of, spelled as `os.PathLike` spells it."""
54
76
 
@@ -196,18 +218,22 @@ if TYPE_CHECKING:
196
218
  self: _CapableAssertion[bytes | bytearray], sub: bytes | bytearray
197
219
  ) -> _CapableAssertion[_CapableT_co]: ...
198
220
  def contains_duplicates(self) -> Self: ...
199
- def contains_entry(self, *args: object, **kwargs: object) -> Self: ...
221
+ def contains_entry(
222
+ self: _CapableAssertion[_KeyedWithItems], *args: object, **kwargs: object
223
+ ) -> _CapableAssertion[_CapableT_co]: ...
200
224
  def contains_error(self, *ex_types: type) -> Self: ...
201
225
  def contains_exactly(self, *items: object) -> Self: ...
202
226
  def contains_exactly_in_any_order(self, *items: object) -> Self: ...
203
227
  def contains_ignoring_case(self, *items: str) -> Self: ...
204
228
  def contains_in_order(self, *items: object) -> Self: ...
205
- def contains_key(self, *keys: object) -> Self: ...
229
+ def contains_key(self: _CapableAssertion[_Keyed], *keys: object) -> _CapableAssertion[_CapableT_co]: ...
206
230
  def contains_none_of(self, *items: str) -> Self: ...
207
231
  def contains_only(self, *items: object) -> Self: ...
208
232
  def contains_only_once(self, *items: object) -> Self: ...
209
233
  def contains_sequence(self, *items: object) -> Self: ...
210
- def contains_value(self, *values: object) -> Self: ...
234
+ def contains_value(
235
+ self: _CapableAssertion[_KeyedWithValues], *values: object
236
+ ) -> _CapableAssertion[_CapableT_co]: ...
211
237
  def decoded_as(
212
238
  self: _CapableAssertion[bytes | bytearray], encoding: str = ...
213
239
  ) -> _CapableAssertion[_CapableT_co]: ...
@@ -215,10 +241,14 @@ if TYPE_CHECKING:
215
241
  def described_as(self, description: str) -> Self: ...
216
242
  def does_not_contain(self, *items: object) -> Self: ...
217
243
  def does_not_contain_duplicates(self) -> Self: ...
218
- def does_not_contain_entry(self, *args: object, **kwargs: object) -> Self: ...
244
+ def does_not_contain_entry(
245
+ self: _CapableAssertion[_KeyedWithItems], *args: object, **kwargs: object
246
+ ) -> _CapableAssertion[_CapableT_co]: ...
219
247
  def does_not_contain_error(self, *ex_types: type) -> Self: ...
220
- def does_not_contain_key(self, *keys: object) -> Self: ...
221
- def does_not_contain_value(self, *values: object) -> Self: ...
248
+ def does_not_contain_key(self: _CapableAssertion[_Keyed], *keys: object) -> _CapableAssertion[_CapableT_co]: ...
249
+ def does_not_contain_value(
250
+ self: _CapableAssertion[_KeyedWithValues], *values: object
251
+ ) -> _CapableAssertion[_CapableT_co]: ...
222
252
  def does_not_exist(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
223
253
  def does_not_have_json_path(self, path: str) -> Self: ...
224
254
  def does_not_match(self: _CapableAssertion[str], pattern: str) -> _CapableAssertion[_CapableT_co]: ...
@@ -355,6 +385,7 @@ if TYPE_CHECKING:
355
385
  def is_zero(self) -> Self: ...
356
386
  def matches(self: _CapableAssertion[str], pattern: str) -> _CapableAssertion[_CapableT_co]: ...
357
387
  def matches_contract_snapshot(self, id: str | None = ..., path: str = ...) -> Self: ...
388
+ def matches_error_tree(self, *expected: type | list[Any]) -> Self: ...
358
389
  def matches_inline(
359
390
  self,
360
391
  expected: object = ...,
@@ -675,6 +675,7 @@ if TYPE_CHECKING:
675
675
  def does_not_contain_error(self, *ex_types: type) -> AssertionOutcome: ...
676
676
  def has_root_cause(self, ex: type) -> AssertionOutcome: ...
677
677
  def contains_error(self, *ex_types: type) -> AssertionOutcome: ...
678
+ def matches_error_tree(self, *expected: type | list[Any]) -> AssertionOutcome: ...
678
679
  @property
679
680
  def not_(self) -> Self: ...
680
681
 
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING, TypeVar
33
33
 
34
34
  from ..errors import DiffEntry, DiffResult, _safe_repr
35
35
  from ._compare import _guarded_not_equal, _node_decision
36
- from ._introspection import is_attrs_instance, is_mapping_like, is_model_dump_object, is_namedtuple
36
+ from ._introspection import is_attrs_instance, is_mapping_like, is_model_dump_object, is_namedtuple, keyed_snapshot
37
37
  from ._path import _ROOT, _Path
38
38
 
39
39
  if TYPE_CHECKING:
@@ -480,6 +480,48 @@ def _build_equality_diff(
480
480
  return DiffResult(kind="scalar", entries=[_prefix.leaf_entry(actual=actual, expected=expected)])
481
481
 
482
482
 
483
+ def _mapping_diff_entries(actual, expected, prefix: _Path, child_seen: set[int], config) -> list[DiffEntry] | None:
484
+ """Path-level entries between two mappings, or ``None`` when either cannot be walked by key.
485
+
486
+ Both sides are snapshotted first, so the keys walked below are the ones that were proved readable
487
+ rather than a second reading of a value free to answer differently between passes.
488
+ """
489
+ kept, kept_expected = keyed_snapshot(actual), keyed_snapshot(expected)
490
+ if kept is None or kept_expected is None:
491
+ return None
492
+ entries: list[DiffEntry] = []
493
+ actual_keys = set(kept)
494
+ expected_keys = set(kept_expected)
495
+ if config is not None and config.strict_types:
496
+ # `{True} & {1}` hands back whichever side the set drew from, losing the type that differs
497
+ stored = {key: key for key in kept_expected}
498
+ for key in kept:
499
+ counterpart = stored.get(key, key)
500
+ if type(key) is not type(counterpart):
501
+ entries.append(prefix.key(key).entry(actual=key, expected=counterpart))
502
+ for key in _ordered_keys(kept, kept_expected):
503
+ if key not in expected_keys:
504
+ entries.append(prefix.key(key).entry(actual=kept[key], expected=None, absent="expected"))
505
+ elif key not in actual_keys:
506
+ entries.append(prefix.key(key).entry(actual=None, absent="actual", expected=kept_expected[key]))
507
+ else:
508
+ decision = _node_decision(kept[key], kept_expected[key], config, field=key)
509
+ if decision == "leaf":
510
+ entries.append(prefix.key(key).entry(actual=kept[key], expected=kept_expected[key]))
511
+ elif decision != "equal":
512
+ entries.extend(
513
+ _child_entries(
514
+ kept[key],
515
+ kept_expected[key],
516
+ prefix.key(key),
517
+ descended_for=decision,
518
+ _seen=child_seen,
519
+ config=config,
520
+ )
521
+ )
522
+ return entries
523
+
524
+
483
525
  def _sub_diff_entries(
484
526
  actual: object, expected: object, prefix: _Path = _ROOT, *, _seen: set[int] | None = None, config=None
485
527
  ) -> list[DiffEntry] | None:
@@ -500,38 +542,7 @@ def _sub_diff_entries(
500
542
  return [prefix.entry(actual="<circular ref>", expected="<circular ref>")]
501
543
 
502
544
  if is_mapping_like(actual) and is_mapping_like(expected):
503
- child_seen = _seen | {id(actual), id(expected)}
504
- entries: list[DiffEntry] = []
505
- actual_keys = set(actual)
506
- expected_keys = set(expected)
507
- if config is not None and config.strict_types:
508
- # `{True} & {1}` hands back whichever side the set drew from, losing the type that differs
509
- stored = {key: key for key in expected}
510
- for key in actual:
511
- counterpart = stored.get(key, key)
512
- if type(key) is not type(counterpart):
513
- entries.append(prefix.key(key).entry(actual=key, expected=counterpart))
514
- for key in _ordered_keys(actual, expected):
515
- if key not in expected_keys:
516
- entries.append(prefix.key(key).entry(actual=actual[key], expected=None, absent="expected"))
517
- elif key not in actual_keys:
518
- entries.append(prefix.key(key).entry(actual=None, absent="actual", expected=expected[key]))
519
- else:
520
- decision = _node_decision(actual[key], expected[key], config, field=key)
521
- if decision == "leaf":
522
- entries.append(prefix.key(key).entry(actual=actual[key], expected=expected[key]))
523
- elif decision != "equal":
524
- entries.extend(
525
- _child_entries(
526
- actual[key],
527
- expected[key],
528
- prefix.key(key),
529
- descended_for=decision,
530
- _seen=child_seen,
531
- config=config,
532
- )
533
- )
534
- return entries
545
+ return _mapping_diff_entries(actual, expected, prefix, _seen | {id(actual), id(expected)}, config)
535
546
  if (
536
547
  dataclasses.is_dataclass(actual)
537
548
  and not isinstance(actual, type)
@@ -563,11 +574,11 @@ def _sub_diff_entries(
563
574
  config=config,
564
575
  )
565
576
  )
566
- for field_name in expected._fields:
567
- if field_name not in actual._fields: # _fields, not hasattr (count/index collide)
568
- entries.append(
569
- prefix.attr(field_name).entry(actual=None, absent="actual", expected=getattr(expected, field_name))
570
- )
577
+ entries.extend(
578
+ prefix.attr(field_name).entry(actual=None, absent="actual", expected=getattr(expected, field_name))
579
+ for field_name in expected._fields
580
+ if field_name not in actual._fields # _fields, not hasattr (count/index collide)
581
+ )
571
582
  return entries
572
583
  both_model = is_model_dump_object(actual) and is_model_dump_object(expected)
573
584
  both_attrs = is_attrs_instance(actual) and is_attrs_instance(expected)