From 6c33dbe93db6ace6a43813a5595444bcce126beb Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 10:18:15 +0200 Subject: [PATCH 1/9] move related tests into separate file --- tests/test_element.py | 34 ---------------------- tests/test_element_delete.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 tests/test_element_delete.py diff --git a/tests/test_element.py b/tests/test_element.py index 79ffa1218..69bc2bafb 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -105,40 +105,6 @@ def assert_props(*props: str) -> None: assert_props('standard') -def test_remove_and_clear(screen: Screen): - with ui.row() as row: - ui.label('Label A') - b = ui.label('Label B') - ui.label('Label C') - - ui.button('Remove B', on_click=lambda: row.remove(b)) - ui.button('Remove 0', on_click=lambda: row.remove(0)) - ui.button('Clear', on_click=row.clear) - - screen.open('/') - screen.should_contain('Label A') - screen.should_contain('Label B') - screen.should_contain('Label C') - - screen.click('Remove B') - screen.wait(0.5) - screen.should_contain('Label A') - screen.should_not_contain('Label B') - screen.should_contain('Label C') - - screen.click('Remove 0') - screen.wait(0.5) - screen.should_not_contain('Label A') - screen.should_not_contain('Label B') - screen.should_contain('Label C') - - screen.click('Clear') - screen.wait(0.5) - screen.should_not_contain('Label A') - screen.should_not_contain('Label B') - screen.should_not_contain('Label C') - - def test_move(screen: Screen): with ui.card() as a: ui.label('A') diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py new file mode 100644 index 000000000..6f4c9d991 --- /dev/null +++ b/tests/test_element_delete.py @@ -0,0 +1,56 @@ +from nicegui import ui + +from .screen import Screen + + +def test_remove_element_by_reference(screen: Screen): + with ui.row() as row: + ui.label('Label A') + b = ui.label('Label B') + ui.label('Label C') + + ui.button('Remove', on_click=lambda: row.remove(b)) + + screen.open('/') + screen.click('Remove') + screen.wait(0.5) + screen.should_contain('Label A') + screen.should_not_contain('Label B') + screen.should_contain('Label C') + assert b.is_deleted + + +def test_remove_element_by_index(screen: Screen): + with ui.row() as row: + ui.label('Label A') + b = ui.label('Label B') + ui.label('Label C') + + ui.button('Remove', on_click=lambda: row.remove(1)) + + screen.open('/') + screen.click('Remove') + screen.wait(0.5) + screen.should_contain('Label A') + screen.should_not_contain('Label B') + screen.should_contain('Label C') + assert b.is_deleted + + +def test_clear(screen: Screen): + with ui.row() as row: + a = ui.label('Label A') + b = ui.label('Label B') + c = ui.label('Label C') + + ui.button('Clear', on_click=row.clear) + + screen.open('/') + screen.click('Clear') + screen.wait(0.5) + screen.should_not_contain('Label A') + screen.should_not_contain('Label B') + screen.should_not_contain('Label C') + assert a.is_deleted + assert b.is_deleted + assert c.is_deleted From ad9916e743d470988377a2bdb908cfb0edcee927 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 10:23:25 +0200 Subject: [PATCH 2/9] test removal of parent element (currently failing) --- tests/test_element_delete.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index 6f4c9d991..bfbce1b9f 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -1,3 +1,5 @@ +import pytest + from nicegui import ui from .screen import Screen @@ -54,3 +56,25 @@ def test_clear(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + + +@pytest.mark.skip(reason='needs fix in element.py') # TODO +def test_remove_parent(screen: Screen): + with ui.element() as container: + with ui.row() as row: + a = ui.label('Label A') + b = ui.label('Label B') + c = ui.label('Label C') + + ui.button('Remove parent', on_click=lambda: container.remove(row)) + + screen.open('/') + screen.click('Remove parent') + screen.wait(0.5) + screen.should_not_contain('Label A') + screen.should_not_contain('Label B') + screen.should_not_contain('Label C') + assert row.is_deleted + assert a.is_deleted + assert b.is_deleted + assert c.is_deleted From 0d2e283a3f246f58fd445982641c699d15028907 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 10:28:15 +0200 Subject: [PATCH 3/9] test element.delete (currently failing) --- tests/test_element_delete.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index bfbce1b9f..eb6ef7294 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -20,6 +20,7 @@ def test_remove_element_by_reference(screen: Screen): screen.should_not_contain('Label B') screen.should_contain('Label C') assert b.is_deleted + assert len(row.default_slot.children) == 2 def test_remove_element_by_index(screen: Screen): @@ -37,6 +38,7 @@ def test_remove_element_by_index(screen: Screen): screen.should_not_contain('Label B') screen.should_contain('Label C') assert b.is_deleted + assert len(row.default_slot.children) == 2 def test_clear(screen: Screen): @@ -56,6 +58,7 @@ def test_clear(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + assert len(row.default_slot.children) == 0 @pytest.mark.skip(reason='needs fix in element.py') # TODO @@ -78,3 +81,22 @@ def test_remove_parent(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + + +@pytest.mark.skip(reason='needs fix in element.py') # TODO +def test_delete_element(screen: Screen): + with ui.row() as row: + ui.label('Label A') + b = ui.label('Label B') + ui.label('Label C') + + ui.button('Delete', on_click=b.delete) + + screen.open('/') + screen.click('Delete') + screen.wait(0.5) + screen.should_contain('Label A') + screen.should_not_contain('Label B') + screen.should_contain('Label C') + assert b.is_deleted + assert len(row.default_slot.children) == 2 From 9e4ef74108b954d24fc06cbdadd8c75ede7782e1 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 10:44:09 +0200 Subject: [PATCH 4/9] include binding in element delete tests --- nicegui/binding.py | 10 ++++++++ tests/conftest.py | 3 ++- tests/test_element_delete.py | 44 +++++++++++++++++++++++------------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/nicegui/binding.py b/nicegui/binding.py index dc431ee77..69aa36f29 100644 --- a/nicegui/binding.py +++ b/nicegui/binding.py @@ -126,3 +126,13 @@ def remove(objects: List[Any], type_: Type) -> None: for (obj_id, name), obj in list(bindable_properties.items()): if isinstance(obj, type_) and obj in objects: del bindable_properties[(obj_id, name)] + + +def reset() -> None: + """Clear all bindings. + + This function is intended for testing purposes only. + """ + bindings.clear() + bindable_properties.clear() + active_links.clear() diff --git a/tests/conftest.py b/tests/conftest.py index 9dd8f50f1..1765edcec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service -from nicegui import Client, globals # pylint: disable=redefined-builtin +from nicegui import Client, binding, globals # pylint: disable=redefined-builtin from nicegui.elements import plotly, pyplot from nicegui.page import page @@ -57,6 +57,7 @@ def reset_globals() -> Generator[None, None, None]: globals.app.storage.clear() globals.index_client = Client(page('/'), shared=True).__enter__() globals.app.get('/')(globals.index_client.build_response) + binding.reset() @pytest.fixture(scope='session', autouse=True) diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index eb6ef7294..02a72e686 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -1,15 +1,16 @@ import pytest -from nicegui import ui +from nicegui import binding, ui from .screen import Screen def test_remove_element_by_reference(screen: Screen): + texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.row() as row: - ui.label('Label A') - b = ui.label('Label B') - ui.label('Label C') + ui.label().bind_text_from(texts, 'a') + b = ui.label().bind_text_from(texts, 'b') + ui.label().bind_text_from(texts, 'c') ui.button('Remove', on_click=lambda: row.remove(b)) @@ -21,13 +22,15 @@ def test_remove_element_by_reference(screen: Screen): screen.should_contain('Label C') assert b.is_deleted assert len(row.default_slot.children) == 2 + assert len(binding.active_links) == 2 def test_remove_element_by_index(screen: Screen): + texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.row() as row: - ui.label('Label A') - b = ui.label('Label B') - ui.label('Label C') + ui.label().bind_text_from(texts, 'a') + b = ui.label().bind_text_from(texts, 'b') + ui.label().bind_text_from(texts, 'c') ui.button('Remove', on_click=lambda: row.remove(1)) @@ -39,13 +42,15 @@ def test_remove_element_by_index(screen: Screen): screen.should_contain('Label C') assert b.is_deleted assert len(row.default_slot.children) == 2 + assert len(binding.active_links) == 2 def test_clear(screen: Screen): + texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.row() as row: - a = ui.label('Label A') - b = ui.label('Label B') - c = ui.label('Label C') + a = ui.label().bind_text_from(texts, 'a') + b = ui.label().bind_text_from(texts, 'b') + c = ui.label().bind_text_from(texts, 'c') ui.button('Clear', on_click=row.clear) @@ -59,15 +64,17 @@ def test_clear(screen: Screen): assert b.is_deleted assert c.is_deleted assert len(row.default_slot.children) == 0 + assert len(binding.active_links) == 0 @pytest.mark.skip(reason='needs fix in element.py') # TODO def test_remove_parent(screen: Screen): + texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.element() as container: with ui.row() as row: - a = ui.label('Label A') - b = ui.label('Label B') - c = ui.label('Label C') + a = ui.label().bind_text_from(texts, 'a') + b = ui.label().bind_text_from(texts, 'b') + c = ui.label().bind_text_from(texts, 'c') ui.button('Remove parent', on_click=lambda: container.remove(row)) @@ -81,14 +88,18 @@ def test_remove_parent(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + assert len(container.default_slot.children) == 0 + assert len(row.default_slot.children) == 0 + assert len(binding.active_links) == 0 @pytest.mark.skip(reason='needs fix in element.py') # TODO def test_delete_element(screen: Screen): + texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.row() as row: - ui.label('Label A') - b = ui.label('Label B') - ui.label('Label C') + ui.label().bind_text_from(texts, 'a') + b = ui.label().bind_text_from(texts, 'b') + ui.label().bind_text_from(texts, 'c') ui.button('Delete', on_click=b.delete) @@ -100,3 +111,4 @@ def test_delete_element(screen: Screen): screen.should_contain('Label C') assert b.is_deleted assert len(row.default_slot.children) == 2 + assert len(binding.active_links) == 2 From ef0f62853a8e76e83970100a88810ca1724c2220 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 10:57:10 +0200 Subject: [PATCH 5/9] also check if element is removed from client --- tests/test_element_delete.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index 02a72e686..d0a38f2be 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -21,6 +21,7 @@ def test_remove_element_by_reference(screen: Screen): screen.should_not_contain('Label B') screen.should_contain('Label C') assert b.is_deleted + assert b.id not in row.client.elements assert len(row.default_slot.children) == 2 assert len(binding.active_links) == 2 @@ -41,6 +42,7 @@ def test_remove_element_by_index(screen: Screen): screen.should_not_contain('Label B') screen.should_contain('Label C') assert b.is_deleted + assert b.id not in row.client.elements assert len(row.default_slot.children) == 2 assert len(binding.active_links) == 2 @@ -63,6 +65,7 @@ def test_clear(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + assert b.id not in row.client.elements assert len(row.default_slot.children) == 0 assert len(binding.active_links) == 0 @@ -88,6 +91,9 @@ def test_remove_parent(screen: Screen): assert a.is_deleted assert b.is_deleted assert c.is_deleted + assert a.id not in container.client.elements + assert b.id not in container.client.elements + assert c.id not in container.client.elements assert len(container.default_slot.children) == 0 assert len(row.default_slot.children) == 0 assert len(binding.active_links) == 0 @@ -110,5 +116,6 @@ def test_delete_element(screen: Screen): screen.should_not_contain('Label B') screen.should_contain('Label C') assert b.is_deleted + assert b.id not in row.client.elements assert len(row.default_slot.children) == 2 assert len(binding.active_links) == 2 From e0b191eb7e76b7d186599457bd39b1e7fd0abaa0 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 11:49:50 +0200 Subject: [PATCH 6/9] introduce _on_delete(), refactoring, re-activate tests --- nicegui/binding.py | 4 +-- nicegui/element.py | 49 ++++++++++++++++++++++-------------- nicegui/elements/scene.py | 4 +-- nicegui/elements/upload.py | 4 +-- nicegui/nicegui.py | 5 ++-- tests/test_element_delete.py | 5 ---- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/nicegui/binding.py b/nicegui/binding.py index 69aa36f29..580386e30 100644 --- a/nicegui/binding.py +++ b/nicegui/binding.py @@ -2,7 +2,7 @@ import time from collections import defaultdict from collections.abc import Mapping -from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Type, Union +from typing import Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Set, Tuple, Type, Union from . import globals # pylint: disable=redefined-builtin @@ -107,7 +107,7 @@ def __set__(self, owner: Any, value: Any) -> None: self.on_change(owner, value) -def remove(objects: List[Any], type_: Type) -> None: +def remove(objects: Iterable[Any], type_: Type) -> None: active_links[:] = [ (source_obj, source_name, target_obj, target_name, transform) for source_obj, source_name, target_obj, target_name, transform in active_links diff --git a/nicegui/element.py b/nicegui/element.py index 8eb244847..b002425c1 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -4,7 +4,7 @@ import re from copy import copy, deepcopy from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Union from typing_extensions import Self @@ -304,19 +304,24 @@ def run_method(self, name: str, *args: Any) -> None: target_id = globals._socket_id or self.client.id # pylint: disable=protected-access outbox.enqueue_message('run_method', data, target_id) - def _collect_descendant_ids(self) -> List[int]: - ids: List[int] = [self.id] + def _collect_descendants(self, *, include_self: bool = False) -> List[Element]: + elements: List[Element] = [self] if include_self else [] for child in self: - ids.extend(child._collect_descendant_ids()) # pylint: disable=protected-access - return ids + elements.extend(child._collect_descendants(include_self=True)) # pylint: disable=protected-access + return elements + + @staticmethod + def _delete_elements(elements: Iterable[Element]) -> None: + binding.remove(elements, Element) + for element in elements: + element._deleted = True # pylint: disable=protected-access + del element.client.elements[element.id] + outbox.enqueue_delete(element) def clear(self) -> None: """Remove all child elements.""" - descendants = [self.client.elements[id] for id in self._collect_descendant_ids()[1:]] - binding.remove(descendants, Element) - for element in descendants: - element.delete() - del self.client.elements[element.id] + descendants = self._collect_descendants() + self._delete_elements(descendants) for slot in self.slots.values(): slot.children.clear() self.update() @@ -344,19 +349,25 @@ def remove(self, element: Union[Element, int]) -> None: if isinstance(element, int): children = list(self) element = children[element] - binding.remove([element], Element) - element.delete() - del self.client.elements[element.id] - for slot in self.slots.values(): - slot.children[:] = [e for e in slot if e.id != element.id] + elements = element._collect_descendants(include_self=True) # pylint: disable=protected-access + self._delete_elements(elements) + assert element.parent_slot is not None + element.parent_slot.children.remove(element) self.update() def delete(self) -> None: - """Perform cleanup when the element is deleted.""" - self._deleted = True - outbox.enqueue_delete(self) + """Delete the element.""" + self._delete_elements([self]) + assert self.parent_slot is not None + self.parent_slot.children.remove(self) + + def _on_delete(self) -> None: + """Called when the element is deleted. + + This method can be overridden in subclasses to perform cleanup tasks. + """ @property - def is_deleted(self) -> None: + def is_deleted(self) -> bool: """Whether the element has been deleted.""" return self._deleted diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index 45eb9678b..3a1c835a0 100644 --- a/nicegui/elements/scene.py +++ b/nicegui/elements/scene.py @@ -184,9 +184,9 @@ def move_camera(self, self.camera.look_at_x, self.camera.look_at_y, self.camera.look_at_z, self.camera.up_x, self.camera.up_y, self.camera.up_z, duration) - def delete(self) -> None: + def _on_delete(self) -> None: binding.remove(list(self.objects.values()), Object3D) - super().delete() + super()._on_delete() def delete_objects(self, predicate: Callable[[Object3D], bool] = lambda _: True) -> None: for obj in list(self.objects.values()): diff --git a/nicegui/elements/upload.py b/nicegui/elements/upload.py index 8cd0fe709..e99932331 100644 --- a/nicegui/elements/upload.py +++ b/nicegui/elements/upload.py @@ -69,6 +69,6 @@ async def upload_route(request: Request) -> Dict[str, str]: def reset(self) -> None: self.run_method('reset') - def delete(self) -> None: + def _on_delete(self) -> None: app.remove_route(self._props['url']) - super().delete() + super()._on_delete() diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index bcf1fdd83..0f5d71ab9 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -229,7 +229,6 @@ async def prune_slot_stacks() -> None: def delete_client(client_id: str) -> None: - binding.remove(list(globals.clients[client_id].elements.values()), Element) - for element in globals.clients[client_id].elements.values(): - element.delete() + elements = globals.clients[client_id].elements.values() + Element._delete_elements(elements) # pylint: disable=protected-access del globals.clients[client_id] diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index d0a38f2be..7e9cb2606 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -1,5 +1,3 @@ -import pytest - from nicegui import binding, ui from .screen import Screen @@ -70,7 +68,6 @@ def test_clear(screen: Screen): assert len(binding.active_links) == 0 -@pytest.mark.skip(reason='needs fix in element.py') # TODO def test_remove_parent(screen: Screen): texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.element() as container: @@ -95,11 +92,9 @@ def test_remove_parent(screen: Screen): assert b.id not in container.client.elements assert c.id not in container.client.elements assert len(container.default_slot.children) == 0 - assert len(row.default_slot.children) == 0 assert len(binding.active_links) == 0 -@pytest.mark.skip(reason='needs fix in element.py') # TODO def test_delete_element(screen: Screen): texts = {'a': 'Label A', 'b': 'Label B', 'c': 'Label C'} with ui.row() as row: From 9956ab5c2a9a7444c72706163e5758846fd8e1fc Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 13:12:00 +0200 Subject: [PATCH 7/9] fix "dictionary changed size during iteration" --- nicegui/client.py | 18 ++++++++++++++++-- nicegui/element.py | 18 +++++------------- nicegui/nicegui.py | 5 +---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index c449642f0..7744accd9 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -4,7 +4,7 @@ import time import uuid from pathlib import Path -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Iterable, List, Optional, Union from fastapi import Request from fastapi.responses import Response @@ -12,7 +12,7 @@ from nicegui import json -from . import globals, outbox # pylint: disable=redefined-builtin +from . import binding, globals, outbox # pylint: disable=redefined-builtin from .dependencies import generate_resources from .element import Element from .favicon import get_favicon_url @@ -162,3 +162,17 @@ def on_connect(self, handler: Union[Callable[..., Any], Awaitable]) -> None: def on_disconnect(self, handler: Union[Callable[..., Any], Awaitable]) -> None: """Register a callback to be called when the client disconnects.""" self.disconnect_handlers.append(handler) + + def remove_elements(self, elements: Iterable[Element]) -> None: + """Remove the given elements from the client.""" + binding.remove(elements, Element) + element_ids = [element.id for element in elements] + for element_id in element_ids: + del self.elements[element_id] + for element in elements: + element._deleted = True # pylint: disable=protected-access + outbox.enqueue_delete(element) + + def remove_all_elements(self) -> None: + """Remove all elements from the client.""" + self.remove_elements(self.elements.values()) diff --git a/nicegui/element.py b/nicegui/element.py index b002425c1..126fbf25f 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -4,13 +4,13 @@ import re from copy import copy, deepcopy from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Union from typing_extensions import Self from nicegui import json -from . import binding, events, globals, outbox, storage # pylint: disable=redefined-builtin +from . import events, globals, outbox, storage # pylint: disable=redefined-builtin from .dependencies import Component, Library, register_library, register_vue_component from .elements.mixins.visibility import Visibility from .event_listener import EventListener @@ -310,18 +310,10 @@ def _collect_descendants(self, *, include_self: bool = False) -> List[Element]: elements.extend(child._collect_descendants(include_self=True)) # pylint: disable=protected-access return elements - @staticmethod - def _delete_elements(elements: Iterable[Element]) -> None: - binding.remove(elements, Element) - for element in elements: - element._deleted = True # pylint: disable=protected-access - del element.client.elements[element.id] - outbox.enqueue_delete(element) - def clear(self) -> None: """Remove all child elements.""" descendants = self._collect_descendants() - self._delete_elements(descendants) + self.client.remove_elements(descendants) for slot in self.slots.values(): slot.children.clear() self.update() @@ -350,14 +342,14 @@ def remove(self, element: Union[Element, int]) -> None: children = list(self) element = children[element] elements = element._collect_descendants(include_self=True) # pylint: disable=protected-access - self._delete_elements(elements) + self.client.remove_elements(elements) assert element.parent_slot is not None element.parent_slot.children.remove(element) self.update() def delete(self) -> None: """Delete the element.""" - self._delete_elements([self]) + self.client.remove_elements([self]) assert self.parent_slot is not None self.parent_slot.children.remove(self) diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index 0f5d71ab9..d403ea387 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -14,7 +14,6 @@ from .app import App from .client import Client from .dependencies import js_components, libraries -from .element import Element from .error import error_content from .helpers import is_file, safe_invoke from .json import NiceGUIJSONResponse @@ -229,6 +228,4 @@ async def prune_slot_stacks() -> None: def delete_client(client_id: str) -> None: - elements = globals.clients[client_id].elements.values() - Element._delete_elements(elements) # pylint: disable=protected-access - del globals.clients[client_id] + globals.clients.pop(client_id).remove_all_elements() From 17e6e133cdc6b5e435adcaf3a91b05d6291853ae Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 13:17:37 +0200 Subject: [PATCH 8/9] add element.delete() to the documentation --- website/documentation.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/website/documentation.py b/website/documentation.py index e7af99a32..84e5ad866 100644 --- a/website/documentation.py +++ b/website/documentation.py @@ -153,9 +153,16 @@ def captions_and_overlays_demo(): load_demo(ui.grid) @text_demo('Clear Containers', ''' - To remove all elements from a row, column or card container, use the `clear()` method. + To remove all elements from a row, column or card container, use can call + ```py + container.clear() + ``` - Alternatively, you can remove individual elements with `remove(element)`, where `element` is an Element or an index. + Alternatively, you can remove individual elements by calling + + - `container.remove(element: Element)`, + - `container.remove(index: int)`, or + - `element.delete()`. ''') def clear_containers_demo(): container = ui.row() From 51a8c54e7a670da7a6bdb4a84cf45531a58a1d35 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 29 Aug 2023 16:58:18 +0200 Subject: [PATCH 9/9] make sure to call _on_delete() for each deleted element --- nicegui/client.py | 1 + tests/test_element_delete.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/nicegui/client.py b/nicegui/client.py index 7744accd9..1ec168728 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -170,6 +170,7 @@ def remove_elements(self, elements: Iterable[Element]) -> None: for element_id in element_ids: del self.elements[element_id] for element in elements: + element._on_delete() # pylint: disable=protected-access element._deleted = True # pylint: disable=protected-access outbox.enqueue_delete(element) diff --git a/tests/test_element_delete.py b/tests/test_element_delete.py index 7e9cb2606..86cf209fa 100644 --- a/tests/test_element_delete.py +++ b/tests/test_element_delete.py @@ -114,3 +114,32 @@ def test_delete_element(screen: Screen): assert b.id not in row.client.elements assert len(row.default_slot.children) == 2 assert len(binding.active_links) == 2 + + +def test_on_delete(screen: Screen): + deleted_labels = [] + + class CustomLabel(ui.label): + + def __init__(self, text: str) -> None: + super().__init__(text) + + def _on_delete(self) -> None: + deleted_labels.append(self.text) + super()._on_delete() + + with ui.row() as row: + CustomLabel('Label A') + b = CustomLabel('Label B') + CustomLabel('Label C') + + ui.button('Delete C', on_click=lambda: row.remove(2)) + ui.button('Delete B', on_click=lambda: row.remove(b)) + ui.button('Clear row', on_click=row.clear) + + screen.open('/') + screen.click('Delete C') + screen.click('Delete B') + screen.click('Clear row') + screen.wait(0.5) + assert deleted_labels == ['Label C', 'Label B', 'Label A']