assertpy 1.1__tar.gz → 2.36.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 (380) hide show
  1. assertpy-2.36.0/.github/workflows/ci.yml +465 -0
  2. assertpy-2.36.0/.gitignore +34 -0
  3. assertpy-2.36.0/ARCHITECTURE.md +167 -0
  4. assertpy-2.36.0/CONTRIBUTING.md +163 -0
  5. {assertpy-1.1 → assertpy-2.36.0}/LICENSE +2 -1
  6. assertpy-2.36.0/PKG-INFO +324 -0
  7. assertpy-2.36.0/README.md +271 -0
  8. assertpy-2.36.0/SECURITY.md +24 -0
  9. assertpy-2.36.0/assertpy/__init__.py +87 -0
  10. assertpy-2.36.0/assertpy/_clustering.py +511 -0
  11. assertpy-2.36.0/assertpy/_dangling.py +505 -0
  12. assertpy-2.36.0/assertpy/_engine/__init__.py +1 -0
  13. assertpy-2.36.0/assertpy/_engine/_builder_check_typing.py +1369 -0
  14. assertpy-2.36.0/assertpy/_engine/_capable_typing.py +778 -0
  15. assertpy-2.36.0/assertpy/_engine/_check_typing.py +756 -0
  16. assertpy-2.36.0/assertpy/_engine/_compare.py +983 -0
  17. assertpy-2.36.0/assertpy/_engine/_compat.py +38 -0
  18. assertpy-2.36.0/assertpy/_engine/_contract.py +2003 -0
  19. assertpy-2.36.0/assertpy/_engine/_diff.py +1058 -0
  20. assertpy-2.36.0/assertpy/_engine/_equality.py +843 -0
  21. assertpy-2.36.0/assertpy/_engine/_introspection.py +729 -0
  22. assertpy-2.36.0/assertpy/_engine/_membership.py +522 -0
  23. assertpy-2.36.0/assertpy/_engine/_mixin_base.py +258 -0
  24. assertpy-2.36.0/assertpy/_engine/_negated_typing.py +2200 -0
  25. assertpy-2.36.0/assertpy/_engine/_operations.py +75 -0
  26. assertpy-2.36.0/assertpy/_engine/_ordering.py +884 -0
  27. assertpy-2.36.0/assertpy/_engine/_pairing.py +70 -0
  28. assertpy-2.36.0/assertpy/_engine/_path.py +158 -0
  29. assertpy-2.36.0/assertpy/_engine/_poll_typing.py +6055 -0
  30. assertpy-2.36.0/assertpy/_engine/_require.py +312 -0
  31. assertpy-2.36.0/assertpy/_engine/_size.py +31 -0
  32. assertpy-2.36.0/assertpy/_engine/_text.py +39 -0
  33. assertpy-2.36.0/assertpy/_engine/_typing.py +1227 -0
  34. assertpy-2.36.0/assertpy/_hints.py +1213 -0
  35. assertpy-2.36.0/assertpy/_html.py +439 -0
  36. assertpy-2.36.0/assertpy/_inline.py +484 -0
  37. assertpy-2.36.0/assertpy/_matcher_impls.py +1911 -0
  38. assertpy-2.36.0/assertpy/_passes.py +249 -0
  39. assertpy-2.36.0/assertpy/_renamed.py +64 -0
  40. assertpy-2.36.0/assertpy/_satisfies.py +835 -0
  41. assertpy-2.36.0/assertpy/_snapshot_codec.py +537 -0
  42. assertpy-2.36.0/assertpy/assertpy.py +2641 -0
  43. assertpy-2.36.0/assertpy/async_assertions.py +864 -0
  44. assertpy-2.36.0/assertpy/base.py +862 -0
  45. assertpy-2.36.0/assertpy/behave_matchers.py +96 -0
  46. assertpy-2.36.0/assertpy/bytes_mixin.py +158 -0
  47. assertpy-2.36.0/assertpy/collection.py +528 -0
  48. assertpy-2.36.0/assertpy/contains.py +1068 -0
  49. assertpy-2.36.0/assertpy/dataframe.py +200 -0
  50. assertpy-2.36.0/assertpy/date.py +327 -0
  51. assertpy-2.36.0/assertpy/dict.py +249 -0
  52. assertpy-2.36.0/assertpy/dynamic.py +222 -0
  53. assertpy-2.36.0/assertpy/errors.py +782 -0
  54. assertpy-2.36.0/assertpy/exception.py +838 -0
  55. assertpy-2.36.0/assertpy/extracting.py +266 -0
  56. assertpy-2.36.0/assertpy/file.py +324 -0
  57. assertpy-2.36.0/assertpy/helpers.py +747 -0
  58. assertpy-2.36.0/assertpy/http_mixin.py +218 -0
  59. assertpy-2.36.0/assertpy/json_mixin.py +1265 -0
  60. assertpy-2.36.0/assertpy/matchers.py +651 -0
  61. assertpy-2.36.0/assertpy/numeric.py +696 -0
  62. assertpy-2.36.0/assertpy/outcome.py +163 -0
  63. assertpy-2.36.0/assertpy/py.typed +0 -0
  64. assertpy-2.36.0/assertpy/pytest_plugin.py +1679 -0
  65. assertpy-2.36.0/assertpy/snapshot.py +1116 -0
  66. assertpy-2.36.0/assertpy/string.py +877 -0
  67. assertpy-2.36.0/assertpy/warning.py +137 -0
  68. assertpy-2.36.0/benchmarks/test_perf.py +492 -0
  69. assertpy-2.36.0/bridge/LICENSE +28 -0
  70. assertpy-2.36.0/bridge/README.md +17 -0
  71. assertpy-2.36.0/bridge/assertpy2/__init__.py +27 -0
  72. assertpy-2.36.0/bridge/assertpy2/assertpy.py +8 -0
  73. assertpy-2.36.0/bridge/assertpy2/async_assertions.py +8 -0
  74. assertpy-2.36.0/bridge/assertpy2/base.py +8 -0
  75. assertpy-2.36.0/bridge/assertpy2/behave_matchers.py +8 -0
  76. assertpy-2.36.0/bridge/assertpy2/bytes_mixin.py +8 -0
  77. assertpy-2.36.0/bridge/assertpy2/collection.py +8 -0
  78. assertpy-2.36.0/bridge/assertpy2/contains.py +8 -0
  79. assertpy-2.36.0/bridge/assertpy2/dataframe.py +8 -0
  80. assertpy-2.36.0/bridge/assertpy2/date.py +8 -0
  81. assertpy-2.36.0/bridge/assertpy2/dict.py +8 -0
  82. assertpy-2.36.0/bridge/assertpy2/dynamic.py +8 -0
  83. assertpy-2.36.0/bridge/assertpy2/errors.py +8 -0
  84. assertpy-2.36.0/bridge/assertpy2/exception.py +8 -0
  85. assertpy-2.36.0/bridge/assertpy2/extracting.py +8 -0
  86. assertpy-2.36.0/bridge/assertpy2/file.py +8 -0
  87. assertpy-2.36.0/bridge/assertpy2/helpers.py +8 -0
  88. assertpy-2.36.0/bridge/assertpy2/http_mixin.py +8 -0
  89. assertpy-2.36.0/bridge/assertpy2/json_mixin.py +8 -0
  90. assertpy-2.36.0/bridge/assertpy2/matchers.py +8 -0
  91. assertpy-2.36.0/bridge/assertpy2/numeric.py +8 -0
  92. assertpy-2.36.0/bridge/assertpy2/outcome.py +8 -0
  93. assertpy-2.36.0/bridge/assertpy2/py.typed +0 -0
  94. assertpy-2.36.0/bridge/assertpy2/pytest_plugin.py +8 -0
  95. assertpy-2.36.0/bridge/assertpy2/snapshot.py +8 -0
  96. assertpy-2.36.0/bridge/assertpy2/string.py +8 -0
  97. assertpy-2.36.0/bridge/assertpy2/warning.py +8 -0
  98. assertpy-2.36.0/bridge/pyproject.toml +39 -0
  99. assertpy-2.36.0/docs/assets/diff-equal.png +0 -0
  100. assertpy-2.36.0/docs/assets/diff-equal.svg +51 -0
  101. assertpy-2.36.0/docs/assets/diff-gallery.png +0 -0
  102. assertpy-2.36.0/docs/assets/diff-match.svg +50 -0
  103. assertpy-2.36.0/docs/assets/diff-sequence.svg +51 -0
  104. assertpy-2.36.0/docs/assets/diff-set.svg +47 -0
  105. assertpy-2.36.0/docs/assets/diff-string.svg +59 -0
  106. assertpy-2.36.0/docs/concepts/stability.md +190 -0
  107. assertpy-2.36.0/docs/concepts/type-safety.md +574 -0
  108. assertpy-2.36.0/docs/extending/custom-assertions.md +186 -0
  109. assertpy-2.36.0/docs/extending/integrations.md +487 -0
  110. assertpy-2.36.0/docs/getting-started/comparison.md +263 -0
  111. assertpy-2.36.0/docs/getting-started/migration.md +209 -0
  112. assertpy-2.36.0/docs/getting-started/quickstart.md +86 -0
  113. assertpy-2.36.0/docs/guides/assertions.md +1228 -0
  114. assertpy-2.36.0/docs/guides/data.md +254 -0
  115. assertpy-2.36.0/docs/guides/errors.md +1370 -0
  116. assertpy-2.36.0/docs/guides/fluent.md +199 -0
  117. assertpy-2.36.0/docs/guides/matchers.md +534 -0
  118. assertpy-2.36.0/docs/guides/testing.md +690 -0
  119. assertpy-2.36.0/docs/index.md +189 -0
  120. assertpy-2.36.0/docs/logo-dark.svg +5 -0
  121. assertpy-2.36.0/docs/logo.svg +5 -0
  122. assertpy-2.36.0/docs/recipes.md +324 -0
  123. assertpy-2.36.0/docs/reference/async.md +30 -0
  124. assertpy-2.36.0/docs/reference/bytes.md +5 -0
  125. assertpy-2.36.0/docs/reference/collections.md +5 -0
  126. assertpy-2.36.0/docs/reference/containment.md +5 -0
  127. assertpy-2.36.0/docs/reference/core.md +12 -0
  128. assertpy-2.36.0/docs/reference/dataframes.md +22 -0
  129. assertpy-2.36.0/docs/reference/dates.md +5 -0
  130. assertpy-2.36.0/docs/reference/dicts.md +5 -0
  131. assertpy-2.36.0/docs/reference/dynamic.md +6 -0
  132. assertpy-2.36.0/docs/reference/entry-points.md +105 -0
  133. assertpy-2.36.0/docs/reference/errors.md +54 -0
  134. assertpy-2.36.0/docs/reference/exceptions.md +5 -0
  135. assertpy-2.36.0/docs/reference/extracting.md +5 -0
  136. assertpy-2.36.0/docs/reference/files.md +11 -0
  137. assertpy-2.36.0/docs/reference/http.md +6 -0
  138. assertpy-2.36.0/docs/reference/json.md +5 -0
  139. assertpy-2.36.0/docs/reference/matchers.md +45 -0
  140. assertpy-2.36.0/docs/reference/numbers.md +5 -0
  141. assertpy-2.36.0/docs/reference/overview.md +31 -0
  142. assertpy-2.36.0/docs/reference/snapshots.md +29 -0
  143. assertpy-2.36.0/docs/reference/strings.md +6 -0
  144. assertpy-2.36.0/docs/reference/warnings.md +5 -0
  145. assertpy-2.36.0/docs/stylesheets/extra.css +56 -0
  146. assertpy-2.36.0/mkdocs.yml +164 -0
  147. assertpy-2.36.0/mkdocs_hooks.py +16 -0
  148. assertpy-2.36.0/pyproject.toml +443 -0
  149. assertpy-2.36.0/pyrefly.toml +14 -0
  150. assertpy-2.36.0/scripts/check_sdist.py +236 -0
  151. assertpy-2.36.0/scripts/generate_check_protocols.py +169 -0
  152. assertpy-2.36.0/scripts/generate_poll_protocols.py +1749 -0
  153. assertpy-2.36.0/scripts/mutation_report.py +145 -0
  154. assertpy-2.36.0/scripts/regenerate.py +49 -0
  155. assertpy-2.36.0/tests/api_snapshot.json +7249 -0
  156. assertpy-2.36.0/tests/api_surface.py +514 -0
  157. assertpy-2.36.0/tests/chain_corpus.py +337 -0
  158. assertpy-2.36.0/tests/chain_model.py +633 -0
  159. assertpy-2.36.0/tests/chain_surfaces.py +226 -0
  160. assertpy-2.36.0/tests/conftest.py +111 -0
  161. assertpy-2.36.0/tests/data/json_schema_test_suite/LICENSE +19 -0
  162. assertpy-2.36.0/tests/data/json_schema_test_suite/date-time.json +226 -0
  163. assertpy-2.36.0/tests/data/json_schema_test_suite/date.json +423 -0
  164. assertpy-2.36.0/tests/data/json_schema_test_suite/duration.json +273 -0
  165. assertpy-2.36.0/tests/data/json_schema_test_suite/email.json +368 -0
  166. assertpy-2.36.0/tests/data/json_schema_test_suite/hostname.json +376 -0
  167. assertpy-2.36.0/tests/data/json_schema_test_suite/multipleOf.json +102 -0
  168. assertpy-2.36.0/tests/data/json_schema_test_suite/time.json +297 -0
  169. assertpy-2.36.0/tests/data/json_schema_test_suite/uri.json +248 -0
  170. assertpy-2.36.0/tests/docs_fixtures.py +251 -0
  171. assertpy-2.36.0/tests/format_corpus.py +137 -0
  172. assertpy-2.36.0/tests/group_compat.py +31 -0
  173. assertpy-2.36.0/tests/poll_parity_baseline.py +919 -0
  174. assertpy-2.36.0/tests/poll_parity_cases.py +3331 -0
  175. assertpy-2.36.0/tests/pyright_baseline.py +112 -0
  176. assertpy-2.36.0/tests/snapshot_files_2_35/snap-custom-rich-payload.json +70 -0
  177. assertpy-2.36.0/tests/snapshot_files_2_35/snap-custom-tuple-payload.json +8 -0
  178. assertpy-2.36.0/tests/snapshot_files_2_35/snap-datetime.json +4 -0
  179. assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-marker-value.json +13 -0
  180. assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-nested-records.json +27 -0
  181. assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-shaped-pair.json +9 -0
  182. assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-shaped-tagged.json +12 -0
  183. assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-tuple.json +10 -0
  184. assertpy-2.36.0/tests/snapshot_files_2_35/snap-instance-tuple.json +11 -0
  185. assertpy-2.36.0/tests/snapshot_files_2_35/snap-keyed.json +7 -0
  186. assertpy-2.36.0/tests/snapshot_files_2_35/snap-nested.json +11 -0
  187. assertpy-2.36.0/tests/snapshot_files_2_35/snap-registered-namedtuple.json +4 -0
  188. assertpy-2.36.0/tests/snapshot_files_2_35/snap-set-of-tuples.json +15 -0
  189. assertpy-2.36.0/tests/snapshot_files_2_35/snap-tuple-keys.json +22 -0
  190. assertpy-2.36.0/tests/snapshot_files_2_35/snap-tuple.json +4 -0
  191. assertpy-2.36.0/tests/snapshot_types_2_35.py +66 -0
  192. assertpy-2.36.0/tests/test_api_compatibility.py +471 -0
  193. assertpy-2.36.0/tests/test_api_vocabulary.py +147 -0
  194. assertpy-2.36.0/tests/test_architecture_doc.py +107 -0
  195. assertpy-2.36.0/tests/test_async.py +1705 -0
  196. assertpy-2.36.0/tests/test_async_predicates.py +195 -0
  197. assertpy-2.36.0/tests/test_attrs_support.py +480 -0
  198. assertpy-2.36.0/tests/test_behave_matchers.py +111 -0
  199. assertpy-2.36.0/tests/test_bool.py +33 -0
  200. assertpy-2.36.0/tests/test_boundary_cases.py +112 -0
  201. assertpy-2.36.0/tests/test_bridge.py +94 -0
  202. assertpy-2.36.0/tests/test_bytes.py +243 -0
  203. assertpy-2.36.0/tests/test_call_awaitables.py +163 -0
  204. assertpy-2.36.0/tests/test_callable.py +48 -0
  205. assertpy-2.36.0/tests/test_capable_protocol.py +1076 -0
  206. assertpy-2.36.0/tests/test_chain_machine.py +664 -0
  207. assertpy-2.36.0/tests/test_chaining.py +22 -0
  208. assertpy-2.36.0/tests/test_check.py +351 -0
  209. assertpy-2.36.0/tests/test_check_protocols.py +230 -0
  210. assertpy-2.36.0/tests/test_class.py +145 -0
  211. assertpy-2.36.0/tests/test_class_in_spec.py +293 -0
  212. assertpy-2.36.0/tests/test_clustering.py +1587 -0
  213. assertpy-2.36.0/tests/test_collection.py +321 -0
  214. assertpy-2.36.0/tests/test_compiled_patterns.py +209 -0
  215. assertpy-2.36.0/tests/test_complexity.py +121 -0
  216. assertpy-2.36.0/tests/test_core.py +194 -0
  217. assertpy-2.36.0/tests/test_custom_dict.py +1013 -0
  218. assertpy-2.36.0/tests/test_custom_list.py +47 -0
  219. assertpy-2.36.0/tests/test_dangling.py +1196 -0
  220. assertpy-2.36.0/tests/test_dataframe.py +299 -0
  221. assertpy-2.36.0/tests/test_datetime.py +1024 -0
  222. assertpy-2.36.0/tests/test_declared_floors.py +190 -0
  223. assertpy-2.36.0/tests/test_deep_nesting.py +438 -0
  224. assertpy-2.36.0/tests/test_description.py +29 -0
  225. assertpy-2.36.0/tests/test_dict.py +422 -0
  226. assertpy-2.36.0/tests/test_dict_compare.py +1086 -0
  227. assertpy-2.36.0/tests/test_diff_laws.py +1772 -0
  228. assertpy-2.36.0/tests/test_difference_places.py +223 -0
  229. assertpy-2.36.0/tests/test_docs_examples.py +334 -0
  230. assertpy-2.36.0/tests/test_docs_typing.py +122 -0
  231. assertpy-2.36.0/tests/test_duality.py +1018 -0
  232. assertpy-2.36.0/tests/test_dynamic.py +460 -0
  233. assertpy-2.36.0/tests/test_equality_signals.py +1206 -0
  234. assertpy-2.36.0/tests/test_equals.py +487 -0
  235. assertpy-2.36.0/tests/test_errors.py +464 -0
  236. assertpy-2.36.0/tests/test_evaluation_core.py +3291 -0
  237. assertpy-2.36.0/tests/test_exact_oracle.py +343 -0
  238. assertpy-2.36.0/tests/test_exception_context.py +90 -0
  239. assertpy-2.36.0/tests/test_exception_fields.py +267 -0
  240. assertpy-2.36.0/tests/test_expected_contract.py +303 -0
  241. assertpy-2.36.0/tests/test_expected_exception.py +1305 -0
  242. assertpy-2.36.0/tests/test_expected_warning.py +288 -0
  243. assertpy-2.36.0/tests/test_extensions.py +639 -0
  244. assertpy-2.36.0/tests/test_extracting.py +494 -0
  245. assertpy-2.36.0/tests/test_fail.py +15 -0
  246. assertpy-2.36.0/tests/test_file.py +297 -0
  247. assertpy-2.36.0/tests/test_fixed_width_integers.py +717 -0
  248. assertpy-2.36.0/tests/test_grouped_soft.py +148 -0
  249. assertpy-2.36.0/tests/test_hints.py +1025 -0
  250. assertpy-2.36.0/tests/test_hostile_values.py +238 -0
  251. assertpy-2.36.0/tests/test_html_equality.py +492 -0
  252. assertpy-2.36.0/tests/test_http.py +599 -0
  253. assertpy-2.36.0/tests/test_import_cost.py +44 -0
  254. assertpy-2.36.0/tests/test_in.py +66 -0
  255. assertpy-2.36.0/tests/test_inline_record.py +1075 -0
  256. assertpy-2.36.0/tests/test_inline_snapshot.py +280 -0
  257. assertpy-2.36.0/tests/test_integer_arguments.py +113 -0
  258. assertpy-2.36.0/tests/test_iterable_cluster.py +426 -0
  259. assertpy-2.36.0/tests/test_json.py +209 -0
  260. assertpy-2.36.0/tests/test_list.py +1012 -0
  261. assertpy-2.36.0/tests/test_log_passes.py +1957 -0
  262. assertpy-2.36.0/tests/test_match_result.py +256 -0
  263. assertpy-2.36.0/tests/test_matcher_algebra.py +230 -0
  264. assertpy-2.36.0/tests/test_matcher_parity.py +210 -0
  265. assertpy-2.36.0/tests/test_matcher_registry.py +292 -0
  266. assertpy-2.36.0/tests/test_matchers.py +1630 -0
  267. assertpy-2.36.0/tests/test_membership_failures.py +270 -0
  268. assertpy-2.36.0/tests/test_membership_family.py +523 -0
  269. assertpy-2.36.0/tests/test_message_elision.py +278 -0
  270. assertpy-2.36.0/tests/test_message_values.py +345 -0
  271. assertpy-2.36.0/tests/test_missing_names.py +488 -0
  272. assertpy-2.36.0/tests/test_moment_relations.py +866 -0
  273. assertpy-2.36.0/tests/test_namedtuple.py +79 -0
  274. assertpy-2.36.0/tests/test_negated_protocols.py +422 -0
  275. assertpy-2.36.0/tests/test_none.py +27 -0
  276. assertpy-2.36.0/tests/test_not.py +421 -0
  277. assertpy-2.36.0/tests/test_number_zoo.py +491 -0
  278. assertpy-2.36.0/tests/test_numbers.py +2208 -0
  279. assertpy-2.36.0/tests/test_numeric_questions.py +322 -0
  280. assertpy-2.36.0/tests/test_one_shot_chains.py +572 -0
  281. assertpy-2.36.0/tests/test_one_shot_matchers.py +422 -0
  282. assertpy-2.36.0/tests/test_openapi_30_keywords.py +576 -0
  283. assertpy-2.36.0/tests/test_openapi_contract.py +1048 -0
  284. assertpy-2.36.0/tests/test_openapi_format_suite.py +118 -0
  285. assertpy-2.36.0/tests/test_openapi_formats.py +1007 -0
  286. assertpy-2.36.0/tests/test_openapi_multiple_of.py +322 -0
  287. assertpy-2.36.0/tests/test_openapi_oracle.py +328 -0
  288. assertpy-2.36.0/tests/test_openapi_part_read.py +441 -0
  289. assertpy-2.36.0/tests/test_operation_contract.py +404 -0
  290. assertpy-2.36.0/tests/test_optional_integration_contracts.py +86 -0
  291. assertpy-2.36.0/tests/test_overload_order.py +174 -0
  292. assertpy-2.36.0/tests/test_overloads.py +58 -0
  293. assertpy-2.36.0/tests/test_pin_coverage.py +784 -0
  294. assertpy-2.36.0/tests/test_pipeline.py +187 -0
  295. assertpy-2.36.0/tests/test_poll_parity.py +388 -0
  296. assertpy-2.36.0/tests/test_poll_protocols.py +489 -0
  297. assertpy-2.36.0/tests/test_prerequisites.py +376 -0
  298. assertpy-2.36.0/tests/test_property_attrs.py +123 -0
  299. assertpy-2.36.0/tests/test_property_based.py +3140 -0
  300. assertpy-2.36.0/tests/test_protocol_parity.py +698 -0
  301. assertpy-2.36.0/tests/test_public_surface.py +423 -0
  302. assertpy-2.36.0/tests/test_pyright_baseline.py +156 -0
  303. assertpy-2.36.0/tests/test_pytest_plugin.py +4458 -0
  304. assertpy-2.36.0/tests/test_recursive_assertion.py +227 -0
  305. assertpy-2.36.0/tests/test_recursive_compare.py +275 -0
  306. assertpy-2.36.0/tests/test_recursive_compare_attrs.py +81 -0
  307. assertpy-2.36.0/tests/test_recursive_compare_config.py +1509 -0
  308. assertpy-2.36.0/tests/test_recursive_compare_pydantic.py +466 -0
  309. assertpy-2.36.0/tests/test_refusal_frames.py +292 -0
  310. assertpy-2.36.0/tests/test_refusals.py +440 -0
  311. assertpy-2.36.0/tests/test_regex_groups.py +114 -0
  312. assertpy-2.36.0/tests/test_relative_tolerance.py +298 -0
  313. assertpy-2.36.0/tests/test_renamed.py +196 -0
  314. assertpy-2.36.0/tests/test_render_laws.py +651 -0
  315. assertpy-2.36.0/tests/test_requirement.py +673 -0
  316. assertpy-2.36.0/tests/test_rich_diff.py +2664 -0
  317. assertpy-2.36.0/tests/test_same_as.py +42 -0
  318. assertpy-2.36.0/tests/test_satisfy.py +651 -0
  319. assertpy-2.36.0/tests/test_skip_guard.py +240 -0
  320. assertpy-2.36.0/tests/test_snapshot_layout.py +295 -0
  321. assertpy-2.36.0/tests/test_snapshot_tuples.py +604 -0
  322. assertpy-2.36.0/tests/test_snapshots.py +1973 -0
  323. assertpy-2.36.0/tests/test_soft.py +645 -0
  324. assertpy-2.36.0/tests/test_soft_fail.py +61 -0
  325. assertpy-2.36.0/tests/test_soft_summary_blocks.py +356 -0
  326. assertpy-2.36.0/tests/test_strict_reach.py +466 -0
  327. assertpy-2.36.0/tests/test_string.py +747 -0
  328. assertpy-2.36.0/tests/test_structural.py +5605 -0
  329. assertpy-2.36.0/tests/test_structure_sentence.py +539 -0
  330. assertpy-2.36.0/tests/test_surface_conformance.py +488 -0
  331. assertpy-2.36.0/tests/test_sync_eventually.py +312 -0
  332. assertpy-2.36.0/tests/test_text_relations.py +404 -0
  333. assertpy-2.36.0/tests/test_traceback.py +84 -0
  334. assertpy-2.36.0/tests/test_type.py +311 -0
  335. assertpy-2.36.0/tests/test_type_expression_failures.py +148 -0
  336. assertpy-2.36.0/tests/test_typing.py +928 -0
  337. assertpy-2.36.0/tests/test_typing_chain_corpus.py +38 -0
  338. assertpy-2.36.0/tests/test_typing_claims.py +195 -0
  339. assertpy-2.36.0/tests/test_typing_completeness.py +142 -0
  340. assertpy-2.36.0/tests/test_typing_conformance.py +489 -0
  341. assertpy-2.36.0/tests/test_typing_from_a_wheel.py +97 -0
  342. assertpy-2.36.0/tests/test_typing_harness.py +79 -0
  343. assertpy-2.36.0/tests/test_typing_http.py +98 -0
  344. assertpy-2.36.0/tests/test_typing_integrations.py +110 -0
  345. assertpy-2.36.0/tests/test_typing_negative.py +208 -0
  346. assertpy-2.36.0/tests/test_typing_promises.py +76 -0
  347. assertpy-2.36.0/tests/test_unrenderable_values.py +244 -0
  348. assertpy-2.36.0/tests/test_vacuity_contract.py +686 -0
  349. assertpy-2.36.0/tests/test_verdict_corpus.py +137 -0
  350. assertpy-2.36.0/tests/test_verdicts_held_to_python.py +294 -0
  351. assertpy-2.36.0/tests/test_warn.py +158 -0
  352. assertpy-2.36.0/tests/test_warn_on_failure.py +158 -0
  353. assertpy-2.36.0/tests/typing_cases.py +705 -0
  354. assertpy-2.36.0/tests/typing_chain_corpus_baseline.json +256 -0
  355. assertpy-2.36.0/tests/typing_harness.py +166 -0
  356. assertpy-2.36.0/tests/typing_http.py +81 -0
  357. assertpy-2.36.0/tests/typing_integrations.py +65 -0
  358. assertpy-2.36.0/tests/typing_integrations_baseline.py +94 -0
  359. assertpy-2.36.0/tests/typing_negative_baseline.py +564 -0
  360. assertpy-2.36.0/tests/typing_promises.py +288 -0
  361. assertpy-2.36.0/tests/verdict_corpus.py +632 -0
  362. assertpy-2.36.0/tests/verdict_golden.txt +127 -0
  363. assertpy-1.1/PKG-INFO +0 -62
  364. assertpy-1.1/assertpy/__init__.py +0 -3
  365. assertpy-1.1/assertpy/assertpy.py +0 -433
  366. assertpy-1.1/assertpy/base.py +0 -427
  367. assertpy-1.1/assertpy/collection.py +0 -196
  368. assertpy-1.1/assertpy/contains.py +0 -362
  369. assertpy-1.1/assertpy/date.py +0 -185
  370. assertpy-1.1/assertpy/dict.py +0 -236
  371. assertpy-1.1/assertpy/dynamic.py +0 -116
  372. assertpy-1.1/assertpy/exception.py +0 -113
  373. assertpy-1.1/assertpy/extracting.py +0 -231
  374. assertpy-1.1/assertpy/file.py +0 -225
  375. assertpy-1.1/assertpy/helpers.py +0 -250
  376. assertpy-1.1/assertpy/numeric.py +0 -513
  377. assertpy-1.1/assertpy/snapshot.py +0 -208
  378. assertpy-1.1/assertpy/string.py +0 -413
  379. assertpy-1.1/setup.cfg +0 -8
  380. assertpy-1.1/setup.py +0 -68
