assertpy2 2.24.0__tar.gz → 2.26.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 (246) hide show
  1. {assertpy2-2.24.0 → assertpy2-2.26.0}/CONTRIBUTING.md +10 -7
  2. {assertpy2-2.24.0 → assertpy2-2.26.0}/PKG-INFO +27 -17
  3. {assertpy2-2.24.0 → assertpy2-2.26.0}/README.md +24 -14
  4. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/__init__.py +2 -1
  5. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_builder_check_typing.py +81 -63
  6. assertpy2-2.26.0/assertpy2/_engine/_capable_typing.py +729 -0
  7. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_check_typing.py +1 -0
  8. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_diff.py +49 -38
  9. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_equality.py +108 -3
  10. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_introspection.py +44 -7
  11. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_mixin_base.py +3 -1
  12. assertpy2-2.26.0/assertpy2/_engine/_negated_typing.py +1688 -0
  13. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_poll_typing.py +151 -145
  14. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_typing.py +100 -8
  15. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_matcher_impls.py +97 -13
  16. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/assertpy.py +260 -14
  17. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/async_assertions.py +4 -0
  18. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/base.py +7 -3
  19. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/dynamic.py +21 -2
  20. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/errors.py +47 -21
  21. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/exception.py +212 -0
  22. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/file.py +3 -3
  23. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/helpers.py +178 -78
  24. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/json_mixin.py +6 -6
  25. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/numeric.py +17 -5
  26. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/outcome.py +46 -1
  27. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/pytest_plugin.py +132 -44
  28. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/snapshot.py +12 -1
  29. {assertpy2-2.24.0 → assertpy2-2.26.0}/benchmarks/test_perf.py +31 -0
  30. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/concepts/stability.md +35 -9
  31. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/extending/custom-assertions.md +51 -2
  32. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/extending/integrations.md +22 -8
  33. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/getting-started/migration.md +3 -3
  34. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/assertions.md +11 -2
  35. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/errors.md +83 -9
  36. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/fluent.md +6 -2
  37. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/matchers.md +14 -0
  38. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/testing.md +5 -0
  39. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/recipes.md +29 -0
  40. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/entry-points.md +6 -0
  41. {assertpy2-2.24.0 → assertpy2-2.26.0}/pyproject.toml +45 -15
  42. {assertpy2-2.24.0 → assertpy2-2.26.0}/scripts/generate_check_protocols.py +18 -2
  43. {assertpy2-2.24.0 → assertpy2-2.26.0}/scripts/generate_poll_protocols.py +377 -23
  44. assertpy2-2.26.0/scripts/regenerate.py +49 -0
  45. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/api_snapshot.json +691 -24
  46. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/api_surface.py +30 -5
  47. assertpy2-2.26.0/tests/poll_parity_baseline.py +921 -0
  48. assertpy2-2.26.0/tests/poll_parity_cases.py +3331 -0
  49. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/pyright_baseline.py +2 -2
  50. assertpy2-2.26.0/tests/test_architecture_doc.py +107 -0
  51. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_async_predicates.py +38 -0
  52. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_capable_protocol.py +84 -6
  53. assertpy2-2.26.0/tests/test_complexity.py +122 -0
  54. assertpy2-2.26.0/tests/test_custom_dict.py +1010 -0
  55. assertpy2-2.26.0/tests/test_declared_floors.py +190 -0
  56. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_dict_compare.py +8 -5
  57. assertpy2-2.26.0/tests/test_docs_examples.py +334 -0
  58. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_equals.py +5 -5
  59. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_expected_contract.py +90 -1
  60. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_expected_exception.py +201 -0
  61. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_extensions.py +91 -0
  62. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_matchers.py +117 -9
  63. assertpy2-2.26.0/tests/test_message_elision.py +267 -0
  64. assertpy2-2.26.0/tests/test_negated_protocols.py +406 -0
  65. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_numbers.py +26 -0
  66. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_openapi_contract.py +23 -0
  67. assertpy2-2.26.0/tests/test_pin_coverage.py +500 -0
  68. assertpy2-2.26.0/tests/test_poll_parity.py +378 -0
  69. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_poll_protocols.py +2 -1
  70. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_property_based.py +259 -36
  71. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_public_surface.py +10 -2
  72. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_pyright_baseline.py +10 -4
  73. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_pytest_plugin.py +563 -62
  74. assertpy2-2.26.0/tests/test_requirement.py +673 -0
  75. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_rich_diff.py +4 -4
  76. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_snapshots.py +74 -0
  77. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing.py +237 -2
  78. assertpy2-2.26.0/tests/test_typing_promises.py +67 -0
  79. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_cases.py +38 -2
  80. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_harness.py +17 -8
  81. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_negative_baseline.py +38 -12
  82. assertpy2-2.26.0/tests/typing_promises.py +199 -0
  83. assertpy2-2.24.0/assertpy2/_engine/_capable_typing.py +0 -402
  84. assertpy2-2.24.0/tests/test_architecture_doc.py +0 -133
  85. assertpy2-2.24.0/tests/test_custom_dict.py +0 -231
  86. assertpy2-2.24.0/tests/test_docs_examples.py +0 -123
  87. assertpy2-2.24.0/tests/test_message_elision.py +0 -117
  88. {assertpy2-2.24.0 → assertpy2-2.26.0}/.gitignore +0 -0
  89. {assertpy2-2.24.0 → assertpy2-2.26.0}/LICENSE +0 -0
  90. {assertpy2-2.24.0 → assertpy2-2.26.0}/SECURITY.md +0 -0
  91. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_clustering.py +0 -0
  92. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_dangling.py +0 -0
  93. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/__init__.py +0 -0
  94. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_compare.py +0 -0
  95. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_compat.py +0 -0
  96. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_contract.py +0 -0
  97. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_membership.py +0 -0
  98. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_operations.py +0 -0
  99. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_ordering.py +0 -0
  100. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_path.py +0 -0
  101. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_require.py +0 -0
  102. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_size.py +0 -0
  103. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_engine/_text.py +0 -0
  104. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_hints.py +0 -0
  105. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_inline.py +0 -0
  106. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_satisfies.py +0 -0
  107. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/_snapshot_codec.py +0 -0
  108. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/behave_matchers.py +0 -0
  109. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/bytes_mixin.py +0 -0
  110. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/collection.py +0 -0
  111. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/contains.py +0 -0
  112. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/dataframe.py +0 -0
  113. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/date.py +0 -0
  114. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/dict.py +0 -0
  115. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/extracting.py +0 -0
  116. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/http_mixin.py +0 -0
  117. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/matchers.py +0 -0
  118. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/py.typed +0 -0
  119. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/string.py +0 -0
  120. {assertpy2-2.24.0 → assertpy2-2.26.0}/assertpy2/warning.py +0 -0
  121. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-equal.png +0 -0
  122. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-equal.svg +0 -0
  123. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-gallery.png +0 -0
  124. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-match.svg +0 -0
  125. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-sequence.svg +0 -0
  126. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-set.svg +0 -0
  127. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/assets/diff-string.svg +0 -0
  128. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/concepts/type-safety.md +0 -0
  129. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/getting-started/comparison.md +0 -0
  130. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/getting-started/quickstart.md +0 -0
  131. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/guides/data.md +0 -0
  132. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/index.md +0 -0
  133. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/logo-dark.svg +0 -0
  134. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/logo.svg +0 -0
  135. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/async.md +0 -0
  136. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/bytes.md +0 -0
  137. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/collections.md +0 -0
  138. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/containment.md +0 -0
  139. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/core.md +0 -0
  140. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/dataframes.md +0 -0
  141. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/dates.md +0 -0
  142. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/dicts.md +0 -0
  143. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/dynamic.md +0 -0
  144. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/errors.md +0 -0
  145. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/exceptions.md +0 -0
  146. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/extracting.md +0 -0
  147. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/files.md +0 -0
  148. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/http.md +0 -0
  149. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/json.md +0 -0
  150. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/matchers.md +0 -0
  151. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/numbers.md +0 -0
  152. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/overview.md +0 -0
  153. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/snapshots.md +0 -0
  154. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/strings.md +0 -0
  155. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/reference/warnings.md +0 -0
  156. {assertpy2-2.24.0 → assertpy2-2.26.0}/docs/stylesheets/extra.css +0 -0
  157. {assertpy2-2.24.0 → assertpy2-2.26.0}/mkdocs.yml +0 -0
  158. {assertpy2-2.24.0 → assertpy2-2.26.0}/mkdocs_hooks.py +0 -0
  159. {assertpy2-2.24.0 → assertpy2-2.26.0}/scripts/mutation_report.py +0 -0
  160. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/conftest.py +0 -0
  161. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/docs_fixtures.py +0 -0
  162. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/group_compat.py +0 -0
  163. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_api_compatibility.py +0 -0
  164. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_api_vocabulary.py +0 -0
  165. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_async.py +0 -0
  166. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_attrs_support.py +0 -0
  167. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_behave_matchers.py +0 -0
  168. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_bool.py +0 -0
  169. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_boundary_cases.py +0 -0
  170. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_bytes.py +0 -0
  171. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_callable.py +0 -0
  172. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_chaining.py +0 -0
  173. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_check.py +0 -0
  174. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_check_protocols.py +0 -0
  175. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_class.py +0 -0
  176. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_clustering.py +0 -0
  177. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_collection.py +0 -0
  178. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_core.py +0 -0
  179. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_custom_list.py +0 -0
  180. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_dangling.py +0 -0
  181. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_dataframe.py +0 -0
  182. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_datetime.py +0 -0
  183. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_description.py +0 -0
  184. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_dict.py +0 -0
  185. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_docs_typing.py +0 -0
  186. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_dynamic.py +0 -0
  187. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_errors.py +0 -0
  188. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_evaluation_core.py +0 -0
  189. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_exception_context.py +0 -0
  190. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_expected_warning.py +0 -0
  191. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_extracting.py +0 -0
  192. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_fail.py +0 -0
  193. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_file.py +0 -0
  194. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_grouped_soft.py +0 -0
  195. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_hints.py +0 -0
  196. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_http.py +0 -0
  197. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_in.py +0 -0
  198. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_inline_record.py +0 -0
  199. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_inline_snapshot.py +0 -0
  200. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_iterable_cluster.py +0 -0
  201. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_json.py +0 -0
  202. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_list.py +0 -0
  203. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_match_result.py +0 -0
  204. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_matcher_parity.py +0 -0
  205. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_matcher_registry.py +0 -0
  206. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_namedtuple.py +0 -0
  207. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_none.py +0 -0
  208. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_not.py +0 -0
  209. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_operation_contract.py +0 -0
  210. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_optional_integration_contracts.py +0 -0
  211. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_overload_order.py +0 -0
  212. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_overloads.py +0 -0
  213. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_pipeline.py +0 -0
  214. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_property_attrs.py +0 -0
  215. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_protocol_parity.py +0 -0
  216. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_recursive_assertion.py +0 -0
  217. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_recursive_compare.py +0 -0
  218. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_recursive_compare_attrs.py +0 -0
  219. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_recursive_compare_config.py +0 -0
  220. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_recursive_compare_pydantic.py +0 -0
  221. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_refusals.py +0 -0
  222. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_regex_groups.py +0 -0
  223. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_same_as.py +0 -0
  224. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_satisfy.py +0 -0
  225. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_skip_guard.py +0 -0
  226. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_soft.py +0 -0
  227. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_soft_fail.py +0 -0
  228. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_string.py +0 -0
  229. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_structural.py +0 -0
  230. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_surface_conformance.py +0 -0
  231. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_sync_eventually.py +0 -0
  232. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_traceback.py +0 -0
  233. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_type.py +0 -0
  234. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_type_expression_failures.py +0 -0
  235. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_claims.py +0 -0
  236. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_completeness.py +0 -0
  237. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_conformance.py +0 -0
  238. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_from_a_wheel.py +0 -0
  239. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_http.py +0 -0
  240. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_integrations.py +0 -0
  241. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_typing_negative.py +0 -0
  242. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_vacuity_contract.py +0 -0
  243. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/test_warn.py +0 -0
  244. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_http.py +0 -0
  245. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_integrations.py +0 -0
  246. {assertpy2-2.24.0 → assertpy2-2.26.0}/tests/typing_integrations_baseline.py +0 -0
