Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
python: ["3.7", "3.13"]
python: ["3.7", "3.14"]
exclude:
- os: windows
python: 3.7
Expand Down Expand Up @@ -368,7 +368,7 @@ jobs:
# osx should work fine (and we test that locally often)
os: [ubuntu, windows]
# just 1 version, it's heavy
python-version: [3.9]
python-version: [3.9, 3.14]
ipywidgets_major: ["7", "8"]
include:
- ipywidgets_major: "7"
Expand Down Expand Up @@ -593,7 +593,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
python: [3.7, 3.12]
python: [3.7, 3.14]
ipywidgets: ["7.7", "8.0"]
exclude:
- os: windows
Expand Down
64 changes: 44 additions & 20 deletions packages/solara-enterprise/solara_enterprise/ssg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import threading
import time
import sys
import typing
import urllib
import weakref
Expand Down Expand Up @@ -65,26 +66,49 @@ def _worker_with_cleanup(*args, **kwargs):


class CleanupThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
def _adjust_thread_count(self):
# copy of the original code with _worker replaced
# if idle threads are available, don't spin new threads
if self._idle_semaphore.acquire(timeout=0):
return

# When the executor gets lost, the weakref callback will wake up
# the worker threads.
def weakref_cb(_, q=self._work_queue):
q.put(None)

num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads)
t = threading.Thread(
name=thread_name, target=_worker_with_cleanup, args=(weakref.ref(self, weakref_cb), self._work_queue, self._initializer, self._initargs)
)
t.start()
self._threads.add(t) # type: ignore
concurrent.futures.thread._threads_queues[t] = self._work_queue # type: ignore
if sys.version_info < (3, 14):

def _adjust_thread_count(self):
# copy of the original code with _worker replaced
# if idle threads are available, don't spin new threads
if self._idle_semaphore.acquire(timeout=0):
return

# When the executor gets lost, the weakref callback will wake up
# the worker threads.
def weakref_cb(_, q=self._work_queue):
q.put(None)

num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads)
t = threading.Thread(
name=thread_name, target=_worker_with_cleanup, args=(weakref.ref(self, weakref_cb), self._work_queue, self._initializer, self._initargs)
)
t.start()
self._threads.add(t) # type: ignore
concurrent.futures.thread._threads_queues[t] = self._work_queue # type: ignore
else:

def _adjust_thread_count(self):
# if idle threads are available, don't spin new threads
if self._idle_semaphore.acquire(timeout=0):
return

# When the executor gets lost, the weakref callback will wake up
# the worker threads.
def weakref_cb(_, q=self._work_queue):
q.put(None)

num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads)
t = threading.Thread(
name=thread_name, target=_worker_with_cleanup, args=(weakref.ref(self, weakref_cb), self._create_worker_context(), self._work_queue)
)
t.start()
self._threads.add(t)
concurrent.futures.thread._threads_queues[t] = self._work_queue # type: ignore


