assertpy2 2.23.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 (239) hide show
  1. {assertpy2-2.23.0 → assertpy2-2.25.0}/CONTRIBUTING.md +54 -11
  2. {assertpy2-2.23.0 → assertpy2-2.25.0}/PKG-INFO +30 -20
  3. {assertpy2-2.23.0 → assertpy2-2.25.0}/README.md +27 -17
  4. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_clustering.py +7 -13
  5. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_dangling.py +156 -53
  6. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_builder_check_typing.py +92 -120
  7. assertpy2-2.25.0/assertpy2/_engine/_capable_typing.py +433 -0
  8. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_check_typing.py +38 -13
  9. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_compare.py +10 -13
  10. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_compat.py +14 -1
  11. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_diff.py +54 -49
  12. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_equality.py +111 -9
  13. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_introspection.py +89 -9
  14. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_membership.py +29 -9
  15. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_mixin_base.py +19 -7
  16. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_operations.py +4 -11
  17. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_ordering.py +5 -9
  18. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_poll_typing.py +183 -241
  19. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_require.py +29 -0
  20. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_size.py +1 -2
  21. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_typing.py +98 -49
  22. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_hints.py +10 -21
  23. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_matcher_impls.py +157 -61
  24. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_satisfies.py +15 -17
  25. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_snapshot_codec.py +4 -2
  26. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/assertpy.py +110 -91
  27. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/async_assertions.py +10 -18
  28. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/base.py +29 -18
  29. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/behave_matchers.py +16 -10
  30. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/collection.py +17 -27
  31. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/contains.py +4 -5
  32. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dict.py +14 -11
  33. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dynamic.py +1 -2
  34. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/errors.py +26 -4
  35. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/exception.py +216 -3
  36. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/extracting.py +19 -18
  37. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/file.py +7 -4
  38. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/helpers.py +88 -48
  39. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/http_mixin.py +1 -2
  40. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/json_mixin.py +3 -2
  41. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/matchers.py +6 -6
  42. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/numeric.py +8 -5
  43. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/pytest_plugin.py +170 -117
  44. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/snapshot.py +42 -31
  45. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/string.py +1 -2
  46. {assertpy2-2.23.0 → assertpy2-2.25.0}/benchmarks/test_perf.py +171 -41
  47. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/concepts/stability.md +77 -40
  48. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/concepts/type-safety.md +100 -78
  49. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/extending/custom-assertions.md +58 -8
  50. assertpy2-2.25.0/docs/getting-started/comparison.md +280 -0
  51. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/getting-started/migration.md +1 -1
  52. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/assertions.md +76 -24
  53. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/data.md +20 -11
  54. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/errors.md +125 -60
  55. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/matchers.md +22 -7
  56. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/testing.md +28 -13
  57. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/recipes.md +34 -3
  58. {assertpy2-2.23.0 → assertpy2-2.25.0}/pyproject.toml +50 -7
  59. {assertpy2-2.23.0 → assertpy2-2.25.0}/scripts/generate_check_protocols.py +26 -11
  60. assertpy2-2.25.0/scripts/generate_poll_protocols.py +907 -0
  61. {assertpy2-2.23.0 → assertpy2-2.25.0}/scripts/mutation_report.py +12 -6
  62. assertpy2-2.25.0/scripts/regenerate.py +45 -0
  63. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/api_snapshot.json +492 -145
  64. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/api_surface.py +32 -9
  65. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/conftest.py +50 -0
  66. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/docs_fixtures.py +37 -13
  67. assertpy2-2.25.0/tests/pyright_baseline.py +108 -0
  68. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_api_compatibility.py +42 -39
  69. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_api_vocabulary.py +5 -10
  70. assertpy2-2.25.0/tests/test_architecture_doc.py +112 -0
  71. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_async.py +13 -26
  72. assertpy2-2.25.0/tests/test_async_predicates.py +195 -0
  73. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_bytes.py +2 -4
  74. assertpy2-2.25.0/tests/test_capable_protocol.py +629 -0
  75. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_check.py +3 -6
  76. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_check_protocols.py +41 -19
  77. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_class.py +35 -0
  78. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_clustering.py +31 -37
  79. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_collection.py +9 -14
  80. assertpy2-2.25.0/tests/test_complexity.py +120 -0
  81. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_core.py +5 -12
  82. assertpy2-2.25.0/tests/test_custom_dict.py +1010 -0
  83. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dangling.py +134 -20
  84. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dataframe.py +3 -7
  85. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_datetime.py +1 -2
  86. assertpy2-2.25.0/tests/test_declared_floors.py +190 -0
  87. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dict.py +1 -2
  88. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dict_compare.py +1 -2
  89. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_docs_examples.py +45 -13
  90. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_docs_typing.py +4 -6
  91. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dynamic.py +1 -2
  92. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_equals.py +7 -15
  93. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_errors.py +2 -5
  94. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_evaluation_core.py +107 -19
  95. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_exception_context.py +1 -2
  96. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_contract.py +92 -5
  97. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_exception.py +205 -8
  98. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_warning.py +1 -2
  99. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_extensions.py +17 -11
  100. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_extracting.py +3 -6
  101. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_hints.py +7 -15
  102. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_http.py +6 -12
  103. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_inline_record.py +1 -2
  104. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_inline_snapshot.py +4 -8
  105. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_iterable_cluster.py +8 -8
  106. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_json.py +1 -2
  107. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_list.py +4 -8
  108. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matcher_parity.py +49 -30
  109. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matcher_registry.py +2 -5
  110. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matchers.py +142 -21
  111. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_message_elision.py +2 -3
  112. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_not.py +4 -7
  113. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_openapi_contract.py +6 -2
  114. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_operation_contract.py +6 -12
  115. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_optional_integration_contracts.py +1 -2
  116. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_overload_order.py +2 -7
  117. assertpy2-2.25.0/tests/test_pin_coverage.py +480 -0
  118. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pipeline.py +8 -9
  119. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_poll_protocols.py +43 -23
  120. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_property_attrs.py +2 -4
  121. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_property_based.py +327 -82
  122. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_protocol_parity.py +33 -52
  123. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_public_surface.py +48 -24
  124. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pyright_baseline.py +45 -21
  125. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pytest_plugin.py +147 -24
  126. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare.py +1 -2
  127. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_config.py +46 -44
  128. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_refusals.py +30 -13
  129. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_rich_diff.py +17 -33
  130. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_satisfy.py +2 -4
  131. assertpy2-2.25.0/tests/test_skip_guard.py +115 -0
  132. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_snapshots.py +99 -15
  133. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_soft.py +6 -12
  134. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_structural.py +10 -19
  135. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_surface_conformance.py +23 -28
  136. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_sync_eventually.py +1 -2
  137. assertpy2-2.25.0/tests/test_type_expression_failures.py +148 -0
  138. assertpy2-2.25.0/tests/test_typing.py +621 -0
  139. assertpy2-2.25.0/tests/test_typing_claims.py +194 -0
  140. assertpy2-2.25.0/tests/test_typing_completeness.py +142 -0
  141. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_conformance.py +64 -15
  142. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_from_a_wheel.py +5 -9
  143. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_http.py +2 -2
  144. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_integrations.py +3 -4
  145. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_negative.py +19 -24
  146. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_vacuity_contract.py +9 -12
  147. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_cases.py +100 -31
  148. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_harness.py +28 -1
  149. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_http.py +7 -7
  150. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_integrations.py +7 -8
  151. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_integrations_baseline.py +3 -4
  152. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_negative_baseline.py +84 -43
  153. assertpy2-2.23.0/docs/getting-started/comparison.md +0 -248
  154. assertpy2-2.23.0/scripts/generate_poll_protocols.py +0 -448
  155. assertpy2-2.23.0/tests/pyright_baseline.py +0 -66
  156. assertpy2-2.23.0/tests/test_custom_dict.py +0 -234
  157. assertpy2-2.23.0/tests/test_typing.py +0 -386
  158. assertpy2-2.23.0/tests/test_typing_claims.py +0 -80
  159. {assertpy2-2.23.0 → assertpy2-2.25.0}/.gitignore +0 -0
  160. {assertpy2-2.23.0 → assertpy2-2.25.0}/LICENSE +0 -0
  161. {assertpy2-2.23.0 → assertpy2-2.25.0}/SECURITY.md +0 -0
  162. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/__init__.py +0 -0
  163. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/__init__.py +0 -0
  164. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_contract.py +0 -0
  165. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_path.py +0 -0
  166. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_text.py +0 -0
  167. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_inline.py +0 -0
  168. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/bytes_mixin.py +0 -0
  169. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dataframe.py +0 -0
  170. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/date.py +0 -0
  171. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/outcome.py +0 -0
  172. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/py.typed +0 -0
  173. {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/warning.py +0 -0
  174. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-equal.png +0 -0
  175. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-equal.svg +0 -0
  176. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-gallery.png +0 -0
  177. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-match.svg +0 -0
  178. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-sequence.svg +0 -0
  179. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-set.svg +0 -0
  180. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-string.svg +0 -0
  181. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/extending/integrations.md +0 -0
  182. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/getting-started/quickstart.md +0 -0
  183. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/fluent.md +0 -0
  184. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/index.md +0 -0
  185. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/logo-dark.svg +0 -0
  186. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/logo.svg +0 -0
  187. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/async.md +0 -0
  188. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/bytes.md +0 -0
  189. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/collections.md +0 -0
  190. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/containment.md +0 -0
  191. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/core.md +0 -0
  192. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dataframes.md +0 -0
  193. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dates.md +0 -0
  194. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dicts.md +0 -0
  195. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dynamic.md +0 -0
  196. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/entry-points.md +0 -0
  197. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/errors.md +0 -0
  198. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/exceptions.md +0 -0
  199. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/extracting.md +0 -0
  200. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/files.md +0 -0
  201. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/http.md +0 -0
  202. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/json.md +0 -0
  203. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/matchers.md +0 -0
  204. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/numbers.md +0 -0
  205. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/overview.md +0 -0
  206. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/snapshots.md +0 -0
  207. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/strings.md +0 -0
  208. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/warnings.md +0 -0
  209. {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/stylesheets/extra.css +0 -0
  210. {assertpy2-2.23.0 → assertpy2-2.25.0}/mkdocs.yml +0 -0
  211. {assertpy2-2.23.0 → assertpy2-2.25.0}/mkdocs_hooks.py +0 -0
  212. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/group_compat.py +0 -0
  213. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_attrs_support.py +0 -0
  214. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_behave_matchers.py +0 -0
  215. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_bool.py +0 -0
  216. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_boundary_cases.py +0 -0
  217. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_callable.py +0 -0
  218. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_chaining.py +0 -0
  219. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_custom_list.py +0 -0
  220. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_description.py +0 -0
  221. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_fail.py +0 -0
  222. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_file.py +0 -0
  223. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_grouped_soft.py +0 -0
  224. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_in.py +0 -0
  225. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_match_result.py +0 -0
  226. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_namedtuple.py +0 -0
  227. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_none.py +0 -0
  228. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_numbers.py +0 -0
  229. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_overloads.py +0 -0
  230. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_assertion.py +0 -0
  231. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_attrs.py +0 -0
  232. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_pydantic.py +0 -0
  233. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_regex_groups.py +0 -0
  234. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_same_as.py +0 -0
  235. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_soft_fail.py +0 -0
  236. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_string.py +0 -0
  237. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_traceback.py +0 -0
  238. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_type.py +0 -0
  239. {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_warn.py +0 -0
@@ -23,17 +23,26 @@ Read more about how pulls work on GitHub's [About pull requests](https://help.gi
23
23
  - Python 3.10+
24
24
  - [uv](https://docs.astral.sh/uv/) as the package manager
25
25
 
26
+ ## Before you change a public signature
27
+
28
+ Read [ARCHITECTURE.md](ARCHITECTURE.md). Four files under `assertpy2/_engine/` are generated, the public
29
+ surface is pinned by a recorded snapshot, and neither is discoverable from the files themselves.
30
+
26
31
  ## Verification pipeline
27
32
 
28
33
  Run all checks before submitting a PR. Every step must pass.
29
34
 
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:
35
+ ### Install
36
+
37
+ A bare `uv sync` leaves out the optional libraries several tests import, and those tests then fail
38
+ rather than skip. Install exactly this set:
32
39
 
33
40
  ```bash
34
41
  uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
35
42
  ```
36
43
 
44
+ ### The main gate
45
+
37
46
  ```bash
38
47
  uv run ruff check .
39
48
  uv run ruff format --check .
@@ -43,22 +52,58 @@ uv run pytest tests/test_docs_examples.py tests/test_typing_claims.py
43
52
  uv run --group docs mkdocs build --strict
44
53
  ```
45
54
 
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:
55
+ CI requires 100% code coverage.
56
+
57
+ ### Then once on the supported floor
58
+
59
+ Run the suite on Python 3.10 as well. This is not optional, and the block above does not cover it.
60
+
61
+ A union carries `__name__` from 3.14 onwards and not before. `logging.LoggerAdapter` became
62
+ subscriptable in 3.11. Neither difference is visible to a type checker told to target 3.10, and both
63
+ shipped as defects that every other gate passed.
64
+
65
+ Use a throwaway environment. `uv run --python 3.10` inside the project rebuilds `.venv` for that
66
+ version, and on Windows it can fail partway and leave you without pytest:
67
+
68
+ ```bash
69
+ uv venv /tmp/py310 --python 3.10
70
+
71
+ export VIRTUAL_ENV=/tmp/py310
72
+ uv pip install -e ".[json,data,inline]"
73
+ uv pip install pytest pytest-cov hypothesis ruff
74
+ uv pip install attrs pydantic requests httpx flask
75
+
76
+ /tmp/py310/bin/python -m pytest tests --ignore=tests/test_docs_examples.py -q
77
+ ```
78
+
79
+ `ruff` belongs in that list: the tests that compare a generated file against its generator shell out
80
+ to it, and without it they fail for a reason that has nothing to do with your change.
81
+
82
+ ### The four type checkers
83
+
84
+ Run these after the main gate, not before. `uv run --group typecheck` adds the checkers to the same
85
+ `.venv` and leaves them there, and a coverage run with them installed collects a different set:
49
86
 
50
87
  ```bash
51
88
  uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
52
- uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
89
+ PYRIGHT_PYTHON_FORCE_VERSION=1.1.413 uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
90
+ uv run --group typecheck pyrefly check tests/test_typing.py
53
91
  uv run --group typecheck pytest tests/test_pyright_baseline.py
54
92
  ```
55
93
 
56
- `--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless told
57
- otherwise, and the count moves with it: 108 diagnostics for this package on 3.10 against 102 on 3.14.
94
+ `PYRIGHT_PYTHON_FORCE_VERSION` picks the engine rather than the launcher. The `pyright` distribution on
95
+ PyPI stopped at 1.1.411 on 25 June while npm has shipped 1.1.412 and 1.1.413 since, so the launcher and
96
+ the engine move apart. The baseline is recorded against one engine, and `tests/typing_harness.py` pins
97
+ the same build, so only this direct invocation needs the variable.
98
+
99
+ `--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless
100
+ told otherwise, and the count moves with it: 174 diagnostics for this package on 3.10 against 169 on
101
+ 3.14.
102
+
58
103
  Without it a contributor on the supported floor meets a red baseline that says nothing about their
59
104
  change. The baseline test passes the same target itself, so it gives one answer everywhere.
60
105
 
61
- Three details that cost time if you meet them the hard way:
106
+ ### Three details that cost time if you meet them the hard way
62
107
 
63
108
  - `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
64
109
  `tests/`, which is full of deliberate negative typing cases: calls that must not type-check.
@@ -68,8 +113,6 @@ Three details that cost time if you meet them the hard way:
68
113
  - `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
69
114
  needs the `docs-examples` group and a different collection.
70
115
 
71
- CI requires 100% code coverage.
72
-
73
116
  ## Commit style
74
117
 
75
118
  Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.23.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'
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
68
68
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
69
69
  <a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
70
70
  <br>
71
- <a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
71
+ <a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
72
72
  <a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
73
73
  <a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
74
74
  </p>
@@ -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"]}
@@ -201,7 +201,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
201
201
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
202
202
 
203
203
  `assert_that()` uses `@overload` to return type-specific Protocols.<br>
204
- Your IDE shows only methods relevant to the value you're testing, not all 100+:
204
+ Your IDE shows only methods relevant to the value you're testing, not the whole surface:
205
205
 
206
206
  - `assert_that("hello").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
207
207
  - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
@@ -210,7 +210,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
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
 
@@ -221,7 +221,7 @@ An assertion hands the value back, statically narrowed.
221
221
  <!-- docs-guard: skip -->
222
222
  ```python
223
223
  order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
224
- order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
224
+ order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
225
225
  ```
226
226
 
227
227
  For API tests,
@@ -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
 
@@ -16,7 +16,7 @@
16
16
  <a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
17
17
  <a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
18
18
  <br>
19
- <a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions"></a>
19
+ <a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
20
20
  <a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
21
21
  <a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
22
22
  </p>
@@ -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"]}
@@ -149,7 +149,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
149
149
  <h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
150
150
 
151
151
  `assert_that()` uses `@overload` to return type-specific Protocols.<br>
152
- Your IDE shows only methods relevant to the value you're testing, not all 100+:
152
+ Your IDE shows only methods relevant to the value you're testing, not the whole surface:
153
153
 
154
154
  - `assert_that("hello").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
155
155
  - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
@@ -158,7 +158,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
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
 
@@ -169,7 +169,7 @@ An assertion hands the value back, statically narrowed.
169
169
  <!-- docs-guard: skip -->
170
170
  ```python
171
171
  order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
172
- order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
172
+ order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
173
173
  ```
174
174
 
175
175
  For API tests,
@@ -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
 
@@ -188,8 +188,7 @@ def stable_repr(value: object, _seen: frozenset[int] = frozenset()) -> str:
188
188
  try:
189
189
  if isinstance(value, (set, frozenset)):
190
190
  members = sorted(stable_repr(member, inner) for member in value)
191
- # an empty one falls back to `repr`, since `{}` is a dict, so `set()` and the `frozenset()` it equals
192
- # read apart
191
+ # an empty one falls back to `repr`: `{}` is a dict, so `set()` and the `frozenset()` it equals read apart
193
192
  return "{" + ", ".join(members) + "}" if value else _safe_repr(value)
194
193
  if isinstance(value, dict):
195
194
  pairs = (f"{stable_repr(key, inner)}: {stable_repr(item, inner)}" for key, item in value.items())
@@ -287,8 +286,7 @@ def is_well_formed(key: Signature) -> bool:
287
286
  )
288
287
  if key.steps:
289
288
  return False
290
- # exactly one of the two: a pair of values only for the kinds whose fields hold them, or a `contains` payload
291
- # would print the `None` standing for a missing item
289
+ # a pair of values only for the kinds whose fields hold them, or a `contains` payload prints a `None`
292
290
  if key.label:
293
291
  return not key.values
294
292
  return len(key.values) == 2 and key.where in _VALUES_ARE_VALUES
@@ -327,8 +325,7 @@ def clusters(
327
325
  """
328
326
  if total_failures <= 0:
329
327
  return []
330
- # keyed on the node id: `pytest-rerunfailures` reports one test failing per retry, and counting attempts printed
331
- # a cluster of six over two tests
328
+ # keyed on the node id: `pytest-rerunfailures` retries printed a cluster of six over two tests
332
329
  grouped: dict[Signature, dict[str, None]] = {}
333
330
  actuals: dict[Signature, dict[str, None]] = {}
334
331
  expecteds: dict[Signature, dict[str, None]] = {}
@@ -473,21 +470,18 @@ def render(
473
470
  lines.append(f"{collect_errors} collection error{'' if one else 's'}, not counted below")
474
471
  for cluster, where in zip(shown, _headings(shown), strict=True):
475
472
  lines.append(_headline(cluster, where, total_failures))
476
- # a cluster keyed on its diagnostic already says the difference in words, and the values under
477
- # it are the ones that vary between its failures rather than the thing they share
473
+ # a cluster keyed on its diagnostic says the difference in words; the values are what varies inside it
478
474
  if cluster.signature.label:
479
475
  lines.append(f" {cluster.signature.label}")
480
476
  else:
481
477
  lines.append(f" actual: {_side(cluster.actuals)}")
482
478
  lines.append(f" expected: {_side(cluster.expecteds)}")
483
479
  if omitted:
484
- # said rather than dropped: five of eleven causes shown silently reads as "these are the
485
- # causes". "more" rather than "smaller", since equal-sized clusters are ordered by signature
480
+ # said rather than dropped: "five of eleven" silently reads as "these are the causes". "more" rather
481
+ # than "smaller", since equal-sized clusters are ordered by signature
486
482
  lines.append(f"{len(omitted)} more cluster{'' if len(omitted) == 1 else 's'} not shown")
487
483
  covered = len({nodeid for cluster in found for nodeid in cluster.nodeids})
488
484
  if covered < total_failures:
489
- # what the number measures, rather than "not clustered": two tests that share a difference under
490
- # a floor of three are related, and the summary declined to print them, which is not the same as
491
- # having found nothing about them
485
+ # what the number measures, not "not clustered": the summary declined to print a pair under the floor
492
486
  lines.append(f"{total_failures - covered} of {total_failures} outside any cluster of {minimum}")
493
487
  return lines