solara-ui 1.31.0__py2.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.
- prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- prefix/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara/__init__.py +124 -0
- solara/__main__.py +734 -0
- solara/alias.py +6 -0
- solara/autorouting.py +546 -0
- solara/cache.py +303 -0
- solara/checks.html +71 -0
- solara/checks.py +224 -0
- solara/comm.py +28 -0
- solara/components/__init__.py +59 -0
- solara/components/alert.py +155 -0
- solara/components/applayout.py +393 -0
- solara/components/button.py +85 -0
- solara/components/card.py +87 -0
- solara/components/checkbox.py +50 -0
- solara/components/code_highlight_css.py +11 -0
- solara/components/code_highlight_css.vue +63 -0
- solara/components/columns.py +159 -0
- solara/components/component_vue.py +110 -0
- solara/components/cross_filter.py +335 -0
- solara/components/dataframe.py +546 -0
- solara/components/datatable.py +221 -0
- solara/components/datatable.vue +175 -0
- solara/components/details.py +21 -0
- solara/components/download.vue +35 -0
- solara/components/echarts.py +75 -0
- solara/components/echarts.vue +128 -0
- solara/components/figure_altair.py +39 -0
- solara/components/file_browser.py +182 -0
- solara/components/file_download.py +199 -0
- solara/components/file_drop.py +139 -0
- solara/components/file_drop.vue +83 -0
- solara/components/file_list_widget.vue +78 -0
- solara/components/head.py +27 -0
- solara/components/head_tag.py +49 -0
- solara/components/head_tag.vue +60 -0
- solara/components/image.py +173 -0
- solara/components/input.py +436 -0
- solara/components/link.py +55 -0
- solara/components/markdown.py +378 -0
- solara/components/markdown_editor.py +25 -0
- solara/components/markdown_editor.vue +362 -0
- solara/components/matplotlib.py +74 -0
- solara/components/meta.py +47 -0
- solara/components/misc.py +333 -0
- solara/components/pivot_table.py +258 -0
- solara/components/pivot_table.vue +158 -0
- solara/components/progress.py +47 -0
- solara/components/select.py +182 -0
- solara/components/select.vue +27 -0
- solara/components/slider.py +442 -0
- solara/components/slider_date.vue +56 -0
- solara/components/spinner-solara.vue +105 -0
- solara/components/spinner.py +30 -0
- solara/components/sql_code.py +33 -0
- solara/components/sql_code.vue +128 -0
- solara/components/style.py +105 -0
- solara/components/switch.py +68 -0
- solara/components/tab_navigation.py +37 -0
- solara/components/title.py +90 -0
- solara/components/title.vue +38 -0
- solara/components/togglebuttons.py +200 -0
- solara/components/tooltip.py +61 -0
- solara/datatypes.py +143 -0
- solara/express.py +241 -0
- solara/hooks/__init__.py +4 -0
- solara/hooks/dataframe.py +99 -0
- solara/hooks/misc.py +263 -0
- solara/hooks/use_reactive.py +129 -0
- solara/hooks/use_thread.py +129 -0
- solara/kitchensink.py +8 -0
- solara/lab/__init__.py +34 -0
- solara/lab/components/__init__.py +6 -0
- solara/lab/components/chat.py +203 -0
- solara/lab/components/confirmation_dialog.py +163 -0
- solara/lab/components/cross_filter.py +7 -0
- solara/lab/components/input_date.py +298 -0
- solara/lab/components/menu.py +181 -0
- solara/lab/components/menu.vue +38 -0
- solara/lab/components/tabs.py +274 -0
- solara/lab/components/theming.py +98 -0
- solara/lab/components/theming.vue +72 -0
- solara/lab/hooks/__init__.py +0 -0
- solara/lab/hooks/dataframe.py +12 -0
- solara/lab/toestand.py +3 -0
- solara/lab/utils/__init__.py +2 -0
- solara/lab/utils/cookies.py +5 -0
- solara/lab/utils/dataframe.py +115 -0
- solara/lab/utils/headers.py +5 -0
- solara/layout.py +44 -0
- solara/lifecycle.py +46 -0
- solara/minisettings.py +133 -0
- solara/py.typed +0 -0
- solara/reactive.py +93 -0
- solara/routing.py +268 -0
- solara/scope/__init__.py +88 -0
- solara/scope/types.py +55 -0
- solara/server/__init__.py +0 -0
- solara/server/app.py +491 -0
- solara/server/assets/custom.css +1 -0
- solara/server/assets/custom.js +1 -0
- solara/server/assets/favicon.png +0 -0
- solara/server/assets/favicon.svg +5 -0
- solara/server/assets/style.css +1665 -0
- solara/server/assets/theme-dark.css +437 -0
- solara/server/assets/theme-light.css +420 -0
- solara/server/assets/theme.js +3 -0
- solara/server/cdn_helper.py +77 -0
- solara/server/esm.py +69 -0
- solara/server/fastapi.py +5 -0
- solara/server/flask.py +286 -0
- solara/server/jupyter/__init__.py +2 -0
- solara/server/jupyter/cdn_handler.py +28 -0
- solara/server/jupyter/server_extension.py +29 -0
- solara/server/jupytertools.py +46 -0
- solara/server/kernel.py +338 -0
- solara/server/kernel_context.py +357 -0
- solara/server/patch.py +552 -0
- solara/server/reload.py +242 -0
- solara/server/server.py +456 -0
- solara/server/settings.py +215 -0
- solara/server/shell.py +251 -0
- solara/server/starlette.py +601 -0
- solara/server/static/ansi.js +270 -0
- solara/server/static/highlight-dark.css +82 -0
- solara/server/static/highlight.css +43 -0
- solara/server/static/main-vuetify.js +260 -0
- solara/server/static/main.js +163 -0
- solara/server/static/solara_bootstrap.py +129 -0
- solara/server/static/sun.svg +23 -0
- solara/server/static/webworker.js +42 -0
- solara/server/telemetry.py +212 -0
- solara/server/templates/index.html.j2 +1 -0
- solara/server/templates/loader-plain.css +11 -0
- solara/server/templates/loader-plain.html +20 -0
- solara/server/templates/loader-solara.css +111 -0
- solara/server/templates/loader-solara.html +40 -0
- solara/server/templates/plain.html +82 -0
- solara/server/templates/solara.html.j2 +446 -0
- solara/server/threaded.py +75 -0
- solara/server/utils.py +30 -0
- solara/server/websocket.py +45 -0
- solara/settings.py +56 -0
- solara/tasks.py +837 -0
- solara/template/button.py +16 -0
- solara/template/markdown.py +42 -0
- solara/template/portal/.flake8 +6 -0
- solara/template/portal/.pre-commit-config.yaml +28 -0
- solara/template/portal/LICENSE +21 -0
- solara/template/portal/Procfile +7 -0
- solara/template/portal/mypy.ini +3 -0
- solara/template/portal/pyproject.toml +26 -0
- solara/template/portal/solara_portal/__init__.py +4 -0
- solara/template/portal/solara_portal/components/__init__.py +2 -0
- solara/template/portal/solara_portal/components/article.py +28 -0
- solara/template/portal/solara_portal/components/data.py +28 -0
- solara/template/portal/solara_portal/components/header.py +6 -0
- solara/template/portal/solara_portal/components/layout.py +6 -0
- solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
- solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
- solara/template/portal/solara_portal/data.py +60 -0
- solara/template/portal/solara_portal/pages/__init__.py +67 -0
- solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
- solara/template/portal/solara_portal/pages/tabular.py +29 -0
- solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
- solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
- solara/test/__init__.py +0 -0
- solara/test/pytest_plugin.py +697 -0
- solara/toestand.py +772 -0
- solara/util.py +308 -0
- solara/website/__init__.py +0 -0
- solara/website/assets/custom.css +468 -0
- solara/website/assets/images/logo-small.png +0 -0
- solara/website/assets/images/logo.svg +17 -0
- solara/website/assets/images/logo_white.svg +50 -0
- solara/website/assets/theme.js +8 -0
- solara/website/components/__init__.py +5 -0
- solara/website/components/algolia.vue +24 -0
- solara/website/components/algolia_api.vue +187 -0
- solara/website/components/docs.py +118 -0
- solara/website/components/header.py +72 -0
- solara/website/components/hero.py +15 -0
- solara/website/components/mailchimp.py +12 -0
- solara/website/components/mailchimp.vue +47 -0
- solara/website/components/markdown.py +30 -0
- solara/website/components/notebook.py +171 -0
- solara/website/pages/__init__.py +575 -0
- solara/website/pages/apps/__init__.py +16 -0
- solara/website/pages/apps/authorization/__init__.py +119 -0
- solara/website/pages/apps/authorization/admin.py +12 -0
- solara/website/pages/apps/authorization/users.py +12 -0
- solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
- solara/website/pages/apps/layout-demo.py +40 -0
- solara/website/pages/apps/multipage/__init__.py +38 -0
- solara/website/pages/apps/multipage/page1.py +26 -0
- solara/website/pages/apps/multipage/page2.py +34 -0
- solara/website/pages/apps/scatter.py +136 -0
- solara/website/pages/apps/scrolling.py +63 -0
- solara/website/pages/apps/tutorial-streamlit.py +18 -0
- solara/website/pages/changelog/__init__.py +8 -0
- solara/website/pages/changelog/changelog.md +204 -0
- solara/website/pages/contact/__init__.py +8 -0
- solara/website/pages/contact/contact.md +17 -0
- solara/website/pages/doc_use_download.py +85 -0
- solara/website/pages/documentation/__init__.py +184 -0
- solara/website/pages/documentation/advanced/__init__.py +9 -0
- solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
- solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
- solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
- solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
- solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +162 -0
- solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
- solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +49 -0
- solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
- solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
- solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
- solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +36 -0
- solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
- solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
- solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
- solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
- solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
- solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
- solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
- solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
- solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
- solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
- solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
- solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +7 -0
- solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
- solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
- solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +240 -0
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +97 -0
- solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +171 -0
- solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
- solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
- solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
- solara/website/pages/documentation/api/__init__.py +19 -0
- solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +23 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +22 -0
- solara/website/pages/documentation/api/hooks/__init__.py +9 -0
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +25 -0
- solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
- solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
- solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
- solara/website/pages/documentation/api/hooks/use_exception.py +33 -0
- solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
- solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
- solara/website/pages/documentation/api/hooks/use_previous.py +33 -0
- solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
- solara/website/pages/documentation/api/hooks/use_state.py +10 -0
- solara/website/pages/documentation/api/hooks/use_state_or_update.py +69 -0
- solara/website/pages/documentation/api/hooks/use_thread.md +58 -0
- solara/website/pages/documentation/api/hooks/use_thread.py +44 -0
- solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
- solara/website/pages/documentation/api/routing/__init__.py +9 -0
- solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
- solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
- solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
- solara/website/pages/documentation/api/routing/route.py +31 -0
- solara/website/pages/documentation/api/routing/use_route.py +80 -0
- solara/website/pages/documentation/api/routing/use_router.py +16 -0
- solara/website/pages/documentation/api/utilities/__init__.py +9 -0
- solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
- solara/website/pages/documentation/api/utilities/computed.py +16 -0
- solara/website/pages/documentation/api/utilities/display.py +16 -0
- solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
- solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
- solara/website/pages/documentation/api/utilities/memoize.py +35 -0
- solara/website/pages/documentation/api/utilities/on_kernel_start.py +27 -0
- solara/website/pages/documentation/api/utilities/reactive.py +16 -0
- solara/website/pages/documentation/api/utilities/widget.py +104 -0
- solara/website/pages/documentation/components/__init__.py +12 -0
- solara/website/pages/documentation/components/advanced/__init__.py +9 -0
- solara/website/pages/documentation/components/advanced/link.py +27 -0
- solara/website/pages/documentation/components/advanced/meta.py +20 -0
- solara/website/pages/documentation/components/advanced/style.py +45 -0
- solara/website/pages/documentation/components/common.py +9 -0
- solara/website/pages/documentation/components/data/__init__.py +9 -0
- solara/website/pages/documentation/components/data/dataframe.py +44 -0
- solara/website/pages/documentation/components/data/pivot_table.py +81 -0
- solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
- solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
- solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
- solara/website/pages/documentation/components/input/__init__.py +9 -0
- solara/website/pages/documentation/components/input/button.py +23 -0
- solara/website/pages/documentation/components/input/checkbox.py +10 -0
- solara/website/pages/documentation/components/input/file_browser.py +32 -0
- solara/website/pages/documentation/components/input/file_drop.py +76 -0
- solara/website/pages/documentation/components/input/input.py +19 -0
- solara/website/pages/documentation/components/input/select.py +22 -0
- solara/website/pages/documentation/components/input/slider.py +29 -0
- solara/website/pages/documentation/components/input/switch.py +10 -0
- solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
- solara/website/pages/documentation/components/lab/__init__.py +9 -0
- solara/website/pages/documentation/components/lab/chat.py +109 -0
- solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
- solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
- solara/website/pages/documentation/components/lab/input_date.py +20 -0
- solara/website/pages/documentation/components/lab/menu.py +22 -0
- solara/website/pages/documentation/components/lab/tab.py +25 -0
- solara/website/pages/documentation/components/lab/tabs.py +45 -0
- solara/website/pages/documentation/components/lab/task.py +11 -0
- solara/website/pages/documentation/components/lab/theming.py +72 -0
- solara/website/pages/documentation/components/lab/use_task.py +11 -0
- solara/website/pages/documentation/components/layout/__init__.py +9 -0
- solara/website/pages/documentation/components/layout/app_bar.py +16 -0
- solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
- solara/website/pages/documentation/components/layout/app_layout.py +24 -0
- solara/website/pages/documentation/components/layout/card.py +15 -0
- solara/website/pages/documentation/components/layout/card_actions.py +16 -0
- solara/website/pages/documentation/components/layout/column.py +30 -0
- solara/website/pages/documentation/components/layout/columns.py +27 -0
- solara/website/pages/documentation/components/layout/columns_responsive.py +68 -0
- solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
- solara/website/pages/documentation/components/layout/gridfixed.py +21 -0
- solara/website/pages/documentation/components/layout/hbox.py +18 -0
- solara/website/pages/documentation/components/layout/row.py +30 -0
- solara/website/pages/documentation/components/layout/sidebar.py +24 -0
- solara/website/pages/documentation/components/layout/vbox.py +19 -0
- solara/website/pages/documentation/components/output/__init__.py +9 -0
- solara/website/pages/documentation/components/output/file_download.py +11 -0
- solara/website/pages/documentation/components/output/html.py +21 -0
- solara/website/pages/documentation/components/output/image.py +11 -0
- solara/website/pages/documentation/components/output/markdown.py +57 -0
- solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
- solara/website/pages/documentation/components/output/sql_code.py +85 -0
- solara/website/pages/documentation/components/output/tooltip.py +11 -0
- solara/website/pages/documentation/components/page/__init__.py +9 -0
- solara/website/pages/documentation/components/page/head.py +18 -0
- solara/website/pages/documentation/components/page/title.py +27 -0
- solara/website/pages/documentation/components/status/__init__.py +9 -0
- solara/website/pages/documentation/components/status/error.py +40 -0
- solara/website/pages/documentation/components/status/info.py +40 -0
- solara/website/pages/documentation/components/status/progress.py +10 -0
- solara/website/pages/documentation/components/status/spinner.py +11 -0
- solara/website/pages/documentation/components/status/success.py +40 -0
- solara/website/pages/documentation/components/status/warning.py +47 -0
- solara/website/pages/documentation/components/viz/__init__.py +9 -0
- solara/website/pages/documentation/components/viz/altair.py +42 -0
- solara/website/pages/documentation/components/viz/echarts.py +75 -0
- solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
- solara/website/pages/documentation/components/viz/plotly.py +63 -0
- solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
- solara/website/pages/documentation/examples/__init__.py +52 -0
- solara/website/pages/documentation/examples/ai/__init__.py +11 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +95 -0
- solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
- solara/website/pages/documentation/examples/basics/__init__.py +10 -0
- solara/website/pages/documentation/examples/basics/sine.py +28 -0
- solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
- solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
- solara/website/pages/documentation/examples/general/__init__.py +10 -0
- solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
- solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
- solara/website/pages/documentation/examples/general/live_update.py +38 -0
- solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
- solara/website/pages/documentation/examples/general/mycard.vue +58 -0
- solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
- solara/website/pages/documentation/examples/general/vue_component.py +50 -0
- solara/website/pages/documentation/examples/ipycanvas.py +49 -0
- solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
- solara/website/pages/documentation/examples/libraries/altair.py +64 -0
- solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
- solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
- solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
- solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
- solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
- solara/website/pages/documentation/examples/utilities/countdown_timer.py +64 -0
- solara/website/pages/documentation/examples/utilities/todo.py +196 -0
- solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
- solara/website/pages/documentation/examples/visualization/annotator.py +69 -0
- solara/website/pages/documentation/examples/visualization/linked_views.py +84 -0
- solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
- solara/website/pages/documentation/faq/__init__.py +12 -0
- solara/website/pages/documentation/faq/content/99-faq.md +76 -0
- solara/website/pages/documentation/getting_started/__init__.py +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +89 -0
- solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
- solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +64 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1000 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +223 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +88 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +273 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
- solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
- solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
- solara/website/pages/docutils.py +38 -0
- solara/website/pages/showcase/__init__.py +105 -0
- solara/website/pages/showcase/domino_code_assist.py +60 -0
- solara/website/pages/showcase/planeto_tessa.py +19 -0
- solara/website/pages/showcase/solara_dev.py +54 -0
- solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
- solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
- solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
- solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
- solara/website/pages/showcase/wanderlust.py +27 -0
- solara/website/public/beach.jpeg +0 -0
- solara/website/public/logo.svg +6 -0
- solara/website/public/social/discord.svg +1 -0
- solara/website/public/social/github.svg +1 -0
- solara/website/public/social/twitter.svg +3 -0
- solara/website/public/success.html +25 -0
- solara/website/templates/index.html.j2 +117 -0
- solara/website/utils.py +51 -0
- solara/widgets/__init__.py +1 -0
- solara/widgets/vue/gridlayout.vue +110 -0
- solara/widgets/vue/html.vue +4 -0
- solara/widgets/vue/navigator.vue +104 -0
- solara/widgets/vue/vegalite.vue +115 -0
- solara/widgets/widgets.py +66 -0
- solara_ui-1.31.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- solara_ui-1.31.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara_ui-1.31.0.dist-info/METADATA +158 -0
- solara_ui-1.31.0.dist-info/RECORD +439 -0
- solara_ui-1.31.0.dist-info/WHEEL +5 -0
- solara_ui-1.31.0.dist-info/licenses/LICENSE +21 -0
solara/toestand.py
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import dataclasses
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import warnings
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from operator import getitem
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
ContextManager,
|
|
14
|
+
Dict,
|
|
15
|
+
Generic,
|
|
16
|
+
Optional,
|
|
17
|
+
Set,
|
|
18
|
+
Tuple,
|
|
19
|
+
TypeVar,
|
|
20
|
+
Union,
|
|
21
|
+
cast,
|
|
22
|
+
overload,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
import react_ipywidgets as react
|
|
26
|
+
import reacton.core
|
|
27
|
+
from reacton.utils import equals
|
|
28
|
+
|
|
29
|
+
import solara
|
|
30
|
+
from solara import _using_solara_server
|
|
31
|
+
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
TS = TypeVar("TS")
|
|
34
|
+
S = TypeVar("S") # used for state
|
|
35
|
+
logger = logging.getLogger("solara.toestand")
|
|
36
|
+
|
|
37
|
+
_DEBUG = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThreadLocal(threading.local):
|
|
41
|
+
reactive_used: Optional[Set["ValueBase"]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
thread_local = ThreadLocal()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# these hooks should go into react-ipywidgets
|
|
48
|
+
def use_sync_external_store(subscribe: Callable[[Callable[[], None]], Callable[[], None]], get_snapshot: Callable[[], Any]):
|
|
49
|
+
_, set_counter = react.use_state(0)
|
|
50
|
+
|
|
51
|
+
def force_update():
|
|
52
|
+
set_counter(lambda x: x + 1)
|
|
53
|
+
|
|
54
|
+
state = get_snapshot()
|
|
55
|
+
prev_state = react.use_ref(state)
|
|
56
|
+
|
|
57
|
+
def update_state():
|
|
58
|
+
prev_state.current = state
|
|
59
|
+
|
|
60
|
+
react.use_effect(update_state)
|
|
61
|
+
|
|
62
|
+
def on_store_change(_ignore_new_state=None):
|
|
63
|
+
new_state = get_snapshot()
|
|
64
|
+
if not equals(new_state, prev_state.current):
|
|
65
|
+
prev_state.current = new_state
|
|
66
|
+
force_update()
|
|
67
|
+
|
|
68
|
+
react.use_effect(lambda: subscribe(on_store_change), [])
|
|
69
|
+
return state
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def use_sync_external_store_with_selector(subscribe, get_snapshot: Callable[[], Any], selector):
|
|
73
|
+
return use_sync_external_store(subscribe, lambda: selector(get_snapshot()))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def merge_state(d1: S, **kwargs) -> S:
|
|
77
|
+
if dataclasses.is_dataclass(d1):
|
|
78
|
+
return dataclasses.replace(d1, **kwargs) # type: ignore
|
|
79
|
+
if "pydantic" in sys.modules and isinstance(d1, sys.modules["pydantic"].BaseModel):
|
|
80
|
+
return type(d1)(**{**d1.dict(), **kwargs}) # type: ignore
|
|
81
|
+
return cast(S, {**cast(dict, d1), **kwargs})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ValueBase(Generic[T]):
|
|
85
|
+
def __init__(self, merge: Callable = merge_state):
|
|
86
|
+
self.merge = merge
|
|
87
|
+
self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
88
|
+
self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def lock(self):
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def value(self) -> T:
|
|
96
|
+
return self.get()
|
|
97
|
+
|
|
98
|
+
@value.setter
|
|
99
|
+
def value(self, value: T):
|
|
100
|
+
self.set(value)
|
|
101
|
+
|
|
102
|
+
def set(self, value: T):
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
def peek(self) -> T:
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def get(self) -> T:
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
def _get_scope_key(self):
|
|
112
|
+
raise NotImplementedError
|
|
113
|
+
|
|
114
|
+
def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None):
|
|
115
|
+
scope_id = self._get_scope_key()
|
|
116
|
+
self.listeners[scope_id].add((listener, scope))
|
|
117
|
+
|
|
118
|
+
def cleanup():
|
|
119
|
+
self.listeners[scope_id].remove((listener, scope))
|
|
120
|
+
|
|
121
|
+
return cleanup
|
|
122
|
+
|
|
123
|
+
def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None):
|
|
124
|
+
scope_id = self._get_scope_key()
|
|
125
|
+
self.listeners2[scope_id].add((listener, scope))
|
|
126
|
+
|
|
127
|
+
def cleanup():
|
|
128
|
+
self.listeners2[scope_id].remove((listener, scope))
|
|
129
|
+
|
|
130
|
+
return cleanup
|
|
131
|
+
|
|
132
|
+
def fire(self, new: T, old: T):
|
|
133
|
+
logger.info("value change from %s to %s, will fire events", old, new)
|
|
134
|
+
scope_id = self._get_scope_key()
|
|
135
|
+
scopes = set()
|
|
136
|
+
for listener, scope in self.listeners[scope_id].copy():
|
|
137
|
+
if scope is not None:
|
|
138
|
+
scopes.add(scope)
|
|
139
|
+
for listener2, scope in self.listeners2[scope_id].copy():
|
|
140
|
+
if scope is not None:
|
|
141
|
+
scopes.add(scope)
|
|
142
|
+
stack = contextlib.ExitStack()
|
|
143
|
+
with contextlib.ExitStack() as stack:
|
|
144
|
+
for scope in scopes:
|
|
145
|
+
stack.enter_context(scope)
|
|
146
|
+
for listener, scope in self.listeners[scope_id].copy():
|
|
147
|
+
listener(new)
|
|
148
|
+
for listener2, scope in self.listeners2[scope_id].copy():
|
|
149
|
+
listener2(new, old)
|
|
150
|
+
|
|
151
|
+
def update(self, _f=None, **kwargs):
|
|
152
|
+
if _f is not None:
|
|
153
|
+
assert not kwargs
|
|
154
|
+
with self.lock:
|
|
155
|
+
kwargs = _f(self.get())
|
|
156
|
+
with self.lock:
|
|
157
|
+
# important to have this part thread-safe
|
|
158
|
+
new = self.merge(self.get(), **kwargs)
|
|
159
|
+
self.set(new)
|
|
160
|
+
|
|
161
|
+
def use_value(self) -> T:
|
|
162
|
+
# .use with the default argument doesn't give good type inference
|
|
163
|
+
return self.use()
|
|
164
|
+
|
|
165
|
+
def use(self, selector: Callable[[T], TS] = lambda x: x) -> TS: # type: ignore
|
|
166
|
+
return selector(self.value)
|
|
167
|
+
|
|
168
|
+
def use_state(self) -> Tuple[T, Callable[[T], None]]:
|
|
169
|
+
setter = self.set
|
|
170
|
+
value = self.use() # type: ignore
|
|
171
|
+
return value, setter
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def fields(self) -> T:
|
|
175
|
+
# we lie about the return type, but in combination with
|
|
176
|
+
# setter we can make type safe setters (see docs/tests)
|
|
177
|
+
return cast(T, Fields(self))
|
|
178
|
+
|
|
179
|
+
def setter(self, field: TS) -> Callable[[TS], None]:
|
|
180
|
+
_field = cast(FieldBase, field)
|
|
181
|
+
|
|
182
|
+
def setter(new_value: TS):
|
|
183
|
+
_field.set(new_value)
|
|
184
|
+
|
|
185
|
+
return cast(Callable[[TS], None], setter)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# the default store for now, stores in a global dict, or when in a solara
|
|
189
|
+
# context, in the solara user context
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class KernelStore(ValueBase[S], ABC):
|
|
193
|
+
_global_dict: Dict[str, S] = {} # outside of solara context, this is used
|
|
194
|
+
# we keep a counter per type, so the storage keys we generate are deterministic
|
|
195
|
+
_type_counter: Dict[Any, int] = defaultdict(int)
|
|
196
|
+
scope_lock = threading.RLock()
|
|
197
|
+
|
|
198
|
+
def __init__(self, key=None):
|
|
199
|
+
super().__init__()
|
|
200
|
+
self.storage_key = key
|
|
201
|
+
self._global_dict = {}
|
|
202
|
+
# since a set can trigger events, which can trigger new updates, we need a recursive lock
|
|
203
|
+
self._lock = threading.RLock()
|
|
204
|
+
self.local = threading.local()
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def lock(self):
|
|
208
|
+
return self._lock
|
|
209
|
+
|
|
210
|
+
def _get_scope_key(self):
|
|
211
|
+
scope_dict, scope_id = self._get_dict()
|
|
212
|
+
return scope_id
|
|
213
|
+
|
|
214
|
+
def _get_dict(self):
|
|
215
|
+
scope_dict = self._global_dict
|
|
216
|
+
scope_id = "global"
|
|
217
|
+
if _using_solara_server():
|
|
218
|
+
import solara.server.kernel_context
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
context = solara.server.kernel_context.get_current_context()
|
|
222
|
+
except RuntimeError: # noqa
|
|
223
|
+
pass # do we need to be more strict?
|
|
224
|
+
else:
|
|
225
|
+
scope_dict = cast(Dict[str, S], context.user_dicts)
|
|
226
|
+
scope_id = context.id
|
|
227
|
+
return cast(Dict[str, S], scope_dict), scope_id
|
|
228
|
+
|
|
229
|
+
def peek(self):
|
|
230
|
+
return self.get()
|
|
231
|
+
|
|
232
|
+
def get(self):
|
|
233
|
+
scope_dict, scope_id = self._get_dict()
|
|
234
|
+
if self.storage_key not in scope_dict:
|
|
235
|
+
with self.scope_lock:
|
|
236
|
+
if self.storage_key not in scope_dict:
|
|
237
|
+
# we assume immutable, so don't make a copy
|
|
238
|
+
scope_dict[self.storage_key] = self.initial_value()
|
|
239
|
+
return scope_dict[self.storage_key]
|
|
240
|
+
|
|
241
|
+
def clear(self):
|
|
242
|
+
scope_dict, scope_id = self._get_dict()
|
|
243
|
+
if self.storage_key in scope_dict:
|
|
244
|
+
del scope_dict[self.storage_key]
|
|
245
|
+
|
|
246
|
+
def set(self, value: S):
|
|
247
|
+
scope_dict, scope_id = self._get_dict()
|
|
248
|
+
old = self.get()
|
|
249
|
+
if equals(old, value):
|
|
250
|
+
return
|
|
251
|
+
scope_dict[self.storage_key] = value
|
|
252
|
+
|
|
253
|
+
if _DEBUG:
|
|
254
|
+
import traceback
|
|
255
|
+
|
|
256
|
+
traceback.print_stack(limit=17, file=sys.stdout)
|
|
257
|
+
|
|
258
|
+
print("change old", old) # noqa
|
|
259
|
+
print("change new", value) # noqa
|
|
260
|
+
|
|
261
|
+
self.fire(value, old)
|
|
262
|
+
|
|
263
|
+
@abstractmethod
|
|
264
|
+
def initial_value(self) -> S:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class KernelStoreValue(KernelStore[S]):
|
|
269
|
+
default_value: S
|
|
270
|
+
|
|
271
|
+
def __init__(self, default_value: S, key=None):
|
|
272
|
+
self.default_value = default_value
|
|
273
|
+
cls = type(default_value)
|
|
274
|
+
if key is None:
|
|
275
|
+
with KernelStoreValue.scope_lock:
|
|
276
|
+
index = self._type_counter[cls]
|
|
277
|
+
self._type_counter[cls] += 1
|
|
278
|
+
key = cls.__module__ + ":" + cls.__name__ + ":" + str(index)
|
|
279
|
+
super().__init__(key=key)
|
|
280
|
+
|
|
281
|
+
def initial_value(self) -> S:
|
|
282
|
+
return self.default_value
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class KernelStoreFactory(KernelStore[S]):
|
|
286
|
+
def __init__(self, factory: Callable[[], S], key=None):
|
|
287
|
+
self.factory = factory
|
|
288
|
+
try:
|
|
289
|
+
prefix = factory.__qualname__
|
|
290
|
+
except Exception:
|
|
291
|
+
prefix = repr(factory)
|
|
292
|
+
if key is None:
|
|
293
|
+
with KernelStore.scope_lock:
|
|
294
|
+
index = self._type_counter[prefix]
|
|
295
|
+
self._type_counter[prefix] += 1
|
|
296
|
+
try:
|
|
297
|
+
key = factory.__module__ + ":" + prefix + ":" + str(index)
|
|
298
|
+
except Exception:
|
|
299
|
+
key = prefix + ":" + str(index)
|
|
300
|
+
super().__init__(key=key)
|
|
301
|
+
|
|
302
|
+
def initial_value(self) -> S:
|
|
303
|
+
return self.factory()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Reactive(ValueBase[S]):
|
|
307
|
+
_storage: ValueBase[S]
|
|
308
|
+
|
|
309
|
+
def __init__(self, default_value: Union[S, ValueBase[S]], key=None):
|
|
310
|
+
super().__init__()
|
|
311
|
+
if not isinstance(default_value, ValueBase):
|
|
312
|
+
self._storage = KernelStoreValue(default_value, key=key)
|
|
313
|
+
else:
|
|
314
|
+
self._storage = default_value
|
|
315
|
+
self.__post__init__()
|
|
316
|
+
self._name = None
|
|
317
|
+
self._owner = None
|
|
318
|
+
|
|
319
|
+
def __set_name__(self, owner, name):
|
|
320
|
+
self._name = name
|
|
321
|
+
self._owner = owner
|
|
322
|
+
|
|
323
|
+
def __repr__(self):
|
|
324
|
+
value = self.peek()
|
|
325
|
+
if self._name:
|
|
326
|
+
return f"<Reactive {self._owner.__name__}.{self._name} value={value!r} id={hex(id(self))}>"
|
|
327
|
+
else:
|
|
328
|
+
return f"<Reactive value={value!r} id={hex(id(self))}>"
|
|
329
|
+
|
|
330
|
+
def __str__(self):
|
|
331
|
+
if self._name:
|
|
332
|
+
return f"{self._owner.__name__}.{self._name}={self.value!r}"
|
|
333
|
+
else:
|
|
334
|
+
return f"{self.value!r}"
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def lock(self):
|
|
338
|
+
return self._storage.lock
|
|
339
|
+
|
|
340
|
+
def __post__init__(self):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
def update(self, *args, **kwargs):
|
|
344
|
+
self._storage.update(*args, **kwargs)
|
|
345
|
+
|
|
346
|
+
def set(self, value: S):
|
|
347
|
+
if value is self:
|
|
348
|
+
raise ValueError("Can't set a reactive to itself")
|
|
349
|
+
self._storage.set(value)
|
|
350
|
+
|
|
351
|
+
def get(self, add_watch=None) -> S:
|
|
352
|
+
if add_watch is not None:
|
|
353
|
+
warnings.warn("add_watch is deprecated, use .peek()", DeprecationWarning)
|
|
354
|
+
if thread_local.reactive_used is not None:
|
|
355
|
+
thread_local.reactive_used.add(self)
|
|
356
|
+
# peek to avoid parents also adding themselves to the reactive_used set
|
|
357
|
+
return self._storage.peek()
|
|
358
|
+
|
|
359
|
+
def peek(self) -> S:
|
|
360
|
+
"""Return the value without automatically subscribing to listeners."""
|
|
361
|
+
return self._storage.peek()
|
|
362
|
+
|
|
363
|
+
def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
|
|
364
|
+
return self._storage.subscribe(listener, scope=scope)
|
|
365
|
+
|
|
366
|
+
def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
|
|
367
|
+
return self._storage.subscribe_change(listener, scope=scope)
|
|
368
|
+
|
|
369
|
+
def computed(self, f: Callable[[S], T]) -> "Computed[T]":
|
|
370
|
+
def func():
|
|
371
|
+
return f(self.get())
|
|
372
|
+
|
|
373
|
+
return Computed(func, key=f.__qualname__)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class Singleton(Reactive[S]):
|
|
377
|
+
_storage: KernelStore[S]
|
|
378
|
+
|
|
379
|
+
def __init__(self, factory: Callable[[], S], key=None):
|
|
380
|
+
import solara.lifecycle
|
|
381
|
+
|
|
382
|
+
super().__init__(KernelStoreFactory(factory, key=key))
|
|
383
|
+
|
|
384
|
+
# reset on kernel restart (e.g. hot reload)
|
|
385
|
+
def reset():
|
|
386
|
+
def cleanup():
|
|
387
|
+
self._storage.clear()
|
|
388
|
+
|
|
389
|
+
return cleanup
|
|
390
|
+
|
|
391
|
+
solara.lifecycle.on_kernel_start(reset)
|
|
392
|
+
|
|
393
|
+
def __set__(self, obj, value):
|
|
394
|
+
raise AttributeError("Can't set a singleton")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class Computed(Reactive[S]):
|
|
398
|
+
_storage: KernelStore[S]
|
|
399
|
+
|
|
400
|
+
def __init__(self, f: Callable[[], S], key=None):
|
|
401
|
+
import solara.lifecycle
|
|
402
|
+
|
|
403
|
+
self.f = f
|
|
404
|
+
|
|
405
|
+
def on_change(*ignore):
|
|
406
|
+
with self._auto_subscriber.value:
|
|
407
|
+
self.set(f())
|
|
408
|
+
|
|
409
|
+
import functools
|
|
410
|
+
|
|
411
|
+
self._auto_subscriber = Singleton(functools.wraps(AutoSubscribeContextManager)(lambda: AutoSubscribeContextManager(on_change)))
|
|
412
|
+
|
|
413
|
+
@functools.wraps(f)
|
|
414
|
+
def factory():
|
|
415
|
+
v = self._auto_subscriber.value
|
|
416
|
+
with v:
|
|
417
|
+
return f()
|
|
418
|
+
|
|
419
|
+
super().__init__(KernelStoreFactory(factory, key=key))
|
|
420
|
+
|
|
421
|
+
# reset on kernel restart (e.g. hot reload)
|
|
422
|
+
def reset():
|
|
423
|
+
def cleanup():
|
|
424
|
+
self._storage.clear()
|
|
425
|
+
|
|
426
|
+
return cleanup
|
|
427
|
+
|
|
428
|
+
solara.lifecycle.on_kernel_start(reset)
|
|
429
|
+
|
|
430
|
+
def __repr__(self):
|
|
431
|
+
value = super().__repr__()
|
|
432
|
+
return "<Computed" + value[len("<Reactive") : -1]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@overload
|
|
436
|
+
def computed(
|
|
437
|
+
f: None,
|
|
438
|
+
*,
|
|
439
|
+
key: Optional[str] = ...,
|
|
440
|
+
) -> Callable[[Callable[[], T]], Reactive[T]]: ...
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@overload
|
|
444
|
+
def computed(
|
|
445
|
+
f: Callable[[], T],
|
|
446
|
+
*,
|
|
447
|
+
key: Optional[str] = ...,
|
|
448
|
+
) -> Reactive[T]: ...
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def computed(
|
|
452
|
+
f: Union[None, Callable[[], T]],
|
|
453
|
+
*,
|
|
454
|
+
key: Optional[str] = None,
|
|
455
|
+
) -> Union[Callable[[Callable[[], T]], Reactive[T]], Reactive[T]]:
|
|
456
|
+
"""Creates a reactive variable that is set to the return value of the function.
|
|
457
|
+
|
|
458
|
+
The value will be updated when any of the reactive variables used in the function
|
|
459
|
+
change.
|
|
460
|
+
|
|
461
|
+
## Example
|
|
462
|
+
|
|
463
|
+
```solara
|
|
464
|
+
import solara
|
|
465
|
+
import solara.lab
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
a = solara.reactive(1)
|
|
469
|
+
b = solara.reactive(2)
|
|
470
|
+
|
|
471
|
+
@solara.lab.computed
|
|
472
|
+
def total():
|
|
473
|
+
return a.value + b.value
|
|
474
|
+
|
|
475
|
+
def reset():
|
|
476
|
+
a.value = 1
|
|
477
|
+
b.value = 2
|
|
478
|
+
|
|
479
|
+
@solara.component
|
|
480
|
+
def Page():
|
|
481
|
+
print(a, b, total)
|
|
482
|
+
solara.IntSlider("a", value=a)
|
|
483
|
+
solara.IntSlider("b", value=b)
|
|
484
|
+
solara.Text(f"a + b = {total.value}")
|
|
485
|
+
solara.Button("reset", on_click=reset)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def wrapper(f: Callable[[], T]):
|
|
491
|
+
return Computed(f, key=key)
|
|
492
|
+
|
|
493
|
+
if f is None:
|
|
494
|
+
return wrapper
|
|
495
|
+
else:
|
|
496
|
+
return wrapper(f)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class ReactiveField(Reactive[T]):
|
|
500
|
+
def __init__(self, field: "FieldBase"):
|
|
501
|
+
# super().__init__() # type: ignore
|
|
502
|
+
# We skip the Reactive constructor, because we do not need it, but we do
|
|
503
|
+
# want to be an instanceof for use in use_reactive
|
|
504
|
+
ValueBase.__init__(self)
|
|
505
|
+
self._field = field
|
|
506
|
+
field = field
|
|
507
|
+
while not isinstance(field, ValueBase):
|
|
508
|
+
field = field._parent
|
|
509
|
+
self._root = field
|
|
510
|
+
assert isinstance(self._root, ValueBase)
|
|
511
|
+
|
|
512
|
+
def __str__(self):
|
|
513
|
+
return str(self._field)
|
|
514
|
+
|
|
515
|
+
def __repr__(self):
|
|
516
|
+
return f"<Reactive field {self._field}>"
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def lock(self):
|
|
520
|
+
return self._root.lock
|
|
521
|
+
|
|
522
|
+
def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None):
|
|
523
|
+
def on_change(new, old):
|
|
524
|
+
try:
|
|
525
|
+
new_value = self._field.get(new)
|
|
526
|
+
except IndexError:
|
|
527
|
+
return # the current design choice to silently drop the update message
|
|
528
|
+
except KeyError:
|
|
529
|
+
return # same
|
|
530
|
+
old_value = self._field.get(old)
|
|
531
|
+
if not equals(new_value, old_value):
|
|
532
|
+
listener(new_value)
|
|
533
|
+
|
|
534
|
+
return self._root.subscribe_change(on_change, scope=scope)
|
|
535
|
+
|
|
536
|
+
def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None):
|
|
537
|
+
def on_change(new, old):
|
|
538
|
+
try:
|
|
539
|
+
new_value = self._field.get(new)
|
|
540
|
+
except IndexError:
|
|
541
|
+
return # see subscribe
|
|
542
|
+
except KeyError:
|
|
543
|
+
return # see subscribe
|
|
544
|
+
old_value = self._field.get(old)
|
|
545
|
+
if not equals(new_value, old_value):
|
|
546
|
+
listener(new_value, old_value)
|
|
547
|
+
|
|
548
|
+
return self._root.subscribe_change(on_change, scope=scope)
|
|
549
|
+
|
|
550
|
+
def get(self, add_watch=None) -> T:
|
|
551
|
+
if add_watch is not None:
|
|
552
|
+
warnings.warn("add_watch is deprecated, use .peek()", DeprecationWarning)
|
|
553
|
+
if thread_local.reactive_used is not None:
|
|
554
|
+
thread_local.reactive_used.add(self)
|
|
555
|
+
# peek to avoid parents also adding themselves to the reactive_used set
|
|
556
|
+
return self._field.peek()
|
|
557
|
+
|
|
558
|
+
def peek(self) -> T:
|
|
559
|
+
return self._field.peek()
|
|
560
|
+
|
|
561
|
+
def set(self, value: T):
|
|
562
|
+
self._field.set(value)
|
|
563
|
+
|
|
564
|
+
def update(self, *args, **kwargs):
|
|
565
|
+
ValueBase.update(cast(ValueBase, self), *args, **kwargs)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def Ref(field: T) -> Reactive[T]:
|
|
569
|
+
_field = cast(FieldBase, field)
|
|
570
|
+
return cast(Reactive[T], ReactiveField[T](_field))
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class FieldBase:
|
|
574
|
+
_parent: Any
|
|
575
|
+
|
|
576
|
+
def __getattr__(self, key):
|
|
577
|
+
if key in ["_parent", "set", "_lock"] or key.startswith("__"):
|
|
578
|
+
return self.__dict__[key]
|
|
579
|
+
return FieldAttr(self, key)
|
|
580
|
+
|
|
581
|
+
def __getitem__(self, key):
|
|
582
|
+
return FieldItem(self, key)
|
|
583
|
+
|
|
584
|
+
def get(self, obj=None):
|
|
585
|
+
raise NotImplementedError
|
|
586
|
+
|
|
587
|
+
def set(self, value):
|
|
588
|
+
raise NotImplementedError
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class Fields(FieldBase):
|
|
592
|
+
def __init__(self, state: ValueBase):
|
|
593
|
+
self._parent = state
|
|
594
|
+
self._lock = state.lock
|
|
595
|
+
|
|
596
|
+
def get(self, obj=None):
|
|
597
|
+
# we are at the root, so override the object
|
|
598
|
+
# so we can get the 'old' value
|
|
599
|
+
if obj is not None:
|
|
600
|
+
return obj
|
|
601
|
+
return self._parent.get()
|
|
602
|
+
|
|
603
|
+
def peek(self, obj=None):
|
|
604
|
+
# we are at the root, so override the object
|
|
605
|
+
# so we can get the 'old' value
|
|
606
|
+
if obj is not None:
|
|
607
|
+
return obj
|
|
608
|
+
return self._parent.peek()
|
|
609
|
+
|
|
610
|
+
def set(self, value):
|
|
611
|
+
self._parent.set(value)
|
|
612
|
+
|
|
613
|
+
def __repr__(self):
|
|
614
|
+
return repr(self._parent)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class FieldAttr(FieldBase):
|
|
618
|
+
def __init__(self, parent, key: str):
|
|
619
|
+
self._parent = parent
|
|
620
|
+
self.key = key
|
|
621
|
+
self._lock = parent._lock
|
|
622
|
+
|
|
623
|
+
def get(self, obj=None):
|
|
624
|
+
obj = self._parent.get(obj)
|
|
625
|
+
return getattr(obj, self.key)
|
|
626
|
+
|
|
627
|
+
def peek(self, obj=None):
|
|
628
|
+
obj = self._parent.peek(obj)
|
|
629
|
+
return getattr(obj, self.key)
|
|
630
|
+
|
|
631
|
+
def set(self, value):
|
|
632
|
+
with self._lock:
|
|
633
|
+
parent_value = self._parent.peek()
|
|
634
|
+
if isinstance(self.key, str):
|
|
635
|
+
parent_value = merge_state(parent_value, **{self.key: value})
|
|
636
|
+
self._parent.set(parent_value)
|
|
637
|
+
else:
|
|
638
|
+
raise TypeError(f"Type of key {self.key!r} is not supported")
|
|
639
|
+
|
|
640
|
+
def __str__(self):
|
|
641
|
+
return f".{self.key}"
|
|
642
|
+
|
|
643
|
+
def __repr__(self):
|
|
644
|
+
return f"<Field {self._parent}{self}>"
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class FieldItem(FieldBase):
|
|
648
|
+
def __init__(self, parent, key: str):
|
|
649
|
+
self._parent = parent
|
|
650
|
+
self.key = key
|
|
651
|
+
self._lock = parent._lock
|
|
652
|
+
|
|
653
|
+
def get(self, obj=None):
|
|
654
|
+
obj = self._parent.get(obj)
|
|
655
|
+
return getitem(obj, self.key)
|
|
656
|
+
|
|
657
|
+
def peek(self, obj=None):
|
|
658
|
+
obj = self._parent.peek(obj)
|
|
659
|
+
return getitem(obj, self.key)
|
|
660
|
+
|
|
661
|
+
def set(self, value):
|
|
662
|
+
with self._lock:
|
|
663
|
+
parent_value = self._parent.peek()
|
|
664
|
+
if isinstance(self.key, int) and isinstance(parent_value, (list, tuple)):
|
|
665
|
+
parent_type = type(parent_value)
|
|
666
|
+
parent_value = parent_value.copy() # type: ignore
|
|
667
|
+
parent_value[self.key] = value
|
|
668
|
+
self._parent.set(parent_type(parent_value))
|
|
669
|
+
else:
|
|
670
|
+
parent_value = merge_state(parent_value, **{self.key: value})
|
|
671
|
+
self._parent.set(parent_value)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class AutoSubscribeContextManagerBase:
|
|
675
|
+
# a render loop might trigger a new render loop of a differtent render context
|
|
676
|
+
# so we want to save, and restore the current reactive_used
|
|
677
|
+
reactive_used: Optional[Set[ValueBase]] = None
|
|
678
|
+
reactive_added_previous_run: Optional[Set[ValueBase]] = None
|
|
679
|
+
subscribed: Dict[ValueBase, Callable]
|
|
680
|
+
|
|
681
|
+
def __init__(self):
|
|
682
|
+
self.subscribed = {}
|
|
683
|
+
|
|
684
|
+
def update_subscribers(self, change_handler, scope=None):
|
|
685
|
+
assert self.reactive_used is not None
|
|
686
|
+
reactive_used = self.reactive_used
|
|
687
|
+
# remove subfields for which we already listen to it's root reactive value
|
|
688
|
+
reactive_used_subfields = {k for k in reactive_used if isinstance(k, ValueSubField)}
|
|
689
|
+
reactive_used = reactive_used - reactive_used_subfields
|
|
690
|
+
# only add subfield for which we don't listen to it's parent
|
|
691
|
+
for reactive_used_subfield in reactive_used_subfields:
|
|
692
|
+
if reactive_used_subfield._root not in reactive_used:
|
|
693
|
+
reactive_used.add(reactive_used_subfield)
|
|
694
|
+
added = reactive_used - (self.reactive_added_previous_run or set())
|
|
695
|
+
|
|
696
|
+
removed = (self.reactive_added_previous_run or set()) - reactive_used
|
|
697
|
+
|
|
698
|
+
for reactive in added:
|
|
699
|
+
if reactive not in self.subscribed:
|
|
700
|
+
unsubscribe = reactive.subscribe_change(change_handler, scope=scope)
|
|
701
|
+
self.subscribed[reactive] = unsubscribe
|
|
702
|
+
for reactive in removed:
|
|
703
|
+
unsubscribe = self.subscribed[reactive]
|
|
704
|
+
unsubscribe()
|
|
705
|
+
del self.subscribed[reactive]
|
|
706
|
+
self.reactive_added_previous_run = added
|
|
707
|
+
|
|
708
|
+
def unsubscribe_all(self):
|
|
709
|
+
for reactive in self.subscribed:
|
|
710
|
+
unsubscribe = self.subscribed[reactive]
|
|
711
|
+
unsubscribe()
|
|
712
|
+
|
|
713
|
+
def __enter__(self):
|
|
714
|
+
self.reactive_used_before = thread_local.reactive_used
|
|
715
|
+
self.reactive_used = thread_local.reactive_used = set()
|
|
716
|
+
assert thread_local.reactive_used is self.reactive_used, f"{hex(id(thread_local.reactive_used))} vs {hex(id(self.reactive_used))}"
|
|
717
|
+
|
|
718
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
719
|
+
thread_local.reactive_used = self.reactive_used_before
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase):
|
|
723
|
+
def __init__(self, element: solara.Element):
|
|
724
|
+
self.element = element
|
|
725
|
+
super().__init__()
|
|
726
|
+
|
|
727
|
+
def __enter__(self):
|
|
728
|
+
_, set_counter = solara.use_state(0, key="auto_subscribe_force_update_counter")
|
|
729
|
+
|
|
730
|
+
def force_update(new_value, old_value):
|
|
731
|
+
# can we do just x+1 to collapse multiple updates into one?
|
|
732
|
+
set_counter(lambda x: x + 1)
|
|
733
|
+
|
|
734
|
+
super().__enter__()
|
|
735
|
+
|
|
736
|
+
def update_subscribers():
|
|
737
|
+
self.update_subscribers(force_update, scope=reacton.core.get_render_context(required=True))
|
|
738
|
+
|
|
739
|
+
solara.use_effect(update_subscribers, None)
|
|
740
|
+
|
|
741
|
+
def on_close():
|
|
742
|
+
def cleanup():
|
|
743
|
+
assert self.reactive_added_previous_run is not None
|
|
744
|
+
self.unsubscribe_all()
|
|
745
|
+
|
|
746
|
+
return cleanup
|
|
747
|
+
|
|
748
|
+
solara.use_effect(on_close, [])
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class AutoSubscribeContextManager(AutoSubscribeContextManagerBase):
|
|
752
|
+
on_change: Callable[[], None]
|
|
753
|
+
|
|
754
|
+
def __init__(self, on_change: Callable[[], None]):
|
|
755
|
+
super().__init__()
|
|
756
|
+
self.on_change = on_change
|
|
757
|
+
|
|
758
|
+
def __enter__(self):
|
|
759
|
+
super().__enter__()
|
|
760
|
+
|
|
761
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
762
|
+
value = super().__exit__(exc_type, exc_val, exc_tb)
|
|
763
|
+
self.update_subscribers(self.on_change)
|
|
764
|
+
return value
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# alias for compatibility
|
|
768
|
+
State = Reactive
|
|
769
|
+
ValueSubField = ReactiveField
|
|
770
|
+
|
|
771
|
+
auto_subscribe_context_manager = AutoSubscribeContextManagerReacton
|
|
772
|
+
reacton.core._component_context_manager_classes.append(auto_subscribe_context_manager)
|