def ssg_crawl(base_url: str):
Expand Down
6 changes: 5 additions & 1 deletion packages/solara-meta/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ assets = [

documentation = [
"bqplot",
"altair",
"altair ; python_version < '3.14'",
"altair >= 6.0.0 ; python_version >= '3.14'",
"folium",
"ipycanvas",
"ipyleaflet",
Expand Down Expand Up @@ -90,6 +91,9 @@ dev = [
"numpy<2",
]

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build.targets.wheel]
include = ["LICENSE"]

Expand Down
27 changes: 21 additions & 6 deletions solara/components/figure_altair.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,27 @@ def FigureAltair(
bundle = chart._repr_mimebundle_()[0]
key4 = "application/vnd.vegalite.v4+json"
key5 = "application/vnd.vegalite.v5+json"
if key4 not in bundle and key5 not in bundle:
raise KeyError(f"{key4} and {key5} not in mimebundle:\n\n{bundle}")
spec = bundle.get(key5, bundle.get(key4))
return solara.widgets.VegaLite.element(
spec=spec, on_click=on_click, listen_to_click=on_click is not None, on_hover=on_hover, listen_to_hover=on_hover is not None
)
key6 = "application/vnd.vegalite.v6.json"
if key6 in bundle:
version = 6
spec = bundle.get(key6)
elif key5 in bundle:
version = 5
spec = bundle.get(key5)
elif key4 in bundle:
version = 4
spec = bundle.get(key4)
else:
raise KeyError(f"{key4} and {key5} and {key6} not in mimebundle:\n\n{bundle}")

return solara.widgets.VegaLite.element(
version=version,
spec=spec,
on_click=on_click,
listen_to_click=on_click is not None,
on_hover=on_hover,
listen_to_hover=on_hover is not None,
)


# alias for backward compatibility
Expand Down
14 changes: 13 additions & 1 deletion solara/server/kernel_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ class Local(threading.local):
local = Local()


def _get_event_loop():
# not 100% why this is needed for Python 3.14 (maybe also lower?)
# but during the tests, even though we have pytest-tornasync installed, there is not
# event loop available.
current_loop = asyncio._get_running_loop()
if current_loop is not None:
return current_loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop


class PageStatus(enum.Enum):
CONNECTED = "connected"
DISCONNECTED = "disconnected"
Expand Down Expand Up @@ -77,7 +89,7 @@ class VirtualKernelContext:
closed_event: threading.Event = dataclasses.field(default_factory=threading.Event)
_on_close_callbacks: List[Callable[[], None]] = dataclasses.field(default_factory=list)
lock: threading.RLock = dataclasses.field(default_factory=threading.RLock)
event_loop: asyncio.AbstractEventLoop = dataclasses.field(default_factory=asyncio.get_event_loop)
event_loop: asyncio.AbstractEventLoop = dataclasses.field(default_factory=_get_event_loop)

def __post_init__(self):
with self:
Expand Down
6 changes: 3 additions & 3 deletions solara/widgets/vue/vegalite.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ module.exports = {
require.config({
map: {
'*': {
'vega': `${this.getCdn()}/vega@5/build/vega.min.js`,
'vega-lite': `${this.getCdn()}/vega-lite@5/build/vega-lite.min.js`,
'vega-embed': `${this.getCdn()}/vega-embed@6/build/vega-embed.min.js`,
'vega': `${this.getCdn()}/vega@${this.version}/build/vega.min.js`,
'vega-lite': `${this.getCdn()}/vega-lite@${this.version}/build/vega-lite.min.js`,
'vega-embed': `${this.getCdn()}/vega-embed@${this.version}/build/vega-embed.min.js`,
}
}
})
Expand Down
1 change: 1 addition & 0 deletions solara/widgets/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class VegaLite(v.VuetifyTemplate):
on_click = traitlets.traitlets.Callable(None, allow_none=True)
on_hover = traitlets.traitlets.Callable(None, allow_none=True)
cdn = traitlets.Unicode(None, allow_none=True).tag(sync=True)
version = traitlets.Int(6).tag(sync=True)

def vue_altair_click(self, *args):
if self.on_click is not None:
Expand Down
32 changes: 14 additions & 18 deletions tests/unit/toestand_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import concurrent
import dataclasses
import threading
import unittest.mock
from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, TypeVar, cast
Expand Down Expand Up @@ -879,28 +879,24 @@ class DataFrame:


def test_thread_local():
def test1():
assert solara.lab.thread_local.reactive_used is None
with concurrent.futures.ThreadPoolExecutor(1) as w1, concurrent.futures.ThreadPoolExecutor(1) as w2:

t = threading.Thread(target=test1)
t.start()
t.join()
def test1():
assert solara.toestand.thread_local.reactive_used is None

def test2():
myset: Set[solara.lab.ValueBase] = set()
solara.lab.local.reactive_used = myset
assert solara.lab.thread_local.reactive_used is myset
w1.submit(test1).result()

t = threading.Thread(target=test2)
t.start()
t.join()
def test2():
myset: Set[solara.toestand.ValueBase] = set()
solara.toestand.thread_local.reactive_used = myset
assert solara.toestand.thread_local.reactive_used is myset

def test3():
assert solara.lab.thread_local.reactive_used is None
w1.submit(test2).result()

t = threading.Thread(target=test3)
t.start()
t.join()
def test3():
assert solara.toestand.thread_local.reactive_used is None

w2.submit(test3).result()


def test_reactive_auto_subscribe(kernel_context):
Expand Down
Loading