Skip to content
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

Using @ui.page decorator to setup pages #86

Merged
merged 37 commits into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
dcda1a4
began experimenting with page decorator
rodja Sep 13, 2022
1d6d07c
breaking change: all pages must be created with a decorator
rodja Sep 13, 2022
a09ddb4
cleanup
falkoschindler Sep 13, 2022
32688f9
first step to remove page_stack
rodja Sep 13, 2022
9153eda
Merge branch '#61-page-decorator' of github.com:zauberzeug/nicegui in…
rodja Sep 13, 2022
7385775
remvoing last bits of page_stack
rodja Sep 13, 2022
84d3f19
fixed TODO message
rodja Sep 13, 2022
39aa280
removed page context an simplified view_stack
rodja Sep 13, 2022
f6a6cf2
renaming
rodja Sep 14, 2022
cc50294
creating default page for element creation without @ui.page decorator
rodja Sep 14, 2022
b32442c
code review; introduce PageBuilder dataclass
falkoschindler Sep 14, 2022
2d6ad59
fix adding head and body html to pages
falkoschindler Sep 14, 2022
fbecb7c
remove unnecessary globals.main_page
falkoschindler Sep 14, 2022
d42eb5a
move index page creation into page.py
falkoschindler Sep 14, 2022
2c5ea4d
allow configuring pages via arguments to ui.page
falkoschindler Sep 14, 2022
1e0315c
apply default page configuration from ui.run to index page
falkoschindler Sep 14, 2022
c8240be
fix delete_flag for implicitly created index page
falkoschindler Sep 15, 2022
d940d65
add 404 error page
falkoschindler Sep 15, 2022
392732e
Merge branch 'main' into #61-page-decorator
falkoschindler Sep 15, 2022
a2ba626
fix page examples; fix on_connect for non-shared pages
falkoschindler Sep 15, 2022
f89f0ba
allow passing page functions to ui.link and ui.open
falkoschindler Sep 15, 2022
43b2933
provide ui.run_javascript and ui.await_javascript
falkoschindler Sep 15, 2022
35798be
tiny fix
falkoschindler Sep 15, 2022
6facf8b
remove pre-evaluation of ui.run
falkoschindler Sep 15, 2022
2216e64
serve highcharts and aggrid libraries only if chart or table elements…
falkoschindler Sep 15, 2022
6040b83
remove obsolete excludes; check for MATPLOTLIB environment variable
falkoschindler Sep 15, 2022
483970b
serve dependencies for custom elements on demand
falkoschindler Sep 15, 2022
108dc48
allow dynamically adding dependencies
falkoschindler Sep 16, 2022
466517a
move page and update out of elements folder
falkoschindler Sep 16, 2022
e64569d
reload page when new dependencies are added dynamically
falkoschindler Sep 16, 2022
dea7a42
update documentation
falkoschindler Sep 16, 2022
4371de2
improve upload example
falkoschindler Sep 16, 2022
6d59940
fix order of dependencies
falkoschindler Sep 16, 2022
c8ad913
add documentation and example about shared, private and index pages
falkoschindler Sep 16, 2022
d618510
remove obsolete special treatment for index page
falkoschindler Sep 16, 2022
e24d61c
removed warning log
rodja Sep 17, 2022
14c5f35
minor documentation improvements
rodja Sep 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 21 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Dock
## Features

- browser-based graphical user interface
- shared state between multiple browser windows
- implicit reload on code change
- standard GUI elements like label, button, checkbox, switch, slider, input, file upload, ...
- simple grouping with rows, columns, cards and dialogs
Expand All @@ -31,13 +30,17 @@ NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Dock
- plot graphs and charts,
- render 3D scenes,
- get steering events via virtual joysticks
- annotate images
- annotate and overlay images
- interact with tables
- navigate foldable tree structures
- built-in timer to refresh data in intervals (even every 10 ms)
- straight-forward data binding to write even less code
- notifications, dialogs and menus to provide state of the art user interaction
- shared and individual web pages
- ability to add custom routes and data responses
- capture keyboard input for global shortcuts etc
- customize look by defining primary, secondary and accent colors
- live-cycle events and session data

## Installation

