Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

manage lambdas #1838

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions taipy/gui/_renderers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
_get_client_var_name,
_get_data_type,
_get_expr_var_name,
_get_lambda_id,
_getscopeattr,
_getscopeattr_drill,
_is_boolean,
Expand Down Expand Up @@ -153,18 +154,24 @@ def _get_variable_hash_names(
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("}")
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
# 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:
FabienLelaquais marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
4 changes: 2 additions & 2 deletions taipy/gui/builder/_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ def __parse_lambda_property(self, key: str, value: t.Any) -> t.Any:
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]
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down
42 changes: 21 additions & 21 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,17 +813,17 @@ def _call_on_change(self, var_name: str, value: t.Any, on_change: t.Optional[str
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
Expand All @@ -849,22 +849,22 @@ def __serve_content(self, path: str) -> t.Any:
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):
Expand All @@ -891,8 +891,8 @@ def __serve_user_content(self, path: str) -> t.Any:
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.")
Expand Down Expand Up @@ -932,8 +932,8 @@ def __append_libraries_to_status(self, status: t.Dict[str, t.Any]):
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
Expand All @@ -942,7 +942,7 @@ def __append_libraries_to_status(self, status: t.Dict[str, t.Any]):
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]]:
Expand Down
1 change: 1 addition & 0 deletions taipy/gui/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
_setscopeattr,
_setscopeattr_drill,
)
from ._lambda import _get_lambda_id
from ._locals_context import _LocalsContext
from ._map_dict import _MapDict
from ._runtime_manager import _RuntimeManager
Expand Down
30 changes: 21 additions & 9 deletions taipy/gui/utils/_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from . import (
_get_client_var_name,
_get_expr_var_name,
_get_lambda_id,
_getscopeattr,
_getscopeattr_drill,
_hasscopeattr,
Expand Down Expand Up @@ -100,19 +101,23 @@ def _analyze_expression(
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("__"):
Expand All @@ -136,14 +141,16 @@ def __save_expression(
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]
gui._bind_var_val(expr_hash, expr_evaluated)
return expr_hash
if expr_hash is None:
expr_hash = _get_expr_var_name(expr)
else:
elif not lambda_expr:
FabienLelaquais marked this conversation as resolved.
Show resolved Hide resolved
# 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
Expand Down Expand Up @@ -223,6 +230,9 @@ def evaluate_expr(
) -> 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
Expand Down Expand Up @@ -252,8 +262,10 @@ def evaluate_expr(
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]):
"""
Expand Down
16 changes: 16 additions & 0 deletions taipy/gui/utils/_lambda.py
Original file line number Diff line number Diff line change
@@ -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)}"
Loading