solara-ui 1.45.0__py3-none-any.whl

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 (464) hide show
  1. prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  2. prefix/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  3. solara/__init__.py +124 -0
  4. solara/__main__.py +765 -0
  5. solara/_stores.py +297 -0
  6. solara/alias.py +6 -0
  7. solara/autorouting.py +555 -0
  8. solara/cache.py +305 -0
  9. solara/checks.html +71 -0
  10. solara/checks.py +227 -0
  11. solara/comm.py +28 -0
  12. solara/components/__init__.py +77 -0
  13. solara/components/alert.py +155 -0
  14. solara/components/applayout.py +397 -0
  15. solara/components/button.py +85 -0
  16. solara/components/card.py +87 -0
  17. solara/components/checkbox.py +50 -0
  18. solara/components/code_highlight_css.py +11 -0
  19. solara/components/code_highlight_css.vue +63 -0
  20. solara/components/columns.py +159 -0
  21. solara/components/component_vue.py +134 -0
  22. solara/components/cross_filter.py +335 -0
  23. solara/components/dataframe.py +546 -0
  24. solara/components/datatable.py +214 -0
  25. solara/components/datatable.vue +175 -0
  26. solara/components/details.py +56 -0
  27. solara/components/download.vue +35 -0
  28. solara/components/echarts.py +86 -0
  29. solara/components/echarts.vue +139 -0
  30. solara/components/figure_altair.py +39 -0
  31. solara/components/file_browser.py +181 -0
  32. solara/components/file_download.py +199 -0
  33. solara/components/file_drop.py +159 -0
  34. solara/components/file_drop.vue +83 -0
  35. solara/components/file_list_widget.vue +78 -0
  36. solara/components/head.py +27 -0
  37. solara/components/head_tag.py +49 -0
  38. solara/components/head_tag.vue +60 -0
  39. solara/components/image.py +173 -0
  40. solara/components/input.py +456 -0
  41. solara/components/input_text_area.py +86 -0
  42. solara/components/link.py +55 -0
  43. solara/components/markdown.py +441 -0
  44. solara/components/markdown_editor.py +33 -0
  45. solara/components/markdown_editor.vue +359 -0
  46. solara/components/matplotlib.py +74 -0
  47. solara/components/meta.py +47 -0
  48. solara/components/misc.py +333 -0
  49. solara/components/pivot_table.py +258 -0
  50. solara/components/pivot_table.vue +158 -0
  51. solara/components/progress.py +47 -0
  52. solara/components/select.py +182 -0
  53. solara/components/select.vue +27 -0
  54. solara/components/slider.py +442 -0
  55. solara/components/slider_date.vue +56 -0
  56. solara/components/spinner-solara.vue +105 -0
  57. solara/components/spinner.py +45 -0
  58. solara/components/sql_code.py +41 -0
  59. solara/components/sql_code.vue +125 -0
  60. solara/components/style.py +105 -0
  61. solara/components/switch.py +71 -0
  62. solara/components/tab_navigation.py +37 -0
  63. solara/components/title.py +90 -0
  64. solara/components/title.vue +38 -0
  65. solara/components/togglebuttons.py +200 -0
  66. solara/components/tooltip.py +61 -0
  67. solara/core.py +42 -0
  68. solara/datatypes.py +143 -0
  69. solara/express.py +241 -0
  70. solara/hooks/__init__.py +4 -0
  71. solara/hooks/dataframe.py +99 -0
  72. solara/hooks/misc.py +263 -0
  73. solara/hooks/use_reactive.py +151 -0
  74. solara/hooks/use_thread.py +129 -0
  75. solara/kitchensink.py +8 -0
  76. solara/lab/__init__.py +34 -0
  77. solara/lab/components/__init__.py +7 -0
  78. solara/lab/components/chat.py +215 -0
  79. solara/lab/components/confirmation_dialog.py +163 -0
  80. solara/lab/components/cross_filter.py +7 -0
  81. solara/lab/components/input_date.py +339 -0
  82. solara/lab/components/input_time.py +133 -0
  83. solara/lab/components/menu.py +181 -0
  84. solara/lab/components/menu.vue +38 -0
  85. solara/lab/components/tabs.py +274 -0
  86. solara/lab/components/theming.py +98 -0
  87. solara/lab/components/theming.vue +72 -0
  88. solara/lab/hooks/__init__.py +0 -0
  89. solara/lab/hooks/dataframe.py +2 -0
  90. solara/lab/toestand.py +3 -0
  91. solara/lab/utils/__init__.py +2 -0
  92. solara/lab/utils/cookies.py +5 -0
  93. solara/lab/utils/dataframe.py +165 -0
  94. solara/lab/utils/headers.py +5 -0
  95. solara/layout.py +44 -0
  96. solara/lifecycle.py +46 -0
  97. solara/minisettings.py +141 -0
  98. solara/py.typed +0 -0
  99. solara/reactive.py +99 -0
  100. solara/routing.py +268 -0
  101. solara/scope/__init__.py +88 -0
  102. solara/scope/types.py +55 -0
  103. solara/server/__init__.py +0 -0
  104. solara/server/app.py +527 -0
  105. solara/server/assets/custom.css +1 -0
  106. solara/server/assets/custom.js +1 -0
  107. solara/server/assets/favicon.png +0 -0
  108. solara/server/assets/favicon.svg +5 -0
  109. solara/server/assets/style.css +1681 -0
  110. solara/server/assets/theme-dark.css +437 -0
  111. solara/server/assets/theme-light.css +420 -0
  112. solara/server/assets/theme.js +3 -0
  113. solara/server/cdn_helper.py +91 -0
  114. solara/server/esm.py +71 -0
  115. solara/server/fastapi.py +5 -0
  116. solara/server/flask.py +297 -0
  117. solara/server/jupyter/__init__.py +2 -0
  118. solara/server/jupyter/cdn_handler.py +28 -0
  119. solara/server/jupyter/server_extension.py +40 -0
  120. solara/server/jupyter/solara.py +91 -0
  121. solara/server/jupytertools.py +46 -0
  122. solara/server/kernel.py +388 -0
  123. solara/server/kernel_context.py +467 -0
  124. solara/server/patch.py +564 -0
  125. solara/server/pyinstaller/__init__.py +9 -0
  126. solara/server/pyinstaller/hook-ipyreact.py +5 -0
  127. solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
  128. solara/server/pyinstaller/hook-solara.py +9 -0
  129. solara/server/qt.py +113 -0
  130. solara/server/reload.py +251 -0
  131. solara/server/server.py +484 -0
  132. solara/server/settings.py +249 -0
  133. solara/server/shell.py +269 -0
  134. solara/server/starlette.py +770 -0
  135. solara/server/static/ansi.js +270 -0
  136. solara/server/static/highlight-dark.css +82 -0
  137. solara/server/static/highlight.css +43 -0
  138. solara/server/static/main-vuetify.js +272 -0
  139. solara/server/static/main.js +163 -0
  140. solara/server/static/solara_bootstrap.py +129 -0
  141. solara/server/static/sun.svg +23 -0
  142. solara/server/static/webworker.js +42 -0
  143. solara/server/telemetry.py +212 -0
  144. solara/server/templates/index.html.j2 +1 -0
  145. solara/server/templates/loader-plain.css +11 -0
  146. solara/server/templates/loader-plain.html +20 -0
  147. solara/server/templates/loader-solara.css +111 -0
  148. solara/server/templates/loader-solara.html +40 -0
  149. solara/server/templates/plain.html +82 -0
  150. solara/server/templates/solara.html.j2 +486 -0
  151. solara/server/threaded.py +84 -0
  152. solara/server/utils.py +44 -0
  153. solara/server/websocket.py +45 -0
  154. solara/settings.py +86 -0
  155. solara/tasks.py +893 -0
  156. solara/template/button.py +16 -0
  157. solara/template/markdown.py +42 -0
  158. solara/template/portal/.flake8 +6 -0
  159. solara/template/portal/.pre-commit-config.yaml +28 -0
  160. solara/template/portal/LICENSE +21 -0
  161. solara/template/portal/Procfile +7 -0
  162. solara/template/portal/mypy.ini +3 -0
  163. solara/template/portal/pyproject.toml +26 -0
  164. solara/template/portal/solara_portal/__init__.py +4 -0
  165. solara/template/portal/solara_portal/components/__init__.py +2 -0
  166. solara/template/portal/solara_portal/components/article.py +28 -0
  167. solara/template/portal/solara_portal/components/data.py +28 -0
  168. solara/template/portal/solara_portal/components/header.py +6 -0
  169. solara/template/portal/solara_portal/components/layout.py +6 -0
  170. solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
  171. solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
  172. solara/template/portal/solara_portal/data.py +60 -0
  173. solara/template/portal/solara_portal/pages/__init__.py +67 -0
  174. solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
  175. solara/template/portal/solara_portal/pages/tabular.py +29 -0
  176. solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
  177. solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
  178. solara/test/__init__.py +0 -0
  179. solara/test/pytest_plugin.py +783 -0
  180. solara/toestand.py +998 -0
  181. solara/util.py +348 -0
  182. solara/validate_hooks.py +258 -0
  183. solara/website/__init__.py +0 -0
  184. solara/website/assets/custom.css +444 -0
  185. solara/website/assets/images/logo-small.png +0 -0
  186. solara/website/assets/images/logo.svg +17 -0
  187. solara/website/assets/images/logo_white.svg +50 -0
  188. solara/website/assets/theme.js +8 -0
  189. solara/website/components/__init__.py +5 -0
  190. solara/website/components/algolia.py +6 -0
  191. solara/website/components/algolia.vue +24 -0
  192. solara/website/components/algolia_api.vue +202 -0
  193. solara/website/components/breadcrumbs.py +28 -0
  194. solara/website/components/contact.py +144 -0
  195. solara/website/components/docs.py +143 -0
  196. solara/website/components/header.py +75 -0
  197. solara/website/components/mailchimp.py +12 -0
  198. solara/website/components/mailchimp.vue +47 -0
  199. solara/website/components/markdown.py +99 -0
  200. solara/website/components/markdown_nav.vue +34 -0
  201. solara/website/components/notebook.py +171 -0
  202. solara/website/components/sidebar.py +105 -0
  203. solara/website/pages/__init__.py +370 -0
  204. solara/website/pages/about/__init__.py +9 -0
  205. solara/website/pages/about/about.md +3 -0
  206. solara/website/pages/apps/__init__.py +16 -0
  207. solara/website/pages/apps/authorization/__init__.py +119 -0
  208. solara/website/pages/apps/authorization/admin.py +12 -0
  209. solara/website/pages/apps/authorization/users.py +12 -0
  210. solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
  211. solara/website/pages/apps/layout-demo.py +40 -0
  212. solara/website/pages/apps/multipage/__init__.py +38 -0
  213. solara/website/pages/apps/multipage/page1.py +26 -0
  214. solara/website/pages/apps/multipage/page2.py +34 -0
  215. solara/website/pages/apps/scatter.py +136 -0
  216. solara/website/pages/apps/scrolling.py +63 -0
  217. solara/website/pages/apps/tutorial-streamlit.py +18 -0
  218. solara/website/pages/careers/__init__.py +27 -0
  219. solara/website/pages/changelog/__init__.py +10 -0
  220. solara/website/pages/changelog/changelog.md +372 -0
  221. solara/website/pages/contact/__init__.py +34 -0
  222. solara/website/pages/doc_use_download.py +85 -0
  223. solara/website/pages/documentation/__init__.py +90 -0
  224. solara/website/pages/documentation/advanced/__init__.py +9 -0
  225. solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
  226. solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
  227. solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
  228. solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
  229. solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +417 -0
  230. solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
  231. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +50 -0
  232. solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
  233. solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
  234. solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
  235. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +72 -0
  236. solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
  237. solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
  238. solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
  239. solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
  240. solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
  241. solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
  242. solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
  243. solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
  244. solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
  245. solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
  246. solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
  247. solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +192 -0
  248. solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
  249. solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
  250. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +256 -0
  251. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +108 -0
  252. solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
  253. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +7 -0
  254. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +187 -0
  255. solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
  256. solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
  257. solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
  258. solara/website/pages/documentation/api/__init__.py +19 -0
  259. solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
  260. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +22 -0
  261. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +20 -0
  262. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +20 -0
  263. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +20 -0
  264. solara/website/pages/documentation/api/hooks/__init__.py +9 -0
  265. solara/website/pages/documentation/api/hooks/use_cross_filter.py +23 -0
  266. solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
  267. solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
  268. solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
  269. solara/website/pages/documentation/api/hooks/use_exception.py +31 -0
  270. solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
  271. solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
  272. solara/website/pages/documentation/api/hooks/use_previous.py +30 -0
  273. solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
  274. solara/website/pages/documentation/api/hooks/use_state.py +10 -0
  275. solara/website/pages/documentation/api/hooks/use_state_or_update.py +66 -0
  276. solara/website/pages/documentation/api/hooks/use_thread.md +64 -0
  277. solara/website/pages/documentation/api/hooks/use_thread.py +42 -0
  278. solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
  279. solara/website/pages/documentation/api/routing/__init__.py +9 -0
  280. solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
  281. solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
  282. solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
  283. solara/website/pages/documentation/api/routing/route.py +29 -0
  284. solara/website/pages/documentation/api/routing/use_route.py +76 -0
  285. solara/website/pages/documentation/api/routing/use_router.py +16 -0
  286. solara/website/pages/documentation/api/utilities/__init__.py +9 -0
  287. solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
  288. solara/website/pages/documentation/api/utilities/computed.py +16 -0
  289. solara/website/pages/documentation/api/utilities/display.py +16 -0
  290. solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
  291. solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
  292. solara/website/pages/documentation/api/utilities/memoize.py +35 -0
  293. solara/website/pages/documentation/api/utilities/on_kernel_start.py +44 -0
  294. solara/website/pages/documentation/api/utilities/reactive.py +16 -0
  295. solara/website/pages/documentation/api/utilities/widget.py +104 -0
  296. solara/website/pages/documentation/components/__init__.py +12 -0
  297. solara/website/pages/documentation/components/advanced/__init__.py +9 -0
  298. solara/website/pages/documentation/components/advanced/link.py +25 -0
  299. solara/website/pages/documentation/components/advanced/meta.py +17 -0
  300. solara/website/pages/documentation/components/advanced/style.py +43 -0
  301. solara/website/pages/documentation/components/common.py +9 -0
  302. solara/website/pages/documentation/components/data/__init__.py +9 -0
  303. solara/website/pages/documentation/components/data/dataframe.py +44 -0
  304. solara/website/pages/documentation/components/data/pivot_table.py +81 -0
  305. solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
  306. solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
  307. solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
  308. solara/website/pages/documentation/components/input/__init__.py +9 -0
  309. solara/website/pages/documentation/components/input/button.py +23 -0
  310. solara/website/pages/documentation/components/input/checkbox.py +10 -0
  311. solara/website/pages/documentation/components/input/file_browser.py +30 -0
  312. solara/website/pages/documentation/components/input/file_drop.py +76 -0
  313. solara/website/pages/documentation/components/input/input.py +43 -0
  314. solara/website/pages/documentation/components/input/select.py +22 -0
  315. solara/website/pages/documentation/components/input/slider.py +29 -0
  316. solara/website/pages/documentation/components/input/switch.py +10 -0
  317. solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
  318. solara/website/pages/documentation/components/lab/__init__.py +9 -0
  319. solara/website/pages/documentation/components/lab/chat.py +109 -0
  320. solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
  321. solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
  322. solara/website/pages/documentation/components/lab/input_date.py +20 -0
  323. solara/website/pages/documentation/components/lab/input_time.py +15 -0
  324. solara/website/pages/documentation/components/lab/menu.py +22 -0
  325. solara/website/pages/documentation/components/lab/tab.py +25 -0
  326. solara/website/pages/documentation/components/lab/tabs.py +45 -0
  327. solara/website/pages/documentation/components/lab/task.py +11 -0
  328. solara/website/pages/documentation/components/lab/theming.py +74 -0
  329. solara/website/pages/documentation/components/lab/use_task.py +11 -0
  330. solara/website/pages/documentation/components/layout/__init__.py +9 -0
  331. solara/website/pages/documentation/components/layout/app_bar.py +16 -0
  332. solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
  333. solara/website/pages/documentation/components/layout/app_layout.py +24 -0
  334. solara/website/pages/documentation/components/layout/card.py +15 -0
  335. solara/website/pages/documentation/components/layout/card_actions.py +16 -0
  336. solara/website/pages/documentation/components/layout/column.py +30 -0
  337. solara/website/pages/documentation/components/layout/columns.py +27 -0
  338. solara/website/pages/documentation/components/layout/columns_responsive.py +66 -0
  339. solara/website/pages/documentation/components/layout/details.py +13 -0
  340. solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
  341. solara/website/pages/documentation/components/layout/gridfixed.py +19 -0
  342. solara/website/pages/documentation/components/layout/hbox.py +18 -0
  343. solara/website/pages/documentation/components/layout/row.py +30 -0
  344. solara/website/pages/documentation/components/layout/sidebar.py +24 -0
  345. solara/website/pages/documentation/components/layout/vbox.py +19 -0
  346. solara/website/pages/documentation/components/output/__init__.py +9 -0
  347. solara/website/pages/documentation/components/output/file_download.py +11 -0
  348. solara/website/pages/documentation/components/output/html.py +19 -0
  349. solara/website/pages/documentation/components/output/image.py +11 -0
  350. solara/website/pages/documentation/components/output/markdown.py +57 -0
  351. solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
  352. solara/website/pages/documentation/components/output/sql_code.py +83 -0
  353. solara/website/pages/documentation/components/output/tooltip.py +11 -0
  354. solara/website/pages/documentation/components/page/__init__.py +9 -0
  355. solara/website/pages/documentation/components/page/head.py +15 -0
  356. solara/website/pages/documentation/components/page/title.py +25 -0
  357. solara/website/pages/documentation/components/status/__init__.py +9 -0
  358. solara/website/pages/documentation/components/status/error.py +39 -0
  359. solara/website/pages/documentation/components/status/info.py +39 -0
  360. solara/website/pages/documentation/components/status/progress.py +10 -0
  361. solara/website/pages/documentation/components/status/spinner.py +11 -0
  362. solara/website/pages/documentation/components/status/success.py +40 -0
  363. solara/website/pages/documentation/components/status/warning.py +47 -0
  364. solara/website/pages/documentation/components/viz/__init__.py +9 -0
  365. solara/website/pages/documentation/components/viz/altair.py +42 -0
  366. solara/website/pages/documentation/components/viz/echarts.py +77 -0
  367. solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
  368. solara/website/pages/documentation/components/viz/plotly.py +63 -0
  369. solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
  370. solara/website/pages/documentation/examples/__init__.py +54 -0
  371. solara/website/pages/documentation/examples/ai/__init__.py +11 -0
  372. solara/website/pages/documentation/examples/ai/chatbot.py +113 -0
  373. solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
  374. solara/website/pages/documentation/examples/basics/__init__.py +10 -0
  375. solara/website/pages/documentation/examples/basics/sine.py +28 -0
  376. solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
  377. solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
  378. solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
  379. solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
  380. solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
  381. solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
  382. solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
  383. solara/website/pages/documentation/examples/general/__init__.py +10 -0
  384. solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
  385. solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
  386. solara/website/pages/documentation/examples/general/live_update.py +32 -0
  387. solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
  388. solara/website/pages/documentation/examples/general/mycard.vue +58 -0
  389. solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
  390. solara/website/pages/documentation/examples/general/vue_component.py +50 -0
  391. solara/website/pages/documentation/examples/ipycanvas.py +49 -0
  392. solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
  393. solara/website/pages/documentation/examples/libraries/altair.py +65 -0
  394. solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
  395. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
  396. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
  397. solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
  398. solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
  399. solara/website/pages/documentation/examples/utilities/countdown_timer.py +62 -0
  400. solara/website/pages/documentation/examples/utilities/todo.py +196 -0
  401. solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
  402. solara/website/pages/documentation/examples/visualization/annotator.py +67 -0
  403. solara/website/pages/documentation/examples/visualization/linked_views.py +81 -0
  404. solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
  405. solara/website/pages/documentation/faq/__init__.py +12 -0
  406. solara/website/pages/documentation/faq/content/99-faq.md +112 -0
  407. solara/website/pages/documentation/getting_started/__init__.py +9 -0
  408. solara/website/pages/documentation/getting_started/content/00-quickstart.md +107 -0
  409. solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
  410. solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
  411. solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
  412. solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
  413. solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
  414. solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
  415. solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
  416. solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
  417. solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +65 -0
  418. solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
  419. solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
  420. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1021 -0
  421. solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
  422. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +228 -0
  423. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +278 -0
  424. solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
  425. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +305 -0
  426. solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
  427. solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
  428. solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
  429. solara/website/pages/docutils.py +38 -0
  430. solara/website/pages/home.vue +1199 -0
  431. solara/website/pages/our_team/__init__.py +83 -0
  432. solara/website/pages/pricing/__init__.py +31 -0
  433. solara/website/pages/roadmap/__init__.py +11 -0
  434. solara/website/pages/roadmap/roadmap.md +47 -0
  435. solara/website/pages/scale_ipywidgets.py +45 -0
  436. solara/website/pages/showcase/__init__.py +105 -0
  437. solara/website/pages/showcase/domino_code_assist.py +60 -0
  438. solara/website/pages/showcase/planeto_tessa.py +19 -0
  439. solara/website/pages/showcase/solara_dev.py +54 -0
  440. solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
  441. solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
  442. solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
  443. solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
  444. solara/website/pages/showcase/wanderlust.py +27 -0
  445. solara/website/public/beach.jpeg +0 -0
  446. solara/website/public/logo.svg +6 -0
  447. solara/website/public/social/discord.svg +1 -0
  448. solara/website/public/social/github.svg +1 -0
  449. solara/website/public/social/twitter.svg +3 -0
  450. solara/website/public/success.html +25 -0
  451. solara/website/templates/index.html.j2 +117 -0
  452. solara/website/utils.py +51 -0
  453. solara/widgets/__init__.py +1 -0
  454. solara/widgets/vue/gridlayout.vue +107 -0
  455. solara/widgets/vue/html.vue +4 -0
  456. solara/widgets/vue/navigator.vue +134 -0
  457. solara/widgets/vue/vegalite.vue +130 -0
  458. solara/widgets/widgets.py +74 -0
  459. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  460. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  461. solara_ui-1.45.0.dist-info/METADATA +162 -0
  462. solara_ui-1.45.0.dist-info/RECORD +464 -0
  463. solara_ui-1.45.0.dist-info/WHEEL +4 -0
  464. solara_ui-1.45.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,783 @@
