Browse Source

Merge branch 'feature/#1429-number-step-attribute' of github.com:Avaiga/taipy into feature/#1429-number-step-attribute

namnguyen 11 months ago
parent
commit
7134f6a960

+ 7 - 5
.github/workflows/frontend.yml

@@ -59,13 +59,15 @@ jobs:
       - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
       - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm run build --if-present
         run: npm run build --if-present
 
 
-      - run: npm test
+      - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        run: npm test
 
 
       - name: Code coverage
       - name: Code coverage
-        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request'
-        uses: artiomtr/jest-coverage-report-action@v2.2.6
+        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        uses: artiomtr/jest-coverage-report-action@v2.3.0
         with:
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
           github-token: ${{ secrets.GITHUB_TOKEN }}
           threshold: "80"
           threshold: "80"
-          working-directory: "frontend/taipy-gui"
-          skip-step: "install"
+          working-directory: "./frontend/taipy-gui"
+          skip-step: install
+          annotations: failed-tests

+ 1 - 0
Pipfile

@@ -42,6 +42,7 @@ numpy = "<2.0.0"
 freezegun = "*"
 freezegun = "*"
 ipython = "*"
 ipython = "*"
 ipykernel = "*"
 ipykernel = "*"
+markdownify = "*"
 mkdocs = "*"
 mkdocs = "*"
 mkdocs-autorefs = "*"
 mkdocs-autorefs = "*"
 mkdocs-include-markdown-plugin = "*"
 mkdocs-include-markdown-plugin = "*"

BIN
frontend/taipy-gui/public/favicon.ico


BIN
frontend/taipy-gui/public/favicon.png


+ 1 - 1
frontend/taipy-gui/public/index.html

@@ -5,7 +5,7 @@
     <meta charset="utf-8" />
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link rel="manifest" href="manifest.json" />
     <link rel="manifest" href="manifest.json" />
-    <link rel="icon" type="image/png" href="{{favicon}}" />
+    <link rel="icon" type="image/png" href="favicon.png" class="taipy-favicon" data-url="{{favicon}}" />
     <link rel="apple-touch-icon" href="favicon.png" />
     <link rel="apple-touch-icon" href="favicon.png" />
     <title>{{title}}</title>
     <title>{{title}}</title>
     <script>{%- if config -%}
     <script>{%- if config -%}

+ 4 - 1
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -22,7 +22,7 @@ import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
 import { getBaseURL, TIMEZONE_CLIENT } from "../utils";
 import { getBaseURL, TIMEZONE_CLIENT } from "../utils";
 import { parseData } from "../utils/dataFormat";
 import { parseData } from "../utils/dataFormat";
 import { MenuProps } from "../utils/lov";
 import { MenuProps } from "../utils/lov";
-import { getLocalStorageValue, IdMessage, storeClientId } from "./utils";
+import { changeFavicon, getLocalStorageValue, IdMessage, storeClientId } from "./utils";
 import { ligthenPayload, sendWsMessage, TAIPY_CLIENT_ID, WsMessage } from "./wsUtils";
 import { ligthenPayload, sendWsMessage, TAIPY_CLIENT_ID, WsMessage } from "./wsUtils";
 
 
 enum Types {
 enum Types {
@@ -228,6 +228,8 @@ const messageToAction = (message: WsMessage) => {
             return createPartialAction((message as unknown as Record<string, string>).name, true);
             return createPartialAction((message as unknown as Record<string, string>).name, true);
         } else if (message.type === "ACK") {
         } else if (message.type === "ACK") {
             return createAckAction((message as unknown as IdMessage).id);
             return createAckAction((message as unknown as IdMessage).id);
+        } else if (message.type === "FV") {
+            changeFavicon((message.payload as Record<string, string>)?.value);
         }
         }
     }
     }
     return {} as TaipyBaseAction;
     return {} as TaipyBaseAction;
@@ -278,6 +280,7 @@ export const initializeWebSocket = (socket: Socket | undefined, dispatch: Dispat
         socket.on("message", getWsMessageListener(dispatch));
         socket.on("message", getWsMessageListener(dispatch));
         // only now does the socket tries to open/connect
         // only now does the socket tries to open/connect
         socket.connect();
         socket.connect();
+        changeFavicon();
     }
     }
 };
 };
 
 

+ 20 - 0
frontend/taipy-gui/src/context/utils.ts

