Skip to content

Commit

Permalink
Merge branch 'v1.3' into on-air
Browse files Browse the repository at this point in the history
# Conflicts:
#	nicegui/templates/index.html
  • Loading branch information
falkoschindler committed Jul 11, 2023
2 parents af4cd78 + 8324c3f commit d73e934
Show file tree
Hide file tree
Showing 36 changed files with 920 additions and 740 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
test:
strategy:
matrix:
python: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python: ["3.8", "3.9", "3.10", "3.11"]
fail-fast: false
runs-on: ubuntu-latest
timeout-minutes: 40
Expand All @@ -23,7 +23,7 @@ jobs:
- name: install dependencies
run: |
poetry config virtualenvs.create false
poetry install
poetry install --all-extras
# install packages to run the examples
pip install opencv-python opencv-contrib-python-headless httpx replicate langchain openai simpy tortoise-orm
pip install -r tests/requirements.txt
Expand Down
4 changes: 2 additions & 2 deletions development.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7.16-slim
FROM python:3.8-slim

RUN apt update && apt install curl -y

Expand All @@ -11,6 +11,6 @@ RUN curl -sSL https://install.python-poetry.org | python3 - && \
WORKDIR /app

COPY ./pyproject.toml ./poetry.lock* main.py ./
RUN poetry install --no-root
RUN poetry install --no-root --all-extras

CMD python3 -m debugpy --listen 5678 main.py
2 changes: 1 addition & 1 deletion examples/lightbox/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Lightbox:
def __init__(self) -> None:
with ui.dialog().props('maximized').classes('bg-black') as self.dialog:
ui.keyboard(self._on_key)
self.large_image = ui.image().props('no-spinner')
self.large_image = ui.image().props('no-spinner fit=scale-down')
self.image_list: List[str] = []

def add_image(self, thumb_url: str, orig_url: str) -> ui.image:
Expand Down
1 change: 0 additions & 1 deletion nicegui.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"recommendations": [
"ms-python.vscode-pylance",
"ms-python.python",
"himanoa.python-autopep8",
"esbenp.prettier-vscode",
"littlefoxteam.vscode-python-test-adapter",
"cschleiden.vscode-github-actions",
Expand Down
2 changes: 1 addition & 1 deletion nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self, page: 'page', *, shared: bool = False) -> None:
self.shared = shared
self.on_air = False

with Element('q-layout', _client=self).props('view="HHH LpR FFF"').classes('nicegui-layout') as self.layout:
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:
with Element('q-page'):
self.content = Element('div').classes('nicegui-content')
Expand Down
80 changes: 45 additions & 35 deletions nicegui/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import hashlib
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
Expand All @@ -17,6 +18,7 @@
class Component:
key: str
name: str
path: Path

@property
def tag(self) -> str:
Expand All @@ -32,7 +34,7 @@ class VueComponent(Component):

@dataclass(**KWONLY_SLOTS)
class JsComponent(Component):
path: Path
pass


@dataclass(**KWONLY_SLOTS)
Expand All @@ -48,52 +50,59 @@ class Library:
libraries: Dict[str, Library] = {}


def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> Component:
def register_vue_component(path: Path) -> Component:
"""Register a .vue or .js Vue component.
Single-file components (.vue) are built right away
to delegate this "long" process to the bootstrap phase
and to avoid building the component on every single request.
:param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
:param base_path: base path where your libraries are located
:return: registered component
"""
path, key, name, suffix = deconstruct_location(location, base_path)
if suffix == '.vue':
key = compute_key(path)
name = get_name(path)
if path.suffix == '.vue':
if key in vue_components and vue_components[key].path == path:
return vue_components[key]
assert key not in vue_components, f'Duplicate VUE component {key}'
build = vbuild.VBuild(name, path.read_text())
vue_components[key] = VueComponent(key=key, name=name, html=build.html, script=build.script, style=build.style)
v = vbuild.VBuild(name, path.read_text())
vue_components[key] = VueComponent(key=key, name=name, path=path, html=v.html, script=v.script, style=v.style)
return vue_components[key]
if suffix == '.js':
if path.suffix == '.js':
if key in js_components and js_components[key].path == path:
return js_components[key]
assert key not in js_components, f'Duplicate JS component {key}'
js_components[key] = JsComponent(key=key, name=name, path=path)
return js_components[key]
raise ValueError(f'Unsupported component type "{suffix}"')
raise ValueError(f'Unsupported component type "{path.suffix}"')


def register_library(location: Path, base_path: Path = Path(__file__).parent / 'elements' / 'lib', *,
expose: bool = False) -> Library:
"""Register a *.js library.
:param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
:param base_path: base path where your libraries are located
:param expose: whether to expose library as an ESM module (exposed modules will NOT be imported)
:return: registered library
"""
path, key, name, suffix = deconstruct_location(location, base_path)
if suffix in {'.js', '.mjs'}:
def register_library(path: Path, *, expose: bool = False) -> Library:
"""Register a *.js library."""
key = compute_key(path)
name = get_name(path)
if path.suffix in {'.js', '.mjs'}:
if key in libraries and libraries[key].path == path:
return libraries[key]
assert key not in libraries, f'Duplicate js library {key}'
libraries[key] = Library(key=key, name=name, path=path, expose=expose)
return libraries[key]
raise ValueError(f'Unsupported library type "{suffix}"')
raise ValueError(f'Unsupported library type "{path.suffix}"')


def compute_key(path: Path) -> str:
"""Compute a key for a given path using a hash function.
If the path is relative to the NiceGUI base directory, the key is computed from the relative path.
"""
nicegui_base = Path(__file__).parent
try:
path = path.relative_to(nicegui_base)
except ValueError:
pass
return f'{hashlib.sha256(str(path.parent).encode()).hexdigest()}/{path.name}'


def deconstruct_location(location: Path, base_path: Path) -> Tuple[Path, str, str, str]:
"""Deconstruct a location into its parts: full path, relative path, name, suffix."""
abs_path = location if location.is_absolute() else base_path / location
rel_path = location if not location.is_absolute() else location.relative_to(base_path)
return abs_path, str(rel_path), location.name.split('.', 1)[0], location.suffix.lower()
def get_name(path: Path) -> str:
return path.name.split('.', 1)[0]


def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
Expand Down Expand Up @@ -129,13 +138,14 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
for library in element.libraries:
if library.key not in done_libraries:
if not library.expose:
js_imports.append(f'import "{prefix}/_nicegui/{__version__}/libraries/{library.key}";')
url = f'{prefix}/_nicegui/{__version__}/libraries/{library.key}'
js_imports.append(f'import "{url}";')
done_libraries.add(library.key)
for component in element.components:
if element.component:
component = element.component
if component.key not in done_components and component.path.suffix.lower() == '.js':
js_imports.extend([
f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{component.key}";',
f'app.component("{component.tag}", {component.name});',
])
url = f'{prefix}/_nicegui/{__version__}/components/{component.key}'
js_imports.append(f'import {{ default as {component.name} }} from "{url}";')
js_imports.append(f'app.component("{component.tag}", {component.name});')
done_components.add(component.key)
return vue_html, vue_styles, vue_scripts, imports, js_imports
47 changes: 39 additions & 8 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


class Element(Visibility):
components: List[JsComponent] = []
component: Optional[JsComponent] = None
libraries: List[Library] = []
extra_libraries: List[Library] = []
exposed_libraries: List[Library] = []
Expand All @@ -42,7 +42,7 @@ def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = Non
self.client = _client or globals.get_client()
self.id = self.client.next_element_id
self.client.next_element_id += 1
self.tag = tag if tag else self.components[0].tag if self.components else 'div'
self.tag = tag if tag else self.component.tag if self.component else 'div'
self._classes: List[str] = []
self._style: Dict[str, str] = {}
self._props: Dict[str, Any] = {'key': self.id} # HACK: workaround for #600 and #898
Expand Down Expand Up @@ -72,10 +72,32 @@ def __init_subclass__(cls, *,
) -> None:
super().__init_subclass__()
base = Path(inspect.getfile(cls)).parent
cls.components = [register_vue_component(Path(component), base)] if component else []
cls.libraries = [register_library(Path(library), base) for library in libraries]
cls.extra_libraries = [register_library(Path(library), base) for library in extra_libraries]
cls.exposed_libraries = [register_library(Path(library), base, expose=True) for library in exposed_libraries]

def glob_absolute_paths(file: Union[str, Path]) -> List[Path]:
path = Path(file)
if not path.is_absolute():
path = base / path
return sorted(path.parent.glob(path.name), key=lambda p: p.stem)

cls.component = None
if component:
for path in glob_absolute_paths(component):
cls.component = register_vue_component(path)

cls.libraries = []
for library in libraries:
for path in glob_absolute_paths(library):
cls.libraries.append(register_library(path))

cls.extra_libraries = []
for library in extra_libraries:
for path in glob_absolute_paths(library):
cls.extra_libraries.append(register_library(path))

cls.exposed_libraries = []
for library in exposed_libraries:
for path in glob_absolute_paths(library):
cls.exposed_libraries.append(register_library(path, expose=True))

def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
"""Add a slot to the element.
Expand Down Expand Up @@ -115,8 +137,17 @@ def _to_dict(self) -> Dict[str, Any]:
'text': self._text,
'slots': self._collect_slot_dict(),
'events': [listener.to_dict() for listener in self._event_listeners.values()],
'components': [{'key': c.key, 'name': c.name, 'tag': c.tag} for c in self.components],
'libraries': [{'key': l.key, 'name': l.name} for l in self.libraries],
'component': {
'key': self.component.key,
'name': self.component.name,
'tag': self.component.tag
} if self.component else None,
'libraries': [
{
'key': library.key,
'name': library.name,
} for library in self.libraries
],
}

@staticmethod
Expand Down
10 changes: 4 additions & 6 deletions nicegui/elements/chart.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from pathlib import Path
from typing import Dict, List

from ..element import Element

base = Path(__file__).parent
libraries = [p.relative_to(base) for p in sorted((base / 'lib' / 'highcharts').glob('*.js'), key=lambda p: p.stem)]
modules = {p.stem: p.relative_to(base) for p in sorted((base / 'lib' / 'highcharts' / 'modules').glob('*.js'))}


class Chart(Element, component='chart.js', libraries=libraries, extra_libraries=list(modules.values())):
class Chart(Element,
component='chart.js',
libraries=['lib/highcharts/*.js'],
extra_libraries=['lib/highcharts/modules/*.js']):

def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = []) -> None:
"""Chart
Expand Down
6 changes: 4 additions & 2 deletions nicegui/elements/knob.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Callable, Optional

from .label import Label
from .mixins.color_elements import TextColorElement
Expand All @@ -19,6 +19,7 @@ def __init__(self,
track_color: Optional[str] = None,
size: Optional[str] = None,
show_value: bool = False,
on_change: Optional[Callable[..., Any]] = None,
) -> None:
"""Knob
Expand All @@ -34,8 +35,9 @@ def __init__(self,
:param track_color: color name for the track of the component, examples: primary, teal-10
:param size: size in CSS units, including unit name or standard size name (xs|sm|md|lg|xl), examples: 16px, 2rem
:param show_value: whether to show the value as text
:param on_change: callback to execute when the value changes
"""
super().__init__(tag='q-knob', value=value, on_value_change=None, throttle=0.05, text_color=color)
super().__init__(tag='q-knob', value=value, on_value_change=on_change, throttle=0.05, text_color=color)

self._props['min'] = min
self._props['max'] = max
Expand Down
6 changes: 0 additions & 6 deletions nicegui/elements/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,3 @@ export default {
},
},
};

function decodeHtml(html) {
const txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
17 changes: 16 additions & 1 deletion nicegui/elements/mermaid.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import mermaid from "mermaid";

let is_running = false;
const queue = [];

export default {
template: `<div></div>`,
data: () => ({
last_content: "",
}),
mounted() {
this.update(this.content);
},
methods: {
async update(content) {
if (this.last_content === content) return;
this.last_content = content;
this.$el.innerHTML = content;
this.$el.removeAttribute("data-processed");
await mermaid.run({ nodes: [this.$el] });
queue.push(this.$el);
if (is_running) return;
is_running = true;
while (queue.length) {
await mermaid.run({ nodes: [queue.shift()] });
}
is_running = false;
},
},
props: {
Expand Down
6 changes: 1 addition & 5 deletions nicegui/elements/mermaid.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
from pathlib import Path

from .mixins.content_element import ContentElement

base = Path(__file__).parent


class Mermaid(ContentElement,
component='mermaid.js',
exposed_libraries=['lib/mermaid/mermaid.esm.min.mjs'],
extra_libraries=[p.relative_to(base) for p in (base / 'lib' / 'mermaid').glob('*.js')]):
extra_libraries=['lib/mermaid/*.js']):
CONTENT_PROP = 'content'

def __init__(self, content: str) -> None:
Expand Down
2 changes: 2 additions & 0 deletions nicegui/elements/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def query(selector: str) -> Query:
To manipulate elements like the document body, you can use the `ui.query` function.
With the query result you can add classes, styles, and attributes like with every other UI element.
This can be useful for example to change the background color of the page (e.g. `ui.query('body').classes('bg-green')`).
:param selector: the CSS selector (e.g. "body", "#my-id", ".my-class", "div > p")
"""
for element in get_client().elements.values():
if isinstance(element, Query) and element._props['selector'] == selector:
Expand Down
Loading

0 comments on commit d73e934

Please sign in to comment.