Skip to content

Commit

Permalink
Builder API improvement (Avaiga#921) (Avaiga#924)
Browse files Browse the repository at this point in the history
* make renderer private

* allow default property

* add html support

* add tests

* per Fred

* fix add_library invocation error

* simplify fixes
  • Loading branch information
dinhlongviolin1 authored Oct 3, 2023
1 parent 4fbff73 commit 0bfd34c
Show file tree
Hide file tree
Showing 32 changed files with 128 additions and 45 deletions.
2 changes: 1 addition & 1 deletion src/taipy/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from importlib.util import find_spec

from ._init import *
from ._renderers import Html, Markdown
from .gui_actions import (
download,
get_module_context,
Expand All @@ -87,7 +88,6 @@
from .icon import Icon
from .page import Page
from .partial import Partial
from .renderers import Html, Markdown
from .state import State
from .utils import is_debugging

Expand Down
2 changes: 1 addition & 1 deletion src/taipy/gui/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import warnings

if t.TYPE_CHECKING:
from ._renderers import Page
from .gui import Gui
from .renderers import Page


class _Page(object):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ElementApi,
_ElementApiContextManager,
_ElementApiGenerator,
html,
)
from ._html import _TaipyHTMLParser

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
# 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 .element_api import BlockElementApi, ControlElementApi, DefaultBlockElement, ElementApi
from .element_api import BlockElementApi, ControlElementApi, DefaultBlockElement, ElementApi, html
from .element_api_context_manager import _ElementApiContextManager
from .element_api_generator import _ElementApiGenerator
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

class ElementApi(ABC):
_ELEMENT_NAME = ""
_DEFAULT_PROPERTY = ""

def __new__(cls, *args, **kwargs):
obj = super(ElementApi, cls).__new__(cls)
Expand All @@ -32,8 +33,11 @@ def __new__(cls, *args, **kwargs):
parent.add(obj)
return obj

def __init__(self, **kwargs):
self._properties = kwargs
def __init__(self, *args, **kwargs):
self._properties: t.Dict[str, t.Any] = {}
if args and self._DEFAULT_PROPERTY != "":
self._properties = {self._DEFAULT_PROPERTY: args[0]}
self._properties.update(kwargs)
self.parse_properties()

def update(self, **kwargs):
Expand All @@ -58,8 +62,8 @@ def _render(self, gui: "Gui") -> str:


class BlockElementApi(ElementApi):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._children: t.List[ElementApi] = []

def add(self, *elements: ElementApi):
Expand All @@ -86,13 +90,27 @@ def _render_children(self, gui: "Gui") -> str:
class DefaultBlockElement(BlockElementApi):
_ELEMENT_NAME = "part"

def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class html(BlockElementApi):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not args:
raise RuntimeError("Can't render html element. Missing html tag name.")
self._ELEMENT_NAME = args[0]
self._content = args[1] if len(args) > 1 else ""

def _render(self, gui: "Gui") -> str:
open_tag_attributes = " ".join([f'{k}="{str(v)}"' for k, v in self._properties.items()])
open_tag = f"<{self._ELEMENT_NAME} {open_tag_attributes}>"
return f"{open_tag}{self._content}{self._render_children(gui)}</{self._ELEMENT_NAME}>"


class ControlElementApi(ElementApi):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _render(self, gui: "Gui") -> str:
el = _ClassApiFactory.create_element(gui, self._ELEMENT_NAME, self._properties)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class _ElementApiGenerator(object, metaclass=_Singleton):
def __init__(self):
self.__module: types.ModuleType | None = None

@staticmethod
def find_default_property(property_list: t.List[t.Dict[str, t.Any]]) -> str:
for property in property_list:
if "default_property" in property and property["default_property"] is True:
return property["name"]
return ""

def add_default(self):
current_frame = inspect.currentframe()
if current_frame is None:
Expand All @@ -44,14 +51,18 @@ def add_default(self):
if "blocks" not in data or "controls" not in data:
raise RuntimeError("Cannot generate Element API for the current module: Invalid viselements.json file.")
for blockElement in data["blocks"]:
default_property = _ElementApiGenerator.find_default_property(blockElement[1]["properties"])
setattr(
module, blockElement[0], _ElementApiGenerator.createBlockElement(blockElement[0], blockElement[0])
module,
blockElement[0],
_ElementApiGenerator.create_block_element(blockElement[0], blockElement[0], default_property),
)
for controlElement in data["controls"]:
default_property = _ElementApiGenerator.find_default_property(controlElement[1]["properties"])
setattr(
module,
controlElement[0],
_ElementApiGenerator.createControlElement(controlElement[0], controlElement[0]),
_ElementApiGenerator.create_control_element(controlElement[0], controlElement[0], default_property),
)

def add_library(self, library: "ElementLibrary"):
Expand All @@ -65,30 +76,44 @@ def add_library(self, library: "ElementLibrary"):
if library_module is None:
library_module = types.ModuleType(library_name)
setattr(self.__module, library_name, library_module)
for element_name in library.get_elements().keys():
for element_name, element in library.get_elements().items():
setattr(
library_module,
element_name,
_ElementApiGenerator().createControlElement(element_name, f"{library_name}.{element_name}"),
_ElementApiGenerator().create_control_element(
element_name, f"{library_name}.{element_name}", element.default_attribute
),
)

@staticmethod
def createBlockElement(classname: str, element_name: str):
return _ElementApiGenerator.createElementApi(classname, element_name, BlockElementApi)
def create_block_element(
classname: str,
element_name: str,
default_property: str,
):
return _ElementApiGenerator.create_element_api(classname, element_name, default_property, BlockElementApi)

@staticmethod
def createControlElement(classname: str, element_name: str):
return _ElementApiGenerator.createElementApi(classname, element_name, ControlElementApi)
def create_control_element(
classname: str,
element_name: str,
default_property: str,
):
return _ElementApiGenerator.create_element_api(classname, element_name, default_property, ControlElementApi)

@staticmethod
def createElementApi(
classname: str, element_name: str, ElementBaseClass: t.Union[t.Type[BlockElementApi], t.Type[ControlElementApi]]
def create_element_api(
classname: str,
element_name: str,
default_property: str,
ElementBaseClass: t.Union[t.Type[BlockElementApi], t.Type[ControlElementApi]],
):
return type(
classname,
(ElementBaseClass,),
{
"__init__": lambda self, **kwargs: ElementBaseClass.__init__(self, **kwargs),
"__init__": lambda self, *args, **kwargs: ElementBaseClass.__init__(self, *args, **kwargs),
"_ELEMENT_NAME": element_name,
"_DEFAULT_PROPERTY": default_property,
},
)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/taipy/gui/builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# 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 ..renderers import BlockElementApi, ControlElementApi, _ElementApiGenerator
from .._renderers import BlockElementApi, ControlElementApi, _ElementApiGenerator, html

# separate import for "Page" class so stubgen can properly generate pyi file
from .page import Page
Expand Down
2 changes: 1 addition & 1 deletion src/taipy/gui/builder/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# 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 ..renderers import ClassApi as Page
from .._renderers import ClassApi as Page
2 changes: 1 addition & 1 deletion src/taipy/gui/extension/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from pathlib import Path
from urllib.parse import urlencode

from .._renderers.builder import _Builder
from .._warnings import _warn
from ..renderers.builder import _Builder
from ..types import PropertyType
from ..utils import _get_broadcast_var_name, _TaipyBase, _to_camel_case

Expand Down
28 changes: 14 additions & 14 deletions src/taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@

from ._default_config import _default_stylekit, default_config
from ._page import _Page
from ._renderers import _EmptyPage
from ._renderers._markdown import _TaipyMarkdownExtension
from ._renderers.factory import _Factory
from ._renderers.json import _TaipyJsonEncoder
from ._renderers.utils import _get_columns_dict
from ._warnings import TaipyGuiWarning, _warn
from .builder import _ElementApiGenerator
from .config import Config, ConfigParameter, ServerConfig, Stylekit, _Config
from .data.content_accessor import _ContentAccessor
from .data.data_accessor import _DataAccessor, _DataAccessors
Expand All @@ -51,12 +57,6 @@
from .extension.library import Element, ElementLibrary
from .page import Page
from .partial import Partial
from .renderers import _EmptyPage
from .renderers._class_api.element_api_generator import _ElementApiGenerator
from .renderers._markdown import _TaipyMarkdownExtension
from .renderers.factory import _Factory
from .renderers.json import _TaipyJsonEncoder
from .renderers.utils import _get_columns_dict
from .server import _Server
from .state import State
from .types import _WsType
Expand Down Expand Up @@ -1245,19 +1245,19 @@ def _taipy_on_cancel_block_ui(guiApp, id: t.Optional[str], payload: t.Any):
return _taipy_on_cancel_block_ui

def __add_pages_in_folder(self, folder_name: str, folder_path: str):
from .renderers import Html, Markdown
from ._renderers import Html, Markdown

list_of_files = os.listdir(folder_path)
for file_name in list_of_files:
if file_name.startswith("__"):
continue
if (re_match := Gui.__RE_HTML.match(file_name)) and f"{re_match.group(1)}.py" not in list_of_files:
renderers = Html(os.path.join(folder_path, file_name), frame=None)
renderers.modify_taipy_base_url(folder_name)
self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=renderers)
_renderers = Html(os.path.join(folder_path, file_name), frame=None)
_renderers.modify_taipy_base_url(folder_name)
self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=_renderers)
elif (re_match := Gui.__RE_MD.match(file_name)) and f"{re_match.group(1)}.py" not in list_of_files:
renderers_md = Markdown(os.path.join(folder_path, file_name), frame=None)
self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=renderers_md)
_renderers_md = Markdown(os.path.join(folder_path, file_name), frame=None)
self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=_renderers_md)
elif re_match := Gui.__RE_PY.match(file_name):
module_name = re_match.group(1)
module_path = os.path.join(folder_name, module_name).replace(os.path.sep, ".")
Expand Down Expand Up @@ -1365,7 +1365,7 @@ def add_page(
if name in self._config.routes: # pragma: no cover
raise Exception(f'Page name "{name if name != Gui.__root_page_name else "/"}" is already defined.')
if isinstance(page, str):
from .renderers import Markdown
from ._renderers import Markdown

page = Markdown(page, frame=None)
elif not isinstance(page, Page): # pragma: no cover
Expand Down Expand Up @@ -1505,7 +1505,7 @@ def add_partial(
): # pragma: no cover
_warn(f'Partial name "{new_partial._route}" is already defined.')
if isinstance(page, str):
from .renderers import Markdown
from ._renderers import Markdown

page = Markdown(page, frame=None)
elif not isinstance(page, Page): # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion src/taipy/gui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .utils import _filter_locals, _get_module_name_from_frame

if t.TYPE_CHECKING:
from .renderers import ElementApi
from ._renderers import ElementApi


class Page:
Expand Down
2 changes: 1 addition & 1 deletion src/taipy/gui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@

from taipy.logger._taipy_logger import _TaipyLogger

from ._renderers.json import _TaipyJsonProvider
from .config import ServerConfig
from .renderers.json import _TaipyJsonProvider
from .utils import _is_in_notebook, _RuntimeManager
from .utils.proxy import NotebookProxy

Expand Down
2 changes: 1 addition & 1 deletion src/taipy/gui/utils/chart_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import typing as t
from enum import Enum

from .._renderers.utils import _get_columns_dict
from .._warnings import _warn
from ..renderers.utils import _get_columns_dict
from ..types import PropertyType
from ..utils import _MapDict

Expand Down
31 changes: 31 additions & 0 deletions tests/taipy/gui/builder/control/test_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2023 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 taipy.gui.builder as tgb
from taipy.gui import Gui


def test_html_builder(gui: Gui, test_client, helpers):
gui._bind_var_val("name", "World!")
gui._bind_var_val("btn_id", "button1")
with tgb.Page(frame=None) as page:
tgb.html("h1", "This is a header", style="color:Tomato;")
with tgb.html("p", "This is a paragraph.", style="color:green;"):
tgb.html("a", "a text", href="https://www.w3schools.com", target="_blank")
tgb.html("br")
tgb.html("b", "This is bold text inside the paragrah.")
expected_list = [
'<h1 style="color:Tomato;">This is a header',
'<p style="color:green;">This is a paragraph.',
'<a href="https://www.w3schools.com" target="_blank">a text',
"<br >",
"<b >This is bold text inside the paragrah.",
]
helpers.test_control_builder(gui, page, expected_list)
8 changes: 8 additions & 0 deletions tests/taipy/gui/builder/control/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ def test_text_builder_1(gui: Gui, test_client, helpers):
tgb.text(value="{x}")
expected_list = ["<Field", 'dataType="int"', 'defaultValue="10"', "value={tpec_TpExPr_x_TPMDL_0}"]
helpers.test_control_builder(gui, page, expected_list)


def test_text_builder_2(gui: Gui, test_client, helpers):
gui._bind_var_val("x", 10)
with tgb.Page(frame=None) as page:
tgb.text("{x}")
expected_list = ["<Field", 'dataType="int"', 'defaultValue="10"', "value={tpec_TpExPr_x_TPMDL_0}"]
helpers.test_control_builder(gui, page, expected_list)
4 changes: 2 additions & 2 deletions tests/taipy/gui/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
def pytest_configure(config):
if (find_spec("src") and find_spec("src.taipy")) and (not find_spec("taipy") or not find_spec("taipy.gui")):
import src.taipy.gui
import src.taipy.gui._renderers.builder
import src.taipy.gui._warnings
import src.taipy.gui.builder
import src.taipy.gui.data.decimator.lttb
Expand All @@ -30,13 +31,12 @@ def pytest_configure(config):
import src.taipy.gui.data.decimator.scatter_decimator
import src.taipy.gui.data.utils
import src.taipy.gui.extension
import src.taipy.gui.renderers.builder
import src.taipy.gui.utils._map_dict
import src.taipy.gui.utils._variable_directory
import src.taipy.gui.utils.expr_var_name

sys.modules["taipy.gui._warnings"] = sys.modules["src.taipy.gui._warnings"]
sys.modules["taipy.gui.renderers.builder"] = sys.modules["src.taipy.gui.renderers.builder"]
sys.modules["taipy.gui._renderers.builder"] = sys.modules["src.taipy.gui._renderers.builder"]
sys.modules["taipy.gui.utils._variable_directory"] = sys.modules["src.taipy.gui.utils._variable_directory"]
sys.modules["taipy.gui.utils.expr_var_name"] = sys.modules["src.taipy.gui.utils.expr_var_name"]
sys.modules["taipy.gui.utils._map_dict"] = sys.modules["src.taipy.gui.utils._map_dict"]
Expand Down
2 changes: 1 addition & 1 deletion tests/taipy/gui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
from types import FrameType

from taipy.gui import Gui, Html, Markdown
from taipy.gui._renderers.builder import _Builder
from taipy.gui._warnings import TaipyGuiWarning
from taipy.gui.renderers.builder import _Builder
from taipy.gui.utils._variable_directory import _reset_name_map
from taipy.gui.utils.expr_var_name import _reset_expr_var_name

Expand Down

0 comments on commit 0bfd34c

Please sign in to comment.