@@ -1,3 +1,4 @@
+import axios from "axios";
 import { TAIPY_CLIENT_ID } from "./wsUtils";
 import { TAIPY_CLIENT_ID } from "./wsUtils";
 
 
 export const getLocalStorageValue = <T = string>(key: string, defaultValue: T, values?: T[]) => {
 export const getLocalStorageValue = <T = string>(key: string, defaultValue: T, values?: T[]) => {
@@ -10,3 +11,22 @@ export const storeClientId = (id: string) => localStorage && localStorage.setIte
 export interface IdMessage {
 export interface IdMessage {
     id: string;
     id: string;
 }
 }
+
+export const changeFavicon = (url?: string) => {
+    const link: HTMLLinkElement | null = document.querySelector("link.taipy-favicon");
+    if (link) {
+        const { url: taipyUrl } = link.dataset;
+        const fetchUrl = url || (taipyUrl as string);
+        axios
+            .get(fetchUrl)
+            .then(() => {
+                link.href = fetchUrl;
+            })
+            .catch((error) => {
+                if (fetchUrl !== taipyUrl) {
+                    link.href = taipyUrl as string;
+                }
+                console.log(error);
+            });
+    }
+};

+ 2 - 1
frontend/taipy-gui/src/context/wsUtils.ts

@@ -20,7 +20,8 @@ export type WsMessageType =
     | "GMC"
     | "GMC"
     | "GDT"
     | "GDT"
     | "AID"
     | "AID"
-    | "GR";
+    | "GR"
+    | "FV";
 
 
 export interface WsMessage {
 export interface WsMessage {
     type: WsMessageType;
     type: WsMessageType;

+ 5 - 5
taipy/config/common/_template_handler.py

@@ -51,15 +51,15 @@ class _TemplateHandler:
                 if required:
                 if required:
                     raise MissingEnvVariableError(f"Environment variable {var} is not set.")
                     raise MissingEnvVariableError(f"Environment variable {var} is not set.")
                 return default
                 return default
-            if type == bool:
+            if type is bool:
                 return cls._to_bool(val)
                 return cls._to_bool(val)
-            elif type == int:
+            elif type is int:
                 return cls._to_int(val)
                 return cls._to_int(val)
-            elif type == float:
+            elif type is float:
                 return cls._to_float(val)
                 return cls._to_float(val)
-            elif type == Scope:
+            elif type is Scope:
                 return cls._to_scope(val)
                 return cls._to_scope(val)
-            elif type == Frequency:
+            elif type is Frequency:
                 return cls._to_frequency(val)
                 return cls._to_frequency(val)
             else:
             else:
                 if dynamic_type == "bool":
                 if dynamic_type == "bool":

+ 44 - 41
taipy/core/_entity/_migrate/_migrate_sql.py

@@ -13,6 +13,7 @@ import json
 import os
 import os
 import shutil
 import shutil
 import sqlite3
 import sqlite3
+from contextlib import closing
 from typing import Dict, Tuple
 from typing import Dict, Tuple
 
 
 from taipy.logger._taipy_logger import _TaipyLogger
 from taipy.logger._taipy_logger import _TaipyLogger
@@ -24,32 +25,34 @@ __logger = _TaipyLogger._get_logger()
 
 
 def _load_all_entities_from_sql(db_file: str) -> Tuple[Dict, Dict]:
 def _load_all_entities_from_sql(db_file: str) -> Tuple[Dict, Dict]:
     conn = sqlite3.connect(db_file)
     conn = sqlite3.connect(db_file)
-    query = "SELECT model_id, document FROM taipy_model"
-    query_version = "SELECT * FROM taipy_version"
-    cursor = conn.execute(query)
-    entities = {}
-    versions = {}
-
-    for row in cursor:
-        _id = row[0]
-        document = row[1]
-        entities[_id] = {"data": json.loads(document)}
-
-    cursor = conn.execute(query_version)
-    for row in cursor:
-        id = row[0]
-        config_id = row[1]
-        creation_date = row[2]
-        is_production = row[3]
-        is_development = row[4]
-        is_latest = row[5]
-        versions[id] = {
-            "config_id": config_id,
-            "creation_date": creation_date,
-            "is_production": is_production,
-            "is_development": is_development,
-            "is_latest": is_latest,
-        }
+    with closing(conn):
+        query = "SELECT model_id, document FROM taipy_model"
+        query_version = "SELECT * FROM taipy_version"
+        cursor = conn.execute(query)
+        entities = {}
+        versions = {}
+
+        for row in cursor:
+            _id = row[0]
+            document = row[1]
+            entities[_id] = {"data": json.loads(document)}
+
+        cursor = conn.execute(query_version)
+        for row in cursor:
+            id = row[0]
+            config_id = row[1]
+            creation_date = row[2]
+            is_production = row[3]
+            is_development = row[4]
+            is_latest = row[5]
+            versions[id] = {
+                "config_id": config_id,
+                "creation_date": creation_date,
+                "is_production": is_production,
+                "is_development": is_development,
+                "is_latest": is_latest,
+            }
+
     return entities, versions
     return entities, versions
 
 
 
 
@@ -123,21 +126,21 @@ def __insert_version(version: dict, conn):
 
 
 def __write_entities_to_sql(_entities: Dict, _versions: Dict, db_file: str):
 def __write_entities_to_sql(_entities: Dict, _versions: Dict, db_file: str):
     conn = sqlite3.connect(db_file)
     conn = sqlite3.connect(db_file)
-
-    for k, entity in _entities.items():
-        if "SCENARIO" in k:
-            __insert_scenario(entity["data"], conn)
-        elif "TASK" in k:
-            __insert_task(entity["data"], conn)
-        elif "DATANODE" in k:
-            __insert_datanode(entity["data"], conn)
-        elif "JOB" in k:
-            __insert_job(entity["data"], conn)
-        elif "CYCLE" in k:
-            __insert_cycle(entity["data"], conn)
-
-    for _, version in _versions.items():
-        __insert_version(version, conn)
+    with closing(conn):
+        for k, entity in _entities.items():
+            if "SCENARIO" in k:
+                __insert_scenario(entity["data"], conn)
+            elif "TASK" in k:
+                __insert_task(entity["data"], conn)
+            elif "DATANODE" in k:
+                __insert_datanode(entity["data"], conn)
+            elif "JOB" in k:
+                __insert_job(entity["data"], conn)
+            elif "CYCLE" in k:
+                __insert_cycle(entity["data"], conn)
+
+        for _, version in _versions.items():
+            __insert_version(version, conn)
 
 
 
 
 def _restore_migrate_sql_entities(path: str) -> bool:
 def _restore_migrate_sql_entities(path: str) -> bool:

+ 4 - 4
taipy/gui/data/array_dict_data_accessor.py

@@ -33,18 +33,18 @@ class _ArrayDictDataAccessor(_PandasDataAccessor):
             types = {type(x) for x in value}
             types = {type(x) for x in value}
             if len(types) == 1:
             if len(types) == 1:
                 type_elt = next(iter(types), None)
                 type_elt = next(iter(types), None)
-                if type_elt == list:
+                if type_elt is list:
                     lengths = {len(x) for x in value}
                     lengths = {len(x) for x in value}
                     return (
                     return (
                         pd.DataFrame(value)
                         pd.DataFrame(value)
                         if len(lengths) == 1
                         if len(lengths) == 1
                         else [pd.DataFrame({f"{i}/0": v}) for i, v in enumerate(value)]
                         else [pd.DataFrame({f"{i}/0": v}) for i, v in enumerate(value)]
                     )
                     )
-                elif type_elt == dict:
+                elif type_elt is dict:
                     return [pd.DataFrame(v) for v in value]
                     return [pd.DataFrame(v) for v in value]
-                elif type_elt == _MapDict:
+                elif type_elt is _MapDict:
                     return [pd.DataFrame(v._dict) for v in value]
                     return [pd.DataFrame(v._dict) for v in value]
-                elif type_elt == pd.DataFrame:
+                elif type_elt is pd.DataFrame:
                     return value
                     return value
 
 
             elif len(types) == 2 and list in types and pd.DataFrame in types:
             elif len(types) == 2 and list in types and pd.DataFrame in types:

+ 45 - 15
taipy/gui/gui.py

@@ -223,6 +223,7 @@ class Gui:
     __ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
     __ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
     __DOWNLOAD_ACTION = "__Taipy__download_csv"
     __DOWNLOAD_ACTION = "__Taipy__download_csv"
     __DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"
     __DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"
+    __DEFAULT_FAVICON_URL = "https://raw.githubusercontent.com/Avaiga/taipy-assets/develop/favicon.png"
 
 
     __RE_HTML = re.compile(r"(.*?)\.html$")
     __RE_HTML = re.compile(r"(.*?)\.html$")
     __RE_MD = re.compile(r"(.*?)\.md$")
     __RE_MD = re.compile(r"(.*?)\.md$")
@@ -322,6 +323,7 @@ class Gui:
         self.__evaluator: _Evaluator = None  # type: ignore
         self.__evaluator: _Evaluator = None  # type: ignore
         self.__adapter = _Adapter()
         self.__adapter = _Adapter()
         self.__directory_name_of_pages: t.List[str] = []
         self.__directory_name_of_pages: t.List[str] = []
+        self.__favicon: t.Optional[t.Union[str, Path]] = None
 
 
         # default actions
         # default actions
         self.on_action: t.Optional[t.Callable] = None
         self.on_action: t.Optional[t.Callable] = None
@@ -1117,9 +1119,7 @@ class Gui:
             # Get Module Context
             # Get Module Context
             if mc := self._get_page_context(page_path):
             if mc := self._get_page_context(page_path):
                 page_renderer = self._get_page(page_path)._renderer
                 page_renderer = self._get_page(page_path)._renderer
-                self._bind_custom_page_variables(
-                    page_renderer, self._get_client_id()
-                )
+                self._bind_custom_page_variables(page_renderer, self._get_client_id())
                 # get metadata if there is one
                 # get metadata if there is one
                 metadata: t.Dict[str, t.Any] = {}
                 metadata: t.Dict[str, t.Any] = {}
                 if hasattr(page_renderer, "_metadata"):
                 if hasattr(page_renderer, "_metadata"):
@@ -1227,7 +1227,7 @@ class Gui:
     def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
     def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
         try:
         try:
             to = list(self.__get_sids(client_id)) if client_id else []
             to = list(self.__get_sids(client_id)) if client_id else []
-            self._server._ws.emit("message", payload, to=to if to else None)
+            self._server._ws.emit("message", payload, to=to if to else None, include_self=True)
             time.sleep(0.001)
             time.sleep(0.001)
         except Exception as e:  # pragma: no cover
         except Exception as e:  # pragma: no cover
             _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
             _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
@@ -1315,9 +1315,19 @@ class Gui:
         else:
         else:
             self.__send_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})
             self.__send_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})
 
 
-    def __send_ws_broadcast(self, var_name: str, var_value: t.Any, client_id: t.Optional[str] = None):
+    def __send_ws_broadcast(
+        self,
+        var_name: str,
+        var_value: t.Any,
+        client_id: t.Optional[str] = None,
+        message_type: t.Optional[_WsType] = None,
+    ):
         self.__broadcast_ws(
         self.__broadcast_ws(
-            {"type": _WsType.UPDATE.value, "name": _get_broadcast_var_name(var_name), "payload": {"value": var_value}},
+            {
+                "type": _WsType.UPDATE.value if message_type is None else message_type.value,
+                "name": _get_broadcast_var_name(var_name),
+                "payload": {"value": var_value},
+            },
             client_id,
             client_id,
         )
         )
 
 
@@ -1977,7 +1987,13 @@ class Gui:
     def load_config(self, config: Config) -> None:
     def load_config(self, config: Config) -> None:
         self._config._load(config)
         self._config._load(config)
 
 
-    def _broadcast(self, name: str, value: t.Any, client_id: t.Optional[str] = None):
+    def _broadcast(
+        self,
+        name: str,
+        value: t.Any,
+        client_id: t.Optional[str] = None,
+        message_type: t.Optional[_WsType] = None,
+    ):
         """NOT DOCUMENTED
         """NOT DOCUMENTED
         Send the new value of a variable to all connected clients.
         Send the new value of a variable to all connected clients.
 
 