@@ -25,7 +25,7 @@ Read more about how pulls work on GitHub's [About pull requests](https://help.gi
25
25
 
26
26
  ## Before you change a public signature
27
27
 
28
- Read [ARCHITECTURE.md](ARCHITECTURE.md). Four files under `assertpy2/_engine/` are generated, the public
28
+ Read [ARCHITECTURE.md](ARCHITECTURE.md). Five files under `assertpy2/_engine/` are generated, the public
29
29
  surface is pinned by a recorded snapshot, and neither is discoverable from the files themselves.
30
30
 
31
31
  ## Verification pipeline
@@ -86,15 +86,16 @@ Run these after the main gate, not before. `uv run --group typecheck` adds the c
86
86
 
87
87
  ```bash
88
88
  uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
89
- PYRIGHT_PYTHON_FORCE_VERSION=1.1.413 uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
89
+ PYRIGHT_PYTHON_FORCE_VERSION=1.1.414 uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
90
90
  uv run --group typecheck pyrefly check tests/test_typing.py
91
91
  uv run --group typecheck pytest tests/test_pyright_baseline.py
92
92
  ```
93
93
 
94
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.
95
+ PyPI is released separately from the npm engine it launches, and the two have drifted apart before: PyPI
96
+ stayed at 1.1.411 from June to September while npm shipped two builds. The baseline is recorded against
97
+ one engine, and `tests/typing_harness.py` pins the same build, so only this direct invocation needs the
98
+ variable.
98
99
 
