-
-
Notifications
You must be signed in to change notification settings - Fork 649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow creating a new page per client #85
Comments
I made some progress on the feature but there is still a lot to do (see list in the issue description). Noteworthy is our "requirement" to have a default page where elements are added. I only got it to work with a "shared" page (see cc50294). Because otherwise we need to re-evaluate the main script again and again. |
My current test script: from nicegui import ui
from uuid import uuid4
@ui.page('/individual_page')
def individual_page():
ui.label(f'your individual page with uuid {uuid4()}')
@ui.page('/shared_page', shared=True)
def shared_page():
ui.label(f'a shared page with uuid {uuid4()}')
# @ui.page('/')
# def index():
ui.link('individual page', '/individual_page')
ui.link('shared page', '/shared_page')
ui.run() |
@rodja I guess you primarily want to avoid getting a 500 Server Error ("UnboundLocalError: local variable 'func_to_run' referenced before assignment"), as it is with the current implementation. Returning 404 would be new behavior, but probably better than redirecting to "/". |
@falkoschindler exactly. If it is much simpler to avoid the 500 Server Errors be redirecting to /, we could create a new issue for the 404 page. But I guess it will be an equally amount of work... |
I think we are loosing the ability to exclude costly imports if we remove the pre-evaluation... 🤔 |
...and it's getting tricky to instantiate a default page without |
After all, NiceGUI might be even nicer, because most JS libraries will be automatically served on demand. And there are only a few optional exclusions left as environment variables. Update: Although the variables "HIGHCHARTS" and "AGGRID" are already read when importing JustPy, they are only written into a dictionary |
I thought I found a solution for injecting new dependencies into running applications. But it's probably more complicated than running let script = document.createElement("script");
script.src = "{src}";
document.body.prepend(script); and calling So I got a new, much simpler idea: In the rare case of dynamically introducing a new dependency, NiceGUI can simply trigger a |
I think we have a memory leak for private pages: #!/usr/bin/env python3
import gc
import os
import psutil
from nicegui import ui
@ui.page('/')
async def index_page():
process = psutil.Process(os.getpid())
gc.collect()
ui.label(f'memory: {process.memory_info().rss:,} bytes')
ui.run() The memory usage keeps growing when refreshing the page. |
Ok, the memory usage is similar for a plain JustPy app: #!/usr/bin/env python3
import gc
import os
import justpy as jp
import psutil
def hello_world():
wp = jp.WebPage()
process = psutil.Process(os.getpid())
gc.collect()
wp.add(jp.Div(text=f'memory: {process.memory_info().rss:,} bytes'))
return wp
jp.justpy(hello_world) |
See my implementation in #6. There's a line in on_disconnect_handler: |
@me21 Ok, but since I'm not holding the pages in a dictionary like you did, I'd expect the garbage collector to reclaim the memory. And especially JustPy's the bare hello-world example shouldn't have this issue. I opened an issue over there: justpy-org/justpy#547 |
Route("/{path:path}", func_to_run, doesn't exist with the path param any more. |
@falkoschindler remove_page is a JustPy method, not mine. Try calling it. |
Looks like an issue with starlette: fastapi/fastapi#4649 @me21 I tried, but it doesn't work. Apparently JustPy isn't the problem anyway, since its |
I think the code is ready for merging and hence I created the pull request #86. Please merge it, if you agree @falkoschindler. |
Ok, the pull request has been merged. But maybe we have still some work to do on main. Currently I'm struggling with the dependency-reload at Lines 87 to 93 in fdd580c
It seems that the |
And it seems we also have a problem with pages which are created after startup. |
I've started with selenium tests with the pull request #88. The motivation was to document a simple reproduction for page builders which are created after startup (the first Lines 36 to 44 in e9d775c
Real world scenarios can be found in RoSys. For example the KPI page: it should only be available if the final application calls the constructor. Which is normally after the startup of NiceGUI. |
Ok, the problem with page generators added after server start is fixed with d5e8e12. I'll try to find a minimal reproduction for the dependency-reload problem I saw a few days ago. |
The exception for the dependency-reload looks like this:
|
It's quite simple to reproduce the error: @ui.page('/')
def page():
ui.keyboard()
ui.run() This will print the TimeoutError to the console after startup. I also created a selenium test (which is still on the pytest branch): Lines 72 to 79 in 2b40c04
|
Strange. The TimeoutError can be easily avoided using from nicegui import ui
@ui.page('/', shared=True)
def page():
ui.label('hello world')
ui.keyboard(on_key=lambda e: print(e))
ui.run() |
Oh, I see! The Line 90 in 89c53c7
@ui.page decorator. I'll try to avoid using get_current_view in this case.
|
@rodja I think I fixed the issue with the above-mentioned In line nicegui/tests/test_dependencies.py Line 31 in 1172ff5
it is unclear how ui.keyboard should know where to place itself. The view_stack is empty at this point, which causes several problems:
|
There is another issue with reloading the page for new dependencies. The following example should add a joystick when pressing the button: from nicegui import ui
@ui.page('/')
def index():
def add():
with row:
ui.joystick()
row = ui.row()
ui.button('Add', on_click=add)
ui.run() But the joystick appears only after pressing the second time. This is unexpected, but explainable: The question is: How to add dependencies on private pages that loose their state when reloading. |
Dependencies are not found when running with from nicegui import ui
ui.joystick()
ui.run(reload=False) nipple.min.js and joystick.js are not found (HTTP 404). |
raise exception instead of reloading the page This does not work, since a simple private page won't be able to add a dependency: @ui.page('/')
def page():
ui.keyboard() I guess we want to support such a trivial example. |
I removed the automatic dependency management, because I think it's almost impossible to accomplish for private pages. When a new Vue component (e.g. "keyboard") is added to the DOM, we might be able to make connected clients load the required libraries (e.g. "keyboard.js"). But we would also need to trigger Vue to interpret the new DOM element (e.g. "") accordingly. This is pretty hard - at least without deeper understanding of Vue and JustPy. That's why I decided to simply reload the page for connected clients. But private pages loose their state, making this approach impractical. Instead I now re-introduced the |
I remember the import of some Python packages on the Beaglebone Black computer was heavy (up to 50 seconds on startup). |
Avoiding the costly Matplotlib import is now controlled with a new environment variable "MATPLOTLIB". You can set it to "false" to disable it: MATPLOTLIB=false python3 your_script.py |
On branch https://github.com/zauberzeug/nicegui/tree/auto-context I'm experimenting with finding the right parent for new UI elements. But it easily fails with async functions like the following example. "A" and "B" should be added to separate cards, but are mixed up completely: async def add_a():
await asyncio.sleep(0.1)
ui.label('A')
async def add_b():
await asyncio.sleep(0.1)
ui.label('B')
with ui.card():
ui.timer(1.0, add_a)
with ui.card():
ui.timer(1.1, add_b) |
In ee0fb9b I introduced separate view stacks per task. This solves the problem with async UI manipulation. |
@rodja It looks like this epic issue is finally ready for review, integration tests in some productive contexts and release. |
Fixed with release https://github.com/zauberzeug/nicegui/releases/tag/v0.9.0. |
Discussed in #61
Originally posted by falkoschindler August 26, 2022
Idea:
ui.page
can not only be used as context, but also accepts aCallable
argument for re-creating the page whenever a client connects.If the path if
/
or omitted, the page replaces the main page.This is related to #6, where @me21 posted a draft for a
PrivatePage
, which I haven't looked into, yet.After discussing several concepts, we tend to break with
with ui.page
contexts and move towards@ui.page
decorators. This simplifies defining page factory functions and is similar to how Flask and FastAPI define routes. In the end, pages are more like routes rather than hierarchical UI elements. Thus defining them like routes makes a lot of sense.Also, in the flavor of Flask, FastAPI or JustPy, pages should be "private" by default, i.e. without shared state between different clients. A page should only be "shared" when an optional decorator argument is set
True
.This change has interesting consequences:
with
expressions. Thus, the (internal)page_stack
is obsolete. This removes one source of confusion.ui.run
is no longer needed. Since the main script primarily defines functions, we don't need to bother about re-evaluation in case of auto-reload being activated. This complies with the traditional way of running uvicorn and removes another confusing "feature" of NiceGUI.To keep NiceGUI very accessible for new Python developers and to reduce boilerplate code for simple single-page applications, we plan to automatically create an index page if there is none. So the NiceGUI hello-world example should remain unchanged:
This will be equivalent to the following:
TODOs
page_stack
ui.page
factory)ui.page
ui.run
to default main page (which already has been created)main.py
)ui.link
andui.open
ui.run
ui.run
?reload=False
The text was updated successfully, but these errors were encountered: