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.
- 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 +765 -0
- solara/_stores.py +297 -0
- solara/alias.py +6 -0
- solara/autorouting.py +555 -0
- solara/cache.py +305 -0
- solara/checks.html +71 -0
- solara/checks.py +227 -0
- solara/comm.py +28 -0
- solara/components/__init__.py +77 -0
- solara/components/alert.py +155 -0
- solara/components/applayout.py +397 -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 +134 -0
- solara/components/cross_filter.py +335 -0
- solara/components/dataframe.py +546 -0
- solara/components/datatable.py +214 -0
- solara/components/datatable.vue +175 -0
- solara/components/details.py +56 -0
- solara/components/download.vue +35 -0
- solara/components/echarts.py +86 -0
- solara/components/echarts.vue +139 -0
- solara/components/figure_altair.py +39 -0
- solara/components/file_browser.py +181 -0
- solara/components/file_download.py +199 -0
- solara/components/file_drop.py +159 -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 +456 -0
- solara/components/input_text_area.py +86 -0
- solara/components/link.py +55 -0
- solara/components/markdown.py +441 -0
- solara/components/markdown_editor.py +33 -0
- solara/components/markdown_editor.vue +359 -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 +45 -0
- solara/components/sql_code.py +41 -0
- solara/components/sql_code.vue +125 -0
- solara/components/style.py +105 -0
- solara/components/switch.py +71 -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/core.py +42 -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 +151 -0
- solara/hooks/use_thread.py +129 -0
- solara/kitchensink.py +8 -0
- solara/lab/__init__.py +34 -0
- solara/lab/components/__init__.py +7 -0
- solara/lab/components/chat.py +215 -0
- solara/lab/components/confirmation_dialog.py +163 -0
- solara/lab/components/cross_filter.py +7 -0
- solara/lab/components/input_date.py +339 -0
- solara/lab/components/input_time.py +133 -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 +2 -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 +165 -0
- solara/lab/utils/headers.py +5 -0
- solara/layout.py +44 -0
- solara/lifecycle.py +46 -0
- solara/minisettings.py +141 -0
- solara/py.typed +0 -0
- solara/reactive.py +99 -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 +527 -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 +1681 -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 +91 -0
- solara/server/esm.py +71 -0
- solara/server/fastapi.py +5 -0
- solara/server/flask.py +297 -0
- solara/server/jupyter/__init__.py +2 -0
- solara/server/jupyter/cdn_handler.py +28 -0
- solara/server/jupyter/server_extension.py +40 -0
- solara/server/jupyter/solara.py +91 -0
- solara/server/jupytertools.py +46 -0
- solara/server/kernel.py +388 -0
- solara/server/kernel_context.py +467 -0
- solara/server/patch.py +564 -0
- solara/server/pyinstaller/__init__.py +9 -0
- solara/server/pyinstaller/hook-ipyreact.py +5 -0
- solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
- solara/server/pyinstaller/hook-solara.py +9 -0
- solara/server/qt.py +113 -0
- solara/server/reload.py +251 -0
- solara/server/server.py +484 -0
- solara/server/settings.py +249 -0
- solara/server/shell.py +269 -0
- solara/server/starlette.py +770 -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 +272 -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 +486 -0
- solara/server/threaded.py +84 -0
- solara/server/utils.py +44 -0
- solara/server/websocket.py +45 -0
- solara/settings.py +86 -0
- solara/tasks.py +893 -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 +783 -0
- solara/toestand.py +998 -0
- solara/util.py +348 -0
- solara/validate_hooks.py +258 -0
- solara/website/__init__.py +0 -0
- solara/website/assets/custom.css +444 -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.py +6 -0
- solara/website/components/algolia.vue +24 -0
- solara/website/components/algolia_api.vue +202 -0
- solara/website/components/breadcrumbs.py +28 -0
- solara/website/components/contact.py +144 -0
- solara/website/components/docs.py +143 -0
- solara/website/components/header.py +75 -0
- solara/website/components/mailchimp.py +12 -0
- solara/website/components/mailchimp.vue +47 -0
- solara/website/components/markdown.py +99 -0
- solara/website/components/markdown_nav.vue +34 -0
- solara/website/components/notebook.py +171 -0
- solara/website/components/sidebar.py +105 -0
- solara/website/pages/__init__.py +370 -0
- solara/website/pages/about/__init__.py +9 -0
- solara/website/pages/about/about.md +3 -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/careers/__init__.py +27 -0
- solara/website/pages/changelog/__init__.py +10 -0
- solara/website/pages/changelog/changelog.md +372 -0
- solara/website/pages/contact/__init__.py +34 -0
- solara/website/pages/doc_use_download.py +85 -0
- solara/website/pages/documentation/__init__.py +90 -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 +417 -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 +50 -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 +72 -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 +192 -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 +256 -0
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +108 -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 +7 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +187 -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 +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +20 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +20 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +20 -0
- solara/website/pages/documentation/api/hooks/__init__.py +9 -0
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +23 -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 +31 -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 +30 -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 +66 -0
- solara/website/pages/documentation/api/hooks/use_thread.md +64 -0
- solara/website/pages/documentation/api/hooks/use_thread.py +42 -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 +29 -0
- solara/website/pages/documentation/api/routing/use_route.py +76 -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 +44 -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 +25 -0
- solara/website/pages/documentation/components/advanced/meta.py +17 -0
- solara/website/pages/documentation/components/advanced/style.py +43 -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 +30 -0
- solara/website/pages/documentation/components/input/file_drop.py +76 -0
- solara/website/pages/documentation/components/input/input.py +43 -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/input_time.py +15 -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 +74 -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 +66 -0
- solara/website/pages/documentation/components/layout/details.py +13 -0
- solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
- solara/website/pages/documentation/components/layout/gridfixed.py +19 -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 +19 -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 +83 -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 +15 -0
- solara/website/pages/documentation/components/page/title.py +25 -0
- solara/website/pages/documentation/components/status/__init__.py +9 -0
- solara/website/pages/documentation/components/status/error.py +39 -0
- solara/website/pages/documentation/components/status/info.py +39 -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 +77 -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 +54 -0
- solara/website/pages/documentation/examples/ai/__init__.py +11 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +113 -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 +32 -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 +65 -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 +62 -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 +67 -0
- solara/website/pages/documentation/examples/visualization/linked_views.py +81 -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 +112 -0
- solara/website/pages/documentation/getting_started/__init__.py +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +107 -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 +65 -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 +1021 -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 +228 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +278 -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 +305 -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/home.vue +1199 -0
- solara/website/pages/our_team/__init__.py +83 -0
- solara/website/pages/pricing/__init__.py +31 -0
- solara/website/pages/roadmap/__init__.py +11 -0
- solara/website/pages/roadmap/roadmap.md +47 -0
- solara/website/pages/scale_ipywidgets.py +45 -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 +107 -0
- solara/widgets/vue/html.vue +4 -0
- solara/widgets/vue/navigator.vue +134 -0
- solara/widgets/vue/vegalite.vue +130 -0
- solara/widgets/widgets.py +74 -0
- solara_ui-1.45.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- solara_ui-1.45.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara_ui-1.45.0.dist-info/METADATA +162 -0
- solara_ui-1.45.0.dist-info/RECORD +464 -0
- solara_ui-1.45.0.dist-info/WHEEL +4 -0
- solara_ui-1.45.0.dist-info/licenses/LICENSE +21 -0
solara/toestand.py
ADDED
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from types import FrameType
|
|
8
|
+
import warnings
|
|
9
|
+
import copy
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from operator import getitem
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
Callable,
|
|
16
|
+
ContextManager,
|
|
17
|
+
Dict,
|
|
18
|
+
Generic,
|
|
19
|
+
Optional,
|
|
20
|
+
Set,
|
|
21
|
+
Tuple,
|
|
22
|
+
TypeVar,
|
|
23
|
+
Union,
|
|
24
|
+
cast,
|
|
25
|
+
overload,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
import react_ipywidgets as react
|
|
29
|
+
import reacton.core
|
|
30
|
+
from solara.util import equals_extra
|
|
31
|
+
|
|
32
|
+
import solara
|
|
33
|
+
import solara.settings
|
|
34
|
+
from solara import _using_solara_server
|
|
35
|
+
from solara.util import nullcontext
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
TS = TypeVar("TS")
|
|
39
|
+
S = TypeVar("S") # used for state
|
|
40
|
+
logger = logging.getLogger("solara.toestand")
|
|
41
|
+
|
|
42
|
+
_DEBUG = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ThreadLocal(threading.local):
|
|
46
|
+
reactive_used: Optional[Set["ValueBase"]] = None
|
|
47
|
+
reactive_watch: Optional[Callable[["ValueBase"], None]] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
thread_local = ThreadLocal()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# these hooks should go into react-ipywidgets
|
|
54
|
+
def use_sync_external_store(subscribe: Callable[[Callable[[], None]], Callable[[], None]], get_snapshot: Callable[[], Any]):
|
|
55
|
+
_, set_counter = react.use_state(0)
|
|
56
|
+
|
|
57
|
+
def force_update():
|
|
58
|
+
set_counter(lambda x: x + 1)
|
|
59
|
+
|
|
60
|
+
state = get_snapshot()
|
|
61
|
+
prev_state = react.use_ref(state)
|
|
62
|
+
|
|
63
|
+
def update_state():
|
|
64
|
+
prev_state.current = state
|
|
65
|
+
|
|
66
|
+
react.use_effect(update_state)
|
|
67
|
+
|
|
68
|
+
def on_store_change(_ignore_new_state=None):
|
|
69
|
+
new_state = get_snapshot()
|
|
70
|
+
if not equals_extra(new_state, prev_state.current):
|
|
71
|
+
prev_state.current = new_state
|
|
72
|
+
force_update()
|
|
73
|
+
|
|
74
|
+
react.use_effect(lambda: subscribe(on_store_change), [])
|
|
75
|
+
return state
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def use_sync_external_store_with_selector(subscribe, get_snapshot: Callable[[], Any], selector):
|
|
79
|
+
return use_sync_external_store(subscribe, lambda: selector(get_snapshot()))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def merge_state(d1: S, **kwargs) -> S:
|
|
83
|
+
if dataclasses.is_dataclass(d1):
|
|
84
|
+
return dataclasses.replace(d1, **kwargs) # type: ignore
|
|
85
|
+
if "pydantic" in sys.modules and isinstance(d1, sys.modules["pydantic"].BaseModel):
|
|
86
|
+
module = sys.modules["pydantic"]
|
|
87
|
+
version_major = int(module.__version__.split(".")[0])
|
|
88
|
+
if version_major >= 2:
|
|
89
|
+
return d1.model_copy(update=kwargs)
|
|
90
|
+
else:
|
|
91
|
+
return d1.copy(update=kwargs)
|
|
92
|
+
return cast(S, {**cast(dict, d1), **kwargs})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ValueBase(Generic[T]):
|
|
96
|
+
def __init__(self, merge: Callable = merge_state, equals=equals_extra):
|
|
97
|
+
self.merge = merge
|
|
98
|
+
self.equals = equals
|
|
99
|
+
self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
100
|
+
self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
101
|
+
|
|
102
|
+
# make sure all boolean operations give type errors
|
|
103
|
+
if not solara.settings.main.allow_reactive_boolean:
|
|
104
|
+
|
|
105
|
+
def __bool__(self):
|
|
106
|
+
raise TypeError("Reactive vars are not allowed in boolean expressions, did you mean to use .value?")
|
|
107
|
+
|
|
108
|
+
def __eq__(self, other):
|
|
109
|
+
raise TypeError(f"'==' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
110
|
+
|
|
111
|
+
def __ne__(self, other):
|
|
112
|
+
raise TypeError(f"'!=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
113
|
+
|
|
114
|
+
# If we explicitly define __eq__, we need to explicitly define __hash__ as well
|
|
115
|
+
# Otherwise our class is marked unhashable
|
|
116
|
+
__hash__ = object.__hash__
|
|
117
|
+
|
|
118
|
+
def __lt__(self, other):
|
|
119
|
+
raise TypeError(f"'<' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
120
|
+
|
|
121
|
+
def __le__(self, other):
|
|
122
|
+
raise TypeError(f"'<=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
123
|
+
|
|
124
|
+
def __gt__(self, other):
|
|
125
|
+
raise TypeError(f"'>' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
126
|
+
|
|
127
|
+
def __ge__(self, other):
|
|
128
|
+
raise TypeError(f"'>=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
129
|
+
|
|
130
|
+
def __len__(self):
|
|
131
|
+
raise TypeError("'len(...)' is not supported for a Reactive, did you mean to use .value?")
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def lock(self):
|
|
135
|
+
raise NotImplementedError
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def value(self) -> T:
|
|
139
|
+
return self.get()
|
|
140
|
+
|
|
141
|
+
@value.setter
|
|
142
|
+
def value(self, value: T):
|
|
143
|
+
self.set(value)
|
|
144
|
+
|
|
145
|
+
def set(self, value: T):
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
|
|
148
|
+
def peek(self) -> T:
|
|
149
|
+
raise NotImplementedError
|
|
150
|
+
|
|
151
|
+
def get(self) -> T:
|
|
152
|
+
raise NotImplementedError
|
|
153
|
+
|
|
154
|
+
def _get_scope_key(self):
|
|
155
|
+
raise NotImplementedError
|
|
156
|
+
|
|
157
|
+
def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None):
|
|
158
|
+
if scope is not None:
|
|
159
|
+
warnings.warn("scope argument should not be used, it was only for internal use")
|
|
160
|
+
del scope
|
|
161
|
+
scope_id = self._get_scope_key()
|
|
162
|
+
rc = reacton.core.get_render_context(required=False)
|
|
163
|
+
if _using_solara_server():
|
|
164
|
+
import solara.server.kernel_context
|
|
165
|
+
|
|
166
|
+
kernel = solara.server.kernel_context.get_current_context() if solara.server.kernel_context.has_current_context() else nullcontext()
|
|
167
|
+
else:
|
|
168
|
+
kernel = nullcontext()
|
|
169
|
+
context = Context(rc, kernel)
|
|
170
|
+
|
|
171
|
+
self.listeners[scope_id].add((listener, context))
|
|
172
|
+
|
|
173
|
+
def cleanup():
|
|
174
|
+
self.listeners[scope_id].remove((listener, context))
|
|
175
|
+
|
|
176
|
+
return cleanup
|
|
177
|
+
|
|
178
|
+
def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None):
|
|
179
|
+
if scope is not None:
|
|
180
|
+
warnings.warn("scope argument should not be used, it was only for internal use")
|
|
181
|
+
del scope
|
|
182
|
+
scope_id = self._get_scope_key()
|
|
183
|
+
rc = reacton.core.get_render_context(required=False)
|
|
184
|
+
if _using_solara_server():
|
|
185
|
+
import solara.server.kernel_context
|
|
186
|
+
|
|
187
|
+
kernel = solara.server.kernel_context.get_current_context() if solara.server.kernel_context.has_current_context() else nullcontext()
|
|
188
|
+
else:
|
|
189
|
+
kernel = nullcontext()
|
|
190
|
+
context = Context(rc, kernel)
|
|
191
|
+
self.listeners2[scope_id].add((listener, context))
|
|
192
|
+
|
|
193
|
+
def cleanup():
|
|
194
|
+
self.listeners2[scope_id].remove((listener, context))
|
|
195
|
+
|
|
196
|
+
return cleanup
|
|
197
|
+
|
|
198
|
+
def fire(self, new: T, old: T):
|
|
199
|
+
logger.info("value change from %s to %s, will fire events", old, new)
|
|
200
|
+
scope_id = self._get_scope_key()
|
|
201
|
+
contexts = set()
|
|
202
|
+
for listener, context in self.listeners[scope_id].copy():
|
|
203
|
+
contexts.add(context)
|
|
204
|
+
for listener2, context in self.listeners2[scope_id].copy():
|
|
205
|
+
contexts.add(context)
|
|
206
|
+
if contexts:
|
|
207
|
+
for context in contexts:
|
|
208
|
+
with context or nullcontext():
|
|
209
|
+
for listener, context_listener in self.listeners[scope_id].copy():
|
|
210
|
+
if context == context_listener:
|
|
211
|
+
listener(new)
|
|
212
|
+
for listener2, context_listener in self.listeners2[scope_id].copy():
|
|
213
|
+
if context == context_listener:
|
|
214
|
+
listener2(new, old)
|
|
215
|
+
|
|
216
|
+
def update(self, _f=None, **kwargs):
|
|
217
|
+
if _f is not None:
|
|
218
|
+
assert not kwargs
|
|
219
|
+
with self.lock:
|
|
220
|
+
kwargs = _f(self.get())
|
|
221
|
+
with self.lock:
|
|
222
|
+
# important to have this part thread-safe
|
|
223
|
+
new = self.merge(self.get(), **kwargs)
|
|
224
|
+
self.set(new)
|
|
225
|
+
|
|
226
|
+
def use_value(self) -> T:
|
|
227
|
+
# .use with the default argument doesn't give good type inference
|
|
228
|
+
return self.use()
|
|
229
|
+
|
|
230
|
+
def use(self, selector: Callable[[T], TS] = lambda x: x) -> TS: # type: ignore
|
|
231
|
+
return selector(self.value)
|
|
232
|
+
|
|
233
|
+
def use_state(self) -> Tuple[T, Callable[[T], None]]:
|
|
234
|
+
setter = self.set
|
|
235
|
+
value = self.use() # type: ignore
|
|
236
|
+
return value, setter
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def fields(self) -> T:
|
|
240
|
+
# we lie about the return type, but in combination with
|
|
241
|
+
# setter we can make type safe setters (see docs/tests)
|
|
242
|
+
return cast(T, Fields(self))
|
|
243
|
+
|
|
244
|
+
def setter(self, field: TS) -> Callable[[TS], None]:
|
|
245
|
+
_field = cast(FieldBase, field)
|
|
246
|
+
|
|
247
|
+
def setter(new_value: TS):
|
|
248
|
+
_field.set(new_value)
|
|
249
|
+
|
|
250
|
+
return cast(Callable[[TS], None], setter)
|
|
251
|
+
|
|
252
|
+
def _check_mutation(self):
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# the default store for now, stores in a global dict, or when in a solara
|
|
257
|
+
# context, in the solara user context
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class KernelStore(ValueBase[S], ABC):
|
|
261
|
+
_global_dict: Dict[str, S] = {} # outside of solara context, this is used
|
|
262
|
+
# we keep a counter per type, so the storage keys we generate are deterministic
|
|
263
|
+
_type_counter: Dict[Any, int] = defaultdict(int)
|
|
264
|
+
scope_lock = threading.RLock()
|
|
265
|
+
|
|
266
|
+
def __init__(self, key: str, equals: Callable[[Any, Any], bool] = equals_extra):
|
|
267
|
+
super().__init__(equals=equals)
|
|
268
|
+
self.storage_key = key
|
|
269
|
+
self._global_dict = {}
|
|
270
|
+
# since a set can trigger events, which can trigger new updates, we need a recursive lock
|
|
271
|
+
self._lock = threading.RLock()
|
|
272
|
+
self.local = threading.local()
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def lock(self):
|
|
276
|
+
return self._lock
|
|
277
|
+
|
|
278
|
+
def _get_scope_key(self):
|
|
279
|
+
scope_dict, scope_id = self._get_dict()
|
|
280
|
+
return scope_id
|
|
281
|
+
|
|
282
|
+
def _get_dict(self):
|
|
283
|
+
scope_dict = self._global_dict
|
|
284
|
+
scope_id = "global"
|
|
285
|
+
if _using_solara_server():
|
|
286
|
+
import solara.server.kernel_context
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
context = solara.server.kernel_context.get_current_context()
|
|
290
|
+
except RuntimeError: # noqa
|
|
291
|
+
pass # do we need to be more strict?
|
|
292
|
+
else:
|
|
293
|
+
scope_dict = cast(Dict[str, S], context.user_dicts)
|
|
294
|
+
scope_id = context.id
|
|
295
|
+
return cast(Dict[str, S], scope_dict), scope_id
|
|
296
|
+
|
|
297
|
+
def peek(self):
|
|
298
|
+
return self.get()
|
|
299
|
+
|
|
300
|
+
def get(self):
|
|
301
|
+
scope_dict, scope_id = self._get_dict()
|
|
302
|
+
if self.storage_key not in scope_dict:
|
|
303
|
+
with self.scope_lock:
|
|
304
|
+
if self.storage_key not in scope_dict:
|
|
305
|
+
# we assume immutable, so don't make a copy
|
|
306
|
+
scope_dict[self.storage_key] = self.initial_value()
|
|
307
|
+
return scope_dict[self.storage_key]
|
|
308
|
+
|
|
309
|
+
def clear(self):
|
|
310
|
+
scope_dict, scope_id = self._get_dict()
|
|
311
|
+
if self.storage_key in scope_dict:
|
|
312
|
+
del scope_dict[self.storage_key]
|
|
313
|
+
|
|
314
|
+
def set(self, value: S):
|
|
315
|
+
scope_dict, scope_id = self._get_dict()
|
|
316
|
+
old = self.get()
|
|
317
|
+
if self.equals(old, value):
|
|
318
|
+
return
|
|
319
|
+
scope_dict[self.storage_key] = value
|
|
320
|
+
|
|
321
|
+
if _DEBUG:
|
|
322
|
+
import traceback
|
|
323
|
+
|
|
324
|
+
traceback.print_stack(limit=17, file=sys.stdout)
|
|
325
|
+
|
|
326
|
+
print("change old", old) # noqa
|
|
327
|
+
print("change new", value) # noqa
|
|
328
|
+
|
|
329
|
+
self.fire(value, old)
|
|
330
|
+
|
|
331
|
+
@abstractmethod
|
|
332
|
+
def initial_value(self) -> S:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
def _check_mutation(self):
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _is_internal_module(file_name: str):
|
|
340
|
+
file_name_parts = file_name.split(os.sep)
|
|
341
|
+
if len(file_name_parts) < 2:
|
|
342
|
+
return False
|
|
343
|
+
return (
|
|
344
|
+
file_name_parts[-2:] == ["solara", "toestand.py"]
|
|
345
|
+
or file_name_parts[-2:] == ["solara", "reactive.py"]
|
|
346
|
+
or file_name_parts[-2:] == ["solara", "_stores.py"]
|
|
347
|
+
or file_name_parts[-3:] == ["solara", "hooks", "use_reactive.py"]
|
|
348
|
+
or file_name_parts[-2:] == ["reacton", "core.py"]
|
|
349
|
+
# If we use SomeClass[K](...) we go via the typing module, so we need to skip that as well
|
|
350
|
+
or (file_name_parts[-2].startswith("python") and file_name_parts[-1] == "typing.py")
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _find_outside_solara_frame() -> Optional[FrameType]:
|
|
355
|
+
# the module where the call stack origined from
|
|
356
|
+
current_frame: Optional[FrameType] = None
|
|
357
|
+
module_frame = None
|
|
358
|
+
|
|
359
|
+
# _getframe is not guaranteed to exist in all Python implementations,
|
|
360
|
+
# but is much faster than the inspect module
|
|
361
|
+
if hasattr(sys, "_getframe"):
|
|
362
|
+
current_frame = sys._getframe(1)
|
|
363
|
+
else:
|
|
364
|
+
current_frame = inspect.currentframe()
|
|
365
|
+
|
|
366
|
+
while current_frame is not None:
|
|
367
|
+
file_name = current_frame.f_code.co_filename
|
|
368
|
+
# Skip most common cases, i.e. toestand.py, reactive.py, use_reactive.py, Reacton's core.py, and the typing module
|
|
369
|
+
if not _is_internal_module(file_name):
|
|
370
|
+
module_frame = current_frame
|
|
371
|
+
break
|
|
372
|
+
current_frame = current_frame.f_back
|
|
373
|
+
|
|
374
|
+
return module_frame
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class KernelStoreValue(KernelStore[S]):
|
|
378
|
+
default_value: S
|
|
379
|
+
_traceback: Optional[inspect.Traceback]
|
|
380
|
+
_default_value_copy: Optional[S]
|
|
381
|
+
|
|
382
|
+
def __init__(self, default_value: S, key=None, equals: Callable[[Any, Any], bool] = equals_extra, unwrap=lambda x: x):
|
|
383
|
+
self.default_value = default_value
|
|
384
|
+
self._unwrap = unwrap
|
|
385
|
+
self.equals = equals
|
|
386
|
+
self._mutation_detection = solara.settings.storage.mutation_detection
|
|
387
|
+
if self._mutation_detection:
|
|
388
|
+
frame = _find_outside_solara_frame()
|
|
389
|
+
if frame is not None:
|
|
390
|
+
self._traceback = inspect.getframeinfo(frame)
|
|
391
|
+
else:
|
|
392
|
+
self._traceback = None
|
|
393
|
+
self._default_value_copy = copy.deepcopy(default_value)
|
|
394
|
+
if not self.equals(self._unwrap(self.default_value), self._unwrap(self._default_value_copy)):
|
|
395
|
+
msg = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
|
|
396
|
+
|
|
397
|
+
This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
|
|
398
|
+
|
|
399
|
+
To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
|
|
400
|
+
A good choice for dataframes and numpy arrays might be solara.util.equals_pickle, which will also attempt to compare the pickled values of the objects.
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
|
|
404
|
+
reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
405
|
+
"""
|
|
406
|
+
tb = self._traceback
|
|
407
|
+
if tb:
|
|
408
|
+
if tb.code_context:
|
|
409
|
+
code = tb.code_context[0]
|
|
410
|
+
else:
|
|
411
|
+
code = "<No code context available>"
|
|
412
|
+
msg += f"This warning was triggered from:\n{tb.filename}:{tb.lineno}\n{code}"
|
|
413
|
+
warnings.warn(msg)
|
|
414
|
+
self._mutation_detection = False
|
|
415
|
+
cls = type(default_value)
|
|
416
|
+
if key is None:
|
|
417
|
+
with KernelStoreValue.scope_lock:
|
|
418
|
+
index = self._type_counter[cls]
|
|
419
|
+
self._type_counter[cls] += 1
|
|
420
|
+
key = cls.__module__ + ":" + cls.__name__ + ":" + str(index)
|
|
421
|
+
super().__init__(key=key, equals=equals)
|
|
422
|
+
|
|
423
|
+
def initial_value(self) -> S:
|
|
424
|
+
self._check_mutation()
|
|
425
|
+
return self.default_value
|
|
426
|
+
|
|
427
|
+
def _check_mutation(self):
|
|
428
|
+
if not self._mutation_detection:
|
|
429
|
+
return
|
|
430
|
+
initial = self._unwrap(self._default_value_copy)
|
|
431
|
+
current = self._unwrap(self.default_value)
|
|
432
|
+
if not self.equals(initial, current):
|
|
433
|
+
tb = self._traceback
|
|
434
|
+
if tb:
|
|
435
|
+
if tb.code_context:
|
|
436
|
+
code = tb.code_context[0].strip()
|
|
437
|
+
else:
|
|
438
|
+
code = "No code context available"
|
|
439
|
+
msg = f"Reactive variable was initialized at {tb.filename}:{tb.lineno} with {initial!r}, but was mutated to {current!r}.\n{code}"
|
|
440
|
+
else:
|
|
441
|
+
msg = f"Reactive variable was initialized with a value of {initial!r}, but was mutated to {current!r} (unable to report the location in the source code)."
|
|
442
|
+
raise ValueError(msg)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _create_key_callable(f: Callable[[], S]):
|
|
446
|
+
try:
|
|
447
|
+
prefix = f.__qualname__
|
|
448
|
+
except Exception:
|
|
449
|
+
prefix = repr(f)
|
|
450
|
+
with KernelStore.scope_lock:
|
|
451
|
+
index = KernelStore._type_counter[prefix]
|
|
452
|
+
KernelStore._type_counter[prefix] += 1
|
|
453
|
+
try:
|
|
454
|
+
key = f.__module__ + ":" + prefix + ":" + str(index)
|
|
455
|
+
except Exception:
|
|
456
|
+
key = prefix + ":" + str(index)
|
|
457
|
+
return key
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class KernelStoreFactory(KernelStore[S]):
|
|
461
|
+
def __init__(self, factory: Callable[[], S], key=None, equals: Callable[[Any, Any], bool] = equals_extra):
|
|
462
|
+
self.factory = factory
|
|
463
|
+
key = key or _create_key_callable(factory)
|
|
464
|
+
super().__init__(key=key, equals=equals)
|
|
465
|
+
|
|
466
|
+
def initial_value(self) -> S:
|
|
467
|
+
return self.factory()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def mutation_detection_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
471
|
+
from solara.util import equals_pickle as default_equals
|
|
472
|
+
from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet, _SetValueNotSet
|
|
473
|
+
|
|
474
|
+
kernel_store = KernelStoreValue[StoreValue[S]](
|
|
475
|
+
StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=_SetValueNotSet(), set_traceback=None),
|
|
476
|
+
key=key,
|
|
477
|
+
unwrap=lambda x: x.private,
|
|
478
|
+
)
|
|
479
|
+
return MutateDetectorStore[S](kernel_store, equals=equals or default_equals)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def default_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
483
|
+
# in solara v2 we will also do this when mutation_detection is None
|
|
484
|
+
# and we do not run on production mode
|
|
485
|
+
if solara.settings.storage.mutation_detection is True:
|
|
486
|
+
return mutation_detection_storage(default_value, key=key, equals=equals)
|
|
487
|
+
else:
|
|
488
|
+
return KernelStoreValue[S](default_value, key=key, equals=equals or equals_extra)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _call_storage_factory(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
492
|
+
factory = solara.settings.storage.get_factory()
|
|
493
|
+
return factory(default_value, key=key, equals=equals)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class Reactive(ValueBase[S]):
|
|
497
|
+
_storage: ValueBase[S]
|
|
498
|
+
|
|
499
|
+
def __init__(self, default_value: Union[S, ValueBase[S]], key=None, equals=None):
|
|
500
|
+
super().__init__()
|
|
501
|
+
if not isinstance(default_value, ValueBase):
|
|
502
|
+
self._storage = _call_storage_factory(default_value, key=key, equals=equals)
|
|
503
|
+
else:
|
|
504
|
+
self._storage = default_value
|
|
505
|
+
self.__post__init__()
|
|
506
|
+
self._name = None
|
|
507
|
+
self._owner = None
|
|
508
|
+
|
|
509
|
+
def __set_name__(self, owner, name):
|
|
510
|
+
self._name = name
|
|
511
|
+
self._owner = owner
|
|
512
|
+
|
|
513
|
+
def __repr__(self):
|
|
514
|
+
value = self.peek()
|
|
515
|
+
if self._name:
|
|
516
|
+
return f"<Reactive {self._owner.__name__}.{self._name} value={value!r} id={hex(id(self))}>"
|
|
517
|
+
else:
|
|
518
|
+
return f"<Reactive value={value!r} id={hex(id(self))}>"
|
|
519
|
+
|
|
520
|
+
def __str__(self):
|
|
521
|
+
if self._name:
|
|
522
|
+
return f"{self._owner.__name__}.{self._name}={self.value!r}"
|
|
523
|
+
else:
|
|
524
|
+
return f"{self.value!r}"
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def lock(self):
|
|
528
|
+
return self._storage.lock
|
|
529
|
+
|
|
530
|
+
def __post__init__(self):
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
def update(self, *args, **kwargs):
|
|
534
|
+
self._storage.update(*args, **kwargs)
|
|
535
|
+
|
|
536
|
+
def set(self, value: S):
|
|
537
|
+
if value is self:
|
|
538
|
+
raise ValueError("Can't set a reactive to itself")
|
|
539
|
+
self._storage.set(value)
|
|
540
|
+
|
|
541
|
+
def get(self, add_watch=None) -> S:
|
|
542
|
+
if add_watch is not None:
|
|
543
|
+
warnings.warn("add_watch is deprecated, use .peek()", DeprecationWarning)
|
|
544
|
+
if thread_local.reactive_used is not None:
|
|
545
|
+
thread_local.reactive_used.add(self)
|
|
546
|
+
if thread_local.reactive_watch is not None:
|
|
547
|
+
thread_local.reactive_watch(self)
|
|
548
|
+
# peek to avoid parents also adding themselves to the reactive_used set
|
|
549
|
+
return self._storage.peek()
|
|
550
|
+
|
|
551
|
+
def peek(self) -> S:
|
|
552
|
+
"""Return the value without automatically subscribing to listeners."""
|
|
553
|
+
return self._storage.peek()
|
|
554
|
+
|
|
555
|
+
def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
|
|
556
|
+
return self._storage.subscribe(listener, scope=scope)
|
|
557
|
+
|
|
558
|
+
def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
|
|
559
|
+
return self._storage.subscribe_change(listener, scope=scope)
|
|
560
|
+
|
|
561
|
+
def computed(self, f: Callable[[S], T]) -> "Computed[T]":
|
|
562
|
+
def func():
|
|
563
|
+
return f(self.get())
|
|
564
|
+
|
|
565
|
+
return Computed(func, key=f.__qualname__)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class Singleton(Reactive[S]):
|
|
569
|
+
_storage: KernelStore[S]
|
|
570
|
+
|
|
571
|
+
def __init__(self, factory: Callable[[], S], key=None):
|
|
572
|
+
import solara.lifecycle
|
|
573
|
+
|
|
574
|
+
super().__init__(KernelStoreFactory(factory, key=key))
|
|
575
|
+
|
|
576
|
+
# reset on kernel restart (e.g. hot reload)
|
|
577
|
+
def reset():
|
|
578
|
+
def cleanup():
|
|
579
|
+
self._storage.clear()
|
|
580
|
+
|
|
581
|
+
return cleanup
|
|
582
|
+
|
|
583
|
+
solara.lifecycle.on_kernel_start(reset)
|
|
584
|
+
|
|
585
|
+
def __set__(self, obj, value):
|
|
586
|
+
raise AttributeError("Can't set a singleton")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class Computed(Reactive[S]):
|
|
590
|
+
_storage: KernelStore[S]
|
|
591
|
+
|
|
592
|
+
def __init__(self, f: Callable[[], S], key=None):
|
|
593
|
+
import solara.lifecycle
|
|
594
|
+
|
|
595
|
+
self.f = f
|
|
596
|
+
|
|
597
|
+
def on_change(*ignore):
|
|
598
|
+
with self._auto_subscriber.value:
|
|
599
|
+
self.set(f())
|
|
600
|
+
|
|
601
|
+
import functools
|
|
602
|
+
|
|
603
|
+
self._auto_subscriber = Singleton(functools.wraps(AutoSubscribeContextManager)(lambda: AutoSubscribeContextManager(on_change)))
|
|
604
|
+
|
|
605
|
+
@functools.wraps(f)
|
|
606
|
+
def factory():
|
|
607
|
+
v = self._auto_subscriber.value
|
|
608
|
+
with v:
|
|
609
|
+
return f()
|
|
610
|
+
|
|
611
|
+
super().__init__(KernelStoreFactory(factory, key=key))
|
|
612
|
+
|
|
613
|
+
# reset on kernel restart (e.g. hot reload)
|
|
614
|
+
def reset():
|
|
615
|
+
def cleanup():
|
|
616
|
+
self._storage.clear()
|
|
617
|
+
|
|
618
|
+
return cleanup
|
|
619
|
+
|
|
620
|
+
solara.lifecycle.on_kernel_start(reset)
|
|
621
|
+
|
|
622
|
+
def __repr__(self):
|
|
623
|
+
value = super().__repr__()
|
|
624
|
+
return "<Computed" + value[len("<Reactive") : -1]
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@overload
|
|
628
|
+
def computed(
|
|
629
|
+
f: None,
|
|
630
|
+
*,
|
|
631
|
+
key: Optional[str] = ...,
|
|
632
|
+
) -> Callable[[Callable[[], T]], Reactive[T]]: ...
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@overload
|
|
636
|
+
def computed(
|
|
637
|
+
f: Callable[[], T],
|
|
638
|
+
*,
|
|
639
|
+
key: Optional[str] = ...,
|
|
640
|
+
) -> Reactive[T]: ...
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def computed(
|
|
644
|
+
f: Union[None, Callable[[], T]],
|
|
645
|
+
*,
|
|
646
|
+
key: Optional[str] = None,
|
|
647
|
+
) -> Union[Callable[[Callable[[], T]], Reactive[T]], Reactive[T]]:
|
|
648
|
+
"""Creates a reactive variable that is set to the return value of the function.
|
|
649
|
+
|
|
650
|
+
The value will be updated when any of the reactive variables used in the function
|
|
651
|
+
change.
|
|
652
|
+
|
|
653
|
+
## Example
|
|
654
|
+
|
|
655
|
+
```solara
|
|
656
|
+
import solara
|
|
657
|
+
import solara.lab
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
a = solara.reactive(1)
|
|
661
|
+
b = solara.reactive(2)
|
|
662
|
+
|
|
663
|
+
@solara.lab.computed
|
|
664
|
+
def total():
|
|
665
|
+
return a.value + b.value
|
|
666
|
+
|
|
667
|
+
def reset():
|
|
668
|
+
a.value = 1
|
|
669
|
+
b.value = 2
|
|
670
|
+
|
|
671
|
+
@solara.component
|
|
672
|
+
def Page():
|
|
673
|
+
print(a, b, total)
|
|
674
|
+
solara.IntSlider("a", value=a)
|
|
675
|
+
solara.IntSlider("b", value=b)
|
|
676
|
+
solara.Text(f"a + b = {total.value}")
|
|
677
|
+
solara.Button("reset", on_click=reset)
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def wrapper(f: Callable[[], T]):
|
|
683
|
+
return Computed(f, key=key)
|
|
684
|
+
|
|
685
|
+
if f is None:
|
|
686
|
+
return wrapper
|
|
687
|
+
else:
|
|
688
|
+
return wrapper(f)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class ReactiveField(Reactive[T]):
|
|
692
|
+
def __init__(self, field: "FieldBase", equals: Callable[[Any, Any], bool] = equals_extra):
|
|
693
|
+
# super().__init__() # type: ignore
|
|
694
|
+
# We skip the Reactive constructor, because we do not need it, but we do
|
|
695
|
+
# want to be an instanceof for use in use_reactive
|
|
696
|
+
ValueBase.__init__(self, equals=equals)
|
|
697
|
+
self._field = field
|
|
698
|
+
field = field
|
|
699
|
+
while not isinstance(field, ValueBase):
|
|
700
|
+
field = field._parent
|
|
701
|
+
self._root = field
|
|
702
|
+
assert isinstance(self._root, ValueBase)
|
|
703
|
+
|
|
704
|
+
def __str__(self):
|
|
705
|
+
return str(self._field)
|
|
706
|
+
|
|
707
|
+
def __repr__(self):
|
|
708
|
+
return f"<Reactive field {self._field}>"
|
|
709
|
+
|
|
710
|
+
@property
|
|
711
|
+
def lock(self):
|
|
712
|
+
return self._root.lock
|
|
713
|
+
|
|
714
|
+
def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None):
|
|
715
|
+
def on_change(new, old):
|
|
716
|
+
try:
|
|
717
|
+
new_value = self._field.get(new)
|
|
718
|
+
except IndexError:
|
|
719
|
+
return # the current design choice to silently drop the update message
|
|
720
|
+
except KeyError:
|
|
721
|
+
return # same
|
|
722
|
+
old_value = self._field.get(old)
|
|
723
|
+
if not self.equals(new_value, old_value):
|
|
724
|
+
listener(new_value)
|
|
725
|
+
|
|
726
|
+
return self._root.subscribe_change(on_change, scope=scope)
|
|
727
|
+
|
|
728
|
+
def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None):
|
|
729
|
+
def on_change(new, old):
|
|
730
|
+
try:
|
|
731
|
+
new_value = self._field.get(new)
|
|
732
|
+
except IndexError:
|
|
733
|
+
return # see subscribe
|
|
734
|
+
except KeyError:
|
|
735
|
+
return # see subscribe
|
|
736
|
+
old_value = self._field.get(old)
|
|
737
|
+
if not self.equals(new_value, old_value):
|
|
738
|
+
listener(new_value, old_value)
|
|
739
|
+
|
|
740
|
+
return self._root.subscribe_change(on_change, scope=scope)
|
|
741
|
+
|
|
742
|
+
def get(self, add_watch=None) -> T:
|
|
743
|
+
if add_watch is not None:
|
|
744
|
+
warnings.warn("add_watch is deprecated, use .peek()", DeprecationWarning)
|
|
745
|
+
if thread_local.reactive_used is not None:
|
|
746
|
+
thread_local.reactive_used.add(self)
|
|
747
|
+
if thread_local.reactive_watch is not None:
|
|
748
|
+
thread_local.reactive_watch(self)
|
|
749
|
+
# peek to avoid parents also adding themselves to the reactive_used set
|
|
750
|
+
return self._field.peek()
|
|
751
|
+
|
|
752
|
+
def peek(self) -> T:
|
|
753
|
+
return self._field.peek()
|
|
754
|
+
|
|
755
|
+
def set(self, value: T):
|
|
756
|
+
self._field.set(value)
|
|
757
|
+
|
|
758
|
+
def update(self, *args, **kwargs):
|
|
759
|
+
ValueBase.update(cast(ValueBase, self), *args, **kwargs)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def Ref(field: T) -> Reactive[T]:
|
|
763
|
+
_field = cast(FieldBase, field)
|
|
764
|
+
return cast(Reactive[T], ReactiveField[T](_field))
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class FieldBase:
|
|
768
|
+
_parent: Any
|
|
769
|
+
|
|
770
|
+
def __getattr__(self, key):
|
|
771
|
+
if key in ["_parent", "set", "_lock"] or key.startswith("__"):
|
|
772
|
+
return self.__dict__[key]
|
|
773
|
+
return FieldAttr(self, key)
|
|
774
|
+
|
|
775
|
+
def __getitem__(self, key):
|
|
776
|
+
return FieldItem(self, key)
|
|
777
|
+
|
|
778
|
+
def get(self, obj=None):
|
|
779
|
+
raise NotImplementedError
|
|
780
|
+
|
|
781
|
+
def set(self, value):
|
|
782
|
+
raise NotImplementedError
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class Fields(FieldBase):
|
|
786
|
+
def __init__(self, state: ValueBase):
|
|
787
|
+
self._parent = state
|
|
788
|
+
self._lock = state.lock
|
|
789
|
+
|
|
790
|
+
def get(self, obj=None):
|
|
791
|
+
# we are at the root, so override the object
|
|
792
|
+
# so we can get the 'old' value
|
|
793
|
+
if obj is not None:
|
|
794
|
+
return obj
|
|
795
|
+
return self._parent.get()
|
|
796
|
+
|
|
797
|
+
def peek(self, obj=None):
|
|
798
|
+
# we are at the root, so override the object
|
|
799
|
+
# so we can get the 'old' value
|
|
800
|
+
if obj is not None:
|
|
801
|
+
return obj
|
|
802
|
+
return self._parent.peek()
|
|
803
|
+
|
|
804
|
+
def set(self, value):
|
|
805
|
+
self._parent.set(value)
|
|
806
|
+
|
|
807
|
+
def __repr__(self):
|
|
808
|
+
return repr(self._parent)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
class FieldAttr(FieldBase):
|
|
812
|
+
def __init__(self, parent, key: str):
|
|
813
|
+
self._parent = parent
|
|
814
|
+
self.key = key
|
|
815
|
+
self._lock = parent._lock
|
|
816
|
+
|
|
817
|
+
def get(self, obj=None):
|
|
818
|
+
obj = self._parent.get(obj)
|
|
819
|
+
return getattr(obj, self.key)
|
|
820
|
+
|
|
821
|
+
def peek(self, obj=None):
|
|
822
|
+
obj = self._parent.peek(obj)
|
|
823
|
+
return getattr(obj, self.key)
|
|
824
|
+
|
|
825
|
+
def set(self, value):
|
|
826
|
+
with self._lock:
|
|
827
|
+
parent_value = self._parent.peek()
|
|
828
|
+
if isinstance(self.key, str):
|
|
829
|
+
parent_value = merge_state(parent_value, **{self.key: value})
|
|
830
|
+
self._parent.set(parent_value)
|
|
831
|
+
else:
|
|
832
|
+
raise TypeError(f"Type of key {self.key!r} is not supported")
|
|
833
|
+
|
|
834
|
+
def __str__(self):
|
|
835
|
+
return f".{self.key}"
|
|
836
|
+
|
|
837
|
+
def __repr__(self):
|
|
838
|
+
return f"<Field {self._parent}{self}>"
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class FieldItem(FieldBase):
|
|
842
|
+
def __init__(self, parent, key: str):
|
|
843
|
+
self._parent = parent
|
|
844
|
+
self.key = key
|
|
845
|
+
self._lock = parent._lock
|
|
846
|
+
|
|
847
|
+
def get(self, obj=None):
|
|
848
|
+
obj = self._parent.get(obj)
|
|
849
|
+
return getitem(obj, self.key)
|
|
850
|
+
|
|
851
|
+
def peek(self, obj=None):
|
|
852
|
+
obj = self._parent.peek(obj)
|
|
853
|
+
return getitem(obj, self.key)
|
|
854
|
+
|
|
855
|
+
def set(self, value):
|
|
856
|
+
with self._lock:
|
|
857
|
+
parent_value = self._parent.peek()
|
|
858
|
+
if isinstance(self.key, int) and isinstance(parent_value, (list, tuple)):
|
|
859
|
+
parent_type = type(parent_value)
|
|
860
|
+
parent_value = parent_value.copy() # type: ignore
|
|
861
|
+
parent_value[self.key] = value
|
|
862
|
+
self._parent.set(parent_type(parent_value))
|
|
863
|
+
else:
|
|
864
|
+
parent_value = merge_state(parent_value, **{self.key: value})
|
|
865
|
+
self._parent.set(parent_value)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class AutoSubscribeContextManagerBase:
|
|
869
|
+
# a render loop might trigger a new render loop of a differtent render context
|
|
870
|
+
# so we want to save, and restore the current reactive_used
|
|
871
|
+
reactive_used: Optional[Set[ValueBase]] = None
|
|
872
|
+
subscribed: Dict[ValueBase, Callable]
|
|
873
|
+
subscribed_previous_run: Dict[ValueBase, Callable]
|
|
874
|
+
on_change: Callable[[], None]
|
|
875
|
+
previous_reactive_watch: Optional[Callable[["ValueBase"], None]] = None
|
|
876
|
+
|
|
877
|
+
def __init__(self):
|
|
878
|
+
self.subscribed = {}
|
|
879
|
+
self.subscribed_previous_run = {}
|
|
880
|
+
self.on_change = lambda: None
|
|
881
|
+
|
|
882
|
+
def unsubscribe_previous(self):
|
|
883
|
+
removed = set(self.subscribed_previous_run or set()) - set(self.subscribed)
|
|
884
|
+
if removed:
|
|
885
|
+
for reactive in removed:
|
|
886
|
+
unsubscribe = self.subscribed_previous_run[reactive]
|
|
887
|
+
unsubscribe()
|
|
888
|
+
del self.subscribed_previous_run[reactive]
|
|
889
|
+
|
|
890
|
+
def add(self, reactive: ValueBase):
|
|
891
|
+
relevant_reactive = reactive
|
|
892
|
+
if isinstance(reactive, ValueSubField):
|
|
893
|
+
root = reactive._root
|
|
894
|
+
if root in self.subscribed or root in self.subscribed_previous_run:
|
|
895
|
+
# we already subscribed to this reactive's root
|
|
896
|
+
return
|
|
897
|
+
else:
|
|
898
|
+
# we are subscribing to this reactive's root
|
|
899
|
+
pass
|
|
900
|
+
|
|
901
|
+
# TODO: we could see if we are the root of any of the subscribed fields,
|
|
902
|
+
# and remove that field.
|
|
903
|
+
if relevant_reactive not in self.subscribed:
|
|
904
|
+
if relevant_reactive not in self.subscribed_previous_run:
|
|
905
|
+
unsubscribe = relevant_reactive.subscribe_change(lambda *args: self.on_change())
|
|
906
|
+
self.subscribed[relevant_reactive] = unsubscribe
|
|
907
|
+
else:
|
|
908
|
+
self.subscribed[relevant_reactive] = self.subscribed_previous_run[relevant_reactive]
|
|
909
|
+
|
|
910
|
+
def unsubscribe_all(self):
|
|
911
|
+
for reactive in self.subscribed:
|
|
912
|
+
unsubscribe = self.subscribed[reactive]
|
|
913
|
+
unsubscribe()
|
|
914
|
+
|
|
915
|
+
def __enter__(self):
|
|
916
|
+
self.subscribed = {}
|
|
917
|
+
self.reactive_used_before = thread_local.reactive_used
|
|
918
|
+
self.previous_reactive_watch = thread_local.reactive_watch
|
|
919
|
+
thread_local.reactive_watch = self.add
|
|
920
|
+
self.reactive_used = thread_local.reactive_used = set()
|
|
921
|
+
|
|
922
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
923
|
+
thread_local.reactive_used = self.reactive_used_before
|
|
924
|
+
thread_local.reactive_watch = self.previous_reactive_watch
|
|
925
|
+
self.unsubscribe_previous()
|
|
926
|
+
self.subscribed_previous_run = self.subscribed.copy()
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class Context:
|
|
930
|
+
def __init__(self, render_context, kernel_context):
|
|
931
|
+
# combine the render context *and* the kernel context into one context
|
|
932
|
+
self.render_context = render_context
|
|
933
|
+
self.kernel_context = kernel_context
|
|
934
|
+
|
|
935
|
+
def __enter__(self):
|
|
936
|
+
if self.render_context is not None:
|
|
937
|
+
self.render_context.__enter__()
|
|
938
|
+
self.kernel_context.__enter__()
|
|
939
|
+
|
|
940
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
941
|
+
if self.render_context is not None:
|
|
942
|
+
# this will trigger a render
|
|
943
|
+
res1 = self.render_context.__exit__(exc_type, exc_val, exc_tb)
|
|
944
|
+
else:
|
|
945
|
+
res1 = None
|
|
946
|
+
|
|
947
|
+
# pop the current context from the stack
|
|
948
|
+
res2 = self.kernel_context.__exit__(exc_type, exc_val, exc_tb)
|
|
949
|
+
return res1 or res2
|
|
950
|
+
|
|
951
|
+
def __eq__(self, value: object) -> bool:
|
|
952
|
+
if not isinstance(value, Context):
|
|
953
|
+
return False
|
|
954
|
+
return self.render_context == value.render_context and self.kernel_context == value.kernel_context
|
|
955
|
+
|
|
956
|
+
def __hash__(self) -> int:
|
|
957
|
+
return hash(id(self.render_context)) ^ hash(id(self.kernel_context))
|
|
958
|
+
|
|
959
|
+
def __repr__(self) -> str:
|
|
960
|
+
return f"Context(render_context={self.render_context}, kernel_context={self.kernel_context})"
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase):
|
|
964
|
+
def __init__(self, element: solara.Element):
|
|
965
|
+
self.element = element
|
|
966
|
+
super().__init__()
|
|
967
|
+
|
|
968
|
+
def __enter__(self):
|
|
969
|
+
_, set_counter = solara.use_state(0, key="auto_subscribe_force_update_counter")
|
|
970
|
+
|
|
971
|
+
def force_update():
|
|
972
|
+
# can we do just x+1 to collapse multiple updates into one?
|
|
973
|
+
set_counter(lambda x: x + 1)
|
|
974
|
+
|
|
975
|
+
super().__enter__()
|
|
976
|
+
self.on_change = force_update
|
|
977
|
+
|
|
978
|
+
def on_close():
|
|
979
|
+
def cleanup():
|
|
980
|
+
self.unsubscribe_all()
|
|
981
|
+
|
|
982
|
+
return cleanup
|
|
983
|
+
|
|
984
|
+
solara.use_effect(on_close, [])
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
class AutoSubscribeContextManager(AutoSubscribeContextManagerBase):
|
|
988
|
+
def __init__(self, on_change: Callable[[], None]):
|
|
989
|
+
super().__init__()
|
|
990
|
+
self.on_change = on_change
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
# alias for compatibility
|
|
994
|
+
State = Reactive
|
|
995
|
+
ValueSubField = ReactiveField
|
|
996
|
+
|
|
997
|
+
auto_subscribe_context_manager = AutoSubscribeContextManagerReacton
|
|
998
|
+
reacton.core._component_context_manager_classes.append(auto_subscribe_context_manager)
|