Skip to content

Commit bae1a87

Browse files
merge both subscribe methods
1 parent 47b052c commit bae1a87

File tree

3 files changed

+39
-44
lines changed

3 files changed

+39
-44
lines changed

nicegui/event.py

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from . import background_tasks, core, helpers
1414
from .awaitable_response import AwaitableResponse
15+
from .client import Client
1516
from .context import context
1617
from .dataclasses import KWONLY_SLOTS
1718
from .logging import log
@@ -46,46 +47,44 @@ def __init__(self) -> None:
4647
self.callbacks: list[Callback[P]] = []
4748
self.instances.append(self)
4849

49-
def subscribe(self, callback: Callable[P, Any] | Callable[[], Any]) -> None:
50+
def subscribe(self, callback: Callable[P, Any] | Callable[[], Any], *,
51+
unsubscribe_on_disconnect: bool | None = None) -> None:
5052
"""Subscribe to the event.
5153
52-
Note that the callback should not be used to update UI since it would probably cause a memory leak.
53-
Use the ``subscribe_ui`` method instead.
54+
The ``unsubscribe_on_disconnect`` can be used to explicitly define
55+
whether the callback should be automatically unsubscribed when the client disconnects.
56+
By default, the callback is automatically unsubscribed if subscribed from within a UI context
57+
to prevent memory leaks.
5458
5559
:param callback: the callback which will be called when the event is fired
60+
:param unsubscribe_on_disconnect: whether to unsubscribe the callback when the client disconnects
61+
(default: ``None`` meaning the callback is automatically unsubscribed if subscribed from within a UI context)
5662
"""
5763
frame = inspect.currentframe()
5864
assert frame is not None
5965
frame = frame.f_back
6066
assert frame is not None
61-
self.callbacks.append(Callback(func=callback, filepath=frame.f_code.co_filename, line=frame.f_lineno))
62-
63-
def subscribe_ui(self, callback: Callable[P, Any] | Callable[[], Any]) -> None:
64-
"""Subscribe to the event and automatically unsubscribe when the client disconnects.
65-
66-
This method is particularly useful for UI callbacks since it cleans up unused references to the callback
67-
which would otherwise cause a memory leak.
68-
69-
:param callback: the callback which will be called when the event is fired
70-
"""
71-
self.subscribe(callback)
67+
callback_ = Callback[P](func=callback, filepath=frame.f_code.co_filename, line=frame.f_lineno)
7268
try:
73-
self.callbacks[-1].slot = weakref.ref(context.slot)
74-
except RuntimeError as e:
75-
raise RuntimeError('Calling `subscribe_ui` outside of a UI context is not supported.') from e
76-
client = context.client
77-
78-
async def register_disconnect() -> None:
79-
try:
80-
await client.connected(timeout=10.0)
81-
client.on_disconnect(lambda: self.unsubscribe(callback))
82-
except TimeoutError:
83-
log.warning('Could not register a disconnect handler for callback %s', callback)
84-
self.unsubscribe(callback)
85-
if core.loop and core.loop.is_running():
86-
background_tasks.create(register_disconnect())
87-
else:
88-
core.app.on_startup(register_disconnect())
69+
callback_.slot = weakref.ref(context.slot)
70+
client: Client | None = context.client
71+
except RuntimeError:
72+
client = None
73+
if callback_.slot is None and unsubscribe_on_disconnect is True:
74+
raise RuntimeError('Calling `subscribe` with `unsubscribe_on_disconnect=True` outside of a UI context '
75+
'is not supported.')
76+
if client is not None and unsubscribe_on_disconnect is not False:
77+
async def register_disconnect() -> None:
78+
try:
79+
await client.connected(timeout=10.0)
80+
client.on_disconnect(lambda: self.unsubscribe(callback))
81+
except TimeoutError:
82+
log.warning('Could not register a disconnect handler for callback %s', callback)
83+
if core.loop and core.loop.is_running():
84+
background_tasks.create(register_disconnect())
85+
else:
86+
core.app.on_startup(register_disconnect())
87+
self.callbacks.append(callback_)
8988

9089
def unsubscribe(self, callback: Callable[P, Any] | Callable[[], Any]) -> None:
9190
"""Unsubscribe a callback from the event.

tests/test_event.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ async def test_event(user: User):
1010
@ui.page('/')
1111
def page():
1212
ui.button('Click me', on_click=event.emit)
13-
event.subscribe_ui(lambda: ui.notify('clicked'))
13+
event.subscribe(lambda: ui.notify('clicked'))
1414

1515
await user.open('/')
1616
user.find('Click me').click()
@@ -23,8 +23,8 @@ async def test_event_with_args(user: User):
2323
@ui.page('/')
2424
def page():
2525
ui.button('Click me', on_click=lambda: event.emit(42))
26-
event.subscribe_ui(lambda: ui.notify('clicked'))
27-
event.subscribe_ui(lambda x: ui.notify(f'{x = }'))
26+
event.subscribe(lambda: ui.notify('clicked'))
27+
event.subscribe(lambda x: ui.notify(f'{x = }'))
2828

2929
await user.open('/')
3030
user.find('Click me').click()
@@ -39,7 +39,7 @@ async def test_event_with_async_handler(user: User):
3939
def page():
4040
ui.button('Click me', on_click=event.emit)
4141

42-
@event.subscribe_ui
42+
@event.subscribe
4343
async def handler():
4444
await asyncio.sleep(0.1)
4545
ui.notify('clicked')
@@ -58,7 +58,7 @@ def page():
5858
nonlocal card
5959
ui.button('Click me', on_click=event.emit)
6060
with ui.card() as card:
61-
event.subscribe_ui(lambda: ui.label('clicked'))
61+
event.subscribe(lambda: ui.label('clicked'))
6262

6363
await user.open('/')
6464
user.find('Click me').click()

website/documentation/content/event_documentation.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,28 @@
99
Events are a powerful tool distribute information between different parts of your code.
1010
The following demo shows how to define an event, subscribe a callback and emit it.
1111
12-
Note that there is also a `subscribe` method for non-UI callbacks.
13-
But to run the handler in the current UI context and to automatically unsubscribe when the client disconnects,
14-
use the `subscribe_ui` method.
15-
1612
Handlers can be synchronous or asynchronous.
1713
They can also take arguments if the event contains arguments.
1814
''')
1915
def events_demo():
2016
from nicegui import Event
2117

2218
click = Event()
23-
click.subscribe_ui(lambda: ui.notify('clicked'))
19+
click.subscribe(lambda: ui.notify('clicked'))
2420

2521
ui.button('Click me', on_click=click.emit)
2622

2723

2824
@doc.demo('Events with arguments', '''
2925
Events can also include arguments.
30-
The callback can take use them, but also ignore them if they are not needed.
26+
The callback can use them, but also ignore them if they are not needed.
3127
''')
3228
def events_with_arguments():
3329
from nicegui import Event
3430

3531
answer = Event[int]()
36-
answer.subscribe_ui(lambda: ui.notify('Answer found!'))
37-
answer.subscribe_ui(lambda x: ui.notify(f'{x = }'))
32+
answer.subscribe(lambda: ui.notify('Answer found!'))
33+
answer.subscribe(lambda x: ui.notify(f'{x = }'))
3834

3935
ui.button('Answer', on_click=lambda: answer.emit(42))
4036

@@ -51,7 +47,7 @@ def emitting_vs_calling_events():
5147

5248
click = Event()
5349

54-
@click.subscribe_ui
50+
@click.subscribe
5551
async def handler():
5652
n = ui.notification('Running...')
5753
await asyncio.sleep(1)

0 commit comments

Comments
 (0)