Expand Down Expand Up @@ -73,38 +76,30 @@ Full documentation can be found at [https://nicegui.io](https://nicegui.io).

You can call `ui.run()` with optional arguments:

<!-- prettier-ignore-start -->
<!-- NOTE: to keep explicit underscores `\_` -->

- `host` (default: `'0.0.0.0'`)
- `port` (default: `8080`)
- `title` (default: `'NiceGUI'`)
- `favicon` (default: `'favicon.ico'`)
- `dark`: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
- `reload`: automatically reload the ui on file changes (default: `True`)
- `main_page_classes`: configure Quasar classes of main page (default: `'q-ma-md column items-start'`)
- `binding_refresh_interval`: time between binding updates (default: `0.1` seconds, bigger is more cpu friendly)
- `show`: automatically open the ui in a browser tab (default: `True`)
- `reload`: automatically reload the ui on file changes (default: `True`)
- `uvicorn_logging_level`: logging level for uvicorn server (default: `'warning'`)
- `uvicorn_reload_dirs`: string with comma-separated list for directories to be monitored (default is current working directory only)
- `uvicorn_reload_includes`: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
- `uvicorn_reload_excludes`: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
- `main_page_classes`: configure Quasar classes of main page (default: `'q-ma-md column items-start'`)
- `binding_refresh_interval`: time between binding updates (default: `0.1` seconds, bigger is more cpu friendly)
- `exclude`: comma-separated string to exclude libraries (with corresponding elements) to save bandwidth and/or startup time:
- "aggrid" (`ui.table`)
- "colors" (`ui.colors`)
- "custom\_example" (`ui.custom_example`)
- "highcharts" (`ui.chart`)
- "interactive\_image" (`ui.interactive_image`)
- "keyboard" (`ui.keyboard`)
- "log" (`ui.log`)
- "matplotlib" (`ui.plot` and `ui.line_plot`)
- "nipple" (`ui.joystick`)
- "three" (`ui.scene`)

<!-- prettier-ignore-end -->

The environment variables `HOST` and `PORT` can also be used to configure NiceGUI.

To avoid the potentially costly import of Matplotlib, you set the environment variable `MATPLOTLIB=false`.
This will make `ui.plot` and `ui.line_plot` unavailable.

Note:
The parameter `exclude` from earlier versions of NiceGUI has been removed.
Libraries are now automatically served on demand.
As a small caveat, the page will be reloaded if a new dependency is added dynamically, e.g. when adding a `ui.chart` only after pressing a button.

## Docker

You can use our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) for pain-free installation:
Expand All @@ -120,12 +115,13 @@ Code modification triggers an automatic reload.
## Why?

