Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,22 +17,22 @@ 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)$
args: ["--skip=**/solara_portal/**"]
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)
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
29 changes: 16 additions & 13 deletions docs/design-redis-state-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
)
```
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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."""
```
Expand Down
6 changes: 5 additions & 1 deletion docs/memory-leak-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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()
Expand Down Expand Up @@ -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}")
```
Expand Down
10 changes: 6 additions & 4 deletions docs/memory-leak-found-report-previous-reactive-watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
```
Expand Down Expand Up @@ -133,20 +134,21 @@ 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)
rc.close()
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()
Expand Down
3 changes: 2 additions & 1 deletion docs/memory-leak-strategy-render-context-weakref.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}")
```
Expand Down
12 changes: 7 additions & 5 deletions docs/memory-usage-inspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)

Expand Down
1 change: 1 addition & 0 deletions packages/pytest-ipywidgets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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): ...
```


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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)
```

Expand All @@ -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
)
Expand All @@ -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
)
```

Expand All @@ -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()
```
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
```

Expand Down Expand Up @@ -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")
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading
Loading