Skip to content

Commit

Permalink
Merge branch 'develop' into feature/#1429-number-step-attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
namnguyen20999 authored Jul 4, 2024
2 parents 7acbe38 + 5380921 commit e57a274
Show file tree
Hide file tree
Showing 6 changed files with 896 additions and 7 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![Taipy Designer](https://github.com/nevo-david/taipy/assets/100117126/e787ba7b-ec7a-4d3f-a7e4-0f195daadce7)
](https://taipy.io/enterprise)
[![Taipy Designer banner](https://github.com/Avaiga/taipy/assets/31435778/6378ffd4-438a-498f-9385-10394f7d53fb)](https://links.taipy.io/306TwUH)


<div align="center">
<a href="https://taipy.io?utm_source=github" target="_blank">
Expand Down
1 change: 0 additions & 1 deletion taipy/gui/builder/_api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def create_element_api(
classname,
(ElementBaseClass,),
{
"__init__": lambda self, *args, **kwargs: ElementBaseClass.__init__(self, *args, **kwargs),
"_ELEMENT_NAME": element_name,
"_DEFAULT_PROPERTY": default_property,
},
Expand Down
71 changes: 67 additions & 4 deletions taipy/gui/builder/_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@

from __future__ import annotations

import ast
import copy
import inspect
import io
import re
import sys
import typing as t
from abc import ABC, abstractmethod
from collections.abc import Iterable
from types import FrameType, FunctionType

from .._warnings import _warn

if sys.version_info < (3, 9):
from ..utils.unparse import _Unparser
from ._context_manager import _BuilderContextManager
from ._factory import _BuilderFactory
from ._utils import _LambdaByName, _python_builtins, _TransformVarToValue

if t.TYPE_CHECKING:
from ..gui import Gui
Expand All @@ -30,6 +40,7 @@ class _Element(ABC):
_ELEMENT_NAME = ""
_DEFAULT_PROPERTY = ""
__RE_INDEXED_PROPERTY = re.compile(r"^(.*?)__([\w\d]+)$")
_NEW_LAMBDA_NAME = "new_lambda"

def __new__(cls, *args, **kwargs):
obj = super(_Element, cls).__new__(cls)
Expand All @@ -39,7 +50,12 @@ def __new__(cls, *args, **kwargs):
return obj

def __init__(self, *args, **kwargs) -> None:
self.__variables: t.Dict[str, FunctionType] = {}
self._properties: t.Dict[str, t.Any] = {}
self.__calling_frame = t.cast(
FrameType, t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back).f_back
)

if args and self._DEFAULT_PROPERTY != "":
self._properties = {self._DEFAULT_PROPERTY: args[0]}
self._properties.update(kwargs)
Expand All @@ -49,10 +65,10 @@ def update(self, **kwargs):
self._properties.update(kwargs)
self.parse_properties()

# Convert property value to string
# Convert property value to string/function
def parse_properties(self):
self._properties = {
_Element._parse_property_key(k): _Element._parse_property(v) for k, v in self._properties.items()
_Element._parse_property_key(k): self._parse_property(k, v) for k, v in self._properties.items()
}

# Get a deepcopy version of the properties
Expand All @@ -65,14 +81,58 @@ def _parse_property_key(key: str) -> str:
return f"{match.group(1)}[{match.group(2)}]"
return key

@staticmethod
def _parse_property(value: t.Any) -> t.Any:
def _parse_property(self, key: str, value: t.Any) -> t.Any:
if isinstance(value, (str, dict, Iterable)):
return value
if isinstance(value, FunctionType):
if key.startswith("on_"):
return value
else:
try:
st = ast.parse(inspect.getsource(value.__code__).strip())
lambda_by_name: t.Dict[str, ast.Lambda] = {}
_LambdaByName(self._ELEMENT_NAME, lambda_by_name).visit(st)
lambda_fn = lambda_by_name.get(
key,
lambda_by_name.get(_LambdaByName._DEFAULT_NAME, None)
if key == self._DEFAULT_PROPERTY
else None,
)
if lambda_fn is not None:
args = [arg.arg for arg in lambda_fn.args.args]
targets = [
compr.target.id # type: ignore[attr-defined]
for node in ast.walk(lambda_fn.body)
if isinstance(node, ast.ListComp)
for compr in node.generators
]
tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(
lambda_fn
)
ast.fix_missing_locations(tree)
if sys.version_info < (3, 9): # python 3.8 ast has no unparse
string_fd = io.StringIO()
_Unparser(tree, string_fd)
string_fd.seek(0)
lambda_text = string_fd.read()
else:
lambda_text = ast.unparse(tree)
new_code = compile(f"{_Element._NEW_LAMBDA_NAME} = {lambda_text}", "<ast>", "exec")
namespace: t.Dict[str, FunctionType] = {}
exec(new_code, namespace)
var_name = f"__lambda_{id(namespace[_Element._NEW_LAMBDA_NAME])}"
self.__variables[var_name] = namespace[_Element._NEW_LAMBDA_NAME]
return f'{{{var_name}({", ".join(args)})}}'
except Exception as e:
_warn("Error in lambda expression", e)
if hasattr(value, "__name__"):
return str(getattr(value, "__name__")) # noqa: B009
return str(value)

def _bind_variables(self, gui: "Gui"):
for var_name, var_value in self.__variables.items():
gui._bind_var_val(var_name, var_value)

@abstractmethod
def _render(self, gui: "Gui") -> str:
pass
Expand All @@ -99,6 +159,7 @@ def __exit__(self, exc_type, exc_value, traceback):
_BuilderContextManager().pop()

def _render(self, gui: "Gui") -> str:
self._bind_variables(gui)
el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
return f"{el[0]}{self._render_children(gui)}</{el[1]}>"

Expand Down Expand Up @@ -162,6 +223,7 @@ def __init__(self, *args, **kwargs):
self._content = args[1] if len(args) > 1 else ""

def _render(self, gui: "Gui") -> str:
self._bind_variables(gui)
if self._ELEMENT_NAME:
attrs = ""
if self._properties:
Expand All @@ -178,6 +240,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _render(self, gui: "Gui") -> str:
self._bind_variables(gui)
el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
return (
f"<div>{el[0]}</{el[1]}></div>"
Expand Down
63 changes: 63 additions & 0 deletions taipy/gui/builder/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 __future__ import annotations

import ast
import builtins
import typing as t
from operator import attrgetter
from types import FrameType

_python_builtins = dir(builtins)


def _get_value_in_frame(frame: FrameType, name: str):
if not frame:
return None
if name in frame.f_locals:
return frame.f_locals.get(name)
return _get_value_in_frame(t.cast(FrameType, frame.f_back), name)


class _TransformVarToValue(ast.NodeTransformer):
def __init__(self, frame: FrameType, non_vars: t.List[str]) -> None:
super().__init__()
self.frame = frame
self.non_vars = non_vars

def visit_Name(self, node):
var_parts = node.id.split(".", 2)
if var_parts[0] in self.non_vars:
return node
value = _get_value_in_frame(self.frame, var_parts[0])
if len(var_parts) > 1:
value = attrgetter(var_parts[1])(value)
return ast.Constant(value=value, kind=None)


class _LambdaByName(ast.NodeVisitor):
_DEFAULT_NAME = "<default>"

def __init__(self, element_name: str, lambdas: t.Dict[str, ast.Lambda]) -> None:
super().__init__()
self.element_name = element_name
self.lambdas = lambdas

def visit_Call(self, node):
if node.func.attr == self.element_name:
if self.lambdas.get(_LambdaByName._DEFAULT_NAME, None) is None:
self.lambdas[_LambdaByName._DEFAULT_NAME] = next(
(arg for arg in node.args if isinstance(arg, ast.Lambda)), None
)
for kwd in node.keywords:
if isinstance(kwd.value, ast.Lambda):
self.lambdas[kwd.arg] = kwd.value
Loading

0 comments on commit e57a274

Please sign in to comment.