assertpy2 2.22.0__tar.gz → 2.23.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 (224) hide show
  1. {assertpy2-2.22.0 → assertpy2-2.23.0}/CONTRIBUTING.md +6 -1
  2. {assertpy2-2.22.0 → assertpy2-2.23.0}/PKG-INFO +4 -3
  3. {assertpy2-2.22.0 → assertpy2-2.23.0}/README.md +3 -2
  4. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_clustering.py +9 -15
  5. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_dangling.py +106 -18
  6. assertpy2-2.23.0/assertpy2/_engine/_builder_check_typing.py +1322 -0
  7. assertpy2-2.23.0/assertpy2/_engine/_check_typing.py +662 -0
  8. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_compare.py +11 -13
  9. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_contract.py +2 -4
  10. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_diff.py +13 -16
  11. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_equality.py +8 -13
  12. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_introspection.py +8 -3
  13. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_membership.py +21 -22
  14. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_mixin_base.py +2 -10
  15. assertpy2-2.23.0/assertpy2/_engine/_operations.py +82 -0
  16. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_ordering.py +5 -8
  17. assertpy2-2.23.0/assertpy2/_engine/_poll_typing.py +2782 -0
  18. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_require.py +7 -14
  19. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_size.py +2 -3
  20. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_typing.py +448 -111
  21. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_hints.py +19 -36
  22. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_inline.py +1 -2
  23. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_matcher_impls.py +47 -72
  24. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_satisfies.py +80 -53
  25. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_snapshot_codec.py +2 -4
  26. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/assertpy.py +211 -116
  27. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/async_assertions.py +29 -55
  28. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/base.py +30 -25
  29. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/bytes_mixin.py +7 -5
  30. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/collection.py +62 -26
  31. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/contains.py +53 -37
  32. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dataframe.py +11 -7
  33. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/date.py +21 -14
  34. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dict.py +4 -2
  35. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dynamic.py +2 -3
  36. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/errors.py +24 -7
  37. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/exception.py +29 -7
  38. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/extracting.py +7 -13
  39. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/file.py +7 -2
  40. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/helpers.py +15 -22
  41. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/http_mixin.py +6 -11
  42. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/json_mixin.py +9 -17
  43. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/matchers.py +6 -18
  44. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/numeric.py +46 -20
  45. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/pytest_plugin.py +125 -40
  46. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/snapshot.py +10 -23
  47. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/string.py +57 -36
  48. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/warning.py +5 -4
  49. {assertpy2-2.22.0 → assertpy2-2.23.0}/benchmarks/test_perf.py +17 -20
  50. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/concepts/stability.md +41 -4
  51. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/concepts/type-safety.md +62 -24
  52. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/extending/custom-assertions.md +6 -3
  53. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/assertions.md +14 -1
  54. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/errors.md +15 -0
  55. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/matchers.md +51 -0
  56. {assertpy2-2.22.0 → assertpy2-2.23.0}/pyproject.toml +15 -2
  57. assertpy2-2.23.0/scripts/generate_check_protocols.py +146 -0
  58. assertpy2-2.23.0/scripts/generate_poll_protocols.py +448 -0
  59. assertpy2-2.23.0/scripts/mutation_report.py +139 -0
  60. assertpy2-2.23.0/tests/api_snapshot.json +5967 -0
  61. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/api_surface.py +113 -3
  62. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/conftest.py +14 -0
  63. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/docs_fixtures.py +8 -0
  64. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/pyright_baseline.py +19 -21
  65. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_api_compatibility.py +82 -1
  66. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_api_vocabulary.py +0 -3
  67. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_async.py +3 -15
  68. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_boundary_cases.py +2 -25
  69. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_bytes.py +2 -4
  70. assertpy2-2.23.0/tests/test_check_protocols.py +205 -0
  71. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_clustering.py +0 -26
  72. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_core.py +13 -30
  73. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_custom_dict.py +7 -14
  74. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_custom_list.py +3 -6
  75. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dangling.py +166 -10
  76. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dataframe.py +27 -3
  77. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_datetime.py +7 -7
  78. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dict_compare.py +0 -3
  79. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_docs_examples.py +1 -1
  80. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_docs_typing.py +33 -9
  81. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dynamic.py +1 -2
  82. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_equals.py +1 -9
  83. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_errors.py +39 -4
  84. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_evaluation_core.py +52 -37
  85. assertpy2-2.23.0/tests/test_expected_contract.py +183 -0
  86. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_expected_exception.py +45 -21
  87. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_expected_warning.py +2 -12
  88. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_extensions.py +3 -5
  89. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_extracting.py +1 -4
  90. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_file.py +1 -1
  91. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_grouped_soft.py +7 -7
  92. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_hints.py +13 -62
  93. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_http.py +0 -12
  94. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_inline_record.py +2 -2
  95. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_iterable_cluster.py +15 -3
  96. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_list.py +2 -10
  97. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_match_result.py +69 -0
  98. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matcher_parity.py +2 -2
  99. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matchers.py +0 -8
  100. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_message_elision.py +2 -2
  101. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_not.py +10 -13
  102. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_numbers.py +0 -1
  103. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_openapi_contract.py +0 -5
  104. assertpy2-2.23.0/tests/test_operation_contract.py +410 -0
  105. assertpy2-2.23.0/tests/test_overload_order.py +170 -0
  106. assertpy2-2.23.0/tests/test_poll_protocols.py +235 -0
  107. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_property_attrs.py +1 -1
  108. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_property_based.py +73 -165
  109. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_protocol_parity.py +104 -10
  110. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_public_surface.py +13 -7
  111. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pyright_baseline.py +22 -2
  112. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pytest_plugin.py +200 -115
  113. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare.py +0 -26
  114. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_config.py +1 -22
  115. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_refusals.py +3 -5
  116. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_rich_diff.py +28 -77
  117. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_satisfy.py +0 -2
  118. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_snapshots.py +16 -45
  119. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_soft.py +2 -5
  120. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_structural.py +26 -32
  121. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_surface_conformance.py +4 -9
  122. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_traceback.py +1 -3
  123. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing.py +139 -84
  124. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing_claims.py +0 -1
  125. assertpy2-2.23.0/tests/test_typing_conformance.py +440 -0
  126. assertpy2-2.23.0/tests/test_typing_from_a_wheel.py +101 -0
  127. assertpy2-2.23.0/tests/test_typing_http.py +78 -0
  128. assertpy2-2.23.0/tests/test_typing_integrations.py +111 -0
  129. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing_negative.py +57 -71
  130. assertpy2-2.23.0/tests/test_vacuity_contract.py +466 -0
  131. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_warn.py +24 -33
  132. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/typing_cases.py +61 -10
  133. assertpy2-2.23.0/tests/typing_harness.py +80 -0
  134. assertpy2-2.23.0/tests/typing_http.py +75 -0
  135. assertpy2-2.23.0/tests/typing_integrations.py +64 -0
  136. assertpy2-2.23.0/tests/typing_integrations_baseline.py +93 -0
  137. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/typing_negative_baseline.py +84 -27
  138. assertpy2-2.22.0/tests/api_snapshot.json +0 -3724
  139. assertpy2-2.22.0/tests/test_vacuity_contract.py +0 -180
  140. {assertpy2-2.22.0 → assertpy2-2.23.0}/.gitignore +0 -0
  141. {assertpy2-2.22.0 → assertpy2-2.23.0}/LICENSE +0 -0
  142. {assertpy2-2.22.0 → assertpy2-2.23.0}/SECURITY.md +0 -0
  143. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/__init__.py +0 -0
  144. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/__init__.py +0 -0
  145. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_compat.py +0 -0
  146. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_path.py +0 -0
  147. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_text.py +0 -0
  148. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/behave_matchers.py +0 -0
  149. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/outcome.py +0 -0
  150. {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/py.typed +0 -0
  151. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-equal.png +0 -0
  152. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-equal.svg +0 -0
  153. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-gallery.png +0 -0
  154. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-match.svg +0 -0
  155. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-sequence.svg +0 -0
  156. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-set.svg +0 -0
  157. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-string.svg +0 -0
  158. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/extending/integrations.md +0 -0
  159. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/comparison.md +0 -0
  160. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/migration.md +0 -0
  161. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/quickstart.md +0 -0
  162. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/data.md +0 -0
  163. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/fluent.md +0 -0
  164. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/testing.md +0 -0
  165. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/index.md +0 -0
  166. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/logo-dark.svg +0 -0
  167. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/logo.svg +0 -0
  168. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/recipes.md +0 -0
  169. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/async.md +0 -0
  170. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/bytes.md +0 -0
  171. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/collections.md +0 -0
  172. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/containment.md +0 -0
  173. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/core.md +0 -0
  174. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dataframes.md +0 -0
  175. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dates.md +0 -0
  176. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dicts.md +0 -0
  177. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dynamic.md +0 -0
  178. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/entry-points.md +0 -0
  179. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/errors.md +0 -0
  180. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/exceptions.md +0 -0
  181. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/extracting.md +0 -0
  182. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/files.md +0 -0
  183. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/http.md +0 -0
  184. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/json.md +0 -0
  185. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/matchers.md +0 -0
  186. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/numbers.md +0 -0
  187. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/overview.md +0 -0
  188. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/snapshots.md +0 -0
  189. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/strings.md +0 -0
  190. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/warnings.md +0 -0
  191. {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/stylesheets/extra.css +0 -0
  192. {assertpy2-2.22.0 → assertpy2-2.23.0}/mkdocs.yml +0 -0
  193. {assertpy2-2.22.0 → assertpy2-2.23.0}/mkdocs_hooks.py +0 -0
  194. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/group_compat.py +0 -0
  195. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_attrs_support.py +0 -0
  196. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_behave_matchers.py +0 -0
  197. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_bool.py +0 -0
  198. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_callable.py +0 -0
  199. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_chaining.py +0 -0
  200. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_check.py +0 -0
  201. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_class.py +0 -0
  202. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_collection.py +0 -0
  203. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_description.py +0 -0
  204. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dict.py +0 -0
  205. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_exception_context.py +0 -0
  206. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_fail.py +0 -0
  207. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_in.py +0 -0
  208. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_inline_snapshot.py +0 -0
  209. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_json.py +0 -0
  210. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matcher_registry.py +0 -0
  211. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_namedtuple.py +0 -0
  212. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_none.py +0 -0
  213. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_optional_integration_contracts.py +0 -0
  214. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_overloads.py +0 -0
  215. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pipeline.py +0 -0
  216. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_assertion.py +0 -0
  217. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_attrs.py +0 -0
  218. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_pydantic.py +0 -0
  219. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_regex_groups.py +0 -0
  220. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_same_as.py +0 -0
  221. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_soft_fail.py +0 -0
  222. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_string.py +0 -0
  223. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_sync_eventually.py +0 -0
  224. {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_type.py +0 -0
@@ -49,10 +49,15 @@ run with them installed collects a different set:
49
49
 
50
50
  ```bash
51
51
  uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
52
- uv run --group typecheck pyright tests/test_typing.py
52
+ uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
53
53
  uv run --group typecheck pytest tests/test_pyright_baseline.py
54
54
  ```
55
55
 
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.
58
+ Without it a contributor on the supported floor meets a red baseline that says nothing about their
59
+ change. The baseline test passes the same target itself, so it gives one answer everywhere.
60
+
56
61
  Three details that cost time if you meet them the hard way:
57
62
 
58
63
  - `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: assertpy2
3
- Version: 2.22.0
3
+ Version: 2.23.0
4
4
  Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
@@ -57,7 +57,7 @@ Description-Content-Type: text/markdown
57
57
  <img src="docs/logo.svg" alt="assertpy2" width="280">
58
58
  </picture>
59
59
  <br>
60
- <b>A fully typed fluent assertion library for Python</b><br>
60
+ <b>Fully typed fluent assertion library for Python</b><br>
61
61
  A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
62
62
  </p>
63
63
 
@@ -209,7 +209,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
209
209
  - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
210
210
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
211
211
 
212
- 11 type-specific Protocols instead of one `Any`.<br>
212
+ 15 type-specific Protocols instead of one `Any`.<br>
213
213
  Works in PyCharm, VS Code, and any LSP-compatible editor.
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>
@@ -263,6 +263,7 @@ is open to anything else you build.
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
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.
266
267
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
267
268
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
268
269
  - [**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.
@@ -5,7 +5,7 @@
5
5
  <img src="docs/logo.svg" alt="assertpy2" width="280">
6
6
  </picture>
7
7
  <br>
8
- <b>A fully typed fluent assertion library for Python</b><br>
8
+ <b>Fully typed fluent assertion library for Python</b><br>
9
9
  A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
10
10
  </p>
11
11
 
@@ -157,7 +157,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
157
157
  - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
158
158
  - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
159
159
 
160
- 11 type-specific Protocols instead of one `Any`.<br>
160
+ 15 type-specific Protocols instead of one `Any`.<br>
161
161
  Works in PyCharm, VS Code, and any LSP-compatible editor.
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>
@@ -211,6 +211,7 @@ is open to anything else you build.
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
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.
214
215
  - [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
215
216
  - [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
216
217
  - [**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.
@@ -59,9 +59,7 @@ __tracebackhide__ = True
59
59
  # steps that name a *place* in a value, as opposed to `item`, which names a member of a set
60
60
  _COORDINATE: Final = frozenset({"key", "index", "attr", "line"})
61
61
 
62
- # a position collapses to one placeholder: `users[0].role` and `users[7].role` are the same difference
63
- # reported against two rows, and a reader chasing a common cause wants them in one line. a line number
64
- # is incidental in the same way
62
+ # a position collapses to one placeholder: `users[0].role` and `users[7].role` are one difference on two rows
65
63
  _POSITIONAL: Final = frozenset({"index", "line"})
66
64
  _ANY_POSITION: Final = "[*]"
67
65
 
@@ -190,8 +188,8 @@ def stable_repr(value: object, _seen: frozenset[int] = frozenset()) -> str:
190
188
  try:
191
189
  if isinstance(value, (set, frozenset)):
192
190
  members = sorted(stable_repr(member, inner) for member in value)
193
- # an empty one falls back to `repr`, since `{}` is a dict; the price is that `set()`
194
- # and the `frozenset()` it equals read apart, where two non-empty equal ones read alike
191
+ # an empty one falls back to `repr`, since `{}` is a dict, so `set()` and the `frozenset()` it equals
192
+ # read apart
195
193
  return "{" + ", ".join(members) + "}" if value else _safe_repr(value)
196
194
  if isinstance(value, dict):
197
195
  pairs = (f"{stable_repr(key, inner)}: {stable_repr(item, inner)}" for key, item in value.items())
@@ -289,9 +287,8 @@ def is_well_formed(key: Signature) -> bool:
289
287
  )
290
288
  if key.steps:
291
289
  return False
292
- # exactly one of the two, which is what `signature()` sets: the diagnostic replaces the values,
293
- # never joins them. A pair of values only for the kinds whose fields hold the compared values, or
294
- # a `contains` payload would print the `None` that stands for a missing item as somebody's value
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
295
292
  if key.label:
296
293
  return not key.values
297
294
  return len(key.values) == 2 and key.where in _VALUES_ARE_VALUES
@@ -330,17 +327,15 @@ def clusters(
330
327
  """
331
328
  if total_failures <= 0:
332
329
  return []
333
- # keyed on the node id, not appended: one test can be reported failing more than once, which is
334
- # what `pytest-rerunfailures` does on every retry, and counting its attempts as separate failures
335
- # printed a cluster of six over two tests
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
336
332
  grouped: dict[Signature, dict[str, None]] = {}
337
333
  actuals: dict[Signature, dict[str, None]] = {}
338
334
  expecteds: dict[Signature, dict[str, None]] = {}
339
335
  for nodeid, found in recorded:
340
336
  for one in found:
341
337
  grouped.setdefault(one.signature, {})[nodeid] = None
342
- # one past the limit, so a full side is distinguishable from a capped one by its length
343
- # alone. The smallest value is always kept, whichever order the failures came in
338
+ # one past the limit, so a full side is distinguishable from a capped one by its length alone
344
339
  for store, value in ((actuals, one.actual), (expecteds, one.expected)):
345
340
  seen = store.setdefault(one.signature, {})
346
341
  if len(seen) <= _EXAMPLE_LIMIT:
@@ -353,8 +348,7 @@ def clusters(
353
348
  for key, nodeids in grouped.items()
354
349
  if len(nodeids) >= minimum
355
350
  ]
356
- # size first, then the signature itself: two clusters of equal size must not swap places between
357
- # runs, and the signature is the only thing about them that cannot vary
351
+ # size first, then the signature: two clusters of equal size must not swap places between runs
358
352
  large.sort(key=lambda cluster: (-cluster.size, cluster.signature))
359
353
  return large
360
354
 
@@ -18,11 +18,21 @@ runtime and cannot shift a frame.
18
18
  What it deliberately does not flag:
19
19
 
20
20
  * a builder bound to a name (`b = assert_that(x)`), because whether `b` is used later is not a
21
- question about this statement;
22
- * a chain that ends on a pivot (`.described_as(...)`, `.extracting(...)`), because the set of
23
- assertions is a runtime property of the builder and hard-coding it here would rot;
21
+ question about this statement, and ruff's F841 answers it when the answer is never;
24
22
  * `assert_conforms()`, `fail()`, `soft_fail()`, which assert on their own rather than returning a
25
23
  builder, so a bare call to them is correct usage.
24
+
25
+ A chain ending on a pivot or a configurer is the same defect wearing a longer tail:
26
+ ``assert_that(load).raises(ValueError)`` never calls anything, and ``assert_that(rows).extracting("id")``
27
+ throws the extracted value away. Both were left out while the set of operations that reach no verdict
28
+ was only a runtime property; `assertpy2/_engine/_operations.py` states it now, and a gate keeps it in
29
+ step with the source, so reading it here cannot rot.
30
+
31
+ An ``assert`` in front reads the chain instead of discarding it, and a builder and a bound method are
32
+ both truthy, so ``assert assert_that(items).is_empty`` is green on every value. Neither B018 nor
33
+ coverage sees that one: the value is consumed rather than dropped, and the line does run. What the
34
+ ``assert`` reads is the whole question, since ``assert assert_that(x).val`` tests that value instead,
35
+ and the register names the members that hand it back.
26
36
  """
27
37
 
28
38
  from __future__ import annotations
@@ -32,13 +42,22 @@ import io
32
42
  import tokenize
33
43
  from typing import TYPE_CHECKING, Final, NamedTuple
34
44
 
45
+ from ._engine._operations import (
46
+ ALSO_ASSERTS,
47
+ CONFIGURES,
48
+ DESCRIBES,
49
+ HANDS_THE_SUBJECT_BACK,
50
+ POLLS,
51
+ TRANSFORMS,
52
+ WITHOUT_A_VERDICT,
53
+ )
54
+
35
55
  if TYPE_CHECKING:
36
56
  from collections.abc import Iterator
37
57
 
38
58
  __tracebackhide__ = True
39
59
 
40
- # the builder factories, and only those: everything else in the package's public surface either
41
- # asserts by itself or is not an entry point at all
60
+ # the builder factories and only those: everything else either asserts by itself or is not an entry point
42
61
  _ENTRY: Final = frozenset({"assert_that", "assert_warn"})
43
62
  _PACKAGE: Final = "assertpy2"
44
63
 
@@ -58,7 +77,27 @@ A project that registered its own wrapper reads `check(1)` on the offending line
58
77
  `assert_that()` there costs a moment of "that is not what my code says" on every finding.
59
78
  """
60
79
  _NOT_CALLED: Final = "assertion is looked up and never called (missing parentheses)"
80
+ _ENDS_ON: Final = {
81
+ CONFIGURES: "{name}() only sets an expectation here, and nothing calls it",
82
+ TRANSFORMS: "{name}() hands back another value here, and nothing asserts on it",
83
+ DESCRIBES: "{name}() only sets the failure description here, and asserts nothing",
84
+ POLLS: "{name}() starts a polling chain here, and no assertion follows it",
85
+ }
86
+ """What to say about a chain ending on an operation that reaches no verdict, one sentence per kind."""
87
+
88
+ _NO_VERDICT: Final = {name: kind for name, kind in WITHOUT_A_VERDICT.items() if name not in ALSO_ASSERTS}
61
89
  _NOT_AWAITED: Final = "eventually() chain is never awaited, so nothing is polled (missing await)"
90
+ _UNDER_ASSERT: Final = "nothing here asserts: assert reads the builder, whose truth says nothing about the value"
91
+ """What to say about a dangling chain an `assert` reads.
92
+
93
+ The wrapper turns every shape below into the same defect and a worse one: a builder, a proxy and a
94
+ bound method are all truthy, so the line is green whatever the value is, and it reads as if it
95
+ asserted. Neither ruff nor coverage sees it, since the value is consumed and the line does run.
96
+
97
+ The sentence stops at "says nothing" rather than promising the line passes, because a few of the
98
+ builder's own fields are falsy by default and turn the line red instead. Green or red, neither
99
+ outcome was decided by the value under test.
100
+ """
62
101
 
63
102
 
64
103
  class Finding(NamedTuple):
@@ -106,9 +145,8 @@ def _rebound(tree: ast.Module) -> frozenset[str]:
106
145
  names.add(node.id)
107
146
  elif isinstance(node, ast.arg):
108
147
  names.add(node.arg)
109
- # kept apart from the branch below, whose body is identical: merging them into one `or` is what
110
- # ruff asks for and what a type checker then rejects, since `ExceptHandler.name` is `str | None`
111
- # and only the separate branch narrows it
148
+ # merging the identical branches is what ruff asks for and a type checker then rejects, since only the
149
+ # separate branch narrows `ExceptHandler.name`
112
150
  elif isinstance(node, ast.ExceptHandler) and node.name: # noqa: SIM114
113
151
  names.add(node.name)
114
152
  elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
@@ -181,12 +219,12 @@ def _reaches_entry(node: ast.expr, bindings: _Bindings) -> bool:
181
219
  return False
182
220
 
183
221
 
184
- def _polls(node: ast.expr) -> bool:
185
- """True when an attribute chain calls ``eventually()``, the async one, anywhere along it."""
222
+ def _runs(node: ast.expr, name: str) -> bool:
223
+ """True when an attribute chain calls *name* anywhere along it."""
186
224
  current: ast.expr = node
187
225
  while True:
188
226
  if isinstance(current, ast.Call):
189
- if isinstance(current.func, ast.Attribute) and current.func.attr == "eventually":
227
+ if isinstance(current.func, ast.Attribute) and current.func.attr == name:
190
228
  return True
191
229
  current = current.func
192
230
  elif isinstance(current, ast.Attribute):
@@ -195,6 +233,49 @@ def _polls(node: ast.expr) -> bool:
195
233
  return False
196
234
 
197
235
 
236
+ def _tail(node: ast.expr) -> str | None:
237
+ """The name a statement ends on when that name reaches no verdict, else ``None``."""
238
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in _NO_VERDICT:
239
+ return node.func.attr
240
+ return None
241
+
242
+
243
+ def _reads_a_verdict_field(node: ast.Attribute) -> bool:
244
+ """Whether the attribute reads a field off a verdict rather than leaving an assertion uncalled.
245
+
246
+ Both sit behind `check()` and only the step before them tells them apart: `check().is_empty().passed`
247
+ reads a field of what the assertion decided, while `check().is_empty` is that assertion with its
248
+ parentheses missing, which is the shape this whole check exists for.
249
+ """
250
+ if not _runs(node, "check") or not isinstance(node.value, ast.Call):
251
+ return False
252
+ called = node.value.func
253
+ return not (isinstance(called, ast.Attribute) and called.attr == "check")
254
+
255
+
256
+ def _reads_a_truthy_chain(node: ast.expr, bindings: _Bindings) -> bool:
257
+ """Whether `assert <node>` reads a chain that decided nothing.
258
+
259
+ An attribute handing the subject back is excluded, since `assert assert_that(x).val` tests that
260
+ value rather than leaving an assertion uncalled, and so is a verdict's own field. Nothing else is:
261
+ reading the builder's state asserts as little as reading a method does, and `logger` is truthy on
262
+ every subject there is.
263
+
264
+ A chain ending on a bare `check()` is left alone deliberately. It is truthy and asserts nothing,
265
+ but calling it a defect means deciding the name is ours, and a project may register an extension
266
+ called `check` that asserts by itself. A read attribute needs no such guess, since nothing was
267
+ called at all. Measured before deciding: no bare `check()` under an `assert` in 524 files across
268
+ this suite, the fixture projects and a consuming framework.
269
+ """
270
+ if not _reaches_entry(node, bindings):
271
+ return False
272
+ if _entry_call(node, bindings) or _tail(node) or (_runs(node, "eventually") and not _closed(node)):
273
+ return True
274
+ if not isinstance(node, ast.Attribute):
275
+ return False
276
+ return node.attr not in HANDS_THE_SUBJECT_BACK and not _reads_a_verdict_field(node)
277
+
278
+
198
279
  def _closed(node: ast.expr) -> bool:
199
280
  """True when the statement ends in a bare ``close()``, the discard the chain itself stays quiet about."""
200
281
  return (
@@ -206,11 +287,12 @@ def _closed(node: ast.expr) -> bool:
206
287
  )
207
288
 
208
289
 
209
- def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr, tuple[str, ...]]]:
210
- """Every expression statement with the scope it sits in, as the chain of names around it.
290
+ def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr | ast.Assert, tuple[str, ...]]]:
291
+ """Every statement that consumes a builder on the spot, with the scope it sits in.
211
292
 
212
- An expression statement is the only place a discarded builder can appear: anywhere else the value
213
- is bound, passed or returned, and whether *that* asserts is not a question about this statement.
293
+ Two do. An expression statement discards the value, and anywhere else it is bound, passed or
294
+ returned, where whether *that* asserts is not a question about this statement. An `assert` reads
295
+ it for its truth, which a builder and a bound method answer the same way whatever the value is.
214
296
 
215
297
  The scope is a chain rather than one name because a bare name does not identify a test: two classes
216
298
  in one file may each define `test_same`, and matching on the name alone handed both findings to
@@ -224,7 +306,7 @@ def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr, tuple[str, ...]]]:
224
306
  if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
225
307
  stack.append((child, (*scope, child.name)))
226
308
  else:
227
- if isinstance(child, ast.Expr):
309
+ if isinstance(child, ast.Expr | ast.Assert):
228
310
  yield child, scope
229
311
  stack.append((child, scope))
230
312
 
@@ -247,7 +329,7 @@ def _marked_lines(source: str) -> frozenset[int]:
247
329
  return frozenset()
248
330
 
249
331
 
250
- def _silenced(statement: ast.Expr, marked: frozenset[int]) -> bool:
332
+ def _silenced(statement: ast.stmt, marked: frozenset[int]) -> bool:
251
333
  """Whether the statement carries the marker on any of its own lines.
252
334
 
253
335
  Every line the statement spans counts, so the marker can sit at the end of a call broken over
@@ -279,6 +361,10 @@ def findings(source: str, path: str, extra_entries: frozenset[str] = frozenset()
279
361
  for statement, scope in _statements(tree):
280
362
  if _silenced(statement, marked):
281
363
  continue
364
+ if isinstance(statement, ast.Assert):
365
+ if _reads_a_truthy_chain(statement.test, bindings):
366
+ found.append(Finding(path, statement.lineno, _UNDER_ASSERT, scope))
367
+ continue
282
368
  value = statement.value
283
369
  awaited = isinstance(value, ast.Await)
284
370
  while isinstance(value, ast.Await): # `await assert_that(x).eventually()...` unwraps to the chain
@@ -287,6 +373,8 @@ def findings(source: str, path: str, extra_entries: frozenset[str] = frozenset()
287
373
  found.append(Finding(path, statement.lineno, _NO_ASSERTION.format(name=name), scope))
288
374
  elif isinstance(value, ast.Attribute) and _reaches_entry(value, bindings):
289
375
  found.append(Finding(path, statement.lineno, _NOT_CALLED, scope))
290
- elif not awaited and _polls(value) and not _closed(value) and _reaches_entry(value, bindings):
376
+ elif not awaited and _runs(value, "eventually") and not _closed(value) and _reaches_entry(value, bindings):
291
377
  found.append(Finding(path, statement.lineno, _NOT_AWAITED, scope))
378
+ elif (tail := _tail(value)) and _reaches_entry(value, bindings):
379
+ found.append(Finding(path, statement.lineno, _ENDS_ON[_NO_VERDICT[tail]].format(name=tail), scope))
292
380
  return sorted(found, key=lambda finding: finding.lineno)