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/util.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import contextlib
|
|
3
|
+
import functools
|
|
4
|
+
import gzip
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from collections import abc
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
|
13
|
+
from urllib.parse import urlencode
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
import ipyvuetify
|
|
19
|
+
import ipywidgets
|
|
20
|
+
import reacton
|
|
21
|
+
|
|
22
|
+
import solara
|
|
23
|
+
|
|
24
|
+
SOLARA_ALLOW_OTHER_TRACER = os.environ.get("SOLARA_ALLOW_OTHER_TRACER", False) in (True, "True", "true", "1")
|
|
25
|
+
ipyvuetify_major_version = int(ipyvuetify.__version__.split(".")[0])
|
|
26
|
+
ipywidgets_major = int(ipywidgets.__version__.split(".")[0])
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
threading.Thread(target=lambda: None).start()
|
|
30
|
+
has_threads = True
|
|
31
|
+
except RuntimeError:
|
|
32
|
+
has_threads = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
from reacton.utils import equals as equals_extra
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def equals_pickle(a, b):
|
|
39
|
+
"""Compare two values for equality.
|
|
40
|
+
|
|
41
|
+
Avoid false negative, e.g. when comparing dataframes, we want to compare
|
|
42
|
+
the data, not the object identity.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
if equals_extra(a, b):
|
|
46
|
+
return True
|
|
47
|
+
import pickle
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if pickle.dumps(a) == pickle.dumps(b):
|
|
51
|
+
return True
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def github_url(file):
|
|
58
|
+
rel_path = os.path.relpath(file, Path(solara.__file__).parent.parent)
|
|
59
|
+
github_url = solara.github_url + f"/blob/{solara.git_branch}/" + rel_path
|
|
60
|
+
return github_url
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def pycafe_url(*, path: Path, requirements: Optional[List[str]] = None):
|
|
64
|
+
json_object = {"code": path.read_text("utf8")}
|
|
65
|
+
if requirements:
|
|
66
|
+
json_object["requirements"] = "\n".join(requirements)
|
|
67
|
+
json_text = json.dumps(json_object)
|
|
68
|
+
# gzip -> base64
|
|
69
|
+
compressed_json_text = gzip.compress(json_text.encode("utf8"))
|
|
70
|
+
base64_text = base64.b64encode(compressed_json_text).decode("utf8")
|
|
71
|
+
query = urlencode({"c": base64_text})
|
|
72
|
+
base_url = "https://app.py.cafe"
|
|
73
|
+
return f"{base_url}/snippet/solara?{query}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def github_edit_url(file):
|
|
77
|
+
# e.g. https://github.com/widgetti/solara/edit/master/solara/__init__.py
|
|
78
|
+
rel_path = os.path.relpath(file, Path(solara.__file__).parent.parent)
|
|
79
|
+
github_url = solara.github_url + f"/edit/{solara.git_branch}/" + rel_path
|
|
80
|
+
return github_url
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_file_as_data_url(file_name, mime):
|
|
84
|
+
with open(file_name, "rb") as f:
|
|
85
|
+
data = f.read()
|
|
86
|
+
return f"data:{mime};base64," + base64.b64encode(data).decode("utf-8")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def isinstanceof(object, spec: str):
|
|
90
|
+
"""Check if object is instance of type '<modulename>:<classname>'
|
|
91
|
+
|
|
92
|
+
This can avoid a runtime dependency, since we do not need to import `modulename`.
|
|
93
|
+
|
|
94
|
+
>>> import numpy as np
|
|
95
|
+
>>> isinstanceof(np.arange(2), "numpy:ndarray")
|
|
96
|
+
True
|
|
97
|
+
"""
|
|
98
|
+
module_name, classname = spec.split(":")
|
|
99
|
+
module = sys.modules.get(module_name)
|
|
100
|
+
if module:
|
|
101
|
+
cls = getattr(module, classname)
|
|
102
|
+
return isinstance(object, cls)
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def numpy_to_image(data: "np.ndarray", format="png"):
|
|
107
|
+
import io
|
|
108
|
+
|
|
109
|
+
if data.ndim == 3:
|
|
110
|
+
try:
|
|
111
|
+
import PIL.Image
|
|
112
|
+
except ModuleNotFoundError:
|
|
113
|
+
raise ModuleNotFoundError("Pillow is required to convert numpy array to image, use pip install pillow to install it.")
|
|
114
|
+
if data.shape[2] == 3:
|
|
115
|
+
im = PIL.Image.fromarray(data[::], "RGB")
|
|
116
|
+
elif data.shape[2] == 4:
|
|
117
|
+
im = PIL.Image.fromarray(data[::], "RGBA")
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Expected last dimension to have 3 or 4 dimensions, total shape we got was {data.shape}")
|
|
120
|
+
f = io.BytesIO()
|
|
121
|
+
im.save(f, format)
|
|
122
|
+
return f.getvalue()
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Expected an image with 3 dimensions (height, width, channel), not {data.shape}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@contextlib.contextmanager
|
|
128
|
+
def cwd(path):
|
|
129
|
+
cwd = os.getcwd()
|
|
130
|
+
try:
|
|
131
|
+
os.chdir(path)
|
|
132
|
+
yield
|
|
133
|
+
finally:
|
|
134
|
+
os.chdir(cwd)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def numpy_equals(a, b):
|
|
138
|
+
import numpy as np
|
|
139
|
+
|
|
140
|
+
if a is b:
|
|
141
|
+
return True
|
|
142
|
+
if a is None or b is None:
|
|
143
|
+
return False
|
|
144
|
+
if np.all(a == b):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _combine_classes(class_list: List[str]) -> str:
|
|
150
|
+
return " ".join(class_list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _flatten_style(style: Union[str, Dict, None] = None) -> str:
|
|
154
|
+
if style is None:
|
|
155
|
+
return ""
|
|
156
|
+
elif isinstance(style, str):
|
|
157
|
+
return style
|
|
158
|
+
elif isinstance(style, dict):
|
|
159
|
+
return ";".join(f"{k}:{v}" for k, v in style.items()) + ";"
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError(f"Expected style to be a string or dict, got {type(style)}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def import_item(name: str):
|
|
165
|
+
"""Import an object by name like solara.cache.LRU"""
|
|
166
|
+
parts = name.rsplit(".", 2)
|
|
167
|
+
if len(parts) == 1:
|
|
168
|
+
return __import__(name)
|
|
169
|
+
else:
|
|
170
|
+
module = __import__(".".join(parts[:-1]), fromlist=[parts[-1]])
|
|
171
|
+
return getattr(module, parts[-1])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_solara_home() -> Path:
|
|
175
|
+
"""Get solara home directory, defaults to ~/.solara.
|
|
176
|
+
|
|
177
|
+
The $SOLARA_HOME environment variable can be set to override this default.
|
|
178
|
+
|
|
179
|
+
If $SOLARA_HOME is not defined and ~ cannot be expanded, the current working directory + ".solara" is used.
|
|
180
|
+
"""
|
|
181
|
+
os_home = None
|
|
182
|
+
try:
|
|
183
|
+
os_home = Path.home()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
if "SOLARA_HOME" in os.environ:
|
|
187
|
+
return Path(os.environ["SOLARA_HOME"])
|
|
188
|
+
elif os_home:
|
|
189
|
+
return os_home / ".solara"
|
|
190
|
+
else:
|
|
191
|
+
return Path(os.getcwd()) / ".solara"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_size(size: str) -> int:
|
|
195
|
+
"""Given a human readable size, return the number of bytes.
|
|
196
|
+
|
|
197
|
+
Supports GB, MB, KB, and bytes. E.g. 10GB, 500MB, 1KB, 1000
|
|
198
|
+
|
|
199
|
+
Commas and _ are ignored, e.g. 1,000,000 is the same as 1000000.
|
|
200
|
+
"""
|
|
201
|
+
size = size.replace(",", "").replace("_", "").upper()
|
|
202
|
+
if size.endswith("GB"):
|
|
203
|
+
return int(float(size[:-2]) * 1024 * 1024 * 1024)
|
|
204
|
+
elif size.endswith("MB"):
|
|
205
|
+
return int(float(size[:-2]) * 1024 * 1024)
|
|
206
|
+
elif size.endswith("KB"):
|
|
207
|
+
return int(float(size[:-2]) * 1024)
|
|
208
|
+
elif size.endswith("B"):
|
|
209
|
+
return int(float(size[:-1]))
|
|
210
|
+
else:
|
|
211
|
+
return int(size)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def nested_get(object, dotted_name: str, default=None):
|
|
215
|
+
names = dotted_name.split(".")
|
|
216
|
+
for name in names:
|
|
217
|
+
if isinstance(object, abc.Mapping):
|
|
218
|
+
if name == names[-1]:
|
|
219
|
+
object = object.get(name, default)
|
|
220
|
+
else:
|
|
221
|
+
object = object.get(name)
|
|
222
|
+
else:
|
|
223
|
+
if name == names[-1]:
|
|
224
|
+
object = getattr(object, name, default)
|
|
225
|
+
else:
|
|
226
|
+
object = getattr(object, name)
|
|
227
|
+
return object
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# inherit from BaseException so less change of being caught
|
|
231
|
+
# in an except
|
|
232
|
+
class CancelledError(BaseException):
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# not available in python 3.6
|
|
237
|
+
class nullcontext(contextlib.AbstractContextManager):
|
|
238
|
+
def __init__(self, enter_result=None):
|
|
239
|
+
self.enter_result = enter_result
|
|
240
|
+
|
|
241
|
+
def __enter__(self):
|
|
242
|
+
return self.enter_result
|
|
243
|
+
|
|
244
|
+
def __exit__(self, *excinfo):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@contextlib.contextmanager
|
|
249
|
+
def cancel_guard(cancelled: threading.Event):
|
|
250
|
+
def tracefunc(frame, event, arg):
|
|
251
|
+
# this gets called at least for every line executed
|
|
252
|
+
if cancelled.is_set():
|
|
253
|
+
rc = reacton.core.get_render_context(required=False)
|
|
254
|
+
# we do not want to cancel the rendering cycle
|
|
255
|
+
if rc is None or not rc._is_rendering:
|
|
256
|
+
# this will bubble up
|
|
257
|
+
raise CancelledError()
|
|
258
|
+
if prev and SOLARA_ALLOW_OTHER_TRACER:
|
|
259
|
+
prev(frame, event, arg)
|
|
260
|
+
# keep tracing:
|
|
261
|
+
return tracefunc
|
|
262
|
+
|
|
263
|
+
# see https://docs.python.org/3/library/sys.html#sys.settrace
|
|
264
|
+
# it is for the calling thread only
|
|
265
|
+
# not every Python implementation has it
|
|
266
|
+
prev = None
|
|
267
|
+
if hasattr(sys, "gettrace"):
|
|
268
|
+
prev = sys.gettrace()
|
|
269
|
+
if hasattr(sys, "settrace"):
|
|
270
|
+
sys.settrace(tracefunc)
|
|
271
|
+
try:
|
|
272
|
+
yield
|
|
273
|
+
finally:
|
|
274
|
+
if hasattr(sys, "settrace"):
|
|
275
|
+
sys.settrace(prev)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_timedelta(size: str) -> float:
|
|
279
|
+
"""Turn a human readable time delta into seconds.
|
|
280
|
+
Supports days(d), hours (h), minutes (m) and seconds (s).
|
|
281
|
+
If not unit is specified, seconds is assumed.
|
|
282
|
+
>>> parse_timedelta("1d")
|
|
283
|
+
86400
|
|
284
|
+
>>> parse_timedelta("1h")
|
|
285
|
+
3600
|
|
286
|
+
>>> parse_timedelta("30m")
|
|
287
|
+
1800
|
|
288
|
+
>>> parse_timedelta("10s")
|
|
289
|
+
10
|
|
290
|
+
>>> parse_timedelta("10")
|
|
291
|
+
10
|
|
292
|
+
"""
|
|
293
|
+
if size.endswith("d"):
|
|
294
|
+
return float(size[:-1]) * 24 * 60 * 60
|
|
295
|
+
elif size.endswith("h"):
|
|
296
|
+
return float(size[:-1]) * 60 * 60
|
|
297
|
+
elif size.endswith("m"):
|
|
298
|
+
return float(size[:-1]) * 60
|
|
299
|
+
elif size.endswith("s"):
|
|
300
|
+
return float(size[:-1])
|
|
301
|
+
else:
|
|
302
|
+
return float(size)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_file_hash(path: Path, algorithm="md5") -> Tuple[bytes, str]:
|
|
306
|
+
"""Compute the hash of a file. Note that we also return the file content as bytes."""
|
|
307
|
+
data = path.read_bytes()
|
|
308
|
+
if sys.version_info[:2] < (3, 9):
|
|
309
|
+
# usedforsecurity is only available in Python 3.9+
|
|
310
|
+
h = hashlib.new(algorithm)
|
|
311
|
+
else:
|
|
312
|
+
h = hashlib.new(algorithm, usedforsecurity=False) # type: ignore
|
|
313
|
+
h.update(data)
|
|
314
|
+
return data, h.hexdigest()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def is_running_in_colab():
|
|
318
|
+
try:
|
|
319
|
+
import google.colab # noqa
|
|
320
|
+
|
|
321
|
+
return True
|
|
322
|
+
except ImportError:
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def is_running_in_vscode():
|
|
327
|
+
return "VSCODE_PID" in os.environ or "VSCODE_CWD" in os.environ
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def is_running_in_voila():
|
|
331
|
+
return os.environ.get("SERVER_SOFTWARE", "").startswith("voila")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def once(f):
|
|
335
|
+
called = False
|
|
336
|
+
return_value = None
|
|
337
|
+
|
|
338
|
+
@functools.wraps(f)
|
|
339
|
+
def wrapper():
|
|
340
|
+
nonlocal called
|
|
341
|
+
nonlocal return_value
|
|
342
|
+
if called:
|
|
343
|
+
return return_value
|
|
344
|
+
called = True
|
|
345
|
+
return_value = f()
|
|
346
|
+
return return_value
|
|
347
|
+
|
|
348
|
+
return wrapper
|
solara/validate_hooks.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import inspect
|
|
3
|
+
import re
|
|
4
|
+
import typing as t
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
DEFAULT_USE_FUNCTIONS = ("^use_.*$",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidReactivityCause(Enum):
|
|
13
|
+
USE_AFTER_RETURN = "early return"
|
|
14
|
+
CONDITIONAL_USE = "conditional"
|
|
15
|
+
LOOP_USE = "loop"
|
|
16
|
+
NESTED_FUNCTION_USE = "nested function"
|
|
17
|
+
VARIABLE_ASSIGNMENT = "assignment"
|
|
18
|
+
EXCEPTION_USE = "exception"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
noqa_code_to_cause = {
|
|
22
|
+
"SH101": InvalidReactivityCause.USE_AFTER_RETURN,
|
|
23
|
+
"SH102": InvalidReactivityCause.CONDITIONAL_USE,
|
|
24
|
+
"SH103": InvalidReactivityCause.LOOP_USE,
|
|
25
|
+
"SH104": InvalidReactivityCause.NESTED_FUNCTION_USE,
|
|
26
|
+
"SH105": InvalidReactivityCause.VARIABLE_ASSIGNMENT,
|
|
27
|
+
"SH106": InvalidReactivityCause.EXCEPTION_USE,
|
|
28
|
+
}
|
|
29
|
+
cause_to_noqa_code = {v: k for k, v in noqa_code_to_cause.items()}
|
|
30
|
+
noqa_pattern = re.compile(r".*# noqa(?::\s*([A-Z]{2,3}\d{2,3})(?:,\s*([A-Z]{2,3}\d{2,3}))*)?")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if sys.version_info < (3, 11):
|
|
34
|
+
ScopeNodeType = t.Union[ast.For, ast.While, ast.If, ast.Try, ast.FunctionDef]
|
|
35
|
+
TryNodes = (ast.Try,)
|
|
36
|
+
else:
|
|
37
|
+
# except* nodes are only standardized in 3.11+
|
|
38
|
+
ScopeNodeType = t.Union[ast.For, ast.While, ast.If, ast.Try, ast.TryStar, ast.FunctionDef]
|
|
39
|
+
TryNodes = (ast.Try, ast.TryStar)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def line_to_noqa(line: str) -> t.Optional[t.Set[InvalidReactivityCause]]:
|
|
43
|
+
"""Return set of noqa code for which to not do quality assurance checks, or None when to do all qa."""
|
|
44
|
+
if "#" in line:
|
|
45
|
+
no_qa = set()
|
|
46
|
+
match = noqa_pattern.match(line)
|
|
47
|
+
if match is not None:
|
|
48
|
+
# not sure why we get some None values
|
|
49
|
+
groups = [k for k in match.groups() if k is not None]
|
|
50
|
+
if groups: # `noqa: SHXXX`` found
|
|
51
|
+
for group in groups:
|
|
52
|
+
if group not in noqa_code_to_cause:
|
|
53
|
+
if group.startswith("SH"):
|
|
54
|
+
raise ValueError(f"Unknown noqa code {group}")
|
|
55
|
+
else:
|
|
56
|
+
no_qa.add(noqa_code_to_cause[group])
|
|
57
|
+
else: # we found `noqa`
|
|
58
|
+
# skip all qa
|
|
59
|
+
no_qa = set(noqa_code_to_cause.values())
|
|
60
|
+
return no_qa
|
|
61
|
+
else:
|
|
62
|
+
return None # no noqa found
|
|
63
|
+
return None # fast path, no comment found
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def should_skip_qa(global_no_qa: t.Optional[t.Set[InvalidReactivityCause]], cause: InvalidReactivityCause, line: str) -> bool:
|
|
67
|
+
line_qa = line_to_noqa(line)
|
|
68
|
+
if global_no_qa is None and line_qa is None:
|
|
69
|
+
# there is not a noqa on the function or line
|
|
70
|
+
return False
|
|
71
|
+
else:
|
|
72
|
+
noqa = global_no_qa or set()
|
|
73
|
+
if line_qa is not None:
|
|
74
|
+
noqa |= line_qa
|
|
75
|
+
return cause in noqa
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class HookValidationError(Exception):
|
|
79
|
+
def __init__(self, cause: InvalidReactivityCause, message: str):
|
|
80
|
+
self.cause = cause
|
|
81
|
+
super().__init__(message)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class HookValidator(ast.NodeVisitor):
|
|
85
|
+
def __init__(self, component: t.Callable, use_functions=DEFAULT_USE_FUNCTIONS):
|
|
86
|
+
self.use_functions = [re.compile(use_func) for use_func in use_functions]
|
|
87
|
+
|
|
88
|
+
self.root_function_return: t.Optional[ast.Return] = None
|
|
89
|
+
self.outer_scope: t.Optional[ScopeNodeType] = None
|
|
90
|
+
|
|
91
|
+
self.filename = component.__code__.co_filename
|
|
92
|
+
self.line_offset = component.__code__.co_firstlineno - 1
|
|
93
|
+
self.component = component
|
|
94
|
+
|
|
95
|
+
self.source = inspect.getsource(self.component)
|
|
96
|
+
# lines before we dedent
|
|
97
|
+
self.lines = self.source.split("\n")
|
|
98
|
+
# dedent the source code to avoid indentation errors
|
|
99
|
+
parsed_source = inspect.cleandoc(self.source)
|
|
100
|
+
parsed = ast.parse(parsed_source)
|
|
101
|
+
# Get nodes from inside the function body
|
|
102
|
+
func_definition = t.cast(ast.FunctionDef, parsed.body[0])
|
|
103
|
+
self.function_scope: ast.FunctionDef = func_definition
|
|
104
|
+
self._root_function_scope = self.function_scope
|
|
105
|
+
# None means, *DO* qa
|
|
106
|
+
self.no_qa: t.Optional[t.Set[InvalidReactivityCause]] = None
|
|
107
|
+
|
|
108
|
+
def run(self):
|
|
109
|
+
import solara.settings
|
|
110
|
+
|
|
111
|
+
if solara.settings.main.check_hooks == "off":
|
|
112
|
+
return
|
|
113
|
+
func_def_line = self.lines[self._root_function_scope.lineno - 1]
|
|
114
|
+
self.no_qa = line_to_noqa(func_def_line)
|
|
115
|
+
try:
|
|
116
|
+
for node in self._root_function_scope.body:
|
|
117
|
+
self.visit(node)
|
|
118
|
+
except HookValidationError as e:
|
|
119
|
+
if solara.settings.main.check_hooks == "error":
|
|
120
|
+
raise e
|
|
121
|
+
elif solara.settings.main.check_hooks == "warn":
|
|
122
|
+
warnings.warn(str(e))
|
|
123
|
+
|
|
124
|
+
def matches_use_function(self, name: str) -> bool:
|
|
125
|
+
return any(use_func.match(name) for use_func in self.use_functions)
|
|
126
|
+
|
|
127
|
+
def node_to_scope_cause(self, node: ScopeNodeType) -> InvalidReactivityCause:
|
|
128
|
+
if isinstance(node, ast.If):
|
|
129
|
+
return InvalidReactivityCause.CONDITIONAL_USE
|
|
130
|
+
elif isinstance(node, (ast.For, ast.While)):
|
|
131
|
+
return InvalidReactivityCause.LOOP_USE
|
|
132
|
+
elif isinstance(node, ast.FunctionDef):
|
|
133
|
+
return InvalidReactivityCause.NESTED_FUNCTION_USE
|
|
134
|
+
elif isinstance(node, TryNodes):
|
|
135
|
+
return InvalidReactivityCause.EXCEPTION_USE
|
|
136
|
+
else:
|
|
137
|
+
warnings.warn(f"Unexpected scope node type: {node}, line={node.lineno}")
|
|
138
|
+
|
|
139
|
+
def visit_Call(self, node: ast.Call):
|
|
140
|
+
"""Records calls of use functions, i.e. solara.use_state(...)"""
|
|
141
|
+
func = node.func
|
|
142
|
+
if isinstance(func, ast.Call):
|
|
143
|
+
# Nested function, it will appear in another node later
|
|
144
|
+
return
|
|
145
|
+
id_ = None
|
|
146
|
+
if isinstance(func, ast.Name):
|
|
147
|
+
id_ = func.id
|
|
148
|
+
elif isinstance(func, ast.Attribute):
|
|
149
|
+
id_ = func.attr
|
|
150
|
+
if id_ is not None and self.matches_use_function(id_):
|
|
151
|
+
self.error_on_early_return(node, id_)
|
|
152
|
+
self.error_on_invalid_scope(node, id_)
|
|
153
|
+
self.generic_visit(node)
|
|
154
|
+
|
|
155
|
+
def visit_If(self, node: ast.If):
|
|
156
|
+
self._visit_children_using_scope(node)
|
|
157
|
+
|
|
158
|
+
def visit_For(self, node: ast.For):
|
|
159
|
+
self._visit_children_using_scope(node)
|
|
160
|
+
|
|
161
|
+
def visit_While(self, node: ast.While):
|
|
162
|
+
self._visit_children_using_scope(node)
|
|
163
|
+
|
|
164
|
+
def visit_Try(self, node: ast.Try) -> t.Any:
|
|
165
|
+
self._visit_children_using_scope(node)
|
|
166
|
+
|
|
167
|
+
if sys.version_info >= (3, 11):
|
|
168
|
+
|
|
169
|
+
def visit_TryStar(self, node: ast.TryStar) -> t.Any:
|
|
170
|
+
self._visit_children_using_scope(node)
|
|
171
|
+
|
|
172
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
173
|
+
old_function_scope = self.function_scope
|
|
174
|
+
self.function_scope = node
|
|
175
|
+
self._visit_children_using_scope(node)
|
|
176
|
+
self.function_scope = old_function_scope
|
|
177
|
+
|
|
178
|
+
def _visit_children_using_scope(self, node: ScopeNodeType):
|
|
179
|
+
outer_scope = self.outer_scope
|
|
180
|
+
self.outer_scope = node
|
|
181
|
+
for child in node.body:
|
|
182
|
+
self.visit(child)
|
|
183
|
+
self.outer_scope = outer_scope
|
|
184
|
+
|
|
185
|
+
def visit_Assign(self, node: ast.Assign):
|
|
186
|
+
self.error_on_invalid_assign(node)
|
|
187
|
+
self.generic_visit(node)
|
|
188
|
+
|
|
189
|
+
def visit_Return(self, node: ast.Return):
|
|
190
|
+
"""
|
|
191
|
+
Records the earliest return statement in the function
|
|
192
|
+
"""
|
|
193
|
+
# Returns are valid in nested functions
|
|
194
|
+
if self.function_scope != self._root_function_scope:
|
|
195
|
+
return
|
|
196
|
+
self.root_function_return = node
|
|
197
|
+
self.generic_visit(node)
|
|
198
|
+
|
|
199
|
+
def error_on_invalid_assign(self, node: ast.Assign):
|
|
200
|
+
if isinstance(node.value, ast.Attribute) and self.matches_use_function(node.value.attr):
|
|
201
|
+
line = node.lineno + self.line_offset
|
|
202
|
+
cause = InvalidReactivityCause.VARIABLE_ASSIGNMENT
|
|
203
|
+
message = f"Assigning the hook `{node.value.attr}` to a variable is not allowed since it complicates the tracking of valid hook use {_hint_supress(line, cause)}"
|
|
204
|
+
|
|
205
|
+
raise HookValidationError(
|
|
206
|
+
cause,
|
|
207
|
+
f"{self.get_source_context(line)}: {message}",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def error_on_early_return(self, use_node: ast.Call, use_node_id: str):
|
|
211
|
+
"""
|
|
212
|
+
Checks if the latest use of a reactive function occurs after the earliest return
|
|
213
|
+
"""
|
|
214
|
+
if self.root_function_return and self.root_function_return.lineno <= use_node.lineno:
|
|
215
|
+
offset_return = self.root_function_return.lineno + self.line_offset
|
|
216
|
+
offset_use = use_node.lineno + self.line_offset
|
|
217
|
+
cause = InvalidReactivityCause.USE_AFTER_RETURN
|
|
218
|
+
line = self.lines[use_node.lineno - 1]
|
|
219
|
+
if should_skip_qa(self.no_qa, cause, line):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
raise HookValidationError(
|
|
223
|
+
cause,
|
|
224
|
+
f"""{self.get_source_context(offset_use)}: `{use_node_id}` found despite early return on line {offset_return}
|
|
225
|
+
{_hint_supress(line, cause)}""",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def error_on_invalid_scope(self, use_node: ast.Call, use_node_id: str):
|
|
229
|
+
"""
|
|
230
|
+
Checks if the latest use of a reactive function occurs after the earliest return or in an invalid scope
|
|
231
|
+
such as try-except blocks.
|
|
232
|
+
"""
|
|
233
|
+
if self.outer_scope is None:
|
|
234
|
+
return
|
|
235
|
+
offset_use = use_node.lineno + self.line_offset
|
|
236
|
+
cause = self.node_to_scope_cause(self.outer_scope)
|
|
237
|
+
|
|
238
|
+
scope_line = self.outer_scope.lineno + self.line_offset
|
|
239
|
+
line = self.lines[use_node.lineno - 1]
|
|
240
|
+
if should_skip_qa(self.no_qa, cause, line):
|
|
241
|
+
return
|
|
242
|
+
raise HookValidationError(
|
|
243
|
+
cause,
|
|
244
|
+
f"""{self.get_source_context(offset_use)}: `{use_node_id}` found within a {cause.value} created on line {scope_line}
|
|
245
|
+
{_hint_supress(line, cause)}""",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def get_source_context(self, lineno):
|
|
249
|
+
return f"{self.filename}:{lineno}: {self.component.__qualname__}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _hint_supress(line, cause):
|
|
253
|
+
return f"""To suppress this check, replace the line with:
|
|
254
|
+
{line} # noqa: {cause_to_noqa_code[cause]}
|
|
255
|
+
|
|
256
|
+
Make sure you understand the consequences of this, by reading about the rules of hooks at:
|
|
257
|
+
https://solara.dev/documentation/advanced/understanding/rules-of-hooks
|
|
258
|
+
"""
|
|
File without changes
|