Skip to content

Commit

Permalink
Merge commit '6c5fceb9fba0c6e386e2a0ae2d2b1e943e449b8e' into integrat…
Browse files Browse the repository at this point in the history
…ion-test-framework
  • Loading branch information
rodja committed Jul 27, 2024
2 parents ce8a4f8 + 6c5fceb commit d12b7cb
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 9 deletions.
6 changes: 3 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors:
given-names: Rodja
orcid: https://orcid.org/0009-0009-4735-6227
title: 'NiceGUI: Web-based user interfaces with Python. The nice way.'
version: v1.4.28
date-released: '2024-06-28'
version: v1.4.29
date-released: '2024-07-09'
url: https://github.com/zauberzeug/nicegui
doi: 10.5281/zenodo.12582703
doi: 10.5281/zenodo.12697345
12 changes: 12 additions & 0 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,18 @@ def run_method(self, name: str, *args: Any, timeout: float = 1, check_interval:
return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})',
timeout=timeout, check_interval=check_interval)

def get_computed_prop(self, prop_name: str, *, timeout: float = 1) -> AwaitableResponse:
"""Return a computed property.
This function should be awaited so that the computed property is properly returned.
:param prop_name: name of the computed prop
:param timeout: maximum time to wait for a response (default: 1 second)
"""
if not core.loop:
return NullResponse()
return self.client.run_javascript(f'return getComputedProp({self.id}, "{prop_name}")', timeout=timeout)

def ancestors(self, *, include_self: bool = False) -> Iterator[Element]:
"""Iterate over the ancestors of the element.
Expand Down
12 changes: 12 additions & 0 deletions nicegui/elements/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@ def update_rows(self, rows: List[Dict], *, clear_selection: bool = True) -> None
self.selected.clear()
self.update()

async def get_filtered_sorted_rows(self, *, timeout: float = 1) -> List[Dict]:
"""Asynchronously return the filtered and sorted rows of the table."""
return await self.get_computed_prop('filteredSortedRows', timeout=timeout)

async def get_computed_rows(self, *, timeout: float = 1) -> List[Dict]:
"""Asynchronously return the computed rows of the table."""
return await self.get_computed_prop('computedRows', timeout=timeout)

async def get_computed_rows_number(self, *, timeout: float = 1) -> int:
"""Asynchronously return the number of computed rows of the table."""
return await self.get_computed_prop('computedRowsNumber', timeout=timeout)

class row(Element):

def __init__(self) -> None:
Expand Down
7 changes: 5 additions & 2 deletions nicegui/json/builtin_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
has_numpy = False


def dumps(obj: Any, sort_keys: bool = False, separators: Optional[Tuple[str, str]] = None):
def dumps(obj: Any,
sort_keys: bool = False,
separators: Optional[Tuple[str, str]] = None, *,
indent: bool = False) -> str:
"""Serializes a Python object to a JSON-encoded string.
This implementation uses Python's default json module, but extends it in order to support NumPy arrays.
Expand All @@ -22,7 +25,7 @@ def dumps(obj: Any, sort_keys: bool = False, separators: Optional[Tuple[str, str
obj,
sort_keys=sort_keys,
separators=separators,
indent=None,
indent=2 if indent else None,
allow_nan=False,
ensure_ascii=False,
cls=NumpyJsonEncoder)
Expand Down
9 changes: 8 additions & 1 deletion nicegui/json/orjson_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
ORJSON_OPTS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS


def dumps(obj: Any, sort_keys: bool = False, separators: Optional[Tuple[str, str]] = None):
def dumps(obj: Any,
sort_keys: bool = False,
separators: Optional[Tuple[str, str]] = None, *,
indent: bool = False) -> str:
"""Serializes a Python object to a JSON-encoded string.
By default, this function supports serializing NumPy arrays, which Python's json module does not.
Expand All @@ -33,6 +36,10 @@ def dumps(obj: Any, sort_keys: bool = False, separators: Optional[Tuple[str, str
if sort_keys:
opts |= orjson.OPT_SORT_KEYS

# flag for pretty-printing with indentation
if indent:
opts |= orjson.OPT_INDENT_2

return orjson.dumps(obj, option=opts, default=_orjson_converter).decode('utf-8')


Expand Down
13 changes: 13 additions & 0 deletions nicegui/static/nicegui.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ function runMethod(target, method_name, args) {
}
}

function getComputedProp(target, prop_name) {
if (typeof target === "object" && prop_name in target) {
return target[prop_name];
}
const element = getElement(target);
if (element === null || element === undefined) return;
if (prop_name in element) {
return element[prop_name];
} else if (prop_name in (element.$refs.qRef || [])) {
return element.$refs.qRef[prop_name];
}
}

function emitEvent(event_name, ...args) {
getElement(0).$emit(event_name, ...args);
}
Expand Down
5 changes: 3 additions & 2 deletions nicegui/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ def __len__(self) -> int:

class PersistentDict(observables.ObservableDict):

def __init__(self, filepath: Path, encoding: Optional[str] = None) -> None:
def __init__(self, filepath: Path, encoding: Optional[str] = None, *, indent: bool = False) -> None:
self.filepath = filepath
self.encoding = encoding
self.indent = indent
try:
data = json.loads(filepath.read_text(encoding)) if filepath.exists() else {}
except Exception:
Expand All @@ -68,7 +69,7 @@ def backup(self) -> None:

async def backup() -> None:
async with aiofiles.open(self.filepath, 'w', encoding=self.encoding) as f:
await f.write(json.dumps(self))
await f.write(json.dumps(self, indent=self.indent))
if core.loop:
background_tasks.create_lazy(backup(), name=self.filepath.stem)
else:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "nicegui"
version = "1.4.29-dev"
version = "1.4.30-dev"
description = "Create web-based user interfaces with Python. The nice way."
authors = ["Zauberzeug GmbH <info@zauberzeug.com>"]
license = "MIT"
Expand Down
26 changes: 26 additions & 0 deletions tests/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,29 @@ def test_problematic_datatypes(screen: Screen):
screen.should_contain('5 days')
screen.should_contain('(1+2j)')
screen.should_contain('2021-01')


def test_table_computed_props(screen: Screen):
all_rows = rows()
filtered_rows = [row for row in all_rows if 'e' in row['name']]
filtered_sorted_rows = sorted(filtered_rows, key=lambda row: row['age'], reverse=True)

@ui.page('/')
async def page():
table = ui.table(
columns=columns(),
rows=all_rows,
row_key='id',
selection='multiple',
pagination={'rowsPerPage': 1, 'sortBy': 'age', 'descending': True})
table.filter = 'e'

await ui.context.client.connected()
assert filtered_sorted_rows == await table.get_filtered_sorted_rows()
assert filtered_sorted_rows[:1] == await table.get_computed_rows()
assert len(filtered_sorted_rows) == await table.get_computed_rows_number()

screen.open('/')
screen.should_contain('Lionel')
screen.should_not_contain('Alice')
screen.should_not_contain('Bob')
7 changes: 7 additions & 0 deletions website/documentation/content/storage_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,10 @@ def short_term_memory():
ui.button('Update content',
on_click=lambda: cache.update(count=cache['count'] + 1))
ui.button('Reload page', on_click=ui.navigate.reload)


doc.text('Indentation', '''
By default, the general and user storage data is stored in JSON format without indentation.
You can change this to an indentation of 2 spaces by setting
`app.storage.general.indent = True` or `app.storage.user.indent = True`.
''')
31 changes: 31 additions & 0 deletions website/documentation/content/table_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,37 @@ def handle_pagination_changes() -> None:
)


@doc.demo('Computed props', '''
You can access the computed props of a table within async callback functions.
''')
def computed_props():
async def show_filtered_sorted_rows():
ui.notify(await table.get_filtered_sorted_rows())

async def show_computed_rows():
ui.notify(await table.get_computed_rows())

table = ui.table(
columns=[
{'name': 'name', 'label': 'Name', 'field': 'name', 'align': 'left', 'sortable': True},
{'name': 'age', 'label': 'Age', 'field': 'age', 'align': 'left', 'sortable': True}
],
rows=[
{'name': 'Noah', 'age': 33},
{'name': 'Emma', 'age': 21},
{'name': 'Rose', 'age': 88},
{'name': 'James', 'age': 59},
{'name': 'Olivia', 'age': 62},
{'name': 'Liam', 'age': 18},
],
row_key='name',
pagination=3,
)
ui.input('Search by name/age').bind_value(table, 'filter')
ui.button('Show filtered/sorted rows', on_click=show_filtered_sorted_rows)
ui.button('Show computed rows', on_click=show_computed_rows)


@doc.demo('Computed fields', '''
You can use functions to compute the value of a column.
The function receives the row as an argument.
Expand Down

0 comments on commit d12b7cb

Please sign in to comment.