From f3190e0ae69b1fb895ef5b83e963fbb73f79e74d Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 12:29:22 +0100 Subject: [PATCH 01/13] - Added taipy.gui.extention entry point for API generation. - Code reorganization. - Hide inner properties. - Store extension libraries element constructors in their library module. - Added PropertyType.any and dynamic_any. - Added optional documentation on extension library elements and their properties. --- .../src/components/Taipy/tableUtils.tsx | 8 +- .../taipy-gui/src/components/Taipy/utils.ts | 36 +- taipy/gui/_renderers/builder.py | 494 +++++++++--------- taipy/gui/_renderers/factory.py | 62 +-- taipy/gui/builder/_api_generator.py | 11 +- taipy/gui/extension/__main__.py | 128 +++++ taipy/gui/extension/library.py | 136 +++-- taipy/gui/types.py | 8 + taipy/gui_core/_GuiCoreLib.py | 15 +- 9 files changed, 546 insertions(+), 352 deletions(-) create mode 100644 taipy/gui/extension/__main__.py diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx index ad747bb166..ea5db924c8 100644 --- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx +++ b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx @@ -44,10 +44,6 @@ import { FormatConfig } from "../../context/taipyReducers"; import { dateToString, getDateTime, getDateTimeString, getNumberString, getTimeZonedDate } from "../../utils/index"; import { TaipyActiveProps, TaipyMultiSelectProps, getSuffixedClassNames } from "./utils"; -/** - * A column description as received by the backend. - */ - /** * Generates a CSS class name for a table header. * @param columnName - The name of the column. @@ -63,6 +59,10 @@ export const generateHeaderClassName = (columnName: string | undefined): string return '-' + columnName.replace(/\W+/g, '-').replace(/-+/g, '-').toLowerCase(); }; +/** + * A column description as received by the backend. + */ + export interface ColumnDesc { /** The unique column identifier. */ dfid: string; diff --git a/frontend/taipy-gui/src/components/Taipy/utils.ts b/frontend/taipy-gui/src/components/Taipy/utils.ts index c04c497311..183bf418fe 100644 --- a/frontend/taipy-gui/src/components/Taipy/utils.ts +++ b/frontend/taipy-gui/src/components/Taipy/utils.ts @@ -14,9 +14,19 @@ import { MouseEvent, ReactNode } from "react"; import { SxProps } from "@mui/material"; -export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps { - defaultActive?: boolean; - active?: boolean; +export interface TaipyBaseProps { + id?: string; + libClassName?: string; + className?: string; + dynamicClassName?: string; + privateClassName?: string; + children?: ReactNode; +} + +interface TaipyDynamicProps extends TaipyBaseProps { + updateVarName?: string; + propagate?: boolean; + updateVars?: string; } export interface TaipyHoverProps { @@ -24,19 +34,13 @@ export interface TaipyHoverProps { defaultHoverText?: string; } -interface TaipyDynamicProps extends TaipyBaseProps { - updateVarName?: string; - propagate?: boolean; - updateVars?: string; +export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps { + defaultActive?: boolean; + active?: boolean; } -export interface TaipyBaseProps { - id?: string; - libClassName?: string; - className?: string; - dynamicClassName?: string; - privateClassName?: string; - children?: ReactNode; +export interface TaipyLabelProps { + label?: string; } export interface TaipyMultiSelectProps { @@ -68,10 +72,6 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai width?: string | number; } -export interface TaipyLabelProps { - label?: string; -} - export interface DateProps { maxDate?: unknown; maxDateTime?: unknown; diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index ba9d5ee95a..f7b3d790d5 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -85,7 +85,7 @@ def __init__( gui: "Gui", control_type: str, element_name: str, - attributes: t.Optional[t.Dict[str, t.Any]], + prop_values: t.Optional[t.Dict[str, t.Any]], hash_names: t.Optional[t.Dict[str, str]] = None, default_value: t.Optional[t.Any] = "", lib_name: str = "taipy", @@ -101,20 +101,20 @@ def __init__( self.__control_type = control_type self.__element_name = element_name self.__lib_name = lib_name - self.__attributes = attributes or {} + self.__prop_values = prop_values or {} self.__hashes = hash_names.copy() self.__update_vars: t.List[str] = [] self.__gui: Gui = gui self.__default_property_name = _Factory.get_default_property_name(control_type) or "" - default_property_value = self.__attributes.get(self.__default_property_name, None) + default_property_value = self.__prop_values.get(self.__default_property_name, None) if default_property_value is None and default_value is not None: - self.__attributes[self.__default_property_name] = default_value + self.__prop_values[self.__default_property_name] = default_value # Bind properties dictionary to attributes if condition is matched (will # leave the binding for function at the builder ) - if "properties" in self.__attributes: - (prop_dict, prop_hash) = _Builder.__parse_attribute_value(gui, self.__attributes["properties"]) + if "properties" in self.__prop_values: + (prop_dict, prop_hash) = _Builder.__parse_attribute_value(gui, self.__prop_values["properties"]) if prop_hash is None: prop_hash = prop_dict prop_hash = self.__gui._bind_var(prop_hash) @@ -125,14 +125,14 @@ def __init__( var_name, _ = gui._get_real_var_name(prop_hash) for k, v in prop_dict.items(): (val, key_hash) = _Builder.__parse_attribute_value(gui, v) - self.__attributes[k] = ( + self.__prop_values[k] = ( f"{{None if ({var_name}) is None else ({var_name}).get('{k}')}}" if key_hash is None else v ) else: _warn(f"{self.__control_type}.properties ({prop_hash}) must be a dict.") # Bind potential function and expressions in self.attributes - self.__hashes.update(_Builder._get_variable_hash_names(gui, self.__attributes, hash_names)) + self.__hashes.update(_Builder._get_variable_hash_names(gui, self.__prop_values, hash_names)) # set classname self.__set_class_names() @@ -206,13 +206,28 @@ def get_name_indexed_property(self, name: str) -> t.Dict[str, t.Any]: name (str): The property name. """ - return _get_name_indexed_property(self.__attributes, name) + return _get_name_indexed_property(self.__prop_values, name) + + def __set_json_attribute(self, name, value): + return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder)) + + def __set_any_attribute(self, name: str, default_value: t.Optional[str] = None): + value = self.__prop_values.get(name, default_value) + return self.__set_json_attribute(_to_camel_case(name), value) + + def __set_dynamic_any_attribute(self, name: str, default_value: t.Optional[str] = None): + value = self.__prop_values.get(name, default_value) + self.__set_json_attribute(_to_camel_case(f"default_{name}"), value) + if hash_name := self.__hashes.get(name): + self.__update_vars.append(f"{name}={hash_name}") + self.__set_react_attribute(name, hash_name) + return self def __get_boolean_attribute(self, name: str, default_value=False): - bool_attr = self.__attributes.get(name, default_value) + bool_attr = self.__prop_values.get(name, default_value) return _is_true(bool_attr) if isinstance(bool_attr, str) else bool(bool_attr) - def set_boolean_attribute(self, name: str, value: bool): + def __set_boolean_attribute(self, name: str, value: bool): """ TODO-undocumented Defines a React Boolean attribute (attr={true|false}). @@ -223,61 +238,22 @@ def set_boolean_attribute(self, name: str, value: bool): """ return self.__set_react_attribute(_to_camel_case(name), value) - def set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None): - """ - TODO-undocumented - Defines a React attribute as a stringified json dict. - The original property can be a dict or a string formed as :;:. - - Arguments: - name (str): The property name. - default value (dict): used if no value is specified. - """ - dict_attr = self.__attributes.get(name) - if dict_attr is None: - dict_attr = default_value - if dict_attr is not None: - if isinstance(dict_attr, str): - vals = [x.strip().split(":") for x in dict_attr.split(";")] - dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} - if isinstance(dict_attr, (dict, _MapDict)): - self.__set_json_attribute(_to_camel_case(name), dict_attr) - else: - _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") - return self - - def set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None): - """ - TODO-undocumented - Defines a React attribute as a stringified json dict. - The original property can be a dict or a string formed as :;:. - - Arguments: - name (str): The property name. - default value (dict): used if no value is specified. - """ - dict_attr = self.__attributes.get(name) - if dict_attr is None: - dict_attr = default_value - if dict_attr is not None: - if isinstance(dict_attr, str): - vals = [x.strip().split(":") for x in dict_attr.split(";")] - dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} - if isinstance(dict_attr, (dict, _MapDict)): - self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr) - else: - _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") - if dict_hash := self.__hashes.get(name): - dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict) - prop_name = _to_camel_case(name) - self.__update_vars.append(f"{prop_name}={dict_hash}") - self.__set_react_attribute(prop_name, dict_hash) - return self - - def __set_json_attribute(self, name, value): - return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder)) + def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True): + hash_name = self.__hashes.get(name) + val = self.__get_boolean_attribute(name, def_val) + default_name = f"default_{name}" if hash_name is not None else name + if val != def_val: + self.__set_boolean_attribute(default_name, val) + if hash_name is not None: + hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean) + self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name)) + if with_update: + if update_main: + self.__set_update_var_name(hash_name) + else: + self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}") - def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True): + def __set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True): """ TODO-undocumented Defines a React number attribute (attr={}). @@ -288,7 +264,7 @@ def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, default_value (optional(str)): the default value as a string. optional (bool): Default to True, the property is required if False. """ - value = self.__attributes.get(name, default_value) + value = self.__prop_values.get(name, default_value) if value is None: if not optional: _warn(f"Property {name} is required for control {self.__control_type}.") @@ -306,25 +282,37 @@ def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, ) return self.__set_react_attribute(_to_camel_case(name), val) + def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any): + hash_name = self.__hashes.get(var_name) + numVal = self.__prop_values.get(var_name) + if numVal is None: + numVal = default_value + if isinstance(numVal, str): + try: + numVal = float(numVal) + except Exception as e: + _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e) + numVal = 0 + if isinstance(numVal, numbers.Number): + self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal) + elif numVal is not None: + _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).") + if hash_name: + hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number) + self.__update_vars.append(f"{var_name}={hash_name}") + self.__set_react_attribute(var_name, hash_name) + return self + def __set_string_attribute( self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True ): - str_attr = self.__attributes.get(name, default_value) + str_attr = self.__prop_values.get(name, default_value) if str_attr is None: if not optional: _warn(f"Property {name} is required for control {self.__control_type}.") return self return self.set_attribute(_to_camel_case(name), str(str_attr)) - def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None): - date_attr = self.__attributes.get(var_name, default_value) - if date_attr is None: - date_attr = default_value - if isinstance(date_attr, (datetime, date, time)): - value = _date_to_string(date_attr) - self.set_attribute(_to_camel_case(var_name), value) - return self - def __set_dynamic_string_attribute( self, name: str, @@ -332,7 +320,7 @@ def __set_dynamic_string_attribute( with_update: t.Optional[bool] = False, dynamic_property_name: t.Optional[str] = None, ): - str_val = self.__attributes.get(name, default_value) + str_val = self.__prop_values.get(name, default_value) if str_val is not None: self.set_attribute( _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(str_val) @@ -344,10 +332,115 @@ def __set_dynamic_string_attribute( self.__set_react_attribute(prop_name, hash_name) return self + def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None): + attr = self.__prop_values.get(name, default_value) + if attr is None: + return self + if isinstance(attr, numbers.Number): + return self.__set_react_attribute(_to_camel_case(name), attr) + else: + return self.set_attribute(_to_camel_case(name), attr) + + def __set_dynamic_string_list(self, name: str, default_value: t.Any): + hash_name = self.__hashes.get(name) + loi = self.__prop_values.get(name) + if loi is None: + loi = default_value + if isinstance(loi, str): + loi = [s.strip() for s in loi.split(";") if s.strip()] + if isinstance(loi, list): + self.__set_json_attribute(_to_camel_case(f"default_{name}"), loi) + if hash_name: + self.__update_vars.append(f"{name}={hash_name}") + self.__set_react_attribute(name, hash_name) + return self + + def __set_list_attribute( + self, + name: str, + hash_name: t.Optional[str], + val: t.Any, + elt_type: t.Type, + dynamic=True, + default_val: t.Optional[t.Any] = None, + ) -> t.List[str]: + val = default_val if val is None else val + if not hash_name and isinstance(val, str): + val = [elt_type(t.strip()) for t in val.split(";")] + if isinstance(val, list): + if hash_name and dynamic: + self.__set_react_attribute(name, hash_name) + return [f"{name}={hash_name}"] + else: + self.__set_json_attribute(name, val) + elif val is not None: + _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.") + return [] + + def __set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None): + """ + TODO-undocumented + Defines a React attribute as a stringified json dict. + The original property can be a dict or a string formed as :;:. + + Arguments: + name (str): The property name. + default value (dict): used if no value is specified. + """ + dict_attr = self.__prop_values.get(name) + if dict_attr is None: + dict_attr = default_value + if dict_attr is not None: + if isinstance(dict_attr, str): + vals = [x.strip().split(":") for x in dict_attr.split(";")] + dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} + if isinstance(dict_attr, (dict, _MapDict)): + self.__set_json_attribute(_to_camel_case(name), dict_attr) + else: + _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") + return self + + def __set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None): + """ + TODO-undocumented + Defines a React attribute as a stringified json dict. + The original property can be a dict or a string formed as :;:. + + Arguments: + name (str): The property name. + default value (dict): used if no value is specified. + """ + dict_attr = self.__prop_values.get(name) + if dict_attr is None: + dict_attr = default_value + if dict_attr is not None: + if isinstance(dict_attr, str): + vals = [x.strip().split(":") for x in dict_attr.split(";")] + dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} + if isinstance(dict_attr, (dict, _MapDict)): + self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr) + else: + _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") + if dict_hash := self.__hashes.get(name): + dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict) + prop_name = _to_camel_case(name) + self.__update_vars.append(f"{prop_name}={dict_hash}") + self.__set_react_attribute(prop_name, dict_hash) + return self + + def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None): + date_attr = self.__prop_values.get(var_name, default_value) + if date_attr is None: + date_attr = default_value + if isinstance(date_attr, (datetime, date, time)): + value = _date_to_string(date_attr) + self.set_attribute(_to_camel_case(var_name), value) + return self + def __set_function_attribute( self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True ): - str_attr = self.__attributes.get(name, default_value) + str_attr = self.__prop_values.get(name, default_value) if str_attr is None: if not optional: _warn(f"Property {name} is required for control {self.__control_type}.") @@ -365,15 +458,6 @@ def __set_function_attribute( _warn(f"{self.__control_type}.{name}: {str_attr} is not a function.") return self.set_attribute(_to_camel_case(name), str_attr) if str_attr else self - def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None): - attr = self.__attributes.get(name, default_value) - if attr is None: - return self - if isinstance(attr, numbers.Number): - return self.__set_react_attribute(_to_camel_case(name), attr) - else: - return self.set_attribute(_to_camel_case(name), attr) - 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)) + "!}") @@ -387,7 +471,7 @@ def _get_lov_adapter( # noqa: C901 property_name = var_name if property_name is None else property_name lov_name = self.__hashes.get(var_name) real_var_name = self.__gui._get_real_var_name(lov_name)[0] if lov_name else None - lov = self.__attributes.get(var_name) + lov = self.__prop_values.get(var_name) adapter: t.Any = None var_type: t.Optional[str] = None if isinstance(lov, str): @@ -405,13 +489,13 @@ def _get_lov_adapter( # noqa: C901 default_lov: t.Optional[t.List[t.Any]] = [] if with_default or not lov_name else None - adapter = self.__attributes.get("adapter", adapter) + adapter = self.__prop_values.get("adapter", adapter) if adapter and isinstance(adapter, str): adapter = self.__gui._get_user_function(adapter) if adapter and not _is_function(adapter): _warn(f"{self.__element_name}: adapter property value is invalid.") adapter = None - var_type = self.__attributes.get("type", var_type) + var_type = self.__prop_values.get("type", var_type) if isclass(var_type): var_type = var_type.__name__ @@ -421,7 +505,7 @@ def _get_lov_adapter( # noqa: C901 if lov: elt = lov[0] else: - value = self.__attributes.get("value") + value = self.__prop_values.get("value") if isinstance(value, list): if len(value) > 0: elt = value[0] @@ -460,7 +544,7 @@ def _get_lov_adapter( # noqa: C901 default_lov.append(ret) ret_list = [] - value = self.__attributes.get("value") + value = self.__prop_values.get("value") val_list = value if isinstance(value, list) else [value] for val in val_list: ret = self.__gui._run_adapter( @@ -475,8 +559,8 @@ def _get_lov_adapter( # noqa: C901 self.__set_default_value("value", ret_list) else: ret_val = ret_list[0] if len(ret_list) else "" - if ret_val == "-1" and self.__attributes.get("unselected_value") is not None: - ret_val = str(self.__attributes.get("unselected_value", "")) + if ret_val == "-1" and self.__prop_values.get("unselected_value") is not None: + ret_val = str(self.__prop_values.get("unselected_value", "")) self.__set_default_value("value", ret_val) # LoV default value @@ -500,12 +584,12 @@ def _get_lov_adapter( # noqa: C901 return self def __filter_attribute_names(self, names: t.Iterable[str]): - return [k for k in self.__attributes if k in names or any(k.startswith(n + "[") for n in names)] + return [k for k in self.__prop_values if k in names or any(k.startswith(n + "[") for n in names)] def __get_held_name(self, key: str): name = self.__hashes.get(key) if name: - v = self.__attributes.get(key) + v = self.__prop_values.get(key) if isinstance(v, _TaipyBase): return name[: len(v.get_hash()) + 1] return name @@ -514,12 +598,12 @@ def __filter_attributes_hashes(self, keys: t.List[str]): hash_names = [k for k in self.__hashes if k in keys] attr_names = [k for k in keys if k not in hash_names] return ( - {k: v for k, v in self.__attributes.items() if k in attr_names}, + {k: v for k, v in self.__prop_values.items() if k in attr_names}, {k: self.__get_held_name(k) for k in self.__hashes if k in hash_names}, ) def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]): - rebuild = self.__attributes.get("rebuild", False) + rebuild = self.__prop_values.get("rebuild", False) rebuild_hash = self.__hashes.get("rebuild") if rebuild_hash or _is_true(rebuild): attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names)) @@ -536,8 +620,8 @@ def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]): return None def _get_dataframe_attributes(self) -> "_Builder": - date_format = _add_to_dict_and_get(self.__attributes, "date_format", "MM/dd/yyyy") - data = self.__attributes.get("data") + date_format = _add_to_dict_and_get(self.__prop_values, "date_format", "MM/dd/yyyy") + data = self.__prop_values.get("data") data_hash = self.__hashes.get("data", "") cmp_hash = "" if data_hash: @@ -552,13 +636,13 @@ def _get_dataframe_attributes(self) -> "_Builder": cmp_hash = self.__gui._evaluate_expr( "{" + f"{self.__gui._get_call_method_name('_compare_data')}" - + f'({self.__gui._get_real_var_name(data_hash)[0]},{",".join(cmp_datas)})' + + f"({self.__gui._get_real_var_name(data_hash)[0]},{','.join(cmp_datas)})" + "}" ) self.__update_vars.append(f"comparedatas={','.join(cmp_datas_hash)}") col_types = self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash)) col_dict = _get_columns_dict( - data, self.__attributes.get("columns", {}), col_types, date_format, self.__attributes.get("number_format") + data, self.__prop_values.get("columns", {}), col_types, date_format, self.__prop_values.get("number_format") ) rebuild_fn_hash = self.__build_rebuild_fn( @@ -567,7 +651,7 @@ def _get_dataframe_attributes(self) -> "_Builder": if rebuild_fn_hash: self.__set_react_attribute("columns", rebuild_fn_hash) if col_dict is not None: - _enhance_columns(self.__attributes, self.__hashes, col_dict, self.__element_name) + _enhance_columns(self.__prop_values, self.__hashes, col_dict, self.__element_name) self.__set_json_attribute("defaultColumns", col_dict) if cmp_hash: hash_name = self.__get_typed_hash_name(cmp_hash, PropertyType.data) @@ -576,12 +660,12 @@ def _get_dataframe_attributes(self) -> "_Builder": _get_client_var_name(hash_name), ) self.__set_update_var_name(hash_name) - self.set_boolean_attribute("compare", True) + self.__set_boolean_attribute("compare", True) self.__set_string_attribute("on_compare") - if not isinstance(self.__attributes.get("style"), (type(None), dict, _MapDict)): + if not isinstance(self.__prop_values.get("style"), (type(None), dict, _MapDict)): _warn("Table: property 'style' has been renamed to 'row_class_name'.") - if row_class_name := self.__attributes.get("row_class_name"): + if row_class_name := self.__prop_values.get("row_class_name"): if _is_function(row_class_name): value = self.__hashes.get("row_class_name") elif isinstance(row_class_name, str): @@ -592,7 +676,7 @@ def _get_dataframe_attributes(self) -> "_Builder": _warn(f"{self.__element_name}: row_class_name={value} must not be a column name.") elif value: self.set_attribute("rowClassName", value) - if tooltip := self.__attributes.get("tooltip"): + if tooltip := self.__prop_values.get("tooltip"): if _is_function(tooltip): value = self.__hashes.get("tooltip") elif isinstance(tooltip, str): @@ -606,8 +690,8 @@ def _get_dataframe_attributes(self) -> "_Builder": return self def _get_chart_config(self, default_type: str, default_mode: str): - self.__attributes["_default_type"] = default_type - self.__attributes["_default_mode"] = default_mode + self.__prop_values["_default_type"] = default_type + self.__prop_values["_default_mode"] = default_mode rebuild_fn_hash = self.__build_rebuild_fn( self.__gui._get_call_method_name("_chart_conf"), _CHART_NAMES + ("_default_type", "_default_mode"), @@ -616,7 +700,7 @@ def _get_chart_config(self, default_type: str, default_mode: str): self.__set_react_attribute("config", rebuild_fn_hash) # read column definitions - data = self.__attributes.get("data") + data = self.__prop_values.get("data") data_hash = self.__hashes.get("data", "") col_types = [self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))] @@ -627,8 +711,8 @@ def _get_chart_config(self, default_type: str, default_mode: str): while add_data_hash := self.__hashes.get(name_idx): typed_hash = self.__get_typed_hash_name(add_data_hash, _TaipyData) data_updates.append(typed_hash) - self.__set_react_attribute(f"data{data_idx}",_get_client_var_name(typed_hash)) - add_data = self.__attributes.get(name_idx) + self.__set_react_attribute(f"data{data_idx}", _get_client_var_name(typed_hash)) + add_data = self.__prop_values.get(name_idx) data_idx += 1 name_idx = f"data[{data_idx}]" col_types.append( @@ -636,7 +720,7 @@ def _get_chart_config(self, default_type: str, default_mode: str): ) self.set_attribute("dataVarNames", ";".join(data_updates)) - config = _build_chart_config(self.__gui, self.__attributes, col_types) + config = _build_chart_config(self.__gui, self.__prop_values, col_types) self.__set_json_attribute("defaultConfig", config) self._set_chart_selected(max=len(config.get("traces", []))) @@ -644,42 +728,20 @@ def _get_chart_config(self, default_type: str, default_mode: str): return self def _set_string_with_check(self, var_name: str, values: t.List[str], default_value: t.Optional[str] = None): - value = self.__attributes.get(var_name, default_value) + value = self.__prop_values.get(var_name, default_value) if value is not None: value = str(value).lower() - self.__attributes[var_name] = value + self.__prop_values[var_name] = value if value not in values: _warn(f"{self.__element_name}: {var_name}={value} should be in {values}.") else: self.__set_string_attribute(var_name, default_value) return self - def __set_list_attribute( - self, - name: str, - hash_name: t.Optional[str], - val: t.Any, - elt_type: t.Type, - dynamic=True, - default_val: t.Optional[t.Any] = None, - ) -> t.List[str]: - val = default_val if val is None else val - if not hash_name and isinstance(val, str): - val = [elt_type(t.strip()) for t in val.split(";")] - if isinstance(val, list): - if hash_name and dynamic: - self.__set_react_attribute(name, hash_name) - return [f"{name}={hash_name}"] - else: - self.__set_json_attribute(name, val) - elif val is not None: - _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.") - return [] - def _set_chart_selected(self, max=0): name = "selected" - default_sel = self.__attributes.get(name) - if not isinstance(default_sel, list) and name in self.__attributes: + default_sel = self.__prop_values.get(name) + if not isinstance(default_sel, list) and name in self.__prop_values: default_sel = [] if max == 0: self.__update_vars.extend( @@ -693,10 +755,10 @@ def _set_chart_selected(self, max=0): return idx = 1 name_idx = f"{name}[{idx}]" - sel = self.__attributes.get(name_idx) - if not isinstance(sel, list) and name_idx in self.__attributes: + sel = self.__prop_values.get(name_idx) + if not isinstance(sel, list) and name_idx in self.__prop_values: sel = [] - while idx <= max or name_idx in self.__attributes: + while idx <= max or name_idx in self.__prop_values: if sel is not None or default_sel is not None: self.__update_vars.extend( self.__set_list_attribute( @@ -708,14 +770,14 @@ def _set_chart_selected(self, max=0): ) idx += 1 name_idx = f"{name}[{idx}]" - sel = self.__attributes.get(name_idx) - if not isinstance(sel, list) and name_idx in self.__attributes: + sel = self.__prop_values.get(name_idx) + if not isinstance(sel, list) and name_idx in self.__prop_values: sel = [] def _get_list_attribute(self, name: str, list_type: PropertyType): hash_name = self.__hashes.get(name) if hash_name is None: - list_val = self.__attributes.get(name) + list_val = self.__prop_values.get(name) if isinstance(list_val, str): list_val = list(list_val.split(";")) if isinstance(list_val, list): @@ -736,7 +798,7 @@ def _get_list_attribute(self, name: str, list_type: PropertyType): def __set_class_names(self): self.set_attribute("libClassName", self.__lib_name + "-" + self.__control_type.replace("_", "-")) - if (private_css := self.__attributes.get("style")) and isinstance(private_css, (dict, _MapDict)): + if (private_css := self.__prop_values.get("style")) and isinstance(private_css, (dict, _MapDict)): taipy_style = etree.Element("TaipyStyle") taipy_style.set("className", f"tpcss-{id(private_css)}") taipy_style.set( @@ -748,7 +810,7 @@ def __set_class_names(self): return self.__set_dynamic_string_attribute("class_name", dynamic_property_name="dynamic_class_name") def _set_dataType(self): - value = self.__attributes.get("value") + value = self.__prop_values.get("value") return self.set_attribute("dataType", _get_data_type(value)) def _set_file_content(self, var_name: str = "content"): @@ -759,7 +821,7 @@ def _set_file_content(self, var_name: str = "content"): return self def _set_content(self, var_name: str = "content", image=True): - content = self.__attributes.get(var_name) + content = self.__prop_values.get(var_name) hash_name = self.__hashes.get(var_name) if content is None and hash_name is None: return self @@ -773,41 +835,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_dynamic_string_list(self, var_name: str, default_value: t.Any): - hash_name = self.__hashes.get(var_name) - loi = self.__attributes.get(var_name) - if loi is None: - loi = default_value - if isinstance(loi, str): - loi = [s.strip() for s in loi.split(";") if s.strip()] - if isinstance(loi, list): - self.__set_json_attribute(_to_camel_case(f"default_{var_name}"), loi) - if hash_name: - self.__update_vars.append(f"{var_name}={hash_name}") - self.__set_react_attribute(var_name, hash_name) - return self - - def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any): - hash_name = self.__hashes.get(var_name) - numVal = self.__attributes.get(var_name) - if numVal is None: - numVal = default_value - if isinstance(numVal, str): - try: - numVal = float(numVal) - except Exception as e: - _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e) - numVal = 0 - if isinstance(numVal, numbers.Number): - self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal) - elif numVal is not None: - _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).") - if hash_name: - hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number) - self.__update_vars.append(f"{var_name}={hash_name}") - self.__set_react_attribute(var_name, hash_name) - return self - def __set_default_value( self, var_name: str, @@ -816,7 +843,7 @@ def __set_default_value( var_type: t.Optional[t.Union[PropertyType, t.Type[_TaipyBase]]] = None, ): if value is None: - value = self.__attributes.get(var_name) + value = self.__prop_values.get(var_name) default_var_name = _to_camel_case(f"default_{var_name}") if isinstance(value, (datetime, date, time)): return self.set_attribute(default_var_name, _date_to_string(value)) @@ -861,7 +888,7 @@ def set_value_and_default( """ var_name = self.__default_property_name if var_name is None else var_name if var_type == PropertyType.slider_value or var_type == PropertyType.toggle_value: - if self.__attributes.get("lov"): + if self.__prop_values.get("lov"): var_type = PropertyType.lov_value native_type = False elif var_type == PropertyType.toggle_value: @@ -871,7 +898,7 @@ def set_value_and_default( else: var_type = ( PropertyType.dynamic_lo_numbers - if isinstance(self.__attributes.get("value"), list) + if isinstance(self.__prop_values.get("value"), list) else PropertyType.dynamic_number ) native_type = True @@ -887,7 +914,7 @@ def set_value_and_default( self.__set_update_var_name(hash_name) if with_default: if native_type: - val = self.__attributes.get(var_name) + val = self.__prop_values.get(var_name) if native_type and isinstance(val, str): with contextlib.suppress(Exception): val = float(val) @@ -895,9 +922,9 @@ def set_value_and_default( else: self.__set_default_value(var_name, var_type=var_type) else: - if var_type == PropertyType.data and (self.__control_type != "chart" or "figure" not in self.__attributes): + if var_type == PropertyType.data and (self.__control_type != "chart" or "figure" not in self.__prop_values): _warn(f"{self.__control_type}.{var_name} property should be bound.") - value = self.__attributes.get(var_name) + value = self.__prop_values.get(var_name) if value is not None: if native_type: if isinstance(value, str): @@ -911,28 +938,28 @@ def set_value_and_default( return self def _set_labels(self, var_name: str = "labels"): - if value := self.__attributes.get(var_name): + if value := self.__prop_values.get(var_name): if _is_true(value): return self.__set_react_attribute(_to_camel_case(var_name), True) elif isinstance(value, (dict, _MapDict)): - return self.set_dict_attribute(var_name) + return self.__set_dict_attribute(var_name) return self def _set_partial(self): if self.__control_type not in _Builder.__BLOCK_CONTROLS: return self - if partial := self.__attributes.get("partial"): - if self.__attributes.get("page"): + if partial := self.__prop_values.get("partial"): + if self.__prop_values.get("page"): _warn(f"{self.__element_name} control: page and partial should not be both defined.") if isinstance(partial, Partial): - self.__attributes["page"] = partial._route + self.__prop_values["page"] = partial._route self.__set_react_attribute("partial", partial._route) self.__set_react_attribute("defaultPartial", True) return self def _set_propagate(self): val = self.__get_boolean_attribute("propagate", t.cast(bool, self.__gui._config.config.get("propagate"))) - return self if val else self.set_boolean_attribute("propagate", False) + return self if val else self.__set_boolean_attribute("propagate", False) def __set_refresh_on_update(self): if self.__update_vars: @@ -942,7 +969,7 @@ def __set_refresh_on_update(self): def _set_table_pagesize_options(self, default_size=None): if default_size is None: default_size = [50, 100, 500] - page_size_options = self.__attributes.get("page_size_options", default_size) + page_size_options = self.__prop_values.get("page_size_options", default_size) if isinstance(page_size_options, str): try: page_size_options = [int(s.strip()) for s in page_size_options.split(";")] @@ -957,10 +984,10 @@ def _set_table_pagesize_options(self, default_size=None): def _set_input_type(self, type_name: str, allow_password=False): if allow_password and self.__get_boolean_attribute("password", False): return self.set_attribute("type", "password") - return self.set_attribute("type", self.__attributes.get("type", type_name)) + return self.set_attribute("type", self.__prop_values.get("type", type_name)) def _set_kind(self): - if self.__attributes.get("theme", False): + if self.__prop_values.get("theme", False): self.set_attribute("mode", "theme") return self @@ -972,21 +999,6 @@ def __get_typed_hash_name( hash_name = self.__gui._evaluate_bind_holder(t.cast(t.Type[_TaipyBase], taipy_type), expr) return hash_name - def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True): - hash_name = self.__hashes.get(name) - val = self.__get_boolean_attribute(name, def_val) - default_name = f"default_{name}" if hash_name is not None else name - if val != def_val: - self.set_boolean_attribute(default_name, val) - if hash_name is not None: - hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean) - self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name)) - if with_update: - if update_main: - self.__set_update_var_name(hash_name) - else: - self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}") - def __set_dynamic_property_without_default( self, name: str, property_type: PropertyType, optional: t.Optional[bool] = False ): @@ -1018,12 +1030,12 @@ def __set_html_content(self, name: str, property_name: str, property_type: Prope return self.__set_react_attribute(_to_camel_case(property_name), _get_client_var_name(front_var)) def _set_indexed_icons(self, name="use_icon"): - global_icon = self.__attributes.get(name) + global_icon = self.__prop_values.get(name) indexed = self.get_name_indexed_property(name) global_bool = _is_true(global_icon) if global_icon is not None and _is_boolean(global_icon) else None if global_icon is not None and not indexed: if global_bool is not None: - self.set_boolean_attribute(name, global_bool) + self.__set_boolean_attribute(name, global_bool) else: self.__set_json_attribute(_to_camel_case(name), {"__default": str(global_icon)}) elif indexed: @@ -1044,19 +1056,25 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 attributes (list(tuple)): The list of attributes as (property name, property type, default value). """ - attributes.append(("id",)) # Every element should have an id attribute + # Every element must have an id attribute + if not any(attr[0] == "id" for attr in attributes): + attributes.append(("id",)) for attr in attributes: if not isinstance(attr, tuple): attr = (attr,) var_type = _get_tuple_val(attr, 1, PropertyType.string) if var_type == PropertyType.to_json: var_type = _TaipyToJson - if var_type == PropertyType.boolean: + if var_type == PropertyType.any: + self.__set_any_attribute(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.dynamic_any: + self.__set_dynamic_any_attribute(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.boolean: def_val = _get_tuple_val(attr, 2, False) - if isinstance(def_val, bool) or self.__attributes.get(attr[0], None) is not None: + if isinstance(def_val, bool) or self.__prop_values.get(attr[0], None) is not None: val = self.__get_boolean_attribute(attr[0], def_val) if val != def_val: - self.set_boolean_attribute(attr[0], val) + self.__set_boolean_attribute(attr[0], val) elif var_type == PropertyType.dynamic_boolean: self.__set_dynamic_bool_attribute( attr[0], @@ -1065,7 +1083,7 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 _get_tuple_val(attr, 4, True), ) elif var_type == PropertyType.number: - self.set_number_attribute(attr[0], _get_tuple_val(attr, 2, None)) + self.__set_number_attribute(attr[0], _get_tuple_val(attr, 2, None)) elif var_type == PropertyType.dynamic_number: self.__set_dynamic_number_attribute(attr[0], _get_tuple_val(attr, 2, None)) elif var_type == PropertyType.string: @@ -1079,12 +1097,27 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 self.__set_list_attribute( attr[0], self.__hashes.get(attr[0]), - self.__attributes.get(attr[0]), + self.__prop_values.get(attr[0]), str, False, _get_tuple_val(attr, 2, None), ) ) + elif var_type == PropertyType.string_or_number: + self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.dynamic_list: + self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.dict: + self.__set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.dynamic_dict: + self.__set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.boolean_or_list: + if _is_boolean(self.__prop_values.get(attr[0])): + self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False) + else: + self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None)) + elif var_type == PropertyType.dynamic_date: + self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None)) elif var_type == PropertyType.function: self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True)) elif var_type == PropertyType.react: @@ -1093,26 +1126,11 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 self.__update_vars.append(f"{prop_name}={hash_name}") self.__set_react_attribute(prop_name, hash_name) else: - self.__set_react_attribute(prop_name, self.__attributes.get(attr[0], _get_tuple_val(attr, 2, None))) + self.__set_react_attribute(prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None))) elif var_type == PropertyType.broadcast: self.__set_react_attribute( _to_camel_case(attr[0]), _get_broadcast_var_name(_get_tuple_val(attr, 2, None)) ) - elif var_type == PropertyType.string_or_number: - self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None)) - elif var_type == PropertyType.dict: - self.set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None)) - elif var_type == PropertyType.dynamic_dict: - self.set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None)) - elif var_type == PropertyType.dynamic_list: - self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None)) - elif var_type == PropertyType.boolean_or_list: - if _is_boolean(self.__attributes.get(attr[0])): - self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False) - else: - self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None)) - elif var_type == PropertyType.dynamic_date: - self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None)) elif var_type == PropertyType.data: self.__set_dynamic_property_without_default(attr[0], t.cast(PropertyType, var_type)) elif ( @@ -1139,7 +1157,7 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 self.__update_vars.append(f"{prop_name}={hash_name}") self.__set_react_attribute(prop_name, hash_name) else: - val = self.__attributes.get(attr[0]) + val = self.__prop_values.get(attr[0]) self.set_attribute( prop_name, var_type(_get_tuple_val(attr, 2, None) if val is None else val, "").get() ) diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 72fe815273..14585273e3 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -75,7 +75,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Alert", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_type=PropertyType.dynamic_string) .set_attributes( @@ -89,7 +89,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Button", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_update=False) .set_attributes( @@ -103,7 +103,7 @@ class _Factory: ] ), "chat": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Chat", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Chat", prop_values=attrs, default_value=None ) .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data) .set_attributes( @@ -123,7 +123,7 @@ class _Factory: ] ), "chart": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Chart", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Chart", prop_values=attrs, default_value=None ) .set_value_and_default(with_default=False, var_type=PropertyType.data) .set_attributes( @@ -148,13 +148,13 @@ class _Factory: ._get_chart_config("scatter", "lines+markers") ._set_propagate(), "content": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="PageContent", attributes=attrs + gui=gui, control_type=control_type, element_name="PageContent", prop_values=attrs ), "date": lambda gui, control_type, attrs: _Builder( gui=gui, control_type=control_type, element_name="DateSelector", - attributes=attrs, + prop_values=attrs, default_value=datetime.fromtimestamp(0), ) .set_value_and_default(var_type=PropertyType.date) @@ -178,7 +178,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="DateRange", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_type=PropertyType.date_range) .set_attributes( @@ -200,7 +200,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Dialog", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_type=PropertyType.dynamic_boolean) ._set_partial() # partial should be set before page @@ -221,7 +221,7 @@ class _Factory: ) ._set_propagate(), "expandable": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Expandable", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Expandable", prop_values=attrs, default_value=None ) .set_value_and_default() ._set_partial() # partial should be set before page @@ -237,7 +237,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="FileDownload", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_name="label", with_update=False) ._set_content("content", image=False) @@ -257,7 +257,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="FileSelector", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_name="label", with_update=False) ._set_file_content() @@ -278,7 +278,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Image", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_name="label", with_update=False) ._set_content("content") @@ -295,7 +295,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Indicator", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_update=False, native_type=True) .set_attributes( @@ -313,7 +313,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Input", - attributes=attrs, + prop_values=attrs, ) ._set_input_type("text", True) .set_value_and_default() @@ -334,7 +334,7 @@ class _Factory: ] ), "layout": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Layout", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Layout", prop_values=attrs, default_value=None ) .set_value_and_default(with_default=False) .set_attributes( @@ -344,7 +344,7 @@ class _Factory: ] ), "login": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Login", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Login", prop_values=attrs, default_value=None ) .set_value_and_default(default_val="Log-in") .set_attributes( @@ -358,7 +358,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="MenuCtl", - attributes=attrs, + prop_values=attrs, ) .set_attributes( [ @@ -378,7 +378,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Metric", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True) .set_attributes( @@ -407,7 +407,7 @@ class _Factory: ] ), "navbar": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="NavBar", prop_values=attrs, default_value=None ).set_attributes( [ ("active", PropertyType.dynamic_boolean, True), @@ -419,7 +419,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Input", - attributes=attrs, + prop_values=attrs, default_value=0, ) ._set_input_type("number") @@ -442,7 +442,7 @@ class _Factory: ] ), "pane": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Pane", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Pane", prop_values=attrs, default_value=None ) .set_value_and_default(var_type=PropertyType.dynamic_boolean) ._set_partial() # partial should be set before page @@ -462,7 +462,7 @@ class _Factory: ) ._set_propagate(), "part": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Part", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Part", prop_values=attrs, default_value=None ) ._set_partial() # partial should be set before page .set_attributes( @@ -478,7 +478,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Progress", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True) .set_attributes( @@ -492,7 +492,7 @@ class _Factory: ] ), "selector": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Selector", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Selector", prop_values=attrs, default_value=None ) .set_value_and_default(with_default=False, var_type=PropertyType.lov_value) .set_attributes( @@ -518,7 +518,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Slider", - attributes=attrs, + prop_values=attrs, default_value=0, ) .set_value_and_default(native_type=True, var_type=PropertyType.slider_value) @@ -546,7 +546,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Status", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_update=False) .set_attributes( @@ -560,7 +560,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Table", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_default=False, var_type=PropertyType.data) ._get_dataframe_attributes() @@ -594,7 +594,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="Field", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_update=False) ._set_dataType() @@ -611,7 +611,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="TimeSelector", - attributes=attrs, + prop_values=attrs, default_value=datetime.today().time(), ) .set_value_and_default(var_type=PropertyType.time) @@ -628,7 +628,7 @@ class _Factory: ) ._set_propagate(), "toggle": lambda gui, control_type, attrs: _Builder( - gui=gui, control_type=control_type, element_name="Toggle", attributes=attrs, default_value=None + gui=gui, control_type=control_type, element_name="Toggle", prop_values=attrs, default_value=None ) .set_value_and_default(with_default=False, var_type=PropertyType.toggle_value) .set_attributes( @@ -650,7 +650,7 @@ class _Factory: gui=gui, control_type=control_type, element_name="TreeView", - attributes=attrs, + prop_values=attrs, ) .set_value_and_default(with_default=False, var_type=PropertyType.lov_value) .set_attributes( diff --git a/taipy/gui/builder/_api_generator.py b/taipy/gui/builder/_api_generator.py index 5a2f43262e..b7439d558d 100644 --- a/taipy/gui/builder/_api_generator.py +++ b/taipy/gui/builder/_api_generator.py @@ -89,10 +89,7 @@ def add_library(self, library: "ElementLibrary"): f"Python API for extension library '{library_name}' is not available. To fix this, import 'taipy.gui.builder' before importing the extension library." # noqa: E501 ) return - library_module = getattr(self.__module, library_name, None) - if library_module is None: - library_module = types.ModuleType(library_name) - setattr(self.__module, library_name, library_module) + library_module = sys.modules[library.__module__] for element_name, element in library.get_elements().items(): setattr( library_module, @@ -104,13 +101,13 @@ def add_library(self, library: "ElementLibrary"): {name: str(prop.property_type) for name, prop in element.attributes.items()}, ), ) - # Allow element to be accessed from the root module + # Allow element to be accessed from this module (taipy.gui.builder) if hasattr(self.__module, element_name): _TaipyLogger._get_logger().info( f"Can't add element `{element_name}` of library `{library_name}` to the root of Builder API as another element with the same name already exists." # noqa: E501 ) - continue - setattr(self.__module, element_name, getattr(library_module, element_name)) + else: + setattr(self.__module, element_name, getattr(library_module, element_name)) @staticmethod def create_block_api( diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py new file mode 100644 index 0000000000..c9ab1673f6 --- /dev/null +++ b/taipy/gui/extension/__main__.py @@ -0,0 +1,128 @@ +# © 2021-2025, Avaiga Pte Ltd. All Rights Reserved. The use of the Taipy software and any part thereof is governed by +# Avaiga Pte Ltd's Software License and Maintenance Agreement. Unauthorised use, reproduction and modification is +# strictly not allowed. + +import argparse +import os +from taipy.gui.extension import ElementLibrary +import typing as t + + +def error(message): + print(message) + exit(1) + + +def generate_tgb(args): + from importlib import import_module + from inspect import getmembers, isclass + + from taipy.gui.types import PropertyType + + package_root_dir = args.package_root_dir[0] + # Remove potential directory separator at the end of the package root dir + if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\": + package_root_dir = package_root_dir[:-1] + module = None + try: + module = import_module(package_root_dir) + except Exception as e: + error(f"Couldn't open module '{package_root_dir}' ({e})") + library: t.Optional[ElementLibrary] = None + for _, member in getmembers(module, lambda o: isclass(o) and issubclass(o, ElementLibrary)): + if library: + error("Extension contains more than one ElementLibrary") + library = member() + if library is None: + error("Extension does not contain any ElementLibrary") + return + pyi_path = os.path.join(package_root_dir, "__init__.pyi") + pyi_file = None + try: + pyi_file = open(pyi_path, "w") + except Exception as e: + error(f"Couldn't open Python Interface Definition file '{pyi_file}' ({e})") + + def clean_doc_string(doc_string) -> t.Optional[str]: + if not doc_string: + return None + lines = doc_string.splitlines() + min_indent = min((len(line) - len(line.lstrip())) for line in lines if line.strip()) + lines = [line[min_indent:] if line.strip() else "" for line in lines] + while lines and not lines[0].strip(): + lines.pop(0) + while lines and not lines[-1].strip(): + lines.pop() + return "\n".join(lines) if lines else None + + print(f"Inspecting extension library '{library.get_name()}'") # noqa: T201 + print("# ----------------------------------------------------------------------", file=pyi_file) + print("# Generated by taipy.gui.extension module", file=pyi_file) + print("# ----------------------------------------------------------------------", file=pyi_file) + for element_name, element in library.get_elements().items(): + properties = [] + property_doc = {} + default_property_found = False + for property_name, property in element.attributes.items(): + desc = property_name + # Could use 'match' with Python >= 3.10 + if property.property_type in [PropertyType.boolean, PropertyType.dynamic_boolean]: + desc = desc + ": bool" + elif property.property_type in [PropertyType.string, PropertyType.dynamic_string]: + desc = desc + ": str" + elif property.property_type in [PropertyType.dict, PropertyType.dynamic_dict]: + desc = desc + ": dict" + if property_name == element.default_attribute: + properties.insert(0, desc) + default_property_found = True + else: + properties.append(desc) + if doc_string := clean_doc_string(property.doc_string): + property_doc[property_name] = doc_string + if default_property_found and len(properties) > 1: + properties.insert(1, "*") + doc_string = clean_doc_string(element.doc_string) + documentation = "" + if doc_string: + lines = doc_string.splitlines() + documentation = f" \"\"\"{lines.pop(0)}\n" + while lines: + line = lines.pop(0) + documentation += f" {line}\n" if line else "\n" + if property_doc: + documentation += "\n Arguments:\n" + for property_name, doc_string in property_doc.items(): + lines = doc_string.splitlines() + documentation += f" {property_name}: {lines.pop(0)}\n" + while lines: + line = lines.pop(0) + if line: + documentation += f" {line}\n" + if documentation: + documentation += ' """\n' + print(f"def {element_name}({', '.join(properties)}):\n{documentation} ...\n\n", file=pyi_file) + if pyi_file: + pyi_file.close() + print(f"File '{pyi_path}' was updated.") # noqa: T201 + + +def main(arg_strings=None): + parser = argparse.ArgumentParser(description="taipy.gui.extensions entry point.") + sub_parser = parser.add_subparsers(dest="command", help="Commands to run", required=True) + + tgb_generation = sub_parser.add_parser( + "generate_tgb", aliases=["api"], help="Generate Page Builder API for a Taipy GUI extension package." + ) + tgb_generation.add_argument( + dest="package_root_dir", + nargs=1, + help="The root dir of the extension package." + " This directory must contain a __init__.py file.", + ) + tgb_generation.set_defaults(func=generate_tgb) + + args = parser.parse_args(arg_strings) + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index 0a7ba76084..40bccd89e9 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -43,6 +43,8 @@ def __init__( default_value: t.Optional[t.Any] = None, js_name: t.Optional[str] = None, with_update: t.Optional[bool] = None, + *, + doc_string: t.Optional[str] = None, ) -> None: """Initializes a new custom property declaration for an `Element^`. @@ -53,6 +55,8 @@ def __init__( If unspecified, a camel case version of `name` is generated: for example, if `name` is "my_property_name", then this property is referred to as "myPropertyName" in the JavaScript code. + doc_string: An optional string that holds documentation for that property.
+ This is used when generating the stub classes for extension libraries. """ self.default_value = default_value self.property_type: t.Union[PropertyType, t.Type[_TaipyBase]] @@ -66,6 +70,7 @@ def __init__( self.property_type = property_type self._js_name = js_name self.with_update = with_update + self.doc_string = doc_string super().__init__() def check(self, element_name: str, prop_name: str): @@ -103,8 +108,9 @@ def __init__( default_property: str, properties: t.Dict[str, ElementProperty], react_component: t.Optional[str] = None, + *, render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None, - inner_properties: t.Optional[t.Dict[str, ElementProperty]] = None, + doc_string: t.Optional[str] = None, ) -> None: """Initializes a new custom element declaration. @@ -112,21 +118,24 @@ def __init__( *react_component* is ignored. Arguments: - default_property (str): The name of the default property for this element. - properties (Dict[str, ElementProperty]): The dictionary containing the properties of this element, where the keys are the property names and the values are instances of ElementProperty. - inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties for this element.
- Default values are set/bound automatically. - react_component (Optional[str]): The name of the component to be created on the front-end.
+ default_property: The name of the default property for this element. + properties: The dictionary containing the properties of this element, where + the keys are the property names and the values are instances of ElementProperty. + react_component: The name of the component to be created on the front-end.
If not specified, it is set to a camel case version of the element's name ("one_name" is transformed to "OneName"). - render_xhtml (Optional[callable[[dict[str, Any]], str]]): A function that receives a - dictionary containing the element's properties and their values - and that must return a valid XHTML string. + render_xhtml: A function that receives a dictionary containing the element's properties and their values + and that must return a valid XHTML string.
+ This is used to implement static elements. + doc_string: The documentation text for this element or None if there is note, which is + the default.
+ This string is used when generating stub functions so elements of extension libraries + can be used with the Page Builder API. """ # noqa: E501 self.default_attribute = default_property self.attributes = properties - self.inner_properties = inner_properties self.js_name = react_component + self.doc_string = doc_string if callable(render_xhtml): self._render_xhtml = render_xhtml super().__init__() @@ -152,6 +161,9 @@ def check(self, name: str): def _is_server_only(self): return hasattr(self, "_render_xhtml") and callable(self._render_xhtml) + def _process_inner_properties(self, _attributes: t.Dict[str, t.Any], _counter: int): + pass + def _call_builder( self, name, @@ -162,40 +174,7 @@ def _call_builder( counter: int = 0, ) -> t.Union[t.Any, t.Tuple[str, str]]: attributes = properties if isinstance(properties, dict) else {} - if self.inner_properties: - uniques: t.Dict[str, int] = {} - self.attributes.update( - { - prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update) - for prop, attr in self.inner_properties.items() - } - ) - for prop, attr in self.inner_properties.items(): - val = attr.default_value - if val: - # handling property replacement in inner properties - while m := Element.__RE_PROP_VAR.search(val): - var = attributes.get(m.group(1)) - hash_value = None if var is None else gui._evaluate_expr(var) - if hash_value: - names = gui._get_real_var_name(hash_value) - hash_value = names[0] if isinstance(names, tuple) else names - else: - hash_value = "None" - val = val[: m.start()] + hash_value + val[m.end() :] - # handling unique id replacement in inner properties - has_uniq = False - while m := Element.__RE_UNIQUE_VAR.search(val): - has_uniq = True - id = uniques.get(m.group(1)) - if id is None: - id = len(uniques) + 1 - uniques[m.group(1)] = id - val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}" - if has_uniq and gui._is_expression(val): - gui._evaluate_expr(val, True) - - attributes[prop] = val + self._process_inner_properties(attributes, counter) # this modifies attributes hash_names = _Builder._get_variable_hash_names(gui, attributes) # variable replacement # call user render if any @@ -226,7 +205,7 @@ def _call_builder( gui=gui, control_type=name, element_name=f"{lib.get_js_module_name()}_{self._get_js_name(name)}", - attributes=properties, + prop_values=properties, hash_names=hash_names, lib_name=lib.get_name(), default_value=default_value, @@ -265,7 +244,7 @@ def get_elements(self) -> t.Dict[str, Element]: The default implementation returns an empty dictionary, indicating that this library contains no custom visual elements. """ - return {} + pass @abstractmethod def get_name(self) -> str: @@ -295,7 +274,7 @@ def get_name(self) -> str: because each JavaScript module will have to have a unique name. """ - raise NotImplementedError + pass def get_js_module_name(self) -> str: """ @@ -464,3 +443,66 @@ def get_version(self) -> t.Optional[str]: This version will be appended to the resource URL as a query arg (?v=) """ return None + + +class _Element_with_inner_props(Element): + def __init__( + self, + default_property: str, + properties: t.Dict[str, ElementProperty], + react_component: t.Optional[str] = None, + render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None, + doc_string: t.Optional[str] = None, + *, + inner_properties: t.Optional[t.Dict[str, ElementProperty]] = None, + ) -> None: + """NOT DOCUMENTED + + Arguments: + inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties for this element.
+ Default values are set/bound automatically. + """ + super().__init__( + default_property=default_property, + properties=properties, + react_component=react_component, + render_xhtml=render_xhtml, + doc_string=doc_string, + ) + self.inner_properties = inner_properties + + def _process_inner_properties(self, attributes: t.Dict[str, t.Any], counter: int): + if self.inner_properties: + uniques: t.Dict[str, int] = {} + self.attributes.update( + { + prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update) + for prop, attr in self.inner_properties.items() + } + ) + for prop, attr in self.inner_properties.items(): + val = attr.default_value + if val: + # handling property replacement in inner properties + while m := Element.__RE_PROP_VAR.search(val): + var = attributes.get(m.group(1)) + hash_value = None if var is None else gui._evaluate_expr(var) + if hash_value: + names = gui._get_real_var_name(hash_value) + hash_value = names[0] if isinstance(names, tuple) else names + else: + hash_value = "None" + val = val[: m.start()] + hash_value + val[m.end() :] + # handling unique id replacement in inner properties + has_uniq = False + while m := Element.__RE_UNIQUE_VAR.search(val): + has_uniq = True + id = uniques.get(m.group(1)) + if id is None: + id = len(uniques) + 1 + uniques[m.group(1)] = id + val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}" + if has_uniq and gui._is_expression(val): + gui._evaluate_expr(val, True) + + attributes[prop] = val diff --git a/taipy/gui/types.py b/taipy/gui/types.py index ae32cad644..ccb095666e 100644 --- a/taipy/gui/types.py +++ b/taipy/gui/types.py @@ -72,6 +72,14 @@ class PropertyType(Enum): See `ElementProperty^` for more details. """ + any = "any" + """ + The property holds a value of any serializable type. + """ + dynamic_any = "dynamicany" + """ + The property is dynamic and holds a value of any serializable type. + """ boolean = "boolean" """ The property holds a Boolean value. diff --git a/taipy/gui_core/_GuiCoreLib.py b/taipy/gui_core/_GuiCoreLib.py index 81bead0fd4..55491b14e7 100644 --- a/taipy/gui_core/_GuiCoreLib.py +++ b/taipy/gui_core/_GuiCoreLib.py @@ -15,6 +15,7 @@ from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, Task from taipy.gui import Gui, State from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType +from taipy.gui.extension.library import _Element_with_inner_props from ..version import _get_version from ._adapters import ( @@ -65,8 +66,8 @@ class _GuiCore(ElementLibrary): __DATANODE_SELECTOR_SORT_VAR = "__tpgc_dn_sort" __DATANODE_SELECTOR_ERROR_VAR = "__tpgc_dn_error" - __elements = { - "scenario_selector": Element( + __elements: dict[str, Element] = { + "scenario_selector": _Element_with_inner_props( "value", { "id": ElementProperty(PropertyType.string), @@ -113,7 +114,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "scenario": Element( + "scenario": _Element_with_inner_props( "scenario", { "id": ElementProperty(PropertyType.string), @@ -144,7 +145,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "scenario_dag": Element( + "scenario_dag": _Element_with_inner_props( "scenario", { "id": ElementProperty(PropertyType.string), @@ -161,7 +162,7 @@ class _GuiCore(ElementLibrary): "on_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.on_dag_select}}"), }, ), - "data_node_selector": Element( + "data_node_selector": _Element_with_inner_props( "value", { "id": ElementProperty(PropertyType.string), @@ -198,7 +199,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "data_node": Element( + "data_node": _Element_with_inner_props( __DATANODE_VIZ_DATA_NODE_PROP, { "id": ElementProperty(PropertyType.string), @@ -274,7 +275,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "job_selector": Element( + "job_selector": _Element_with_inner_props( "value", { "id": ElementProperty(PropertyType.string), From 1044cb12dc22f6a416c90b0c69094b3420249a61 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 12:43:55 +0100 Subject: [PATCH 02/13] Failing test --- taipy/gui/extension/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index 40bccd89e9..e588196554 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -161,7 +161,7 @@ def check(self, name: str): def _is_server_only(self): return hasattr(self, "_render_xhtml") and callable(self._render_xhtml) - def _process_inner_properties(self, _attributes: t.Dict[str, t.Any], _counter: int): + def _process_inner_properties(self, _gui: "Gui", _attributes: t.Dict[str, t.Any], _counter: int): pass def _call_builder( @@ -174,7 +174,7 @@ def _call_builder( counter: int = 0, ) -> t.Union[t.Any, t.Tuple[str, str]]: attributes = properties if isinstance(properties, dict) else {} - self._process_inner_properties(attributes, counter) + self._process_inner_properties(gui, attributes, counter) # this modifies attributes hash_names = _Builder._get_variable_hash_names(gui, attributes) # variable replacement # call user render if any @@ -471,7 +471,7 @@ def __init__( ) self.inner_properties = inner_properties - def _process_inner_properties(self, attributes: t.Dict[str, t.Any], counter: int): + def _process_inner_properties(self, gui: "Gui", attributes: t.Dict[str, t.Any], counter: int): if self.inner_properties: uniques: t.Dict[str, int] = {} self.attributes.update( From fb85c799c72ca67aff039bed65358550ade23c81 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 12:56:51 +0100 Subject: [PATCH 03/13] Linter --- taipy/gui/extension/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py index c9ab1673f6..ebf8c5200e 100644 --- a/taipy/gui/extension/__main__.py +++ b/taipy/gui/extension/__main__.py @@ -13,7 +13,7 @@ def error(message): exit(1) -def generate_tgb(args): +def generate_tgb(args) -> None: from importlib import import_module from inspect import getmembers, isclass @@ -60,7 +60,7 @@ def clean_doc_string(doc_string) -> t.Optional[str]: print("# Generated by taipy.gui.extension module", file=pyi_file) print("# ----------------------------------------------------------------------", file=pyi_file) for element_name, element in library.get_elements().items(): - properties = [] + properties: list[str] = [] property_doc = {} default_property_found = False for property_name, property in element.attributes.items(): @@ -106,7 +106,7 @@ def clean_doc_string(doc_string) -> t.Optional[str]: print(f"File '{pyi_path}' was updated.") # noqa: T201 -def main(arg_strings=None): +def main(arg_strings=None) -> None: parser = argparse.ArgumentParser(description="taipy.gui.extensions entry point.") sub_parser = parser.add_subparsers(dest="command", help="Commands to run", required=True) From 2cc327168ab20d3feeb26d180a1493d275a19c35 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 12:59:33 +0100 Subject: [PATCH 04/13] Linter(2) --- tests/gui/extension/test_library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/gui/extension/test_library.py b/tests/gui/extension/test_library.py index 6990aea427..b6b4468650 100644 --- a/tests/gui/extension/test_library.py +++ b/tests/gui/extension/test_library.py @@ -16,6 +16,7 @@ from taipy.gui import Gui from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType +from taipy.gui.extension.library import _Element_with_inner_props def render_xhtml_4_my_library(properties: t.Dict[str, t.Any]) -> str: @@ -53,7 +54,7 @@ class MyLibrary(ElementLibrary): "h1", render_xhtml=render_xhtml_4_my_library_fail, ), - "inner": Element( + "inner": _Element_with_inner_props( "value", {"value": ElementProperty(PropertyType.string, "")}, inner_properties={ From fdd1f0f823a489a30161afa70edb44e5a282f994 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 13:04:38 +0100 Subject: [PATCH 05/13] Linter(3) --- taipy/gui/extension/__main__.py | 7 ++++--- taipy/gui/extension/library.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py index ebf8c5200e..8762a11d87 100644 --- a/taipy/gui/extension/__main__.py +++ b/taipy/gui/extension/__main__.py @@ -4,16 +4,17 @@ import argparse import os -from taipy.gui.extension import ElementLibrary import typing as t +from taipy.gui.extension import ElementLibrary + def error(message): - print(message) + print(message) # noqa: T201 exit(1) -def generate_tgb(args) -> None: +def generate_tgb(args) -> None: # noqa: C901 from importlib import import_module from inspect import getmembers, isclass diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index e588196554..9a9148b2c9 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -459,7 +459,8 @@ def __init__( """NOT DOCUMENTED Arguments: - inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties for this element.
+ inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties + for this element.
Default values are set/bound automatically. """ super().__init__( From 70c6f33e522d7960f332d217026a704988f1e734 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 13:09:58 +0100 Subject: [PATCH 06/13] Linter(4) --- taipy/gui/_renderers/builder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index e825075850..d0cdaf1b98 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -253,7 +253,9 @@ def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: b else: self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}") - def __set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True): + def __set_number_attribute( + self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True + ): """ TODO-undocumented Defines a React number attribute (attr={}). @@ -1129,7 +1131,9 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 self.__update_vars.append(f"{prop_name}={hash_name}") self.__set_react_attribute(prop_name, hash_name) else: - self.__set_react_attribute(prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None))) + self.__set_react_attribute( + prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None)) + ) elif var_type == PropertyType.broadcast: self.__set_react_attribute( _to_camel_case(attr[0]), _get_broadcast_var_name(_get_tuple_val(attr, 2, None)) From 5453b05253aed187b6121dd2816a2e6c0f0dd9aa Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 5 Feb 2025 15:34:07 +0100 Subject: [PATCH 07/13] Typo --- taipy/gui/extension/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index 9a9148b2c9..b8c66b2ab0 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -127,7 +127,7 @@ def __init__( render_xhtml: A function that receives a dictionary containing the element's properties and their values and that must return a valid XHTML string.
This is used to implement static elements. - doc_string: The documentation text for this element or None if there is note, which is + doc_string: The documentation text for this element or None if there is none, which is the default.
This string is used when generating stub functions so elements of extension libraries can be used with the Page Builder API. From d997b9d5c7bbb5d641cd0a655aae65617e95028e Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Thu, 6 Feb 2025 10:39:49 +0100 Subject: [PATCH 08/13] Rename _Element_with_inner_props to _ElementWithInnerProps --- taipy/gui/extension/__main__.py | 2 +- taipy/gui/extension/library.py | 2 +- taipy/gui_core/_GuiCoreLib.py | 14 +++++++------- tests/gui/extension/test_library.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py index 8762a11d87..3923a71a8a 100644 --- a/taipy/gui/extension/__main__.py +++ b/taipy/gui/extension/__main__.py @@ -30,7 +30,7 @@ def generate_tgb(args) -> None: # noqa: C901 except Exception as e: error(f"Couldn't open module '{package_root_dir}' ({e})") library: t.Optional[ElementLibrary] = None - for _, member in getmembers(module, lambda o: isclass(o) and issubclass(o, ElementLibrary)): + for _, member in getmembers(module, lambda o: issubclass(o, ElementLibrary)): if library: error("Extension contains more than one ElementLibrary") library = member() diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index b8c66b2ab0..5efbb32515 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -445,7 +445,7 @@ def get_version(self) -> t.Optional[str]: return None -class _Element_with_inner_props(Element): +class _ElementWithInnerProps(Element): def __init__( self, default_property: str, diff --git a/taipy/gui_core/_GuiCoreLib.py b/taipy/gui_core/_GuiCoreLib.py index 55491b14e7..d48a16a6ea 100644 --- a/taipy/gui_core/_GuiCoreLib.py +++ b/taipy/gui_core/_GuiCoreLib.py @@ -15,7 +15,7 @@ from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, Task from taipy.gui import Gui, State from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType -from taipy.gui.extension.library import _Element_with_inner_props +from taipy.gui.extension.library import _ElementWithInnerProps from ..version import _get_version from ._adapters import ( @@ -67,7 +67,7 @@ class _GuiCore(ElementLibrary): __DATANODE_SELECTOR_ERROR_VAR = "__tpgc_dn_error" __elements: dict[str, Element] = { - "scenario_selector": _Element_with_inner_props( + "scenario_selector": _ElementWithInnerProps( "value", { "id": ElementProperty(PropertyType.string), @@ -114,7 +114,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "scenario": _Element_with_inner_props( + "scenario": _ElementWithInnerProps( "scenario", { "id": ElementProperty(PropertyType.string), @@ -145,7 +145,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "scenario_dag": _Element_with_inner_props( + "scenario_dag": _ElementWithInnerProps( "scenario", { "id": ElementProperty(PropertyType.string), @@ -162,7 +162,7 @@ class _GuiCore(ElementLibrary): "on_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.on_dag_select}}"), }, ), - "data_node_selector": _Element_with_inner_props( + "data_node_selector": _ElementWithInnerProps( "value", { "id": ElementProperty(PropertyType.string), @@ -199,7 +199,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "data_node": _Element_with_inner_props( + "data_node": _ElementWithInnerProps( __DATANODE_VIZ_DATA_NODE_PROP, { "id": ElementProperty(PropertyType.string), @@ -275,7 +275,7 @@ class _GuiCore(ElementLibrary): ), }, ), - "job_selector": _Element_with_inner_props( + "job_selector": _ElementWithInnerProps( "value", { "id": ElementProperty(PropertyType.string), diff --git a/tests/gui/extension/test_library.py b/tests/gui/extension/test_library.py index b6b4468650..0a3bf31b8d 100644 --- a/tests/gui/extension/test_library.py +++ b/tests/gui/extension/test_library.py @@ -16,7 +16,7 @@ from taipy.gui import Gui from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType -from taipy.gui.extension.library import _Element_with_inner_props +from taipy.gui.extension.library import _ElementWithInnerProps def render_xhtml_4_my_library(properties: t.Dict[str, t.Any]) -> str: @@ -54,7 +54,7 @@ class MyLibrary(ElementLibrary): "h1", render_xhtml=render_xhtml_4_my_library_fail, ), - "inner": _Element_with_inner_props( + "inner": _ElementWithInnerProps( "value", {"value": ElementProperty(PropertyType.string, "")}, inner_properties={ From 52ccc66915fca2ae93507b8525e7f96227dbef80 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Thu, 6 Feb 2025 11:33:09 +0100 Subject: [PATCH 09/13] Linter(5) --- taipy/gui/extension/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py index 3923a71a8a..006058a706 100644 --- a/taipy/gui/extension/__main__.py +++ b/taipy/gui/extension/__main__.py @@ -16,7 +16,7 @@ def error(message): def generate_tgb(args) -> None: # noqa: C901 from importlib import import_module - from inspect import getmembers, isclass + from inspect import getmembers from taipy.gui.types import PropertyType From 75ba5cbbfc2bb7b34baf3f2ae5441a8b2895d185 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Fri, 7 Feb 2025 08:21:45 +0100 Subject: [PATCH 10/13] Fix Elements with inner properties --- taipy/gui/_renderers/builder.py | 219 ++++++++++++++++---------------- taipy/gui/extension/library.py | 10 +- 2 files changed, 117 insertions(+), 112 deletions(-) diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index d0cdaf1b98..6f8568b0c7 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -218,9 +218,16 @@ def __set_any_attribute(self, name: str, default_value: t.Optional[str] = None): def __set_dynamic_any_attribute(self, name: str, default_value: t.Optional[str] = None): value = self.__prop_values.get(name, default_value) self.__set_json_attribute(_to_camel_case(f"default_{name}"), value) - if hash_name := self.__hashes.get(name): - self.__update_vars.append(f"{name}={hash_name}") - self.__set_react_attribute(name, hash_name) + + if hash := self.__hashes.get(name): + if isinstance(value, (dict, _MapDict)): + hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_dict) + react_name = _to_camel_case(name) + self.__update_vars.append(f"{react_name}={hash}") + self.__set_react_attribute(react_name, hash) + else: + self.__update_vars.append(f"{name}={hash}") + self.__set_react_attribute(name, hash) return self def __get_boolean_attribute(self, name: str, default_value=False): @@ -239,19 +246,19 @@ def __set_boolean_attribute(self, name: str, value: bool): return self.__set_react_attribute(_to_camel_case(name), value) def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True): - hash_name = self.__hashes.get(name) - val = self.__get_boolean_attribute(name, def_val) - default_name = f"default_{name}" if hash_name is not None else name - if val != def_val: - self.__set_boolean_attribute(default_name, val) - if hash_name is not None: - hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean) - self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name)) + value = self.__get_boolean_attribute(name, def_val) + hash = self.__hashes.get(name) + default_name = f"default_{name}" if hash is not None else name + if value != def_val: + self.__set_boolean_attribute(default_name, value) + if hash is not None: + hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_boolean) + self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash)) if with_update: if update_main: - self.__set_update_var_name(hash_name) + self.__set_update_var_name(hash) else: - self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}") + self.__update_vars.append(f"{_to_camel_case(name)}={hash}") def __set_number_attribute( self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True @@ -285,35 +292,34 @@ def __set_number_attribute( return self.__set_react_attribute(_to_camel_case(name), val) def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any): - hash_name = self.__hashes.get(var_name) - numVal = self.__prop_values.get(var_name) - if numVal is None: - numVal = default_value - if isinstance(numVal, str): + value = self.__prop_values.get(var_name) + if value is None: + value = default_value + if isinstance(value, str): try: - numVal = float(numVal) + value = float(value) except Exception as e: _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e) - numVal = 0 - if isinstance(numVal, numbers.Number): - self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal) - elif numVal is not None: - _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).") - if hash_name: - hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number) - self.__update_vars.append(f"{var_name}={hash_name}") - self.__set_react_attribute(var_name, hash_name) + value = 0 + if isinstance(value, numbers.Number): + self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), value) + elif value is not None: + _warn(f"{self.__element_name}: {var_name} value is not valid ({value}).") + if hash := self.__hashes.get(var_name): + hash = self.__get_typed_hash_name(hash, PropertyType.number) + self.__update_vars.append(f"{var_name}={hash}") + self.__set_react_attribute(var_name, hash) return self def __set_string_attribute( self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True ): - str_attr = self.__prop_values.get(name, default_value) - if str_attr is None: + value = self.__prop_values.get(name, default_value) + if value is None: if not optional: _warn(f"Property {name} is required for control {self.__control_type}.") return self - return self.set_attribute(_to_camel_case(name), str(str_attr)) + return self.set_attribute(_to_camel_case(name), str(value)) def __set_dynamic_string_attribute( self, @@ -322,60 +328,59 @@ def __set_dynamic_string_attribute( with_update: t.Optional[bool] = False, dynamic_property_name: t.Optional[str] = None, ): - str_val = self.__prop_values.get(name, default_value) - if str_val is not None: + value = self.__prop_values.get(name, default_value) + if value is not None: self.set_attribute( - _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(str_val) + _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(value) ) - if hash_name := self.__hashes.get(name): + if hash := self.__hashes.get(name): prop_name = _to_camel_case(name if dynamic_property_name is None else dynamic_property_name) if with_update: - self.__update_vars.append(f"{prop_name}={hash_name}") - self.__set_react_attribute(prop_name, hash_name) + self.__update_vars.append(f"{prop_name}={hash}") + self.__set_react_attribute(prop_name, hash) return self def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None): - attr = self.__prop_values.get(name, default_value) - if attr is None: + value = self.__prop_values.get(name, default_value) + if value is None: return self - if isinstance(attr, numbers.Number): - return self.__set_react_attribute(_to_camel_case(name), attr) + if isinstance(value, numbers.Number): + return self.__set_react_attribute(_to_camel_case(name), value) else: - return self.set_attribute(_to_camel_case(name), attr) + return self.set_attribute(_to_camel_case(name), value) def __set_dynamic_string_list(self, name: str, default_value: t.Any): - hash_name = self.__hashes.get(name) - loi = self.__prop_values.get(name) - if loi is None: - loi = default_value - if isinstance(loi, str): - loi = [s.strip() for s in loi.split(";") if s.strip()] - if isinstance(loi, list): - self.__set_json_attribute(_to_camel_case(f"default_{name}"), loi) - if hash_name: - self.__update_vars.append(f"{name}={hash_name}") - self.__set_react_attribute(name, hash_name) + value = self.__prop_values.get(name) + if value is None: + value = default_value + if isinstance(value, str): + value = [s.strip() for s in value.split(";") if s.strip()] + if isinstance(value, list): + self.__set_json_attribute(_to_camel_case(f"default_{name}"), value) + if hash := self.__hashes.get(name): + self.__update_vars.append(f"{name}={hash}") + self.__set_react_attribute(name, hash) return self def __set_list_attribute( self, name: str, - hash_name: t.Optional[str], - val: t.Any, + hash: t.Optional[str], + value: t.Any, elt_type: t.Type, dynamic=True, default_val: t.Optional[t.Any] = None, ) -> t.List[str]: - val = default_val if val is None else val - if not hash_name and isinstance(val, str): - val = [elt_type(t.strip()) for t in val.split(";")] - if isinstance(val, list): - if hash_name and dynamic: - self.__set_react_attribute(name, hash_name) - return [f"{name}={hash_name}"] + value = default_val if value is None else value + if not hash and isinstance(value, str): + value = [elt_type(t.strip()) for t in value.split(";")] + if isinstance(value, list): + if hash and dynamic: + self.__set_react_attribute(name, hash) + return [f"{name}={hash}"] else: - self.__set_json_attribute(name, val) - elif val is not None: + self.__set_json_attribute(name, value) + elif value is not None: _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.") return [] @@ -389,17 +394,17 @@ def __set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, name (str): The property name. default value (dict): used if no value is specified. """ - dict_attr = self.__prop_values.get(name) - if dict_attr is None: - dict_attr = default_value - if dict_attr is not None: - if isinstance(dict_attr, str): - vals = [x.strip().split(":") for x in dict_attr.split(";")] - dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} - if isinstance(dict_attr, (dict, _MapDict)): - self.__set_json_attribute(_to_camel_case(name), dict_attr) + value = self.__prop_values.get(name) + if value is None: + value = default_value + if value is not None: + if isinstance(value, str): + vals = [x.strip().split(":") for x in value.split(";")] + value = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} + if isinstance(value, (dict, _MapDict)): + self.__set_json_attribute(_to_camel_case(name), value) else: - _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") + _warn(f"{self.__element_name}: {name} should be a dict: '{str(value)}'.") return self def __set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None): @@ -412,53 +417,53 @@ def __set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Di name (str): The property name. default value (dict): used if no value is specified. """ - dict_attr = self.__prop_values.get(name) - if dict_attr is None: - dict_attr = default_value - if dict_attr is not None: - if isinstance(dict_attr, str): - vals = [x.strip().split(":") for x in dict_attr.split(";")] - dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} - if isinstance(dict_attr, (dict, _MapDict)): - self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr) + value = self.__prop_values.get(name) + if value is None: + value = default_value + if value is not None: + if isinstance(value, str): + vals = [x.strip().split(":") for x in value.split(";")] + value = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1} + if isinstance(value, (dict, _MapDict)): + self.__set_json_attribute(_to_camel_case("default_" + name), value) else: - _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.") - if dict_hash := self.__hashes.get(name): - dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict) + _warn(f"{self.__element_name}: {name} should be a dict: '{str(value)}'.") + if hash := self.__hashes.get(name): + hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_dict) prop_name = _to_camel_case(name) - self.__update_vars.append(f"{prop_name}={dict_hash}") - self.__set_react_attribute(prop_name, dict_hash) + self.__update_vars.append(f"{prop_name}={hash}") + self.__set_react_attribute(prop_name, hash) return self def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None): - date_attr = self.__prop_values.get(var_name, default_value) - if date_attr is None: - date_attr = default_value - if isinstance(date_attr, (datetime, date, time)): - value = _date_to_string(date_attr) + value = self.__prop_values.get(var_name, default_value) + if value is None: + value = default_value + if isinstance(value, (datetime, date, time)): + value = _date_to_string(value) self.set_attribute(_to_camel_case(var_name), value) return self def __set_function_attribute( self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True ): - str_attr = self.__prop_values.get(name, default_value) - if str_attr is None: + value = self.__prop_values.get(name, default_value) + if value is None: if not optional: _warn(f"Property {name} is required for control {self.__control_type}.") return self - elif _is_function(str_attr): - str_attr = self.__hashes.get(name) - if str_attr is None: + elif _is_function(value): + value = self.__hashes.get(name) + if value is None: return self - elif _is_boolean(str_attr) and not _is_true(t.cast(str, str_attr)): + elif _is_boolean(value) and not _is_true(t.cast(str, value)): return self.__set_react_attribute(_to_camel_case(name), False) - elif str_attr: - str_attr = str(str_attr) - func = self.__gui._get_user_function(str_attr) - if func == str_attr: - _warn(f"{self.__control_type}.{name}: {str_attr} is not a function.") - return self.set_attribute(_to_camel_case(name), str_attr) if str_attr else self + elif value: + value = str(value) + func = self.__gui._get_user_function(value) + if func == value: + _warn(f"{self.__control_type}.{name}: {value} is not a function.") + return self.set_attribute(_to_camel_case(name), value) if value else self 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)) + "!}") @@ -644,10 +649,10 @@ def _get_dataframe_attributes(self) -> "_Builder": self.__update_vars.append(f"comparedatas={','.join(cmp_datas_hash)}") cols_description = self.__gui._get_accessor().get_cols_description(data_hash, _TaipyData(data, data_hash)) col_dict = _get_columns_dict( - self.__attributes.get("columns", {}), + self.__prop_values.get("columns", {}), cols_description, date_format, - self.__attributes.get("number_format"), + self.__prop_values.get("number_format"), ) rebuild_fn_hash = self.__build_rebuild_fn( diff --git a/taipy/gui/extension/library.py b/taipy/gui/extension/library.py index 5efbb32515..9e1fe21210 100644 --- a/taipy/gui/extension/library.py +++ b/taipy/gui/extension/library.py @@ -100,9 +100,6 @@ class Element: what the default property name is. """ - __RE_PROP_VAR = re.compile(r"") - __RE_UNIQUE_VAR = re.compile(r"") - def __init__( self, default_property: str, @@ -446,6 +443,9 @@ def get_version(self) -> t.Optional[str]: class _ElementWithInnerProps(Element): + __RE_PROP_VAR = re.compile(r"") + __RE_UNIQUE_VAR = re.compile(r"") + def __init__( self, default_property: str, @@ -485,7 +485,7 @@ def _process_inner_properties(self, gui: "Gui", attributes: t.Dict[str, t.Any], val = attr.default_value if val: # handling property replacement in inner properties - while m := Element.__RE_PROP_VAR.search(val): + while m := _ElementWithInnerProps.__RE_PROP_VAR.search(val): var = attributes.get(m.group(1)) hash_value = None if var is None else gui._evaluate_expr(var) if hash_value: @@ -496,7 +496,7 @@ def _process_inner_properties(self, gui: "Gui", attributes: t.Dict[str, t.Any], val = val[: m.start()] + hash_value + val[m.end() :] # handling unique id replacement in inner properties has_uniq = False - while m := Element.__RE_UNIQUE_VAR.search(val): + while m := _ElementWithInnerProps.__RE_UNIQUE_VAR.search(val): has_uniq = True id = uniques.get(m.group(1)) if id is None: From 289c8acc800e16e2d12ee62004d3452d9c1f1e79 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Fri, 7 Feb 2025 10:21:29 +0100 Subject: [PATCH 11/13] A little bit more doc. --- taipy/gui/types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/taipy/gui/types.py b/taipy/gui/types.py index ccb095666e..27ebaf0bc5 100644 --- a/taipy/gui/types.py +++ b/taipy/gui/types.py @@ -117,6 +117,10 @@ class PropertyType(Enum): dynamic_list = "dynamiclist" """ The property is dynamic and holds a list. + + The React component must have two parameters: "" that must be a list of object, and + "default" that must be a string, set to the JSON representation of the initial value + of the property. """ dynamic_string = "dynamicstring" """ From 09f6f53fd40531918b4313bcabde5928d46b3741 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Fri, 7 Feb 2025 12:33:02 +0100 Subject: [PATCH 12/13] Added test on tgb api generation for extension libraries. --- taipy/gui/extension/__main__.py | 81 ++++++++++++++++++--------------- tests/gui/extension/test_tgb.py | 65 ++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 tests/gui/extension/test_tgb.py diff --git a/taipy/gui/extension/__main__.py b/taipy/gui/extension/__main__.py index 006058a706..6ff7cf2185 100644 --- a/taipy/gui/extension/__main__.py +++ b/taipy/gui/extension/__main__.py @@ -5,8 +5,9 @@ import argparse import os import typing as t +from io import StringIO -from taipy.gui.extension import ElementLibrary +from taipy.gui.extension import ElementLibrary, PropertyType def error(message): @@ -14,35 +15,8 @@ def error(message): exit(1) -def generate_tgb(args) -> None: # noqa: C901 - from importlib import import_module - from inspect import getmembers - - from taipy.gui.types import PropertyType - - package_root_dir = args.package_root_dir[0] - # Remove potential directory separator at the end of the package root dir - if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\": - package_root_dir = package_root_dir[:-1] - module = None - try: - module = import_module(package_root_dir) - except Exception as e: - error(f"Couldn't open module '{package_root_dir}' ({e})") - library: t.Optional[ElementLibrary] = None - for _, member in getmembers(module, lambda o: issubclass(o, ElementLibrary)): - if library: - error("Extension contains more than one ElementLibrary") - library = member() - if library is None: - error("Extension does not contain any ElementLibrary") - return - pyi_path = os.path.join(package_root_dir, "__init__.pyi") - pyi_file = None - try: - pyi_file = open(pyi_path, "w") - except Exception as e: - error(f"Couldn't open Python Interface Definition file '{pyi_file}' ({e})") +def generate_doc(library: ElementLibrary) -> str: # noqa: C901F + stream = StringIO() def clean_doc_string(doc_string) -> t.Optional[str]: if not doc_string: @@ -56,10 +30,9 @@ def clean_doc_string(doc_string) -> t.Optional[str]: lines.pop() return "\n".join(lines) if lines else None - print(f"Inspecting extension library '{library.get_name()}'") # noqa: T201 - print("# ----------------------------------------------------------------------", file=pyi_file) - print("# Generated by taipy.gui.extension module", file=pyi_file) - print("# ----------------------------------------------------------------------", file=pyi_file) + print("# ----------------------------------------------------------------------", file=stream) + print("# Generated by taipy.gui.extension module", file=stream) + print("# ----------------------------------------------------------------------", file=stream) for element_name, element in library.get_elements().items(): properties: list[str] = [] property_doc = {} @@ -86,7 +59,7 @@ def clean_doc_string(doc_string) -> t.Optional[str]: documentation = "" if doc_string: lines = doc_string.splitlines() - documentation = f" \"\"\"{lines.pop(0)}\n" + documentation = f' """{lines.pop(0)}\n' while lines: line = lines.pop(0) documentation += f" {line}\n" if line else "\n" @@ -101,8 +74,44 @@ def clean_doc_string(doc_string) -> t.Optional[str]: documentation += f" {line}\n" if documentation: documentation += ' """\n' - print(f"def {element_name}({', '.join(properties)}):\n{documentation} ...\n\n", file=pyi_file) + print(f"def {element_name}({', '.join(properties)}):\n{documentation} ...\n\n", file=stream) + + return stream.getvalue() + + +def generate_tgb(args) -> None: + from importlib import import_module + from inspect import getmembers, isclass + + package_root_dir = args.package_root_dir[0] + # Remove potential directory separator at the end of the package root dir + if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\": + package_root_dir = package_root_dir[:-1] + module = None + try: + module = import_module(package_root_dir) + except Exception as e: + error(f"Couldn't open module '{package_root_dir}' ({e})") + library: t.Optional[ElementLibrary] = None + for _, member in getmembers(module, lambda o: isclass(o) and issubclass(o, ElementLibrary)): + if library: + error("Extension contains more than one ElementLibrary") + library = member() + if library is None: + error("Extension does not contain any ElementLibrary") + return # To avoid having to deal with this case in the following code + pyi_path = os.path.join(package_root_dir, "__init__.pyi") + pyi_file = None + try: + pyi_file = open(pyi_path, "w") + except Exception as e: + error(f"Couldn't open Python Interface Definition file '{pyi_file}' ({e})") + + print(f"Inspecting extension library '{library.get_name()}'") # noqa: T201 + content = generate_doc(library) + if pyi_file: + print(content, file=pyi_file) pyi_file.close() print(f"File '{pyi_path}' was updated.") # noqa: T201 diff --git a/tests/gui/extension/test_tgb.py b/tests/gui/extension/test_tgb.py new file mode 100644 index 0000000000..eb7d919300 --- /dev/null +++ b/tests/gui/extension/test_tgb.py @@ -0,0 +1,65 @@ +# Copyright 2021-2025 Avaiga Private Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +import typing as t +from pathlib import Path + +from taipy.gui import Gui +from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType + + +class TgbLibrary(ElementLibrary): + elements = { + "e1": Element( + "s1", + { + "b1": ElementProperty(PropertyType.boolean, doc_string="e1.b1 doc"), + "b2": ElementProperty(PropertyType.dynamic_boolean), + "s1": ElementProperty(PropertyType.string), + "s2": ElementProperty(PropertyType.dynamic_string), + "d1": ElementProperty(PropertyType.dict), + "d2": ElementProperty(PropertyType.dynamic_dict), + }, + "E1", doc_string="e1 doc", + ), + "e2": Element( + "x", + { + "p1": ElementProperty(PropertyType.any), + "p2": ElementProperty(PropertyType.any), + }, + "E2", + ), + } + + def get_name(self) -> str: + return "test_ext_tgb" + + def get_elements(self) -> t.Dict[str, Element]: + return TgbLibrary.elements + + +def test_tgb_generation(gui: Gui, test_client, helpers): + from taipy.gui.extension.__main__ import generate_doc + + library = TgbLibrary() + api = generate_doc(library) + assert "def e1(" in api, "Missing element e1" + assert "s1" in api, "Missing property s1" + assert "s1: str" in api, "Incorrect property type for s1" + assert "(s1: str, *" in api, "Property s1 should be the default property" + assert "b1: bool" in api, "Missing or incorrect property type for b1" + assert "b2: bool" in api, "Missing or incorrect property type for b2" + assert "s2: str" in api, "Missing or incorrect property type for s2" + assert "d1: dict" in api, "Missing or incorrect property type for d2" + assert "d2: dict" in api, "Missing or incorrect property type for d2" + assert "e1 doc" in api, "Missing doc for e1" + assert "def e2(" in api, "Missing element e2" + assert "e2(p1, p2)" in api, "Wrong default property in e2" From 36c1700637d17d219fe49db4b60864c44885cb06 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Fri, 7 Feb 2025 12:35:03 +0100 Subject: [PATCH 13/13] Linter(6) --- tests/gui/extension/test_tgb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/gui/extension/test_tgb.py b/tests/gui/extension/test_tgb.py index eb7d919300..0b029e5250 100644 --- a/tests/gui/extension/test_tgb.py +++ b/tests/gui/extension/test_tgb.py @@ -9,7 +9,6 @@ # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. import typing as t -from pathlib import Path from taipy.gui import Gui from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType