diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84cbaec29..9b78ebafe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - id: check-toml @@ -17,7 +17,7 @@ repos: args: [--py36-plus] language_version: python3.12 - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 + rev: v2.4.3 hooks: - id: codespell files: ^.*\.(py|md|yaml|js|ts|ipynb)$ @@ -25,14 +25,14 @@ repos: additional_dependencies: - tomli - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.16.6 hooks: - id: ruff stages: [pre-commit] - id: ruff-format stages: [pre-commit] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v2.3.1 hooks: - id: mypy name: mypy (ipyvuetify 1) diff --git a/CLAUDE.md b/CLAUDE.md index 768ff3e19..64fc7f9cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,10 +82,12 @@ Unit tests use `solara.render_fixed()` for testing components: ```python import solara + @solara.component def Test(): return solara.FileBrowser(tmpdir, watch=True) + div, rc = solara.render_fixed(Test(), handle_error=False) # Access widgets via div.children file_list = div.children[1] diff --git a/docs/design-redis-state-persistence.md b/docs/design-redis-state-persistence.md index f985abaca..d598a34fe 100644 --- a/docs/design-redis-state-persistence.md +++ b/docs/design-redis-state-persistence.md @@ -129,14 +129,14 @@ fragility by using AST-node resolution instead of line parsing. `executing` is a a transitive dependency (via ipython); declare it explicitly in `pyproject.toml`. ```python -count = solara.reactive(0, persist=True) # key auto-derived: "myapp.pages:count" +count = solara.reactive(0, persist=True) # key auto-derived: "myapp.pages:count" count = solara.reactive(0, persist=True, key="myapp.count") # explicit, refactor-proof filters = solara.reactive( Filters(), persist=solara.PersistConfig( key="myapp.filters", - serializer="json", # default; or "pickle" (gated, §4.2), or a registered custom codec + serializer="json", # default; or "pickle" (gated, §4.2), or a registered custom codec ), ) ``` @@ -629,18 +629,18 @@ package never eagerly drags server modules on solara.state's account: ```python class State(BaseSettings): - backend: str = "" # "" = disabled; name in state_backend_map ("redis", "memory", ...) - url: str = "" # backend DSN, e.g. "redis://localhost:6379/0" - secret_keys: List[str] = [] # HMAC keys; verify-any, sign-first (rotation §4.2). REQUIRED when enabled - allow_pickle: bool = False # deployer gate; serializer="pickle" raises without it (§4.2) - ttl: Optional[str] = None # default: kernel.cull_timeout - orphan_cull_timeout: str = "5m" # applies only with a shared backend (§5.4) - prefix: str = "solara:state:" # key prefix / table name, backend-interpreted + backend: str = "" # "" = disabled; name in state_backend_map ("redis", "memory", ...) + url: str = "" # backend DSN, e.g. "redis://localhost:6379/0" + secret_keys: List[str] = [] # HMAC keys; verify-any, sign-first (rotation §4.2). REQUIRED when enabled + allow_pickle: bool = False # deployer gate; serializer="pickle" raises without it (§4.2) + ttl: Optional[str] = None # default: kernel.cull_timeout + orphan_cull_timeout: str = "5m" # applies only with a shared backend (§5.4) + prefix: str = "solara:state:" # key prefix / table name, backend-interpreted flush_debounce: str = "300ms" - connect_timeout: float = 0.3 # hard cap on takeover/flush blocking - breaker_failures: int = 3 # circuit breaker: consecutive failures to open - breaker_window: str = "30s" # open duration before a half-open probe - schema_tag: str = "" # state-schema tag ("" -> derived); mismatch => clean state reset (§6.1) + connect_timeout: float = 0.3 # hard cap on takeover/flush blocking + breaker_failures: int = 3 # circuit breaker: consecutive failures to open + breaker_window: str = "30s" # open duration before a half-open probe + schema_tag: str = "" # state-schema tag ("" -> derived); mismatch => clean state reset (§6.1) auto_remount: Optional[bool] = None # None: on iff backend set; can force on/off bailout_storm_threshold: float = 0.5 # bail-out rate valve (§4.3) @@ -672,11 +672,14 @@ class StateBackend(Protocol): def takeover(self, kernel_id, session_hmac) -> tuple[int, dict[str, bytes]]: """Atomically claim ownership and read all envelopes: bump-then-read as one unit. Returns (new_generation, fields). Raises/None on identity mismatch.""" + def flush(self, kernel_id, generation, fields: dict[str, bytes], ttl) -> bool: """Atomically write the batch iff generation still matches (fenced). Returns False when rejected.""" + def peek_generation(self, kernel_id) -> Optional[int]: """Cheap ownership check (reuse branch, owns_state() probe).""" + def delete(self, kernel_id) -> None: """Atomic wipe on clean close.""" ``` diff --git a/docs/memory-leak-detection.md b/docs/memory-leak-detection.md index 16c62b9f8..fe35f9415 100644 --- a/docs/memory-leak-detection.md +++ b/docs/memory-leak-detection.md @@ -191,12 +191,15 @@ The custom `SolaraInteractiveShell` (in `solara/server/shell.py`) disables sever def init_sys_modules(self): pass # don't create a __main__, it will cause a mem leak + def init_prefilter(self): pass # avoid consuming memory + def init_history(self): self.history_manager = Mock() # don't accumulate history + def reset(self, new_session=True, aggressive=False): pass # IPython's reset() calls gc.collect() which causes slow shutdowns ``` @@ -221,6 +224,7 @@ When adding a new feature that manages resources (widgets, connections, tasks, c import gc import weakref + def test_my_feature_does_not_leak(): # 1. Create the object obj = create_my_object() @@ -270,7 +274,7 @@ import objgraph chain = objgraph.find_backref_chain(leaked_obj, objgraph.is_proper_module, max_depth=20) for i, item in enumerate(chain): type_name = type(item).__name__ - if hasattr(item, '__name__'): + if hasattr(item, "__name__"): type_name += f" ({item.__name__})" print(f" [{i:2d}] {type_name}") ``` diff --git a/docs/memory-leak-found-report-previous-reactive-watch.md b/docs/memory-leak-found-report-previous-reactive-watch.md index aeafa6816..4e7acde55 100644 --- a/docs/memory-leak-found-report-previous-reactive-watch.md +++ b/docs/memory-leak-found-report-previous-reactive-watch.md @@ -96,10 +96,11 @@ The breakthrough was using `objgraph.find_backref_chain()`: ```python import objgraph + chain = objgraph.find_backref_chain(rc_ref(), objgraph.is_proper_module, max_depth=20) for i, item in enumerate(chain): type_name = type(item).__name__ - if hasattr(item, '__name__'): + if hasattr(item, "__name__"): type_name += f" ({item.__name__})" print(f" [{i:2d}] {type_name}") ``` @@ -133,6 +134,7 @@ import solara import solara.server.kernel import solara.server.kernel_context + def _scoped_render(component_el): widget, rc = solara.render_fixed(component_el, handle_error=False) rc_ref = weakref.ref(rc) @@ -140,13 +142,13 @@ def _scoped_render(component_el): del widget, rc return rc_ref + kernel = solara.server.kernel.Kernel() -kc = solara.server.kernel_context.VirtualKernelContext( - id=str(uuid.uuid4()), kernel=kernel, session_id="s1" -) +kc = solara.server.kernel_context.VirtualKernelContext(id=str(uuid.uuid4()), kernel=kernel, session_id="s1") with kc: # Any component that reads a @slab.computed value from your_app import ComponentThatReadsComputed + rc_ref = _scoped_render(ComponentThatReadsComputed()) for _ in range(20): gc.collect() diff --git a/docs/memory-leak-strategy-render-context-weakref.md b/docs/memory-leak-strategy-render-context-weakref.md index 6f2a5e426..1bac1025f 100644 --- a/docs/memory-leak-strategy-render-context-weakref.md +++ b/docs/memory-leak-strategy-render-context-weakref.md @@ -83,6 +83,7 @@ This strategy was validated with two components: one clean, one intentionally le ```python _leaked_references: list = [] + @solara.component def LeakyComponent(): text, set_text = solara.use_state("hello") @@ -135,7 +136,7 @@ obj = rc_ref() chain = objgraph.find_backref_chain(obj, objgraph.is_proper_module, max_depth=20) for i, item in enumerate(chain): type_name = type(item).__name__ - if hasattr(item, '__name__'): + if hasattr(item, "__name__"): type_name += f" ({item.__name__})" print(f" [{i:2d}] {type_name}") ``` diff --git a/docs/memory-usage-inspection.md b/docs/memory-usage-inspection.md index f7c3922db..c4648ea26 100644 --- a/docs/memory-usage-inspection.md +++ b/docs/memory-usage-inspection.md @@ -104,9 +104,10 @@ More macOS traps (measured): ```python import psutil + p = psutil.Process() -p.memory_info().rss # OK on Linux; macOS: trends unreliable -p.memory_full_info().uss # Linux only in practice +p.memory_info().rss # OK on Linux; macOS: trends unreliable +p.memory_full_info().uss # Linux only in practice ``` ```bash @@ -208,7 +209,8 @@ Once the protocol says "memory grows", use these to find where. ```python import tracemalloc -tracemalloc.start(10) # keep 10 stack frames per allocation + +tracemalloc.start(10) # keep 10 stack frames per allocation snap1 = tracemalloc.take_snapshot() # ... one or more workload cycles ... snap2 = tracemalloc.take_snapshot() @@ -227,8 +229,8 @@ shows up as 0 bytes. C extensions that allocate outside the Python allocator import sys, gc from collections import Counter -sys.getallocatedblocks() # live allocations Python knows about -sys._debugmallocstats() # pymalloc arena/pool stats: how full are the arenas? +sys.getallocatedblocks() # live allocations Python knows about +sys._debugmallocstats() # pymalloc arena/pool stats: how full are the arenas? # writes to C-level stderr: contextlib.redirect_stderr CANNOT capture it, # redirect fd 2 instead (python app.py 2>stats.txt) diff --git a/packages/pytest-ipywidgets/README.md b/packages/pytest-ipywidgets/README.md index a875b4aad..b298e104a 100644 --- a/packages/pytest-ipywidgets/README.md +++ b/packages/pytest-ipywidgets/README.md @@ -21,6 +21,7 @@ import ipywidgets as widgets import playwright.sync_api from IPython.display import display + def test_widget_button_solara(solara_test, page_session: playwright.sync_api.Page): # this all runs in-process button = widgets.Button(description="Click Me!") diff --git a/solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md b/solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md index 747163c95..9b918eddf 100644 --- a/solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +++ b/solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md @@ -161,8 +161,7 @@ tabular.py, we see the `Page` component takes an additional arguments. ```python @solara.component -def Page(name: str): - ... +def Page(name: str): ... ``` diff --git a/solara/website/pages/documentation/advanced/content/10-howto/30-testing.md b/solara/website/pages/documentation/advanced/content/10-howto/30-testing.md index b40b2f29f..93b1c1135 100644 --- a/solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +++ b/solara/website/pages/documentation/advanced/content/10-howto/30-testing.md @@ -176,6 +176,7 @@ import ipywidgets as widgets import playwright.sync_api from IPython.display import display + def test_widget_button_solara(solara_test, page_session: playwright.sync_api.Page): # The test code runs in the same process as solara-server (which runs in a separate thread) # Note: this test uses ipywidgets directly, not solara components. diff --git a/solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md b/solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md index d9b6e37f5..726a85ea0 100644 --- a/solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +++ b/solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md @@ -10,6 +10,7 @@ After `solara` is imported, every widget class has an extra `.element(...)` meth ```python import ipywidgets + button_element = ipywidgets.Button.element(description="Click me") ``` diff --git a/solara/website/pages/documentation/advanced/content/10-howto/55-vue_esm_components.md b/solara/website/pages/documentation/advanced/content/10-howto/55-vue_esm_components.md index 80f53870b..2946bb4e3 100644 --- a/solara/website/pages/documentation/advanced/content/10-howto/55-vue_esm_components.md +++ b/solara/website/pages/documentation/advanced/content/10-howto/55-vue_esm_components.md @@ -135,9 +135,7 @@ class MyWidget(ipyvue.VueTemplate): """ ).tag(sync=True) enabled = traitlets.Bool(False).tag(sync=True) - components = traitlets.Dict( - {"my-toggle": {"esm_module": "my-components", "esm_export": "Toggle"}} - ).tag(sync=True, **widget_serialization) + components = traitlets.Dict({"my-toggle": {"esm_module": "my-components", "esm_export": "Toggle"}}).tag(sync=True, **widget_serialization) def vue_on_input(self, value): self.enabled = value diff --git a/solara/website/pages/documentation/advanced/content/15-reference/95-caching.md b/solara/website/pages/documentation/advanced/content/15-reference/95-caching.md index 2a6f666c4..7ab14e132 100644 --- a/solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +++ b/solara/website/pages/documentation/advanced/content/15-reference/95-caching.md @@ -10,6 +10,7 @@ object as normal dict, e.g.: ```python import solara + solara.cache.storage["my-key"] = expensive_function_call() assert "my-key" in solara.cache.storage ``` @@ -48,6 +49,7 @@ The [cachetools](https://cachetools.readthedocs.io/) project has some caching ty ```python import cachetools import solara + solara.cache.storage = cachetools.FIFOCache(maxsize=100) ``` @@ -63,9 +65,11 @@ This can be set using code: ```python import solara + solara.cache.configure("memory-size") # or more explicit import solara_enterprise.cache.memory_size + solara.cache.storage = solara_enterprise.cache.memory_size.MemorySize( max_size="1GB" # already the default ) @@ -88,11 +92,14 @@ This can be set using code: ```python import solara + solara.cache.configure("disk") # or more explicit import solara_enterprise.cache.disk + solara.cache.storage = solara_enterprise.cache.disk.Disk( - max_size="10GB", path="/home/maarten/.solara/cache" # already the default + max_size="10GB", + path="/home/maarten/.solara/cache", # already the default ) ``` @@ -111,9 +118,11 @@ This can be set using code: ```python import solara + solara.cache.configure("redis") # or more explicit import solara_enterprise.cache.redis + # optionally pass in a redis.Client object solara.cache.storage = solara_enterprise.cache.redis.Redis() ``` @@ -129,17 +138,17 @@ We can chain a few caches to get a multilevel cache. This allows us to get the b ```python import solara + solara.cache.configure("memory,disk,redis") # or more explicit import solara_enterprise.cache.multi_level import solara_enterprise.cache.disk import solara_enterprise.cache.redis + l1 = solara.cache.Memory() l2 = solara_enterprise.cache.disk.Disk() l3 = solara_enterprise.cache.redis.Redis() -solara.cache.storage = solara_enterprise.cache.multi_level.MultiLevel( - l1, l2, l3 -) +solara.cache.storage = solara_enterprise.cache.multi_level.MultiLevel(l1, l2, l3) ``` Or configured using an environment variable: diff --git a/solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md b/solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md index 3da314916..46c1d66b6 100644 --- a/solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +++ b/solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md @@ -19,7 +19,7 @@ def Page(): if x < 10: y, set_y = solara.use_state(2) # state 'slot' 2 else: - y, set_y = solara.use_state("foo") # *also* state 'slot' 2 + y, set_y = solara.use_state("foo") # *also* state 'slot' 2 solara.Text("Done") ``` @@ -153,6 +153,7 @@ import solara def Page(): def inner(): solara.use_state(1) # will use_state always be called? Difficult to analyze, so don't do it + inner() solara.Text("Done") ``` diff --git a/solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md b/solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md index 247270ac6..636683770 100644 --- a/solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +++ b/solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md @@ -94,7 +94,6 @@ Here we are using the top-level Row as a context manager (with the name `main`). Context managers are a Python language feature, there are two ways to use them: ```python - with some_anonymous_context_manager(): print("some code") diff --git a/solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md b/solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md index 9c3144465..15dad3b51 100644 --- a/solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +++ b/solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md @@ -78,27 +78,31 @@ routes = [ path="docs", # matches '/docs' children=[ # route level == 2 - solara.Route(path="basics", children=[ # matches '/docs/basics' - # route level == 3 - solara.Route(path="react"), # matches '/docs/basics/react' - solara.Route(path="ipywidgets"), # matches '/docs/basics/ipywidgets' - solara.Route(path="solara"), # matches '/docs/basics/solara' - ]), - solara.Route(path="advanced") # matches '/docs/advanced' + solara.Route( + path="basics", + children=[ # matches '/docs/basics' + # route level == 3 + solara.Route(path="react"), # matches '/docs/basics/react' + solara.Route(path="ipywidgets"), # matches '/docs/basics/ipywidgets' + solara.Route(path="solara"), # matches '/docs/basics/solara' + ], + ), + solara.Route(path="advanced"), # matches '/docs/advanced' ], ), solara.Route( path="blog", # route level == 1 children=[ - solara.Route(path="/"), # matches '/blog' - solara.Route(path="foo"), # matches '/blog/foo' - solara.Route(path="bar"), # matches '/blog/bar' + solara.Route(path="/"), # matches '/blog' + solara.Route(path="foo"), # matches '/blog/foo' + solara.Route(path="bar"), # matches '/blog/bar' ], ), - solara.Route(path="contact") # matches '/contact' + solara.Route(path="contact"), # matches '/contact' ] + # Lets assume our pathname is `/docs/basics/react`, @solara.component def Page(): @@ -108,7 +112,7 @@ def Page(): # routes_current_level is [routes[0], routes[1], routes[2], routes[3]], i.e.: # [solara.Route(path="/"), solara.Route(path="docs", children=[...]), # solara.Route(path="blog", children=[...]), solara.Route(path="contact")] - if route_current is None: # no matching route + if route_current is None: # no matching route return solara.Error("oops, page not found") else: # we could render some top level navigation here based on route_current_level and route_current @@ -129,12 +133,11 @@ def MyFirstLevelChildComponent(): # route_current is routes[1].children[0], i.e. solara.Route(path="basics", children=[...]) # routes_current_level is [routes[1].children[0], routes[1].children[1]], i.e.: # [solara.Route(path="basics", children=[...]), solara.Route(path="advanced")] - if route_current is None: # no matching route + if route_current is None: # no matching route return solara.Error("oops, page not found") else: # we could render some mid level navigation here based on route_current_level and route_current return MySecondLevelChildComponent() - ``` And the `MySecondLevelChildComponent` component is responsible for rendering the third level navigation: @@ -147,7 +150,7 @@ def MySecondLevelChildComponent(): # route_current is routes[1].children[0].children[0], i.e. solara.Route(path="react") # routes_current_level is [routes[1].children[0].children[0], routes[1].children[0].children[1], routes[1].children[0].children[2]], i.e. # [solara.Route(path="react"), solara.Route(path="ipywidgets"), solara.Route(path="solara")] - if route_current is None: # no matching route + if route_current is None: # no matching route return solara.Error("oops, page not found") else: # we could render some mid level navigation here based on route_current_level and route_current @@ -159,7 +162,6 @@ def MySecondLevelChildComponent(): return DocsBasicSolara() else: return solara.Error("oops, not possible!") - ``` From this code, we can see we are free how we transform the routes into the state of the UI (i.e. which components are rendered). @@ -180,7 +182,7 @@ routes = [ children=demo.routes, label="Demo", ), - ... + ..., ] ``` @@ -201,8 +203,7 @@ Therefore you should never use the `route.path` for navigation since the route o Using [`resolve_path`](/documentation/api/routing/resolve_path) we can request the full url for navigation. ```python -def resolve_path(path_or_route: Union[str, solara.Route], level=0) -> str: - ... +def resolve_path(path_or_route: Union[str, solara.Route], level=0) -> str: ... ``` We can pass this full URL to the [`solara.Link`](/documentation/components/advanced/link) component, e.g. like: diff --git a/solara/website/pages/documentation/advanced/content/20-understanding/55-state-persistence.md b/solara/website/pages/documentation/advanced/content/20-understanding/55-state-persistence.md index 9e33f542a..960438ff2 100644 --- a/solara/website/pages/documentation/advanced/content/20-understanding/55-state-persistence.md +++ b/solara/website/pages/documentation/advanced/content/20-understanding/55-state-persistence.md @@ -107,7 +107,7 @@ user's state to another on restore. ```python def make_query_state(): - return solara.reactive("", persist=True) # raises: no stable per-instance key + return solara.reactive("", persist=True) # raises: no stable per-instance key ``` The error names the definition site and shows the correct per-instance fix: @@ -248,9 +248,7 @@ solara.state.register_codec( lambda blob: shapely.from_wkb(blob), ) -area = solara.reactive( - DEFAULT_AREA, persist=solara.PersistConfig(key="myapp.area", serializer="geo") -) +area = solara.reactive(DEFAULT_AREA, persist=solara.PersistConfig(key="myapp.area", serializer="geo")) ``` ### The `pickle` codec @@ -302,13 +300,15 @@ success and is still wrong**. ```python # WRONG -continent = solara.reactive("") # not persisted (looks harmless) +continent = solara.reactive("") # not persisted (looks harmless) + @solara.lab.computed -def country_options() -> list[str]: # derived: recomputed on the new kernel - return countries_in(continent.value) # continent == "" -> [] +def country_options() -> list[str]: # derived: recomputed on the new kernel + return countries_in(continent.value) # continent == "" -> [] + -country = solara.reactive("", persist=True, key="geo.country") # persisted leaf into a non-persisted set +country = solara.reactive("", persist=True, key="geo.country") # persisted leaf into a non-persisted set ``` After a failover `continent` is back at `""`, `country_options` recomputes to `[]`, and the @@ -330,6 +330,7 @@ instance may also see newer data). A decision ladder: ```python city_id = solara.reactive(None, persist=True, key="geo.city_id") + @solara.lab.computed def country() -> str | None: return city_lookup(city_id.value).country if city_id.value is not None else None @@ -349,11 +350,12 @@ instance may also see newer data). A decision ladder: continent = solara.reactive("", persist=True, key="geo.continent") country = solara.reactive("", persist=True, key="geo.country") + @solara.component def CountrySelect(): - options = country_options.value # recomputed from the restored continent + options = country_options.value # recomputed from the restored continent if country.value and country.value not in options: - country.set("") # orphaned by restore or a data change: reset + country.set("") # orphaned by restore or a data change: reset ... ``` @@ -409,7 +411,7 @@ def charge(): payment.charge( order_id=order_id.value, idempotency_key=order_id.value, # stable across a re-run -> charged at most once - fencing_epoch=epoch, # lets the payment service reject an older instance + fencing_epoch=epoch, # lets the payment service reject an older instance ) ``` diff --git a/solara/website/pages/documentation/api/hooks/use_memo.md b/solara/website/pages/documentation/api/hooks/use_memo.md index 231a45d7c..a326d3b8f 100644 --- a/solara/website/pages/documentation/api/hooks/use_memo.md +++ b/solara/website/pages/documentation/api/hooks/use_memo.md @@ -1,12 +1,7 @@ # use_memo ```python -def use_memo( - f: Any, - dependencies: Any | None = None, - debug_name: str = None -) -> Any: - ... +def use_memo(f: Any, dependencies: Any | None = None, debug_name: str = None) -> Any: ... ``` `use_memo` stores ([memoize](https://en.wikipedia.org/wiki/Memoization)) the function return on first render, and then excludes it from being re-executed, except when one of the `dependencies` changes. `dependencies` can take the value `None`, in which case dependencies are automatically obtained from nonlocal variables. If an empty list is passed as `dependencies` instead, the function is only executed once over the entire lifetime of the component. diff --git a/solara/website/pages/documentation/api/hooks/use_thread.md b/solara/website/pages/documentation/api/hooks/use_thread.md index 4055de8b0..41e550027 100644 --- a/solara/website/pages/documentation/api/hooks/use_thread.md +++ b/solara/website/pages/documentation/api/hooks/use_thread.md @@ -10,8 +10,7 @@ def use_thread( ], intrusive_cancel=True, dependencies=[], -) -> Result[T]: - ... +) -> Result[T]: ... ``` `use_thread` can be used to run medium or long running jobs in a separate thread to avoid blocking the render loop. diff --git a/solara/website/pages/documentation/getting_started/content/00-quickstart.md b/solara/website/pages/documentation/getting_started/content/00-quickstart.md index c0a4bbcd3..b2728e74a 100644 --- a/solara/website/pages/documentation/getting_started/content/00-quickstart.md +++ b/solara/website/pages/documentation/getting_started/content/00-quickstart.md @@ -73,6 +73,7 @@ From Jupyter Notebook (classic) or Jupyter Lab, navigate to the same directory a ```python from sol import Page + display(Page()) ``` diff --git a/solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md b/solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md index a9ed8db0d..75d83d301 100644 --- a/solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +++ b/solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md @@ -26,6 +26,7 @@ clicks = 0 print("I get run at startup, and for every page request") + def on_click(button): global clicks clicks += 1 @@ -80,7 +81,6 @@ classic ipywidgets. Use the [.widget(...)](/documentation/api/utilities/widget) method on a component to create a widget that can be used in your existing classic ipywidget application. ```python - import ipywidgets as widgets import solara diff --git a/solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md b/solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md index dae55219b..0a12de71f 100644 --- a/solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +++ b/solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md @@ -126,6 +126,7 @@ def square(name): x_squared = x**2 st.markdown(f"{name}: {x} squared = {x_squared}") + square("x") square("y") ``` diff --git a/solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md b/solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md index 7db3c1d68..ed65a2ee5 100644 --- a/solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +++ b/solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md @@ -114,7 +114,6 @@ app.layout = html.Div( if __name__ == "__main__": app.run_server(debug=True) - ``` ### In Solara diff --git a/solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md b/solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md index b3c07f9d9..7b6d0697f 100644 --- a/solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +++ b/solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md @@ -48,6 +48,7 @@ Here's an example of a simple Solara component that displays a button: ```python import solara + @solara.component def MyButton(): solara.Button("Click me!") @@ -63,6 +64,7 @@ Here's an example of using the `MyButton` component we defined earlier: ```python import solara + @solara.component def MyApp(): MyButton() @@ -79,9 +81,11 @@ Here's an example of a Solara component that displays a button and responds to c ```python import solara + def on_button_click(event): print("Button clicked!") + @solara.component def MyInteractiveButton(): solara.Button("Click me!", on_click=on_button_click) @@ -103,6 +107,7 @@ Here's an example of a Solara component that accepts an argument: ```python import solara + @solara.component def MyButton(text): solara.Button(text) diff --git a/solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md b/solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md index 104100c50..f69544077 100644 --- a/solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +++ b/solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md @@ -102,7 +102,7 @@ a = 1 b = a # a.value = 2 # ERROR: numbers are immutable a = 2 # instead, re-assign a new value, the number 1 will not change -assert b == 1 # b is still 1 +assert b == 1 # b is still 1 ``` However, many objects in Python are mutable, including lists and dictionaries or potentially user defined classes. This means that you can change the value of a list, dictionary, or user defined class in place without creating a new object. ```python diff --git a/solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md b/solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md index b47321a9c..a2165f8d8 100644 --- a/solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +++ b/solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md @@ -78,6 +78,7 @@ import solara.server.flask app = Flask(__name__) app.register_blueprint(solara.server.flask.blueprint, url_prefix="/solara/") + @app.route("/") def hello_world(): return "
Hello, World!
" @@ -178,8 +179,9 @@ If you use Voila, [you can use those deployment options](https://voila.readthedo Make sure you run a notebook where you display the app, e.g. ```python @solara.component -def Page(): - ... +def Page(): ... + + element = Page() display(element) ``` @@ -201,10 +203,13 @@ import solara @solara.component def ButtonClick(label="Hi"): clicks, set_clicks = solara.use_state(0) + def increment(): set_clicks(clicks + 1) + return solara.Button(f"{label}: Clicked {clicks} times", on_click=increment) + # this creates just an element, Panel doesn't know what to do with that element = ButtonClick("Solara+Panel") # we explicitly ask Reacton to render it, and give us the widget