Przeglądaj źródła

Merge branch 'develop' into fix/remove-C405-rule

ooo oo 1 rok temu
rodzic
commit
3228828991

+ 2 - 0
Pipfile

@@ -34,6 +34,8 @@ toml = "==0.10"
 twisted = "==23.8.0"
 tzlocal = "==3.0"
 boto3 = "==1.29.1"
+watchdog = "==4.0.0"
+charset-normalizer = "==3.3.2"
 
 [dev-packages]
 freezegun = "*"

+ 26 - 12
frontend/taipy-gui/src/components/Taipy/Navigate.tsx

@@ -27,32 +27,46 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
     const { dispatch, state } = useContext(TaipyContext);
     const navigate = useNavigate();
     const location = useLocation();
+    const SPECIAL_PARAMS = ["tp_reload_all", "tp_reload_same_route_only", "tprh", "tp_cp_meta"];
 
     useEffect(() => {
         if (to) {
             const tos = to === "/" ? to : "/" + to;
-            const searchParams = new URLSearchParams(params || "");
-            // Handle Resource Handler Id
-            let tprh: string | null = null;
-            let meta: string | null = null;
-            if (searchParams.has("tprh")) {
-                tprh = searchParams.get("tprh");
-                searchParams.delete("tprh");
-                if (searchParams.has("tp_cp_meta")) {
-                    meta = searchParams.get("tp_cp_meta");
-                    searchParams.delete("tp_cp_meta");
+            const filteredParams = params
+                ? Object.keys(params).reduce((acc, key) => {
+                      if (!SPECIAL_PARAMS.includes(key)) {
+                          acc[key] = params[key];
+                      }
+                      return acc;
+                  }, {} as Record<string, string>)
+                : {};
+            const searchParams = new URLSearchParams(filteredParams);
+            // Special case for notebook reload
+            const reloadAll = params?.tp_reload_all === "true";
+            const reloadSameRouteOnly = params?.tp_reload_same_route_only === "true";
+            if (reloadAll) {
+                return navigate(0);
+            }
+            if (reloadSameRouteOnly) {
+                if (location.pathname === tos) {
+                    navigate(0);
                 }
+                return;
             }
+            // Regular navigate cases
             if (Object.keys(state.locations || {}).some((route) => tos === route)) {
                 const searchParamsLocation = new URLSearchParams(location.search);
                 if (force && location.pathname === tos && searchParamsLocation.toString() === searchParams.toString()) {
                     navigate(0);
                 } else {
                     navigate({ pathname: to, search: `?${searchParams.toString()}` });
-                    if (tprh !== null) {
+                    // Handle Resource Handler Id
+                    const tprh = params?.tprh;
+                    if (tprh !== undefined) {
                         // Add a session cookie for the resource handler id
                         document.cookie = `tprh=${tprh};path=/;`;
-                        if (meta !== null) {
+                        const meta = params?.tp_cp_meta;
+                        if (meta !== undefined) {
                             localStorage.setItem("tp_cp_meta", meta);
                         }
                         navigate(0);

+ 9 - 4
frontend/taipy-gui/src/components/pages/TaipyRendered.tsx

@@ -77,7 +77,8 @@ const TaipyRendered = (props: TaipyRenderedProps) => {
 
     const baseURL = getBaseURL();
     const pathname = baseURL == "/" ? location.pathname : location.pathname.replace(baseURL, "/");
-    const path = props.path || (state.locations && pathname in state.locations && state.locations[pathname]) || pathname;
+    const path =
+        props.path || (state.locations && pathname in state.locations && state.locations[pathname]) || pathname;
 
     useEffect(() => {
         // Fetch JSX Flask Backend Render
@@ -93,7 +94,7 @@ const TaipyRendered = (props: TaipyRenderedProps) => {
                 .then((result) => {
                     // set rendered JSX and CSS style from fetch result
                     if (typeof result.data.jsx === "string") {
-                        setPageState({module: result.data.context, jsx: result.data.jsx });
+                        setPageState({ module: result.data.context, jsx: result.data.jsx });
                     }
                     if (!fromBlock) {
                         setStyle("Taipy_style", result.data.style || "");
@@ -104,7 +105,9 @@ const TaipyRendered = (props: TaipyRenderedProps) => {
                     setPageState({
                         jsx: `<h1>${
                             error.response?.data ||
-                            `No data fetched from backend from ${path === "/TaiPy_root_page" ? baseURL : baseURL + path}`
+                            `No data fetched from backend from ${
+                                path === "/TaiPy_root_page" ? baseURL : baseURL + path
+                            }`
                         }</h1><br></br>${error}`,
                     })
                 );
@@ -115,7 +118,9 @@ const TaipyRendered = (props: TaipyRenderedProps) => {
     return (
         <ErrorBoundary FallbackComponent={ErrorFallback}>
             {head.length ? (
-                <Helmet>{head.map((v) => React.createElement(v.tag, v.props, v.content))}</Helmet>
+                <Helmet>
+                    {head.map((v, i) => React.createElement(v.tag, { key: `head${i}`, ...v.props }, v.content))}
+                </Helmet>
             ) : null}
             <PageContext.Provider value={pageState}>
                 <JsxParser

+ 40 - 10
taipy/gui/_renderers/__init__.py

@@ -13,12 +13,17 @@ import typing as t
 from abc import ABC, abstractmethod
 from os import path
 
+from charset_normalizer import detect
+
+from taipy.logger._taipy_logger import _TaipyLogger
+
 from ..page import Page
 from ..utils import _is_in_notebook, _varname_from_content
 from ._html import _TaipyHTMLParser
 
 if t.TYPE_CHECKING:
-    from ..builder._element import _Element
+    from watchdog.observers import BaseObserverSubclassCallable
+
     from ..gui import Gui
 
 
@@ -41,6 +46,7 @@ class _Renderer(Page, ABC):
         self._content = ""
         self._base_element: t.Optional[_Element] = None
         self._filepath = ""
+        self._observer: t.Optional["BaseObserverSubclassCallable"] = None
         if isinstance(content, str):
             self.__process_content(content)
         elif isinstance(content, _Element):
@@ -51,18 +57,37 @@ class _Renderer(Page, ABC):
             )
 
     def __process_content(self, content: str) -> None:
-        if path.exists(content) and path.isfile(content):
-            return self.__parse_file_content(content)
-        if self._frame is not None:
-            frame_dir_path = path.dirname(path.abspath(self._frame.f_code.co_filename))
-            content_path = path.join(frame_dir_path, content)
-            if path.exists(content_path) and path.isfile(content_path):
-                return self.__parse_file_content(content_path)
+        relative_file_path = (
+            None if self._frame is None else path.join(path.dirname(self._frame.f_code.co_filename), content)
+        )
+        if relative_file_path is not None and path.exists(relative_file_path) and path.isfile(relative_file_path):
+            content = relative_file_path
+        if content == relative_file_path or (path.exists(content) and path.isfile(content)):
+            self.__parse_file_content(content)
+            # Watchdog observer: watch for file changes
+            if _is_in_notebook() and self._observer is None:
+                self.__observe_file_change(content)
+            return
         self._content = content
 
+    def __observe_file_change(self, file_path: str):
+        from watchdog.observers import Observer
+
+        from .utils import FileWatchdogHandler
+
+        self._observer = Observer()
+        file_path = path.abspath(file_path)
+        self._observer.schedule(FileWatchdogHandler(file_path, self), path.dirname(file_path), recursive=False)
+        self._observer.start()
+
     def __parse_file_content(self, content):
-        with open(t.cast(str, content), "r") as f:
-            self._content = f.read()
+        with open(t.cast(str, content), "rb") as f:
+            file_content = f.read()
+            encoding = "utf-8"
+            if (detected_encoding := detect(file_content)["encoding"]) is not None:
+                encoding = detected_encoding
+                _TaipyLogger._get_logger().info(f"Detected '{encoding}' encoding for file '{content}'.")
+            self._content = file_content.decode(encoding)
             # Save file path for error handling
             self._filepath = content
 
@@ -85,6 +110,11 @@ class _Renderer(Page, ABC):
         if not _is_in_notebook():
             raise RuntimeError("'set_content()' must be used in an IPython notebook context")
         self.__process_content(content)
+        if self._notebook_gui is not None and self._notebook_page is not None:
+            if self._notebook_gui._config.root_page is self._notebook_page:
+                self._notebook_gui._navigate("/", {"tp_reload_all": "true"})
+                return
+            self._notebook_gui._navigate(self._notebook_page._route, {"tp_reload_same_route_only": "true"})
 
     def _get_content_detail(self, gui: "Gui") -> str:
         if self._filepath:

+ 25 - 0
taipy/gui/_renderers/utils.py

@@ -9,14 +9,22 @@
 # 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 datetime
 import typing as t
+from pathlib import Path
 
 import pandas as pd
+from watchdog.events import FileSystemEventHandler
+
+from taipy.logger._taipy_logger import _TaipyLogger
 
 from .._warnings import _warn
 from ..types import NumberTypes
 from ..utils import _RE_PD_TYPE, _get_date_col_str_name, _MapDict
 
+if t.TYPE_CHECKING:
+    from . import _Renderer
+
 
 def _add_to_dict_and_get(dico: t.Dict[str, t.Any], key: str, value: t.Any) -> t.Any:
     if key not in dico.keys():
@@ -117,3 +125,20 @@ def _get_columns_dict(  # noqa: C901
             elif number_format and ctype in NumberTypes:
                 _add_to_dict_and_get(col_dict[col], "format", number_format)
     return col_dict
+
+
+class FileWatchdogHandler(FileSystemEventHandler):
+    def __init__(self, file_path: str, renderer: "_Renderer") -> None:
+        self._file_path = file_path
+        self._renderer = renderer
+        self._last_modified = datetime.datetime.now()
+
+    def on_modified(self, event):
+        if datetime.datetime.now() - self._last_modified < datetime.timedelta(seconds=1):
+            return
+        self._last_modified = datetime.datetime.now()
+        if Path(event.src_path).resolve() == Path(self._file_path).resolve():
+            self._renderer.set_content(self._file_path)
+            _TaipyLogger._get_logger().info(
+                f"File '{self._file_path}' has been modified."
+            )

+ 7 - 0
taipy/gui/builder/_api_generator.py

@@ -83,6 +83,13 @@ class _ElementApiGenerator(object, metaclass=_Singleton):
                     element_name, f"{library_name}.{element_name}", element.default_attribute
                 ),
             )
+            # Allow element to be accessed from the root module
+            if hasattr(self.__module, element_name):
+                _TaipyLogger()._get_logger().info(
+                    f"Can't add element `{element_name}` of library `{library_name}` to the root of Builder API as another element with the same name already exists."  # noqa: E501
+                )
+                continue
+            setattr(self.__module, element_name, getattr(library_module, element_name))
 
     @staticmethod
     def create_block_api(

+ 4 - 0
taipy/gui/gui.py

@@ -1716,6 +1716,10 @@ class Gui:
         # Update variable directory
         if not page._is_class_module():
             self.__var_dir.add_frame(page._frame)
+        # Special case needed for page to access gui to trigger reload in notebook
+        if _is_in_notebook():
+            page._notebook_gui = self
+            page._notebook_page = new_page
 
     def add_pages(self, pages: t.Optional[t.Union[t.Mapping[str, t.Union[str, Page]], str]] = None) -> None:
         """Add several pages to the Graphical User Interface.

+ 8 - 2
taipy/gui/page.py

@@ -18,7 +18,8 @@ from types import FrameType
 from .utils import _filter_locals, _get_module_name_from_frame
 
 if t.TYPE_CHECKING:
-    from ._renderers import _Element  # noqa: F401
+    from ._page import _Page
+    from .gui import Gui
 
 
 class Page:
@@ -68,6 +69,9 @@ class Page:
                     cls_locals[f] = func.__func__
             self._class_module_name = cls.__name__
             self._class_locals = cls_locals
+        # Special variables only use for page reloading in notebook context
+        self._notebook_gui: t.Optional["Gui"] = None
+        self._notebook_page: t.Optional["_Page"] = None
 
     def create_page(self) -> t.Optional[Page]:
         """Create the page content for page modules.
@@ -88,7 +92,9 @@ class Page:
         return (
             self._class_locals
             if self._is_class_module()
-            else None if (frame := self._get_frame()) is None else _filter_locals(frame.f_locals)
+            else None
+            if (frame := self._get_frame()) is None
+            else _filter_locals(frame.f_locals)
         )
 
     def _is_class_module(self):

+ 2 - 0
tools/packages/pipfiles/Pipfile3.10.max

@@ -79,3 +79,5 @@ version = "==4.2.13"
 "marshmallow" = {version="==3.20.2"}
 "apispec" = {version="==6.4.0", extras=["yaml"]}
 "apispec-webframeworks" = {version="==1.0.0"}
+"watchdog" = {version="==4.0.0"}
+"charset-normalizer" = {version="==3.3.2"}

+ 2 - 0
tools/packages/pipfiles/Pipfile3.11.max

@@ -79,3 +79,5 @@ version = "==4.2.13"
 "marshmallow" = {version="==3.20.2"}
 "apispec" = {version="==6.4.0", extras=["yaml"]}
 "apispec-webframeworks" = {version="==1.0.0"}
+"watchdog" = {version="==4.0.0"}
+"charset-normalizer" = {version="==3.3.2"}

+ 2 - 0
tools/packages/pipfiles/Pipfile3.12.max

@@ -79,3 +79,5 @@ version = "==4.2.13"
 "marshmallow" = {version="==3.20.2"}
 "apispec" = {version="==6.4.0", extras=["yaml"]}
 "apispec-webframeworks" = {version="==1.0.0"}
+"watchdog" = {version="==4.0.0"}
+"charset-normalizer" = {version="==3.3.2"}

+ 2 - 0
tools/packages/pipfiles/Pipfile3.8.max

@@ -79,3 +79,5 @@ version = "==4.2.13"
 "marshmallow" = {version="==3.20.2"}
 "apispec" = {version="==6.4.0", extras=["yaml"]}
 "apispec-webframeworks" = {version="==1.0.0"}
+"watchdog" = {version="==4.0.0"}
+"charset-normalizer" = {version="==3.3.2"}

+ 2 - 0
tools/packages/pipfiles/Pipfile3.9.max

@@ -79,3 +79,5 @@ version = "==4.2.13"
 "marshmallow" = {version="==3.20.2"}
 "apispec" = {version="==6.4.0", extras=["yaml"]}
 "apispec-webframeworks" = {version="==1.0.0"}
+"watchdog" = {version="==4.0.0"}
+"charset-normalizer" = {version="==3.3.2"}

+ 2 - 0
tools/packages/taipy-gui/setup.requirements.txt

@@ -14,3 +14,5 @@ simple-websocket>=0.10.1,<=1.0.0
 taipy-config
 twisted>=23.8.0,<=23.10.0
 tzlocal>=3.0,<=5.2
+watchdog>=4.0.0,<=4.0.0
+charset-normalizer>=3.3.2,<=3.3.2