Forráskód Böngészése

manage lambdas (#1838)

* manage lambdas
resolves #1832

* builder case

* comments for Fab

* Long's comment

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 7 hónapja
szülő
commit
e656520a86

+ 15 - 8
taipy/gui/_renderers/builder.py

@@ -28,6 +28,7 @@ from ..utils import (
     _get_client_var_name,
     _get_data_type,
     _get_expr_var_name,
+    _get_lambda_id,
     _getscopeattr,
     _getscopeattr_drill,
     _is_boolean,
@@ -153,18 +154,24 @@ class _Builder:
         hashes = {}
         # Bind potential function and expressions in self.attributes
         for k, v in attributes.items():
-            val = v
             hash_name = hash_names.get(k)
             if hash_name is None:
-                if callable(v):
-                    if v.__name__ == "<lambda>":
-                        hash_name = f"__lambda_{id(v)}"
-                        gui._bind_var_val(hash_name, v)
-                    else:
-                        hash_name = _get_expr_var_name(v.__name__)
-                elif isinstance(v, str):
+                if isinstance(v, str):
+                    looks_like_a_lambda = v.startswith("{lambda ") and v.endswith("}")
                     # need to unescape the double quotes that were escaped during preprocessing
                     (val, hash_name) = _Builder.__parse_attribute_value(gui, v.replace('\\"', '"'))
+                else:
+                    looks_like_a_lambda = False
+                    val = v
+                if callable(val):
+                    # if it's not a callable (and not a string), forget it
+                    if val.__name__ == "<lambda>":
+                        # if it is a lambda and it has already a hash_name, we're fine
+                        if looks_like_a_lambda or not hash_name:
+                            hash_name = _get_lambda_id(val)
+                            gui._bind_var_val(hash_name, val)  # type: ignore[arg-type]
+                    else:
+                        hash_name = _get_expr_var_name(val.__name__)
 
                 if val is not None or hash_name:
                     attributes[k] = val

+ 2 - 2
taipy/gui/builder/_element.py

@@ -121,10 +121,10 @@ class _Element(ABC):
                 return None
             args = [arg.arg for arg in lambda_fn.args.args]
             targets = [
-                compr.target.id  # type: ignore[attr-defined]
+                comprehension.target.id  # type: ignore[attr-defined]
                 for node in ast.walk(lambda_fn.body)
                 if isinstance(node, ast.ListComp)
-                for compr in node.generators
+                for comprehension in node.generators
             ]
             tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(lambda_fn)
             ast.fix_missing_locations(tree)

+ 21 - 21
taipy/gui/gui.py

@@ -813,17 +813,17 @@ class Gui:
             on_change_fn = self._get_user_function("on_change")
         if callable(on_change_fn):
             try:
-                argcount = on_change_fn.__code__.co_argcount
-                if argcount > 0 and inspect.ismethod(on_change_fn):
-                    argcount -= 1
-                args: t.List[t.Any] = [None for _ in range(argcount)]
-                if argcount > 0:
+                arg_count = on_change_fn.__code__.co_argcount
+                if arg_count > 0 and inspect.ismethod(on_change_fn):
+                    arg_count -= 1
+                args: t.List[t.Any] = [None for _ in range(arg_count)]
+                if arg_count > 0:
                     args[0] = self.__get_state()
-                if argcount > 1:
+                if arg_count > 1:
                     args[1] = var_name
-                if argcount > 2:
+                if arg_count > 2:
                     args[2] = value
-                if argcount > 3:
+                if arg_count > 3:
                     args[3] = current_context
                 on_change_fn(*args)
             except Exception as e:  # pragma: no cover
@@ -849,22 +849,22 @@ class Gui:
     def _get_user_content_url(
         self, path: t.Optional[str] = None, query_args: t.Optional[t.Dict[str, str]] = None
     ) -> t.Optional[str]:
-        qargs = query_args or {}
-        qargs.update({Gui.__ARG_CLIENT_ID: self._get_client_id()})
-        return f"/{Gui.__USER_CONTENT_URL}/{path or 'TaIpY'}?{urlencode(qargs)}"
+        q_args = query_args or {}
+        q_args.update({Gui.__ARG_CLIENT_ID: self._get_client_id()})
+        return f"/{Gui.__USER_CONTENT_URL}/{path or 'TaIpY'}?{urlencode(q_args)}"
 
     def __serve_user_content(self, path: str) -> t.Any:
         self.__set_client_id_in_context()
-        qargs: t.Dict[str, str] = {}
-        qargs.update(request.args)
-        qargs.pop(Gui.__ARG_CLIENT_ID, None)
+        q_args: t.Dict[str, str] = {}
+        q_args.update(request.args)
+        q_args.pop(Gui.__ARG_CLIENT_ID, None)
         cb_function: t.Optional[t.Union[t.Callable, str]] = None
         cb_function_name = None
-        if qargs.get(Gui._HTML_CONTENT_KEY):
+        if q_args.get(Gui._HTML_CONTENT_KEY):
             cb_function = self.__process_content_provider
             cb_function_name = cb_function.__name__
         else:
-            cb_function_name = qargs.get(Gui.__USER_CONTENT_CB)
+            cb_function_name = q_args.get(Gui.__USER_CONTENT_CB)
             if cb_function_name:
                 cb_function = self._get_user_function(cb_function_name)
                 if not callable(cb_function):
@@ -891,8 +891,8 @@ class Gui:
                 args: t.List[t.Any] = []
                 if path:
                     args.append(path)
-                if len(qargs):
-                    args.append(qargs)
+                if len(q_args):
+                    args.append(q_args)
                 ret = self._call_function_with_state(cb_function, args)
                 if ret is None:
                     _warn(f"{cb_function_name}() callback function must return a value.")
@@ -932,8 +932,8 @@ class Gui:
                 if libs is None:
                     libs = []
                     libraries[lib.get_name()] = libs
-                elts: t.List[t.Dict[str, str]] = []
-                libs.append({"js module": lib.get_js_module_name(), "elements": elts})
+                elements: t.List[t.Dict[str, str]] = []
+                libs.append({"js module": lib.get_js_module_name(), "elements": elements})
                 for element_name, elt in lib.get_elements().items():
                     if not isinstance(elt, Element):
                         continue
@@ -942,7 +942,7 @@ class Gui:
                         elt_dict["render function"] = elt._render_xhtml.__code__.co_name
                     else:
                         elt_dict["react name"] = elt._get_js_name(element_name)
-                    elts.append(elt_dict)
+                    elements.append(elt_dict)
         status.update({"libraries": libraries})
 
     def _serve_status(self, template: Path) -> t.Dict[str, t.Dict[str, str]]:

+ 1 - 0
taipy/gui/utils/__init__.py

@@ -17,6 +17,7 @@ from ._attributes import (
     _setscopeattr,
     _setscopeattr_drill,
 )
+from ._lambda import _get_lambda_id
 from ._locals_context import _LocalsContext
 from ._map_dict import _MapDict
 from ._runtime_manager import _RuntimeManager

+ 21 - 9
taipy/gui/utils/_evaluator.py

@@ -25,6 +25,7 @@ if t.TYPE_CHECKING:
 from . import (
     _get_client_var_name,
     _get_expr_var_name,
+    _get_lambda_id,
     _getscopeattr,
     _getscopeattr_drill,
     _hasscopeattr,
@@ -100,19 +101,23 @@ class _Evaluator:
             st = ast.parse('f"{' + e + '}"' if _Evaluator.__EXPR_EDGE_CASE_F_STRING.match(e) else e)
             args = [arg.arg for node in ast.walk(st) if isinstance(node, ast.arguments) for arg in node.args]
             targets = [
-                compr.target.id  # type: ignore[attr-defined]
+                comprehension.target.id  # type: ignore[attr-defined]
                 for node in ast.walk(st)
                 if isinstance(node, ast.ListComp)
-                for compr in node.generators
+                for comprehension in node.generators
             ]
+            functionsCalls = set()
             for node in ast.walk(st):
-                if isinstance(node, ast.Name):
+                if isinstance(node, ast.Call):
+                    functionsCalls.add(node.func)
+                elif isinstance(node, ast.Name):
                     var_name = node.id.split(sep=".")[0]
                     if var_name in builtin_vars:
-                        _warn(
-                            f"Variable '{var_name}' cannot be used in Taipy expressions "
-                            "as its name collides with a Python built-in identifier."
-                        )
+                        if node not in functionsCalls:
+                            _warn(
+                                f"Variable '{var_name}' cannot be used in Taipy expressions "
+                                "as its name collides with a Python built-in identifier."
+                            )
                     elif var_name not in args and var_name not in targets and var_name not in non_vars:
                         try:
                             if lazy_declare and var_name.startswith("__"):
@@ -136,6 +141,7 @@ class _Evaluator:
         expr_hash: t.Optional[str],
         expr_evaluated: t.Optional[t.Any],
         var_map: t.Dict[str, str],
+        lambda_expr: t.Optional[bool] = False,
     ):
         if expr in self.__expr_to_hash:
             expr_hash = self.__expr_to_hash[expr]
@@ -143,7 +149,8 @@ class _Evaluator:
             return expr_hash
         if expr_hash is None:
             expr_hash = _get_expr_var_name(expr)
-        else:
+        elif not lambda_expr:
+            # if lambda expr, it has a hasname, we work with that
             # edge case, only a single variable
             expr_hash = f"tpec_{_get_client_var_name(expr)}"
         self.__expr_to_hash[expr] = expr_hash
@@ -223,6 +230,9 @@ class _Evaluator:
     ) -> t.Any:
         if not self._is_expression(expr) and not lambda_expr:
             return expr
+        if not lambda_expr and expr.startswith("{lambda ") and expr.endswith("}"):
+            lambda_expr = True
+            expr = expr[1:-1]
         var_val, var_map = ({}, {}) if lambda_expr else self._analyze_expression(gui, expr, lazy_declare)
         expr_hash = None
         is_edge_case = False
@@ -252,8 +262,10 @@ class _Evaluator:
         except Exception as e:
             _warn(f"Cannot evaluate expression '{not_encoded_expr if is_edge_case else expr_string}'", e)
             expr_evaluated = None
+        if lambda_expr and callable(expr_evaluated):
+            expr_hash = _get_lambda_id(expr_evaluated)
         # save the expression if it needs to be re-evaluated
-        return self.__save_expression(gui, expr, expr_hash, expr_evaluated, var_map)
+        return self.__save_expression(gui, expr, expr_hash, expr_evaluated, var_map, lambda_expr)
 
     def refresh_expr(self, gui: Gui, var_name: str, holder: t.Optional[_TaipyBase]):
         """

+ 16 - 0
taipy/gui/utils/_lambda.py

@@ -0,0 +1,16 @@
+# Copyright 2021-2024 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.
+
+from types import LambdaType
+
+
+def _get_lambda_id(lambda_fn: LambdaType):
+    return f"__lambda_{id(lambda_fn)}"