@@ -1986,7 +2002,7 @@ class Gui:
             value: The value (must be serializable to the JSON format).
             value: The value (must be serializable to the JSON format).
             client_id: The client id (broadcast to all client if None)
             client_id: The client id (broadcast to all client if None)
         """
         """
-        self.__send_ws_broadcast(name, value, client_id)
+        self.__send_ws_broadcast(name, value, client_id, message_type)
 
 
     def _broadcast_all_clients(self, name: str, value: t.Any):
     def _broadcast_all_clients(self, name: str, value: t.Any):
         try:
         try:
@@ -2411,7 +2427,7 @@ class Gui:
                 static_folder=_webapp_path,
                 static_folder=_webapp_path,
                 template_folder=_webapp_path,
                 template_folder=_webapp_path,
                 title=self._get_config("title", "Taipy App"),
                 title=self._get_config("title", "Taipy App"),
-                favicon=self._get_config("favicon", "favicon.png"),
+                favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
                 root_margin=self._get_config("margin", None),
                 root_margin=self._get_config("margin", None),
                 scripts=scripts,
                 scripts=scripts,
                 styles=styles,
                 styles=styles,
@@ -2440,8 +2456,7 @@ class Gui:
         async_mode: str = "gevent",
         async_mode: str = "gevent",
         **kwargs,
         **kwargs,
     ) -> t.Optional[Flask]:
     ) -> t.Optional[Flask]:
-        """
-        Start the server that delivers pages to web clients.
+        """Start the server that delivers pages to web clients.
 
 
         Once you enter `run()`, users can run web browsers and point to the web server
         Once you enter `run()`, users can run web browsers and point to the web server
         URL that `Gui` serves. The default is to listen to the *localhost* address
         URL that `Gui` serves. The default is to listen to the *localhost* address
@@ -2593,8 +2608,7 @@ class Gui:
         )
         )
 
 
     def reload(self):  # pragma: no cover
     def reload(self):  # pragma: no cover
-        """
-        Reload the web server.
+        """Reload the web server.
 
 
         This function reloads the underlying web server only in the situation where
         This function reloads the underlying web server only in the situation where
         it was run in a separated thread: the *run_in_thread* parameter to the
         it was run in a separated thread: the *run_in_thread* parameter to the
@@ -2607,8 +2621,7 @@ class Gui:
             _TaipyLogger._get_logger().info("Gui server has been reloaded.")
             _TaipyLogger._get_logger().info("Gui server has been reloaded.")
 
 
     def stop(self):
     def stop(self):
-        """
-        Stop the web server.
+        """Stop the web server.
 
 
         This function stops the underlying web server only in the situation where
         This function stops the underlying web server only in the situation where
         it was run in a separated thread: the *run_in_thread* parameter to the
         it was run in a separated thread: the *run_in_thread* parameter to the
@@ -2621,3 +2634,20 @@ class Gui:
 
 
     def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
     def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
         return contextlib.nullcontext()
         return contextlib.nullcontext()
+
+    def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
+        """Change the favicon for all clients.
+
+        This function dynamically changes the favicon of Taipy GUI pages for all connected client.
+        favicon_path can be an URL (relative or not) or a file path.
+        TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
+         the favicon when the application starts.
+
+        """
+        if state or self.__favicon != favicon_path:
+            if not state:
+                self.__favicon = favicon_path
+            url = self._get_content("__taipy_favicon", favicon_path, True)
+            self._broadcast(
+                "taipy_favicon", url, self._get_client_id() if state else None, message_type=_WsType.FAVICON
+            )

+ 1 - 1
taipy/gui/server.py

@@ -169,7 +169,7 @@ class _Server:
                     return render_template(
                     return render_template(
                         "index.html",
                         "index.html",
                         title=title,
                         title=title,
-                        favicon=favicon,
+                        favicon=f"{favicon}?version={version}",
                         root_margin=root_margin,
                         root_margin=root_margin,
                         watermark=watermark,
                         watermark=watermark,
                         config=client_config,
                         config=client_config,

+ 12 - 0
taipy/gui/state.py

@@ -13,6 +13,7 @@ import inspect
 import typing as t
 import typing as t
 from contextlib import nullcontext
 from contextlib import nullcontext
 from operator import attrgetter
 from operator import attrgetter
+from pathlib import Path
 from types import FrameType
 from types import FrameType
 
 
 from flask import has_app_context
 from flask import has_app_context
@@ -83,6 +84,7 @@ class State:
         "broadcast",
         "broadcast",
         "get_gui",
         "get_gui",
         "refresh",
         "refresh",
+        "set_favicon",
         "_set_context",
         "_set_context",
         "_notebook_context",
         "_notebook_context",
         "_get_placeholder",
         "_get_placeholder",
@@ -243,3 +245,13 @@ class State:
 
 
     def __exit__(self, exc_type, exc_value, traceback):
     def __exit__(self, exc_type, exc_value, traceback):
         return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)
         return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)
+
+    def set_favicon(self, favicon_path: t.Union[str, Path]):
+        """Change the favicon for the client of this state.
+
+        This function dynamically changes the favicon of Taipy GUI pages for a specific client.
+        favicon_path can be an URL (relative or not) or a file path.
+        TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
+         the favicon when the application starts.
+        """
+        super().__getattribute__(State.__gui_attr).set_favicon(favicon_path, self)

+ 1 - 0
taipy/gui/types.py

