Skip to content

Commit

Permalink
add context to lambda var (#1897)
Browse files Browse the repository at this point in the history
* add context to lambda var
resolves #1893

* "value" lambda

* lint

* fab's comment

---------

Co-authored-by: Fred Lefévère-Laoide <[email protected]>
  • Loading branch information
FredLL-Avaiga and Fred Lefévère-Laoide authored Oct 3, 2024
1 parent e999101 commit e15585e
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Data

# demo files
demo-*
demo_*/
demo_*
.airflow
*.dags
data_sources
Expand Down
15 changes: 9 additions & 6 deletions taipy/gui/_renderers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,29 +428,29 @@ def _get_lov_adapter( # noqa: C901
var_type = self.__gui._get_unique_type_adapter(type(elt).__name__)
if adapter is None:
adapter = self.__gui._get_adapter_for_type(var_type)
elif var_type == str.__name__ and callable(adapter):
elif var_type == str.__name__ and isroutine(adapter):
var_type += (
f"__lambda_{id(adapter)}"
_get_lambda_id(t.cast(LambdaType, adapter))
if adapter.__name__ == "<lambda>"
else _get_expr_var_name(adapter.__name__)
)
if lov_name:
if adapter is None:
adapter = self.__gui._get_adapter_for_type(lov_name)
else:
self.__gui._add_type_for_var(lov_name, var_type)
self.__gui._add_type_for_var(lov_name, t.cast(str, var_type))
if value_name := self.__hashes.get("value"):
if adapter is None:
adapter = self.__gui._get_adapter_for_type(value_name)
else:
self.__gui._add_type_for_var(value_name, var_type)
self.__gui._add_type_for_var(value_name, t.cast(str, var_type))
if adapter is not None:
self.__gui._add_adapter_for_type(var_type, adapter) # type: ignore

if default_lov is not None and lov:
for elt in lov:
ret = self.__gui._run_adapter(
t.cast(t.Callable, adapter), elt, adapter.__name__ if callable(adapter) else "adapter"
t.cast(t.Callable, adapter), elt, adapter.__name__ if isroutine(adapter) else "adapter"
) # type: ignore
if ret is not None:
default_lov.append(ret)
Expand All @@ -460,7 +460,10 @@ def _get_lov_adapter( # noqa: C901
val_list = value if isinstance(value, list) else [value]
for val in val_list:
ret = self.__gui._run_adapter(
t.cast(t.Callable, adapter), val, adapter.__name__ if callable(adapter) else "adapter", id_only=True
t.cast(t.Callable, adapter),
val,
adapter.__name__ if isroutine(adapter) else "adapter",
id_only=True,
) # type: ignore
if ret is not None:
ret_list.append(ret)
Expand Down
17 changes: 11 additions & 6 deletions taipy/gui/builder/_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
import inspect
import re
import typing as t
import uuid
from abc import ABC, abstractmethod
from collections.abc import Iterable
from types import FrameType, FunctionType

from .._warnings import _warn
from ..utils import _getscopeattr
from ..utils import _get_lambda_id, _getscopeattr
from ._context_manager import _BuilderContextManager
from ._factory import _BuilderFactory
from ._utils import _LambdaByName, _python_builtins, _TransformVarToValue
Expand All @@ -37,10 +36,10 @@ class _Element(ABC):
_ELEMENT_NAME = ""
_DEFAULT_PROPERTY = ""
__RE_INDEXED_PROPERTY = re.compile(r"^(.*?)__([\w\d]+)$")
_NEW_LAMBDA_NAME = "new_lambda"
_TAIPY_EMBEDDED_PREFIX = "_tp_embedded_"
_EMBEDDED_PROPERTIES = ["decimator"]
_TYPES: t.Dict[str, str] = {}
__LAMBDA_VALUE_IDX = 0

def __new__(cls, *args, **kwargs):
obj = super(_Element, cls).__new__(cls)
Expand All @@ -67,6 +66,12 @@ def update(self, **kwargs):
self._properties.update(kwargs)
self.parse_properties()

@staticmethod
def __get_lambda_index():
_Element.__LAMBDA_VALUE_IDX += 1
_Element.__LAMBDA_VALUE_IDX %= 0xFFFFFFF0
return _Element.__LAMBDA_VALUE_IDX

def _evaluate_lambdas(self, gui: Gui):
for k, lmbd in self._lambdas.items():
expr = gui._evaluate_expr(lmbd, lambda_expr=True)
Expand Down Expand Up @@ -100,8 +105,8 @@ def _parse_property(self, key: str, value: t.Any) -> t.Any:
if key.startswith("on_") or self._is_callable(key):
return value if value.__name__.startswith("<") else value.__name__
# Parse lambda function_is_callable
if (lambda_name := self.__parse_lambda_property(key, value)) is not None:
return lambda_name
if (lambda_call := self.__parse_lambda_property(key, value)) is not None:
return lambda_call
# Embed value in the caller frame
if not isinstance(value, str) and key in self._EMBEDDED_PROPERTIES:
return self.__embed_object(value, is_expression=False)
Expand Down Expand Up @@ -131,7 +136,7 @@ def __parse_lambda_property(self, key: str, value: t.Any) -> t.Any:
tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(lambda_fn)
ast.fix_missing_locations(tree)
lambda_text = ast.unparse(tree)
lambda_name = f"__lambda_{uuid.uuid4().hex}"
lambda_name = _get_lambda_id(value, index=(_Element.__get_lambda_index()))
self._lambdas[lambda_name] = lambda_text
return f'{{{lambda_name}({", ".join(args)})}}'
except Exception as e:
Expand Down
18 changes: 14 additions & 4 deletions taipy/gui/builder/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,28 @@ def __init__(self, element_name: str, lineno: int, lambdas: t.Dict[str, ast.Lamb

def visit_Call(self, node):
if getattr(node.func, "attr", None) == self.element_name:
if self.lambdas.get(_LambdaByName._DEFAULT_NAME, None) is None:
self.lambdas[_LambdaByName._DEFAULT_NAME] = next(
if self.lambdas.get(_LambdaByName._DEFAULT_NAME, None) is None and (
a_lambda := next(
(
arg
for arg in node.args
if isinstance(arg, ast.Lambda) and self.lineno >= arg.lineno and self.lineno <= arg.end_lineno
if isinstance(arg, ast.Lambda)
and arg.lineno is not None
and arg.end_lineno is not None
and self.lineno >= arg.lineno
and self.lineno <= arg.end_lineno
),
None,
)
):
self.lambdas[_LambdaByName._DEFAULT_NAME] = a_lambda

for kwd in node.keywords:
if (
isinstance(kwd.value, ast.Lambda)
kwd.arg is not None
and isinstance(kwd.value, ast.Lambda)
and kwd.value.lineno is not None
and kwd.value.end_lineno is not None
and self.lineno >= kwd.value.lineno
and self.lineno <= kwd.value.end_lineno
):
Expand Down
23 changes: 12 additions & 11 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import contextlib
import importlib
import inspect
import json
import math
import os
Expand All @@ -25,6 +24,7 @@
import warnings
from importlib import metadata, util
from importlib.util import find_spec
from inspect import currentframe, getabsfile, ismethod, ismodule, isroutine
from pathlib import Path
from threading import Timer
from types import FrameType, FunctionType, LambdaType, ModuleType, SimpleNamespace
Expand Down Expand Up @@ -81,6 +81,7 @@
_get_client_var_name,
_get_css_var_value,
_get_expr_var_name,
_get_lambda_id,
_get_module_name_from_frame,
_get_non_existent_file_path,
_get_page_from_module,
Expand Down Expand Up @@ -311,7 +312,7 @@ def __init__(
own Flask application instance and use it to serve the pages.
"""
# store suspected local containing frame
self.__frame = t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back)
self.__frame = t.cast(FrameType, t.cast(FrameType, currentframe()).f_back)
self.__default_module_name = _get_module_name_from_frame(self.__frame)
self._set_css_file(css_file)

Expand Down Expand Up @@ -824,7 +825,7 @@ def _call_on_change(self, var_name: str, value: t.Any, on_change: t.Optional[str
if callable(on_change_fn):
try:
arg_count = on_change_fn.__code__.co_argcount
if arg_count > 0 and inspect.ismethod(on_change_fn):
if arg_count > 0 and ismethod(on_change_fn):
arg_count -= 1
args: t.List[t.Any] = [None for _ in range(arg_count)]
if arg_count > 0:
Expand Down Expand Up @@ -1509,7 +1510,7 @@ def __call_function_with_args(self, **kwargs):
if callable(action_function):
try:
argcount = action_function.__code__.co_argcount
if argcount > 0 and inspect.ismethod(action_function):
if argcount > 0 and ismethod(action_function):
argcount -= 1
args = t.cast(list, [None for _ in range(argcount)])
if argcount > 0:
Expand All @@ -1532,7 +1533,7 @@ def _call_function_with_state(self, user_function: t.Callable, args: t.Optional[
cp_args = [] if args is None else args.copy()
cp_args.insert(0, self.__get_state())
argcount = user_function.__code__.co_argcount
if argcount > 0 and inspect.ismethod(user_function):
if argcount > 0 and ismethod(user_function):
argcount -= 1
if argcount > len(cp_args):
cp_args += (argcount - len(cp_args)) * [None]
Expand Down Expand Up @@ -2073,7 +2074,7 @@ def add_pages(self, pages: t.Optional[t.Union[t.Mapping[str, t.Union[str, Page]]
self.add_page(name=k, page=v)
elif isinstance(folder_name := pages, str):
if not hasattr(self, "_root_dir"):
self._root_dir = os.path.dirname(inspect.getabsfile(self.__frame))
self._root_dir = os.path.dirname(getabsfile(self.__frame))
folder_path = folder_name if os.path.isabs(folder_name) else os.path.join(self._root_dir, folder_name)
folder_name = os.path.basename(folder_path)
if not os.path.isdir(folder_path): # pragma: no cover
Expand Down Expand Up @@ -2224,9 +2225,9 @@ def _is_broadcasting(self) -> bool:
def _download(
self, content: t.Any, name: t.Optional[str] = "", on_action: t.Optional[t.Union[str, t.Callable]] = ""
):
if callable(on_action) and on_action.__name__:
if isroutine(on_action) and on_action.__name__:
on_action_name = (
_get_expr_var_name(str(on_action.__code__))
_get_lambda_id(t.cast(LambdaType, on_action))
if on_action.__name__ == "<lambda>"
else _get_expr_var_name(on_action.__name__)
)
Expand Down Expand Up @@ -2377,7 +2378,7 @@ def _call_on_page_load(self, page_name: str) -> None:
return
try:
arg_count = on_page_load_fn.__code__.co_argcount
if arg_count > 0 and inspect.ismethod(on_page_load_fn):
if arg_count > 0 and ismethod(on_page_load_fn):
arg_count -= 1
args: t.List[t.Any] = [None for _ in range(arg_count)]
if arg_count > 0:
Expand Down Expand Up @@ -2752,7 +2753,7 @@ def run(

app_config = self._config.config

run_root_dir = os.path.dirname(inspect.getabsfile(self.__frame))
run_root_dir = os.path.dirname(getabsfile(self.__frame))

# Register _root_dir for abs path
if not hasattr(self, "_root_dir"):
Expand Down Expand Up @@ -2797,7 +2798,7 @@ def run(
glob_ctx: t.Dict[str, t.Any] = {t.__name__: t for t in _TaipyBase.__subclasses__()}
glob_ctx[Gui.__SELF_VAR] = self
glob_ctx["state"] = self.__state
glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
glob_ctx.update({k: v for k, v in locals_bind.items() if ismodule(v) or callable(v)})

# Call on_init on each library
for name, libs in self.__extensions.items():
Expand Down
2 changes: 1 addition & 1 deletion taipy/gui/utils/_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def evaluate_expr(
_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)
expr_hash = _get_lambda_id(expr_evaluated, module=module_name)
# save the expression if it needs to be re-evaluated
return self.__save_expression(gui, expr, expr_hash, expr_evaluated, var_map, lambda_expr)

Expand Down
9 changes: 7 additions & 2 deletions taipy/gui/utils/_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
# 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 types import LambdaType

from ._variable_directory import _variable_encode

def _get_lambda_id(lambda_fn: LambdaType):
return f"__lambda_{id(lambda_fn)}"

def _get_lambda_id(lambda_fn: LambdaType, module: t.Optional[str] = None, index: t.Optional[int] = None):
return _variable_encode(
f"__lambda_{id(lambda_fn)}{f'_{index}' if index is not None else ''}", lambda_fn.__module__ or module
)

0 comments on commit e15585e

Please sign in to comment.