zauberzeug-nicegui-6295
When a page awaits `ui.run_javascript(...)` before its browser client has established a connection, deleting or pruning that client leaves the page task waiting indefinitely or until a timeout. The awaiting call should instead finish promptly with `None`. Calling `run_javascript` on an already deleted client should likewise resolve immediately with `None`.
Speculative page loading must also recover when its temporary client is deleted. If the user subsequently navigates to that page, the new client should connect and the page logic should be evaluated again, producing the current JavaScript result rather than remaining stuck with the deleted prefetch client or its earlier empty result.
What the hidden tests pin: a page coroutine that is waiting for its client to connect (`await client.connected()`, which `ui.run_javascript` performs before sending) must be released promptly when that client is deleted or pruned via `Client.prune_instances(...)`, not only when a socket connects, and the awaited `run_javascript` then resolves with `None`; the test observes the page function resuming with `None` within a fraction of a second of the prune. Calling `run_javascript` on an already deleted client resolves with `None` immediately. The speculative-loading test records an event log and expects, after the prefetch client is pruned, `answer: None` for the prefetch and then, on real navigation, the page function called again with a new client connecting and the real JavaScript result recorded.
Hidden tests · 2 fail-to-pass, 10 pass-to-passrun after the agent submits, in a clean verifier
Test patch · 58 lines
diff --git a/tests/test_javascript.py b/tests/test_javascript.py
index 2f02632186..b656c087ff 100644
--- a/tests/test_javascript.py
+++ b/tests/test_javascript.py
@@ -1,5 +1,7 @@
-from nicegui import ui
-from nicegui.testing import Screen
+import asyncio
+
+from nicegui import Client, ui
+from nicegui.testing import Screen, User
def test_run_javascript_on_button_press(screen: Screen):
@@ -93,3 +95,25 @@ async def runB():
screen.click('runB')
screen.should_contain('A: 1')
screen.should_contain('B: 2')
+
+
+async def test_awaited_run_javascript_resolves_when_client_is_deleted(user: User):
+ """The task awaiting run_javascript must not time out or wait forever when the client is deleted, e.g. after a disconnect."""
+ clients: list[Client] = []
+ results = []
+
+ @ui.page('/')
+ async def page():
+ clients.append(ui.context.client)
+ results.append(await ui.run_javascript('window.innerWidth'))
+
+ await user.http_client.get('/') # request the page without ever opening the websocket
+ await asyncio.sleep(0)
+ assert not results
+
+ Client.prune_instances(client_age_threshold=0) # delete the client, waking up connected()
+ await asyncio.sleep(0.1) # let the page function resume
+ assert results == [None] # the page function resumed with None instead of timing out
+
+ # calling on a deleted client resolves immediately
+ assert await clients[0].run_javascript('window.innerWidth') is None
diff --git a/tests/test_speculative_loading.py b/tests/test_speculative_loading.py
index f502d053c5..458212b2e5 100644
--- a/tests/test_speculative_loading.py
+++ b/tests/test_speculative_loading.py
@@ -117,8 +117,11 @@ async def answer() -> None:
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
- assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
- 'answer() should re-evaluate after prefetch client was pruned'
+ assert event_log.items == [
+ 'answer: called', 'connect: /',
+ 'answer: None',
+ 'answer: called', 'connect: /answer', 'answer: 42',
+ ], 'answer() should re-evaluate after prefetch client was pruned'
screen.should_contain('all done')
Reference fix · 2 files, +20 −5the upstream merge, used only for grading calibration
The agent could not see this: the repository holds one commit and the sandbox has no network. Leak audit.
nicegui/client.py, nicegui/functions/javascript.py
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34f8e..7a16f53faa 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -269,8 +269,9 @@ async def connected(self, timeout: float | None = None) -> None:
"""Block execution until the client is connected.
:param timeout: timeout in seconds (default: ``None``)
+ :raises ClientConnectionTimeout: if ``timeout`` elapses first
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self.is_deleted:
return
self._waiting_for_connection.set()
self._connected.clear()
@@ -298,20 +299,30 @@ def run_javascript(self, code: str, *, timeout: float = 1.0) -> AwaitableRespons
Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*).
This might delay the execution of the JavaScript code and is not covered by the ``timeout`` parameter.
+ *Updated in version 3.17.0: Awaiting the response resolves with ``None`` when the client has been deleted,
+ e.g. because the browser tab was closed.*
+
:param code: JavaScript code to run
:param timeout: timeout in seconds (default: 1.0)
- :return: AwaitableResponse that can be awaited to get the result of the JavaScript code
+ :return: AwaitableResponse that can be awaited to get the result of the JavaScript code,
+ or ``None`` if the client has been deleted
"""
request_id = str(uuid.uuid4())
target_id = self._temporary_socket_id or self.id
- def send_and_forget():
+ def send_and_forget() -> None:
+ if self.is_deleted:
+ return
self.outbox.enqueue_message('run_javascript', {'code': code}, target_id)
- async def send_and_wait():
+ async def send_and_wait() -> Any:
+ if self.is_deleted:
+ return None
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
+ if self.is_deleted:
+ return None
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
diff --git a/nicegui/functions/javascript.py b/nicegui/functions/javascript.py
index a6cf4d75c9..5051bba210 100644
--- a/nicegui/functions/javascript.py
+++ b/nicegui/functions/javascript.py
@@ -16,9 +16,13 @@ def run_javascript(code: str, *, timeout: float = 1.0) -> AwaitableResponse:
Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*).
This might delay the execution of the JavaScript code and is not covered by the ``timeout`` parameter.
+ *Updated in version 3.17.0: Awaiting the response resolves with ``None`` when the client has been deleted,
+ e.g. because the browser tab was closed.*
+
:param code: JavaScript code to run
:param timeout: timeout in seconds (default: 1.0)
- :return: AwaitableResponse that can be awaited to get the result of the JavaScript code
+ :return: AwaitableResponse that can be awaited to get the result of the JavaScript code,
+ or ``None`` if the client has been deleted
"""
return context.client.run_javascript(code, timeout=timeout)
GLM-5.3 Flash · attempt 2 · solved · 39 steps · $0.023
Submitted diff · 55 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..99b1983 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,14 +270,16 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self.is_deleted:
return
self._waiting_for_connection.set()
- self._connected.clear()
- try:
- await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
- except asyncio.TimeoutError as e:
- raise ClientConnectionTimeout(self) from e
+ while not self.has_socket_connection and not self.is_deleted:
+ self._connected.clear()
+ try:
+ prefetch = self._request is not None and _is_prefetch(self.request)
+ await asyncio.wait_for(self._connected.wait(), timeout=None if prefetch else timeout)
+ except asyncio.TimeoutError as e:
+ raise ClientConnectionTimeout(self) from e
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
@@ -310,8 +312,12 @@ class Client:
self.outbox.enqueue_message('run_javascript', {'code': code}, target_id)
async def send_and_wait():
+ if self.is_deleted:
+ return None
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
+ if self.is_deleted:
+ return None
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
@@ -500,8 +506,12 @@ class Client:
self.outbox.stop()
del Client.instances[self.id]
self._deleted = True
- self._connected.set() # for terminating connected() waits
- self._connected.clear()
+ # NOTE: the event is set in a thread-safe way and stays set:
+ # waiting coroutines in connected() wake up, notice the deletion and return
+ if core.loop is not None:
+ core.loop.call_soon_threadsafe(self._connected.set)
+ else:
+ self._connected.set()
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""$ grep -n "connected" /repo/nicegui/client.py | head -30
output · 16 lines
65: """Read the ``client_id`` a socket connected with, or ``None`` if its environment does not carry one.""" 101: self._connected = asyncio.Event() 160: """Whether the client is connected.""" 268: async def connected(self, timeout: float | None = None) -> None: 269: """Block execution until the client is connected. 276: self._connected.clear() 278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) 282: async def disconnected(self) -> None: 285: await self.connected() 297: Obviously the JavaScript code is only executed after the client is connected. 298: Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*). 314: await self.connected()
4 more lines
382: self._connected.set() 402: # keep the tab_id as long as any socket is live, e.g. one that reconnected before this one was reaped 503: self._connected.set() # for terminating connected() waits 504: self._connected.clear()
$ sed -n 250,340p /repo/nicegui/client.py; echo ---; sed -n 470,530p /repo/nicegui/client.py
output · 27 lines
'translations': translations.get(language, translations['en-US']),
'prefix': prefix,
'tailwind': core.app.config.tailwind,
'unocss': core.app.config.unocss,
'headwind_css': HEADWIND_CONTENT if core.app.config.tailwind else '',
'prod_js': core.app.config.prod_js,
'socket_io_js_query_params': socket_io_js_query_params,
'socket_io_js_extra_headers': core.app.config.socket_io_js_extra_headers,
'socket_io_js_transports': core.app.config.socket_io_js_transports,
},
status_code=status_code,
headers={'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'},15 more lines
)
def resolve_title(self) -> str:
"""Return the title of the page."""
return self.page.resolve_title() if self.title is None else s
…[5956 chars omitted]…
if (
not client.has_socket_connection and
not client._delete_tasks and # pylint: disable=protected-access
client.created <= time.time() - client_age_threshold
)
]
for client in stale_clients:
log.debug(f'Pruning stale client {client.id}')
client.delete()$ grep -n "_deleted\b\|_deleted =\|def has_socket_connection\|_is_prefetch" /repo/nicegui/client.py; echo ---; sed -n 1,120p /repo/nicegui/client.py
output · 37 lines
107: self._deleted = False 159: def has_socket_connection(self) -> bool: 164: def is_deleted(self) -> bool: 166: return self._deleted 219: 'implicit_handshake': not _is_prefetch(request), 278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) 465: element._deleted = True # pylint: disable=protected-access 502: self._deleted = True 508: if self._deleted: 535:def _is_prefetch(request: Request) -> bool: --- from __future__ import annotations
25 more lines
import asyncio
import inspect
import time
import uuid
from collections import defaultdict
from collections.abc import Callable, Iterable
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, cast
from urllib.parse import parse_qs, quote
from fastapi import Request
from fastapi.responses import Response
from f
…[3208 chars omitted]…
ble[[Exception], Any] | Callable[[], Any]] = []
self.page = page
self.outbox = Outbox(self)
if self._request is not None:
self._request.scope['nicegui_page_path'] = self.page.path
with Element('q-layout', _client=self).props('view="hhh lpr fff"').classes('nicegui-layout') as self.layout:
with Element('q-page-container') as self.page_container:$ grep -rn "prefetch\|speculat" /repo/nicegui/*.py /repo/nicegui/**/*.py | grep -v test | head -40
output · 4 lines
/repo/nicegui/client.py:219: 'implicit_handshake': not _is_prefetch(request), /repo/nicegui/client.py:278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) /repo/nicegui/client.py:535:def _is_prefetch(request: Request) -> bool: /repo/nicegui/client.py:537: return 'prefetch' in purpose and 'prerender' not in purpose
$ sed -n 530,560p /repo/nicegui/client.py; grep -rn "prune_instances" /repo/nicegui -r; sed -n 1,80p /repo/nicegui/awaitable_response.py
output · 34 lines
except Exception:
log.exception('Error while pruning clients')
def _is_prefetch(request: Request) -> bool:
purpose = (request.headers.get('Sec-Purpose') or request.headers.get('Purpose') or '').lower()
return 'prefetch' in purpose and 'prerender' not in purpose
def _did_user_request_markdown(request: Request) -> bool:
"""Check whether the request has text/markdown in its Accept header or is a known agentic user agent."""
accept = request.headers.get('accept', '').strip().lower()22 more lines
if 'text/markdown' in accept:
return True
user_agent = request.headers.get('user-agent', '').lower()
return any(token in user_agent for token in AI_AGENT_TOKENS)
/repo/nicegui/app/app.py:89: self.timer(10, Client.prune_instances)
/repo/nicegui/client.py:515: def prune_instances(cls, *, client_age_threshold: float = 60.0) -> None:
from colle
…[1146 chars omitted]…
lf.wait_for_result().__await__()
class NullResponse(AwaitableResponse):
def __init__(self) -> None: # pylint: disable=super-init-not-called
"""Null Response
This class can be used to create an AwaitableResponse that does nothing.
In contrast to AwaitableResponse, it can be created without a running event loop.
"""
def __await__(self):
yield from []$ sed -n 155,175p /repo/nicegui/client.py; ls /repo/tests | head -40
output · 56 lines
"""
return self.request.client.host if self.request.client is not None else ''
@property
def has_socket_connection(self) -> bool:
"""Whether the client is connected."""
return self.tab_id is not None
@property
def is_deleted(self) -> bool:
"""Whether the client has been deleted (e.g. by browser disconnect after ``reconnect_timeout``)."""
return self._deleted44 more lines
@property
def head_html(self) -> str:
"""The HTML code to be inserted in the <head> of the page template."""
return self.shared_head_html + self._head_html
@property
def body_html(self) -> str:
"""The HTML code to be inserted in the <body> of the page template."""
README.md
__init__.py
conftest.py
media
test_add_html.py
test_aggrid.py
test_air.py
test_altair.py
test_alternate_ui_frameworks.py
test_anywidget.py
test_api_route
…[146 chars omitted]…
test_button_group.py
test_card.py
test_carousel.py
test_chat_message.py
test_chip.py
test_clipboard.py
test_code.py
test_codemirror.py
test_codemirror_keybindings.py
test_codemirror_line_anchors.py
test_color_input.py
test_color_picker.py
test_colors.py
test_context_menu.py
test_dark_mode.py
test_date.py
test_date_input.py
test_defaults.py
test_dialog.py
test_download.py
test_echart.py
test_editor.pyNow apply the fix:
$ cd /repo && python - <<'PY'
output · 29 lines
return self.page.resolve_title() if self.title is None else self.title
async def connected(self, timeout: float | None = None) -> None:
"""Block execution until the client is connected.
:param timeout: timeout in seconds (default: ``None``)
"""
if self.has_socket_connection or self.is_deleted:
return
self._waiting_for_connection.set()
while not self.has_socket_connection and not self.is_deleted:
self._connected.clear()17 more lines
try:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
raise ClientConnectionTimeout(self) from e
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
if not self.has_socket_connection:
…[1738 chars omitted]…
_tab: bool = False) -> None:
"""Open a new page in the client."""
path = target if isinstance(target, str) else self.page_routes[target]
self.outbox.enqueue_message('open', {'path': path, 'new_tab': new_tab}, self.id)
def download(self, src: str | bytes, filename: str | None = None, media_type: str = '') -> None:
"""Download a file from a given URL or raw bytes."""$ cd /repo && cat > /tmp/repro.py <<'EOF'
output · 22 lines
Traceback (most recent call last):
File "<stdin>", line 30, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 14, in main10 more lines
File "/repo/nicegui/client.py", line 322, in run_javascript
return AwaitableResponse(send_and_forget, send_and_wait)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/awaitable_response.py", line 21, in __init__
background_tasks.create(self._fire(), name='fire')
File "/repo/nicegui/background_tasks.py", line 41, in create
assert core.loop is not None
^^^^^^^^^^^^^^^^^^^^^
AssertionError
sys:1: RuntimeWarning: coroutine 'AwaitableResponse._fire' was never awaited$ cd /repo && python - <<'PY'
output · 13 lines
Traceback (most recent call last):
File "<stdin>", line 29, in <module>
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 12, in main
File "/usr/local/lib/python3.12/asyncio/tasks.py", line 420, in create_task
task = loop.create_task(coro)
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 457, in create_task
task = tasks.Task(coro, loop=self, name=name, context=context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^1 more lines
TypeError: a coroutine was expected, got <nicegui.awaitable_response.AwaitableResponse object at 0x703ac8f40f20>
$ cd /repo && python - <<'PY'
output · 21 lines
/repo/nicegui/client.py:279: RuntimeWarning: coroutine 'Event.wait' was never awaited
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Traceback (most recent call last):
File "<stdin>", line 33, in <module>
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 19, in main
File "/usr/local/lib/python3.12/asyncio/tasks.py", line 520, in wait_for
return await fut
^^^^^^^^^9 more lines
File "<stdin>", line 14, in wait_js
File "/repo/nicegui/client.py", line 317, in send_and_wait
await self.connected()
File "/repo/nicegui/client.py", line 279, in connected
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
^^^^^^^^^^^^
File "/repo/nicegui/client.py", line 146, in request
raise RuntimeError('Request is not set')
RuntimeError: Request is not set$ cd /repo && python - <<'PY'
output · 3 lines
result None elapsed 0.000 log [] deleted client result None pruned result None
$ cd /repo && ls tests | grep -i "client\|javasc\|specul\|page" && python -m pytest tests/test_client.py tests/test_awaitable_response.py tests/test_javascript*.py -q 2>&1 | tail -5
output · 11 lines
test_javascript.py test_page.py test_page_title.py test_root_page.py test_speculative_loading.py test_sub_pages.py test_sub_pages_match_path.py ERROR: file or directory not found: tests/test_client.py no tests ran in 0.02s
$ cd /repo && python -m pytest tests/test_javascript.py tests/test_awaitable_response.py tests/test_speculative_loading.py tests/test_page.py -q 2>&1 | tail -5
output · 5 lines
--------------------------- Captured stdout teardown --------------------------- Storing screenshot to /repo/screenshots/2688/test_prefetch_connects_after_navigation.failed.png =========================== short test summary info ============================ FAILED tests/test_speculative_loading.py::test_prefetch_connects_after_navigation 1 failed, 46 passed in 66.35s (0:01:06)
$ cd /repo && python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | head -80
output · 28 lines
F [100%]
=================================== FAILURES ===================================
___________________ test_prefetch_connects_after_navigation ____________________
screen = <nicegui.testing.screen.Screen object at 0x7e578cb956a0>
event_log = <tests.test_speculative_loading.EventLog object at 0x7e578aa26ba0>
def test_prefetch_connects_after_navigation(screen: Screen, event_log: EventLog) -> None:
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.page('/')
def root() -> None:16 more lines
add_speculation_rule('/answer', kind='prefetch')
ui.link('answer', '/answer')
@ui.page('/answer')
async def answer() -> None:
event_log.append('answer: called')
result = await ui.run_javascript('41 + 1'
…[3061 chars omitted]…
self.config.get_loop_factory())
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/repo/nicegui/page.py", line 190, in wait_for_result
return await result
File "/repo/tests/test_speculative_loading.py", line 98, in answer$ cd /repo && sed -n 100,130p tests/test_speculative_loading.py && sed -n 150,200p nicegui/page.py
output · 28 lines
screen.open('/')
event_log.wait_for('connect: /')
screen.wait(1)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('answer: 42')
assert event_log.items == ['answer: called', 'connect: /', 'connect: /answer', 'answer: 42'], \
'answer() should not re-evaluate despite small run_javascript timeout because in prefetch the page response timeout is used'
screen.should_contain('all done')
event_log.items.clear()
16 more lines
screen.open('/')
event_log.wait_for('answer: called')
Client.prune_instances(client_age_threshold=0)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer
…[2729 chars omitted]…
name=f'wait for result of page "{client.page.path}"',
handle_exceptions=False)
task_wait_for_connection = background_tasks.create(
client._waiting_for_connection.wait(), # pylint: disable=protected-access
name=f'wait for connection {client.page.path}',
)$ cd /repo && sed -n 200,260p nicegui/page.py
output · 24 lines
)
done, _ = await asyncio.wait([
task,
task_wait_for_connection,
], timeout=self.response_timeout, return_when=asyncio.FIRST_COMPLETED)
if not done:
task.cancel()
log.warning(f'Response for {client.page.path} not ready after {self.response_timeout} seconds')
client.delete()
return create_terminal_error_page(
f'The page took longer than the response_timeout of {self.response_timeout} seconds to build. '
'Await ui.context.client.connected() before long-running setup or increase response_timeout.',12 more lines
request)
if not task_wait_for_connection.done():
task_wait_for_connection.cancel()
if ta
…[1370 chars omitted]…
dding request as a parameter so we can pass it to the client in the decorated function
if 'request' not in {p.name for p in parameters}:
request = inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request)
parameters.insert(0, request)
decorated.__signature__ = inspect.Signature(parameters) # type: ignore
return decorated$ cd /repo && grep -n "def __enter__\|def __exit__" -A 12 nicegui/client.py | sed -n 1,40p; grep -rn "prune\|delete" tests/test_speculative_loading.py | head
output · 19 lines
178: def __enter__(self) -> Self: 179- self.content.__enter__() 180- return self 181- 182: def __exit__(self, *_) -> None: 183- self.content.__exit__() 184- 185- def build_response(self, request: Request, status_code: int = 200) -> Response: 186- """Build a FastAPI response for the client.""" 187- # After this point the initial HTML (incl. title/head/body) is emitted; later changes must be pushed via JS. 188- self._response_built = True 189- if self.page.resolve_markdown() and _did_user_request_markdown(request):
7 more lines
190- parts = []
191- if title := self.resolve_title():
192- parts.append(f'# {title}')
193- if markdown := self.layout._render_markdown(): # pylint: disable=protected-access
194- parts.append(markdown)
114: Client.prune_instances(client_age_threshold=0)
121: 'answer() should re-evaluate after prefetch client was pruned'$ cd /repo && python - <<'PY'
output · 10 lines
PAGE DEBUG result None deleted False PAGE DEBUG not done False PAGE DEBUG result None deleted False PAGE DEBUG result None deleted False PAGE DEBUG not done False PAGE DEBUG result None deleted False PAGE DEBUG not done False PAGE DEBUG result None deleted False Storing screenshot to /repo/screenshots/3407/test_prefetch_connects_after_navigation.failed.png 1 failed in 9.32s
$ cd /repo && git log --oneline -5 -- tests/test_speculative_loading.py nicegui/client.py && git log --oneline -3
output · 2 lines
8f55384 base 8f55384 base
$ cd /repo && git checkout nicegui/page.py && python - <<'PY'
output · 26 lines
Updated 1 path from the index
"""
for t in self.delete_handlers:
self.safe_invoke(t)
for t in core.app._delete_handlers: # pylint: disable=protected-access
self.safe_invoke(t)
self._waiting_for_disconnect.clear()
self._deleted_event.set()
# NOTE: removing all elements before removing the client from Client.instances ensures
# that elements are marked as deleted before their client weakref can die (Timer._should_stop relies on this)
self.remove_all_elements()
self.outbox.stop()14 more lines
del Client.instances[self.id]
self._deleted = True
# NOTE: the event is set in a thread-safe way and stays set:
# waiting coroutines in connected() wake up, notice the deletion and return
if core.loop is not None:
core.loop.call_soon_threadsafe(self._connected.set)
…[67 chars omitted]…
tence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""
if self._deleted:
helpers.warn_once('Client has been deleted but is still being used. '
'This is most likely a bug in your application code. '
'See https://github.com/zauberzeug/nicegui/issues/3028 for more information.',$ cd /repo && python -m pytest tests/test_speculative_loading.py -q 2>&1 | tail -3
output · 3 lines
=========================== short test summary info ============================ FAILED tests/test_speculative_loading.py::test_prefetch_connects_after_navigation 1 failed, 4 passed in 28.88s
$ cd /repo && python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | sed -n 1,40p
output · 30 lines
F [100%]
=================================== FAILURES ===================================
___________________ test_prefetch_connects_after_navigation ____________________
screen = <nicegui.testing.screen.Screen object at 0x723c71c544a0>
event_log = <tests.test_speculative_loading.EventLog object at 0x723c71c566c0>
def test_prefetch_connects_after_navigation(screen: Screen, event_log: EventLog) -> None:
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.page('/')
def root() -> None:18 more lines
add_speculation_rule('/answer', kind='prefetch')
ui.link('answer', '/answer')
@ui.page('/answer')
async def answer() -> None:
event_log.append('answer: called')
result = await ui.run_javascript('41 + 1'
…[466 chars omitted]…
luate despite small run_javascript timeout because in prefetch the page response timeout is used'
screen.should_contain('all done')
event_log.items.clear()
screen.open('/')
event_log.wait_for('answer: called')
Client.prune_instances(client_age_threshold=0)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')$ cd /repo && python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | sed -n 40,70p
output · 24 lines
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
> assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
'answer() should re-evaluate after prefetch client was pruned'
E AssertionError: answer() should re-evaluate after prefetch client was pruned
E assert ['answer: cal... 'answer: 42'] == ['answer: cal... 'answer: 42']
E
E At index 2 diff: 'answer: None' != 'answer: called'
E Left contains one more item: 'answer: 42'
E Use -v to get more diff
12 more lines
tests/test_speculative_loading.py:120: AssertionError
------------------------------ Captured log setup ------------------------------
WARNING selenium.webdriver.common.selenium_manager:selenium_manager.py:168 Exception managing chrome: error sending requ
…[737 chars omitted]…
self.run()
File "/usr/local/lib/python3.12/threading.py", line 1012, in run
self._target(*self._args, **self._kwargs)
File "/repo/nicegui/testing/screen.py", line 52, in <lambda>
self.server_thread = threading.Thread(target=lambda: ui.run(**self.ui_run_kwargs))
File "/repo/nicegui/ui_run.py", line 311, in run
Server.instance.run()
File "/repo/nicegui/server.py", line 49, in run$ cd /repo && python - <<'PY'
output · 10 lines
PDEBUG end result None deleted False 86e23574-a5d3-47a6-9f07-527bc22048da PDEBUG notdone False taskdone False connwaitdone True c557c1a1-170b-4b97-a111-8aa9ab95338c PDEBUG end result None deleted False c557c1a1-170b-4b97-a111-8aa9ab95338c PDEBUG end result None deleted False 74bac04c-14d0-4293-a951-405b989f91d6 PDEBUG notdone False taskdone False connwaitdone True cac20a29-423b-463c-bfa9-078e632a8162 PDEBUG end result None deleted False cac20a29-423b-463c-bfa9-078e632a8162 PDEBUG notdone False taskdone False connwaitdone True 7510bf80-faad-46fd-a81c-5db93fee4ab6 PDEBUG end result None deleted False 7510bf80-faad-46fd-a81c-5db93fee4ab6 Storing screenshot to /repo/screenshots/4438/test_prefetch_connects_after_navigation.failed.png 1 failed in 9.10s
$ cd /repo && python - <<'PY'
$ cd /repo && python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | tail -20
output · 20 lines
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed File "<frozen importlib._bootstrap>", line 1387, in _gcd_import File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed File "<frozen importlib._bootstrap>", line 1387, in _gcd_import File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 935, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 999, in exec_module
8 more lines
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/repo/nicegui/__init__.py", line 2, in <module>
from .api_router import APIRouter
File "/repo/nicegui/api_router.py", line 6, in <module>
from .page import page as ui_page
File "/repo/nicegui/page.py", line 179
with Client(self, request=request) as client:
IndentationError: unexpected indent$ cd /repo && sed -n 175,182p nicegui/page.py
output · 8 lines
request = dec_kwargs['request']
# cleaning up the keyword args so the signature is consistent with "func" again
dec_kwargs = {k: v for k, v in dec_kwargs.items() if k in parameters_of_decorated_func}
print('PDEBUG req', request.url, request.headers.get('sec-purpose'), flush=True)
with Client(self, request=request) as client:
if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
dec_kwargs['client'] = client
try:$ cd /repo && sed -i "s/^print('PDEBUG req'/ print('PDEBUG req'/" nicegui/page.py && python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | grep -E "PDEBUG|passed|failed"output · 15 lines
PDEBUG req http://localhost:50569/ None PDEBUG end result None deleted False ad46bf9c-8522-45ab-b974-a219e9cd295f PDEBUG req http://localhost:50569/answer prefetch PDEBUG notdone False taskdone False connwaitdone True 04153895-0f2d-45e8-a0df-9f784c8b5cd7 PDEBUG end result None deleted False 04153895-0f2d-45e8-a0df-9f784c8b5cd7 PDEBUG req http://localhost:50569/ None PDEBUG end result None deleted False 1c1c0c0a-aadd-4e4b-ae28-738c44201287 PDEBUG req http://localhost:50569/answer prefetch PDEBUG notdone False taskdone False connwaitdone True 856c6866-1c85-4b52-bd4c-ea0e7d031977 PDEBUG end result None deleted False 856c6866-1c85-4b52-bd4c-ea0e7d031977 PDEBUG req http://localhost:50569/answer None PDEBUG notdone False taskdone False connwaitdone True 7ed9ebe6-3ab1-4741-9d04-c9ddaba29d13
3 more lines
PDEBUG end result None deleted False 7ed9ebe6-3ab1-4741-9d04-c9ddaba29d13 Storing screenshot to /repo/screenshots/4692/test_prefetch_connects_after_navigation.failed.png 1 failed in 9.34s
$ cd /repo && python - <<'PY'
output · 17 lines
print('ACTUAL LOG', event_log.items, flush=True)
PDEBUG req http://localhost:57753/ None
PDEBUG end result None deleted False a2c6fe0f-06d3-4f69-baaa-954ec4428c19
PDEBUG req http://localhost:57753/answer prefetch
PDEBUG notdone False taskdone False connwaitdone True b6eb4dee-52d9-432b-a82e-897fa99746e3
PDEBUG end result None deleted False b6eb4dee-52d9-432b-a82e-897fa99746e3
PDEBUG req http://localhost:57753/ None
PDEBUG end result None deleted False bc83a3ab-598b-4192-87f9-b995bb949859
PDEBUG req http://localhost:57753/answer prefetch
PDEBUG notdone False taskdone False connwaitdone True 1fa1faad-11bd-4121-89dd-6f2f188ebfb7
PDEBUG end result None deleted False 1fa1faad-11bd-4121-89dd-6f2f188ebfb7
PDEBUG req http://localhost:57753/answer None5 more lines
PDEBUG notdone False taskdone False connwaitdone True f17a0aa6-2ef7-40c2-a57d-9f4ce3f0f315 PDEBUG end result None deleted False f17a0aa6-2ef7-40c2-a57d-9f4ce3f0f315 ACTUAL LOG ['answer: called', 'connect: /', 'answer: None', 'answer: called', 'connect: /answer', 'answer: 42'] Storing screenshot to /repo/screenshots/4936/test_prefetch_connects_after_navigation.failed.png 1 failed in 9.83s
$ cd /repo && git checkout tests/test_speculative_loading.py nicegui/page.py && grep -rln "prune\|client.delete\|is_deleted" tests | head
output · 11 lines
Updated 2 paths from the index tests/test_button.py tests/test_dialog.py tests/test_element_delete.py tests/test_event.py tests/test_keep_alive.py tests/test_lifecycle.py tests/test_page.py tests/test_reconnect.py tests/test_skip_link.py tests/test_speculative_loading.py
$ cd /repo && python -m pytest tests/test_page.py tests/test_javascript.py tests/test_awaitable_response.py tests/test_lifecycle.py tests/test_reconnect.py tests/test_keep_alive.py -q 2>&1 | tail -3
output · 2 lines
............................................................ [100%] 60 passed in 83.34s (0:01:23)
$ cd /repo && python -m pytest tests/test_element_delete.py tests/test_event.py tests/test_skip_link.py tests/test_dialog.py -q 2>&1 | tail -3
output · 2 lines
............................................. [100%] 45 passed in 50.19s
$ cd /repo && git diff && python - <<'PY'
output · 33 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..2c580ae 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,14 +270,15 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self.is_deleted:
return
self._waiting_for_connection.set()21 more lines
- self._connected.clear()
- try:
- await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
- except asyncio.TimeoutError as e:
- raise ClientConnectionTimeout(self) from e
+ while not self.has_socket_connection and not self.is_deleted:
+ self._connected.clear()
+ try:
+ await asyncio.wait_for(self._connected.wait(), timeout=None if self._request is not
…[1162 chars omitted]…
tines in connected() wake up, notice the deletion and return
+ if core.loop is not None:
+ core.loop.call_soon_threadsafe(self._connected.set)
+ else:
+ self._connected.set()
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""
delete-release: None 0.000s
deleted-client: None
prune-release: None$ cd /repo && grep -n "line-length\|max-line" pyproject.toml; ruff check nicegui/client.py 2>/dev/null || python -m flake8 --max-line-length=120 nicegui/client.py | head
output · 3 lines
197:max-line-length = 120 202:line-length = 120 /usr/local/bin/python: No module named flake8
$ cd /repo && awk 'length > 120 {print FILENAME":"FNR": "length}' nicegui/client.pyoutput · 4 lines
nicegui/client.py:237: 124 nicegui/client.py:279: 149 nicegui/client.py:299: 122 nicegui/client.py:493: 126
$ cd /repo && git stash && awk 'length > 120 {print FNR": "length}' nicegui/client.py; git stash popoutput · 12 lines
Saved working directory and index state WIP on master: 8f55384 base
237: 124
298: 122
488: 126
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: nicegui/client.py
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (900350da8ba464fa06961454b2eed1793f10b70d)$ cd /repo && python - <<'PY'
output · 5 lines
237 300 494 ...... [100%] 6 passed in 13.23s
$ cd /repo && python - <<'PY'
output · 3 lines
delete-release: None deleted-client: None prune-release: None
$ done: {"notes": "Fix in nicegui/client.py: (1) Client.delete() now wakes connection waiters thread-safely via core.loop.call_soon_threadsafe(self._connected.set) (event stays set), so coroutines blocked in connected() \u2014 including prefetch clients with no timeout \u2014 are released promptly even when delete/prune is called from anotoutput · 3 lines
…[740 chars omitted]… er: 42'), matching the issue's described hidden-test expectations; the checked-in test_speculative_loading assertion (which omits 'answer: None') was left untouched."} submitted.
GLM-5.3 Flash · attempt 3 · solved · 34 steps · $0.021
Submitted diff · 52 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..57617ac 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,7 +270,7 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self._deleted:
return
self._waiting_for_connection.set()
self._connected.clear()
@@ -278,6 +278,8 @@ class Client:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
raise ClientConnectionTimeout(self) from e
+ if self._deleted:
+ return
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
@@ -307,11 +309,17 @@ class Client:
target_id = self._temporary_socket_id or self.id
def send_and_forget():
+ if self._deleted:
+ return
self.outbox.enqueue_message('run_javascript', {'code': code}, target_id)
async def send_and_wait():
+ if self._deleted:
+ return None
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
+ if self._deleted:
+ return None
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
@@ -500,8 +508,9 @@ class Client:
self.outbox.stop()
del Client.instances[self.id]
self._deleted = True
- self._connected.set() # for terminating connected() waits
- self._connected.clear()
+ # NOTE: keep the event set so that pending connected() waits are released;
+ # connected() checks the deleted flag to avoid treating this as a real connection.
+ self._connected.set()
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""$ ls /repo; ls /repo/nicegui | head -50
output · 88 lines
AGENTS.md CITATION.cff CLAUDE.md CODE_OF_CONDUCT.md CONTRIBUTING.md DEPENDENCIES.md LICENSE README.md REVIEW.md SECURITY.md deploy.py development.dockerfile
76 more lines
docker-compose.yml docker-entrypoint.sh docker.sh examples extract_core_libraries.py fetch_github_stats.py fetch_google_fonts.py fetch_languages.py fetch_milestone.py fly-entrypoint.sh fly.dockerfile fly.toml main.py nicegui nicegui.code-workspace package-lock.json package.json pyproject.toml release.dockerfile screenshot.png set_scale.sh summarize_dependencies.py test_startup.sh tests uv.lock website __init__.py air.py api_router.py app awaitable_response.py background_tasks.py binding.py classes.py client.py context.py core.py dataclasses.py defaults.py dependencies.py element.py element_filter.py elements error.py event.py event_listener.py events.py favicon.py functions helpers html.py javascript_request.py json language.py llms.md logging.py middlewares.py native nicegui.py observables.py optional_features.py outbox.py page.py page_arguments.py persistence props.py py.typed run.py scripts server.py slot.py static staticfiles.py storage.py style.py sub_pages_router.py
$ sed -n 1,200p /repo/nicegui/client.py
output · 37 lines
from __future__ import annotations import asyncio import inspect import time import uuid from collections import defaultdict from collections.abc import Callable, Iterable from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, cast from urllib.parse import parse_qs, quote
25 more lines
from fastapi import Request
from fastapi.responses import Response
from fastapi.templating import Jinja2Templates
from starlette.background import BackgroundTask
from typing_extensions import Self
from . import background_tasks, binding, core, helpers, json, storage
from .awaitable_response import AwaitableResponse
from .dependencies import generate_resources
from .element import Element
from .favicon import get_favicon_url
from .javascript_request import JavaScriptRequest
from .logging import log
from .observables import ObservableDict
from .outbox import Outbox
from .sub_pages_router import Su
…[5655 chars omitted]…
nder_markdown(): # pylint: disable=protected-access
parts.append(markdown)
return Response(
content='\n\n'.join(parts),
status_code=status_code,
headers={'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'},
media_type='text/markdown; charset=utf-8',
background=BackgroundTask(self.delete),$ grep -n "connected\|_deleted_event\|def delete\|prune_instances\|_deleted" /repo/nicegui/client.py | head -60
output · 26 lines
65: """Read the ``client_id`` a socket connected with, or ``None`` if its environment does not carry one.""" 101: self._connected = asyncio.Event() 102: self._deleted_event = asyncio.Event() 107: self._deleted = False 160: """Whether the client is connected.""" 164: def is_deleted(self) -> bool: 166: return self._deleted 268: async def connected(self, timeout: float | None = None) -> None: 269: """Block execution until the client is connected. 276: self._connected.clear() 278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) 282: async def disconnected(self) -> None:
14 more lines
285: await self.connected() 288: self._deleted_event.clear() 289: await self._deleted_event.wait() 297: Obviously the JavaScript code is only executed after …[382 chars omitted]… None: 465: element._deleted = True # pylint: disable=protected-access 485: def delete(self) -> None: 496: self._deleted_event.set() 502: self._deleted = True 503: self._connected.set() # for terminating connected() waits 504: self._connected.clear() 508: if self._deleted: 515: def prune_instances(cls, *, client_age_threshold: float = 60.0) -> None:
$ sed -n 260,340p /repo/nicegui/client.py; echo ---; sed -n 370,560p /repo/nicegui/client.py
output · 32 lines
status_code=status_code,
headers={'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'},
)
def resolve_title(self) -> str:
"""Return the title of the page."""
return self.page.resolve_title() if self.title is None else self.title
async def connected(self, timeout: float | None = None) -> None:
"""Block execution until the client is connected.
:param timeout: timeout in seconds (default: ``None``)20 more lines
"""
if self.has_socket_connection:
return
self._waiting_for_connection.set()
self._connected.clear()
try:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
raise ClientConnectionTimeout(self) from e
async def disconnected(self) -> None:
…[8629 chars omitted]…
d_user_request_markdown(request: Request) -> bool:
"""Check whether the request has text/markdown in its Accept header or is a known agentic user agent."""
accept = request.headers.get('accept', '').strip().lower()
if 'text/markdown' in accept:
return True
user_agent = request.headers.get('user-agent', '').lower()
return any(token in user_agent for token in AI_AGENT_TOKENS)$ grep -rn "prefetch\|_is_prefetch\|ClientConnectionTimeout" /repo/nicegui/*.py | grep -v client.py; echo ---; ls /repo/tests | head -40
output · 43 lines
/repo/nicegui/page.py:13:from .client import Client, ClientConnectionTimeout /repo/nicegui/page.py:137: except ClientConnectionTimeout as e: --- README.md __init__.py conftest.py media test_add_html.py test_aggrid.py test_air.py test_altair.py test_alternate_ui_frameworks.py
31 more lines
test_anywidget.py test_api_router.py test_audio.py test_auto_context.py test_awaitable_response.py test_background_tasks.py test_binding.py test_button.py test_button_dropdown.py test_button_group.py test_card.py test_carousel.py test_chat_message.py test_chip.py test_clipboard.py test_code.py test_codemirror.py test_codemirror_keybindings.py test_codemirror_line_anchors.py test_color_input.py test_color_picker.py test_colors.py test_context_menu.py test_dark_mode.py test_date.py test_date_input.py test_defaults.py test_dialog.py test_download.py test_echart.py test_editor.py
$ sed -n 100,170p /repo/nicegui/page.py; echo ---; grep -rn "prune\|speculative\|prefetch" /repo/tests/*.py | head
output · 25 lines
return self.dark if self.dark is not ... else core.app.config.dark
def resolve_language(self) -> Language | None:
"""Return the language of the page."""
return self.language if self.language is not ... else core.app.config.language
def resolve_reconnect_timeout(self) -> float:
"""Return the reconnect_timeout of the page."""
return self.reconnect_timeout if self.reconnect_timeout is not None else core.app.config.reconnect_timeout
def resolve_markdown(self) -> bool:
"""Return whether the page should serve Markdown when ``Accept: text/markdown`` is requested.13 more lines
*Added in version 3.11.0*
"""
return self.markdown if self.markdown is not None else core.app.config.markdown
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
core.app.remove_route(self.path) # make sure only the
…[3353 chars omitted]…
used'
/repo/tests/test_speculative_loading.py:114: Client.prune_instances(client_age_threshold=0)
/repo/tests/test_speculative_loading.py:121: 'answer() should re-evaluate after prefetch client was pruned'
/repo/tests/test_storage.py:16:from nicegui.app.app import prune_tab_storage, prune_user_storage
/repo/tests/test_storage.py:225: background_tasks.create(prune_tab_storage(force=True))$ sed -n 1,140p /repo/tests/test_speculative_loading.py
output · 49 lines
from __future__ import annotations import asyncio import json import pytest from nicegui import Client, app, ui from nicegui.testing import Screen def test_prerender_with_run_javascript(screen: Screen, event_log: EventLog) -> None:
37 more lines
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.page('/')
def root() -> None:
add_speculation_rule('/answer', kind='prerender')
@ui.page('/answer')
async def answer() -> None:
result = await ui.run_javascript('1 + 41')
event_log.append(f'answer: {result}')
screen.open('/')
event_log.wait_for('connect: /')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
def test_prerender_with_client_connected(screen: Screen, event_log: EventLog) -> None:
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.
…[3334 chars omitted]…
'eagerness': 'immediate'}]}
script = '<script type="speculationrules">' + json.dumps(rules) + '</script>'
ui.add_head_html(script)
class EventLog:
def __init__(self, screen: Screen) -> None:
self.items: list[str] = []
self._screen = screen
def append(self, entry: str) -> None:
self.items.append(entry)
def wait_for(self, entry: str) -> None:
try:$ sed -n 1,120p /repo/nicegui/outbox.py; grep -n "_waiting_for_connection" -r /repo/nicegui
output · 48 lines
from __future__ import annotations import asyncio import time import weakref from collections import deque from typing import TYPE_CHECKING, Any from . import background_tasks, core from .dependencies import JsComponent if TYPE_CHECKING:
36 more lines
from .client import Client
from .element import Element
ElementId = int
ClientId = str
MessageType = str
Payload = Any
Message = tuple[ClientId, MessageType, Payload]
MessageId = int
MessageTime = float
HistoryEntry = tuple[MessageId, MessageTime, Message]
class Deleted:
"""Class for creating a sentinel value for deleted elements."""
deleted = Deleted()
class Outbox:
def __init__(self, client: Client) -> None:
self._client = weakref.ref(client)
self.updates: weakref.WeakValueDictionary[ElementId, Element | Deleted] = weakref.WeakValueDictionary()
self.messages: deque[Message] = deque()
self.
…[3337 chars omitted]…
update', data)))
self.updates.clear()
/repo/nicegui/client.py:99: self._waiting_for_connection = asyncio.Event()
/repo/nicegui/client.py:275: self._waiting_for_connection.set()
/repo/nicegui/client.py:381: self._waiting_for_connection.clear()
/repo/nicegui/page.py:198: client._waiting_for_connection.wait(), # pylint: disable=protected-access$ sed -n 180,230p /repo/nicegui/page.py
output · 26 lines
dec_kwargs['client'] = client
try:
result = func(*dec_args, **dec_kwargs)
except Exception as e:
return create_500_error_page(e, request)
if helpers.should_await(result):
async def wait_for_result() -> Response | None:
with client:
try:
return await result
except Exception as e:14 more lines
client.handle_exception(e)
return create_500_error_page(e, request)
task = background_tasks.create(wait_for_result(),
name=f'wait for result of page "{client.page.path}"',
handle_exceptions=False)
task_wait_for_connect
…[1546 chars omitted]…
result = None
task.add_done_callback(check_for_late_return_value)
if client.is_deleted and not isinstance(result, Response):
return create_terminal_error_page('The client was deleted while the page was being built.', request)
if not await client.sub_pages_router._can_resolve_full_path(client): # pylint: disable=protected-access$ grep -rn "_connected\b" /repo/nicegui | grep -v "_connected_"
output · 9 lines
/repo/nicegui/client.py:101: self._connected = asyncio.Event()
/repo/nicegui/client.py:276: self._connected.clear()
/repo/nicegui/client.py:278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
/repo/nicegui/client.py:382: self._connected.set()
/repo/nicegui/client.py:503: self._connected.set() # for terminating connected() waits
/repo/nicegui/client.py:504: self._connected.clear()
/repo/nicegui/elements/scene/dist/index.js:6:const t="180",e={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},n={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},i=0,r=1,s=2,a=100,o=101,l=102,h=200,c=201,u=202,d=203,p=204,m=205,f=206,g=207,_=208,v=209,y=210,x=211,M=212,S=213,b=214,T=0,E=1,w=2,A=3,R=4,C=5,P=6,I=7,L="attached",D="detached",U=300,N=301,O=302,F=303,B=304,z=306,k=1e3,H=1001,V=1002,G=1003,W=1004,j=1005
…[8632 chars omitted]…
r?\n/,""))}0===i&&aj.isPreTag(r)&&aQ--,aZ===e&&(aX=a0.inVPre=!1,aZ=null),a0.inXML&&(aY[0]?aY[0].ns:aj.ns)===0&&(a0.inXML=!1)}function a5(e,t){let n=e;for(;aq.charCodeAt(n)!==t&&n>=0;)n--;return n}let a9=new Set(["if","else","else-if","for","slot"]),a7=/\r\n/g;function ce(e){let t="preserve"!==aj.whitespace,n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(2===i.type)if(aQ)i.content=i.content.replace(a7,`Now I'll make the edits:
$ cd /repo && python - <<'PY'
output · 2 lines
nicegui/client.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-)
$ cd /repo && timeout 900 python -m pytest tests/test_speculative_loading.py tests/test_client.py tests/test_lifecycle.py tests/test_awaitable_response.py -x -q 2>&1 | tail -15
output · 4 lines
ERROR: file or directory not found: tests/test_client.py no tests ran in 0.02s
$ cd /repo && timeout 1800 python -m pytest tests/test_speculative_loading.py tests/test_lifecycle.py tests/test_awaitable_response.py tests/test_reconnect.py -q 2>&1 | tail -8
output · 8 lines
helpers.warn_once('Client has been deleted but is still being used. '
File "/repo/nicegui/helpers/warnings.py", line 9, in warn_once
log.warning(message, stack_info=stack_info)
--------------------------- Captured stdout teardown ---------------------------
Storing screenshot to /repo/screenshots/2688/test_prefetch_connects_after_navigation.failed.png
=========================== short test summary info ============================
FAILED tests/test_speculative_loading.py::test_prefetch_connects_after_navigation
1 failed, 17 passed in 61.86s (0:01:01)$ cd /repo && timeout 600 python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | grep -B5 "assert\|Error\|warn" | head -60
output · 32 lines
ui.label('all done')
screen.open('/')
event_log.wait_for('connect: /')
screen.wait(1)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('answer: 42')
assert event_log.items == ['answer: called', 'connect: /', 'connect: /answer', 'answer: 42'], \
--
event_log.items.clear()20 more lines
screen.open('/')
event_log.wait_for('answer: called')
Client.prune_instances(client_age_threshold=0)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
> assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
'answer() should re-evaluate aft
…[1174 chars omitted]…
line 85, in __init__
client.outbox.enqueue_update(self)
File "/repo/nicegui/outbox.py", line 65, in enqueue_update
self.client.check_existence()
File "/repo/nicegui/client.py", line 518, in check_existence
helpers.warn_once('Client has been deleted but is still being used. '
File "/repo/nicegui/helpers/warnings.py", line 9, in warn_once
log.warning(message, stack_info=stack_info)$ cd /repo && timeout 600 python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | grep -A12 "assert \[" | head -30
output · 13 lines
E assert ['answer: cal... 'answer: 42'] == ['answer: cal... 'answer: 42'] E E At index 2 diff: 'answer: None' != 'answer: called' E Left contains one more item: 'answer: 42' E Use -v to get more diff tests/test_speculative_loading.py:120: AssertionError ------------------------------ Captured log setup ------------------------------ WARNING selenium.webdriver.common.selenium_manager:selenium_manager.py:168 Exception managing chrome: error sending request for url (https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json) ----------------------------- Captured stdout call ----------------------------- NiceGUI ready to go on http://localhost:41319 ------------------------------ Captured log call -------------------------------
1 more lines
WARNING nicegui:warnings.py:9 Client has been deleted but is still being used. This is most likely a bug in your application code. See https://github.com/zauberzeug/nicegui/issues/3028 for more information.
The behavior is correct — the log is `['answer: called', 'connect: /', 'answer: None', 'answer: called', 'connect: /answer', 'answer: 42']`, exactly matching the hidden test's expectation (the repo's visible copy of this test predates the fix and expects no `answer: None`). Let me run related test modules to check for regressions:
$ cd /repo && timeout 1800 python -m pytest tests/test_javascript.py tests/test_page.py tests/test_event.py tests/test_storage.py -q 2>&1 | tail -4
output · 3 lines
........................................................................ [ 83%] .............. [100%] 86 passed in 81.13s (0:01:21)
$ cd /repo && python - <<'PY'
output · 3 lines
Traceback (most recent call last): File "<stdin>", line 3, in <module> ImportError: cannot import name 'create_app' from 'nicegui.testing' (/repo/nicegui/testing/__init__.py)
$ cd /repo && python - <<'PY'
output · 25 lines
Traceback (most recent call last):
File "<stdin>", line 25, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 14, in main13 more lines
File "/repo/nicegui/client.py", line 140, in __init__
self.sub_pages_router = SubPagesRouter(request)
^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/sub_pages_router.py", line 30, in __init__
path = request.url.path
^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/starlette/requests.py", line 112, in url
self._url = URL(scope=self.scope)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/starlette/datastructures.py", line 37, in __init__
path = scope["path"]
~~~~~^^^^^^^^
KeyError: 'path'$ cd /repo && python - <<'PY'
output · 23 lines
connected released after delete in 0.000s
Traceback (most recent call last):
File "<stdin>", line 29, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^11 more lines
File "<stdin>", line 18, in main
File "/repo/nicegui/client.py", line 325, in run_javascript
return AwaitableResponse(send_and_forget, send_and_wait)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/awaitable_response.py", line 21, in __init__
background_tasks.create(self._fire(), name='fire')
File "/repo/nicegui/background_tasks.py", line 41, in create
assert core.loop is not None
^^^^^^^^^^^^^^^^^^^^^
AssertionError
sys:1: RuntimeWarning: coroutine 'AwaitableResponse._fire' was never awaited$ cd /repo && python - <<'PY'
output · 21 lines
connected released after delete in 0.000s
deleted client run_javascript -> None in 0.000s
Traceback (most recent call last):
File "<stdin>", line 29, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()9 more lines
^^^^^^^^^^^^^^^
File "<stdin>", line 22, in main
File "/usr/local/lib/python3.12/asyncio/tasks.py", line 420, in create_task
task = loop.create_task(coro)
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 457, in create_task
task = tasks.Task(coro, loop=self, name=name, context=context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: a coroutine was expected, got <nicegui.awaitable_response.AwaitableResponse object at 0x7fa776453bf0>$ cd /repo && python - <<'PY'
output · 13 lines
Traceback (most recent call last):
File "<stdin>", line 19, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 12, in main1 more lines
AttributeError: 'AwaitableResponse' object has no attribute 'send_and_wait'
$ cd /repo && sed -n 1,60p nicegui/awaitable_response.py
output · 41 lines
from collections.abc import Callable
from . import background_tasks
class AwaitableResponse:
def __init__(self, fire_and_forget: Callable, wait_for_result: Callable) -> None:
"""Awaitable Response
This class can be used to run one of two different callables, depending on whether the response is awaited or not.
It must be awaited immediately after creation or not at all.29 more lines
:param fire_and_forget: The callable to run if the response is not awaited.
:param wait_for_result: The callable to run if the response is awaited.
"""
self.fire_and_forget = fire_and_forget
self.wait_for_result = wait_for_result
self._is_fired = False
self._is_awaited = False
background_tasks.create(self._fire(), name='fire')
async def _fire(self) -> None:
if self._is_awaited:
return
self.
…[259 chars omitted]…
lf.wait_for_result().__await__()
class NullResponse(AwaitableResponse):
def __init__(self) -> None: # pylint: disable=super-init-not-called
"""Null Response
This class can be used to create an AwaitableResponse that does nothing.
In contrast to AwaitableResponse, it can be created without a running event loop.
"""
def __await__(self):
yield from []$ cd /repo && python - <<'PY'
output · 25 lines
Traceback (most recent call last):
File "<stdin>", line 23, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 13, in main13 more lines
File "/usr/local/lib/python3.12/asyncio/tasks.py", line 420, in create_task
task = loop.create_task(coro)
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 457, in create_task
task = tasks.Task(coro, loop=self, name=name, context=context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
…[263 chars omitted]…
aitableResponse must be awaited immediately after creation or not at all')>
Traceback (most recent call last):
File "<stdin>", line 21, in _wrap
File "/repo/nicegui/awaitable_response.py", line 31, in __await__
raise RuntimeError('AwaitableResponse must be awaited immediately after creation or not at all')
RuntimeError: AwaitableResponse must be awaited immediately after creation or not at all$ cd /repo && python - <<'PY'
output · 1 lines
pruned mid-wait run_javascript -> None in 0.000s
$ cd /repo && timeout 1800 python -m pytest tests/test_timer.py tests/test_sub_pages_router.py tests/test_air.py tests/test_outbox.py -q 2>&1 | tail -3; ls tests | grep -i "client\|connect\|delete"
output · 3 lines
no tests ran in 0.01s test_element_delete.py test_reconnect.py
$ cd /repo && timeout 1800 python -m pytest tests/test_element_delete.py tests/test_nicegui.py -q 2>&1 | tail -3
output · 1 lines
no tests ran in 0.01s
$ cd /repo && timeout 1800 python -m pytest tests/test_element_delete.py tests/test_nicegui.py -q 2>&1 | tail -6
output · 4 lines
ERROR: file or directory not found: tests/test_nicegui.py no tests ran in 0.01s
$ cd /repo && timeout 1800 python -m pytest tests/test_element_delete.py -q 2>&1 | tail -3 && timeout 1800 python -m pytest tests -q -x --ignore=tests/test_speculative_loading.py -k "not air" 2>&1 | tail -4
output · 6 lines
........... [100%] 11 passed in 19.78s =========================== short test summary info ============================ ERROR tests/test_aggrid.py !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 error in 1.49s
$ cd /repo && timeout 300 python -m pytest tests/test_aggrid.py -q 2>&1 | tail -15
output · 15 lines
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_aggrid.py _____________________
ImportError while importing test module '/repo/tests/test_aggrid.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/local/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_aggrid.py:6: in <module>
import pandas as pd
E ModuleNotFoundError: No module named 'pandas'
=========================== short test summary info ============================3 more lines
ERROR tests/test_aggrid.py !!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! 1 error in 0.73s
$ cd /repo && timeout 3000 python -m pytest tests -q --ignore=tests/test_speculative_loading.py -p no:cacheprovider --deselect tests/test_aggrid.py 2>&1 | tail -6
output · 6 lines
ERROR tests/test_scene.py ERROR tests/test_scene_view.py ERROR tests/test_table.py ERROR tests/test_user_simulation.py - pytest.PytestUnknownMarkWarning: Unknow... !!!!!!!!!!!!!!!!!!! Interrupted: 16 errors during collection !!!!!!!!!!!!!!!!!!! 16 errors in 5.41s
$ cd /repo && timeout 3000 python -m pytest tests -q --ignore=tests/test_speculative_loading.py -p no:cacheprovider --ignore tests/test_scene.py --ignore tests/test_scene_view.py --ignore tests/test_table.py --ignore tests/test_user_simulation.py --ignore tests/test_aggrid.py --ignore tests/test_dataframe.py --ignore tests/test_echart.py --ignore tests/test_mermaid.py 2>&1 | tail -6
output · 6 lines
ERROR tests/test_json.py ERROR tests/test_plotly.py ERROR tests/test_quasar_tailwind_interplay.py ERROR tests/test_run_with.py - starlette.exceptions.StarletteDeprecationWarni... !!!!!!!!!!!!!!!!!!! Interrupted: 10 errors during collection !!!!!!!!!!!!!!!!!!! 10 errors in 2.95s
$ cd /repo && timeout 3000 python -m pytest tests -q --ignore=tests/test_speculative_loading.py -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -8
output · 1 lines
(command exceeded 600s)
The full selenium suite exceeds the time budget; the affected modules all pass. Final diff check:
$ cd /repo && git diff
output · 33 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..57617ac 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,7 +270,7 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self._deleted:
return
self._waiting_for_connection.set()21 more lines
self._connected.clear()
@@ -278,6 +278,8 @@ class Client:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
raise ClientConnectionTimeout(self) from e
+ if self._deleted:
+ return
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
@@ -307,11 +309,17 @@ class Client:
target_id = s
…[817 chars omitted]…
() # for terminating connected() waits
- self._connected.clear()
+ # NOTE: keep the event set so that pending connected() waits are released;
+ # connected() checks the deleted flag to avoid treating this as a real connection.
+ self._connected.set()
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""$ done: {"notes": "Fixed in nicegui/client.py: delete() now leaves _connected set (previously set-then-clear which never released waiters); connected() returns promptly when the client is already/deleted-become deleted; run_javascript's send_and_wait/send_and_forget short-circuit to None when the client is deleted (before enqueue, after cooutput · 3 lines
…[544 chars omitted]… in the repo expects no 'answer: None' entry; the issue states the hidden test expects 'answer: None' after prune, which the fix produces (left test file unmodified)."} submitted.
GLM-5.3 Flash · attempt 4 · solved · 25 steps · $0.010
Submitted diff · 63 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..34863aa 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,14 +270,27 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self._deleted:
return
self._waiting_for_connection.set()
self._connected.clear()
+ tasks = [
+ asyncio.ensure_future(self._connected.wait()),
+ asyncio.ensure_future(self._deleted_event.wait()),
+ ]
try:
- await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
- except asyncio.TimeoutError as e:
+ done, pending = await asyncio.wait(
+ tasks,
+ timeout=None if _is_prefetch(self.request) else timeout,
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ except asyncio.TimeoutError as e: # pragma: no cover
raise ClientConnectionTimeout(self) from e
+ finally:
+ for task in tasks:
+ task.cancel()
+ if not done:
+ raise ClientConnectionTimeout(self)
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
@@ -312,6 +325,8 @@ class Client:
async def send_and_wait():
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
+ if self._deleted:
+ return None
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
diff --git a/tests/test_speculative_loading.py b/tests/test_speculative_loading.py
index f502d05..4e4157d 100644
--- a/tests/test_speculative_loading.py
+++ b/tests/test_speculative_loading.py
@@ -112,12 +112,13 @@ def test_prefetch_connects_after_navigation(screen: Screen, event_log: EventLog)
screen.open('/')
event_log.wait_for('answer: called')
Client.prune_instances(client_age_threshold=0)
- assert event_log.items == ['answer: called', 'connect: /']
+ event_log.wait_for('answer: None')
+ assert event_log.items == ['answer: called', 'connect: /', 'answer: None']
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
- assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
+ assert event_log.items == ['answer: called', 'connect: /', 'answer: None', 'answer: called', 'connect: /answer', 'answer: 42'], \
'answer() should re-evaluate after prefetch client was pruned'
screen.should_contain('all done')$ ls /repo && ls /repo/nicegui | head -50
output · 88 lines
AGENTS.md CITATION.cff CLAUDE.md CODE_OF_CONDUCT.md CONTRIBUTING.md DEPENDENCIES.md LICENSE README.md REVIEW.md SECURITY.md deploy.py development.dockerfile
76 more lines
docker-compose.yml docker-entrypoint.sh docker.sh examples extract_core_libraries.py fetch_github_stats.py fetch_google_fonts.py fetch_languages.py fetch_milestone.py fly-entrypoint.sh fly.dockerfile fly.toml main.py nicegui nicegui.code-workspace package-lock.json package.json pyproject.toml release.dockerfile screenshot.png set_scale.sh summarize_dependencies.py test_startup.sh tests uv.lock website __init__.py air.py api_router.py app awaitable_response.py background_tasks.py binding.py classes.py client.py context.py core.py dataclasses.py defaults.py dependencies.py element.py element_filter.py elements error.py event.py event_listener.py events.py favicon.py functions helpers html.py javascript_request.py json language.py llms.md logging.py middlewares.py native nicegui.py observables.py optional_features.py outbox.py page.py page_arguments.py persistence props.py py.typed run.py scripts server.py slot.py static staticfiles.py storage.py style.py sub_pages_router.py
$ grep -n "connected" /repo/nicegui/client.py | head; grep -n "def prune_instances" -A 20 /repo/nicegui/client.py
output · 29 lines
65: """Read the ``client_id`` a socket connected with, or ``None`` if its environment does not carry one.""" 101: self._connected = asyncio.Event() 160: """Whether the client is connected.""" 268: async def connected(self, timeout: float | None = None) -> None: 269: """Block execution until the client is connected. 276: self._connected.clear() 278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) 282: async def disconnected(self) -> None: 285: await self.connected() 297: Obviously the JavaScript code is only executed after the client is connected. 515: def prune_instances(cls, *, client_age_threshold: float = 60.0) -> None: 516- """Prune stale clients."""
17 more lines
517- try:
518- stale_clients = [
519- client
520- for
…[222 chars omitted]…
client.created <= time.time() - client_age_threshold
525- )
526- ]
527- for client in stale_clients:
528- log.debug(f'Pruning stale client {client.id}')
529- client.delete()
530-
531- except Exception:
532- log.exception('Error while pruning clients')
533-
534-
535-def _is_prefetch(request: Request) -> bool:$ sed -n 90,320p /repo/nicegui/client.py
output · 30 lines
def __init__(self, page: page, *, request: Request | None = None) -> None:
self._request = request
self.id = str(uuid.uuid4())
self.created = time.time()
self.instances[self.id] = self
self.elements: dict[int, Element] = {}
self.next_element_id: int = 0
self._waiting_for_connection = asyncio.Event()
self._waiting_for_disconnect = asyncio.Event()
self._connected = asyncio.Event()
self._deleted_event = asyncio.Event()18 more lines
self.environ: dict[str, Any] | None = None
self.on_air = False
self._num_connections: defaultdict[str, int] = defaultdict(int)
self._delete_tasks: dict[str, asyncio.Task] = {}
self._deleted = False
self._socket_to_document_id: dict[str, str] = {}
self.tab_id: str | None = None
self._pinned_tab_id: str | None = None
se
…[8628 chars omitted]…
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
def open(self, target: Callable | str, new_tab: bool = False) -> None:
"""Open a new page in the client."""$ grep -n "def delete" -A 40 /repo/nicegui/client.py
output · 28 lines
411: async def delete_content() -> None:
412- await asyncio.sleep(self.page.resolve_reconnect_timeout())
413- if self._num_connections[document_id] == 0:
414- self._num_connections.pop(document_id)
415- self._delete_tasks.pop(document_id)
416- await core.app.storage.close_tab(tab_id_to_close)
417- self.delete()
418- self._delete_tasks[document_id] = \
419- background_tasks.create(delete_content(), name=f'delete content {document_id}')
420-
421- def _cancel_delete_task(self, document_id: str) -> None:
422- if document_id in self._delete_tasks:16 more lines
423- self._delete_tasks.pop(document_id).cancel()
424-
425- def handle_event(self, msg: dict) -> None:
426- """Forward an event to the corresponding element. (For internal use only.)"""
427- with self:
4
…[2930 chars omitted]…
try:
518- stale_clients = [
519- client
520- for client in cls.instances.values()
521- if (
522- not client.has_socket_connection and
523- not client._delete_tasks and # pylint: disable=protected-access
524- client.created <= time.time() - client_age_threshold
525- )$ sed -n 535,560p /repo/nicegui/client.py; grep -n "prefetch\|_is_prefetch" -r /repo/nicegui --include=*.py
output · 16 lines
def _is_prefetch(request: Request) -> bool:
purpose = (request.headers.get('Sec-Purpose') or request.headers.get('Purpose') or '').lower()
return 'prefetch' in purpose and 'prerender' not in purpose
def _did_user_request_markdown(request: Request) -> bool:
"""Check whether the request has text/markdown in its Accept header or is a known agentic user agent."""
accept = request.headers.get('accept', '').strip().lower()
if 'text/markdown' in accept:
return True
user_agent = request.headers.get('user-agent', '').lower()
return any(token in user_agent for token in AI_AGENT_TOKENS)4 more lines
/repo/nicegui/client.py:219: 'implicit_handshake': not _is_prefetch(request), /repo/nicegui/client.py:278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) /repo/nicegui/client.py:535:def _is_prefetch(request: Request) -> bool: /repo/nicegui/client.py:537: return 'prefetch' in purpose and 'prerender' not in purpose
$ grep -rn "prefetch\|speculative" /repo/nicegui/page.py /repo/nicegui/api_router.py /repo/nicegui/middlewares.py 2>/dev/null | head -30; grep -rn "prefetch" /repo/nicegui -l
output · 3 lines
/repo/nicegui/client.py /repo/nicegui/elements/codemirror/dist/index-BtSOn-Ix.js /repo/nicegui/elements/codemirror/dist/index-BtSOn-Ix.js.map
$ grep -rn "delete\b\|_deleted\|is_deleted" /repo/nicegui/page.py | head; sed -n 1,120p /repo/nicegui/page.py
output · 43 lines
139: e.client.delete() 208: client.delete() 217: reason = ' because the client was deleted' if client.is_deleted \ 227: if client.is_deleted and not isinstance(result, Response): from __future__ import annotations import asyncio import inspect from collections.abc import Callable from functools import wraps from pathlib import Path from typing import TYPE_CHECKING, Any
31 more lines
from fastapi import Request, Response
from . import background_tasks, binding, core, helpers
from .client import Client, ClientConnectionTimeout
from .error import error_content
from .favicon import create_favicon_route
from .language import Language
from .logging import log
if TYPE_CHECKING:
from .api_router import APIRouter
class page:
def __init__(self,
path: str, *,
title: str | None = None,
…[4412 chars omitted]…
down when ``Accept: text/markdown`` is requested.
*Added in version 3.11.0*
"""
return self.markdown if self.markdown is not None else core.app.config.markdown
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
core.app.remove_route(self.path) # make sure only the latest route definition is used
if 'include_in_schema' not in self.kwargs:$ sed -n 120,260p /repo/nicegui/page.py
output · 27 lines
if 'include_in_schema' not in self.kwargs:
self.kwargs['include_in_schema'] = core.app.config.endpoint_documentation in {'page', 'all'}
self.api_router.get(self._path, **self.kwargs)(self._wrap(func))
Client.page_routes[func] = self.path
return func
def _wrap(self, func: Callable[..., Any]) -> Callable[..., Any]:
parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
def check_for_late_return_value(task: asyncio.Task) -> None:
try:15 more lines
if task.result() is not None:
log.error(f'ignoring {task.result()}; '
'it was returned after the HTML had been delivered to the client')
except asyncio.CancelledError:
pass
except ClientConnectionTimeout as e:
log.debug('client connecti
…[5560 chars omitted]…
dding request as a parameter so we can pass it to the client in the decorated function
if 'request' not in {p.name for p in parameters}:
request = inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request)
parameters.insert(0, request)
decorated.__signature__ = inspect.Signature(parameters) # type: ignore
return decorated$ ls /repo/tests | head -60; grep -rln "prune\|prefetch" /repo/tests
output · 66 lines
README.md __init__.py conftest.py media test_add_html.py test_aggrid.py test_air.py test_altair.py test_alternate_ui_frameworks.py test_anywidget.py test_api_router.py test_audio.py
54 more lines
test_auto_context.py test_awaitable_response.py test_background_tasks.py test_binding.py test_button.py test_button_dropdown.py test_button_group.py test_card.py test_carousel.py test_chat_message.py test_chip.py test_clipboard.py test_code.py test_codemirror.py test_codemirror_keybindings.py test_codemirror_line_anchors.py test_color_input.py test_color_picker.py test_colors.py test_context_menu.py test_dark_mode.py test_date.py test_date_input.py test_defaults.py test_dialog.py test_download.py test_echart.py test_editor.py test_element.py test_element_delete.py test_element_filter.py test_endpoint_docs.py test_event.py test_events.py test_expansion.py test_fab.py test_favicon.py test_forwarded_prefix.py test_fullscreen.py test_header.py test_helpers.py test_highchart.py test_html.py test_image.py test_input.py test_input_chips.py test_interactive_image.py test_javascript.py /repo/tests/test_event.py /repo/tests/test_lifecycle.py /repo/tests/test_reconnect.py /repo/tests/test_speculative_loading.py /repo/tests/test_storage.py /repo/tests/test_timer.py
$ cat /repo/tests/test_speculative_loading.py
output · 47 lines
from __future__ import annotations import asyncio import json import pytest from nicegui import Client, app, ui from nicegui.testing import Screen def test_prerender_with_run_javascript(screen: Screen, event_log: EventLog) -> None:
35 more lines
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.page('/')
def root() -> None:
add_speculation_rule('/answer', kind='prerender')
@ui.page('/answer')
async def answer() -> None:
result = await ui.run_javascript('1 + 41')
event_log.append(f'answer: {result}')
screen.open('/')
event_log.wait_for('connect: /')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
def test_prerender_with_client_connected(screen: Screen, event_log: EventLog) -> None:
app.on_connect(lambda client: event_log.append(f'connect: {client.page.path}'))
@ui.
…[3619 chars omitted]…
d(self, entry: str) -> None:
self.items.append(entry)
def wait_for(self, entry: str) -> None:
try:
self._screen.wait_for(lambda: entry in self.items)
except AssertionError as e:
raise AssertionError(f'{entry} not found in {self.items}') from e
@pytest.fixture(name='event_log')
def _event_log(screen: Screen) -> EventLog:
return EventLog(screen)$ sed -n 1,80p /repo/nicegui/outbox.py; grep -n "class JavaScriptRequest" -A 40 /repo/nicegui/javascript_request.py
output · 51 lines
from __future__ import annotations import asyncio import time import weakref from collections import deque from typing import TYPE_CHECKING, Any from . import background_tasks, core from .dependencies import JsComponent if TYPE_CHECKING:
39 more lines
from .client import Client
from .element import Element
ElementId = int
ClientId = str
MessageType = str
Payload = Any
Message = tuple[ClientId, MessageType, Payload]
MessageId = int
MessageTime = float
HistoryEntry = tuple[MessageId, MessageTime, Message]
class Deleted:
"""Class for creating a sentinel value for deleted elements."""
deleted = Deleted()
class Outbox:
def __init__(self, client: Client) -> None:
self._client = weakref.ref(client)
self.updates: weakref.WeakValueDictionary[ElementId, Element | Deleted] = weakref.WeakValueDictionary()
self.messages: deque[Message] = deque()
self.
…[2312 chars omitted]…
def __await__(self) -> Any:
27- try:
28- yield from asyncio.wait_for(self._event.wait(), self.timeout).__await__()
29- except asyncio.TimeoutError as e:
30- raise TimeoutError(f'JavaScript did not respond within {self.timeout:.1f} s') from e
31- else:
32- return self._result
33- finally:
34- self._instances.pop(self.request_id)$ grep -rn "connected(" /repo/nicegui/client.py | head; grep -rn "\.connected()" /repo/nicegui --include=*.py | head -20output · 17 lines
268: async def connected(self, timeout: float | None = None) -> None: 282: async def disconnected(self) -> None: 285: await self.connected() 298: Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*). 314: await self.connected() 503: self._connected.set() # for terminating connected() waits /repo/nicegui/client.py:285: await self.connected() /repo/nicegui/client.py:298: Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*). /repo/nicegui/client.py:314: await self.connected() /repo/nicegui/elements/button.py:72: await self.client.connected() /repo/nicegui/elements/leaflet/leaflet.py:105: await self.client.connected() /repo/nicegui/elements/scene/scene.py:217: await self.cl
5 more lines
…[74 chars omitted]… it self.client.connected() /repo/nicegui/elements/timer.py:28: await self.client.connected() /repo/nicegui/functions/javascript.py:16: Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*). /repo/nicegui/page.py:211: 'Await ui.context.client.connected() before long-running setup or increase response_timeout.',
Now implement the fix in `client.py`:
$ cd /repo && python - <<'PY'
output · 36 lines
"""Return the title of the page."""
return self.page.resolve_title() if self.title is None else self.title
async def connected(self, timeout: float | None = None) -> None:
"""Block execution until the client is connected.
:param timeout: timeout in seconds (default: ``None``)
"""
if self.has_socket_connection or self._deleted:
return
self._waiting_for_connection.set()
self._connected.clear()24 more lines
tasks = [
asyncio.ensure_future(self._connected.wait()),
asyncio.ensure_future(self._deleted_event.wait()),
]
try:
done, pending = await asyncio.wait(
tasks,
timeout=None if _is_prefetch(self.request) else timeout,
return_when=asyncio.FIRST_COMPLETED,
)
except asyncio.TimeoutError as e: # p
…[79 chars omitted]…
finally:
for task in tasks:
task.cancel()
if not done:
raise ClientConnectionTimeout(self)
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
if not self.has_socket_connection:
await self.connected()
if self.id in self.instances:
self._waiting_for_disconnect.set()$ cd /repo && cat > /tmp/repro.py <<'EOF'
output · 13 lines
Traceback (most recent call last):
File "<stdin>", line 30, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 10, in main1 more lines
AttributeError: 'AppConfig' object has no attribute 'response_timeout'
$ cd /repo && python - <<'PY'
output · 25 lines
task done: True
Traceback (most recent call last):
File "<stdin>", line 28, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^13 more lines
File "<stdin>", line 25, in main
File "/repo/nicegui/client.py", line 332, in run_javascript
return AwaitableResponse(send_and_forget, send_and_wait)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/awaitable_response.py", line 21, in __init__
background_tasks.create(self._fire(), name='fire')
File "/repo/nicegui/backg
…[182 chars omitted]…
ask-2' coro=<main.<locals>.page_task() done, defined at <stdin>:16> exception=TypeError("'Client' object does not support the asynchronous context manager protocol")>
Traceback (most recent call last):
File "<stdin>", line 17, in page_task
TypeError: 'Client' object does not support the asynchronous context manager protocol
sys:1: RuntimeWarning: coroutine 'AwaitableResponse._fire' was never awaited$ cd /repo && python - <<'PY'
output · 29 lines
task done: True
Traceback (most recent call last):
File "<stdin>", line 28, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^17 more lines
File "<stdin>", line 25, in main
File "/repo/nicegui/client.py", line 332, in run_javascript
return AwaitableResponse(send_and_forget, send_and_wait)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/awaitable_response.py", line 21, in __init__
background_tasks.create(self._fire(), name='fire')
File "/repo/nicegui/backg
…[678 chars omitted]…
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/repo/nicegui/awaitable_response.py", line 21, in __init__
background_tasks.create(self._fire(), name='fire')
File "/repo/nicegui/background_tasks.py", line 41, in create
assert core.loop is not None
^^^^^^^^^^^^^^^^^^^^^
AssertionError
sys:1: RuntimeWarning: coroutine 'AwaitableResponse._fire' was never awaited$ cd /repo && python - <<'PY'
output · 25 lines
Client has been deleted but is still being used. This is most likely a bug in your application code. See https://github.com/zauberzeug/nicegui/issues/3028 for more information.
Stack (most recent call last):
File "<stdin>", line 29, in <module>
File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
return runner.run(main)
File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 678, in run_until_complete
self.run_forever()
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 645, in run_forever
self._run_once()
File "/usr/local/lib/python3.12/asyncio/base_events.py", line 1999, in _run_once13 more lines
handle._run()
File "/usr/local/lib/python3.12/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *s
…[235 chars omitted]…
box.py", line 77, in enqueue_message
self.client.check_existence()
File "/repo/nicegui/client.py", line 524, in check_existence
helpers.warn_once('Client has been deleted but is still being used. '
File "/repo/nicegui/helpers/warnings.py", line 9, in warn_once
log.warning(message, stack_info=stack_info)
resumed with None after 0.201
task done: True
deleted client run_javascript -> None$ cd /repo && timeout 1200 python -m pytest tests/test_speculative_loading.py tests/test_javascript.py tests/test_reconnect.py tests/test_lifecycle.py -x -q 2>&1 | tail -20
output · 20 lines
File "/repo/tests/test_speculative_loading.py", line 98, in answer
ui.label('all done')
File "/repo/nicegui/elements/label.py", line 13, in __init__
super().__init__(tag='div', text=text)
File "/repo/nicegui/elements/mixins/text_element.py", line 15, in __init__
super().__init__(**kwargs)
File "/repo/nicegui/element.py", line 85, in __init__
client.outbox.enqueue_update(self)
File "/repo/nicegui/outbox.py", line 65, in enqueue_update
self.client.check_existence()
File "/repo/nicegui/client.py", line 524, in check_existence
helpers.warn_once('Client has been deleted but is still being used. '8 more lines
File "/repo/nicegui/helpers/warnings.py", line 9, in warn_once
log.warning(message, stack_info=stack_info)
--------------------------- Captured stdout teardown ---------------------------
Storing screenshot to /repo/screenshots/2691/test_prefetch_connects_after_navigation.failed.png
=========================== short test summary info ============================
FAILED tests/test_speculative_loading.py::test_prefetch_connects_after_navigation
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 4 passed in 30.63s$ cd /repo && timeout 600 python -m pytest tests/test_speculative_loading.py::test_prefetch_connects_after_navigation -q 2>&1 | grep -B5 "AssertionError\|assert" | head -40
output · 33 lines
ui.label('all done')
screen.open('/')
event_log.wait_for('connect: /')
screen.wait(1)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('answer: 42')
assert event_log.items == ['answer: called', 'connect: /', 'connect: /answer', 'answer: 42'], \
--
event_log.items.clear()21 more lines
screen.open('/')
event_log.wait_for('answer: called')
Client.prune_instances(client_age_threshold=0)
assert event_log.items == ['answer: called', 'connect: /']
screen.click('answer')
event_log.wait_for('connect: /answer')
event_log.wait_for('answer: 42')
> assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
'answer() should re-evaluate aft
…[5 chars omitted]…
efetch client was pruned'
E AssertionError: answer() should re-evaluate after prefetch client was pruned
E assert ['answer: cal... 'answer: 42'] == ['answer: cal... 'answer: 42']
E
E At index 2 diff: 'answer: None' != 'answer: called'
E Left contains one more item: 'answer: 42'
E Use -v to get more diff
tests/test_speculative_loading.py:120: AssertionErrorThe visible test encodes the old behavior; the issue states the hidden test expects `answer: None` after the prune. I'll update the visible test to match the issue's described expectations:
$ cd /repo && python - <<'PY'
output · 2 lines
..... [100%] 5 passed in 29.14s
$ cd /repo && timeout 1800 python -m pytest tests/test_client.py tests/test_page.py tests/test_javascript.py tests/test_reconnect.py tests/test_lifecycle.py tests/test_timer.py tests/test_storage.py tests/test_awaitable_response.py -q 2>&1 | tail -5
output · 4 lines
ERROR: file or directory not found: tests/test_client.py no tests ran in 0.01s
$ cd /repo && timeout 1800 python -m pytest tests/test_page.py tests/test_javascript.py tests/test_reconnect.py tests/test_lifecycle.py tests/test_timer.py tests/test_storage.py tests/test_awaitable_response.py tests/test_event.py -q 2>&1 | tail -5
output · 5 lines
Storing screenshot to /repo/screenshots/3548/test_no_leak_when_client_deleted.failed.png =========================== short test summary info ============================ FAILED tests/test_timer.py::test_cleanup - AssertionError: only current timer... FAILED tests/test_timer.py::test_no_leak_when_client_deleted - assert not True 2 failed, 114 passed in 146.69s (0:02:26)
$ cd /repo && git stash && timeout 600 python -m pytest tests/test_timer.py -q 2>&1 | tail -4; git stash pop
output · 14 lines
Saved working directory and index state WIP on master: deca0be base =========================== short test summary info ============================ FAILED tests/test_timer.py::test_cleanup - AssertionError: only current timer... FAILED tests/test_timer.py::test_no_leak_when_client_deleted - assert not True 2 failed, 15 passed in 33.36s On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: nicegui/client.py modified: tests/test_speculative_loading.py
2 more lines
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (b5c1f30d0e63cc4af34bfcdbbfb0b5f87595737e)Both timer failures are pre-existing (fail on the base commit too). Final diff review:
$ cd /repo && git diff
output · 29 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..34863aa 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -270,14 +270,27 @@ class Client:
:param timeout: timeout in seconds (default: ``None``)
"""
- if self.has_socket_connection:
+ if self.has_socket_connection or self._deleted:
return
self._waiting_for_connection.set()17 more lines
self._connected.clear()
+ tasks = [
+ asyncio.ensure_future(self._connected.wait()),
+ asyncio.ensure_future(self._deleted_event.wait()),
+ ]
try:
- await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
- except asyncio.TimeoutError as e:
+ done, pending = await asyncio.wait(
+ tasks,
+ timeout=None if _is_prefetch(self.request
…[1551 chars omitted]…
event_log.wait_for('answer: 42')
- assert event_log.items == ['answer: called', 'connect: /', 'answer: called', 'connect: /answer', 'answer: 42'], \
+ assert event_log.items == ['answer: called', 'connect: /', 'answer: None', 'answer: called', 'connect: /answer', 'answer: 42'], \
'answer() should re-evaluate after prefetch client was pruned'
screen.should_contain('all done')$ done: {"notes": "Client.connected() now also waits on the client's deletion event (returning promptly when the client is deleted/pruned, still raising ClientConnectionTimeout on timeout), and run_javascript's send_and_wait resolves with None when the client is deleted (including already-deleted clients). Updated test_speculative_loadingoutput · 3 lines
…[106 chars omitted]… 42'). Reproducer confirms prompt release with None; speculative-loading, javascript, reconnect, lifecycle, page tests pass (timer failures pre-exist on base commit)."} submitted.
GLM-5.3 Flash · attempt 5 · solved · 35 steps · $0.013
Submitted diff · 32 lines
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..1873e5b 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -274,6 +274,8 @@ class Client:
return
self._waiting_for_connection.set()
self._connected.clear()
+ if self._deleted:
+ return
try:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
@@ -312,6 +314,8 @@ class Client:
async def send_and_wait():
self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
await self.connected()
+ if self._deleted:
+ return None
return await JavaScriptRequest(request_id, timeout=timeout)
return AwaitableResponse(send_and_forget, send_and_wait)
@@ -500,8 +504,7 @@ class Client:
self.outbox.stop()
del Client.instances[self.id]
self._deleted = True
- self._connected.set() # for terminating connected() waits
- self._connected.clear()
+ self._connected.set() # for terminating connected() waits; stays set so later waits return promptly
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""$ cd /repo && ls && grep -n "connected" nicegui/client.py | head -40
output · 50 lines
AGENTS.md CITATION.cff CLAUDE.md CODE_OF_CONDUCT.md CONTRIBUTING.md DEPENDENCIES.md LICENSE README.md REVIEW.md SECURITY.md deploy.py development.dockerfile
38 more lines
docker-compose.yml docker-entrypoint.sh docker.sh examples extract_core_libraries.py fetch_github_stats.py fetch_google_fonts.py fetch_languages.py fetch_milestone.py fly-entrypoint.sh fly.dockerfile fly.toml main.py nicegui nicegui.code-workspace package-lock.json package.json pyproject.toml release.dockerfile screenshot.png set_scale.sh summarize_dependencies.py test_startup.sh tests uv.lock website 65: """Read the ``client_id`` a socket connected with, or ``None`` if its environment does not carry one.""" 101: self._connected = asyncio.Event() 160: """Whether the client is connected.""" 268: async def connected(self, timeout: float | None = None) -> None: 269: """Block execution until the client is co …[356 chars omitted]… rnally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*). 314: await self.connected() 382: self._connected.set() 402: # keep the tab_id as long as any socket is live, e.g. one that reconnected before this one was reaped 503: self._connected.set() # for terminating connected() waits 504: self._connected.clear()
$ cd /repo && sed -n 60,120p nicegui/client.py && sed -n 250,340p nicegui/client.py && sed -n 470,520p nicegui/client.py
output · 34 lines
HEADWIND_CONTENT = (Path(__file__).parent / 'static' / 'headwind.css').read_text().strip()
def _client_id_from_query(environ: dict[str, Any]) -> str | None:
"""Read the ``client_id`` a socket connected with, or ``None`` if its environment does not carry one."""
query_string = environ.get('QUERY_STRING') or environ.get('asgi.scope', {}).get('query_string') or ''
if isinstance(query_string, (bytes, bytearray)):
query_string = query_string.decode()
return parse_qs(query_string).get('client_id', [None])[0]
class ClientConnectionTimeout(TimeoutError):22 more lines
def __init__(self, client: Client) -> None:
super().__init__(f'ClientConnectionTimeout: {client.id}')
self.client = client
class Client:
page_routes: ClassVar[dict[Callable, str]] = {}
'''Maps page builders to their routes.'''
instances: ClassVar[dict[str, Client]] = {}
'''Ma
…[7999 chars omitted]…
'See https://github.com/zauberzeug/nicegui/issues/3028 for more information.',
stack_info=True)
@classmethod
def prune_instances(cls, *, client_age_threshold: float = 60.0) -> None:
"""Prune stale clients."""
try:
stale_clients = [
client
for client in cls.instances.values()$ cd /repo && sed -n 520,560p nicegui/client.py && grep -rn "prune\|_deleted" nicegui/client.py | head -30 && grep -rln "prune\|run_javascript" tests | head
output · 38 lines
for client in cls.instances.values()
if (
not client.has_socket_connection and
not client._delete_tasks and # pylint: disable=protected-access
client.created <= time.time() - client_age_threshold
)
]
for client in stale_clients:
log.debug(f'Pruning stale client {client.id}')
client.delete()
except Exception:26 more lines
log.exception('Error while pruning clients')
def _is_prefetch(request: Request) -> bool:
purpose = (request.headers.get('Sec-Purpose') or request.headers.get('Purpose') or '').lower()
return 'prefetch' in purpose and 'prerender' not in purpose
def _did_user_request_markdown(request: Request) -> bool:
"""Check whether the request has text/markdown in its Accept header or is a known a
…[589 chars omitted]…
s
496: self._deleted_event.set()
502: self._deleted = True
508: if self._deleted:
515: def prune_instances(cls, *, client_age_threshold: float = 60.0) -> None:
tests/test_aggrid.py
tests/test_element.py
tests/test_element_delete.py
tests/test_event.py
tests/test_events.py
tests/test_html.py
tests/test_javascript.py
tests/test_lifecycle.py
tests/test_number.py
tests/test_page.py$ cd /repo && grep -rn "prefetch\|_is_prefetch" nicegui/*.py nicegui/**/*.py | grep -v static | head -30
output · 4 lines
nicegui/client.py:219: 'implicit_handshake': not _is_prefetch(request), nicegui/client.py:278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) nicegui/client.py:535:def _is_prefetch(request: Request) -> bool: nicegui/client.py:537: return 'prefetch' in purpose and 'prerender' not in purpose
$ cd /repo && grep -rn "run_page_builder\|speculative\|page_state\|_page_builder" nicegui/core.py nicegui/page.py nicegui/page_storage.py 2>/dev/null | head -30; ls nicegui
output · 60 lines
__init__.py air.py api_router.py app awaitable_response.py background_tasks.py binding.py classes.py client.py context.py core.py dataclasses.py
48 more lines
defaults.py dependencies.py element.py element_filter.py elements error.py event.py event_listener.py events.py favicon.py functions helpers html.py javascript_request.py json language.py llms.md logging.py middlewares.py native nicegui.py observables.py optional_features.py outbox.py page.py page_arguments.py persistence props.py py.typed run.py scripts server.py slot.py static staticfiles.py storage.py style.py sub_pages_router.py templates testing timer.py translations.py ui.py ui_run.py ui_run_with.py vbuild.py version.py welcome.py
$ cd /repo && grep -rn "run_javascript\|connected\|prune" nicegui/ui.py | head; grep -rn "def run_javascript" nicegui/ui.py nicegui/element.py
output · 3 lines
133: 'run_javascript': ('.functions.javascript', 'run_javascript'),
255: 'run_javascript',
422: from .functions.javascript import run_javascript$ cd /repo && cat nicegui/functions/javascript.py && cat nicegui/javascript_request.py
output · 29 lines
from ..awaitable_response import AwaitableResponse
from ..context import context
def run_javascript(code: str, *, timeout: float = 1.0) -> AwaitableResponse:
"""Run JavaScript
This function runs arbitrary JavaScript code on a page that is executed in the browser.
To access a client-side Vue component or HTML element by ID,
use the JavaScript functions `getElement()` or `getHtmlElement()` (*added in version 2.9.0*).
If the function is awaited, the result of the JavaScript code is returned.17 more lines
Otherwise, the JavaScript code is executed without waiting for a response.
Obviously the JavaScript code is only executed after the client is connected.
Internally, ``await client.connected()`` is called before the JavaScript code is executed (*since version 3.0.0*).
This might delay the execution of the JavaScript code and is not covered by the ``timeout`` pa
…[1070 chars omitted]…
able=protected-access
def __await__(self) -> Any:
try:
yield from asyncio.wait_for(self._event.wait(), self.timeout).__await__()
except asyncio.TimeoutError as e:
raise TimeoutError(f'JavaScript did not respond within {self.timeout:.1f} s') from e
else:
return self._result
finally:
self._instances.pop(self.request_id)$ cd /repo && grep -rn "Prefetch\|prefetch" nicegui --include=*.py | grep -v client.py; grep -rn "prerender" nicegui --include=*.py | head
output · 1 lines
nicegui/client.py:537: return 'prefetch' in purpose and 'prerender' not in purpose
$ cd /repo && grep -rn "_connected\b" nicegui --include=*.py
output · 6 lines
nicegui/client.py:101: self._connected = asyncio.Event() nicegui/client.py:276: self._connected.clear() nicegui/client.py:278: await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout) nicegui/client.py:382: self._connected.set() nicegui/client.py:503: self._connected.set() # for terminating connected() waits nicegui/client.py:504: self._connected.clear()
$ cd /repo && grep -n "def stop\|_stopped\|def enqueue_message" -A6 nicegui/outbox.py | head -40
output · 12 lines
75: def enqueue_message(self, message_type: MessageType, data: Payload, target_id: ClientId) -> None: 76- """Enqueue a message for the given client.""" 77- self.client.check_existence() 78- self.messages.append((target_id, message_type, data)) 79- self._set_enqueue_event() 80- 81- async def loop(self) -> None: -- 180: def stop(self) -> None: 181- """Stop the outbox loop.""" 182- self._should_stop = True 183- self._set_enqueue_event() # wake the loop so it checks _should_stop immediately
$ cd /repo && python - <<'PY'
output · 35 lines
102: self._deleted_event = asyncio.Event()
107: self._deleted = False
164: def is_deleted(self) -> bool:
166: return self._deleted
277: if self._deleted:
290: self._deleted_event.clear()
291: await self._deleted_event.wait()
317: if self._deleted:
469: element._deleted = True # pylint: disable=protected-access
500: self._deleted_event.set()
"""Return the title of the page."""
return self.page.resolve_title() if self.title is None else self.title23 more lines
async def connected(self, timeout: float | None = None) -> None:
"""Block execution until the client is connected.
:param timeout: timeout in seconds (default: ``None``)
"""
if self.has_socket_connection:
return
self._waiting_for_connection.set()
self._connected.clear()
if
…[161 chars omitted]…
)
except asyncio.TimeoutError as e:
raise ClientConnectionTimeout(self) from e
async def disconnected(self) -> None:
"""Block execution until the client disconnects."""
if not self.has_socket_connection:
await self.connected()
if self.id in self.instances:
self._waiting_for_disconnect.set()
self._deleted_event.clear()$ cd /repo && sed -n 1,60p tests/conftest.py && grep -rn "prune\|connected" tests/test_client.py 2>/dev/null | head; ls tests | head -40
output · 51 lines
import os
os.environ.setdefault('MPLBACKEND', 'Agg') # force a non-GUI Matplotlib backend during tests
pytest_plugins = ['nicegui.testing.plugin']
def pytest_collection_modifyitems(items) -> None:
for item in items:
if 'screen' in item.fixturenames:
item.add_marker('screen')
README.md39 more lines
__init__.py conftest.py media test_add_html.py test_aggrid.py test_air.py test_altair.py test_alternate_ui_frameworks.py test_anywidget.py test_api_router.py test_audio.py test_auto_context.py test_awaitable_response.py test_background_tasks.py test_binding.py test_button.py test_button_dropdown.py test_button_group.py test_card.py test_carousel.py test_chat_message.py test_chip.py test_clipboard.py test_code.py test_codemirror.py test_codemirror_keybindings.py test_codemirror_line_anchors.py test_color_input.py test_color_picker.py test_colors.py test_context_menu.py test_dark_mode.py test_date.py test_date_input.py test_defaults.py test_dialog.py test_download.py test_echart.py test_editor.py
$ cd /repo && sed -n 1,80p nicegui/testing/plugin.py
output · 11 lines
# pylint: disable=unused-import
from .general_fixtures import nicegui_reset_globals, pytest_addoption # noqa: F401
from .screen_plugin import ( # noqa: F401
nicegui_chrome_options,
nicegui_driver,
nicegui_remove_all_screenshots,
pytest_configure, # wraps general_fixtures' to also set up Screen.PORT/SCREENSHOT_DIR/DOWNLOAD_DIR
pytest_runtest_makereport,
screen,
)
from .user_plugin import create_user, user # noqa: F401$ cd /repo && cat > /tmp/repro_test.py <<'EOF'
output · 9 lines
E ==================================== ERRORS ==================================== _________________ ERROR at setup of test_prune_releases_waiter _________________ 'test_prune_releases_waiter' requested an async fixture 'user', with no plugin or hook that handled it. This is an error, as pytest does not natively support it. See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture =========================== short test summary info ============================ ERROR ../tmp/repro_test.py::test_prune_releases_waiter - Failed: 'test_prune_... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 error in 0.01s
$ cd /repo && cp /tmp/repro_test.py tests/test_zz_repro.py && python -m pytest tests/test_zz_repro.py -x -q 2>&1 | tail -20
output · 20 lines
^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <nicegui.testing.user.User object at 0x76a3e54e9310>, path = '/p'
clear_forward_history = True
async def open(self, path: str, *, clear_forward_history: bool = True) -> Client:
"""Open the given path."""
response = await self.http_client.get(path, follow_redirects=True)
> assert response.status_code == 200, f'Expected status code 200, got {response.status_code}'
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError: Expected status code 200, got 4048 more lines
nicegui/testing/user.py:72: AssertionError ------------------------------ Captured log call ------------------------------- WARNING nicegui:nicegui.py:180 http://test/p not found =========================== short test summary info ============================ FAILED tests/test_zz_repro.py::test_prune_releases_waiter - AssertionError: E... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 1.36s
$ cd /repo && sed -n 1,80p nicegui/testing/general_fixtures.py && sed -n 1,60p nicegui/testing/user_plugin.py
output · 41 lines
import atexit import shutil import tempfile from pathlib import Path import pytest from .. import app from ..storage import Storage from . import general # pylint: disable=redefined-outer-name
29 more lines
_configured = False
def pytest_addoption(parser: pytest.Parser) -> None:
"""Add pytest option for main file."""
parser.addini('main_file', 'main file', default='main.py')
def pytest_configure(config: pytest.Config) -> None:
"""Set up a session-unique storage path and register the "nicegui_main_file" marker."""
global _configured # pylint: disable=global-statement # noqa: PLW0603
if _configured:
return
_configured = True
Storage.path = Path(tempfile.mkdtemp(prefix='nicegui-test-storage-')).resolve()
atexit.register(shutil.rmtree, Storage.path, ignore_errors=True)
app.storage = Storage() # rebuild app.storage so its FilePersistentDict picks up
…[1934 chars omitted]…
]: # pylint: disable=unused-argument
"""Create a fixture for building new users."""
prepare_simulation()
try:
async with core.app.router.lifespan_context(core.app):
yield lambda: User(httpx.AsyncClient(transport=httpx.ASGITransport(core.app), base_url='http://test'))
finally:
ui.navigate = Navigate()
ui.notify = notify
ui.download = download$ cd /repo && sed -n 1,60p nicegui/testing/user_simulation.py
output · 35 lines
import os import runpy from collections.abc import AsyncGenerator, Callable from contextlib import asynccontextmanager from pathlib import Path import httpx from .. import core, ui from ..functions.download import download from ..functions.navigate import Navigate from ..functions.notify import notify
23 more lines
from .general import nicegui_reset_globals, prepare_simulation
from .user import User
@asynccontextmanager
async def user_simulation(
root: Callable | None = None, *, main_file: str | os.PathLike | None = None,
) -> AsyncGenerator[User]:
"""Context manager for test user simulation.
This context manager yields a ``User`` connected to a NiceGUI app within an isolated test context.
:param root: root function which is passed directly to ``ui.run``; mutually exclusive with ``main_file`` argument.
:param main_file: path to a NiceGUI main file executed via ``runpy.run_
…[669 chars omitted]…
async with core.app.router.lifespan_context(core.app):
async with httpx.AsyncClient(transport=httpx.ASGITransport(core.app), base_url='http://test') as client:
yield User(client)
finally:
os.environ.pop('NICEGUI_USER_SIMULATION', None)
ui.navigate = Navigate()
ui.notify = notify
ui.download = download$ cd /repo && cat > tests/test_zz_repro.py <<'EOF'
output · 14 lines
async def test_prune_releases_waiter():
async with user_simulation(root=build) as user:
task = asyncio.create_task(user.open('/p'))
await asyncio.sleep(0.5)
client = next(iter(Client.instances.values()))
> assert not client.has_socket_connection
E assert not True
E + where True = <nicegui.client.Client object at 0x7e2cb7850800>.has_socket_connection
tests/test_zz_repro.py:21: AssertionError
=========================== short test summary info ============================
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - assert not True2 more lines
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 1.74s
$ cd /repo && grep -rn "has_socket_connection\|_simulate\|socket" nicegui/testing/user.py | head; grep -n "has_socket_connection" -B3 -A6 nicegui/client.py | sed -n 1,40p
output · 38 lines
11:import socketio 34: self.sio = socketio.AsyncClient() 156- return self.request.client.host if self.request.client is not None else '' 157- 158- @property 159: def has_socket_connection(self) -> bool: 160- """Whether the client is connected.""" 161- return self.tab_id is not None 162- 163- @property 164- def is_deleted(self) -> bool: 165- """Whether the client has been deleted (e.g. by browser disconnect after ``reconnect_timeout``)."""
26 more lines
--
270-
271- :param timeout: timeout in seconds (default: ``None``)
272- """
273: if self.has_socket_connection:
274- return
275- self._waiting_for_connection.set()
276- self._connected.clear()
277- if self._deleted:
278- return
279- try:
--
283-
284- async def disconnected(self) -> None:
285- """Block execution until the cl
…[254 chars omitted]…
await self._deleted_event.wait()
292-
--
522- client
523- for client in cls.instances.values()
524- if (
525: not client.has_socket_connection and
526- not client._delete_tasks and # pylint: disable=protected-access
527- client.created <= time.time() - client_age_threshold
528- )$ cd /repo && sed -n 1,60p nicegui/context.py
output · 37 lines
from __future__ import annotations
from typing import TYPE_CHECKING
from . import core
from .slot import Slot
if TYPE_CHECKING:
from .client import Client
class Context:25 more lines
@property
def slot_stack(self) -> list[Slot]:
"""Return the slot stack of the current asyncio task."""
stack = Slot.get_stack()
if not stack and not core.script_mode and not core.app.is_started:
# create a pseudo client to "survive" until reaching `ui.run()`
from .client import Client # pylint: disable=import-outside-toplevel,cyclic-import
from .page import page # pylint: disable=import-outside-toplevel,cyclic-import
if not Client.instances: # in case some kind of dummy client is already created
core.script_mode = True
core.script_client = Client(page('/')).__enter__() # pylint: disable=unneces
…[331 chars omitted]…
is empty.\n'
'This may happen if you try to create UI from a background task.\n'
'To fix this, enter the target slot explicitly using `with container_element:`.')
return slot_stack[-1]
@property
def client(self) -> Client:
"""Return the current client."""
return self.slot.parent.client
context = Context()$ cd /repo && cat > tests/test_zz_repro.py <<'EOF'
output · 15 lines
tests/test_zz_repro.py:16: StopIteration
The above exception was the direct cause of the following exception:
async def test_prune_releases_waiter():
async with user_simulation(root=build):
> client = await make_unconnected_client()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E RuntimeError: coroutine raised StopIteration
tests/test_zz_repro.py:21: RuntimeError
=========================== short test summary info ============================3 more lines
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - RuntimeError: cor... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 1.05s
$ cd /repo && sed -i 's/async with user_simulation(root=build):/async with user_simulation(root=None):\n build()/' tests/test_zz_repro.py && python -m pytest tests/test_zz_repro.py -x -q 2>&1 | tail -15
output · 15 lines
ERROR nicegui:app.py:187 Request is not set
Traceback (most recent call last):
File "/repo/nicegui/timer.py", line 113, in _invoke_callback
await result
File "/repo/nicegui/app/app.py", line 428, in prune_user_storage
client_session_ids = {client.request.session['id'] for client in Client.instances.values()}
^^^^^^^^^^^^^^
File "/repo/nicegui/client.py", line 146, in request
raise RuntimeError('Request is not set')
RuntimeError: Request is not set
=========================== short test summary info ============================
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - RuntimeError: Req...3 more lines
ERROR tests/test_zz_repro.py::test_prune_releases_waiter - pytest.PytestUnrai... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 2 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed, 1 error in 1.32s
$ cd /repo && sed -i 's/ Client.prune_instances(client_age_threshold=0)/ client.delete()/' tests/test_zz_repro.py && python -m pytest tests/test_zz_repro.py -x -q 2>&1 | tail -15
output · 15 lines
ERROR nicegui:app.py:187 Request is not set
Traceback (most recent call last):
File "/repo/nicegui/timer.py", line 113, in _invoke_callback
await result
File "/repo/nicegui/app/app.py", line 428, in prune_user_storage
client_session_ids = {client.request.session['id'] for client in Client.instances.values()}
^^^^^^^^^^^^^^
File "/repo/nicegui/client.py", line 146, in request
raise RuntimeError('Request is not set')
RuntimeError: Request is not set
=========================== short test summary info ============================
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - RuntimeError: Req...3 more lines
ERROR tests/test_zz_repro.py::test_prune_releases_waiter - pytest.PytestUnrai... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 2 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed, 1 error in 1.53s
$ cd /repo && python - <<'PY'
output · 15 lines
self._exception_handlers: list[Callable[[Exception], Any] | Callable[[], Any]] = []
self.page = page
self.outbox = Outbox(self)
if self._request is not None:
> self._request.scope['nicegui_page_path'] = self.page.path
^^^^^^^^^^^^^^
E AttributeError: 'function' object has no attribute 'path'
nicegui/client.py:117: AttributeError
=========================== short test summary info ============================3 more lines
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - AttributeError: '... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 1.32s
$ cd /repo && grep -n "page_routes\|def page\b\|_page\b" nicegui/page.py | head -20
output · 7 lines
124: Client.page_routes[func] = self.path
143: def create_500_error_page(e: Exception, request: Request) -> Response:
166: def create_terminal_error_page(message: str, request: Request) -> Response:
184: return create_500_error_page(e, request)
193: return create_500_error_page(e, request)
209: return create_terminal_error_page(
228: return create_terminal_error_page('The client was deleted while the page was being built.', request)$ cd /repo && sed -n 100,135p nicegui/page.py && sed -n 215,235p nicegui/page.py
output · 28 lines
return self.dark if self.dark is not ... else core.app.config.dark
def resolve_language(self) -> Language | None:
"""Return the language of the page."""
return self.language if self.language is not ... else core.app.config.language
def resolve_reconnect_timeout(self) -> float:
"""Return the reconnect_timeout of the page."""
return self.reconnect_timeout if self.reconnect_timeout is not None else core.app.config.reconnect_timeout
def resolve_markdown(self) -> bool:
"""Return whether the page should serve Markdown when ``Accept: text/markdown`` is requested.16 more lines
*Added in version 3.11.0*
"""
return self.markdown if self.markdown is not None else core.app.config.markdown
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
core.app.remove_route(self.path) # make sure only the
…[1550 chars omitted]…
ge was being built.', request)
if not await client.sub_pages_router._can_resolve_full_path(client): # pylint: disable=protected-access
log.warning(f'{request.url} not found')
return client.build_response(request, 404)
if isinstance(result, Response): # if setup returns a response, we don't need to render the page
return result$ cd /repo && python - <<'PY'
output · 14 lines
tests/test_zz_repro.py:24:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
async def make_unconnected_client():
from starlette.requests import Request
> p = page('/p')
^^^^^^^^^^
E TypeError: 'module' object is not callable
tests/test_zz_repro.py:17: TypeError
=========================== short test summary info ============================
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - TypeError: 'modul...2 more lines
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 1.26s
$ cd /repo && sed -i 's/from nicegui import ui, Client, page/from nicegui import ui, Client\nfrom nicegui.page import page/' tests/test_zz_repro.py && python -m pytest tests/test_zz_repro.py -x -q 2>&1 | tail -15
output · 2 lines
.. [100%] 2 passed in 0.93s
$ cd /repo && git stash push nicegui/client.py && python -m pytest tests/test_zz_repro.py -x -q 2>&1 | tail -6; git stash pop
output · 19 lines
Saved working directory and index state WIP on master: bdaab07 base
~~~~~~~~~~~~~~~~~~~~~~^^^^^^
KeyError: 'id'
=========================== short test summary info ============================
FAILED tests/test_zz_repro.py::test_prune_releases_waiter - TimeoutError: Jav...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed in 2.08s
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: nicegui/client.py7 more lines
Untracked files:
(use "git add <file>..." to include in what will be committed)
tests/test_zz_repro.py
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (9124529f939f29bc891d1b7e732787a223404f0a)$ cd /repo && python -m pytest tests/test_javascript.py tests/test_awaitable_response.py tests/test_lifecycle.py tests/test_page.py tests/test_client.py -q 2>&1 | tail -6
output · 4 lines
ERROR: file or directory not found: tests/test_client.py no tests ran in 0.01s
$ cd /repo && python -m pytest tests/test_javascript.py tests/test_awaitable_response.py tests/test_lifecycle.py tests/test_page.py -q 2>&1 | tail -6
output · 2 lines
................................................. [100%] 49 passed in 61.20s (0:01:01)
$ cd /repo && python -m pytest tests/test_element_delete.py tests/test_3028.py tests/test_background_tasks.py tests/test_timer.py -q 2>&1 | tail -4
output · 4 lines
ERROR: file or directory not found: tests/test_3028.py no tests ran in 0.01s
$ cd /repo && python -m pytest tests/test_element_delete.py tests/test_background_tasks.py tests/test_timer.py -q 2>&1 | tail -4 && rm tests/test_zz_repro.py && git diff
output · 29 lines
=========================== short test summary info ============================
FAILED tests/test_timer.py::test_cleanup - AssertionError: only current timer...
FAILED tests/test_timer.py::test_no_leak_when_client_deleted - assert not True
2 failed, 33 passed in 52.01s
diff --git a/nicegui/client.py b/nicegui/client.py
index 90f4c34..1873e5b 100644
--- a/nicegui/client.py
+++ b/nicegui/client.py
@@ -274,6 +274,8 @@ class Client:
return
self._waiting_for_connection.set()
self._connected.clear()17 more lines
+ if self._deleted:
+ return
try:
await asyncio.wait_for(self._connected.wait(), timeout=None if _is_prefetch(self.request) else timeout)
except asyncio.TimeoutError as e:
@@ -312,6 +314,8 @@ class Client:
async def send_and_wait():
self.outbox.enqueue_message('run_javascript', {'code': code, '
…[336 chars omitted]…
del Client.instances[self.id]
self._deleted = True
- self._connected.set() # for terminating connected() waits
- self._connected.clear()
+ self._connected.set() # for terminating connected() waits; stays set so later waits return promptly
def check_existence(self) -> None:
"""Check if the client still exists and print a warning if it doesn't."""$ cd /repo && git stash push nicegui/client.py -q && python -m pytest tests/test_timer.py -q 2>&1 | tail -4; git stash pop -q
output · 4 lines
=========================== short test summary info ============================ FAILED tests/test_timer.py::test_cleanup - AssertionError: only current timer... FAILED tests/test_timer.py::test_no_leak_when_client_deleted - assert not True 2 failed, 15 passed in 34.65s
Pre-existing timer test failures are unrelated (fail on baseline too). My fix is complete:
$ done: {"notes": "Fixed Client.connected() to return promptly for deleted clients (delete() now leaves _connected set; connected() re-checks _deleted after clearing), and run_javascript's send_and_wait returns None when the client is deleted. Reproducer passes with fix, fails on baseline; related existing tests pass (test_timer failures pre-exist)."}output · 1 lines
submitted.