99
100
  `--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless
100
101
  told otherwise, and the count moves with it: 174 diagnostics for this package on 3.10 against 169 on
@@ -123,9 +124,11 @@ Write tests for every new feature or bug fix. Use `assertpy2` assertions in test
123
124
 
124
125
  ## Documentation examples
125
126
 
126
- Guide code blocks are executed and type-checked in CI, so an example you add has to run. A block that
127
+ Guide code blocks are type-checked in CI, and every page that can run in the suite is executed, with what
128
+ each block prints compared with the comment beside it, so an example you add has to run. A block that
127
129
  cannot (pseudo-context, a deliberate failure, a rejected counter-example) is marked with an HTML comment
128
- above the fence: `tests/test_docs_examples.py` lists the markers and what each one exempts.
130
+ above the fence. `tests/test_docs_examples.py` lists the markers and what each one exempts, and it and
131
+ `tests/test_docs_typing.py` each name the pages they leave out, with the reason.
129
132
 
130
133
  Setup a page assumes (a domain class, a repository, an HTTP response) goes in
131
134
  `tests/docs_fixtures.py`, not into an extra block on the page.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.24.0
3
+ Version: 2.26.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
 
@@ -35,7 +35,7 @@ from .matchers import (
35
35
  register_matcher,
36
36
  unregister_matcher,
37
37
  )
38
- from .outcome import AssertionOutcome
38
+ from .outcome import AssertionOutcome, Requirement
39
39
  from .snapshot import (
40
40
  SnapshotCreatedWarning,
41
41
  SnapshotKeyReusedWarning,
@@ -57,6 +57,7 @@ __all__ = [
57
57
  "NegatedBuilder",
58
58
  "PollSample",
59
59
  "PollTrace",
60
+ "Requirement",
60
61
  "SnapshotCreatedWarning",
61
62
  "SnapshotKeyReusedWarning",
62
63
  "SnapshotUpdatedWarning",
@@ -26,7 +26,15 @@ 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 (
30
+ _Callable,
31
+ _Indexed,
32
+ _Keyed,
33
+ _KeyedWithItems,
34
+ _KeyedWithValues,
35
+ _Orderable,
36
+ _PathLike,
37
+ )
30
38
  from ._introspection import MappingLike
31
39
  from ._typing import (
32
40
  _U,
@@ -551,6 +559,53 @@ if TYPE_CHECKING:
551
559
 
552
560
  def matches_with_groups(self: _CheckAnyValue[str], pattern: str) -> AssertionOutcome: ...
553
561
 
562
+ @overload
563
+ def exists(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
564
+ @overload
565
+ def exists(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
566
+
567
+ @overload
568
+ def does_not_exist(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
569
+ @overload
570
+ def does_not_exist(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
571
+
572
+ @overload
573
+ def is_file(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
574
+ @overload
575
+ def is_file(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
576
+
577
+ @overload
578
+ def is_directory(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
579
+ @overload
580
+ def is_directory(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
581
+
582
+ @overload
583
+ def is_named(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path], filename: str) -> AssertionOutcome: ...
584
+ @overload
585
+ def is_named(self: _CheckAnyValue[_PathLike], filename: str) -> AssertionOutcome: ...
586
+
587
+ @overload
588
+ def is_child_of(
589
+ self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path], parent: object
590
+ ) -> AssertionOutcome: ...
591
+ @overload
592
+ def is_child_of(self: _CheckAnyValue[_PathLike], parent: object) -> AssertionOutcome: ...
593
+
594
+ @overload
595
+ def is_readable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
596
+ @overload
597
+ def is_readable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
598
+
599
+ @overload
600
+ def is_writable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
601
+ @overload
602
+ def is_writable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
603
+
604
+ @overload
605
+ def is_executable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
606
+ @overload
607
+ def is_executable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
608
+
554
609
  @overload
555
610
  def is_subset_of(
556
611
  self: _CheckAnyValue[str]
@@ -574,10 +629,11 @@ if TYPE_CHECKING:
574
629
  self: _CheckAnyValue[_CapableT], *supersets: object, allow_empty: bool = ...
575
630
  ) -> AssertionOutcome: ...
576
631
 
632
+ @overload
633
+ def contains_duplicates(self: _CheckAnyValue[str]) -> AssertionOutcome: ...
577
634
  @overload
578
635
  def contains_duplicates(
579
- self: _CheckAnyValue[str]
580
- | _CheckAnyValue[list[_E]]
636
+ self: _CheckAnyValue[list[_E]]
581
637
  | _CheckAnyValue[tuple[_E, ...]]
582
638
  | _CheckAnyValue[set[_E]]
583
639
  | _CheckAnyValue[frozenset[_E]],
@@ -585,10 +641,11 @@ if TYPE_CHECKING:
585
641
  @overload
586
642
  def contains_duplicates(self: _CheckAnyValue[_CapableT]) -> AssertionOutcome: ...
587
643
 
644
+ @overload
645
+ def does_not_contain_duplicates(self: _CheckAnyValue[str]) -> AssertionOutcome: ...
588
646
  @overload
589
647
  def does_not_contain_duplicates(
590
- self: _CheckAnyValue[str]
591
- | _CheckAnyValue[list[_E]]
648
+ self: _CheckAnyValue[list[_E]]
592
649
  | _CheckAnyValue[tuple[_E, ...]]
593
650
  | _CheckAnyValue[set[_E]]
594
651
  | _CheckAnyValue[frozenset[_E]],
@@ -596,10 +653,11 @@ if TYPE_CHECKING:
596
653
  @overload
597
654
  def does_not_contain_duplicates(self: _CheckAnyValue[_CapableT]) -> AssertionOutcome: ...
598
655
 
656
+ @overload
657
+ def contains_only_once(self: _CheckAnyValue[str], *items: object) -> AssertionOutcome: ...
599
658
  @overload
600
659
  def contains_only_once(
601
- self: _CheckAnyValue[str]
602
- | _CheckAnyValue[list[_E]]
660
+ self: _CheckAnyValue[list[_E]]
603
661
  | _CheckAnyValue[tuple[_E, ...]]
604
662
  | _CheckAnyValue[set[_E]]
605
663
  | _CheckAnyValue[frozenset[_E]],
@@ -608,17 +666,20 @@ if TYPE_CHECKING:
608
666
  @overload
609
667
  def contains_only_once(self: _CheckAnyValue[_CapableT], *items: object) -> AssertionOutcome: ...
610
668
 
669
+ @overload
670
+ def contains_in_order(self: _CheckAnyValue[str], *items: str | Matcher[str]) -> AssertionOutcome: ...
611
671
  @overload
612
672
  def contains_in_order(
613
- self: _CheckAnyValue[str]
614
- | _CheckAnyValue[list[_E]]
673
+ self: _CheckAnyValue[list[_E]]
615
674
  | _CheckAnyValue[tuple[_E, ...]]
616
675
  | _CheckAnyValue[set[_E]]
617
676
  | _CheckAnyValue[frozenset[_E]],
618
677
  *items: _E | Matcher[_E],
619
678
  ) -> AssertionOutcome: ...
620
679
  @overload
621
- def contains_in_order(self: _CheckAnyValue[_CapableT], *items: _E | Matcher[_E]) -> AssertionOutcome: ...
680
+ def contains_in_order(
681
+ self: _CheckAnyValue[Iterable[_E] | _Indexed[_E]], *items: _E | Matcher[_E]
682
+ ) -> AssertionOutcome: ...
622
683
 
623
684
  @overload
624
685
  def has_same_size_as(
@@ -721,53 +782,6 @@ if TYPE_CHECKING:
721
782
  @overload
722
783
  def is_not_empty(self: _CheckAnyValue[_CapableT]) -> AssertionOutcome: ...
723
784
 
724
- @overload
725
- def exists(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
726
- @overload
727
- def exists(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
728
-
729
- @overload
730
- def does_not_exist(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
731
- @overload
732
- def does_not_exist(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
733
-
734
- @overload
735
- def is_file(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
736
- @overload
737
- def is_file(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
738
-
739
- @overload
740
- def is_directory(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
741
- @overload
742
- def is_directory(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
743
-
744
- @overload
745
- def is_named(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path], filename: str) -> AssertionOutcome: ...
746
- @overload
747
- def is_named(self: _CheckAnyValue[_PathLike], filename: str) -> AssertionOutcome: ...
748
-
749
- @overload
750
- def is_child_of(
751
- self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path], parent: object
752
- ) -> AssertionOutcome: ...
753
- @overload
754
- def is_child_of(self: _CheckAnyValue[_PathLike], parent: object) -> AssertionOutcome: ...
755
-
756
- @overload
757
- def is_readable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
758
- @overload
759
- def is_readable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
760
-
761
- @overload
762
- def is_writable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
763
- @overload
764
- def is_writable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
765
-
766
- @overload
767
- def is_executable(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
768
- @overload
769
- def is_executable(self: _CheckAnyValue[_PathLike]) -> AssertionOutcome: ...
770
-
771
785
  @overload
772
786
  def is_positive(
773
787
  self: _CheckAnyValue[bool] | _CheckAnyValue[int] | _CheckAnyValue[float],
@@ -906,29 +920,33 @@ if TYPE_CHECKING:
906
920
  @overload
907
921
  def contains_key(self: _CheckAnyValue[dict[_K, _V]], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
908
922
  @overload
909
- def contains_key(self: _CheckAnyValue[_CapableT], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
923
+ def contains_key(self: _CheckAnyValue[_Keyed], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
910
924
 
911
925
  @overload
912
926
  def does_not_contain_key(self: _CheckAnyValue[dict[_K, _V]], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
913
927
  @overload
914
- def does_not_contain_key(self: _CheckAnyValue[_CapableT], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
928
+ def does_not_contain_key(self: _CheckAnyValue[_Keyed], *keys: _K | Matcher[_K]) -> AssertionOutcome: ...
915
929
 
916
930
  @overload
917
931
  def contains_value(self: _CheckAnyValue[dict[_K, _V]], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
918
932
  @overload
919
- def contains_value(self: _CheckAnyValue[_CapableT], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
933
+ def contains_value(self: _CheckAnyValue[_KeyedWithValues], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
920
934
 
921
935
  @overload
922
936
  def does_not_contain_value(
923
937
  self: _CheckAnyValue[dict[_K, _V]], *values: _V | Matcher[_V]
924
938
  ) -> AssertionOutcome: ...
925
939
  @overload
926
- def does_not_contain_value(self: _CheckAnyValue[_CapableT], *values: _V | Matcher[_V]) -> AssertionOutcome: ...
940
+ def does_not_contain_value(
941
+ self: _CheckAnyValue[_KeyedWithValues], *values: _V | Matcher[_V]
942
+ ) -> AssertionOutcome: ...
927
943
 
928
944
  @overload
929
945
  def contains_entry(self: _CheckAnyValue[dict[_K, _V]], *args: object, **kwargs: object) -> AssertionOutcome: ...
930
946
  @overload
931
- def contains_entry(self: _CheckAnyValue[_CapableT], *args: object, **kwargs: object) -> AssertionOutcome: ...
947
+ def contains_entry(
948
+ self: _CheckAnyValue[_KeyedWithItems], *args: object, **kwargs: object
949
+ ) -> AssertionOutcome: ...
932
950
 
933
951
  @overload
934
952
  def does_not_contain_entry(
@@ -936,7 +954,7 @@ if TYPE_CHECKING:
936
954
  ) -> AssertionOutcome: ...
937
955
  @overload
938
956
  def does_not_contain_entry(
939
- self: _CheckAnyValue[_CapableT], *args: object, **kwargs: object
957
+ self: _CheckAnyValue[_KeyedWithItems], *args: object, **kwargs: object
940
958
  ) -> AssertionOutcome: ...
941
959
 
942
960
  @overload