@@ -50,6 +50,7 @@ class _WsType(Enum):
     GET_MODULE_CONTEXT = "GMC"
     GET_MODULE_CONTEXT = "GMC"
     GET_DATA_TREE = "GDT"
     GET_DATA_TREE = "GDT"
     GET_ROUTES = "GR"
     GET_ROUTES = "GR"
+    FAVICON = "FV"
 
 
 
 
 NumberTypes = {"int", "int64", "float", "float64"}
 NumberTypes = {"int", "int64", "float", "float64"}

+ 1 - 0
taipy/gui_core/_context.py

@@ -125,6 +125,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.JOB:
         elif event.entity_type == EventEntityType.JOB:
             with self.lock:
             with self.lock:
                 self.jobs_list = None
                 self.jobs_list = None
+            self.broadcast_core_changed({"jobs": event.entity_id})
         elif event.entity_type == EventEntityType.SUBMISSION:
         elif event.entity_type == EventEntityType.SUBMISSION:
             self.submission_status_callback(event.entity_id, event)
             self.submission_status_callback(event.entity_id, event)
         elif event.entity_type == EventEntityType.DATA_NODE:
         elif event.entity_type == EventEntityType.DATA_NODE:

+ 0 - 2
tests/core/_entity/test_migrate_cli.py

@@ -12,7 +12,6 @@
 import filecmp
 import filecmp
 import os
 import os
 import shutil
 import shutil
-import sys
 from sqlite3 import OperationalError
 from sqlite3 import OperationalError
 from unittest.mock import patch
 from unittest.mock import patch
 
 
@@ -207,7 +206,6 @@ def test_migrate_sql_backup_and_remove(caplog, tmp_sqlite):
     assert not os.path.exists(backup_sqlite)
     assert not os.path.exists(backup_sqlite)
 
 
 
 
-@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on windows due to PermissionError: [WinError 32]")
 def test_migrate_sql_backup_and_restore(caplog, tmp_sqlite):
 def test_migrate_sql_backup_and_restore(caplog, tmp_sqlite):
     _MigrateCLI.create_parser()
     _MigrateCLI.create_parser()
 
 

+ 9 - 9
tests/core/config/test_file_config.py

@@ -193,9 +193,9 @@ def test_read_configuration_file():
     Config.override(file_config.filename)
     Config.override(file_config.filename)
 
 
     assert len(Config.data_nodes) == 4
     assert len(Config.data_nodes) == 4
-    assert type(Config.data_nodes["my_datanode"]) == DataNodeConfig
-    assert type(Config.data_nodes["my_datanode2"]) == DataNodeConfig
-    assert type(Config.data_nodes["my_datanode3"]) == DataNodeConfig
+    assert type(Config.data_nodes["my_datanode"]) is DataNodeConfig
+    assert type(Config.data_nodes["my_datanode2"]) is DataNodeConfig
+    assert type(Config.data_nodes["my_datanode3"]) is DataNodeConfig
     assert Config.data_nodes["my_datanode"].path == "/data/csv"
     assert Config.data_nodes["my_datanode"].path == "/data/csv"
     assert Config.data_nodes["my_datanode2"].path == "/data2/csv"
     assert Config.data_nodes["my_datanode2"].path == "/data2/csv"
     assert Config.data_nodes["my_datanode3"].path == "/data3/csv"
     assert Config.data_nodes["my_datanode3"].path == "/data3/csv"
@@ -206,27 +206,27 @@ def test_read_configuration_file():
     assert Config.data_nodes["my_datanode3"].source == "local"
     assert Config.data_nodes["my_datanode3"].source == "local"
 
 
     assert len(Config.tasks) == 2
     assert len(Config.tasks) == 2
-    assert type(Config.tasks["my_task"]) == TaskConfig
+    assert type(Config.tasks["my_task"]) is TaskConfig
     assert Config.tasks["my_task"].id == "my_task"
     assert Config.tasks["my_task"].id == "my_task"
     assert Config.tasks["my_task"].description == "task description"
     assert Config.tasks["my_task"].description == "task description"
     assert Config.tasks["my_task"].function == print
     assert Config.tasks["my_task"].function == print
     assert len(Config.tasks["my_task"].inputs) == 1
     assert len(Config.tasks["my_task"].inputs) == 1