@@ -0,0 +1,465 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ # every suite runs on four workers by file: split by test, a pair in test_snapshots.py races on the cwd __snapshots
17
+ jobs:
18
+ test:
19
+ timeout-minutes: 15
20
+ runs-on: ${{ matrix.os }}
21
+ # mapped at job level so the upload step can ask whether it has a token before it runs: the secrets
22
+ # context is not readable from a step `if`, and step-level env is not in scope for its own condition
23
+ env:
24
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
25
+ strategy:
26
+ matrix:
27
+ os: [ubuntu-latest]
28
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
29
+ include:
30
+ # the Codecov cell also installs every optional integration (pandas/polars/numpy extras plus
31
+ # pydantic/attrs/requests), so the tests the other minimal-env cells skip run here for real
32
+ - os: ubuntu-latest
33
+ python-version: "3.14"
34
+ full-deps: true
35
+ - os: windows-latest
36
+ python-version: "3.14"
37
+ - os: macos-latest
38
+ python-version: "3.14"
39
+ steps:
40
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
41
+ with:
42
+ persist-credentials: false
43
+
44
+ - name: Install uv
45
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
46
+
47
+ - name: Set up Python ${{ matrix.python-version }}
48
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
49
+ with:
50
+ python-version: ${{ matrix.python-version }}
51
+ allow-prereleases: true
52
+
53
+ - name: Install dependencies
54
+ if: '!matrix.full-deps'
55
+ run: uv sync --locked --extra json
56
+
57
+ - name: Install dependencies (all integrations)
58
+ if: matrix.full-deps
59
+ run: uv sync --locked --extra json --extra data --extra inline --group integrations --group docs-examples --group oracle
60
+
61
+ - name: Test
62
+ # no coverage: only the full-deps cell reports it, and on 3.12 its tracer made this the slowest cell of the run
63
+ if: '!matrix.full-deps'
64
+ run: uv run pytest -v -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
65
+
66
+ - name: The oracle is installed
67
+ # its module skips where a package is absent, so an absence here would pass for a green run
68
+ if: matrix.full-deps
69
+ run: uv run python -c "import openapi_schema_validator, fqdn, isoduration, rfc3986_validator"
70
+
71
+ - name: Test with coverage (enforce 100%)
72
+ # Only the full-deps cell has every integration installed and 0 skips, so it is
73
+ # the only cell that can reach 100%. This is the hard local gate; Codecov's status
74
+ # is reporting on top, not the sole guard (it uploads async and can fail open).
75
+ # The doc-example guard is excluded here so its runs never pad the coverage number.
76
+ if: matrix.full-deps
77
+ run: uv run pytest -v -n 4 --dist loadfile --cov=assertpy --cov-report=term-missing --cov-report=xml --cov-fail-under=100 --ignore=tests/test_docs_examples.py tests
78
+
79
+ - name: Guard doc examples
80
+ # Runs once (docs are platform-independent) and without coverage, so executing the
81
+ # README/guide snippets can't mask a real coverage gap in the test suite.
82
+ if: matrix.full-deps
83
+ run: uv run pytest tests/test_docs_examples.py
84
+
85
+ - name: Upload coverage to Codecov
86
+ # Only the full-deps cell. The bare 3.14/ubuntu cell skips every integration test and used
87
+ # to upload too: Codecov merges reports so the number stayed right, but one of the two was
88
+ # always a partial report sent at a 100% target.
89
+ # A run without repository secrets gets an empty token, and Codecov rejects an untokened upload
90
+ # to a protected branch, so the job went red on the upload while every test passed. The gate is
91
+ # the token rather than who opened the pull request: dependabot pushes its branches into this
92
+ # repository, so a fork test reads false for it and it still runs without secrets.
93
+ if: matrix.full-deps && env.CODECOV_TOKEN != ''
94
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
95
+ with:
96
+ token: ${{ secrets.CODECOV_TOKEN }}
97
+ files: coverage.xml
98
+ # A failed upload is a broken report, not a passing build. The 100% gate above is the hard
99
+ # guard either way, so this cannot turn a real coverage drop into a green run.
100
+ fail_ci_if_error: true
101
+
102
+ declared-floors:
103
+ # Every lower bound on a published extra is a promise, and the lockfile only ever resolves the newest
104
+ # of each, so the bottom of every range was a claim nobody checked. This job checks them, and each
105
+ # widening of it found a real defect: first the data libraries, where the refusal gate keyed on
106
+ # numpy's wording for an unusable operand and that wording is different below numpy 2, then
107
+ # `asttokens>=2.0` and `behave>=1.2.6`, which were simply wrong, then `starlette>=0.40`, whose test
108
+ # client reaches a function deprecated in 3.14.
109
+ #
110
+ # Scope, said exactly: every extra in `[project.optional-dependencies]`, plus the `dev` and
111
+ # `integrations` groups, which is what running the suite needs. The `docs`, `typecheck`, `mutation`,
112
+ # `docs-examples` and `benchmark` groups are not here: each drives a tool with its own command rather
113
+ # than this suite, and none of them is a promise to anyone installing the package.
114
+ #
115
+ # Its own job rather than a cell in the matrix above. An `include` entry naming an `os` and a
116
+ # `python-version` that already exist is merged into that combination instead of adding one, so a
117
+ # cell written that way would have replaced the ordinary runs rather than joining them.
118
+ timeout-minutes: 15
119
+ strategy:
120
+ fail-fast: false
121
+ matrix:
122
+ include:
123
+ - python-version: "3.10"
124
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
125
+ groups: "--group dev --group integrations"
126
+ imports: "typing_extensions, numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
127
+ - python-version: "3.11"
128
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
129
+ groups: "--group dev --group integrations"
130
+ imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
131
+ - python-version: "3.12"
132
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
133
+ groups: "--group dev --group integrations"
134
+ imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
135
+ - python-version: "3.13"
136
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
137
+ groups: "--group dev --group integrations"
138
+ imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
139
+ - python-version: "3.14"
140
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
141
+ groups: "--group dev --group integrations"
142
+ imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
143
+ # The one dependency every user gets, at its own floor, and it needs a cell to itself: pydantic
144
+ # 2.0 requires typing-extensions>=4.6.1, so every cell installing `integrations` resolves it
145
+ # above the declared bound. Measured 4.0.0 here against 4.6.1 in each of those.
146
+ - python-version: "3.10"
147
+ extras: "allure,behave,data,inline,json,numpy,pandas,polars"
148
+ groups: "--group dev"
149
+ imports: "typing_extensions, numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave"
150
+ reduced: "no integrations, so pydantic cannot lift typing-extensions off its declared floor"
151
+ # 3.15 carries what is installable and green there. No combination of the data libraries
152
+ # publishes a wheel for it yet. The `integrations` group installs since pydantic 2.14, and stays
153
+ # out for one test: this cell resolves anyio 4.15.1 on 3.15 where the cells below resolve
154
+ # 4.14.2, and starlette at its floor 0.46.0 imports a name that anyio deprecates, which the
155
+ # suite's `-W error` refuses. Move this cell up to the others when both are settled.
156
+ - python-version: "3.15"
157
+ extras: "allure,behave,inline,json"
158
+ groups: "--group dev"
159
+ imports: "asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave"
160
+ reduced: "no data-library wheel for 3.15 yet, and starlette at its floor warns under the anyio resolved here"
161
+ runs-on: ubuntu-latest
162
+ steps:
163
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
164
+ with:
165
+ persist-credentials: false
166
+
167
+ - name: Install uv
168
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
169
+
170
+ - name: Set up Python ${{ matrix.python-version }}
171
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
172
+ with:
173
+ python-version: ${{ matrix.python-version }}
174
+ allow-prereleases: true
175
+
176
+ - name: Install every declared dependency at its lower bound
177
+ # `lowest-direct` resolves every direct dependency to the oldest its specifier allows, which is
178
+ # what the metadata promises. Hand-pinning `numpy==1.26.0` did the same for three of them and
179
+ # covered none of the rest.
180
+ #
181
+ # `--only-binary` because the oldest allowed release is not installable everywhere: pandas 2.0.0
182
+ # publishes no wheel above 3.11 and building it fails on a missing `pkg_resources`. With it the
183
+ # resolver lands on the oldest release that has a wheel for this interpreter, which is the oldest
184
+ # a user of it can actually get. Measured: pandas 2.0.0 on 3.10 and 3.11, 2.1.1 on 3.12, 2.2.3 on
185
+ # 3.13, 2.3.3 on 3.14.
186
+ #
187
+ # `uv pip install` and not `uv sync`, which was measured rewriting `uv.lock` wholesale: the tests
188
+ # that shell out then read a lockfile that is not the committed one, and six of them failed on
189
+ # exactly that.
190
+ #
191
+ # Every extra by name, `data` and the three it aggregates alike, because
192
+ # `tests/test_declared_floors.py` reads these lines and asks for each of them.
193
+ run: |
194
+ uv venv --python ${{ matrix.python-version }}
195
+ uv pip install --only-binary=:all: --resolution lowest-direct -e ".[${{ matrix.extras }}]" ${{ matrix.groups }}
196
+
197
+ - name: Every installed floor must import
198
+ # A release old enough to install and too old to import would otherwise leave the cell green:
199
+ # its tests answer `pytest.importorskip` with a skip, and a skip carrying its own reason is
200
+ # invisible to the missing-module guard in `tests/conftest.py`.
201
+ #
202
+ # The `integrations` group is in the list too, not only the published extras: it is installed by
203
+ # the same command and its tests skip the same way. The `dev` group is not, because nothing in it
204
+ # skips: the suite imports pytest and hypothesis to run at all, and the gates that shell out to
205
+ # ruff and ty refuse rather than skip when the tool does not answer.
206
+ run: uv run --no-sync python -c "import ${{ matrix.imports }}"
207
+
208
+ - name: What this cell resolved
209
+ # Just what is installed and at which version. Which of those are declared floors, and whether
210
+ # each was reached, is decided once in `floors-reached` below, where `tomllib` is available and
211
+ # a requirement carrying an environment marker parses rather than being mangled by a regex.
212
+ run: |
213
+ uv run --no-sync python - <<'PROBE'
214
+ import importlib.metadata as md
215
+ import pathlib
216
+
217
+ with pathlib.Path("resolved.txt").open("w", encoding="utf-8") as verdict:
218
+ for distribution in sorted(md.distributions(), key=lambda item: item.metadata["Name"] or ""):
219
+ name = distribution.metadata["Name"]
220
+ if name:
221
+ print(f"{name} {distribution.version}", file=verdict)
222
+ print(pathlib.Path("resolved.txt").read_text(encoding="utf-8"))
223
+ PROBE
224
+
225
+ - name: Keep this cell's resolution for the aggregate
226
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
227
+ with:
228
+ name: floors-resolved-${{ matrix.python-version }}-${{ strategy.job-index }}
229
+ path: resolved.txt
230
+ retention-days: 1
231
+
232
+ - name: Suite at the floor
233
+ # `--no-sync` is load bearing here and nowhere else: a bare `uv run` restores the environment
234
+ # from the lockfile, which would put the newest of everything back and leave this job testing
235
+ # what the matrix above already tests
236
+ run: uv run --no-sync pytest -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
237
+
238
+ floors-reached:
239
+ # The cross-cell half of the floor job, and it has to be cross-cell: `typing-extensions` reaches its
240
+ # floor only where pydantic is absent, and `attrs` only where it is installed by name rather than
241
+ # arriving through jsonschema. No cell reaches everything, and between them they do.
242
+ #
243
+ # Every declaration is read here rather than in the cells, with `tomllib` rather than a regex: a
244
+ # requirement carrying an environment marker holds quotes of its own, and reading it by pattern
245
+ # silently truncated `typing-extensions>=4.0; python_version < '3.11'` to nothing.
246
+ timeout-minutes: 5
247
+ needs: declared-floors
248
+ runs-on: ubuntu-latest
249
+ steps:
250
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
251
+ with:
252
+ persist-credentials: false
253
+
254
+ - name: Install uv
255
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
256
+
257
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
258
+ with:
259
+ pattern: floors-resolved-*
260
+ path: resolutions
261
+
262
+ - name: Every declared floor is reached somewhere
263
+ run: |
264
+ uv run --no-project --with packaging python - <<'AGGREGATE'
265
+ import pathlib
266
+ import tomllib
267
+ from packaging.requirements import Requirement
268
+ from packaging.version import Version
269
+
270
+ # keyed by name and floor, because two extras may declare two different bounds on one
271
+ # distribution and reaching either says nothing about the other
272
+ UNREACHED: dict[tuple[str, str], str] = {}
273
+
274
+ declared = {}
275
+ project = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
276
+ sources = [project["project"]["dependencies"]]
277
+ sources += list(project["project"]["optional-dependencies"].values())
278
+ sources += [project["dependency-groups"][name] for name in ("dev", "integrations")]
279
+ for group in sources:
280
+ for line in group:
281
+ requirement = Requirement(line)
282
+ floors = [spec.version for spec in requirement.specifier if spec.operator == ">="]
283
+ if floors and requirement.name != "assertpy":
284
+ declared.setdefault(requirement.name, set()).add(floors[0])
285
+
286
+ installed = {}
287
+ files = sorted(pathlib.Path("resolutions").rglob("resolved.txt"))
288
+ if not files:
289
+ raise SystemExit("no cell reported a resolution, so this job proves nothing")
290
+ for path in files:
291
+ for row in path.read_text(encoding="utf-8").splitlines():
292
+ name, version = row.rsplit(" ", 1)
293
+ installed.setdefault(name.lower().replace("_", "-"), set()).add(version)
294
+
295
+ never, stale = [], []
296
+ for name, floors in sorted(declared.items()):
297
+ key = name.lower().replace("_", "-")
298
+ for floor in sorted(floors, key=Version):
299
+ reached = any(Version(seen) == Version(floor) for seen in installed.get(key, ()))
300
+ excused = (key, floor) in UNREACHED
301
+ print(f"{name:18} floor {floor:10} {'reached' if reached else 'NEVER REACHED'}")
302
+ if not reached and not excused:
303
+ never.append(f"{name}>={floor}")
304
+ if reached and excused:
305
+ stale.append(f"{name}>={floor}")
306
+ if never:
307
+ raise SystemExit(f"declared floors no cell installs: {', '.join(never)}")
308
+ if stale:
309
+ raise SystemExit(f"recorded as unreachable and reached anyway, so the note is stale: {', '.join(stale)}")
310
+ AGGREGATE
311
+
312
+ optional-integrations:
313
+ # allure and behave get their own job because installing them is incompatible with the coverage
314
+ # gate: with allure present the `except ImportError` fallback in pytest_plugin.py never runs, and
315
+ # without it the import itself never runs. The gating cell keeps the second state, this one holds
316
+ # the first. Everywhere else the two are tested against mocks, which accept any call and would
317
+ # wave through the renamed parameter or moved attribute a Dependabot bump can bring.
318
+ timeout-minutes: 10
319
+ runs-on: ubuntu-latest
320
+ steps:
321
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
322
+ with:
323
+ persist-credentials: false
324
+
325
+ - name: Install uv
326
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
327
+
328
+ - name: Set up Python
329
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
330
+ with:
331
+ python-version: "3.14"
332
+
333
+ - name: Install dependencies
334
+ run: uv sync --locked --extra json --extra allure --extra behave
335
+
336
+ - name: Contract tests against the real libraries
337
+ run: uv run pytest -v tests/test_optional_integration_contracts.py
338
+
339
+ - name: The rest of the suite must also pass with both installed
340
+ # Installing allure flips `_HAS_ALLURE`, so every failing assertion in the suite takes the
341
+ # real attach path. That must not change any other test's outcome.
342
+ run: uv run pytest -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
343
+
344
+ lint:
345
+ timeout-minutes: 10
346
+ runs-on: ubuntu-latest
347
+ steps:
348
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
349
+ with:
350
+ persist-credentials: false
351
+
352
+ - name: Install uv
353
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
354
+
355
+ - name: Set up Python
356
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
357
+ with:
358
+ python-version: "3.14"
359
+
360
+ - name: Install dependencies
361
+ # the whole `data` stack, because the overloads narrow on *structure* and the structures are
362
+ # pandas', polars' and numpy's to change. A stub release that stops declaring the member a
363
+ # shape keys on moves those values back to the generic builder, silently: it type-checks, and
364
+ # the narrowed methods just stop being offered. The integration matrix below is what notices
365
+ run: uv sync --locked --extra json --extra data --extra inline --group typecheck --group integrations
366
+
367
+ - name: Ruff check
368
+ run: uv run ruff check .
369
+
370
+ - name: Ruff format check
371
+ run: uv run ruff format --check .
372
+
373
+ - name: Type check (ty)
374
+ run: uv run ty check assertpy/ tests/test_typing.py
375
+
376
+ - name: Type check (mypy --strict, public typing surface)
377
+ run: uv run mypy --strict --follow-imports=silent tests/test_typing.py
378
+
379
+ # the engine, not the launcher: `pyright` on PyPI and the npm engine it launches are released apart
380
+ # and have drifted before, and every recorded number here was recorded against one build. The
381
+ # gates that call pyright from Python pin the same one, in `tests/typing_harness.py`
382
+ - name: Type check (pyright, public typing surface)
383
+ env:
384
+ PYRIGHT_PYTHON_FORCE_VERSION: "1.1.414"
385
+ run: uv run pyright --pythonversion 3.14 tests/test_typing.py
386
+
387
+ # a fourth overload-resolution engine on the same file, which is what the badge names. Its
388
+ # settings are in `pyrefly.toml`, and the preset there is deliberate: the fallback one answers
389
+ # green while looking at much less
390
+ - name: Type check (pyrefly, public typing surface)
391
+ run: uv run pyrefly check tests/test_typing.py
392
+
393
+ # the fourth checker over the promises too: the harness reads three, and both defects the file
394
+ # was written for were accepted by all four
395
+ - name: Type check (pyrefly, the object matches the promise)
396
+ run: uv run pyrefly check tests/typing_promises.py
397
+
398
+ # the gates that need the checkers this job installs: each file's docstring says what it holds
399
+ - name: Type check (pytest gates, all four checkers)
400
+ run: >
401
+ uv run pytest -q -n 4 --dist loadfile
402
+ tests/test_pyright_baseline.py
403
+ tests/test_typing_completeness.py
404
+ tests/test_typing_promises.py
405
+ tests/test_typing_negative.py
406
+ tests/test_typing_chain_corpus.py
407
+ tests/test_typing_integrations.py
408
+ tests/test_typing_http.py
409
+ tests/test_typing_from_a_wheel.py
410
+ tests/test_check_protocols.py
411
+ tests/test_operation_contract.py
412
+ tests/test_typing_conformance.py
413
+
414
+ sdist:
415
+ # the suite as a distribution runs it, from the sdist: `scripts/check_sdist.py` says how and why
416
+ timeout-minutes: 20
417
+ runs-on: ubuntu-latest
418
+ strategy:
419
+ fail-fast: false
420
+ matrix:
421
+ include:
422
+ - mode: packager
423
+ options: '--export="--extra json"'
424
+ # the lint job's set, for the type-checker gates
425
+ - mode: developer
426
+ options: '--developer --export="--extra json --extra data --extra inline --group typecheck --group integrations"'
427
+ steps:
428
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
429
+ with:
430
+ persist-credentials: false
431
+
432
+ - name: Install uv
433
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
434
+
435
+ - name: Set up Python
436
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
437
+ with:
438
+ python-version: "3.14"
439
+
440
+ - name: The suite from the sdist and from the checkout (${{ matrix.mode }})
441
+ run: >
442
+ python scripts/check_sdist.py ${{ matrix.options }} --work "$RUNNER_TEMP/check_sdist"
443
+ -- -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
444
+
445
+ ci-ok:
446
+ timeout-minutes: 5
447
+ needs: [test, declared-floors, floors-reached, optional-integrations, lint, sdist]
448
+ if: always()
449
+ runs-on: ubuntu-latest
450
+ steps:
451
+ - name: Verify required jobs succeeded
452
+ env:
453
+ TEST_RESULT: ${{ needs.test.result }}
454
+ FLOOR_RESULT: ${{ needs.declared-floors.result }}
455
+ REACHED_RESULT: ${{ needs.floors-reached.result }}
456
+ INTEGRATIONS_RESULT: ${{ needs.optional-integrations.result }}
457
+ LINT_RESULT: ${{ needs.lint.result }}
458
+ SDIST_RESULT: ${{ needs.sdist.result }}
459
+ run: |
460
+ [ "$TEST_RESULT" = "success" ] &&
461
+ [ "$FLOOR_RESULT" = "success" ] &&
462
+ [ "$REACHED_RESULT" = "success" ] &&
463
+ [ "$INTEGRATIONS_RESULT" = "success" ] &&
464
+ [ "$LINT_RESULT" = "success" ] &&
465
+ [ "$SDIST_RESULT" = "success" ]
@@ -0,0 +1,34 @@
1
+ # .gitignore
2
+ *.py[cod]
3
+ .cache
4
+ .pytest_cache
5
+ __pycache__
6
+
7
+ # testing output
8
+ *.xml
9
+ *.log
10
+ cover
11
+ .coverage
12
+ htmlcov
13
+ .hypothesis
14
+ __snapshots
15
+ mycustompath
16
+
17
+ # packaging
18
+ MANIFEST
19
+ dist
20
+
21
+ # mac
22
+ .DS_Store
23
+
24
+ # venv
25
+ .venv
26
+
27
+ # IDE
28
+ /.idea
29
+ .vscode
30
+
31
+ # docs output
32
+ docs/build
33
+ docs/out
34
+ /site/
@@ -0,0 +1,167 @@
1
+ # The typed surface
2
+
3
+ `assert_that()` hands back a different set of methods depending on what you gave it, and four checkers have to
4
+ agree on which set. Read this before changing a public signature. None of it is discoverable from the files.
5
+
6
+ ## Two halves
7
+
8
+ The runtime is a stack of mixins. `AssertionBuilder` inherits `StringMixin`, `NumericMixin`, `DictMixin` and a
9
+ dozen more, so every assertion exists on every value and refuses what it cannot handle.
10
+
11
+ The typed surface is Protocols in `assertpy/_engine/_typing.py`, one per kind of value, picked by an overload
12
+ ladder on `assert_that` in `assertpy/assertpy.py`. A checker sees only the protocol, so
13
+ `assert_that({}).is_positive()` is a type error and `assert_that(1).is_positive()` is not.
14
+
15
+ Neither half knows about the other. Everything below keeps them from drifting apart.
16
+
17
+ Most of it is not yours to edit:
18
+
19
+ | file | protocols | declarations |
20
+ |---|---|---|
21
+ | `_engine/_typing.py` | 43 | 415 |
22
+ | `_engine/_check_typing.py` | 34 | 326 |
23
+ | `_engine/_builder_check_typing.py` | 1 | 320 |
24
+ | `_engine/_capable_typing.py` | 10 | 325 |
25
+ | `_engine/_poll_typing.py` | 22 | 1505 |
26
+ | `_engine/_negated_typing.py` | 23 | 1053 |
27
+
28
+ One is written by hand. `tests/test_architecture_doc.py` recomputes this table. No line counts, which
29
+ move on every edit to a generated file.
30
+
31
+ ## What is generated
32
+
33
+ | file | written by | built from |
34
+ |---|---|---|
35
+ | `_engine/_typing.py` | hand | the protocols themselves |
36
+ | `assertpy.py` overload ladder | hand | which protocol each subject resolves to |
37
+ | `_engine/_poll_typing.py` | `scripts/generate_poll_protocols.py` | `_typing.py`, `assertpy.py`, `_engine/_operations.py` |
38
+ | `_engine/_builder_check_typing.py` | `scripts/generate_poll_protocols.py` | the same three |
39
+ | `_engine/_capable_typing.py` | `scripts/generate_poll_protocols.py` | the **runtime mixins** under `assertpy/*.py`, `_engine/_operations.py` |
40
+ | `_engine/_negated_typing.py` | `scripts/generate_poll_protocols.py` | `_typing.py` and `_engine/_operations.py` |
41
+ | `_engine/_check_typing.py` | `scripts/generate_check_protocols.py` | `_typing.py`, `_engine/_operations.py` |
42
+
43
+ One script writes four of them. That is the part people miss.
44
+
45
+ - **polling twins** answer `eventually()` and `eventually_sync()`, so every assertion hands back the chain.
46
+ - **verdict twins** answer `check()`, returning an `AssertionOutcome` instead of `Self`.
47
+ - **the capability façade** stands in for the builder itself, so it reads the mixins rather than the views:
48
+ reading the views would narrow it to what one value type answers. It skips its own hand-written list, and
49
+ takes which mixin methods pivot from `_engine/_operations.py`, since the mixins write those `-> Self` too.
50
+ - **negation twins** answer `.not_`, where one assertion hands back the view the chain had before it.
51
+
52
+ `_engine/_operations.py` decides what the first two leave out: `NOT_AN_OPERATION`, `WITHOUT_A_VERDICT`.
53
+
54
+ ## Changing a signature
55
+
56
+ ```bash
57
+ uv run python scripts/regenerate.py
58
+ ```
59
+
60
+ That runs both generators, re-records `tests/api_snapshot.json`, which pins the public surface, and
61
+ re-records the table of sizes above. Read the diff it leaves.
62
+
63
+ **The generators carry hard-coded import lines.** A signature naming something the generated file did not
64
+ already import produces a file that will not compile, and ruff reports `F821 Undefined name` against a file you
65
+ never edited. Add the name to the import template in the generator.
66
+
67
+ ## Adding a type of value
68
+
69
+ The runtime comes first, because the rest is generated or recorded from it.
70
+
71
+ 1. Write the mixin, compose it into `AssertionBuilder`'s bases.
72
+ 2. Add it to `_COVERAGE` in `tests/test_protocol_parity.py`, which has its own gate.
73
+ 3. Declare the protocol in `_engine/_typing.py`.
74
+ 4. Add the arm to the `assert_that` ladder in `assertpy.py`.
75
+ 5. Pin it with `assert_type` in `tests/test_typing.py`, the file the README badge is about.
76
+ 6. Regenerate.
77
+ 7. Re-record the snapshot, last.
78
+
79
+ Regenerating before step 1 produces a stale façade. Recording the snapshot before the mixin is composed makes
80
+ its gate red again.
81
+
82
+ ## Adding an assertion to a type that exists
83
+
84
+ The skipped step is the third.
85
+
86
+ 1. Write the method on its mixin.
87
+ 2. Declare it on the protocols it applies to in `_engine/_typing.py`, and on those only. A declaration
88
+ on `_CoreAssertion` offers it to every value, which is the thing the typed surface exists to avoid.
89
+ 3. Register it in `_engine/_operations.py` unless it is a plain assertion. No verdict goes in
90
+ `WITHOUT_A_VERDICT` under what it does instead, since `check()` and `not_` mean nothing on it and
91
+ both used to accept them. A pivot that also asserts goes in `ALSO_ASSERTS`, since reaching
92
+ `self.error()` does not separate a verdict from a precondition.
93
+ 4. If the polling twin is meant to leave it out, add the name to `_AFTER_A_CALL` in
94
+ `tests/test_poll_protocols.py`. The whole caught-exception family is absent from that twin, and the
95
+ list is what says so on purpose rather than by accident.
96
+ 5. Regenerate. The generators emit what they find, so a missing declaration surfaces not here but in
97
+ `test_protocol_parity.py::test_mixin_methods_are_declared_on_its_protocols`.
98
+
99
+ A signature naming a type the generated files do not import produces a file that does not compile. The
100
+ import lines are templates inside the generators.
101
+
102
+ ## The gates
103
+
104
+ | gate | the question it answers |
105
+ |---|---|
106
+ | `test_typing.py` | does each subject resolve to the protocol it should, under all four checkers |
107
+ | `test_pin_coverage.py` | does every declaration that changes the type have a pin above |
108
+ | `test_protocol_parity.py` | does every declared method exist on the runtime builder |
109
+ | `test_typing_conformance.py` | do the declaration and the runtime agree parameter by parameter |
110
+ | `test_poll_protocols.py` | are the polling twins what the generator produces today |
111
+ | `test_check_protocols.py` | are the verdict twins |
112
+ | `test_capable_protocol.py` | is the façade |
113
+ | `test_api_compatibility.py` | has the public surface moved without the snapshot being re-recorded |
114
+ | `test_typing_completeness.py` | can a checker name a type for every exported symbol |
115
+ | `test_pyright_baseline.py` | has a new pyright diagnostic appeared in the package |
116
+ | `test_typing_promises.py` | is the object at the end of a chain the type the checker was promised |
117
+ | `test_negated_protocols.py` | does every reachable view have a negation twin that hands the view back, and is the file what the generator produces |
118
+ | `test_architecture_doc.py` | does this document still describe the tree, counts included |
119
+ | `test_typing_negative.py` | do the checkers still refuse what they should |
120
+ | `test_typing_chain_corpus.py` | do the chains the chain model takes, to two pivots, still type the way they were recorded |
121
+ | `test_typing_from_a_wheel.py` | does the typed surface survive packaging |
122
+ | `test_typing_integrations.py`, `test_typing_http.py` | do real pandas, polars, numpy and HTTP values still resolve |
123
+ | `test_typing_harness.py` | is pyright's report read off its output alone, and refused where that is not one report |
124
+ | `test_overload_order.py` | is the frame overload still above every other shape-keyed one |
125
+ | `test_operation_contract.py` | is the register of operations that reach no verdict still the true one |
126
+ | `test_typing_claims.py` | are the four checkers still run with zero suppressions, as the badge says |
127
+ | `test_docs_typing.py` | do the snippets in the README and the guides type-check |
128
+ | `test_public_surface.py` | does `import assertpy` still give what it gave |
129
+
130
+ Which one goes red tells you what you did:
131
+
132
+ - a declaration the runtime does not have: `test_protocol_parity.py`
133
+ - a declaration whose parameters the runtime does not match: `test_typing_conformance.py`
134
+ - a generated file edited by hand: that file's own gate, one of the four
135
+ - a protocol changed without regenerating: the polling, verdict and negation gates, never the facade one,
136
+ which reads the mixins instead
137
+ - a subject resolving to the wrong protocol: `test_typing.py`, and only under whichever checker sees it
138
+ - a public name added or moved: `test_api_compatibility.py`, which is a snapshot rather than a rule
139
+
140
+ The four checkers are ty, mypy `--strict`, Pyright and Pyrefly. Where they disagree it is recorded, in
141
+ `tests/typing_negative_baseline.py`, `tests/typing_integrations_baseline.py` and
142
+ `tests/typing_chain_corpus_baseline.json`.
143
+
144
+ Pyright's engine is pinned in `tests/typing_harness.py`. The PyPI distribution is a launcher for a node package
145
+ and the two move at different speeds.
146
+
147
+ ## True, though it looks wrong
148
+
149
+ - **`assert_that`'s implementation returns `Any`.** The real return made Pyright allocate four gigabytes against
150
+ a protocol carrying the whole surface and die. The overloads are what callers see.
151
+ - **Some overlapping overloads are deliberate**, recorded per method in `tests/pyright_baseline.py`.
152
+ - **Some variance suggestions are refused**, recorded there too, per TypeVar.
153
+ - **`ty` is scoped to `assertpy/` and `tests/test_typing.py`.** Over the whole tree it reports hundreds of
154
+ diagnostics from `tests/`, which is full of calls that must not type-check.
155
+
156
+ ## Where the two halves may differ
157
+
158
+ **Which methods are offered.** At run time every assertion exists on every value; a typed view offers only the
159
+ ones that apply. That gap is the point of the typed surface.
160
+
161
+ **What an offered method accepts.** Here they must agree exactly, and both directions have shipped as defects:
162
+
163
+ - the type **refuses** what the runtime accepts, so working code is rejected
164
+ - the type **accepts** what the runtime refuses, so a checker approves a call that raises
165
+
166
+ The runtime is the fact and the declaration follows it. `ClassInfo` in `_matcher_impls.py` is the worked
167
+ example: recursive because `isinstance` accepts tuples nested to any depth.