Skip to content

Commit

Permalink
manage lov type dynamically (#1195)
Browse files Browse the repository at this point in the history
* manage lov type dynamically
resolves #1159

* tx ruff

* fix tests

* fix tests take 2

* fix tests take 3

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
  • Loading branch information
FredLL-Avaiga and Fred Lefévère-Laoide authored Apr 22, 2024
1 parent d036c6a commit 7d48ce6
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 80 deletions.
61 changes: 32 additions & 29 deletions taipy/gui/_renderers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,6 @@ def set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict
def __set_json_attribute(self, name, value):
return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder))

def __set_list_of_(self, name: str):
lof = self.__get_list_of_(name)
if not isinstance(lof, (list, tuple)):
if lof is not None:
_warn(f"{self.__element_name}: {name} should be a list.")
return self
return self.__set_json_attribute(_to_camel_case(name), lof)

def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True):
"""
TODO-undocumented
Expand Down Expand Up @@ -369,15 +361,17 @@ def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[
def __set_react_attribute(self, name: str, value: t.Any):
return self.set_attribute(name, "{!" + (str(value).lower() if isinstance(value, bool) else str(value)) + "!}")

def _get_adapter(self, var_name: str, property_name: t.Optional[str] = None, multi_selection=True): # noqa: C901
def _get_lov_adapter(self, var_name: str, property_name: t.Optional[str] = None, multi_selection=True): # noqa: C901
property_name = var_name if property_name is None else property_name
lov_name = self.__hashes.get(var_name)
lov = self.__get_list_of_(var_name)
default_lov = []
if isinstance(lov, list):
adapter = self.__attributes.get("adapter")
if adapter and isinstance(adapter, str):
adapter = self.__gui._get_user_function(adapter)
if adapter and not callable(adapter):
_warn("'adapter' property value is invalid.")
_warn(f"{self.__element_name}: adapter property value is invalid.")
adapter = None
var_type = self.__attributes.get("type")
if isclass(var_type):
Expand All @@ -402,7 +396,7 @@ def _get_adapter(self, var_name: str, property_name: t.Optional[str] = None, mul
if adapter.__name__ == "<lambda>"
else _get_expr_var_name(adapter.__name__)
)
if lov_name := self.__hashes.get(var_name):
if lov_name:
if adapter is None:
adapter = self.__gui._get_adapter_for_type(lov_name)
else:
Expand All @@ -415,15 +409,13 @@ def _get_adapter(self, var_name: str, property_name: t.Optional[str] = None, mul
if adapter is not None:
self.__gui._add_adapter_for_type(var_type, adapter) # type: ignore

ret_list = []
if len(lov) > 0:
for elt in lov:
ret = self.__gui._run_adapter(
t.cast(t.Callable, adapter), elt, adapter.__name__ if callable(adapter) else "adapter"
) # type: ignore
if ret is not None:
ret_list.append(ret)
self.__attributes[f"default_{property_name}"] = ret_list
default_lov.append(ret)

ret_list = []
value = self.__attributes.get("value")
Expand All @@ -441,6 +433,26 @@ def _get_adapter(self, var_name: str, property_name: t.Optional[str] = None, mul
if ret_val == "-1" and self.__attributes.get("unselected_value") is not None:
ret_val = str(self.__attributes.get("unselected_value", ""))
self.__set_default_value("value", ret_val)

# LoV default value
self.__set_json_attribute(_to_camel_case(f"default_{property_name}"), default_lov)

# LoV expression binding
if lov_name:
typed_lov_hash = (
self.__gui._evaluate_expr(
"{"
+ f"{self.__gui._get_call_method_name('_get_adapted_lov')}"
+ f"({self.__gui._get_real_var_name(lov_name)[0]},'{var_type}')"
+ "}"
)
if var_type
else lov_name
)
hash_name = self.__get_typed_hash_name(typed_lov_hash, PropertyType.lov)
self.__update_vars.append(f"{property_name}={hash_name}")
self.__set_react_attribute(property_name, hash_name)

return self

def __filter_attribute_names(self, names: t.Iterable[str]):
Expand Down Expand Up @@ -495,7 +507,7 @@ def _get_dataframe_attributes(self) -> "_Builder":
if cmp_datas:
cmp_hash = self.__gui._evaluate_expr(
"{"
+ f'{self.__gui._get_rebuild_fn_name("_compare_data")}'
+ f"{self.__gui._get_call_method_name('_compare_data')}"
+ f'({self.__gui._get_real_var_name(data_hash)[0]},{",".join(cmp_datas)})'
+ "}"
)
Expand All @@ -506,7 +518,7 @@ def _get_dataframe_attributes(self) -> "_Builder":
)

rebuild_fn_hash = self.__build_rebuild_fn(
self.__gui._get_rebuild_fn_name("_tbl_cols"), _Builder.__TABLE_COLUMNS_DEPS
self.__gui._get_call_method_name("_tbl_cols"), _Builder.__TABLE_COLUMNS_DEPS
)
if rebuild_fn_hash:
self.__set_react_attribute("columns", rebuild_fn_hash)
Expand Down Expand Up @@ -551,7 +563,8 @@ def _get_chart_config(self, default_type: str, default_mode: str):
self.__attributes["_default_type"] = default_type
self.__attributes["_default_mode"] = default_mode
rebuild_fn_hash = self.__build_rebuild_fn(
self.__gui._get_rebuild_fn_name("_chart_conf"), _CHART_NAMES + ("_default_type", "_default_mode", "data")
self.__gui._get_call_method_name("_chart_conf"),
_CHART_NAMES + ("_default_type", "_default_mode", "data"),
)
if rebuild_fn_hash:
self.__set_react_attribute("config", rebuild_fn_hash)
Expand Down Expand Up @@ -671,15 +684,6 @@ def _set_content(self, var_name: str = "content", image=True):
)
return self.set_attribute(_to_camel_case(f"default_{var_name}"), value)

def _set_lov(self, var_name="lov", property_name: t.Optional[str] = None):
property_name = var_name if property_name is None else property_name
self.__set_list_of_(f"default_{property_name}")
if hash_name := self.__hashes.get(var_name):
hash_name = self.__get_typed_hash_name(hash_name, PropertyType.lov)
self.__update_vars.append(f"{property_name}={hash_name}")
self.__set_react_attribute(property_name, hash_name)
return self

def __set_dynamic_string_list(self, var_name: str, default_value: t.Any):
hash_name = self.__hashes.get(var_name)
loi = self.__attributes.get(var_name)
Expand Down Expand Up @@ -989,9 +993,8 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901
self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
elif var_type == PropertyType.data:
self.__set_dynamic_property_without_default(attr[0], var_type)
elif var_type == PropertyType.lov:
self._get_adapter(attr[0]) # need to be called before set_lov
self._set_lov(attr[0])
elif var_type == PropertyType.lov or var_type == PropertyType.single_lov:
self._get_lov_adapter(attr[0], multi_selection=var_type != PropertyType.single_lov)
elif var_type == PropertyType.lov_value:
self.__set_dynamic_property_without_default(
attr[0], var_type, _get_tuple_val(attr, 2, None) == "optional"
Expand Down
20 changes: 8 additions & 12 deletions taipy/gui/_renderers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,31 +317,29 @@ class _Factory:
element_name="MenuCtl",
attributes=attrs,
)
._get_adapter("lov") # need to be called before set_lov
._set_lov()
.set_attributes(
[
("id",),
("active", PropertyType.dynamic_boolean, True),
("label"),
("width"),
("label",),
("width",),
("width[mobile]",),
("on_action", PropertyType.function),
("inactive_ids", PropertyType.dynamic_list),
("hover_text", PropertyType.dynamic_string),
("lov", PropertyType.lov)
]
)
._set_propagate(),
"navbar": lambda gui, control_type, attrs: _Builder(
gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None
)
._get_adapter("lov", multi_selection=False) # need to be called before set_lov
._set_lov()
.set_attributes(
[
("id",),
("active", PropertyType.dynamic_boolean, True),
("hover_text", PropertyType.dynamic_string),
("lov", PropertyType.single_lov)
]
),
"number": lambda gui, control_type, attrs: _Builder(
Expand Down Expand Up @@ -402,8 +400,6 @@ class _Factory:
gui=gui, control_type=control_type, element_name="Selector", attributes=attrs, default_value=None
)
.set_value_and_default(with_default=False, var_type=PropertyType.lov_value)
._get_adapter("lov") # need to be called before set_lov
._set_lov()
.set_attributes(
[
("active", PropertyType.dynamic_boolean, True),
Expand All @@ -418,6 +414,7 @@ class _Factory:
("on_change", PropertyType.function),
("label",),
("mode",),
("lov", PropertyType.lov)
]
)
._set_propagate(),
Expand All @@ -432,14 +429,14 @@ class _Factory:
.set_attributes(
[
("active", PropertyType.dynamic_boolean, True),
("height"),
("height",),
("hover_text", PropertyType.dynamic_string),
("id",),
("value_by_id", PropertyType.boolean),
("max", PropertyType.number, 100),
("min", PropertyType.number, 0),
("step", PropertyType.number, 1),
("orientation"),
("orientation",),
("width", PropertyType.string, "300px"),
("on_change", PropertyType.function),
("continuous", PropertyType.boolean, True),
Expand Down Expand Up @@ -518,8 +515,6 @@ class _Factory:
gui=gui, control_type=control_type, element_name="Toggle", attributes=attrs, default_value=None
)
.set_value_and_default(with_default=False, var_type=PropertyType.toggle_value)
._get_adapter("lov", multi_selection=False) # need to be called before set_lov
._set_lov()
.set_attributes(
[
("active", PropertyType.dynamic_boolean, True),
Expand All @@ -531,6 +526,7 @@ class _Factory:
("allow_unselect", PropertyType.boolean),
("on_change", PropertyType.function),
("mode",),
("lov", PropertyType.single_lov)
]
)
._set_kind()
Expand Down
19 changes: 8 additions & 11 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,16 +1006,10 @@ def __send_var_list_update( # noqa C901
newvalue = self._get_user_content_url(
None, {"variable_name": str(_var), Gui._HTML_CONTENT_KEY: str(time.time())}
)
elif isinstance(newvalue, _TaipyLov):
newvalue = [self.__adapter._run_for_var(newvalue.get_name(), elt) for elt in newvalue.get()]
elif isinstance(newvalue, _TaipyLovValue):
if isinstance(newvalue.get(), list):
newvalue = [
self.__adapter._run_for_var(newvalue.get_name(), elt, id_only=True)
for elt in newvalue.get()
]
else:
newvalue = self.__adapter._run_for_var(newvalue.get_name(), newvalue.get(), id_only=True)
elif isinstance(newvalue, (_TaipyLov, _TaipyLovValue)):
newvalue = self.__adapter.run(
newvalue.get_name(), newvalue.get(), id_only=isinstance(newvalue, _TaipyLovValue)
)
elif isinstance(newvalue, _TaipyToJson):
newvalue = newvalue.get()
if isinstance(newvalue, (dict, _MapDict)):
Expand Down Expand Up @@ -1493,7 +1487,7 @@ def _set_building(self, building: bool):
def __is_building(self):
return hasattr(self, "_building") and self._building

def _get_rebuild_fn_name(self, name: str):
def _get_call_method_name(self, name: str):
return f"{Gui.__SELF_VAR}.{name}"

def __get_attributes(self, attr_json: str, hash_json: str, args_dict: t.Dict[str, t.Any]):
Expand All @@ -1505,6 +1499,9 @@ def __get_attributes(self, attr_json: str, hash_json: str, args_dict: t.Dict[str
def _compare_data(self, *data):
return data[0]

def _get_adapted_lov(self, lov: list, var_type: str):
return self.__adapter._get_adapted_lov(lov, var_type)

def _tbl_cols(
self, rebuild: bool, rebuild_val: t.Optional[bool], attr_json: str, hash_json: str, **kwargs
) -> t.Union[str, _DoNotUpdate]:
Expand Down
1 change: 1 addition & 0 deletions taipy/gui/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class PropertyType(Enum):
"""
image = _TaipyContentImage
json = "json"
single_lov = "singlelov"
lov = _TaipyLov
"""
The property holds a LoV (list of values).
Expand Down
52 changes: 45 additions & 7 deletions taipy/gui/utils/_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,29 @@
from . import _MapDict


class _AdaptedLov:
def __init__(self, lov: t.Any, var_type: str) -> None:
self._lov = lov
self._type = var_type

@staticmethod
def get_lov(lov: t.Any):
return lov._lov if isinstance(lov, _AdaptedLov) else lov

@staticmethod
def get_type(lov: t.Any):
return lov._type if isinstance(lov, _AdaptedLov) else None


class _Adapter:
def __init__(self) -> None:
self.__adapter_for_type: t.Dict[str, t.Callable] = {}
self.__type_for_variable: t.Dict[str, str] = {}
self.__warning_by_type: t.Set[str] = set()

def _get_adapted_lov(self, lov: t.Any, var_type: str) -> _AdaptedLov:
return _AdaptedLov(lov, var_type)

def _add_for_type(self, type_name: str, adapter: t.Callable) -> None:
self.__adapter_for_type[type_name] = adapter

Expand All @@ -40,31 +57,52 @@ def _get_unique_type(self, type_name: str) -> str:
index += 1
return type_name

def _run_for_var(self, var_name: str, value: t.Any, id_only=False) -> t.Any:
ret = self._run(self.__get_for_var(var_name, value), value, var_name, id_only)
return ret if ret is not None else value
def run(self, var_name: str, value: t.Any, id_only=False) -> t.Any:
lov = _AdaptedLov.get_lov(value)
adapter = self.__get_for_var(var_name, value)
if isinstance(lov, (list, tuple)):
res = []
for elt in lov:
v = self._run(adapter, elt, var_name, id_only)
res.append(v if v is not None else elt)
return res
return self._run(adapter, lov, var_name, id_only)

def __get_for_var(self, var_name: str, value: t.Any) -> t.Optional[t.Callable]:
adapter = None
type_name = _AdaptedLov.get_type(value)
if type_name:
adapter = self.__adapter_for_type.get(type_name)
if callable(adapter):
return adapter
type_name = self.__type_for_variable.get(var_name)
if not isinstance(type_name, str):
adapter = self.__adapter_for_type.get(var_name)
type_name = var_name if callable(adapter) else type(value).__name__
lov = _AdaptedLov.get_lov(value)
elt = lov[0] if isinstance(lov, (list, tuple)) and len(lov) else None
type_name = var_name if callable(adapter) else type(elt).__name__
if adapter is None:
adapter = self.__adapter_for_type.get(type_name)
return adapter if callable(adapter) else None

def _get_elt_per_ids(self, var_name: str, lov: t.List[t.Any]) -> t.Dict[str, t.Any]:
def _get_elt_per_ids(
self, var_name: str, lov: t.List[t.Any], adapter: t.Optional[t.Callable] = None
) -> t.Dict[str, t.Any]:
dict_res = {}
adapter = self.__get_for_var(var_name, lov[0] if lov else None)
type_name = _AdaptedLov.get_type(lov)
lov = _AdaptedLov.get_lov(lov)
if not adapter and type_name:
adapter = self.__adapter_for_type.get(type_name)
if not adapter:
adapter = self.__get_for_var(var_name, lov[0] if lov else None)
for value in lov:
try:
result = adapter(value._dict if isinstance(value, _MapDict) else value) if adapter else value
if result is not None:
dict_res[self.__get_id(result)] = value
children = self.__get_children(result)
if children is not None:
dict_res.update(self._get_elt_per_ids(var_name, children))
dict_res.update(self._get_elt_per_ids(var_name, children, adapter))
except Exception as e:
_warn(f"Cannot run adapter for {var_name}", e)
return dict_res
Expand Down
4 changes: 2 additions & 2 deletions tests/gui/builder/control/test_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def test_menu_builder(gui: Gui, test_client, helpers):
"<MenuCtl",
'libClassName="taipy-menu"',
'defaultLov="[&quot;Item 1&quot;, &quot;Item 2&quot;, &quot;Item 3&quot;, &quot;Item 4&quot;]"',
"lov={_TpL_tpec_TpExPr_lov_TPMDL_0}",
"lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_str_TPMDL_0_0}",
'onAction="on_menu_action"',
'updateVars="lov=_TpL_tpec_TpExPr_lov_TPMDL_0"',
'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_str_TPMDL_0_0"',
]
helpers.test_control_builder(gui, page, expected_list)
2 changes: 1 addition & 1 deletion tests/gui/builder/control/test_navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ def test_navbar_builder(gui: Gui, test_client, helpers):
expected_list = [
"<NavBar",
'defaultLov="[[&quot;/page1&quot;, &quot;Page 1&quot;], [&quot;/page2&quot;, &quot;Page 2&quot;], [&quot;/page3&quot;, &quot;Page 3&quot;], [&quot;/page4&quot;, &quot;Page 4&quot;]]"', # noqa: E501
"lov={_TpL_tpec_TpExPr_navlov_TPMDL_0}",
"lov={_TpL_tp_TpExPr_gui_get_adapted_lov_navlov_tuple_TPMDL_0_0}",
]
helpers.test_control_builder(gui, page, expected_list)
4 changes: 2 additions & 2 deletions tests/gui/builder/control/test_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def test_selector_builder_3(gui: Gui, test_client, helpers):
"<Selector",
'defaultLov="[[&quot;1&quot;, &quot;scenario 1&quot;], [&quot;3&quot;, &quot;scenario 3&quot;], [&quot;2&quot;, &quot;scenario 2&quot;]]"', # noqa: E501
'defaultValue="[&quot;1&quot;]"',
"lov={_TpL_tpec_TpExPr_scenario_list_TPMDL_0}",
"lov={_TpL_tp_TpExPr_gui_get_adapted_lov_scenario_list_dict_TPMDL_0_0}",
"propagate={false}",
'updateVars="lov=_TpL_tpec_TpExPr_scenario_list_TPMDL_0"',
'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_scenario_list_dict_TPMDL_0_0"',
'updateVarName="_TpLv_tpec_TpExPr_selected_obj_TPMDL_0"',
"value={_TpLv_tpec_TpExPr_selected_obj_TPMDL_0}",
]
Expand Down
Loading

0 comments on commit 7d48ce6

Please sign in to comment.