We like [Streamlit](https://streamlit.io/) but find it does [too much magic when it comes to state handling](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651).
In search for an alternative nice library to write simple graphical user interfaces in Python we discovered [justpy](https://justpy.io/).
While too "low-level HTML" for our daily usage it provides a great basis for "NiceGUI".
In search for an alternative nice library to write simple graphical user interfaces in Python we discovered [JustPy](https://justpy.io/).
While it is too "low-level HTML" for our daily usage, it provides a great basis for NiceGUI.

## API
## Documentation and Examples

The API reference is hosted at [https://nicegui.io](https://nicegui.io) and is [implemented with NiceGUI itself](https://github.com/zauberzeug/nicegui/blob/main/main.py).
The API reference is hosted at [https://nicegui.io](https://nicegui.io).
It is [implemented with NiceGUI itself](https://github.com/zauberzeug/nicegui/blob/main/main.py).
You may also have a look at [examples.py](https://github.com/zauberzeug/nicegui/tree/main/examples.py) for more demonstrations of what you can do with NiceGUI.

## Abstraction
Expand Down
72 changes: 62 additions & 10 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ def h3(text: str) -> None:
button = ui.button(on_click=picker.open).props('icon=colorize')

with example(ui.upload):
ui.upload(on_upload=lambda e: upload_result.set_text(e.files))
upload_result = ui.label()
ui.upload(on_upload=lambda e: ui.notify(f'{len(e.files[0])} bytes'))

h3('Markdown and HTML')

Expand Down Expand Up @@ -584,23 +583,55 @@ async def async_task():
h3('Pages and Routes')

with example(ui.page):
with ui.page('/other_page'):
@ui.page('/other_page')
def other_page():
ui.label('Welcome to the other side')
ui.link('Back to main page', '#page')

with ui.page('/dark_page', dark=True):
@ui.page('/dark_page', dark=True)
def dark_page():
ui.label('Welcome to the dark side')
ui.link('Back to main page', '#page')

ui.link('Visit other page', 'other_page')
ui.link('Visit dark page', 'dark_page')
ui.link('Visit other page', other_page)
ui.link('Visit dark page', dark_page)

shared_and_private_pages = '''#### Shared and Private Pages

By default, pages created with the `@ui.page` decorator are "private".
Their content is re-created for each client.
Thus, in the example to the right, the displayed ID changes when the browser reloads the page.

With `shared=True` you can create a shared page.
Its content is created once at startup and each client sees the *same* elements.
Here, the displayed ID remains constant when the browser reloads the page.

#### Index page

All elements that are not created within a decorated page function are automatically added to a new, *shared* index page at route "/".
To make it "private" or to change other attributes like title, favicon etc. you can wrap it in a page function with `@ui.page('/', ...)` decorator.
'''
with example(shared_and_private_pages):
from uuid import uuid4

@ui.page('/private_page')
async def private_page():
ui.label(f'private page with ID {uuid4()}')

@ui.page('/shared_page', shared=True)
async def shared_page():
ui.label(f'shared page with ID {uuid4()}')

ui.link('private page', private_page)
ui.link('shared page', shared_page)

with example(ui.open):
with ui.page('/yet_another_page') as other:
@ui.page('/yet_another_page')
def yet_another_page():
ui.label('Welcome to yet another page')
ui.button('RETURN', on_click=lambda e: ui.open('#open', e.socket))

ui.button('REDIRECT', on_click=lambda e: ui.open(other, e.socket))
ui.button('REDIRECT', on_click=lambda e: ui.open(yet_another_page, e.socket))

add_route = '''#### Route

Expand Down Expand Up @@ -655,9 +686,30 @@ def handle_connection(request: Request):
id_counter[request.session_id] += 1
visits.set_text(f'{len(id_counter)} unique views ({sum(id_counter.values())} overall) since {creation}')

with ui.page('/session_demo', on_connect=handle_connection) as page:
@ui.page('/session_demo', on_connect=handle_connection)
def session_demo():
global visits
visits = ui.label()

ui.link('Visit session demo', page)
ui.link('Visit session demo', session_demo)

javascript = '''#### JavaScript

With `ui.run_javascript()` you can run arbitrary JavaScript code on a page that is executed in the browser.
The asynchronous function will return after sending the command.

With `ui.await_javascript()` you can send a JavaScript command and wait for its response.
The asynchronous function will only return after receiving the result.
'''
with example(javascript):
async def run_javascript():
await ui.run_javascript('alert("Hello!")')

async def await_javascript():
response = await ui.await_javascript('Date()')
ui.notify(f'Browser time: {response}')

ui.button('run JavaScript', on_click=run_javascript)
ui.button('await JavaScript', on_click=await_javascript)

ui.run()
4 changes: 2 additions & 2 deletions nicegui/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from justpy.htmlcomponents import HTMLBaseComponent

from .globals import config
from . import globals
from .task_logger import create_task

bindings = defaultdict(list)
Expand All @@ -31,7 +31,7 @@ async def loop():
update_views(visited_views)
if time.time() - t > 0.01:
logging.warning(f'binding update for {len(visited_views)} visited views took {time.time() - t:.3f} s')
await asyncio.sleep(config.binding_refresh_interval)
await asyncio.sleep(globals.config.binding_refresh_interval)


async def update_views_async(views: Set[HTMLBaseComponent]):
Expand Down
60 changes: 0 additions & 60 deletions nicegui/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import ast
import inspect
import os
from dataclasses import dataclass
from typing import Optional

from . import globals


@dataclass
class Config():
Expand All @@ -15,61 +11,5 @@ class Config():
title: str = 'NiceGUI'
favicon: str = 'favicon.ico'
dark: Optional[bool] = False
reload: bool = True
show: bool = True
uvicorn_logging_level: str = 'warning'
uvicorn_reload_dirs: str = '.'
uvicorn_reload_includes: str = '*.py'
uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*'
main_page_classes: str = 'q-ma-md column items-start'
binding_refresh_interval: float = 0.1
exclude: str = ''


excluded_endings = (
'<string>',
'spawn.py',
'runpy.py',
os.path.join('debugpy', 'server', 'cli.py'),
os.path.join('debugpy', '__main__.py'),
'pydevd.py',
'_pydev_execfile.py',
)
for f in reversed(inspect.stack()):
if not any(f.filename.endswith(ending) for ending in excluded_endings):
filepath = f.filename
break
else:
raise Exception('Could not find main script in stacktrace')

try:
with open(filepath) as f:
source = f.read()
except FileNotFoundError:
config = Config()
else:
for node in ast.walk(ast.parse(source)):
try:
func = node.value.func
if func.value.id == 'ui' and func.attr == 'run':
args = {
keyword.arg:
keyword.value.n if isinstance(keyword.value, ast.Num) else
keyword.value.s if isinstance(keyword.value, ast.Str) else
keyword.value.value
for keyword in node.value.keywords
}
config = Config(**args)
globals.pre_evaluation_succeeded = True
break
except AttributeError:
continue
else:
config = Config()

os.environ['HOST'] = config.host
os.environ['PORT'] = str(config.port)
os.environ['STATIC_DIRECTORY'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
os.environ['TEMPLATES_DIRECTORY'] = os.path.join(os.environ['STATIC_DIRECTORY'], 'templates')

globals.config = config
11 changes: 10 additions & 1 deletion nicegui/elements/chart.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import asyncio
from typing import Dict

import justpy as jp

from ..task_logger import create_task
from .element import Element

jp.template_options['highcharts'] = False


class Chart(Element):

def __init__(self, options: Dict):
"""Chart

An element to create a chart using `Highcharts <https://www.highcharts.com/>`_.

:param options: dictionary of highcharts options
:param options: dictionary of Highcharts options
"""
view = jp.HighCharts(temp=False)
view.options = self.options = jp.Dict(**options)
super().__init__(view)

if not jp.template_options['highcharts'] and asyncio.get_event_loop().is_running():
create_task(self.page.run_javascript('location.reload()'))
jp.template_options['highcharts'] = True
4 changes: 2 additions & 2 deletions nicegui/elements/colors.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from ..routes import add_dependencies
from .custom_view import CustomView
from .element import Element

CustomView.use(__file__)


class ColorsView(CustomView):

def __init__(self, primary, secondary, accent, positive, negative, info, warning):
add_dependencies(__file__)
super().__init__('colors',
primary=primary,
secondary=secondary,
Expand Down
4 changes: 2 additions & 2 deletions nicegui/elements/custom_example.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from ..routes import add_dependencies
from .custom_view import CustomView
from .element import Element

CustomView.use(__file__)


class CustomExampleView(CustomView):

def __init__(self, on_change):
add_dependencies(__file__)
super().__init__('custom_example', value=0)

self.on_change = on_change
Expand Down
24 changes: 0 additions & 24 deletions nicegui/elements/custom_view.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import os.path
from typing import List

import justpy as jp
from starlette.responses import FileResponse
from starlette.routing import Route


class CustomView(jp.JustpyBaseComponent):
Expand Down Expand Up @@ -31,22 +26,3 @@ def convert_object_to_dict(self):
'style': self.style,
'options': self.options,
}

@staticmethod
def use(py_filepath: str, dependencies: List[str] = []):
vue_filepath = os.path.splitext(os.path.realpath(py_filepath))[0] + '.js'

for dependency in dependencies:
is_remote = dependency.startswith('http://') or dependency.startswith('https://')
src = dependency if is_remote else f'lib/{dependency}'
if src not in jp.component_file_list:
jp.component_file_list += [src]
if not is_remote:
filepath = f'{os.path.dirname(vue_filepath)}/{src}'
route = Route(f'/{src}', lambda _, filepath=filepath: FileResponse(filepath))
jp.app.routes.insert(0, route)

if vue_filepath not in jp.component_file_list:
filename = os.path.basename(vue_filepath)
jp.app.routes.insert(0, Route(f'/{filename}', lambda _: FileResponse(vue_filepath)))
jp.component_file_list += [filename]