Skip to content

Commit 2f29be2

Browse files
author
Andrei Neagu
committed
baseline test
1 parent 27ffd9c commit 2f29be2

File tree

3 files changed

+75
-47
lines changed

3 files changed

+75
-47
lines changed

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_common/base_display_model.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable
2-
from typing import Any, TypeAlias
2+
from typing import Any, Self, TypeAlias
33

44
from pydantic import BaseModel, PrivateAttr, TypeAdapter
55

@@ -43,23 +43,15 @@ def _get_on_change_callbacks_to_run(
4343

4444
return callbaks_to_run
4545

46-
def update(self, updates: CompleteModelDict) -> None:
47-
"""
48-
updates a the model properties by by reading the keys form a dcitionary
49-
It can also update nested models if the property is also a BaseUpdatableDisplayModel
50-
"""
51-
52-
callbacks = self._get_on_change_callbacks_to_run(updates)
46+
def update(self, update_obj: Self) -> None:
47+
callbacks_to_run = self._get_on_change_callbacks_to_run(update_obj.__dict__)
5348

5449
current = self.__dict__
55-
for key, value in updates.items():
56-
if key in current and current[key] != value:
57-
if isinstance(key, BaseUpdatableDisplayModel):
58-
key.update(value)
59-
else:
60-
setattr(self, key, value)
61-
62-
for callback in callbacks:
50+
for attribute_name, value in update_obj.__dict__.items():
51+
if attribute_name in current and current[attribute_name] != value:
52+
setattr(self, attribute_name, value)
53+
54+
for callback in callbacks_to_run:
6355
callback()
6456

6557
def _raise_if_attribute_not_declared_in_model(self, attribute: str) -> None:
@@ -68,11 +60,13 @@ def _raise_if_attribute_not_declared_in_model(self, attribute: str) -> None:
6860
raise RuntimeError(msg)
6961

7062
def on_type_change(self, attribute: str, callback: Callable) -> None:
63+
"""subscribe callback to an attribute TYPE change"""
7164
self._raise_if_attribute_not_declared_in_model(attribute)
7265

7366
self._on_type_change_subscribers[attribute] = callback
7467

7568
def on_value_change(self, attribute: str, callback: Callable) -> None:
69+
"""subscribe callback to an attribute VALUE change"""
7670
self._raise_if_attribute_not_declared_in_model(attribute)
7771

7872
self._on_value_change_subscribers[attribute] = callback
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Generic, TypeVar
3+
4+
from .base_display_model import BaseUpdatableDisplayModel
5+
6+
T = TypeVar("T", bound=BaseUpdatableDisplayModel)
7+
8+
9+
class BaseUpdatableComponent(ABC, Generic[T]):
10+
def __init__(self, display_model: T):
11+
self.display_model = display_model
12+
13+
@abstractmethod
14+
def add_to_ui(self) -> None:
15+
"""creates ui elements inside the parent container"""

services/dynamic-scheduler/tests/unit/api_frontend/_common/test_updatable_component.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# pylint:disable=redefined-outer-name
22
# pylint:disable=unused-argument
33

4+
from copy import deepcopy
45
from functools import cached_property
56

67
import nicegui
78
import pytest
89
from fastapi import FastAPI
910
from helpers import assert_contains_text
1011
from nicegui import APIRouter, ui
11-
from nicegui.element import Element
1212
from playwright.async_api import Page
1313
from pytest_mock import MockerFixture
1414
from simcore_service_dynamic_scheduler.api.frontend._common.base_display_model import (
@@ -54,37 +54,51 @@ def rerender_on_type_change(self) -> set[str]:
5454
companion: Pet | Friend
5555

5656

57-
class PersonDisplay(BaseUpdatableComponent[Person]):
58-
def _get_parent(self) -> Element:
59-
return ui.column()
60-
61-
def _draw(self) -> None:
62-
print("calling _draw")
57+
class FriendComponent(BaseUpdatableComponent[Friend]):
58+
def add_to_ui(self) -> None:
59+
ui.label().bind_text_from(
60+
self.display_model,
61+
"name",
62+
backward=lambda name: f"Friend Name: {name}",
63+
)
64+
ui.label().bind_text_from(
65+
self.display_model,
66+
"age",
67+
backward=lambda age: f"Friend Age: {age}",
68+
)
69+
70+
71+
class PetComponent(BaseUpdatableComponent[Pet]):
72+
def add_to_ui(self) -> None:
73+
ui.label().bind_text_from(
74+
self.display_model,
75+
"name",
76+
backward=lambda name: f"Pet Name: {name}",
77+
)
78+
ui.label().bind_text_from(
79+
self.display_model,
80+
"species",
81+
backward=lambda species: f"Pet Species: {species}",
82+
)
83+
84+
85+
class PersonComponent(BaseUpdatableComponent[Person]):
86+
def add_to_ui(self) -> None:
6387
ui.label(f"Name: {self.display_model.name}")
6488
ui.label(f"Age: {self.display_model.age}")
6589

6690
@ui.refreshable
67-
def comp_ui() -> None:
91+
def _ui_friend_or_pret() -> None:
6892
if isinstance(self.display_model.companion, Friend):
69-
ui.label().bind_text_from(
70-
self.display_model.companion,
71-
"name",
72-
backward=lambda name: f"Friend Name: {name}",
73-
)
74-
ui.label(f"Friend Age: {self.display_model.companion.age}")
75-
76-
if isinstance(self.display_model.companion, Pet):
77-
ui.label().bind_text_from(
78-
self.display_model.companion,
79-
"name",
80-
backward=lambda name: f"Pet Name: {name}",
81-
)
82-
ui.label(f"Pet Species: {self.display_model.companion.species}")
83-
84-
comp_ui()
93+
FriendComponent(self.display_model.companion).add_to_ui()
94+
95+
elif isinstance(self.display_model.companion, Pet):
96+
PetComponent(self.display_model.companion).add_to_ui()
97+
98+
_ui_friend_or_pret()
8599

86100
# self.display_model.on_value_change("companion", comp_ui.refresh)
87-
self.display_model.on_type_change("companion", comp_ui.refresh)
101+
self.display_model.on_type_change("companion", _ui_friend_or_pret.refresh)
88102

89103

90104
@pytest.fixture
@@ -104,10 +118,8 @@ def router(person: Person) -> APIRouter:
104118

105119
@ui.page("/", api_router=router)
106120
async def index():
107-
person_display = PersonDisplay(person)
108-
109121
ui.label("BEFORE_LABEL")
110-
person_display.add_to_ui()
122+
PersonComponent(person).add_to_ui()
111123
ui.label("AFTER_LABEL")
112124

113125
return router
@@ -157,7 +169,6 @@ async def test_updatable_component(
157169

158170
await assert_contains_text(async_page, "Pet Name: Fluffy")
159171
person.companion.name = "Buddy"
160-
# : TODO: bidn property
161172
await assert_contains_text(async_page, "Pet Name: Buddy", timeout=2)
162173

163174
# # TODO: check that UI was rerendered with new pet name
@@ -168,8 +179,16 @@ async def test_updatable_component(
168179
# person.companion = Pet(name="Buddy", species="dog")
169180
# # TODO: check that ui has no changes only values changed
170181

171-
person.companion = Friend(name="Charlie", age=25)
172-
await assert_contains_text(async_page, "Friend Name: Charlie2", timeout=2)
182+
# on_value_cahnge rerender
183+
# simulate an update from a new incoming object in memory
184+
185+
person_update = deepcopy(person)
186+
person_update.companion = Friend(name="Charlie", age=25)
187+
188+
person.update(person_update)
189+
await assert_contains_text(async_page, "Friend Name: Charlie", timeout=2)
190+
191+
# TODO: on type change rerender
173192

174193
# # TODO: test that it changes if we apply the updates via the update method on the object?
175194

0 commit comments

Comments
 (0)