-    assert type(Config.tasks["my_task"].inputs[0]) == DataNodeConfig
+    assert type(Config.tasks["my_task"].inputs[0]) is DataNodeConfig
     assert Config.tasks["my_task"].inputs[0].path == "/data/csv"
     assert Config.tasks["my_task"].inputs[0].path == "/data/csv"
     assert Config.tasks["my_task"].inputs[0].id == "my_datanode"
     assert Config.tasks["my_task"].inputs[0].id == "my_datanode"
     assert len(Config.tasks["my_task"].outputs) == 1
     assert len(Config.tasks["my_task"].outputs) == 1
-    assert type(Config.tasks["my_task"].outputs[0]) == DataNodeConfig
+    assert type(Config.tasks["my_task"].outputs[0]) is DataNodeConfig
     assert Config.tasks["my_task"].outputs[0].path == "/data2/csv"
     assert Config.tasks["my_task"].outputs[0].path == "/data2/csv"
     assert Config.tasks["my_task"].outputs[0].id == "my_datanode2"
     assert Config.tasks["my_task"].outputs[0].id == "my_datanode2"
 
 
     assert len(Config.scenarios) == 2
     assert len(Config.scenarios) == 2
-    assert type(Config.scenarios["my_scenario"]) == ScenarioConfig
+    assert type(Config.scenarios["my_scenario"]) is ScenarioConfig
     assert Config.scenarios["my_scenario"].id == "my_scenario"
     assert Config.scenarios["my_scenario"].id == "my_scenario"
     assert Config.scenarios["my_scenario"].owner == "John Doe"
     assert Config.scenarios["my_scenario"].owner == "John Doe"
     assert len(Config.scenarios["my_scenario"].tasks) == 1
     assert len(Config.scenarios["my_scenario"].tasks) == 1
-    assert type(Config.scenarios["my_scenario"].tasks[0]) == TaskConfig
+    assert type(Config.scenarios["my_scenario"].tasks[0]) is TaskConfig
     assert len(Config.scenarios["my_scenario"].additional_data_nodes) == 1
     assert len(Config.scenarios["my_scenario"].additional_data_nodes) == 1
-    assert type(Config.scenarios["my_scenario"].additional_data_nodes[0]) == DataNodeConfig
+    assert type(Config.scenarios["my_scenario"].additional_data_nodes[0]) is DataNodeConfig
     assert Config.scenarios["my_scenario"].tasks[0].id == "my_task"
     assert Config.scenarios["my_scenario"].tasks[0].id == "my_task"
     assert Config.scenarios["my_scenario"].tasks[0].description == "task description"
     assert Config.scenarios["my_scenario"].tasks[0].description == "task description"
     assert Config.scenarios["my_scenario"].additional_data_nodes[0].id == "my_datanode3"
     assert Config.scenarios["my_scenario"].additional_data_nodes[0].id == "my_datanode3"

+ 35 - 0
tests/gui/gui_specific/test_favicon.py

@@ -0,0 +1,35 @@
+# 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.
+
+import inspect
+import warnings
+
+from taipy.gui import Gui, Markdown
+
+
+def test_favicon(gui: Gui, helpers):
+
+    with warnings.catch_warnings(record=True):
+        gui._set_frame(inspect.currentframe())
+        gui.add_page("test", Markdown("#This is a page"))
+        gui.run(run_server=False)
+        client = gui._server.test_client()
+        # WS client and emit
+        ws_client = gui._server._ws.test_client(gui._server.get_flask())
+        # Get the jsx once so that the page will be evaluated -> variable will be registered
+        sid = helpers.create_scope_and_get_sid(gui)
+        client.get(f"/taipy-jsx/test/?client_id={sid}")
+        gui.set_favicon("https://newfavicon.com/favicon.png")
+        # assert for received message (message that would be sent to the front-end client)
+        msgs = ws_client.get_received()
+        assert msgs
+        assert msgs[0].get("args", {}).get("type", None) == "FV"
+        assert msgs[0].get("args", {}).get("payload", {}).get("value", None) == "https://newfavicon.com/favicon.png"

+ 2 - 3
tools/gui/builder/block.txt

@@ -2,8 +2,7 @@
 class {{name}}(_Block):
 class {{name}}(_Block):
     _ELEMENT_NAME: str
     _ELEMENT_NAME: str
     def __init__(self, {{properties}}) -> None:
     def __init__(self, {{properties}}) -> None:
-        """
-        Arguments:
-            {{doc_arguments}}
+        """### Arguments:
+{{doc_arguments}}
         """
         """
         ...
         ...

+ 2 - 3
tools/gui/builder/control.txt

