xy 0.0.2__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 (481) hide show
  1. xy-0.0.2/.github/workflows/ci.yml +796 -0
  2. xy-0.0.2/.github/workflows/codspeed.yml +76 -0
  3. xy-0.0.2/.github/workflows/release.yml +357 -0
  4. xy-0.0.2/.gitignore +17 -0
  5. xy-0.0.2/CHANGELOG.md +570 -0
  6. xy-0.0.2/CONTRIBUTING.md +31 -0
  7. xy-0.0.2/Cargo.lock +85 -0
  8. xy-0.0.2/Cargo.toml +21 -0
  9. xy-0.0.2/LICENSE +202 -0
  10. xy-0.0.2/Makefile +169 -0
  11. xy-0.0.2/PKG-INFO +239 -0
  12. xy-0.0.2/README.md +212 -0
  13. xy-0.0.2/SECURITY.md +36 -0
  14. xy-0.0.2/benchmarks/README.md +221 -0
  15. xy-0.0.2/benchmarks/__init__.py +1 -0
  16. xy-0.0.2/benchmarks/_browser.py +180 -0
  17. xy-0.0.2/benchmarks/_cdp.py +326 -0
  18. xy-0.0.2/benchmarks/_launch_interactive.py +559 -0
  19. xy-0.0.2/benchmarks/_launch_static.py +271 -0
  20. xy-0.0.2/benchmarks/_launch_webagg.py +402 -0
  21. xy-0.0.2/benchmarks/_xy_browser.py +176 -0
  22. xy-0.0.2/benchmarks/baseline.json +52 -0
  23. xy-0.0.2/benchmarks/bench.py +94 -0
  24. xy-0.0.2/benchmarks/bench_2d_charts.py +898 -0
  25. xy-0.0.2/benchmarks/bench_animation.py +158 -0
  26. xy-0.0.2/benchmarks/bench_dashboard.py +503 -0
  27. xy-0.0.2/benchmarks/bench_heatmap_native.py +167 -0
  28. xy-0.0.2/benchmarks/bench_install.py +291 -0
  29. xy-0.0.2/benchmarks/bench_interaction.py +1045 -0
  30. xy-0.0.2/benchmarks/bench_launch_scatter.py +190 -0
  31. xy-0.0.2/benchmarks/bench_line.py +290 -0
  32. xy-0.0.2/benchmarks/bench_native.py +233 -0
  33. xy-0.0.2/benchmarks/bench_native_vs_agg.py +230 -0
  34. xy-0.0.2/benchmarks/bench_pyplot_vs_matplotlib.py +587 -0
  35. xy-0.0.2/benchmarks/bench_raw_scatter.py +233 -0
  36. xy-0.0.2/benchmarks/bench_scatter_native.py +521 -0
  37. xy-0.0.2/benchmarks/bench_transport.py +541 -0
  38. xy-0.0.2/benchmarks/bench_vs.py +952 -0
  39. xy-0.0.2/benchmarks/bench_workflows.py +413 -0
  40. xy-0.0.2/benchmarks/categories.py +158 -0
  41. xy-0.0.2/benchmarks/environment.py +147 -0
  42. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/cpu-fallback-results.json +1162 -0
  43. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/default-results.json +2021 -0
  44. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/environment.json +24 -0
  45. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/manifest.json +48 -0
  46. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/pyproject.toml +19 -0
  47. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/report.md +256 -0
  48. xy-0.0.2/benchmarks/launch_baselines/xy-0.1.0/macos-arm64-m5-pro/uv.lock +772 -0
  49. xy-0.0.2/benchmarks/requirements-ci.in +20 -0
  50. xy-0.0.2/benchmarks/requirements-ci.lock +315 -0
  51. xy-0.0.2/benchmarks/test_codspeed_animation.py +95 -0
  52. xy-0.0.2/benchmarks/test_codspeed_kernels.py +1238 -0
  53. xy-0.0.2/benchmarks/test_codspeed_pyplot.py +395 -0
  54. xy-0.0.2/benchmarks/test_codspeed_selection.py +147 -0
  55. xy-0.0.2/benchmarks/test_codspeed_transport.py +100 -0
  56. xy-0.0.2/docs/advanced/index.md +139 -0
  57. xy-0.0.2/docs/advanced/runtime-and-deployment.md +134 -0
  58. xy-0.0.2/docs/api-reference/changelog.md +43 -0
  59. xy-0.0.2/docs/api-reference/chart-factories.md +54 -0
  60. xy-0.0.2/docs/api-reference/contributing.md +73 -0
  61. xy-0.0.2/docs/api-reference/events-and-callbacks.md +81 -0
  62. xy-0.0.2/docs/api-reference/figure-methods.md +132 -0
  63. xy-0.0.2/docs/api-reference/index.md +31 -0
  64. xy-0.0.2/docs/api-reference/limitations-and-alpha-status.md +92 -0
  65. xy-0.0.2/docs/api-reference/marks-and-components.md +57 -0
  66. xy-0.0.2/docs/api-reference/public-types.md +68 -0
  67. xy-0.0.2/docs/charts/area-chart.md +195 -0
  68. xy-0.0.2/docs/charts/bar-chart.md +197 -0
  69. xy-0.0.2/docs/charts/box-plot.md +189 -0
  70. xy-0.0.2/docs/charts/contour-plot.md +186 -0
  71. xy-0.0.2/docs/charts/ecdf.md +172 -0
  72. xy-0.0.2/docs/charts/heatmap.md +172 -0
  73. xy-0.0.2/docs/charts/hexbin.md +180 -0
  74. xy-0.0.2/docs/charts/histogram.md +178 -0
  75. xy-0.0.2/docs/charts/line-chart.md +160 -0
  76. xy-0.0.2/docs/charts/scatter.md +195 -0
  77. xy-0.0.2/docs/charts/segments.md +179 -0
  78. xy-0.0.2/docs/charts/stem-plot.md +169 -0
  79. xy-0.0.2/docs/charts/uncertainty.md +185 -0
  80. xy-0.0.2/docs/charts/violin-plot.md +191 -0
  81. xy-0.0.2/docs/components/annotations.md +257 -0
  82. xy-0.0.2/docs/components/axes.md +237 -0
  83. xy-0.0.2/docs/components/colorbars.md +245 -0
  84. xy-0.0.2/docs/components/facets-and-layers.md +165 -0
  85. xy-0.0.2/docs/components/index.md +41 -0
  86. xy-0.0.2/docs/components/legends.md +178 -0
  87. xy-0.0.2/docs/components/marks.md +258 -0
  88. xy-0.0.2/docs/components/modebars-and-interaction-controls.md +312 -0
  89. xy-0.0.2/docs/components/tooltips.md +179 -0
  90. xy-0.0.2/docs/components/triangle-mesh.md +232 -0
  91. xy-0.0.2/docs/core-concepts/axes-and-scales.md +149 -0
  92. xy-0.0.2/docs/core-concepts/configuration.md +162 -0
  93. xy-0.0.2/docs/core-concepts/data.md +302 -0
  94. xy-0.0.2/docs/core-concepts/index.md +203 -0
  95. xy-0.0.2/docs/core-concepts/interactions.md +279 -0
  96. xy-0.0.2/docs/core-concepts/large-data-and-performance.md +107 -0
  97. xy-0.0.2/docs/guides/dashboards-and-linked-views.md +65 -0
  98. xy-0.0.2/docs/guides/dataframes-and-real-data.md +169 -0
  99. xy-0.0.2/docs/guides/deployment-recipes.md +177 -0
  100. xy-0.0.2/docs/guides/display-and-export.md +201 -0
  101. xy-0.0.2/docs/guides/getting-help.md +135 -0
  102. xy-0.0.2/docs/guides/index.md +23 -0
  103. xy-0.0.2/docs/guides/real-time-and-streaming-data.md +72 -0
  104. xy-0.0.2/docs/guides/serving-csp-and-offline-use.md +66 -0
  105. xy-0.0.2/docs/guides/troubleshooting.md +108 -0
  106. xy-0.0.2/docs/index.md +91 -0
  107. xy-0.0.2/docs/integrations/index.md +20 -0
  108. xy-0.0.2/docs/integrations/matplotlib.md +66 -0
  109. xy-0.0.2/docs/integrations/notebooks.md +86 -0
  110. xy-0.0.2/docs/integrations/reflex.md +236 -0
  111. xy-0.0.2/docs/overview/benchmarks.md +156 -0
  112. xy-0.0.2/docs/overview/first-chart.md +164 -0
  113. xy-0.0.2/docs/overview/gallery.md +48 -0
  114. xy-0.0.2/docs/overview/installation.md +121 -0
  115. xy-0.0.2/docs/styling/animations.md +720 -0
  116. xy-0.0.2/docs/styling/chrome-slots.md +261 -0
  117. xy-0.0.2/docs/styling/component-variations.md +623 -0
  118. xy-0.0.2/docs/styling/customize.md +782 -0
  119. xy-0.0.2/docs/styling/examples.md +837 -0
  120. xy-0.0.2/docs/styling/gallery.md +719 -0
  121. xy-0.0.2/docs/styling/index.md +271 -0
  122. xy-0.0.2/docs/styling/mark-styles.md +239 -0
  123. xy-0.0.2/docs/styling/playground.md +16 -0
  124. xy-0.0.2/docs/styling/recipes.md +520 -0
  125. xy-0.0.2/docs/styling/themes-and-tokens.md +551 -0
  126. xy-0.0.2/examples/bad_apple/README.md +92 -0
  127. xy-0.0.2/examples/fastapi/README.md +55 -0
  128. xy-0.0.2/examples/fastapi/app.py +281 -0
  129. xy-0.0.2/examples/fastapi/charts.py +601 -0
  130. xy-0.0.2/examples/fastapi/live_drilldown.py +339 -0
  131. xy-0.0.2/examples/fastapi/pyproject.toml +16 -0
  132. xy-0.0.2/examples/osm/README.md +99 -0
  133. xy-0.0.2/examples/osm/osmium-rs/Cargo.lock +105 -0
  134. xy-0.0.2/examples/osm/osmium-rs/Cargo.toml +30 -0
  135. xy-0.0.2/examples/osm/osmium-rs/README.md +66 -0
  136. xy-0.0.2/examples/osm/osmium-rs/src/lib.rs +576 -0
  137. xy-0.0.2/examples/osm/osmium-rs/src/main.rs +88 -0
  138. xy-0.0.2/examples/osm/osmium-rs/src/sort.rs +369 -0
  139. xy-0.0.2/examples/osm/osmium-rs/src/sort_main.rs +105 -0
  140. xy-0.0.2/examples/pdsh/README.md +130 -0
  141. xy-0.0.2/examples/reflex/README.md +80 -0
  142. xy-0.0.2/examples/reflex/pyproject.toml +16 -0
  143. xy-0.0.2/examples/reflex/rxconfig.py +10 -0
  144. xy-0.0.2/examples/reflex/xy_reflex_demo/__init__.py +1 -0
  145. xy-0.0.2/examples/reflex/xy_reflex_demo/xy_reflex_demo.py +722 -0
  146. xy-0.0.2/hatch_build.py +326 -0
  147. xy-0.0.2/js/build.mjs +127 -0
  148. xy-0.0.2/js/src/00_header.ts +224 -0
  149. xy-0.0.2/js/src/10_colormaps.ts +51 -0
  150. xy-0.0.2/js/src/20_theme.ts +213 -0
  151. xy-0.0.2/js/src/30_ticks.ts +232 -0
  152. xy-0.0.2/js/src/40_gl.ts +975 -0
  153. xy-0.0.2/js/src/45_lod.ts +1457 -0
  154. xy-0.0.2/js/src/46_worker.ts +103 -0
  155. xy-0.0.2/js/src/50_chartview.ts +5647 -0
  156. xy-0.0.2/js/src/51_annotations.ts +674 -0
  157. xy-0.0.2/js/src/52_tooltip.ts +329 -0
  158. xy-0.0.2/js/src/53_interaction.ts +2491 -0
  159. xy-0.0.2/js/src/54_kernel.ts +995 -0
  160. xy-0.0.2/js/src/55_marks.ts +222 -0
  161. xy-0.0.2/js/src/56_animation.ts +504 -0
  162. xy-0.0.2/js/src/57_viewstate.ts +583 -0
  163. xy-0.0.2/js/src/60_entries.ts +94 -0
  164. xy-0.0.2/js/tsconfig.json +23 -0
  165. xy-0.0.2/package-lock.json +1324 -0
  166. xy-0.0.2/package.json +14 -0
  167. xy-0.0.2/pyproject.toml +230 -0
  168. xy-0.0.2/python/xy/__init__.py +325 -0
  169. xy-0.0.2/python/xy/_annotations.py +598 -0
  170. xy-0.0.2/python/xy/_arrowgeom.py +210 -0
  171. xy-0.0.2/python/xy/_chromium.py +436 -0
  172. xy-0.0.2/python/xy/_figure.py +1889 -0
  173. xy-0.0.2/python/xy/_framing.py +306 -0
  174. xy-0.0.2/python/xy/_hosts.py +68 -0
  175. xy-0.0.2/python/xy/_jpeg.py +382 -0
  176. xy-0.0.2/python/xy/_native.py +3258 -0
  177. xy-0.0.2/python/xy/_ooc.py +150 -0
  178. xy-0.0.2/python/xy/_paint.py +85 -0
  179. xy-0.0.2/python/xy/_payload.py +1140 -0
  180. xy-0.0.2/python/xy/_pdf.py +1262 -0
  181. xy-0.0.2/python/xy/_png.py +86 -0
  182. xy-0.0.2/python/xy/_raster.py +2177 -0
  183. xy-0.0.2/python/xy/_scene.py +107 -0
  184. xy-0.0.2/python/xy/_spatial.py +118 -0
  185. xy-0.0.2/python/xy/_svg.py +3000 -0
  186. xy-0.0.2/python/xy/_trace.py +147 -0
  187. xy-0.0.2/python/xy/_typing.py +57 -0
  188. xy-0.0.2/python/xy/_validate.py +502 -0
  189. xy-0.0.2/python/xy/_webp.py +322 -0
  190. xy-0.0.2/python/xy/channel.py +411 -0
  191. xy-0.0.2/python/xy/channels.py +811 -0
  192. xy-0.0.2/python/xy/columns.py +610 -0
  193. xy-0.0.2/python/xy/components.py +5197 -0
  194. xy-0.0.2/python/xy/config.py +174 -0
  195. xy-0.0.2/python/xy/dom.py +40 -0
  196. xy-0.0.2/python/xy/export.py +1139 -0
  197. xy-0.0.2/python/xy/facets.py +485 -0
  198. xy-0.0.2/python/xy/interaction.py +1398 -0
  199. xy-0.0.2/python/xy/kernels.py +155 -0
  200. xy-0.0.2/python/xy/lod.py +991 -0
  201. xy-0.0.2/python/xy/marks.py +2485 -0
  202. xy-0.0.2/python/xy/py.typed +0 -0
  203. xy-0.0.2/python/xy/pyplot/__init__.py +3093 -0
  204. xy-0.0.2/python/xy/pyplot/_artists.py +1175 -0
  205. xy-0.0.2/python/xy/pyplot/_axes.py +5325 -0
  206. xy-0.0.2/python/xy/pyplot/_axisgrid.py +301 -0
  207. xy-0.0.2/python/xy/pyplot/_colors.py +397 -0
  208. xy-0.0.2/python/xy/pyplot/_fmt.py +52 -0
  209. xy-0.0.2/python/xy/pyplot/_grid.py +345 -0
  210. xy-0.0.2/python/xy/pyplot/_mathtext.py +150 -0
  211. xy-0.0.2/python/xy/pyplot/_mplfig.py +1273 -0
  212. xy-0.0.2/python/xy/pyplot/_plot_types.py +4709 -0
  213. xy-0.0.2/python/xy/pyplot/_rc.py +190 -0
  214. xy-0.0.2/python/xy/pyplot/_state.py +155 -0
  215. xy-0.0.2/python/xy/pyplot/_ticker.py +276 -0
  216. xy-0.0.2/python/xy/pyplot/_transforms.py +111 -0
  217. xy-0.0.2/python/xy/pyplot/_translate.py +141 -0
  218. xy-0.0.2/python/xy/pyplot/dates.py +188 -0
  219. xy-0.0.2/python/xy/static/index.js +853 -0
  220. xy-0.0.2/python/xy/static/standalone.js +853 -0
  221. xy-0.0.2/python/xy/styles.py +379 -0
  222. xy-0.0.2/python/xy/widget.py +192 -0
  223. xy-0.0.2/scripts/_app_smoke.py +279 -0
  224. xy-0.0.2/scripts/_protocol.py +18 -0
  225. xy-0.0.2/scripts/abi_smoke.py +1699 -0
  226. xy-0.0.2/scripts/animation_smoke.py +420 -0
  227. xy-0.0.2/scripts/append_stream_smoke.py +430 -0
  228. xy-0.0.2/scripts/bench.py +11 -0
  229. xy-0.0.2/scripts/bench_dashboard.py +11 -0
  230. xy-0.0.2/scripts/bench_interaction.py +11 -0
  231. xy-0.0.2/scripts/bench_native.py +11 -0
  232. xy-0.0.2/scripts/bench_pyplot_vs_matplotlib.py +11 -0
  233. xy-0.0.2/scripts/bench_scatter_native.py +11 -0
  234. xy-0.0.2/scripts/bench_vs.py +11 -0
  235. xy-0.0.2/scripts/browser_conformance.mjs +300 -0
  236. xy-0.0.2/scripts/check_claim_guardrails.py +216 -0
  237. xy-0.0.2/scripts/check_public_api.py +520 -0
  238. xy-0.0.2/scripts/check_python_floor.py +180 -0
  239. xy-0.0.2/scripts/check_regressions.py +268 -0
  240. xy-0.0.2/scripts/check_release_version.py +81 -0
  241. xy-0.0.2/scripts/gen_font.py +124 -0
  242. xy-0.0.2/scripts/interaction_stress_smoke.py +131 -0
  243. xy-0.0.2/scripts/merge_benchmark_reports.py +116 -0
  244. xy-0.0.2/scripts/pick_boundary_smoke.py +316 -0
  245. xy-0.0.2/scripts/png_export_smoke.py +181 -0
  246. xy-0.0.2/scripts/pyodide_load_smoke.py +107 -0
  247. xy-0.0.2/scripts/reflex_lifecycle_smoke.py +325 -0
  248. xy-0.0.2/scripts/reflex_ws_smoke.py +356 -0
  249. xy-0.0.2/scripts/rename_fc_to_xy.py +199 -0
  250. xy-0.0.2/scripts/render_smoke_nonumpy.py +1624 -0
  251. xy-0.0.2/scripts/smoke_render.py +130 -0
  252. xy-0.0.2/scripts/step_tier_smoke.py +143 -0
  253. xy-0.0.2/scripts/sync_matplotlib_compat.py +186 -0
  254. xy-0.0.2/scripts/verify_benchmark_report.py +2179 -0
  255. xy-0.0.2/scripts/verify_ci_workflow.py +880 -0
  256. xy-0.0.2/scripts/verify_docs_quickstart.py +184 -0
  257. xy-0.0.2/scripts/verify_local.py +658 -0
  258. xy-0.0.2/scripts/verify_sdist.py +435 -0
  259. xy-0.0.2/scripts/verify_wheel.py +424 -0
  260. xy-0.0.2/scripts/visual_regression_smoke.py +259 -0
  261. xy-0.0.2/spec/README.md +111 -0
  262. xy-0.0.2/spec/api/api-examples.md +590 -0
  263. xy-0.0.2/spec/api/chart-kind-contract.md +259 -0
  264. xy-0.0.2/spec/api/chart-roadmap.md +453 -0
  265. xy-0.0.2/spec/api/export.md +218 -0
  266. xy-0.0.2/spec/api/interaction.md +507 -0
  267. xy-0.0.2/spec/api/styling.md +626 -0
  268. xy-0.0.2/spec/assets/benchmark-snapshot.svg +93 -0
  269. xy-0.0.2/spec/assets/launch-benchmark-comparison.svg +90 -0
  270. xy-0.0.2/spec/assets/xy-sdf-binned-scatter.png +0 -0
  271. xy-0.0.2/spec/benchmarks/methodology.md +371 -0
  272. xy-0.0.2/spec/benchmarks/metrics.md +56 -0
  273. xy-0.0.2/spec/benchmarks/results.md +1015 -0
  274. xy-0.0.2/spec/design/animation.md +164 -0
  275. xy-0.0.2/spec/design/chart-grammar.md +351 -0
  276. xy-0.0.2/spec/design/lod-architecture.md +738 -0
  277. xy-0.0.2/spec/design/pan-and-zoom-configuration.md +902 -0
  278. xy-0.0.2/spec/design/reflex-integration.md +641 -0
  279. xy-0.0.2/spec/design/reflex-shaped-api.md +778 -0
  280. xy-0.0.2/spec/design/renderer-architecture.md +418 -0
  281. xy-0.0.2/spec/design/rust-engine.md +340 -0
  282. xy-0.0.2/spec/design/view-state.md +447 -0
  283. xy-0.0.2/spec/design/wire-protocol.md +448 -0
  284. xy-0.0.2/spec/design-dossier.md +1945 -0
  285. xy-0.0.2/spec/matplotlib/compat-changelog.md +270 -0
  286. xy-0.0.2/spec/matplotlib/compat-matrix.md +138 -0
  287. xy-0.0.2/spec/matplotlib/compat.md +141 -0
  288. xy-0.0.2/spec/matplotlib/shim-todo.md +680 -0
  289. xy-0.0.2/spec/process/contributing.md +278 -0
  290. xy-0.0.2/spec/process/perf-audit-2026-07-22.md +194 -0
  291. xy-0.0.2/spec/process/production-readiness.md +471 -0
  292. xy-0.0.2/spec/process/rendering-verification.md +257 -0
  293. xy-0.0.2/spec/process/security-audit-2026-07-06.md +228 -0
  294. xy-0.0.2/src/css.rs +788 -0
  295. xy-0.0.2/src/font.rs +1039 -0
  296. xy-0.0.2/src/kernels.rs +7214 -0
  297. xy-0.0.2/src/lib.rs +3352 -0
  298. xy-0.0.2/src/raster.rs +3451 -0
  299. xy-0.0.2/src/simd.rs +448 -0
  300. xy-0.0.2/src/svg.rs +58 -0
  301. xy-0.0.2/src/tiles.rs +1160 -0
  302. xy-0.0.2/tests/conftest.py +167 -0
  303. xy-0.0.2/tests/pyplot/__init__.py +0 -0
  304. xy-0.0.2/tests/pyplot/compatibility.json +27 -0
  305. xy-0.0.2/tests/pyplot/conftest.py +17 -0
  306. xy-0.0.2/tests/pyplot/corpus/01_basic_line.py +7 -0
  307. xy-0.0.2/tests/pyplot/corpus/02_plot_fmt_red_dashed.py +8 -0
  308. xy-0.0.2/tests/pyplot/corpus/03_plot_fmt_green_circles.py +3 -0
  309. xy-0.0.2/tests/pyplot/corpus/04_plot_fmt_cycle_dashdot_square.py +7 -0
  310. xy-0.0.2/tests/pyplot/corpus/05_multi_series_one_call.py +6 -0
  311. xy-0.0.2/tests/pyplot/corpus/06_implicit_x.py +4 -0
  312. xy-0.0.2/tests/pyplot/corpus/07_labels_title_legend_grid.py +13 -0
  313. xy-0.0.2/tests/pyplot/corpus/08_xlim_ylim.py +8 -0
  314. xy-0.0.2/tests/pyplot/corpus/09_log_scale.py +9 -0
  315. xy-0.0.2/tests/pyplot/corpus/10_scatter_basic.py +6 -0
  316. xy-0.0.2/tests/pyplot/corpus/11_scatter_color_array_cmap.py +9 -0
  317. xy-0.0.2/tests/pyplot/corpus/12_scatter_size_array.py +9 -0
  318. xy-0.0.2/tests/pyplot/corpus/13_scatter_edgecolors.py +15 -0
  319. xy-0.0.2/tests/pyplot/corpus/14_bar_categories.py +7 -0
  320. xy-0.0.2/tests/pyplot/corpus/15_bar_stacked_bottom.py +9 -0
  321. xy-0.0.2/tests/pyplot/corpus/16_barh.py +5 -0
  322. xy-0.0.2/tests/pyplot/corpus/17_hist_bins.py +7 -0
  323. xy-0.0.2/tests/pyplot/corpus/18_hist_density.py +6 -0
  324. xy-0.0.2/tests/pyplot/corpus/19_hist_cumulative.py +7 -0
  325. xy-0.0.2/tests/pyplot/corpus/20_fill_between_band.py +11 -0
  326. xy-0.0.2/tests/pyplot/corpus/21_imshow_cmap.py +8 -0
  327. xy-0.0.2/tests/pyplot/corpus/22_step.py +7 -0
  328. xy-0.0.2/tests/pyplot/corpus/23_axhline_axvline.py +8 -0
  329. xy-0.0.2/tests/pyplot/corpus/24_axvspan_band.py +9 -0
  330. xy-0.0.2/tests/pyplot/corpus/25_annotate_text.py +10 -0
  331. xy-0.0.2/tests/pyplot/corpus/26_twinx_dual_axis.py +11 -0
  332. xy-0.0.2/tests/pyplot/corpus/27_subplots_2x2_mixed.py +17 -0
  333. xy-0.0.2/tests/pyplot/corpus/28_subplots_figsize.py +8 -0
  334. xy-0.0.2/tests/pyplot/corpus/29_implicit_state_savefig.py +13 -0
  335. xy-0.0.2/tests/pyplot/corpus/30_savefig_html.py +10 -0
  336. xy-0.0.2/tests/pyplot/corpus/31_rcparams_figsize.py +9 -0
  337. xy-0.0.2/tests/pyplot/corpus/32_xticks_rotation.py +7 -0
  338. xy-0.0.2/tests/pyplot/corpus/33_set_data_mutation.py +16 -0
  339. xy-0.0.2/tests/pyplot/corpus/34_gray_string_color.py +8 -0
  340. xy-0.0.2/tests/pyplot/corpus/35_tab_colors.py +5 -0
  341. xy-0.0.2/tests/pyplot/corpus/36_color_cycle_c0_c9.py +8 -0
  342. xy-0.0.2/tests/pyplot/corpus/37_markers_only_fmt.py +8 -0
  343. xy-0.0.2/tests/pyplot/corpus/38_close_all_hygiene.py +5 -0
  344. xy-0.0.2/tests/pyplot/corpus/39_multiple_figures.py +8 -0
  345. xy-0.0.2/tests/pyplot/corpus/40_subplots_row_sharex.py +9 -0
  346. xy-0.0.2/tests/pyplot/corpus/41_line_kwargs.py +17 -0
  347. xy-0.0.2/tests/pyplot/corpus/42_tick_params_rotation.py +5 -0
  348. xy-0.0.2/tests/pyplot/corpus/43_grid_html_suptitle.py +15 -0
  349. xy-0.0.2/tests/pyplot/corpus/44_subplot_classic.py +11 -0
  350. xy-0.0.2/tests/pyplot/corpus/45_xticks_positions_labels.py +7 -0
  351. xy-0.0.2/tests/pyplot/corpus/46_stackplot.py +7 -0
  352. xy-0.0.2/tests/pyplot/corpus/47_statistical_families.py +12 -0
  353. xy-0.0.2/tests/pyplot/corpus/48_vector_fields.py +11 -0
  354. xy-0.0.2/tests/pyplot/corpus/49_unstructured_mesh.py +14 -0
  355. xy-0.0.2/tests/pyplot/corpus/50_pie_donut.py +10 -0
  356. xy-0.0.2/tests/pyplot/corpus/51_basic_2d_aliases.py +11 -0
  357. xy-0.0.2/tests/pyplot/corpus/52_spectral.py +16 -0
  358. xy-0.0.2/tests/pyplot/corpus/53_matplotlib_311_plotting.py +47 -0
  359. xy-0.0.2/tests/pyplot/corpus/54_plotting_method_coverage.py +32 -0
  360. xy-0.0.2/tests/pyplot/matplotlib_311_plotting.json +29 -0
  361. xy-0.0.2/tests/pyplot/test_advanced_compatibility.py +82 -0
  362. xy-0.0.2/tests/pyplot/test_adversarial_parity_fixes.py +237 -0
  363. xy-0.0.2/tests/pyplot/test_annotation_label_clearance.py +174 -0
  364. xy-0.0.2/tests/pyplot/test_artist_mutations.py +94 -0
  365. xy-0.0.2/tests/pyplot/test_artist_transform_contracts.py +102 -0
  366. xy-0.0.2/tests/pyplot/test_axes_charts.py +588 -0
  367. xy-0.0.2/tests/pyplot/test_axes_helpers.py +116 -0
  368. xy-0.0.2/tests/pyplot/test_axes_layout.py +209 -0
  369. xy-0.0.2/tests/pyplot/test_boundaries.py +131 -0
  370. xy-0.0.2/tests/pyplot/test_color_pipeline_fixes.py +257 -0
  371. xy-0.0.2/tests/pyplot/test_compatibility_metadata.py +90 -0
  372. xy-0.0.2/tests/pyplot/test_corpus.py +85 -0
  373. xy-0.0.2/tests/pyplot/test_facetgrid.py +190 -0
  374. xy-0.0.2/tests/pyplot/test_figure_state.py +130 -0
  375. xy-0.0.2/tests/pyplot/test_fmt.py +39 -0
  376. xy-0.0.2/tests/pyplot/test_grid_legend_contracts.py +163 -0
  377. xy-0.0.2/tests/pyplot/test_launch_compat.py +375 -0
  378. xy-0.0.2/tests/pyplot/test_layout_noops.py +77 -0
  379. xy-0.0.2/tests/pyplot/test_layout_text_parity_fixes.py +298 -0
  380. xy-0.0.2/tests/pyplot/test_marker_fidelity.py +42 -0
  381. xy-0.0.2/tests/pyplot/test_p3_option_contracts.py +430 -0
  382. xy-0.0.2/tests/pyplot/test_pdsh_gap_features.py +704 -0
  383. xy-0.0.2/tests/pyplot/test_perf_guardrail.py +78 -0
  384. xy-0.0.2/tests/pyplot/test_public_annotations.py +45 -0
  385. xy-0.0.2/tests/pyplot/test_pyplot_state_management.py +97 -0
  386. xy-0.0.2/tests/pyplot/test_rc_chrome_contracts.py +98 -0
  387. xy-0.0.2/tests/pyplot/test_rc_color_export_contracts.py +114 -0
  388. xy-0.0.2/tests/pyplot/test_reference_corpus.py +136 -0
  389. xy-0.0.2/tests/pyplot/test_reference_semantics.py +268 -0
  390. xy-0.0.2/tests/pyplot/test_silent_drop_regressions.py +413 -0
  391. xy-0.0.2/tests/pyplot/test_state_and_figs.py +183 -0
  392. xy-0.0.2/tests/pyplot/test_toolbar_controls.py +68 -0
  393. xy-0.0.2/tests/pyplot/test_vectorized_styles.py +246 -0
  394. xy-0.0.2/tests/pyplot/test_visible_style_contracts.py +88 -0
  395. xy-0.0.2/tests/pyplot/test_visual_parity_fixes.py +250 -0
  396. xy-0.0.2/tests/test_accessibility_contract.py +128 -0
  397. xy-0.0.2/tests/test_animation.py +396 -0
  398. xy-0.0.2/tests/test_api_parity.py +179 -0
  399. xy-0.0.2/tests/test_arrow_ingest.py +122 -0
  400. xy-0.0.2/tests/test_arrowgeom.py +63 -0
  401. xy-0.0.2/tests/test_batch_export.py +109 -0
  402. xy-0.0.2/tests/test_bench_pyplot_vs_matplotlib.py +83 -0
  403. xy-0.0.2/tests/test_bench_transport.py +92 -0
  404. xy-0.0.2/tests/test_bench_vs.py +102 -0
  405. xy-0.0.2/tests/test_benchmark_environment.py +353 -0
  406. xy-0.0.2/tests/test_benchmark_requirements.py +22 -0
  407. xy-0.0.2/tests/test_channel.py +384 -0
  408. xy-0.0.2/tests/test_check_regressions.py +245 -0
  409. xy-0.0.2/tests/test_check_release_version.py +88 -0
  410. xy-0.0.2/tests/test_claim_guardrails.py +154 -0
  411. xy-0.0.2/tests/test_components.py +2214 -0
  412. xy-0.0.2/tests/test_context_loss_preserves_view.py +134 -0
  413. xy-0.0.2/tests/test_css_mark_styles.py +309 -0
  414. xy-0.0.2/tests/test_css_validation.py +172 -0
  415. xy-0.0.2/tests/test_declarative_colorbar.py +342 -0
  416. xy-0.0.2/tests/test_density_mean_color.py +523 -0
  417. xy-0.0.2/tests/test_density_pan_no_rebin.py +148 -0
  418. xy-0.0.2/tests/test_density_physical_alpha.py +151 -0
  419. xy-0.0.2/tests/test_density_request_economy.py +258 -0
  420. xy-0.0.2/tests/test_density_sample_resolvability_gate.py +218 -0
  421. xy-0.0.2/tests/test_density_sample_zoomout_fades.py +204 -0
  422. xy-0.0.2/tests/test_density_texture_eviction.py +121 -0
  423. xy-0.0.2/tests/test_density_wire_economy.py +105 -0
  424. xy-0.0.2/tests/test_dependencies.py +50 -0
  425. xy-0.0.2/tests/test_drill_point_window_cache.py +216 -0
  426. xy-0.0.2/tests/test_drill_transition_continuity.py +221 -0
  427. xy-0.0.2/tests/test_drill_window_padding.py +158 -0
  428. xy-0.0.2/tests/test_drill_zoomin_elides_request.py +214 -0
  429. xy-0.0.2/tests/test_drill_zoomout_clears.py +164 -0
  430. xy-0.0.2/tests/test_example_apps.py +268 -0
  431. xy-0.0.2/tests/test_facets.py +382 -0
  432. xy-0.0.2/tests/test_figure.py +2233 -0
  433. xy-0.0.2/tests/test_framing.py +307 -0
  434. xy-0.0.2/tests/test_framing_property.py +42 -0
  435. xy-0.0.2/tests/test_hatch_build.py +91 -0
  436. xy-0.0.2/tests/test_html_transport.py +50 -0
  437. xy-0.0.2/tests/test_image_export.py +494 -0
  438. xy-0.0.2/tests/test_import.py +384 -0
  439. xy-0.0.2/tests/test_interaction_probe_retries.py +84 -0
  440. xy-0.0.2/tests/test_jpeg.py +172 -0
  441. xy-0.0.2/tests/test_kernels.py +1215 -0
  442. xy-0.0.2/tests/test_legend_highlight.py +381 -0
  443. xy-0.0.2/tests/test_legend_resize_regression.py +690 -0
  444. xy-0.0.2/tests/test_legend_toggle.py +566 -0
  445. xy-0.0.2/tests/test_lod.py +629 -0
  446. xy-0.0.2/tests/test_merge_benchmark_reports.py +70 -0
  447. xy-0.0.2/tests/test_modebar_select_drill.py +105 -0
  448. xy-0.0.2/tests/test_ooc.py +170 -0
  449. xy-0.0.2/tests/test_pan_lock_containment.py +215 -0
  450. xy-0.0.2/tests/test_payload_update_rehome.py +130 -0
  451. xy-0.0.2/tests/test_pdf_export.py +220 -0
  452. xy-0.0.2/tests/test_pdsh_dual_engine_notebooks.py +50 -0
  453. xy-0.0.2/tests/test_plot_families.py +356 -0
  454. xy-0.0.2/tests/test_png_export.py +917 -0
  455. xy-0.0.2/tests/test_property_figure.py +223 -0
  456. xy-0.0.2/tests/test_public_api.py +407 -0
  457. xy-0.0.2/tests/test_pyodide_load_smoke.py +46 -0
  458. xy-0.0.2/tests/test_python_floor.py +117 -0
  459. xy-0.0.2/tests/test_scatter.py +1227 -0
  460. xy-0.0.2/tests/test_scatter_matrix.py +480 -0
  461. xy-0.0.2/tests/test_select_polygon.py +146 -0
  462. xy-0.0.2/tests/test_selection_rows.py +119 -0
  463. xy-0.0.2/tests/test_spatial.py +120 -0
  464. xy-0.0.2/tests/test_static_client_security.py +946 -0
  465. xy-0.0.2/tests/test_streaming.py +422 -0
  466. xy-0.0.2/tests/test_svg_export.py +733 -0
  467. xy-0.0.2/tests/test_symlog_axis.py +238 -0
  468. xy-0.0.2/tests/test_type_surface.py +453 -0
  469. xy-0.0.2/tests/test_ui_issue_regressions.py +325 -0
  470. xy-0.0.2/tests/test_verify_benchmark_report.py +1544 -0
  471. xy-0.0.2/tests/test_verify_ci_workflow.py +765 -0
  472. xy-0.0.2/tests/test_verify_local.py +675 -0
  473. xy-0.0.2/tests/test_verify_sdist.py +498 -0
  474. xy-0.0.2/tests/test_verify_wheel.py +471 -0
  475. xy-0.0.2/tests/test_view_state.py +344 -0
  476. xy-0.0.2/tests/test_view_state_client.py +1254 -0
  477. xy-0.0.2/tests/test_viewport_bounds.py +58 -0
  478. xy-0.0.2/tests/test_webp.py +142 -0
  479. xy-0.0.2/tests/test_wheel_gesture_end.py +178 -0
  480. xy-0.0.2/tests/test_widget.py +153 -0
  481. xy-0.0.2/tests/test_zoom_precision.py +104 -0