1
+ import contextlib
2
+ import inspect
3
+ import json
4
+ import logging
5
+ import os
6
+ import shlex
7
+ import subprocess
8
+ import sys
9
+ import textwrap
10
+ import threading
11
+ import typing
12
+ import tempfile
13
+ import urllib.parse
14
+ import uuid
15
+ from io import BytesIO
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, Generator, List, Union, cast
18
+
19
+ import ipywidgets as widgets
20
+ import pytest
21
+ import requests
22
+ from IPython.display import display
23
+
24
+ import solara.server.app
25
+ import solara.server.kernel_context
26
+ import solara.server.server
27
+ import solara.server.settings
28
+ from solara.server import reload
29
+ from solara.server.starlette import ServerStarlette
30
+ from solara.server.threaded import ServerBase
31
+
32
+ if typing.TYPE_CHECKING:
33
+ import playwright.sync_api
34
+
35
+
36
+ logger = logging.getLogger("solara.pytest_plugin")
37
+
38
+ TEST_PORT_START = int(os.environ.get("PORT", "18765")) + 100 # do not interfere with the solara integration tests
39
+ TEST_HOST = solara.server.settings.main.host
40
+ TIMEOUT = float(os.environ.get("SOLARA_PW_TIMEOUT", "18"))
41
+ PYTEST_IPYWIDGETS_SOLARA_APP_WAIT_TIMEOUT = int(os.environ.get("PYTEST_IPYWIDGETS_SOLARA_APP_WAIT_TIMEOUT", "10"))
42
+ runners = os.environ.get("SOLARA_TEST_RUNNERS", "solara,voila,jupyter_lab,jupyter_notebook").split(",")
43
+
44
+
45
+ @pytest.fixture(scope="session")
46
+ def solara_server(pytestconfig: Any, request):
47
+ port = pytestconfig.getoption("--solara-port")
48
+ host = pytestconfig.getoption("--solara-host")
49
+ webserver = ServerStarlette(port, host)
50
+
51
+ try:
52
+ webserver.serve_threaded()
53
+ webserver.wait_until_serving()
54
+ yield webserver
55
+ finally:
56
+ webserver.stop_serving()
57
+
58
+
59
+ try:
60
+ from pytest_playwright.pytest_playwright import connect_options # noqa
61
+
62
+ has_pytest_070 = True
63
+ except ImportError:
64
+ has_pytest_070 = False
65
+
66
+
67
+ if has_pytest_070:
68
+ # pytest-playwright >= 0.7.0
69
+ from pytest_playwright.pytest_playwright import Browser, CreateContextCallback, BrowserContext, Playwright, ArtifactsRecorder, _truncate_file_name
70
+ from slugify import slugify
71
+
72
+ @pytest.fixture(scope="session")
73
+ def output_path_session(pytestconfig: Any, request: pytest.FixtureRequest) -> str:
74
+ output_dir = Path(pytestconfig.getoption("--output")).absolute()
75
+ return os.path.join(output_dir, _truncate_file_name(slugify(request.node.nodeid)))
76
+
77
+ @pytest.fixture(scope="session")
78
+ def _artifacts_recorder_session(
79
+ request: pytest.FixtureRequest,
80
+ output_path_session: str,
81
+ playwright: Playwright,
82
+ pytestconfig: Any,
83
+ _pw_artifacts_folder: tempfile.TemporaryDirectory,
84
+ ) -> Generator["ArtifactsRecorder", None, None]:
85
+ artifacts_recorder = ArtifactsRecorder(pytestconfig, request, output_path_session, playwright, _pw_artifacts_folder)
86
+ yield artifacts_recorder
87
+ # If request.node is missing rep_call, then some error happened during execution
88
+ # that prevented teardown, but should still be counted as a failure
89
+ failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True
90
+ artifacts_recorder.did_finish_test(failed)
91
+
92
+ @pytest.fixture(scope="session")
93
+ def new_context_session(
94
+ browser: Browser,
95
+ browser_context_args: Dict,
96
+ _artifacts_recorder_session: "ArtifactsRecorder",
97
+ request: pytest.FixtureRequest,
98
+ ) -> Generator[CreateContextCallback, None, None]:
99
+ browser_context_args = browser_context_args.copy()
100
+ context_args_marker = next(request.node.iter_markers("browser_context_args"), None)
101
+ additional_context_args = context_args_marker.kwargs if context_args_marker else {}
102
+ browser_context_args.update(additional_context_args)
103
+ contexts: List[BrowserContext] = []
104
+
105
+ def _new_context(**kwargs: Any) -> BrowserContext:
106
+ context = browser.new_context(**browser_context_args, **kwargs)
107
+ original_close = context.close
108
+
109
+ def _close_wrapper(*args: Any, **kwargs: Any) -> None:
110
+ contexts.remove(context)
111
+ _artifacts_recorder_session.on_will_close_browser_context(context)
112
+ original_close(*args, **kwargs)
113
+
114
+ context.close = _close_wrapper
115
+ contexts.append(context)
116
+ _artifacts_recorder_session.on_did_create_browser_context(context)
117
+ return context
118
+
119
+ yield cast(CreateContextCallback, _new_context)
120
+ for context in contexts.copy():
121
+ context.close()
122
+
123
+ @pytest.fixture(scope="session")
124
+ def context_session(new_context_session):
125
+ return new_context_session()
126
+ else:
127
+
128
+ @pytest.fixture(scope="session")
129
+ def context_session(
130
+ browser: "playwright.sync_api.Browser",
131
+ browser_context_args: Dict,
132
+ pytestconfig: Any,
133
+ request: pytest.FixtureRequest,
134
+ ) -> Generator["playwright.sync_api.BrowserContext", None, None]:
135
+ from playwright.sync_api import Error, Page
136
+ from pytest_playwright.pytest_playwright import _build_artifact_test_folder
137
+ from slugify import slugify # type: ignore
138
+
139
+ pages: List[Page] = []
140
+ context = browser.new_context(**browser_context_args)
141
+ context.on("page", lambda page: pages.append(page))
142
+
143
+ tracing_option = pytestconfig.getoption("--tracing")
144
+ capture_trace = tracing_option in ["on", "retain-on-failure"]
145
+ if capture_trace:
146
+ context.tracing.start(
147
+ title=slugify(request.node.nodeid),
148
+ screenshots=True,
149
+ snapshots=True,
150
+ sources=True,
151
+ )
152
+
153
+ # decrease timeout from 30s to 10s
154
+ context.set_default_timeout(TIMEOUT * 1000)
155
+ yield context
156
+
157
+ # If request.node is missing rep_call, then some error happened during execution
158
+ # that prevented teardown, but should still be counted as a failure
159
+ failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True
160
+
161
+ if capture_trace:
162
+ retain_trace = tracing_option == "on" or (failed and tracing_option == "retain-on-failure")
163
+ if retain_trace:
164
+ trace_path = _build_artifact_test_folder(pytestconfig, request, "trace.zip")
165
+ context.tracing.stop(path=trace_path)
166
+ else:
167
+ context.tracing.stop()
168
+
169
+ screenshot_option = pytestconfig.getoption("--screenshot")
170
+ capture_screenshot = screenshot_option == "on" or (failed and screenshot_option == "only-on-failure")
171
+ if capture_screenshot:
172
+ for index, page in enumerate(pages):
173
+ human_readable_status = "failed" if failed else "finished"
174
+ screenshot_path = _build_artifact_test_folder(pytestconfig, request, f"test-{human_readable_status}-{index + 1}.png")
175
+ try:
176
+ page.screenshot(timeout=5000, path=screenshot_path)
177
+ except Error:
178
+ pass
179
+
180
+ context.close()
181
+
182
+ video_option = pytestconfig.getoption("--video")
183
+ preserve_video = video_option == "on" or (failed and video_option == "retain-on-failure")
184
+ if preserve_video:
185
+ for page in pages:
186
+ video = page.video
187
+ if not video:
188
+ continue
189
+ try:
190
+ video_path = video.path()
191
+ file_name = os.path.basename(video_path)
192
+ video.save_as(path=_build_artifact_test_folder(pytestconfig, request, file_name))
193
+ except Error:
194
+ # Silent catch empty videos.
195
+ pass
196
+
197
+
198
+ # page fixture that keeps open all the time, is faster
199
+ @pytest.fixture(scope="session")
200
+ def page_session(context_session: "playwright.sync_api.BrowserContext"):
201
+ page = context_session.new_page()
202
+
203
+ def log(msg):
204
+ print("BROWSER LOG:", msg.text) # noqa
205
+ logger.debug("PAGE LOG: %s", msg.text)
206
+
207
+ page.on("console", log)
208
+ yield page
209
+ page.close()
210
+
211
+
212
+ @pytest.fixture()
213
+ def solara_app(solara_server):
214
+ used_app = None
215
+
216
+ @contextlib.contextmanager
217
+ def run(app: Union[solara.server.app.AppScript, str], init=True):
218
+ nonlocal used_app
219
+ if "__default__" in solara.server.app.apps:
220
+ solara.server.app.apps["__default__"].close()
221
+ if isinstance(app, str):
222
+ app = solara.server.app.AppScript(app)
223
+ if init:
224
+ app.init()
225
+ used_app = app
226
+ solara.server.app.apps["__default__"] = app
227
+ try:
228
+ yield
229
+ finally:
230
+ if app.type == solara.server.app.AppType.MODULE:
231
+ if app.name in sys.modules and app.name.startswith("tests.integration.testapp"):
232
+ del sys.modules[app.name]
233
+ if app.name in reload.reloader.watched_modules:
234
+ reload.reloader.watched_modules.remove(app.name)
235
+
236
+ yield run
237
+ if used_app:
238
+ used_app.close()
239
+
240
+
241
+ run_events: Dict[str, threading.Event] = {}
242
+ used_contexts: Dict[str, solara.server.kernel_context.VirtualKernelContext] = {}
243
+
244
+
245
+ @solara.component
246
+ def SyncWrapper():
247
+ global run_calls
248
+ router = solara.use_router()
249
+ values = urllib.parse.parse_qs(router.search, keep_blank_values=True)
250
+ id = values.get("id", [None])[0] # type: ignore
251
+ if id is None:
252
+ solara.Error("No id found in url")
253
+ else:
254
+ import reacton.ipywidgets as w
255
+
256
+ used_contexts[id] = solara.server.kernel_context.get_current_context()
257
+ run_events[id].set()
258
+
259
+ return w.VBox(children=[w.HTML(value="Test in solara"), w.VBox()])
260
+
261
+
262
+ @contextlib.contextmanager
263
+ def _solara_test(solara_server, solara_app, page_session: "playwright.sync_api.Page", require_vuetify_warmup: bool):
264
+ with solara_app("solara.test.pytest_plugin:SyncWrapper"):
265
+ id = str(uuid.uuid4())
266
+ run_events[id] = run_event = threading.Event()
267
+ page_session.goto(solara_server.base_url + f"?id={id}")
268
+ try:
269
+ assert run_event.wait(PYTEST_IPYWIDGETS_SOLARA_APP_WAIT_TIMEOUT)
270
+ context = used_contexts[id]
271
+ with context:
272
+ test_output_warmup = widgets.Output()
273
+ test_output = widgets.Output()
274
+ try:
275
+ page_session.locator("text=Test in solara").wait_for()
276
+ assert context.container
277
+ context.container.children[0].children[1].children[1].children = [test_output_warmup] # type: ignore
278
+ with test_output_warmup:
279
+ page_session.add_style_tag(
280
+ content="""
281
+ .solara-content-main {
282
+ animation-duration: 0s !important;
283
+ }
284
+ """
285
+ )
286
+ if require_vuetify_warmup:
287
+ warmup()
288
+ button = page_session.locator(".solara-warmup-widget")
289
+ button.wait_for()
290
+ page_session.evaluate("document.fonts.ready")
291
+ button.click()
292
+ button.wait_for(state="detached")
293
+ page_session.evaluate("document.fonts.ready")
294
+ context.container.children[0].children[1].children[1].children = [test_output] # type: ignore
295
+ with test_output:
296
+ yield
297
+ finally:
298
+ test_output.close()
299
+ test_output_warmup.close()
300
+ finally:
301
+ del run_events[id]
302
+ page_session.goto("about:blank")
303
+ if id in used_contexts:
304
+ # handle when run_event.wait(10) fails
305
+ del used_contexts[id]
306
+ assert context.closed_event.wait(10)
307
+
308
+
309
+ @pytest.fixture()
310
+ def solara_test(solara_server, solara_app, page_session: "playwright.sync_api.Page", pytestconfig: Any):
311
+ require_vuetify_warmup = pytestconfig.getoption("solara_vuetify_warmup")
312
+ with _solara_test(solara_server, solara_app, page_session, require_vuetify_warmup):
313
+ yield
314
+
315
+
316
+ class ServerVoila(ServerBase):
317
+ popen = None
318
+
319
+ def __init__(self, notebook_path, port: int, host: str = "localhost", **kwargs):
320
+ self.notebook_path = notebook_path
321
+ super().__init__(port, host)
322
+
323
+ def has_started(self):
324
+ try:
325
+ return requests.get(self.base_url).status_code // 100 in [2, 3]
326
+ except requests.exceptions.ConnectionError:
327
+ return False
328
+
329
+ def signal_stop(self):
330
+ if self.popen is None:
331
+ return
332
+ self.popen.terminate()
333
+ self.popen.kill()
334
+
335
+ def serve(self):
336
+ if self.has_started():
337
+ raise RuntimeError("Jupyter server already running, use lsof -i :{self.port} to find the process and kill it")
338
+ cmd = (
339
+ "voila --no-browser --VoilaTest.log_level=DEBUG --Voila.port_retries=0 --VoilaExecutor.timeout=240"
340
+ f' --Voila.port={self.port} --show_tracebacks=True "{self.notebook_path}" --enable_nbextensions=True'
341
+ )
342
+ logger.info(f"Starting Voila server at {self.base_url} with command {cmd}")
343
+ if sys.platform == "win32":
344
+ args = cmd
345
+ else:
346
+ args = shlex.split(cmd)
347
+ self.popen = subprocess.Popen(args, shell=False, stdout=sys.stdout, stderr=sys.stderr, stdin=None)
348
+ self.started.set()
349
+
350
+
351
+ class ServerJupyter(ServerBase):
352
+ popen = None
353
+
354
+ def __init__(self, notebook_path, port: int, host: str = "localhost", **kwargs):
355
+ self.notebook_path = notebook_path
356
+ super().__init__(port, host)
357
+
358
+ def has_started(self):
359
+ try:
360
+ return requests.get(self.base_url).status_code // 100 in [2, 3]
361
+ except requests.exceptions.ConnectionError:
362
+ return False
363
+
364
+ def signal_stop(self):
365
+ if self.popen is None:
366
+ return
367
+ self.popen.terminate()
368
+ self.popen.kill()
369
+
370
+ def serve(self):
371
+ if self.has_started():
372
+ raise RuntimeError(f"Jupyter server already running, use lsof -i :{self.port} to find the process and kill it")
373
+ cmd = f'jupyter lab --port={self.port} --no-browser --ServerApp.token="" --port-retries=0 "{self.notebook_path}"'
374
+ logger.info(f"Starting Jupyter (lab) server at {self.base_url} with command {cmd}")
375
+ if sys.platform == "win32":
376
+ args = cmd
377
+ else:
378
+ args = shlex.split(cmd)
379
+ self.popen = subprocess.Popen(args, shell=False, stdout=sys.stdout, stderr=sys.stderr, stdin=None)
380
+ self.started.set()
381
+
382
+
383
+ @pytest.fixture(scope="session")
384
+ def voila_server(pytestconfig: Any, notebook_path):
385
+ if "voila" not in runners:
386
+ yield None
387
+ return
388
+ port = pytestconfig.getoption("--voila-port")
389
+ host = pytestconfig.getoption("--solara-host")
390
+ write_notebook(["print('hello')"], notebook_path)
391
+ server = ServerVoila(notebook_path, port, host)
392
+ try:
393
+ server.serve_threaded()
394
+ server.wait_until_serving()
395
+ yield server
396
+ finally:
397
+ server.stop_serving()
398
+
399
+
400
+ @pytest.fixture(scope="session")
401
+ def jupyter_server(pytestconfig: Any, notebook_path):
402
+ if "jupyter_lab" not in runners and "jupyter_notebook" not in runners:
403
+ yield None
404
+ return
405
+ port = pytestconfig.getoption("--jupyter-port")
406
+ host = pytestconfig.getoption("--solara-host")
407
+ write_notebook(["print('hello')"], notebook_path)
408
+ server = ServerJupyter(notebook_path, port, host)
409
+ try:
410
+ server.serve_threaded()
411
+ server.wait_until_serving()
412
+ yield server
413
+ finally:
414
+ server.stop_serving()
415
+
416
+
417
+ def code_from_function(f) -> str:
418
+ lines = inspect.getsourcelines(f)[0]
419
+ lines = lines[1:]
420
+ return textwrap.dedent("".join(lines))
421
+
422
+
423
+ def write_notebook(codes: List[str], path: str):
424
+ notebook = {
425
+ "cells": [{"cell_type": "code", "execution_count": None, "id": "df77670d", "metadata": {}, "outputs": [], "source": [code]} for code in codes],
426
+ "metadata": {
427
+ "kernelspec": {"display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3"},
428
+ "language_info": {
429
+ "codemirror_mode": {"name": "ipython", "version": 3},
430
+ "file_extension": ".py",
431
+ "mimetype": "text/x-python",
432
+ "name": "python",
433
+ "nbconvert_exporter": "python",
434
+ "pygments_lexer": "ipython3",
435
+ "version": "3.9.16",
436
+ },
437
+ },
438
+ "nbformat": 4,
439
+ "nbformat_minor": 5,
440
+ }
441
+
442
+ with open(path, "w") as file:
443
+ json.dump(notebook, file)
444
+
445
+
446
+ @pytest.fixture(scope="session")
447
+ def notebook_path(tmp_path_factory):
448
+ path = tmp_path_factory.mktemp("notebooks") / "notebook.ipynb"
449
+ yield str(path)
450
+
451
+
452
+ def warmup():
453
+ import ipyvuetify as v
454
+
455
+ # give it a scope so we do not collide with the user's code
456
+ def scoped():
457
+ button = v.Btn(children=["Warmup js/css/fonts", v.Icon(children=["mdi-check"])], class_="solara-warmup-widget")
458
+ container = v.Container(
459
+ children=[button],
460
+ class_="ma-2 snapshot-container",
461
+ )
462
+
463
+ def remove_button(*ignore):
464
+ container.children = []
465
+ container.style_ = "display: none"
466
+
467
+ button.on_event("click", remove_button)
468
+ return container
469
+
470
+ display(scoped())
471
+
472
+
473
+ def create_runner_voila(voila_server, notebook_path, page_session: "playwright.sync_api.Page", require_vuetify_warmup: bool):
474
+ count = 0
475
+ base_url = voila_server.base_url
476
+
477
+ def run(f: Callable, locals={}):
478
+ nonlocal count
479
+ path = Path(f.__code__.co_filename)
480
+ cwd = str(path.parent)
481
+ code_setup = f"""
482
+ import os
483
+ os.chdir({cwd!r})
484
+ \n"""
485
+ for name, value in locals.items():
486
+ code_setup += f"{name} = {value!r}\n"
487
+ if require_vuetify_warmup:
488
+ write_notebook([code_setup, code_from_function(warmup), code_from_function(f)], notebook_path)
489
+ else:
490
+ write_notebook([code_setup, code_from_function(f)], notebook_path)
491
+ page_session.goto(base_url + f"?v={count}")
492
+ if require_vuetify_warmup:
493
+ button = page_session.locator(".solara-warmup-widget")
494
+ button.wait_for()
495
+ page_session.evaluate("document.fonts.ready")
496
+ button.click()
497
+ button.wait_for(state="detached")
498
+ page_session.evaluate("document.fonts.ready")
499
+ count += 1
500
+
501
+ return run
502
+
503
+
504
+ def create_runner_jupyter_lab(jupyter_server, notebook_path, page_session: "playwright.sync_api.Page", require_vuetify_warmup: bool):
505
+ count = 0
506
+ base_url = jupyter_server.base_url
507
+
508
+ def run(f: Callable, locals={}):
509
+ nonlocal count
510
+ path = Path(f.__code__.co_filename)
511
+ cwd = str(path.parent)
512
+ code_setup = f"""
513
+ import os
514
+ os.chdir({cwd!r})
515
+ \n"""
516
+ if require_vuetify_warmup:
517
+ code_setup += """
518
+ import ipyvuetify as v;
519
+ display(v.Btn(children=['Warmup js/css/fonts', v.Icon(children=["mdi-check"])], class_="solara-warmup-widget"))
520
+ \n"""
521
+ for name, value in locals.items():
522
+ code_setup += f"{name} = {value!r}\n"
523
+
524
+ write_notebook([code_setup, code_from_function(f)], notebook_path)
525
+ page_session.goto(base_url + f"/lab/workspaces/solara-test/tree/notebook.ipynb?reset&v={count}")
526
+ page_session.locator('css=[data-command="runmenu:run"]').wait_for()
527
+ # close the file browser tab, it does not give a consistent width each time
528
+ # which leads to fractional pixel x, which causes the screenshot to be different
529
+ # by 1 pixel
530
+ # first make sure the page is loaded
531
+ page_session.locator('css=[data-id="filebrowser"]').wait_for()
532
+ page_session.locator('css=[data-command="filebrowser:create-main-launcher"]').wait_for()
533
+ # close
534
+ page_session.locator('css=[data-id="filebrowser"]').click()
535
+ # make sure it is closed
536
+ page_session.locator('css=[data-command="filebrowser:create-main-launcher"]').wait_for(state="hidden")
537
+
538
+ page_session.locator('button:has-text("No Kernel")').wait_for(state="detached")
539
+ page_session.locator('css=[data-status="idle"]').wait_for()
540
+ page_session.locator('css=[data-command="runmenu:run"]').click()
541
+ if require_vuetify_warmup:
542
+ page_session.locator(".solara-warmup-widget").wait_for()
543
+ page_session.evaluate("document.fonts.ready")
544
+ page_session.locator('css=[data-command="runmenu:run"]').click()
545
+ count += 1
546
+
547
+ return run
548
+
549
+
550
+ def create_runner_jupyter_notebook(jupyter_server, notebook_path, page_session: "playwright.sync_api.Page", require_vuetify_warmup: bool):
551
+ count = 0
552
+ base_url = jupyter_server.base_url
553
+
554
+ def run(f: Callable, locals={}):
555
+ nonlocal count
556
+ path = Path(f.__code__.co_filename)
557
+ cwd = str(path.parent)
558
+ code_setup = f"""
559
+ import os
560
+ os.chdir({cwd!r})
561
+ """
562
+ if require_vuetify_warmup:
563
+ code_setup += """
564
+ import ipyvuetify as v;
565
+ display(v.Btn(children=['Warmup js/css/fonts', v.Icon(children=["mdi-check"])], class_="solara-warmup-widget"))
566
+ \n"""
567
+ for name, value in locals.items():
568
+ code_setup += f"{name} = {value!r}\n"
569
+ write_notebook([code_setup, code_from_function(f)], notebook_path)
570
+ page_session.goto(base_url + f"/notebooks/notebook.ipynb?v={count}")
571
+ page_session.locator(".prompt_container >> nth=0").wait_for()
572
+ page_session.locator("text=Kernel starting, please wait...").wait_for(state="detached")
573
+ page_session.locator("Kernel Ready").wait_for(state="detached")
574
+ page_session.locator('css=[data-jupyter-action="jupyter-notebook:run-cell-and-select-next"]').click()
575
+ if require_vuetify_warmup:
576
+ page_session.locator(".solara-warmup-widget").wait_for()
577
+ page_session.evaluate("document.fonts.ready")
578
+ page_session.locator('css=[data-jupyter-action="jupyter-notebook:run-cell-and-select-next"]').click()
579
+ count += 1
580
+
581
+ return run
582
+
583
+
584
+ @contextlib.contextmanager
585
+ def create_runner_solara(solara_server, solara_app, page_session: "playwright.sync_api.Page", require_vuetify_warmup: bool):
586
+ count = 0
587
+
588
+ def run(f: Callable, locals={}):
589
+ nonlocal count
590
+ from IPython.display import clear_output
591
+
592
+ path = Path(f.__code__.co_filename)
593
+ cwd = str(path.parent)
594
+ current_dir = os.getcwd()
595
+ os.chdir(cwd)
596
+ import sys
597
+
598
+ sys.path.append(cwd)
599
+ try:
600
+ clear_output()
601
+ f()
602
+ finally:
603
+ os.chdir(current_dir)
604
+ sys.path.remove(cwd)
605
+ count += 1
606
+
607
+ with _solara_test(solara_server, solara_app, page_session, require_vuetify_warmup):
608
+ yield run
609
+
610
+
611
+ @pytest.fixture(params=runners)
612
+ def ipywidgets_runner(
613
+ solara_server,
614
+ solara_app,
615
+ voila_server,
616
+ jupyter_server,
617
+ notebook_path,
618
+ page_session: "playwright.sync_api.Page",
619
+ request,
620
+ pytestconfig: Any,
621
+ ):
622
+ runner = request.param
623
+ require_vuetify_warmup = pytestconfig.getoption("solara_vuetify_warmup")
624
+ if runner == "solara":
625
+ with create_runner_solara(solara_server, solara_app, page_session, require_vuetify_warmup) as runner:
626
+ yield runner
627
+ elif runner == "voila":
628
+ yield create_runner_voila(voila_server, notebook_path, page_session, require_vuetify_warmup)
629
+ elif runner == "jupyter_lab":
630
+ yield create_runner_jupyter_lab(jupyter_server, notebook_path, page_session, require_vuetify_warmup)
631
+ elif runner == "jupyter_notebook":
632
+ yield create_runner_jupyter_notebook(jupyter_server, notebook_path, page_session, require_vuetify_warmup)
633
+ else:
634
+ raise RuntimeError(f"Unknown runner {runner}")
635
+
636
+
637
+ @pytest.fixture(scope="session")
638
+ def solara_snapshots_directory(request: Any) -> Path:
639
+ path = Path(request.config.rootpath) / "tests" / "ui" / "snapshots"
640
+ if not path.exists():
641
+ path.mkdir(exist_ok=True, parents=True)
642
+ return path
643
+
644
+
645
+ def compare_default(reference, result, threshold=0.1):
646
+ from PIL import Image
647
+ from pixelmatch.contrib.PIL import pixelmatch
648
+
649
+ difference = Image.new("RGB", reference.size)
650
+ diff = pixelmatch(reference, result, difference, threshold=threshold)
651
+ return diff, difference
652
+
653
+
654
+ @pytest.fixture
655
+ def assert_solara_snapshot(pytestconfig: Any, request: Any, browser_name: str, solara_snapshots_directory) -> Callable:
656
+ from PIL import Image
657
+
658
+ testname = f"{str(Path(request.node.name))}".replace("[", "-").replace("]", "").replace(" ", "-").replace(",", "-")
659
+ directory = solara_snapshots_directory / request.node.location[0]
660
+ output_dir = Path(pytestconfig.getoption("--output")) / request.node.location[0]
661
+ if not directory.exists():
662
+ directory.mkdir(exist_ok=True, parents=True)
663
+ if not output_dir.exists():
664
+ output_dir.mkdir(exist_ok=True, parents=True)
665
+
666
+ def assert_implementation(
667
+ image: bytes,
668
+ compare: Callable = compare_default,
669
+ testname=testname,
670
+ format="{prefix}{testname}-{platform}{postfix}-{type}.png",
671
+ prefix="",
672
+ postfix="",
673
+ ):
674
+ update_snapshot = pytestconfig.getoption("--solara-update-snapshots")
675
+ format_kwargs = dict(testname=testname, platform=sys.platform, browser=browser_name, prefix=prefix, postfix=postfix)
676
+ path_reference = directory / format.format(**format_kwargs, type="reference").format(**format_kwargs)
677
+ path_reference_output = output_dir / format.format(**format_kwargs, type="reference").format(**format_kwargs)
678
+ path_previous = output_dir / format.format(**format_kwargs, type="previous").format(**format_kwargs)
679
+ path_diff = output_dir / format.format(**format_kwargs, type="diff").format(**format_kwargs)
680
+ if not path_reference.exists():
681
+ if update_snapshot:
682
+ path_reference.write_bytes(image)
683
+ else:
684
+ # CI run, store the reference, but fail
685
+ path_reference_output.write_bytes(image)
686
+ raise AssertionError(
687
+ f'Snapshot {path_reference} did not exist, file written. Run `cp "{path_reference_output}" "{path_reference}"` '
688
+ "Commit this file and rerun the CI. Or run with --solara-update-snapshots to update it."
689
+ )
690
+ else:
691
+ if update_snapshot:
692
+ path_reference.write_bytes(image)
693
+ else:
694
+ reference = Image.open(path_reference)
695
+ result = Image.open(BytesIO(image))
696
+ difference = None
697
+
698
+ def write():
699
+ if update_snapshot:
700
+ path_reference.write_bytes(image)
701
+ else:
702
+ # CI run, update the reference in the output dir, and store the previous run next to it
703
+ path_reference_output.write_bytes(image)
704
+ reference.save(path_previous)
705
+ if difference is not None:
706
+ difference.save(path_diff)
707
+
708
+ # the error msg of the default compare is not very helpful
709
+ if reference.size != result.size:
710
+ write()
711
+ raise AssertionError(
712
+ f"Snapshot {path_reference} has a different size than the result {reference.size} != {result.size}."
713
+ f'Run `cp "{path_reference_output}" "{path_reference}"` Commit this file and rerun the CI. '
714
+ "Or run with --solara-update-snapshots to update it."
715
+ )
716
+ diff, difference = compare(reference, result)
717
+ if diff > 0:
718
+ write()
719
+ raise AssertionError(
720
+ f'Snapshot {path_reference} does not match, Run `cp "{path_reference_output}" "{path_reference}"` Commit this file and rerun the CI. '
721
+ "Or run with --solara-update-snapshots to update it."
722
+ )
723
+
724
+ return assert_implementation
725
+
726
+
727
+ def pytest_addoption(parser: Any) -> None:
728
+ group = parser.getgroup("solara", "Solara")
729
+ group.addoption(
730
+ "--solara-update-snapshots",
731
+ action="store_true",
732
+ default=False,
733
+ help="Do not compare, but store the snapshots.",
734
+ )
735
+ group.addoption(
736
+ "--solara-update-snapshots-ci",
737
+ action="store_true",
738
+ default=False,
739
+ help="On compare failure, store to the reference image. Useful for running in CI and downloading the snapshots.",
740
+ )
741
+ group.addoption(
742
+ "--solara-host",
743
+ default=TEST_HOST,
744
+ help="Host or IP all servers will bind to (solara, jupyter, voila)",
745
+ )
746
+ group.addoption(
747
+ "--solara-port",
748
+ type=int,
749
+ default=TEST_PORT_START + 0,
750
+ help="Port the solara server is running on for the test",
751
+ )
752
+ group.addoption(
753
+ "--jupyter-port",
754
+ type=int,
755
+ default=TEST_PORT_START + 1,
756
+ help="Port the jupyter server is running on for the test (for classic notebook and juptyer lab)",
757
+ )
758
+ group.addoption(
759
+ "--voila-port",
760
+ type=int,
761
+ default=TEST_PORT_START + 2,
762
+ help="Port the voila server is running on for the test (for classic notebook and juptyer lab)",
763
+ )
764
+ vuetify_warmup = os.environ.get("SOLARA_TEST_VUETIFY_WARMUP", "true") in ["true", "True", "1", "on", "On"]
765
+ help = (
766
+ "Load/not load the vuetify fonts and css before running the test, leading to more stable screenshots. If (ipy)vuetify is not used this can be disabled."
767
+ )
768
+ if vuetify_warmup:
769
+ group.addoption(
770
+ "--no-solara-vuetify-warmup",
771
+ action="store_false",
772
+ default=vuetify_warmup,
773
+ help=help,
774
+ dest="solara_vuetify_warmup",
775
+ )
776
+ else:
777
+ group.addoption(
778
+ "--solara-vuetify-warmup",
779
+ action="store_false",
780
+ default=vuetify_warmup,
781
+ help=help,
782
+ dest="solara_vuetify_warmup",
783
+ )