@@ -2,8 +2,7 @@
 class {{name}}(_Control):
 class {{name}}(_Control):
     _ELEMENT_NAME: str
     _ELEMENT_NAME: str
     def __init__(self, {{properties}}) -> None:
     def __init__(self, {{properties}}) -> None:
-        """
-        Arguments:
-            {{doc_arguments}}
+        """### Arguments:
+{{doc_arguments}}
         """
         """
         ...
         ...

+ 31 - 4
tools/gui/generate_pyi.py

@@ -11,8 +11,11 @@
 
 
 import json
 import json
 import os
 import os
+import re
 import typing as t
 import typing as t
 
 
+from markdownify import markdownify
+
 # ############################################################
 # ############################################################
 # Generate Python interface definition files
 # Generate Python interface definition files
 # ############################################################
 # ############################################################
@@ -46,6 +49,19 @@ with open(gui_pyi_file, "r") as file:
 with open(gui_pyi_file, "w") as write_file:
 with open(gui_pyi_file, "w") as write_file:
     write_file.write(replaced_content)
     write_file.write(replaced_content)
 
 
+# ################
+# Read the version
+# ################
+current_version = "latest"
+with open("./taipy/gui/version.json", "r") as vfile:
+    version = json.load(vfile)
+    if "dev" in version.get("ext", ""):
+        current_version = "develop"
+    else:
+        current_version = f'release-{version.get("major", 0)}.{version.get("minor", 0)}'
+taipy_doc_url = f"https://docs.taipy.io/en/{current_version}/manuals/gui/viselements/"
+
+
 # ############################################################
 # ############################################################
 # Generate Page Builder pyi file (gui/builder/__init__.pyi)
 # Generate Page Builder pyi file (gui/builder/__init__.pyi)
 # ############################################################
 # ############################################################
@@ -80,11 +96,22 @@ def get_properties(element, viselements) -> t.List[t.Dict[str, t.Any]]:
     return properties
     return properties
 
 
 
 
-def build_doc(element: t.Dict[str, t.Any]):
+def build_doc(name: str, element: t.Dict[str, t.Any]):
     if "doc" not in element:
     if "doc" not in element:
         return ""
         return ""
     doc = str(element["doc"]).replace("\n", f'\n{16*" "}')
     doc = str(element["doc"]).replace("\n", f'\n{16*" "}')
-    return f"{element['name']} ({element['type']}): {doc} {'(default: '+element['default_value'] + ')' if 'default_value' in element else ''}"  # noqa: E501
+    doc = re.sub(
+        r"^(.*\..*\shref=\")([^h].*)(\".*\..*)$",
+        r"\1" + taipy_doc_url + name + r"/\2\3",
+        doc,
+    )
+    doc = re.sub(
+        r"^(.*\.)(<br/>|\s)(See below((?!href=).)*\.)(.*)$",
+        r"\1\3",
+        doc,
+    )
+    doc = markdownify(doc, strip=['br'])
+    return f"{element['name']} ({element['type']}): {doc} {'(default: '+markdownify(element['default_value']) + ')' if 'default_value' in element else ''}"  # noqa: E501
 
 
 
 
 for control_element in viselements["controls"]:
 for control_element in viselements["controls"]:
@@ -100,7 +127,7 @@ for control_element in viselements["controls"]:
             property_list.append(property)
             property_list.append(property)
             property_names.append(property["name"])
             property_names.append(property["name"])
     properties = ", ".join([f"{p} = ..." for p in property_names])
     properties = ", ".join([f"{p} = ..." for p in property_names])
-    doc_arguments = f"\n{12*' '}".join([build_doc(p) for p in property_list])
+    doc_arguments = "\n".join([build_doc(name, p) for p in property_list])
     # append properties to __init__.pyi
     # append properties to __init__.pyi
     with open(builder_pyi_file, "a") as file:
     with open(builder_pyi_file, "a") as file:
         file.write(
         file.write(
@@ -118,7 +145,7 @@ for block_element in viselements["blocks"]:
             property_list.append(property)
             property_list.append(property)
             property_names.append(property["name"])
             property_names.append(property["name"])
     properties = ", ".join([f"{p} = ..." for p in property_names])
     properties = ", ".join([f"{p} = ..." for p in property_names])
-    doc_arguments = f"{8*' '}".join([build_doc(p) for p in property_list])
+    doc_arguments = "\n".join([build_doc(name, p) for p in property_list])
     # append properties to __init__.pyi
     # append properties to __init__.pyi
     with open(builder_pyi_file, "a") as file:
     with open(builder_pyi_file, "a") as file:
         file.write(
         file.write(