@@ -0,0 +1,796 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main", "claude/**"]
6
+ pull_request:
7
+ paths-ignore:
8
+ - "docs/**"
9
+ - "spec/**"
10
+ workflow_dispatch:
11
+
12
+ # Multiple sessions push to two branches in bursts; without this the runner
13
+ # pool drowns in runs for superseded commits and fresh tips queue for ~30 min.
14
+ # Only the newest run per ref carries signal — cancel the rest.
15
+ concurrency:
16
+ group: ci-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ matplotlib_reference:
21
+ name: Matplotlib 3.11 reference compatibility
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
+ with:
26
+ # Full history and tags: the distribution version is derived
27
+ # from the latest `v*` tag, and a shallow clone has none.
28
+ fetch-depth: 0
29
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
30
+ with:
31
+ enable-cache: false
32
+ # The editable install below builds the render client from source via the
33
+ # hatch hook (generated, not committed; §33) — the pyplot corpus exports
34
+ # HTML, which inlines the client bundle. Pin Node so the build uses a
35
+ # vite-compatible version rather than the runner's system Node.
36
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
37
+ with:
38
+ node-version: "22"
39
+ - name: Install xy and released reference wheel
40
+ run: |
41
+ uv venv .venv
42
+ uv pip install -p .venv/bin/python -e ".[dev]"
43
+ uv pip install -p .venv/bin/python "matplotlib==3.11.0"
44
+ - name: Verify released reference and reviewed snapshot
45
+ run: |
46
+ .venv/bin/python -c "import matplotlib; assert matplotlib.__version__ == '3.11.0'"
47
+ .venv/bin/python scripts/sync_matplotlib_compat.py --check
48
+ - name: Run optional-interoperability and dual-engine corpus tests
49
+ env:
50
+ MPLBACKEND: Agg
51
+ run: |
52
+ .venv/bin/pytest -q tests/pyplot/test_launch_compat.py
53
+ .venv/bin/pytest -q tests/pyplot/test_reference_corpus.py
54
+ .venv/bin/pytest -q tests/pyplot/test_reference_semantics.py
55
+
56
+ test:
57
+ name: Test (Rust + Python + JS)
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
61
+ with:
62
+ # Full history and tags: the distribution version is derived
63
+ # from the latest `v*` tag, and a shallow clone has none.
64
+ fetch-depth: 0
65
+ - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
66
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
67
+ with:
68
+ enable-cache: false
69
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
70
+ with:
71
+ node-version: "22"
72
+
73
+ - name: Install JS build toolchain
74
+ run: npm ci
75
+
76
+ - name: Rust unit tests
77
+ run: cargo test
78
+
79
+ - name: Rust lint
80
+ run: cargo clippy --all-targets -- -D warnings
81
+
82
+ - name: Build native core
83
+ run: cargo build --release
84
+
85
+ - name: Build JS client
86
+ run: node js/build.mjs
87
+
88
+ - name: JS parses (ESM + IIFE)
89
+ run: |
90
+ node -e "import('./python/xy/static/index.js').then(m => { if (!m.default.render) throw new Error('no render export'); })"
91
+ node --check python/xy/static/standalone.js
92
+
93
+ - name: Python 3.11 floor syntax check
94
+ run: python3 scripts/check_python_floor.py
95
+
96
+ - name: Workflow gate wiring
97
+ run: python3 scripts/verify_ci_workflow.py
98
+
99
+ # Dependency-free gates — verify the two seams cargo can't (the C ABI and
100
+ # the WebGL2 render path) and run even if PyPI is unreachable.
101
+ - name: Native ABI smoke (stdlib only)
102
+ run: python3 scripts/abi_smoke.py
103
+
104
+ # Playwright's Chromium, not the apt/snap one: Ubuntu's chromium-browser is
105
+ # a confined snap that can't read the temp HTML under /tmp and whose GPU
106
+ # process crashes headless — it fails the render probe for environment
107
+ # reasons, not code. Playwright ships an unconfined build.
108
+ - name: Install Chromium (Playwright)
109
+ run: |
110
+ npm ci
111
+ npx playwright install --with-deps chromium
112
+ - name: Headless render smoke (stdlib + Chromium)
113
+ run: |
114
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
115
+ echo "using $CHROME"
116
+ python3 scripts/render_smoke_nonumpy.py "$CHROME"
117
+
118
+ - name: Streaming append smoke (stdlib + Chromium)
119
+ run: |
120
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
121
+ python3 scripts/append_stream_smoke.py "$CHROME"
122
+
123
+ - name: Static PNG export smoke (stdlib + Chromium)
124
+ run: XY_CHROMIUM="$(node -e "console.log(require('playwright').chromium.executablePath())")" python3 scripts/png_export_smoke.py
125
+
126
+ - name: Native benchmark (stdlib only — executed numbers, no PyPI needed)
127
+ run: python3 scripts/bench_native.py --sizes 1e5,1e6
128
+
129
+ - name: Install package + dev deps
130
+ run: |
131
+ uv venv .venv
132
+ uv pip install -p .venv/bin/python -e ".[dev]"
133
+
134
+ - name: Regression gate (deterministic payloads = hard fail; timing = advisory)
135
+ run: |
136
+ .venv/bin/python benchmarks/bench_scatter_native.py \
137
+ --sizes 1e5,1e6,1e7 --production --json scatter.json
138
+ python3 scripts/bench_native.py --sizes 1e6,1e7 --json kernel.json
139
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
140
+ .venv/bin/python benchmarks/bench_transport.py --n 1e6 --reps 15 \
141
+ --browser-reps 12 --chromium "$CHROME" --require-browser --json transport.json
142
+ python3 scripts/verify_benchmark_report.py scatter.json --kind scatter-native
143
+ python3 scripts/verify_benchmark_report.py kernel.json --kind kernel-native
144
+ python3 scripts/verify_benchmark_report.py transport.json --kind transport-loopback
145
+ python3 scripts/check_regressions.py --scatter scatter.json --kernel kernel.json \
146
+ --transport transport.json --emit-md spec/benchmarks/metrics.md
147
+
148
+ - name: Upload regression benchmark report
149
+ if: always()
150
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
151
+ with:
152
+ name: regression-benchmark-report
153
+ if-no-files-found: warn
154
+ path: |
155
+ spec/benchmarks/metrics.md
156
+ scatter.json
157
+ kernel.json
158
+ transport.json
159
+
160
+ - name: Public API coherence
161
+ run: .venv/bin/python scripts/check_public_api.py
162
+
163
+ - name: Claim guardrails
164
+ run: .venv/bin/python scripts/check_claim_guardrails.py
165
+
166
+ - name: Lint (ruff)
167
+ run: |
168
+ .venv/bin/ruff check .
169
+ .venv/bin/ruff format --check .
170
+
171
+ # ty is pre-1.0 (0.0.x) and advisory here: it type-checks the shippable
172
+ # library and reports findings, but doesn't gate the build — pytest, ruff,
173
+ # and cargo are the hard gates. Test/benchmark code intentionally
174
+ # subscripts known-non-None Optionals, which ty can't narrow.
175
+ - name: Typecheck (ty, advisory)
176
+ run: .venv/bin/ty check python || echo "::warning::ty reported type findings (advisory)"
177
+
178
+ - name: Python tests (native core)
179
+ # Point PNG-export tests at the Playwright Chromium (not on PATH) so the
180
+ # full Figure.to_png path runs here rather than skipping.
181
+ run: |
182
+ export XY_CHROMIUM="$(node -e "console.log(require('playwright').chromium.executablePath())")"
183
+ # Chromium is installed here, so a probe that cannot launch it is a
184
+ # defect: fail rather than skip, so the browser layer cannot pass by
185
+ # absence. (A crash or non-zero exit already fails unconditionally.)
186
+ export XY_REQUIRE_BROWSER=1
187
+ .venv/bin/pytest -q
188
+
189
+ # End-to-end: a real Figure (line decimation + scatter) through to_html's
190
+ # exact payload path, rendered in Chromium, pixels read back. Complements
191
+ # the stdlib smoke, which hand-builds payloads. (Was dead code — audit
192
+ # finding: the only real-Figure→browser gate, never executed.)
193
+ - name: Real-Figure render smoke (numpy + Chromium)
194
+ run: |
195
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
196
+ .venv/bin/python scripts/smoke_render.py "$CHROME"
197
+
198
+ - name: Browser lifecycle smoke (Chromium)
199
+ run: |
200
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
201
+ .venv/bin/python scripts/reflex_lifecycle_smoke.py "$CHROME"
202
+
203
+ - name: Browser visual regression smoke (Chromium)
204
+ run: |
205
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
206
+ .venv/bin/python scripts/visual_regression_smoke.py "$CHROME"
207
+
208
+ - name: Step tier-update smoke (Chromium)
209
+ run: |
210
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
211
+ .venv/bin/python scripts/step_tier_smoke.py "$CHROME"
212
+
213
+ - name: Browser interaction stress smoke (Chromium)
214
+ run: |
215
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
216
+ .venv/bin/python scripts/interaction_stress_smoke.py "$CHROME"
217
+
218
+ - name: Browser dashboard reliability smoke (Chromium)
219
+ run: |
220
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
221
+ .venv/bin/python benchmarks/bench_dashboard.py \
222
+ --chart-counts 10,20,50 --chromium "$CHROME" --json dashboard-smoke.json
223
+ .venv/bin/python scripts/verify_benchmark_report.py \
224
+ dashboard-smoke.json --kind dashboard-browser
225
+
226
+ - name: Benchmark smoke (1e5/1e6 — §12 harness runs every phase)
227
+ run: .venv/bin/python scripts/bench.py --sizes 1e5,1e6
228
+
229
+ browser_conformance:
230
+ name: Accessibility + cross-browser conformance
231
+ runs-on: ubuntu-latest
232
+ steps:
233
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
234
+ with:
235
+ # Full history and tags: the distribution version is derived
236
+ # from the latest `v*` tag, and a shallow clone has none.
237
+ fetch-depth: 0
238
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
239
+ with:
240
+ node-version: "22"
241
+ - name: Cache Playwright browsers
242
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
243
+ with:
244
+ path: ~/.cache/ms-playwright
245
+ key: playwright-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
246
+ - name: Install Playwright browser engines
247
+ run: |
248
+ npm ci
249
+ npx playwright install --with-deps chromium firefox webkit
250
+ - name: Build render client
251
+ run: node js/build.mjs
252
+ - name: Run accessibility and cross-browser conformance
253
+ # Headless Firefox on GitHub's Linux runners exposes no WebGL2. Use the
254
+ # virtual display installed by Playwright so every engine still runs
255
+ # the real WebGL rendering and perceptual assertions.
256
+ env:
257
+ MOZ_WEBRENDER: "1"
258
+ XY_CONFORMANCE_HEADFUL: "1"
259
+ run: xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24" node scripts/browser_conformance.mjs
260
+
261
+ python_floor:
262
+ name: Python 3.11 floor
263
+ runs-on: ubuntu-latest
264
+ steps:
265
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
266
+ with:
267
+ # Full history and tags: the distribution version is derived
268
+ # from the latest `v*` tag, and a shallow clone has none.
269
+ fetch-depth: 0
270
+ - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
271
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
272
+ with:
273
+ python-version: "3.11"
274
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
275
+ with:
276
+ enable-cache: false
277
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
278
+ with:
279
+ node-version: "22"
280
+
281
+ - name: Verify syntax/annotation floor
282
+ run: python scripts/check_python_floor.py
283
+
284
+ - name: Build native core
285
+ run: cargo build --release
286
+
287
+ # The render client is a generated artifact (§33): build it so the full
288
+ # test suite (which reads python/xy/static/*.js) has it on disk.
289
+ - name: Build render client
290
+ run: |
291
+ npm ci
292
+ node js/build.mjs
293
+
294
+ - name: Install package for Python 3.11
295
+ run: |
296
+ python -VV
297
+ # Absolute path: newer uv resolves a bare name as a directory, not
298
+ # the interpreter setup-python put on PATH.
299
+ uv venv .venv --python "$(command -v python)"
300
+ uv pip install -p .venv/bin/python -e . "pytest>=8,<9"
301
+
302
+ - name: Public API coherence
303
+ run: .venv/bin/python scripts/check_public_api.py
304
+
305
+ - name: Python 3.11 tests (native core)
306
+ run: .venv/bin/python -m pytest -q
307
+
308
+ # Cross-library scatter benchmark incl. time-to-first-render (spec/benchmarks/results.md).
309
+ # Each isolated group uses the same deterministic input sizes and runs in
310
+ # parallel. The report job below merges the partial results.
311
+ benchmark_vs:
312
+ name: Cross-library benchmark (${{ matrix.name }})
313
+ runs-on: ubuntu-latest
314
+ timeout-minutes: 10
315
+ continue-on-error: true
316
+ strategy:
317
+ fail-fast: false
318
+ matrix:
319
+ include:
320
+ - name: native-and-webgl
321
+ libraries: xy,plotly_gl,bokeh_webgl,datashader
322
+ packages: plotly kaleido bokeh datashader psutil
323
+ max_n: 10000000
324
+ xy: true
325
+ browser: true
326
+ build_js: true
327
+ # These adapters each run a timed render plus a separate tracemalloc
328
+ # render. Keep them isolated so their expensive ceiling probes run in
329
+ # parallel instead of serializing the slowest benchmark job.
330
+ - name: matplotlib
331
+ libraries: matplotlib
332
+ packages: numpy matplotlib psutil
333
+ max_n: 10000000
334
+ xy: false
335
+ browser: false
336
+ build_js: false
337
+ - name: seaborn
338
+ libraries: seaborn
339
+ packages: numpy seaborn psutil
340
+ max_n: 10000000
341
+ xy: false
342
+ browser: false
343
+ build_js: false
344
+ - name: plotly-svg
345
+ libraries: plotly_svg
346
+ packages: numpy plotly kaleido psutil
347
+ max_n: 10000000
348
+ xy: false
349
+ browser: true
350
+ build_js: false
351
+ - name: bokeh-canvas
352
+ libraries: bokeh_canvas
353
+ packages: numpy bokeh psutil
354
+ max_n: 10000000
355
+ xy: false
356
+ browser: true
357
+ build_js: false
358
+ - name: html-adapters
359
+ libraries: altair,hvplot_bokeh
360
+ packages: numpy altair hvplot psutil
361
+ # HTML serialization becomes pathological at multi-million-point
362
+ # sizes. Keep this group aligned with the existing TTFR cap; the
363
+ # skipped rows remain explicit in the merged report.
364
+ max_n: 100000
365
+ xy: false
366
+ browser: true
367
+ build_js: false
368
+ steps:
369
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
370
+ with:
371
+ # Full history and tags: the distribution version is derived
372
+ # from the latest `v*` tag, and a shallow clone has none.
373
+ fetch-depth: 0
374
+ - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
375
+ if: matrix.xy
376
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
377
+ with:
378
+ enable-cache: false
379
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
380
+ if: matrix.browser || matrix.build_js
381
+ with:
382
+ node-version: "22"
383
+ - name: Install JS build toolchain
384
+ if: matrix.build_js
385
+ run: npm ci
386
+ - name: Build native core
387
+ if: matrix.xy
388
+ run: cargo build --release
389
+ - name: Build JS client
390
+ if: matrix.build_js
391
+ run: node js/build.mjs
392
+ - name: Create benchmark environment
393
+ run: uv venv .venv
394
+ - name: Install xy
395
+ if: matrix.xy
396
+ env:
397
+ XY_REQUIRE_CARGO: "1"
398
+ run: |
399
+ uv pip install -p .venv/bin/python \
400
+ --constraint benchmarks/requirements-ci.lock -e .
401
+ - name: Install selected competitors
402
+ run: |
403
+ uv pip install -p .venv/bin/python \
404
+ --constraint benchmarks/requirements-ci.lock ${{ matrix.packages }}
405
+ - name: Verify native benchmark backend
406
+ if: matrix.xy
407
+ run: |
408
+ .venv/bin/python - <<'PY'
409
+ import xy.kernels as k
410
+ assert k.BACKEND == "native", f"benchmark job requires native backend, got {k.BACKEND!r}"
411
+ PY
412
+ - name: Install Chromium (Playwright)
413
+ if: matrix.browser
414
+ run: |
415
+ npm ci
416
+ npx playwright install --with-deps chromium
417
+ - name: Run cross-library benchmark group
418
+ run: |
419
+ CHROMIUM_ARGS=()
420
+ if [[ "${{ matrix.browser }}" == "true" ]]; then
421
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
422
+ CHROMIUM_ARGS=(--chromium "$CHROME")
423
+ fi
424
+ .venv/bin/python scripts/bench_vs.py \
425
+ --libraries "${{ matrix.libraries }}" \
426
+ --max-n "${{ matrix.max_n }}" \
427
+ --sizes 1e3,1e4,1e5,1e6,3e6,1e7 --budget 45 \
428
+ --ttfr --ttfr-max-n 1e5 "${CHROMIUM_ARGS[@]}" \
429
+ --out scatter-${{ matrix.name }}.md --json scatter-${{ matrix.name }}.json
430
+ - name: Upload cross-library benchmark part
431
+ if: always()
432
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
433
+ with:
434
+ name: benchmark-vs-${{ matrix.name }}
435
+ if-no-files-found: warn
436
+ path: |
437
+ scatter-${{ matrix.name }}.md
438
+ scatter-${{ matrix.name }}.json
439
+
440
+ # Other benchmark suites run concurrently with the cross-library matrix.
441
+ benchmark_methodology:
442
+ name: Benchmark methodology suites
443
+ runs-on: ubuntu-latest
444
+ continue-on-error: true
445
+ steps:
446
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
447
+ with:
448
+ # Full history and tags: the distribution version is derived
449
+ # from the latest `v*` tag, and a shallow clone has none.
450
+ fetch-depth: 0
451
+ - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
452
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
453
+ with:
454
+ enable-cache: false
455
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
456
+ with:
457
+ node-version: "22"
458
+ - name: Install JS build toolchain
459
+ run: npm ci
460
+ - name: Build core + JS client
461
+ run: |
462
+ node js/build.mjs
463
+ cargo build --release
464
+ - name: Install xy + competitors
465
+ env:
466
+ XY_REQUIRE_CARGO: "1"
467
+ run: |
468
+ uv venv .venv
469
+ uv pip install -p .venv/bin/python \
470
+ --constraint benchmarks/requirements-ci.lock -e .
471
+ uv pip install -p .venv/bin/python \
472
+ --constraint benchmarks/requirements-ci.lock \
473
+ matplotlib seaborn plotly kaleido bokeh altair datashader hvplot \
474
+ plotly-resampler psutil
475
+ - name: Verify native benchmark backend
476
+ run: |
477
+ .venv/bin/python - <<'PY'
478
+ import xy.kernels as k
479
+ assert k.BACKEND == "native", f"benchmark job requires native backend, got {k.BACKEND!r}"
480
+ PY
481
+ # Playwright Chromium for the time-to-first-render (browser paint) pass.
482
+ - name: Install Chromium (Playwright)
483
+ run: |
484
+ npm ci
485
+ npx playwright install --with-deps chromium
486
+ - name: Line decimation vs plotly-resampler (methodology line_10M)
487
+ run: |
488
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
489
+ .venv/bin/python benchmarks/bench_line.py --sizes 1e5,1e6,1e7 \
490
+ --ttfr --ttfr-max-n 1e5 --chromium "$CHROME" --json line.json \
491
+ | tee -a docs/benchmark_ci.md
492
+ - name: Install footprint & cold import (methodology §1)
493
+ run: .venv/bin/python benchmarks/bench_install.py --json install.json | tee -a docs/benchmark_ci.md
494
+ - name: Fresh-environment footprint (xy vs Plotly)
495
+ run: |
496
+ .venv/bin/python benchmarks/bench_install.py --packages xy,plotly \
497
+ --repeat 3 --fresh-venv --json install-fresh.json | tee -a docs/benchmark_ci.md
498
+ - name: Ingestion, streaming, and export workflows
499
+ run: |
500
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
501
+ .venv/bin/python benchmarks/bench_workflows.py --profile standard --reps 5 \
502
+ --chromium "$CHROME" --json workflows.json | tee -a docs/benchmark_ci.md
503
+ - name: Browser interaction latency (methodology interaction_smoothness)
504
+ run: |
505
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
506
+ .venv/bin/python benchmarks/bench_interaction.py \
507
+ --sizes 1e4,2.5e5 --reps 24 --chromium "$CHROME" --retries 2 \
508
+ --json interaction.json | tee -a docs/benchmark_ci.md
509
+ - name: Many-chart dashboard startup (methodology dashboard_20)
510
+ run: |
511
+ CHROME=$(node -e "console.log(require('playwright').chromium.executablePath())")
512
+ .venv/bin/python benchmarks/bench_dashboard.py \
513
+ --chart-counts 10,20,50 --chromium "$CHROME" \
514
+ --json dashboard.json | tee -a docs/benchmark_ci.md
515
+ - name: Verify benchmark JSON artifacts
516
+ run: |
517
+ .venv/bin/python scripts/verify_benchmark_report.py line.json --kind line-decimation
518
+ .venv/bin/python scripts/verify_benchmark_report.py install.json --kind install-footprint
519
+ .venv/bin/python scripts/verify_benchmark_report.py install-fresh.json --kind install-footprint
520
+ .venv/bin/python scripts/verify_benchmark_report.py workflows.json --kind workflow-native
521
+ .venv/bin/python scripts/verify_benchmark_report.py interaction.json --kind interaction-browser
522
+ .venv/bin/python scripts/verify_benchmark_report.py dashboard.json --kind dashboard-browser
523
+ - name: Show report
524
+ run: cat docs/benchmark_ci.md
525
+ - name: Upload benchmark methodology
526
+ if: always()
527
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
528
+ with:
529
+ name: benchmark-methodology
530
+ if-no-files-found: warn
531
+ path: |
532
+ docs/benchmark_ci.md
533
+ line.json
534
+ install.json
535
+ install-fresh.json
536
+ workflows.json
537
+ interaction.json
538
+ dashboard.json
539
+
540
+ benchmark:
541
+ name: Merge and verify benchmark reports
542
+ runs-on: ubuntu-latest
543
+ needs: [benchmark_vs, benchmark_methodology]
544
+ if: always()
545
+ continue-on-error: true
546
+ steps:
547
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
548
+ with:
549
+ # Full history and tags: the distribution version is derived
550
+ # from the latest `v*` tag, and a shallow clone has none.
551
+ fetch-depth: 0
552
+ - name: Download cross-library benchmark parts
553
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
554
+ with:
555
+ pattern: benchmark-vs-*
556
+ path: benchmark-vs-parts
557
+ - name: Download methodology benchmark artifacts
558
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
559
+ with:
560
+ name: benchmark-methodology
561
+ path: benchmark-methodology
562
+ - name: Merge cross-library benchmark parts
563
+ run: |
564
+ python scripts/merge_benchmark_reports.py \
565
+ --parts benchmark-vs-parts/*/*.json \
566
+ --expected-libraries xy,matplotlib,seaborn,plotly_gl,plotly_svg,bokeh_canvas,bokeh_webgl,altair,datashader,hvplot_bokeh \
567
+ --out benchmark.json --out-md scatter-vs.md
568
+ - name: Assemble benchmark report
569
+ run: |
570
+ cat scatter-vs.md benchmark-methodology/docs/benchmark_ci.md > docs/benchmark_ci.md
571
+ - name: Verify benchmark JSON artifacts
572
+ run: |
573
+ python scripts/verify_benchmark_report.py benchmark.json --kind scatter-vs
574
+ python scripts/verify_benchmark_report.py benchmark-methodology/line.json --kind line-decimation
575
+ python scripts/verify_benchmark_report.py benchmark-methodology/install.json --kind install-footprint
576
+ python scripts/verify_benchmark_report.py benchmark-methodology/install-fresh.json --kind install-footprint
577
+ python scripts/verify_benchmark_report.py benchmark-methodology/workflows.json --kind workflow-native
578
+ python scripts/verify_benchmark_report.py benchmark-methodology/interaction.json --kind interaction-browser
579
+ python scripts/verify_benchmark_report.py benchmark-methodology/dashboard.json --kind dashboard-browser
580
+ - name: Show report
581
+ run: cat docs/benchmark_ci.md
582
+ - name: Upload benchmark report
583
+ if: always()
584
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
585
+ with:
586
+ name: benchmark-report
587
+ if-no-files-found: warn
588
+ path: |
589
+ docs/benchmark_ci.md
590
+ benchmark.json
591
+ benchmark-methodology/line.json
592
+ benchmark-methodology/install.json
593
+ benchmark-methodology/install-fresh.json
594
+ benchmark-methodology/workflows.json
595
+ benchmark-methodology/interaction.json
596
+ benchmark-methodology/dashboard.json
597
+
598
+ sdist:
599
+ name: Source distribution
600
+ runs-on: ubuntu-latest
601
+ steps:
602
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
603
+ with:
604
+ # Full history and tags: the distribution version is derived
605
+ # from the latest `v*` tag, and a shallow clone has none.
606
+ fetch-depth: 0
607
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
608
+ with:
609
+ enable-cache: false
610
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
611
+ with:
612
+ node-version: "22"
613
+
614
+ - name: Install JS build toolchain
615
+ run: npm ci
616
+
617
+ - name: Build sdist
618
+ # The hatch build hook builds the render client (node) and the artifacts
619
+ # config carries it into the sdist. The client is required by default, so
620
+ # a client-less sdist fails the build here rather than shipping broken.
621
+ run: uv build --sdist
622
+
623
+ - name: Verify sdist contents
624
+ shell: bash
625
+ run: |
626
+ sdist=$(ls dist/*.tar.gz)
627
+ python scripts/verify_sdist.py "$sdist"
628
+
629
+ - name: Verify sdist without Rust errors clearly on import
630
+ shell: bash
631
+ env:
632
+ XY_SKIP_CARGO: "1"
633
+ run: |
634
+ uv venv smoke
635
+ uv pip install -p smoke dist/*.tar.gz numpy anywidget
636
+ ./smoke/bin/python - <<'PY'
637
+ # No native core and no fallback: importing the compute layer must
638
+ # raise a clear, actionable error (§33: no-wheel behavior is defined
639
+ # as a loud failure, not a silent degrade).
640
+ import xy
641
+
642
+ assert xy.__version__
643
+ try:
644
+ import xy.kernels # noqa: F401
645
+ except ImportError as err:
646
+ msg = str(err)
647
+ assert "native Rust core" in msg, msg
648
+ assert "rustup.rs" in msg, msg
649
+ print("sdist without Rust errors clearly on import")
650
+ else:
651
+ raise SystemExit("expected ImportError without the native core")
652
+ PY
653
+
654
+ # Linux and Windows wheel smoke matrix (§33). macOS wheels are built only by
655
+ # release.yml so pull requests do not consume macOS runners; release coverage
656
+ # still includes both Intel and Apple Silicon macOS wheels.
657
+ wheels:
658
+ name: Wheel ${{ matrix.os }}
659
+ needs: test
660
+ runs-on: ${{ matrix.os }}
661
+ strategy:
662
+ fail-fast: false
663
+ matrix:
664
+ os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest]
665
+ steps:
666
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
667
+ with:
668
+ # Full history and tags: the distribution version is derived
669
+ # from the latest `v*` tag, and a shallow clone has none.
670
+ fetch-depth: 0
671
+ - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
672
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
673
+ with:
674
+ enable-cache: false
675
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
676
+ with:
677
+ node-version: "22"
678
+
679
+ - name: Install JS build toolchain
680
+ run: npm ci
681
+
682
+ - name: Build wheel
683
+ # The hatch build hook builds both artifacts. The render client is
684
+ # required by default; REQUIRE_CARGO additionally forbids the native core
685
+ # from silently falling back to a pure-Python wheel here.
686
+ env:
687
+ XY_REQUIRE_CARGO: "1"
688
+ run: uv build --wheel
689
+
690
+ # A raw linux_x86_64 tag is not installable off this machine and PyPI
691
+ # rejects it; auditwheel relabels to manylinux (our cdylib needs only
692
+ # libc/libm, so there's nothing external to bundle). The glibc floor is
693
+ # the runner's; a manylinux-container build (cibuildwheel) would lower it.
694
+ - name: Repair Linux wheel to manylinux
695
+ if: runner.os == 'Linux'
696
+ shell: bash
697
+ run: |
698
+ sudo apt-get update && sudo apt-get install -y patchelf
699
+ uv tool install auditwheel
700
+ uv tool run auditwheel repair dist/*.whl -w wheelhouse
701
+ rm dist/*.whl && mv wheelhouse/*.whl dist/
702
+ ls -la dist/
703
+
704
+ - name: Verify wheel contents
705
+ shell: bash
706
+ run: |
707
+ whl=$(ls dist/*.whl)
708
+ python scripts/verify_wheel.py "$whl" --expect-native
709
+
710
+ - name: Install-size budget (≤ 15 MB, §33)
711
+ shell: bash
712
+ run: |
713
+ # wc -c is portable (macOS/BSD du has no -b); trim BSD's leading spaces.
714
+ whl=$(ls dist/*.whl)
715
+ size=$(wc -c < "$whl" | tr -d ' ')
716
+ echo "wheel size: $size bytes ($whl)"
717
+ test "$size" -le 15728640
718
+
719
+ - name: Smoke-install and import
720
+ shell: bash
721
+ run: |
722
+ uv venv smoke
723
+ uv pip install -p smoke dist/*.whl numpy anywidget
724
+ ./smoke/bin/python -c "import xy, xy.kernels as k; assert k.BACKEND == 'native', k.BACKEND; print('native core loaded')" \
725
+ || ./smoke/Scripts/python.exe -c "import xy, xy.kernels as k; assert k.BACKEND == 'native', k.BACKEND; print('native core loaded')"
726
+
727
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
728
+ with:
729
+ name: wheel-${{ matrix.os }}
730
+ path: dist/*.whl
731
+
732
+ # Proves the no-wheel contract (§33): a source build with NO Rust toolchain
733
+ # produces a pure-Python wheel that imports the package fine but raises a
734
+ # clear, actionable error the moment compute is needed — never a silent
735
+ # fallback and never an opaque crash.
736
+ install_without_rust:
737
+ name: Install without Rust (clear error)
738
+ runs-on: ubuntu-latest
739
+ steps:
740
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
741
+ with:
742
+ # Full history and tags: the distribution version is derived
743
+ # from the latest `v*` tag, and a shallow clone has none.
744
+ fetch-depth: 0
745
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
746
+ with:
747
+ enable-cache: false
748
+ # The render client is independent of the native core (§33): this job
749
+ # drops Rust but keeps Node so the coreless wheel still carries the client.
750
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
751
+ with:
752
+ node-version: "22"
753
+ - name: Install JS build toolchain
754
+ run: npm ci
755
+ # GitHub runners preinstall Rust (which would defeat this job's purpose
756
+ # by happily compiling the core!) — strip every cargo from PATH so the
757
+ # build genuinely exercises the no-toolchain path.
758
+ - name: Remove preinstalled Rust
759
+ run: |
760
+ while p=$(command -v cargo 2>/dev/null); do
761
+ echo "removing $p"; sudo rm -f "$p"
762
+ done
763
+ if command -v cargo; then echo "cargo still present"; exit 1; fi
764
+ echo "cargo is gone"
765
+ - name: Build wheel with no toolchain
766
+ # No Rust (pure-Python wheel), but the render client is required by
767
+ # default and must still ship — this job exercises exactly that split
768
+ # (verify_wheel.py --expect-pure below asserts the client is present).
769
+ run: uv build --wheel
770
+ - name: Wheel is pure-Python (py3-none-any)
771
+ shell: bash
772
+ run: ls dist/ && ls dist/*-py3-none-any.whl
773
+ - name: Verify pure (no-native) wheel contents
774
+ shell: bash
775
+ run: |
776
+ whl=$(ls dist/*.whl)
777
+ python scripts/verify_wheel.py "$whl" --expect-pure
778
+ - name: Install and verify it errors clearly on compute
779
+ shell: bash
780
+ run: |
781
+ uv venv smoke
782
+ uv pip install -p smoke dist/*.whl numpy anywidget
783
+ ./smoke/bin/python - <<'PY'
784
+ import xy # lazy top-level import still succeeds
785
+
786
+ assert xy.__version__
787
+ try:
788
+ import xy.kernels # noqa: F401
789
+ except ImportError as err:
790
+ msg = str(err)
791
+ assert "native Rust core" in msg, msg
792
+ assert "rustup.rs" in msg, msg
793
+ print("no-Rust install errors clearly on import; OK")
794
+ else:
795
+ raise SystemExit("expected ImportError without the native core")
796
+ PY