From ffc3b3256ff033fdb267d5fa0a7d987992e664d0 Mon Sep 17 00:00:00 2001 From: Toan Quach Date: Wed, 18 Sep 2024 14:30:55 +0700 Subject: [PATCH 01/10] Recreate the Core service --- taipy/core/_core.py | 28 ++++++++ taipy/core/_init.py | 1 + tests/core/test_core.py | 110 ++++-------------------------- tests/core/test_orchestrator.py | 116 ++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 taipy/core/_core.py create mode 100644 tests/core/test_orchestrator.py diff --git a/taipy/core/_core.py b/taipy/core/_core.py new file mode 100644 index 0000000000..c926517cad --- /dev/null +++ b/taipy/core/_core.py @@ -0,0 +1,28 @@ +# 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 taipy.logger._taipy_logger import _TaipyLogger + +from .common._warnings import _warn_deprecated +from .orchestrator import Orchestrator + + +class Core: + """Deprecated. Use `Orchestrator` class with `tp.Orchestrator()` instead.""" + + __logger = _TaipyLogger._get_logger() + + def __new__(cls) -> Orchestrator: + _warn_deprecated("'Core'", suggest="the 'Orchestrator' class") + cls.__logger.warning( + "Class `Core` has been deprecated, an object of class `Orchestrator` will be created instead." + ) + return Orchestrator() diff --git a/taipy/core/_init.py b/taipy/core/_init.py index bf1cc077f4..48f0a65929 100644 --- a/taipy/core/_init.py +++ b/taipy/core/_init.py @@ -9,6 +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 ._core import Core from ._entity.submittable import Submittable from .cycle.cycle import Cycle from .cycle.cycle_id import CycleId diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 87a3f0e1eb..a51e6aed6c 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -12,105 +12,19 @@ import pytest -from taipy.config import Config -from taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked -from taipy.core import Orchestrator -from taipy.core._orchestrator._dispatcher import _DevelopmentJobDispatcher, _StandaloneJobDispatcher -from taipy.core._orchestrator._orchestrator import _Orchestrator -from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory -from taipy.core.config.job_config import JobConfig -from taipy.core.exceptions.exceptions import OrchestratorServiceIsAlreadyRunning +from taipy.core import Core, Orchestrator -class TestOrchestrator: - def test_run_orchestrator_trigger_config_check(self, caplog): - Config.configure_data_node(id="d0", storage_type="toto") - with pytest.raises(SystemExit): - orchestrator = Orchestrator() - orchestrator.run() - expected_error_message = ( - "`storage_type` field of DataNodeConfig `d0` must be either csv, sql_table," - " sql, mongo_collection, pickle, excel, generic, json, parquet, s3_object, or in_memory." - ' Current value of property `storage_type` is "toto".' - ) - assert expected_error_message in caplog.text - orchestrator.stop() - - def test_run_orchestrator_as_a_service_development_mode(self): - _OrchestratorFactory._dispatcher = None - - orchestrator = Orchestrator() - assert orchestrator._orchestrator is None - assert orchestrator._dispatcher is None - assert _OrchestratorFactory._dispatcher is None - - orchestrator.run() - assert orchestrator._orchestrator is not None - assert orchestrator._orchestrator == _Orchestrator - assert _OrchestratorFactory._orchestrator is not None - assert _OrchestratorFactory._orchestrator == _Orchestrator - assert orchestrator._dispatcher is not None - assert isinstance(orchestrator._dispatcher, _DevelopmentJobDispatcher) - assert isinstance(_OrchestratorFactory._dispatcher, _DevelopmentJobDispatcher) - orchestrator.stop() - - def test_run_orchestrator_as_a_service_standalone_mode(self): - _OrchestratorFactory._dispatcher = None - - orchestrator = Orchestrator() - assert orchestrator._orchestrator is None - - assert orchestrator._dispatcher is None - assert _OrchestratorFactory._dispatcher is None - - Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2) - orchestrator.run() - assert orchestrator._orchestrator is not None - assert orchestrator._orchestrator == _Orchestrator - assert _OrchestratorFactory._orchestrator is not None - assert _OrchestratorFactory._orchestrator == _Orchestrator - assert orchestrator._dispatcher is not None - assert isinstance(orchestrator._dispatcher, _StandaloneJobDispatcher) - assert isinstance(_OrchestratorFactory._dispatcher, _StandaloneJobDispatcher) - assert orchestrator._dispatcher.is_running() - assert _OrchestratorFactory._dispatcher.is_running() - orchestrator.stop() +class TestCore: + def test_run_core_with_depracated_message(self, caplog): + with pytest.warns(DeprecationWarning): + core = Core() + core.run() - def test_orchestrator_service_can_only_be_run_once(self): - orchestrator_instance_1 = Orchestrator() - orchestrator_instance_2 = Orchestrator() - - orchestrator_instance_1.run() - - with pytest.raises(OrchestratorServiceIsAlreadyRunning): - orchestrator_instance_1.run() - with pytest.raises(OrchestratorServiceIsAlreadyRunning): - orchestrator_instance_2.run() - - # Stop the Orchestrator service and run it again should work - orchestrator_instance_1.stop() - - orchestrator_instance_1.run() - orchestrator_instance_1.stop() - orchestrator_instance_2.run() - orchestrator_instance_2.stop() - - def test_block_config_update_when_orchestrator_service_is_running_development_mode(self): - _OrchestratorFactory._dispatcher = None - - orchestrator = Orchestrator() - orchestrator.run() - with pytest.raises(ConfigurationUpdateBlocked): - Config.configure_data_node(id="i1") - orchestrator.stop() - - @pytest.mark.standalone - def test_block_config_update_when_orchestrator_service_is_running_standalone_mode(self): - _OrchestratorFactory._dispatcher = None + assert isinstance(core, Orchestrator) + expected_message = ( + "Class `Core` has been deprecated, an object of class `Orchestrator` will be created instead." + ) + assert expected_message in caplog.text - orchestrator = Orchestrator() - Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2) - orchestrator.run() - with pytest.raises(ConfigurationUpdateBlocked): - Config.configure_data_node(id="i1") - orchestrator.stop() + core.stop() diff --git a/tests/core/test_orchestrator.py b/tests/core/test_orchestrator.py new file mode 100644 index 0000000000..87a3f0e1eb --- /dev/null +++ b/tests/core/test_orchestrator.py @@ -0,0 +1,116 @@ +# 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 pytest + +from taipy.config import Config +from taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked +from taipy.core import Orchestrator +from taipy.core._orchestrator._dispatcher import _DevelopmentJobDispatcher, _StandaloneJobDispatcher +from taipy.core._orchestrator._orchestrator import _Orchestrator +from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory +from taipy.core.config.job_config import JobConfig +from taipy.core.exceptions.exceptions import OrchestratorServiceIsAlreadyRunning + + +class TestOrchestrator: + def test_run_orchestrator_trigger_config_check(self, caplog): + Config.configure_data_node(id="d0", storage_type="toto") + with pytest.raises(SystemExit): + orchestrator = Orchestrator() + orchestrator.run() + expected_error_message = ( + "`storage_type` field of DataNodeConfig `d0` must be either csv, sql_table," + " sql, mongo_collection, pickle, excel, generic, json, parquet, s3_object, or in_memory." + ' Current value of property `storage_type` is "toto".' + ) + assert expected_error_message in caplog.text + orchestrator.stop() + + def test_run_orchestrator_as_a_service_development_mode(self): + _OrchestratorFactory._dispatcher = None + + orchestrator = Orchestrator() + assert orchestrator._orchestrator is None + assert orchestrator._dispatcher is None + assert _OrchestratorFactory._dispatcher is None + + orchestrator.run() + assert orchestrator._orchestrator is not None + assert orchestrator._orchestrator == _Orchestrator + assert _OrchestratorFactory._orchestrator is not None + assert _OrchestratorFactory._orchestrator == _Orchestrator + assert orchestrator._dispatcher is not None + assert isinstance(orchestrator._dispatcher, _DevelopmentJobDispatcher) + assert isinstance(_OrchestratorFactory._dispatcher, _DevelopmentJobDispatcher) + orchestrator.stop() + + def test_run_orchestrator_as_a_service_standalone_mode(self): + _OrchestratorFactory._dispatcher = None + + orchestrator = Orchestrator() + assert orchestrator._orchestrator is None + + assert orchestrator._dispatcher is None + assert _OrchestratorFactory._dispatcher is None + + Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2) + orchestrator.run() + assert orchestrator._orchestrator is not None + assert orchestrator._orchestrator == _Orchestrator + assert _OrchestratorFactory._orchestrator is not None + assert _OrchestratorFactory._orchestrator == _Orchestrator + assert orchestrator._dispatcher is not None + assert isinstance(orchestrator._dispatcher, _StandaloneJobDispatcher) + assert isinstance(_OrchestratorFactory._dispatcher, _StandaloneJobDispatcher) + assert orchestrator._dispatcher.is_running() + assert _OrchestratorFactory._dispatcher.is_running() + orchestrator.stop() + + def test_orchestrator_service_can_only_be_run_once(self): + orchestrator_instance_1 = Orchestrator() + orchestrator_instance_2 = Orchestrator() + + orchestrator_instance_1.run() + + with pytest.raises(OrchestratorServiceIsAlreadyRunning): + orchestrator_instance_1.run() + with pytest.raises(OrchestratorServiceIsAlreadyRunning): + orchestrator_instance_2.run() + + # Stop the Orchestrator service and run it again should work + orchestrator_instance_1.stop() + + orchestrator_instance_1.run() + orchestrator_instance_1.stop() + orchestrator_instance_2.run() + orchestrator_instance_2.stop() + + def test_block_config_update_when_orchestrator_service_is_running_development_mode(self): + _OrchestratorFactory._dispatcher = None + + orchestrator = Orchestrator() + orchestrator.run() + with pytest.raises(ConfigurationUpdateBlocked): + Config.configure_data_node(id="i1") + orchestrator.stop() + + @pytest.mark.standalone + def test_block_config_update_when_orchestrator_service_is_running_standalone_mode(self): + _OrchestratorFactory._dispatcher = None + + orchestrator = Orchestrator() + Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE, max_nb_of_workers=2) + orchestrator.run() + with pytest.raises(ConfigurationUpdateBlocked): + Config.configure_data_node(id="i1") + orchestrator.stop() From 1071eaa2f8cf453bd29ee6219a0ea527d267e83c Mon Sep 17 00:00:00 2001 From: Toan Quach Date: Wed, 18 Sep 2024 14:37:50 +0700 Subject: [PATCH 02/10] make linter stop complaining --- taipy/core/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/core/_core.py b/taipy/core/_core.py index c926517cad..4057b197be 100644 --- a/taipy/core/_core.py +++ b/taipy/core/_core.py @@ -20,7 +20,7 @@ class Core: __logger = _TaipyLogger._get_logger() - def __new__(cls) -> Orchestrator: + def __new__(cls) -> Orchestrator: # type: ignore _warn_deprecated("'Core'", suggest="the 'Orchestrator' class") cls.__logger.warning( "Class `Core` has been deprecated, an object of class `Orchestrator` will be created instead." From d608cf9ad4477d5eb98dd8ae661fa8e697e5c87f Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Thu, 19 Sep 2024 00:43:27 +0700 Subject: [PATCH 03/10] reformat export decouple (#1802) --- .../base/src/packaging/taipy-gui-base.d.ts | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 2df8b76559..ee9a76411c 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -1,4 +1,4 @@ -import { Socket } from 'socket.io-client'; +import { Socket } from "socket.io-client"; export type ModuleData = Record; export type VarName = Record; @@ -48,10 +48,7 @@ declare class DataManager { constructor(variableModuleData: ModuleData); init(variableModuleData: ModuleData): ModuleData; getEncodedName(varName: string, module: string): string | undefined; - getName(encodedName: string): [ - string, - string - ] | undefined; + getName(encodedName: string): [string, string] | undefined; get(encodedName: string, dataEventKey?: string): unknown; addRequestDataOptions(encodedName: string, dataEventKey: string, options: RequestDataOptions): void; getInfo(encodedName: string): VarData | undefined; @@ -60,7 +57,25 @@ declare class DataManager { update(encodedName: string, value: unknown, dataEventKey?: string): void; deleteRequestedData(encodedName: string, dataEventKey: string): void; } -export type WsMessageType = "A" | "U" | "DU" | "MU" | "RU" | "AL" | "BL" | "NA" | "ID" | "MS" | "DF" | "PR" | "ACK" | "GMC" | "GDT" | "AID" | "GR" | "FV"; +export type WsMessageType = + | "A" + | "U" + | "DU" + | "MU" + | "RU" + | "AL" + | "BL" + | "NA" + | "ID" + | "MS" + | "DF" + | "PR" + | "ACK" + | "GMC" + | "GDT" + | "AID" + | "GR" + | "FV"; export interface WsMessage { type: WsMessageType | string; name: string; @@ -76,11 +91,13 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; -export type Route = [ - string, - string -]; -export type RequestDataCallback = (taipyApp: TaipyApp, encodedName: string, dataEventKey: string, value: unknown) => void; +export type Route = [string, string]; +export type RequestDataCallback = ( + taipyApp: TaipyApp, + encodedName: string, + dataEventKey: string, + value: unknown, +) => void; export declare class TaipyApp { socket: Socket; _onInit: OnInitHandler | undefined; @@ -100,7 +117,12 @@ export declare class TaipyApp { path: string | undefined; routes: Route[] | undefined; wsAdapters: WsAdapter[]; - constructor(onInit?: OnInitHandler | undefined, onChange?: OnChangeHandler | undefined, path?: string | undefined, socket?: Socket | undefined); + constructor( + onInit?: OnInitHandler | undefined, + onChange?: OnChangeHandler | undefined, + path?: string | undefined, + socket?: Socket | undefined, + ); get onInit(): OnInitHandler | undefined; set onInit(handler: OnInitHandler | undefined); onInitEvent(): void; @@ -123,10 +145,7 @@ export declare class TaipyApp { sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void; registerWsAdapter(wsAdapter: WsAdapter): void; getEncodedName(varName: string, module: string): string | undefined; - getName(encodedName: string): [ - string, - string - ] | undefined; + getName(encodedName: string): [string, string] | undefined; get(encodedName: string, dataEventKey?: string): unknown; getInfo(encodedName: string): VarData | undefined; getDataTree(): ModuleData | undefined; From 286a233794e8ff4f5411ddd3f68786924386d05b Mon Sep 17 00:00:00 2001 From: Toan Quach <93168955+toan-quach@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:35:22 +0700 Subject: [PATCH 04/10] Update taipy/core/_core.py Co-authored-by: Jean-Robin --- taipy/core/_core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/taipy/core/_core.py b/taipy/core/_core.py index 4057b197be..2c0bf5fde9 100644 --- a/taipy/core/_core.py +++ b/taipy/core/_core.py @@ -23,6 +23,7 @@ class Core: def __new__(cls) -> Orchestrator: # type: ignore _warn_deprecated("'Core'", suggest="the 'Orchestrator' class") cls.__logger.warning( - "Class `Core` has been deprecated, an object of class `Orchestrator` will be created instead." + "The `Core` service is deprecated and replaced by the `Orchestrator` service. " + "An `Orchestrator` instance has been instantiated instead." ) return Orchestrator() From 50957ae76bcc4c600f8d31e198d8c050367993d8 Mon Sep 17 00:00:00 2001 From: Toan Quach <93168955+toan-quach@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:35:29 +0700 Subject: [PATCH 05/10] Update taipy/core/_core.py Co-authored-by: Jean-Robin --- taipy/core/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/core/_core.py b/taipy/core/_core.py index 2c0bf5fde9..e8883f408e 100644 --- a/taipy/core/_core.py +++ b/taipy/core/_core.py @@ -16,7 +16,7 @@ class Core: - """Deprecated. Use `Orchestrator` class with `tp.Orchestrator()` instead.""" + """Deprecated. Use the `Orchestrator^` service class with `taipy.Orchestrator()` instead.""" __logger = _TaipyLogger._get_logger() From 9be5a9c9a70ed5eb5473725300f73a8b6859d1c2 Mon Sep 17 00:00:00 2001 From: Toan Quach Date: Thu, 19 Sep 2024 09:57:21 +0700 Subject: [PATCH 06/10] fixed failed test --- tests/core/test_core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core/test_core.py b/tests/core/test_core.py index a51e6aed6c..1932f05605 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -23,7 +23,8 @@ def test_run_core_with_depracated_message(self, caplog): assert isinstance(core, Orchestrator) expected_message = ( - "Class `Core` has been deprecated, an object of class `Orchestrator` will be created instead." + "The `Core` service is deprecated and replaced by the `Orchestrator` service. " + "An `Orchestrator` instance has been instantiated instead." ) assert expected_message in caplog.text From 2f33ab1e3cdbc2f91553fe16ff60ea8eeab73422 Mon Sep 17 00:00:00 2001 From: Rym <31435778+RymMichaut@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:42:19 +0200 Subject: [PATCH 07/10] pre announcing hacktoberfest (#1806) adding a banner to announce the Hacktoberfest 2024 --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 652861e552..1fa1d9b655 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ + +![Hactoberfestnew](https://github.com/user-attachments/assets/149a5cee-6af1-4d4e-9c43-6fcf82a9b07e) + + From 6c2df92707d7850885432461c4fcb2c8317193aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:41:58 +0200 Subject: [PATCH 08/10] Scenario viewer update (#1803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Scenario viewer update resolves #1669 resolves a few mypy/ruff "errors" in python as well * sometimes _is_deletable breaks because Job is None * comments and Mui 7 preparation * mypy * ruff * doc embryo * fix test * filters * JR' comment + Mui update * fab's comment * job_id cannot be empty/None in the exception block --------- Co-authored-by: Fred Lefévère-Laoide --- frontend/taipy-gui/package-lock.json | 148 ++++++------------ .../taipy-gui/src/components/Taipy/Chat.tsx | 4 +- .../taipy-gui/src/components/Taipy/Input.tsx | 100 ++++++------ .../taipy-gui/src/components/Taipy/Login.tsx | 11 +- frontend/taipy/package-lock.json | 90 +++++------ frontend/taipy/src/DataNodeViewer.tsx | 8 +- frontend/taipy/src/PropertiesEditor.tsx | 8 +- frontend/taipy/src/ScenarioViewer.tsx | 12 +- taipy/core/job/_job_manager.py | 2 +- taipy/gui/_gui_section.py | 9 +- taipy/gui/_page.py | 2 +- taipy/gui/_renderers/factory.py | 2 +- taipy/gui/config.py | 46 +++--- taipy/gui/gui.py | 82 +++++----- taipy/gui/partial.py | 4 +- taipy/gui/server.py | 10 +- taipy/gui/utils/_adapter.py | 26 +-- taipy/gui/utils/_bindings.py | 2 +- taipy/gui/utils/_evaluator.py | 2 +- taipy/gui/utils/_map_dict.py | 8 +- taipy/gui/utils/_runtime_manager.py | 4 +- taipy/gui/utils/_variable_directory.py | 8 +- taipy/gui/utils/chart_config_builder.py | 5 +- taipy/gui/utils/datatype.py | 2 +- taipy/gui/utils/isnotebook.py | 2 +- taipy/gui_core/__init__.py | 2 +- taipy/gui_core/_adapters.py | 113 +++---------- taipy/gui_core/_context.py | 135 ++++++++-------- taipy/gui_core/filters.py | 99 ++++++++++++ tests/gui_core/test_context_is_readable.py | 2 +- 30 files changed, 475 insertions(+), 473 deletions(-) create mode 100644 taipy/gui_core/filters.py diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index a1a23d590f..ae9654a7ca 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -87,26 +87,6 @@ "webpack-cli": "^5.0.0" } }, - "node_modules/@75lb/deep-merge": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", - "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", - "dependencies": { - "lodash": "^4.17.21", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@75lb/deep-merge/node_modules/typical": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", - "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==", - "engines": { - "node": ">=12.17" - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", @@ -1912,18 +1892,18 @@ "dev": true }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.0.tgz", - "integrity": "sha512-covEnIn/2er5YdtuukDRA52kmARhKrHjOvPsyTFMQApZdrTBI4h8jbEy2mxZqwMwcAFS9coonQXnEZKL1rUNdQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz", + "integrity": "sha512-VdQC1tPIIcZAnf62L2M1eQif0x2vlKg3YK4kGYbtijSH4niEgI21GnstykW1vQIs+Bc6L+Hua2GATYVjilJ22A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.0.tgz", - "integrity": "sha512-HxfB0jxwiMTYMN8gAnYn3avbF1aDrqBEuGIj6JDQ3YkLl650E1Wy8AIhwwyP47wdrv0at9aAR0iOO6VLb74A9w==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.1.tgz", + "integrity": "sha512-sy/YKwcLPW8VcacNP2uWMYR9xyWuwO9NN9FXuGEU90bRshBXj8pdKk+joe3TCW7oviVS3zXLHlc94wQ0jNsQRQ==", "dependencies": { "@babel/runtime": "^7.25.6" }, @@ -1935,7 +1915,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.1.0", + "@mui/material": "^6.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1946,15 +1926,15 @@ } }, "node_modules/@mui/material": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.0.tgz", - "integrity": "sha512-4MJ46vmy1xbm8x+ZdRcWm8jEMMowdS8pYlhKQzg/qoKhOcLhImZvf2Jn6z9Dj6gl+lY+C/0MxaHF/avAAGys3Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz", + "integrity": "sha512-b+eULldTqtqTCbN++2BtBWCir/1LwEYw+2mIlOt2GiEUh1EBBw4/wIukGKKNt3xrCZqRA80yLLkV6tF61Lq3cA==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/core-downloads-tracker": "^6.1.0", - "@mui/system": "^6.1.0", - "@mui/types": "^7.2.16", - "@mui/utils": "^6.1.0", + "@mui/core-downloads-tracker": "^6.1.1", + "@mui/system": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -1973,7 +1953,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.0", + "@mui/material-pigment-css": "^6.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1994,12 +1974,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.0.tgz", - "integrity": "sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz", + "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/utils": "^6.1.0", + "@mui/utils": "^6.1.1", "prop-types": "^15.8.1" }, "engines": { @@ -2020,9 +2000,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.0.tgz", - "integrity": "sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz", + "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==", "dependencies": { "@babel/runtime": "^7.25.6", "@emotion/cache": "^11.13.1", @@ -2052,15 +2032,15 @@ } }, "node_modules/@mui/system": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.0.tgz", - "integrity": "sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz", + "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/private-theming": "^6.1.0", - "@mui/styled-engine": "^6.1.0", - "@mui/types": "^7.2.16", - "@mui/utils": "^6.1.0", + "@mui/private-theming": "^6.1.1", + "@mui/styled-engine": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -2091,9 +2071,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.16", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz", - "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==", + "version": "7.2.17", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", + "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2104,12 +2084,12 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz", + "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.16", + "@mui/types": "^7.2.17", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -2954,9 +2934,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -4561,9 +4541,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", "funding": [ { "type": "opencollective", @@ -5080,13 +5060,13 @@ } }, "node_modules/command-line-usage": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.1.tgz", - "integrity": "sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", - "table-layout": "^3.0.0", + "table-layout": "^4.1.0", "typical": "^7.1.1" }, "engines": { @@ -6135,9 +6115,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz", - "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==" + "version": "1.5.25", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz", + "integrity": "sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==" }, "node_modules/element-size": { "version": "1.1.1", @@ -14757,14 +14737,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/stream-read-all": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/stream-read-all/-/stream-read-all-3.0.1.tgz", - "integrity": "sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==", - "engines": { - "node": ">=10" - } - }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -15125,21 +15097,13 @@ "dev": true }, "node_modules/table-layout": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-3.0.2.tgz", - "integrity": "sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "dependencies": { - "@75lb/deep-merge": "^1.1.1", "array-back": "^6.2.2", - "command-line-args": "^5.2.1", - "command-line-usage": "^7.0.0", - "stream-read-all": "^3.0.1", - "typical": "^7.1.1", "wordwrapjs": "^5.1.0" }, - "bin": { - "table-layout": "bin/cli.js" - }, "engines": { "node": ">=12.17" } @@ -15152,14 +15116,6 @@ "node": ">=12.17" } }, - "node_modules/table-layout/node_modules/typical": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", - "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==", - "engines": { - "node": ">=12.17" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.tsx index 38f3c51056..55acb528c2 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chat.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chat.tsx @@ -406,7 +406,7 @@ const Chat = (props: ChatProps) => { label={`message (${senderId})`} disabled={!active} onKeyDown={handleAction} - InputProps={{ + slotProps={{input: { endAdornment: ( { ), - }} + }}} sx={inputSx} /> ) : null} diff --git a/frontend/taipy-gui/src/components/Taipy/Input.tsx b/frontend/taipy-gui/src/components/Taipy/Input.tsx index 2dc9de6219..dba0d31d53 100644 --- a/frontend/taipy-gui/src/components/Taipy/Input.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Input.tsx @@ -260,45 +260,58 @@ const Input = (props: TaipyInputProps) => { (event: React.MouseEvent) => event.preventDefault(), [] ); - const muiInputProps = useMemo( + const inputProps = useMemo( () => - type == "password" + type == "number" ? { - endAdornment: ( - - {showPassword ? : } - - ), + htmlInput: { + step: step ? step : 1, + min: min, + max: max, + }, + input: { + endAdornment: ( +
+ + + + + + +
+ ), + }, } - : type == "number" - ? { - endAdornment: ( -
- - - - - - -
- ), - } - : undefined, + : type == "password" + ? { + htmlInput: { autoComplete: "current-password" }, + input: { + endAdornment: ( + + {showPassword ? : } + + ), + }, + } + : undefined, [ type, + step, + min, + max, showPassword, handleClickShowPassword, handleMouseDownPassword, @@ -307,20 +320,6 @@ const Input = (props: TaipyInputProps) => { ] ); - const inputProps = useMemo( - () => - type == "number" - ? { - step: step ? step : 1, - min: min, - max: max, - } - : type == "password" - ? { autoComplete: "current-password" } - : undefined, - [type, step, min, max] - ); - useEffect(() => { if (props.value !== undefined) { setValue(props.value); @@ -337,10 +336,7 @@ const Input = (props: TaipyInputProps) => { className={className} type={showPassword && type == "password" ? "text" : type} id={id} - slotProps={{ - htmlInput: inputProps, - input: muiInputProps, - }} + slotProps={inputProps} label={props.label} onChange={handleInput} disabled={!active} diff --git a/frontend/taipy-gui/src/components/Taipy/Login.tsx b/frontend/taipy-gui/src/components/Taipy/Login.tsx index aa37371f2d..a2058732c6 100644 --- a/frontend/taipy-gui/src/components/Taipy/Login.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Login.tsx @@ -48,7 +48,7 @@ const closeSx: SxProps = { alignSelf: "start", }; const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" }; -const userProps = { autoComplete: "username" }; +const userProps = { htmlInput: { autoComplete: "username" }}; const pwdProps = { autoComplete: "current-password" }; const Login = (props: LoginProps) => { @@ -97,7 +97,7 @@ const Login = (props: LoginProps) => { [] ); const passwordProps = useMemo( - () => ({ + () => ({input: { endAdornment: ( { ), - }), + }, htmlInput: pwdProps}), [showPassword, handleClickShowPassword, handleMouseDownPassword] ); @@ -145,7 +145,7 @@ const Login = (props: LoginProps) => { onChange={changeInput} data-input="user" onKeyDown={handleEnter} - inputProps={userProps} + slotProps={userProps} > { onChange={changeInput} data-input="password" onKeyDown={handleEnter} - inputProps={pwdProps} - InputProps={passwordProps} + slotProps={passwordProps} /> {message || defaultMessage} diff --git a/frontend/taipy/package-lock.json b/frontend/taipy/package-lock.json index c5466b3b46..a38a3cad5c 100644 --- a/frontend/taipy/package-lock.json +++ b/frontend/taipy/package-lock.json @@ -660,18 +660,18 @@ "dev": true }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.0.tgz", - "integrity": "sha512-covEnIn/2er5YdtuukDRA52kmARhKrHjOvPsyTFMQApZdrTBI4h8jbEy2mxZqwMwcAFS9coonQXnEZKL1rUNdQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz", + "integrity": "sha512-VdQC1tPIIcZAnf62L2M1eQif0x2vlKg3YK4kGYbtijSH4niEgI21GnstykW1vQIs+Bc6L+Hua2GATYVjilJ22A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.0.tgz", - "integrity": "sha512-HxfB0jxwiMTYMN8gAnYn3avbF1aDrqBEuGIj6JDQ3YkLl650E1Wy8AIhwwyP47wdrv0at9aAR0iOO6VLb74A9w==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.1.tgz", + "integrity": "sha512-sy/YKwcLPW8VcacNP2uWMYR9xyWuwO9NN9FXuGEU90bRshBXj8pdKk+joe3TCW7oviVS3zXLHlc94wQ0jNsQRQ==", "dependencies": { "@babel/runtime": "^7.25.6" }, @@ -683,7 +683,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.1.0", + "@mui/material": "^6.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -694,15 +694,15 @@ } }, "node_modules/@mui/material": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.0.tgz", - "integrity": "sha512-4MJ46vmy1xbm8x+ZdRcWm8jEMMowdS8pYlhKQzg/qoKhOcLhImZvf2Jn6z9Dj6gl+lY+C/0MxaHF/avAAGys3Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz", + "integrity": "sha512-b+eULldTqtqTCbN++2BtBWCir/1LwEYw+2mIlOt2GiEUh1EBBw4/wIukGKKNt3xrCZqRA80yLLkV6tF61Lq3cA==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/core-downloads-tracker": "^6.1.0", - "@mui/system": "^6.1.0", - "@mui/types": "^7.2.16", - "@mui/utils": "^6.1.0", + "@mui/core-downloads-tracker": "^6.1.1", + "@mui/system": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -721,7 +721,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.0", + "@mui/material-pigment-css": "^6.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -742,12 +742,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.0.tgz", - "integrity": "sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz", + "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/utils": "^6.1.0", + "@mui/utils": "^6.1.1", "prop-types": "^15.8.1" }, "engines": { @@ -768,9 +768,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.0.tgz", - "integrity": "sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz", + "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==", "dependencies": { "@babel/runtime": "^7.25.6", "@emotion/cache": "^11.13.1", @@ -800,15 +800,15 @@ } }, "node_modules/@mui/system": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.0.tgz", - "integrity": "sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz", + "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/private-theming": "^6.1.0", - "@mui/styled-engine": "^6.1.0", - "@mui/types": "^7.2.16", - "@mui/utils": "^6.1.0", + "@mui/private-theming": "^6.1.1", + "@mui/styled-engine": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -839,9 +839,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.16", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz", - "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==", + "version": "7.2.17", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", + "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -852,12 +852,12 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz", + "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==", "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.16", + "@mui/types": "^7.2.17", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1223,9 +1223,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/hoist-non-react-statics": { @@ -2080,9 +2080,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", "dev": true, "funding": [ { @@ -2409,9 +2409,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz", - "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==", + "version": "1.5.25", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz", + "integrity": "sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==", "dev": true }, "node_modules/enhanced-resolve": { diff --git a/frontend/taipy/src/DataNodeViewer.tsx b/frontend/taipy/src/DataNodeViewer.tsx index 001df68c7e..fe523ae609 100644 --- a/frontend/taipy/src/DataNodeViewer.tsx +++ b/frontend/taipy/src/DataNodeViewer.tsx @@ -786,7 +786,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => { sx={FieldNoMaxWidth} value={label || ""} onChange={onLabelChange} - InputProps={{ + slotProps={{input:{ endAdornment: ( @@ -809,7 +809,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => { ), - }} + }}} disabled={!valid} /> ) : ( @@ -1067,7 +1067,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => { ? "number" : undefined } - InputProps={{ + slotProps={{input: { endAdornment: ( @@ -1090,7 +1090,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => { ), - }} + }}} disabled={!valid} /> )} diff --git a/frontend/taipy/src/PropertiesEditor.tsx b/frontend/taipy/src/PropertiesEditor.tsx index 7e9b2f1c1e..92655ee174 100644 --- a/frontend/taipy/src/PropertiesEditor.tsx +++ b/frontend/taipy/src/PropertiesEditor.tsx @@ -206,7 +206,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => { data-name="key" data-id={property.id} onChange={updatePropertyField} - inputProps={{ onKeyDown }} + slotProps={{ input: { onKeyDown } }} /> @@ -219,7 +219,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => { data-name="value" data-id={property.id} onChange={updatePropertyField} - inputProps={{ onKeyDown, "data-enter": true }} + slotProps={{ htmlInput: { onKeyDown, "data-enter": true } }} /> { variant="outlined" sx={FieldNoMaxWidth} disabled={!isDefined} - inputProps={{ onKeyDown }} + slotProps={{ htmlInput: { onKeyDown } }} /> @@ -321,7 +321,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => { variant="outlined" sx={FieldNoMaxWidth} disabled={!isDefined} - inputProps={{ onKeyDown, "data-enter": true }} + slotProps={{ htmlInput: { onKeyDown, "data-enter": true }}} /> diff --git a/frontend/taipy/src/ScenarioViewer.tsx b/frontend/taipy/src/ScenarioViewer.tsx index 06f0c925a7..c5a26d3a7d 100644 --- a/frontend/taipy/src/ScenarioViewer.tsx +++ b/frontend/taipy/src/ScenarioViewer.tsx @@ -629,7 +629,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { expandIcon={expandable ? : null} sx={AccordionSummarySx} > - + {scLabel} {scPrimary ? ( @@ -712,7 +712,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { sx={FieldNoMaxWidth} value={label || ""} onChange={onLabelChange} - InputProps={{ + slotProps={{input: { onKeyDown: onLabelKeyDown, endAdornment: ( @@ -736,7 +736,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { ), - }} + }}} disabled={!valid} /> ) : ( @@ -752,7 +752,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { {showTags ? ( { label="Tags" sx={tagsAutocompleteSx} fullWidth - InputProps={{ + slotProps={{input: { ...params.InputProps, onKeyDown: onTagsKeyDown, endAdornment: ( @@ -812,7 +812,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => { ), - }} + }}} /> )} disabled={!valid} diff --git a/taipy/core/job/_job_manager.py b/taipy/core/job/_job_manager.py index 0f64d4d618..10deec2cf1 100644 --- a/taipy/core/job/_job_manager.py +++ b/taipy/core/job/_job_manager.py @@ -94,7 +94,7 @@ def _is_deletable(cls, job: Union[Job, JobId]) -> ReasonCollection: if isinstance(job, str): job = cls._get(job) - if not job.is_finished(): + if job and not job.is_finished(): reason_collector._add_reason(job.id, JobIsNotFinished(job.id)) return reason_collector diff --git a/taipy/gui/_gui_section.py b/taipy/gui/_gui_section.py index d062801259..7ce5e517fa 100644 --- a/taipy/gui/_gui_section.py +++ b/taipy/gui/_gui_section.py @@ -19,7 +19,7 @@ class _GuiSection(UniqueSection): - name = "gui" + name = "gui" # type: ignore[reportAssignmentType] def __init__(self, property_list: t.Optional[t.List] = None, **properties): self._property_list = property_list @@ -37,13 +37,14 @@ def _to_dict(self): return as_dict @classmethod - def _from_dict(cls, as_dict: t.Dict[str, t.Any], *_): - return _GuiSection(property_list=list(default_config), **as_dict) + def _from_dict(cls, config_as_dict: t.Dict[str, t.Any], id, config): + return _GuiSection(property_list=list(default_config), **config_as_dict) def _update(self, config_as_dict: t.Dict[str, t.Any], default_section=None): + as_dict = None if self._property_list: as_dict = {k: v for k, v in config_as_dict.items() if k in self._property_list} - self._properties.update(as_dict) + self._properties.update(as_dict or config_as_dict) @staticmethod def _configure(**properties) -> "_GuiSection": diff --git a/taipy/gui/_page.py b/taipy/gui/_page.py index 8decae1933..26b98dcd79 100644 --- a/taipy/gui/_page.py +++ b/taipy/gui/_page.py @@ -25,7 +25,7 @@ class _Page(object): def __init__(self) -> None: self._rendered_jsx: t.Optional[str] = None self._renderer: t.Optional[Page] = None - self._style: t.Optional[str] = None + self._style: t.Optional[t.Union[str, t.Dict[str, t.Any]]] = None self._route: t.Optional[str] = None self._head: t.Optional[list] = None diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 76c62b45a5..88144c7c8c 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -685,7 +685,7 @@ def call_builder( builder = _Factory.__CONTROL_BUILDERS.get(name) built = None _Factory.__COUNTER += 1 - with gui._get_autorization(): + with gui._get_authorization(): if builder is None: lib, element_name, element = _Factory.__get_library_element(name) if lib: diff --git a/taipy/gui/config.py b/taipy/gui/config.py index eca991afe1..00dda222ea 100644 --- a/taipy/gui/config.py +++ b/taipy/gui/config.py @@ -162,14 +162,16 @@ def _load(self, config: Config) -> None: self.get_time_zone() def _get_config(self, name: ConfigParameter, default_value: t.Any) -> t.Any: # pragma: no cover - if name in self.config and self.config[name] is not None: - if default_value is not None and not isinstance(self.config[name], type(default_value)): + if name in self.config and self.config.get(name) is not None: + if default_value is not None and not isinstance(self.config.get(name), type(default_value)): try: - return type(default_value)(self.config[name]) + return type(default_value)(self.config.get(name)) except Exception as e: - _warn(f'app_config "{name}" value "{self.config[name]}" is not of type {type(default_value)}', e) + _warn( + f'app_config "{name}" value "{self.config.get(name)}" is not of type {type(default_value)}', e + ) return default_value - return self.config[name] + return self.config.get(name) return default_value def get_time_zone(self) -> t.Optional[str]: @@ -234,12 +236,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover config[key] = _default_stylekit if value else {} continue try: - if isinstance(value, dict) and isinstance(config[key], dict): - config[key].update(value) + if isinstance(value, dict) and isinstance(config.get(key), dict): + t.cast(dict, config.get(key)).update(value) elif key == "port" and str(value).strip() == "auto": config["port"] = "auto" else: - config[key] = value if config[key] is None else type(config[key])(value) + config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue] except Exception as e: _warn( f"Invalid keyword arguments value in Gui.run {key} - {value}. Unable to parse value to the correct type", # noqa: E501 @@ -282,29 +284,29 @@ def resolve(self): app_config = self.config logger = _TaipyLogger._get_logger() # Special config for notebook runtime - if _is_in_notebook() or app_config["run_in_thread"] and not app_config["single_client"]: + if _is_in_notebook() or app_config.get("run_in_thread") and not app_config.get("single_client"): app_config["single_client"] = True self.__log_outside_reloader(logger, "Running in 'single_client' mode in notebook environment") - if app_config["run_server"] and app_config["ngrok_token"] and app_config["use_reloader"]: + if app_config.get("run_server") and app_config.get("ngrok_token") and app_config.get("use_reloader"): app_config["use_reloader"] = False self.__log_outside_reloader( logger, "'use_reloader' parameter will not be used when 'ngrok_token' parameter is available" ) - if app_config["use_reloader"] and _is_in_notebook(): + if app_config.get("use_reloader") and _is_in_notebook(): app_config["use_reloader"] = False self.__log_outside_reloader(logger, "'use_reloader' parameter is not available in notebook environment") - if app_config["use_reloader"] and not app_config["debug"]: + if app_config.get("use_reloader") and not app_config.get("debug"): app_config["debug"] = True self.__log_outside_reloader(logger, "Application is running in 'debug' mode") - if app_config["debug"] and not app_config["allow_unsafe_werkzeug"]: + if app_config.get("debug") and not app_config.get("allow_unsafe_werkzeug"): app_config["allow_unsafe_werkzeug"] = True self.__log_outside_reloader(logger, "'allow_unsafe_werkzeug' has been set to True") - if app_config["debug"] and app_config["async_mode"] != "threading": + if app_config.get("debug") and app_config.get("async_mode") != "threading": app_config["async_mode"] = "threading" self.__log_outside_reloader( logger, @@ -318,17 +320,15 @@ def resolve(self): def _resolve_stylekit(self): app_config = self.config # support legacy margin variable - stylekit_config = app_config["stylekit"] + stylekit_config = app_config.get("stylekit") - if isinstance(app_config["stylekit"], dict) and "root_margin" in app_config["stylekit"]: + if isinstance(stylekit_config, dict) and "root_margin" in stylekit_config: from ._default_config import _default_stylekit, default_config - stylekit_config = app_config["stylekit"] - if ( - stylekit_config["root_margin"] == _default_stylekit["root_margin"] - and app_config["margin"] != default_config["margin"] - ): - app_config["stylekit"]["root_margin"] = str(app_config["margin"]) + if stylekit_config.get("root_margin") == _default_stylekit.get("root_margin") and app_config.get( + "margin" + ) != default_config.get("margin"): + stylekit_config["root_margin"] = str(app_config.get("margin")) app_config["margin"] = None def _resolve_url_prefix(self): @@ -343,4 +343,4 @@ def _resolve_url_prefix(self): def _resolve_notebook_proxy(self): app_config = self.config - app_config["notebook_proxy"] = app_config["notebook_proxy"] if _is_in_notebook() else False + app_config["notebook_proxy"] = app_config.get("notebook_proxy", False) if _is_in_notebook() else False diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 698eb58cc9..3dfbbdde37 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -470,7 +470,7 @@ def get_matplotlib_content(figure: MatplotlibFigure): if callable(provider_fn): try: - return provider_fn(content) + return provider_fn(t.cast(t.Any, content)) except Exception as e: _warn(f"Error in content provider for type {str(type(content))}", e) return ( @@ -639,7 +639,7 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None: self.__set_client_id_in_context(expected_client_id) g.ws_client_id = expected_client_id with self._set_locals_context(message.get("module_context") or None): - with self._get_autorization(): + with self._get_authorization(): payload = message.get("payload", {}) if msg_type == _WsType.UPDATE.value: self.__front_end_update( @@ -966,7 +966,7 @@ def _serve_status(self, template: Path) -> t.Dict[str, t.Dict[str, str]]: def __upload_files(self): self.__set_client_id_in_context() on_upload_action = request.form.get("on_action", None) - var_name = request.form.get("var_name", None) + var_name = t.cast(str, request.form.get("var_name", None)) if not var_name and not on_upload_action: _warn("upload files: No var name") return ("upload files: No var name", 400) @@ -1057,7 +1057,7 @@ def __send_var_list_update( # noqa C901 else: if isinstance(newvalue, (_TaipyContent, _TaipyContentImage)): ret_value = self.__get_content_accessor().get_info( - front_var, newvalue.get(), isinstance(newvalue, _TaipyContentImage) + t.cast(str, front_var), newvalue.get(), isinstance(newvalue, _TaipyContentImage) ) if isinstance(ret_value, tuple): newvalue = f"/{Gui.__CONTENT_ROOT}/{ret_value[0]}" @@ -1160,8 +1160,8 @@ def __handle_ws_get_module_context(self, payload: t.Any): page_path = Gui.__root_page_name # Get Module Context if mc := self._get_page_context(page_path): - page_renderer = self._get_page(page_path)._renderer - self._bind_custom_page_variables(page_renderer, self._get_client_id()) + page_renderer = t.cast(_Page, self._get_page(page_path))._renderer + self._bind_custom_page_variables(t.cast(t.Any, page_renderer), self._get_client_id()) # get metadata if there is one metadata: t.Dict[str, t.Any] = {} if hasattr(page_renderer, "_metadata"): @@ -1245,12 +1245,12 @@ def __handle_ws_app_id(self, message: t.Any): def __handle_ws_get_routes(self): routes = ( - [[self._config.root_page._route, self._config.root_page._renderer.page_type]] + [[self._config.root_page._route, t.cast(t.Any, self._config.root_page._renderer).page_type]] if self._config.root_page else [] ) routes += [ - [page._route, page._renderer.page_type] + [page._route, t.cast(t.Any, page._renderer).page_type] for page in self._config.pages if page._route != Gui.__root_page_name ] @@ -1269,7 +1269,7 @@ def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> self._server._ws.emit( "message", payload, - to=self.__get_ws_receiver(send_back_only), + to=t.cast(str, self.__get_ws_receiver(send_back_only)), ) time.sleep(0.001) except Exception as e: # pragma: no cover @@ -1280,7 +1280,7 @@ def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None): try: to = list(self.__get_sids(client_id)) if client_id else [] - self._server._ws.emit("message", payload, to=to if to else None, include_self=True) + self._server._ws.emit("message", payload, to=t.cast(str, to) if to else None, include_self=True) time.sleep(0.001) except Exception as e: # pragma: no cover _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e) @@ -1291,7 +1291,7 @@ def __send_ack(self, ack_id: t.Optional[str]) -> None: self._server._ws.emit( "message", {"type": _WsType.ACKNOWLEDGEMENT.value, "id": ack_id}, - to=self.__get_ws_receiver(True), + to=t.cast(str, self.__get_ws_receiver(True)), ) time.sleep(0.001) except Exception as e: # pragma: no cover @@ -1493,7 +1493,7 @@ def __on_action(self, id: t.Optional[str], payload: t.Any) -> None: def __call_function_with_args(self, **kwargs): action_function = kwargs.get("action_function") - id = kwargs.get("id") + id = t.cast(str, kwargs.get("id")) payload = kwargs.get("payload") if callable(action_function): @@ -1501,7 +1501,7 @@ def __call_function_with_args(self, **kwargs): argcount = action_function.__code__.co_argcount if argcount > 0 and inspect.ismethod(action_function): argcount -= 1 - args = [None for _ in range(argcount)] + args = t.cast(list, [None for _ in range(argcount)]) if argcount > 0: args[0] = self.__get_state() if argcount > 1: @@ -1746,7 +1746,7 @@ def _tbl_cols( attributes.get("date_format"), attributes.get("number_format"), ) - _enhance_columns(attributes, hashes, col_dict, "table(cols)") + _enhance_columns(attributes, hashes, t.cast(dict, col_dict), "table(cols)") return json.dumps(col_dict, cls=_TaipyJsonEncoder) except Exception as e: # pragma: no cover @@ -1846,7 +1846,7 @@ def _get_locals_bind_from_context(self, context: t.Optional[str]) -> t.Dict[str, def _get_locals_context(self) -> str: current_context = self.__locals_context.get_context() - return current_context if current_context is not None else self.__default_module_name + return current_context if current_context is not None else t.cast(str, self.__default_module_name) def _set_locals_context(self, context: t.Optional[str]) -> t.ContextManager[None]: return self.__locals_context.set_locals_context(context) @@ -2409,7 +2409,7 @@ def get_flask_app(self) -> Flask: The Flask instance used. """ if hasattr(self, "_server"): - return self._server.get_flask() + return t.cast(Flask, self._server.get_flask()) raise RuntimeError("get_flask_app() cannot be invoked before run() has been called.") def _set_frame(self, frame: t.Optional[FrameType]): @@ -2486,21 +2486,21 @@ def __init_server(self): self, path_mapping=self._path_mapping, flask=self._flask, - async_mode=app_config["async_mode"], - allow_upgrades=not app_config["notebook_proxy"], + async_mode=app_config.get("async_mode"), + allow_upgrades=not app_config.get("notebook_proxy"), server_config=app_config.get("server_config"), ) # Stop and reinitialize the server if it is still running as a thread - if (_is_in_notebook() or app_config["run_in_thread"]) and hasattr(self._server, "_thread"): + if (_is_in_notebook() or app_config.get("run_in_thread")) and hasattr(self._server, "_thread"): self.stop() self._flask_blueprint = [] self._server = _Server( self, path_mapping=self._path_mapping, flask=self._flask, - async_mode=app_config["async_mode"], - allow_upgrades=not app_config["notebook_proxy"], + async_mode=app_config.get("async_mode"), + allow_upgrades=not app_config.get("notebook_proxy"), server_config=app_config.get("server_config"), ) self._bindings()._new_scopes() @@ -2509,16 +2509,16 @@ def __init_ngrok(self): app_config = self._config.config if hasattr(self, "_ngrok"): # Keep the ngrok instance if token has not changed - if app_config["ngrok_token"] == self._ngrok[1]: + if app_config.get("ngrok_token") == self._ngrok[1]: _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}") return # Close the old tunnel so new tunnel can open for new token - ngrok.disconnect(self._ngrok[0].public_url) - if app_config["run_server"] and (token := app_config["ngrok_token"]): # pragma: no cover + ngrok.disconnect(self._ngrok[0].public_url) # type: ignore[reportPossiblyUnboundVariable] + if app_config.get("run_server") and (token := app_config.get("ngrok_token")): # pragma: no cover if not util.find_spec("pyngrok"): raise RuntimeError("Cannot use ngrok as pyngrok package is not installed.") - ngrok.set_auth_token(token) - self._ngrok = (ngrok.connect(app_config["port"], "http"), token) + ngrok.set_auth_token(token) # type: ignore[reportPossiblyUnboundVariable] + self._ngrok = (ngrok.connect(app_config.get("port"), "http"), token) # type: ignore[reportPossiblyUnboundVariable] _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}") def __bind_default_function(self): @@ -2611,7 +2611,7 @@ def __register_blueprint(self): # Register Flask Blueprint if available for bp in self._flask_blueprint: - self._server.get_flask().register_blueprint(bp) + t.cast(Flask, self._server.get_flask()).register_blueprint(bp) def _get_accessor(self): if self.__accessors is None: @@ -2711,7 +2711,7 @@ def run( locals_bind = _filter_locals(self.__frame.f_locals) - self.__locals_context.set_default(locals_bind, self.__default_module_name) + self.__locals_context.set_default(locals_bind, t.cast(str, self.__default_module_name)) self.__var_dir.set_default(self.__frame) @@ -2761,25 +2761,27 @@ def run( self.__register_blueprint() # Register data accessor communication data format (JSON, Apache Arrow) - self._get_accessor().set_data_format(_DataFormat.APACHE_ARROW if app_config["use_arrow"] else _DataFormat.JSON) + self._get_accessor().set_data_format( + _DataFormat.APACHE_ARROW if app_config.get("use_arrow") else _DataFormat.JSON + ) # Use multi user or not - self._bindings()._set_single_client(bool(app_config["single_client"])) + self._bindings()._set_single_client(bool(app_config.get("single_client"))) # Start Flask Server if not run_server: return self.get_flask_app() return self._server.run( - host=app_config["host"], - port=app_config["port"], - debug=app_config["debug"], - use_reloader=app_config["use_reloader"], - flask_log=app_config["flask_log"], - run_in_thread=app_config["run_in_thread"], - allow_unsafe_werkzeug=app_config["allow_unsafe_werkzeug"], - notebook_proxy=app_config["notebook_proxy"], - port_auto_ranges=app_config["port_auto_ranges"], + host=app_config.get("host"), + port=app_config.get("port"), + debug=app_config.get("debug"), + use_reloader=app_config.get("use_reloader"), + flask_log=app_config.get("flask_log"), + run_in_thread=app_config.get("run_in_thread"), + allow_unsafe_werkzeug=app_config.get("allow_unsafe_werkzeug"), + notebook_proxy=app_config.get("notebook_proxy"), + port_auto_ranges=app_config.get("port_auto_ranges"), ) def reload(self): # pragma: no cover @@ -2807,7 +2809,7 @@ def stop(self): self._server.stop_thread() _TaipyLogger._get_logger().info("Gui server has been stopped.") - def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False): + def _get_authorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False): return contextlib.nullcontext() def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None): diff --git a/taipy/gui/partial.py b/taipy/gui/partial.py index 47a5d22097..54d5ef7318 100644 --- a/taipy/gui/partial.py +++ b/taipy/gui/partial.py @@ -53,7 +53,7 @@ def __init__(self, route: t.Optional[str] = None): else: self._route = route - def update_content(self, state: State, content: str | "Page"): + def update_content(self, state: State, content: t.Union[str, "Page"]): """Update partial content. Arguments: @@ -65,7 +65,7 @@ def update_content(self, state: State, content: str | "Page"): else: _warn("'Partial.update_content()' must be called in the context of a callback.") - def __copy(self, content: str | "Page") -> Partial: + def __copy(self, content: t.Union[str, "Page"]) -> Partial: new_partial = Partial(self._route) from .page import Page diff --git a/taipy/gui/server.py b/taipy/gui/server.py index ab62ed02bc..dfada6091f 100644 --- a/taipy/gui/server.py +++ b/taipy/gui/server.py @@ -247,14 +247,14 @@ def get_flask(self): return self._flask def test_client(self): - return self._flask.test_client() + return t.cast(Flask, self._flask).test_client() def _run_notebook(self): self._is_running = True self._ws.run(self._flask, host=self._host, port=self._port, debug=False, use_reloader=False) def _get_async_mode(self) -> str: - return self._ws.async_mode + return self._ws.async_mode # type: ignore[reportAttributeAccessIssue] def _apply_patch(self): if self._get_async_mode() == "gevent" and util.find_spec("gevent"): @@ -264,7 +264,7 @@ def _apply_patch(self): if not monkey.is_module_patched("time"): monkey.patch_time() if self._get_async_mode() == "eventlet" and util.find_spec("eventlet"): - from eventlet import monkey_patch, patcher + from eventlet import monkey_patch, patcher # type: ignore[reportMissingImport] if not patcher.is_monkey_patched("time"): monkey_patch(time=True) @@ -349,8 +349,8 @@ def stop_thread(self): self._is_running = False with contextlib.suppress(Exception): if self._get_async_mode() == "gevent": - if self._ws.wsgi_server is not None: - self._ws.wsgi_server.stop() + if self._ws.wsgi_server is not None: # type: ignore[reportAttributeAccessIssue] + self._ws.wsgi_server.stop() # type: ignore[reportAttributeAccessIssue] else: self._thread.kill() else: diff --git a/taipy/gui/utils/_adapter.py b/taipy/gui/utils/_adapter.py index 58f97510a2..4aa944dd7c 100644 --- a/taipy/gui/utils/_adapter.py +++ b/taipy/gui/utils/_adapter.py @@ -137,7 +137,7 @@ def _run( return ( add(type(result)(tpl_res), result[len(tpl_res) :]) if isinstance(result, (tuple, list)) and isinstance(tpl_res, (tuple, list)) - else tpl_res + else tpl_res # type: ignore[reportReturnType] ) except Exception as e: _warn(f"Cannot run adapter for {var_name}", e) @@ -168,9 +168,9 @@ def __get_id(self, value: t.Any, dig=True) -> str: if isinstance(value, (list, tuple)) and len(value): return self.__get_id(value[0], False) elif hasattr(value, "id"): - return self.__get_id(value.id, False) + return self.__get_id(t.cast(t.Any, value).id, False) elif hasattr(value, "__getitem__") and "id" in value: - return self.__get_id(value.get("id"), False) + return self.__get_id(t.cast(dict, value).get("id"), False) if value is not None and type(value).__name__ not in self.__warning_by_type: _warn(f"LoV id must be a string, using a string representation of {type(value)}.") self.__warning_by_type.add(type(value).__name__) @@ -183,9 +183,9 @@ def __get_label(self, value: t.Any, dig=True) -> t.Union[str, t.Dict, None]: if isinstance(value, (list, tuple)) and len(value) > 1: return self.__get_label(value[1], False) elif hasattr(value, "label"): - return self.__get_label(value.label, False) + return self.__get_label(t.cast(t.Any, value).label, False) elif hasattr(value, "__getitem__") and "label" in value: - return self.__get_label(value["label"], False) + return self.__get_label(t.cast(dict, value).get("label"), False) return None def __get_children(self, value: t.Any) -> t.Optional[t.List[t.Any]]: @@ -193,18 +193,18 @@ def __get_children(self, value: t.Any) -> t.Optional[t.List[t.Any]]: return value[2] if isinstance(value[2], list) else None if value[2] is None else [value[2]] elif hasattr(value, "children"): return ( - value.children - if isinstance(value.children, list) + t.cast(t.Any, value).children + if isinstance(t.cast(t.Any, value).children, list) else None - if value.children is None - else [value.children] + if t.cast(t.Any, value).children is None + else [t.cast(t.Any, value).children] ) elif hasattr(value, "__getitem__") and "children" in value: return ( - value["children"] - if isinstance(value["children"], list) + t.cast(dict, value).get("children") + if isinstance(t.cast(dict, value).get("children"), list) else None - if value["children"] is None - else [value["children"]] + if t.cast(dict, value).get("children") is None + else [t.cast(dict, value).get("children")] ) return None diff --git a/taipy/gui/utils/_bindings.py b/taipy/gui/utils/_bindings.py index 2139888dac..6189245ccb 100644 --- a/taipy/gui/utils/_bindings.py +++ b/taipy/gui/utils/_bindings.py @@ -40,7 +40,7 @@ def _bind(self, name: str, value: t.Any) -> None: def __get_property(self, name): def __setter(ud: _Bindings, value: t.Any): if isinstance(value, _MapDict): - value._update_var = None + value._update_var = None # type: ignore[assignment] elif isinstance(value, dict): value = _MapDict(value, None) ud.__gui._update_var(name, value) diff --git a/taipy/gui/utils/_evaluator.py b/taipy/gui/utils/_evaluator.py index bcafc8547b..a5aa3c83a7 100644 --- a/taipy/gui/utils/_evaluator.py +++ b/taipy/gui/utils/_evaluator.py @@ -242,7 +242,7 @@ def evaluate_expr( ctx.update(self.__global_ctx) # entries in var_val are not always seen (NameError) when passed as locals ctx.update(var_val) - with gui._get_autorization(): + with gui._get_authorization(): expr_evaluated = eval(not_encoded_expr if is_edge_case else expr_string, ctx) except Exception as e: _warn(f"Cannot evaluate expression '{not_encoded_expr if is_edge_case else expr_string}'", e) diff --git a/taipy/gui/utils/_map_dict.py b/taipy/gui/utils/_map_dict.py index c6ba4db0a0..8c1cae7d10 100644 --- a/taipy/gui/utils/_map_dict.py +++ b/taipy/gui/utils/_map_dict.py @@ -22,16 +22,16 @@ class _MapDict(object): __local_vars = ("_dict", "_update_var") - def __init__(self, dict_import: dict, app_update_var=None): + def __init__(self, dict_import: dict, app_update_var: t.Optional[t.Callable]=None): self._dict = dict_import # Bind app update var function - self._update_var = app_update_var + self._update_var = t.cast(t.Callable, app_update_var) def __len__(self): return self._dict.__len__() def __length_hint__(self): - return self._dict.__length_hint__() + return self._dict.__length_hint__() # type: ignore[reportAttributeAccessIssue] def __getitem__(self, key): value = self._dict.__getitem__(key) @@ -52,7 +52,7 @@ def __delitem__(self, key): self._dict.__delitem__(key) def __missing__(self, key): - return self._dict.__missing__(key) + return self._dict.__missing__(key) # type: ignore[reportAttributeAccessIssue] def __iter__(self): return self._dict.__iter__() diff --git a/taipy/gui/utils/_runtime_manager.py b/taipy/gui/utils/_runtime_manager.py index 44877ef706..383afb1c57 100644 --- a/taipy/gui/utils/_runtime_manager.py +++ b/taipy/gui/utils/_runtime_manager.py @@ -22,8 +22,8 @@ def __init__(self) -> None: self.__port_gui: t.Dict[int, "Gui"] = {} def add_gui(self, gui: "Gui", port: int): - if port in self.__port_gui: - self.__port_gui[port].stop() + if gui_port := self.__port_gui.get(port): + gui_port.stop() self.__port_gui[port] = gui def get_used_port(self): diff --git a/taipy/gui/utils/_variable_directory.py b/taipy/gui/utils/_variable_directory.py index ad38e7c6bb..4d58442681 100644 --- a/taipy/gui/utils/_variable_directory.py +++ b/taipy/gui/utils/_variable_directory.py @@ -36,7 +36,7 @@ def add_frame(self, frame: t.Optional[FrameType]) -> None: module_name = _get_module_name_from_frame(frame) if module_name not in self._imported_var_dir: imported_var_list = _get_imported_var(frame) - self._imported_var_dir[module_name] = imported_var_list + self._imported_var_dir[t.cast(str, module_name)] = imported_var_list def pre_process_module_import_all(self) -> None: for imported_dir in self._imported_var_dir.values(): @@ -54,7 +54,7 @@ def pre_process_module_import_all(self) -> None: def process_imported_var(self) -> None: self.pre_process_module_import_all() - default_imported_dir = self._imported_var_dir[self._default_module] + default_imported_dir = self._imported_var_dir[t.cast(str, self._default_module)] with self._locals_context.set_locals_context(self._default_module): for name, asname, module in default_imported_dir: if name == "*" and asname == "*": @@ -85,7 +85,7 @@ def process_imported_var(self) -> None: def add_var(self, name: str, module: t.Optional[str], var_name: t.Optional[str] = None) -> str: if module is None: - module = self._default_module + module = t.cast(str, self._default_module) if gv := self.get_var(name, module): return gv var_encode = _variable_encode(name, module) if module != self._default_module else name @@ -95,7 +95,7 @@ def add_var(self, name: str, module: t.Optional[str], var_name: t.Optional[str] if var_encode != var_name: var_name_decode, module_decode = _variable_decode(var_name) if module_decode is None: - module_decode = self._default_module + module_decode = t.cast(str, self._default_module) self.__add_var_head(var_name_decode, module_decode, var_encode) if name not in self._var_dir: self._var_dir[name] = {module: var_name} diff --git a/taipy/gui/utils/chart_config_builder.py b/taipy/gui/utils/chart_config_builder.py index 8d871137e8..12e4942aeb 100644 --- a/taipy/gui/utils/chart_config_builder.py +++ b/taipy/gui/utils/chart_config_builder.py @@ -207,7 +207,10 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t decimators.append(None) # set default columns if not defined - icols = [[c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in col_dict.keys()] if c2] for i in range(len(traces))] + icols = [ + [c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in t.cast(dict, col_dict).keys()] if c2] + for i in range(len(traces)) + ] for i, tr in enumerate(traces): if i < len(axis): diff --git a/taipy/gui/utils/datatype.py b/taipy/gui/utils/datatype.py index 197bf6fadf..f6b89bad0f 100644 --- a/taipy/gui/utils/datatype.py +++ b/taipy/gui/utils/datatype.py @@ -22,4 +22,4 @@ def _get_data_type(value): return "int" elif pd.api.types.is_float_dtype(value): return "float" - return re.match(r"^", str(type(value))).group(2) + return re.match(r"^", str(type(value))).group(2) # type: ignore[reportOptionalMemberAccess] diff --git a/taipy/gui/utils/isnotebook.py b/taipy/gui/utils/isnotebook.py index c9ef17a08a..05f9d72937 100644 --- a/taipy/gui/utils/isnotebook.py +++ b/taipy/gui/utils/isnotebook.py @@ -17,7 +17,7 @@ def _is_in_notebook(): # pragma: no cover if not util.find_spec("IPython"): return False - from IPython import get_ipython + from IPython import get_ipython # type: ignore[reportPrivateImportUsage] ipython = get_ipython() diff --git a/taipy/gui_core/__init__.py b/taipy/gui_core/__init__.py index 61ef2419e0..68565af06f 100644 --- a/taipy/gui_core/__init__.py +++ b/taipy/gui_core/__init__.py @@ -14,5 +14,5 @@ This package provides classes that can be used in GUI controls dedicated to scenario management. """ -from ._adapters import CustomScenarioFilter, DataNodeFilter, DataNodeScenarioFilter, ScenarioFilter from ._init import * +from .filters import CustomScenarioFilter, DataNodeFilter, DataNodeScenarioFilter, ScenarioFilter diff --git a/taipy/gui_core/_adapters.py b/taipy/gui_core/_adapters.py index b48a2ceb4f..e28d0327ae 100644 --- a/taipy/gui_core/_adapters.py +++ b/taipy/gui_core/_adapters.py @@ -43,11 +43,13 @@ from taipy.gui.gui import _DoNotUpdate from taipy.gui.utils import _is_boolean, _is_true, _TaipyBase +from .filters import DataNodeFilter, ScenarioFilter, _Filter + # prevent gui from trying to push scenario instances to the front-end class _GuiCoreDoNotUpdate(_DoNotUpdate): def __repr__(self): - return self.get_label() if hasattr(self, "get_label") else super().__repr__() + return self.get_label() if hasattr(self, "get_label") else super().__repr__() # type: ignore[reportAttributeAccessIssue] class _EntityType(Enum): @@ -353,18 +355,18 @@ def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]): # we compare only strings if isinstance(entity, types): if isinstance(entity, Cycle): - lcol = "creation_date" - lfn = None + the_col = "creation_date" + the_fn = None else: - lcol = col - lfn = col_fn + the_col = col + the_fn = col_fn try: - val = attrgetter(lfn or lcol)(entity) - if lfn: + val = attrgetter(the_fn or the_col)(entity) + if the_fn: val = val() except AttributeError as e: if _is_debugging(): - _warn("Attribute", e) + _warn(f"sort_key({entity.id}):", e) val = "" else: val = "" @@ -373,76 +375,17 @@ def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]): return sort_key -@dataclass -class _Filter(_DoNotUpdate): - label: str - property_type: t.Optional[t.Type] - - def get_property(self): - return self.label - - def get_type(self): - if self.property_type is bool: - return "boolean" - elif self.property_type is int or self.property_type is float: - return "number" - elif self.property_type is datetime or self.property_type is date: - return "date" - elif self.property_type is str: - return "str" - return "any" - - -@dataclass -class ScenarioFilter(_Filter): - property_id: str - - def get_property(self): - return self.property_id - - -@dataclass -class DataNodeScenarioFilter(_Filter): - datanode_config_id: str - property_id: str - - def get_property(self): - return f"{self.datanode_config_id}.{self.property_id}" - - -_CUSTOM_PREFIX = "fn:" - - -@dataclass -class CustomScenarioFilter(_Filter): - filter_function: t.Callable[[Scenario], t.Any] - - def __post_init__(self): - if self.filter_function.__name__ == "": - raise TypeError("ScenarioCustomFilter does not support lambda functions.") - mod = self.filter_function.__module__ - self.module = mod if isinstance(mod, str) else mod.__name__ - - def get_property(self): - return f"{_CUSTOM_PREFIX}{self.module}:{self.filter_function.__name__}" - - @staticmethod - def _get_custom(col: str) -> t.Optional[t.List[str]]: - return col[len(_CUSTOM_PREFIX) :].split(":") if col.startswith(_CUSTOM_PREFIX) else None - - -@dataclass -class DataNodeFilter(_Filter): - property_id: str - - def get_property(self): - return self.property_id +@dataclass(frozen=True) +class _GuiCorePropDesc: + filter: _Filter + extended: bool = False + for_sort: bool = False class _GuiCoreProperties(ABC): @staticmethod @abstractmethod - def get_default_list() -> t.List[_Filter]: + def get_default_list() -> t.List[_GuiCorePropDesc]: raise NotImplementedError @staticmethod @@ -454,7 +397,7 @@ def get_enums(self): return {} def get(self): - data = super().get() + data = super().get() # type: ignore[reportAttributeAccessIssue] if _is_boolean(data): if _is_true(data): data = self.get_default_list() @@ -465,18 +408,21 @@ def get(self): if isinstance(data, _Filter): data = (data,) if isinstance(data, (list, tuple)): - flist: t.List[_Filter] = [] # type: ignore[annotation-unchecked] + f_list: t.List[_Filter] = [] # type: ignore[annotation-unchecked] for f in data: if isinstance(f, str): f = f.strip() if f == "*": - flist.extend(p.filter for p in self.get_default_list()) + f_list.extend(p.filter for p in self.get_default_list()) elif f: - flist.append( - next((p.filter for p in self.get_default_list() if p.get_property() == f), _Filter(f)) + f_list.append( + next( + (p.filter for p in self.get_default_list() if p.filter.get_property() == f), + _Filter(f, None), + ) ) elif isinstance(f, _Filter): - flist.append(f) + f_list.append(f) return json.dumps( [ ( @@ -487,19 +433,12 @@ def get(self): ) if self.full_desc() else (attr.label, attr.get_property()) - for attr in flist + for attr in f_list ] ) return None -@dataclass(frozen=True) -class _GuiCorePropDesc: - filter: _Filter - extended: bool = False - for_sort: bool = False - - class _GuiCoreScenarioProperties(_GuiCoreProperties): _SC_PROPS: t.List[_GuiCorePropDesc] = [ _GuiCorePropDesc(ScenarioFilter("Config id", str, "config_id"), for_sort=True), diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py index 53f8bd8e59..2c40a3854a 100644 --- a/taipy/gui_core/_context.py +++ b/taipy/gui_core/_context.py @@ -67,13 +67,13 @@ from taipy.gui.utils._map_dict import _MapDict from ._adapters import ( - CustomScenarioFilter, _EntityType, _get_entity_property, _GuiCoreDatanodeAdapter, _GuiCoreScenarioProperties, _invoke_action, ) +from .filters import CustomScenarioFilter class _GuiCoreContext(CoreEventConsumerBase): @@ -113,20 +113,20 @@ def __lazy_start(self): def process_event(self, event: Event): self.__lazy_start() - if event.entity_type == EventEntityType.SCENARIO: - with self.gui._get_autorization(system=True): + if event.entity_type is EventEntityType.SCENARIO: + with self.gui._get_authorization(system=True): self.scenario_refresh( event.entity_id - if event.operation == EventOperation.DELETION or is_readable(t.cast(ScenarioId, event.entity_id)) + if event.operation is EventOperation.DELETION or is_readable(t.cast(ScenarioId, event.entity_id)) else None ) - elif event.entity_type == EventEntityType.SEQUENCE and event.entity_id: + elif event.entity_type is EventEntityType.SEQUENCE and event.entity_id: sequence = None try: - with self.gui._get_autorization(system=True): + with self.gui._get_authorization(system=True): sequence = ( core_get(event.entity_id) - if event.operation != EventOperation.DELETION + if event.operation is not EventOperation.DELETION and is_readable(t.cast(SequenceId, event.entity_id)) else None ) @@ -134,13 +134,15 @@ def process_event(self, event: Event): self.broadcast_core_changed({"scenario": list(sequence.parent_ids)}) # type: ignore except Exception as e: _warn(f"Access to sequence {event.entity_id} failed", e) - elif event.entity_type == EventEntityType.JOB: + elif event.entity_type is EventEntityType.JOB: with self.lock: self.jobs_list = None - self.broadcast_core_changed({"jobs": event.entity_id}) - elif event.entity_type == EventEntityType.SUBMISSION: + # no broadcast because the submission status will do the job + if event.operation is EventOperation.DELETION: + self.broadcast_core_changed({"jobs": True}) + elif event.entity_type is EventEntityType.SUBMISSION: self.submission_status_callback(event.entity_id, event) - elif event.entity_type == EventEntityType.DATA_NODE: + elif event.entity_type is EventEntityType.DATA_NODE: with self.lock: self.data_nodes_by_owner = None self.broadcast_core_changed( @@ -178,9 +180,9 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even client_id = submission.properties.get("client_id") if client_id: running_tasks = {} - with self.gui._get_autorization(client_id): + with self.gui._get_authorization(client_id): for job in submission.jobs: - job = job if isinstance(job, Job) else core_get(job) + job = job if isinstance(job, Job) else t.cast(Job, core_get(job)) running_tasks[job.task.id] = ( SubmissionStatus.RUNNING.value if job.is_running() @@ -190,7 +192,7 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even ) payload.update(tasks=running_tasks) - if last_status != new_status: + if last_status is not new_status: # callback submission_name = submission.properties.get("on_submission") if submission_name: @@ -308,7 +310,7 @@ def get_sorted_scenario_list( def get_filtered_scenario_list( self, - entities: t.List[t.Union[t.List, Scenario]], + entities: t.List[t.Union[t.List, Scenario, None]], filters: t.Optional[t.List[t.Dict[str, t.Any]]], ): if not filters: @@ -340,13 +342,15 @@ def get_filtered_scenario_list( e for e in filtered_list if not isinstance(e, Scenario) - or _invoke_action(e, col, col_type, is_datanode_prop, action, val, col_fn) + or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn) ] # level 2 filtering filtered_list = [ e if isinstance(e, Scenario) - else self.filter_entities(e, col, col_type, is_datanode_prop, action, val, col_fn) + else self.filter_entities( + t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn + ) for e in filtered_list ] # remove empty cycles @@ -414,17 +418,17 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no or not isinstance(args[start_idx + 2], dict) ): return - error_var = payload.get("error_id") + error_var = t.cast(str, payload.get("error_id")) update = args[start_idx] delete = args[start_idx + 1] - data = args[start_idx + 2] + data = t.cast(dict, args[start_idx + 2]) with_dialog = True if len(args) < start_idx + 4 else bool(args[start_idx + 3]) scenario = None user_scenario = None name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME) if update: - scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) + scenario_id = t.cast(ScenarioId, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) if delete: if not (reason := is_deletable(scenario_id)): state.assign(error_var, f"Scenario. {scenario_id} is not deletable: {_get_reason(reason)}.") @@ -454,11 +458,11 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no scenario_config = None date = None scenario_id = None + gui = state.get_gui() try: - gui = state.get_gui() on_creation = args[0] if isinstance(args[0], str) else None on_creation_function = gui._get_user_function(on_creation) if on_creation else None - if callable(on_creation_function): + if callable(on_creation_function) and on_creation: try: res = gui._call_function_with_state( on_creation_function, @@ -503,9 +507,8 @@ def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # no + f"({len(Config.scenarios) - 1}) found.", ) return - - scenario = create_scenario(scenario_config, date, name) - scenario_id = scenario.id + scenario = create_scenario(scenario_config, date, name) if scenario_config else None + scenario_id = scenario.id if scenario else None except Exception as e: state.assign(error_var, f"Error creating Scenario. {e}") finally: @@ -547,17 +550,17 @@ def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]): if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return error_var = payload.get("error_id") - data = args[0] - entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) + data = t.cast(dict, args[0]) + entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) sequence = data.get("sequence") if not self.__check_readable_editable(state, entity_id, "Scenario", error_var): return - scenario: Scenario = core_get(entity_id) + scenario = t.cast(Scenario, core_get(entity_id)) if scenario: try: if not sequence: if isinstance(sequence, str) and (name := data.get(_GuiCoreContext.__PROP_ENTITY_NAME)): - scenario.add_sequence(name, data.get("task_ids")) + scenario.add_sequence(name, t.cast(list, data.get("task_ids"))) else: primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY) if primary is True: @@ -572,11 +575,11 @@ def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]): if data.get("del", False): scenario.remove_sequence(sequence) else: - name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME) + name = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_NAME)) if sequence != name: scenario.rename_sequence(sequence, name) if seqEntity := scenario.sequences.get(name): - seqEntity.tasks = data.get("task_ids") + seqEntity.tasks = t.cast(list, data.get("task_ids")) self.__edit_properties(seqEntity, data) else: _GuiCoreContext.__assign_var( @@ -595,13 +598,13 @@ def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]): args = payload.get("args") if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return - data = args[0] + data = t.cast(dict, args[0]) error_var = payload.get("error_id") try: - scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) - entity = core_get(scenario_id) - if sequence := data.get("sequence"): - entity = entity.sequences.get(sequence) + scenario_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) + entity = t.cast(Scenario, core_get(scenario_id)) + if sequence := t.cast(str, data.get("sequence")): + entity = t.cast(Sequence, entity.sequences.get(sequence)) if not (reason := is_submittable(entity)): _GuiCoreContext.__assign_var( @@ -631,7 +634,7 @@ def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]): def get_filtered_datanode_list( self, - entities: t.List[t.Union[t.List, DataNode]], + entities: t.List[t.Union[t.List, DataNode, None]], filters: t.Optional[t.List[t.Dict[str, t.Any]]], ): if not filters or not entities: @@ -660,13 +663,16 @@ def get_filtered_datanode_list( filtered_list = [ e for e in filtered_list - if not isinstance(e, DataNode) or _invoke_action(e, col, col_type, False, action, val, col_fn) + if not isinstance(e, DataNode) + or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn) ] # level 3 filtering filtered_list = [ - e if isinstance(e, DataNode) else self.filter_entities(d, col, col_type, False, action, val, col_fn) + e + if isinstance(e, DataNode) + else self.filter_entities(d, t.cast(str, col), col_type, False, action, val, col_fn) for e in filtered_list - for d in e[2] + for d in t.cast(list, t.cast(list, e)[2]) ] # remove empty cycles return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))] @@ -724,8 +730,8 @@ def get_datanodes_tree( base_list = [] else: base_list = datanodes - adapted_list = self.get_sorted_datanode_list(base_list, sorts) - return self.get_filtered_datanode_list(adapted_list, filters) + adapted_list = self.get_sorted_datanode_list(t.cast(list, base_list), sorts) + return self.get_filtered_datanode_list(t.cast(list, adapted_list), filters) def data_node_adapter( self, @@ -737,8 +743,8 @@ def data_node_adapter( if isinstance(data, tuple): raise NotImplementedError if isinstance(data, list): - if data[2] and isinstance(data[2][0], (Cycle, Scenario, Sequence, DataNode)): - data[2] = self.get_sorted_datanode_list(data[2], sorts, False) + if data[2] and isinstance(t.cast(list, data[2])[0], (Cycle, Scenario, Sequence, DataNode)): + data[2] = self.get_sorted_datanode_list(t.cast(list, data[2]), sorts, False) return data try: if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None: @@ -770,7 +776,7 @@ def data_node_adapter( data.id, data.get_simple_label(), self.get_sorted_datanode_list( - self.data_nodes_by_owner.get(data.id, []) + list(data.sequences.values()), + t.cast(list, self.data_nodes_by_owner.get(data.id, []) + list(data.sequences.values())), sorts, False, ), @@ -828,7 +834,7 @@ def act_on_jobs(self, state: State, id: str, payload: t.Dict[str, str]): args = payload.get("args") if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return - data = args[0] + data = t.cast(dict, args[0]) job_ids = data.get(_GuiCoreContext.__PROP_ENTITY_ID) job_action = data.get(_GuiCoreContext.__ACTION) if job_action and isinstance(job_ids, list): @@ -879,7 +885,7 @@ def get_job_details(self, job_id: t.Optional[JobId]): [] if job.stacktrace is None else job.stacktrace, ) except Exception as e: - _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e) + _warn(f"Access to job ({job_id}) failed", e) return None def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]): @@ -888,11 +894,11 @@ def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]): if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return error_var = payload.get("error_id") - data = args[0] - entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) + data = t.cast(dict, args[0]) + entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) if not self.__check_readable_editable(state, entity_id, "DataNode", error_var): return - entity: DataNode = core_get(entity_id) + entity = t.cast(DataNode, core_get(entity_id)) if isinstance(entity, DataNode): try: self.__edit_properties(entity, data) @@ -905,13 +911,13 @@ def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str args = payload.get("args") if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return - data = args[0] + data = t.cast(dict, args[0]) error_var = payload.get("error_id") - entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) + entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) if not self.__check_readable_editable(state, entity_id, "Data node", error_var): return lock = data.get("lock", True) - entity: DataNode = core_get(entity_id) + entity = t.cast(DataNode, core_get(entity_id)) if isinstance(entity, DataNode): try: if lock: @@ -937,13 +943,13 @@ def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: props = data.get("properties") if isinstance(props, (list, tuple)): for prop in props: - key = prop.get("key") + key = t.cast(dict, prop).get("key") if key and key not in _GuiCoreContext.__ENTITY_PROPS: - ent.properties[key] = prop.get("value") + ent.properties[key] = t.cast(dict, prop).get("value") deleted_props = data.get("deleted_properties") if isinstance(deleted_props, (list, tuple)): for prop in deleted_props: - key = prop.get("key") + key = t.cast(dict, prop).get("key") if key and key not in _GuiCoreContext.__ENTITY_PROPS: ent.properties.pop(key, None) @@ -1006,23 +1012,24 @@ def update_data(self, state: State, id: str, payload: t.Dict[str, str]): args = payload.get("args") if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return - data = args[0] + data = t.cast(dict, args[0]) error_var = payload.get("error_id") - entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID) + entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) if not self.__check_readable_editable(state, entity_id, "Data node", error_var): return - entity: DataNode = core_get(entity_id) + entity = t.cast(DataNode, core_get(entity_id)) if isinstance(entity, DataNode): try: + val = t.cast(str, data.get("value")) entity.write( - parser.parse(data.get("value")) + parser.parse(val) if data.get("type") == "date" - else int(data.get("value")) + else int(val) if data.get("type") == "int" - else float(data.get("value")) + else float(val) if data.get("type") == "float" else data.get("value"), - comment=data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT), + comment=t.cast(dict, data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT)), ) entity.unlock_edit(self.gui._get_client_id()) _GuiCoreContext.__assign_var(state, error_var, "") @@ -1184,7 +1191,7 @@ def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]): try: entity = ( core_get(args[0]) - if (reason := is_readable(args[0])) + if (reason := is_readable(t.cast(ScenarioId, args[0]))) else f"{args[0]} is not readable: {_get_reason(reason)}" ) self.gui._call_function_with_state( diff --git a/taipy/gui_core/filters.py b/taipy/gui_core/filters.py new file mode 100644 index 0000000000..94870968d2 --- /dev/null +++ b/taipy/gui_core/filters.py @@ -0,0 +1,99 @@ +# 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 typing as t +from dataclasses import dataclass +from datetime import date, datetime + +from taipy.core import Scenario +from taipy.gui.gui import _DoNotUpdate + + +@dataclass +class _Filter(_DoNotUpdate): + label: str + property_type: t.Optional[t.Type] + + def get_property(self): + return self.label + + def get_type(self): + if self.property_type is bool: + return "boolean" + elif self.property_type is int or self.property_type is float: + return "number" + elif self.property_type is datetime or self.property_type is date: + return "date" + elif self.property_type is str: + return "str" + return "any" + + +@dataclass +class ScenarioFilter(_Filter): + """ + used to describe a filter on a scenario property + """ + + property_id: str + + def get_property(self): + return self.property_id + + +@dataclass +class DataNodeScenarioFilter(_Filter): + """ + used to describe a filter on a scenario datanode's property + """ + + datanode_config_id: str + property_id: str + + def get_property(self): + return f"{self.datanode_config_id}.{self.property_id}" + + +_CUSTOM_PREFIX = "fn:" + + +@dataclass +class CustomScenarioFilter(_Filter): + """ + used to describe a custom scenario filter ie based on a user defined function + """ + + filter_function: t.Callable[[Scenario], t.Any] + + def __post_init__(self): + if self.filter_function.__name__ == "": + raise TypeError("CustomScenarioFilter does not support lambda functions.") + mod = self.filter_function.__module__ + self.module = mod if isinstance(mod, str) else mod.__name__ + + def get_property(self): + return f"{_CUSTOM_PREFIX}{self.module}:{self.filter_function.__name__}" + + @staticmethod + def _get_custom(col: str) -> t.Optional[t.List[str]]: + return col[len(_CUSTOM_PREFIX) :].split(":") if col.startswith(_CUSTOM_PREFIX) else None + + +@dataclass +class DataNodeFilter(_Filter): + """ + used to describe a filter on a datanode property + """ + + property_id: str + + def get_property(self): + return self.property_id diff --git a/tests/gui_core/test_context_is_readable.py b/tests/gui_core/test_context_is_readable.py index cd9b389cfe..2514bc18c6 100644 --- a/tests/gui_core/test_context_is_readable.py +++ b/tests/gui_core/test_context_is_readable.py @@ -178,7 +178,7 @@ def test_submission_status_callback(self): with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget: mockget.reset_mock() mockGui = Mock(Gui) - mockGui._get_autorization = lambda s: contextlib.nullcontext() + mockGui._get_authorization = lambda s: contextlib.nullcontext() gui_core_context = _GuiCoreContext(mockGui) def sub_cb(): From cdbe08740f2625ec471d42572f877d9f42b63136 Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Fri, 20 Sep 2024 15:33:56 +0700 Subject: [PATCH 09/10] export taipy gui base (#1811) --- frontend/taipy-gui/base/src/exports.ts | 10 +++++----- .../base/src/packaging/taipy-gui-base.d.ts | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/frontend/taipy-gui/base/src/exports.ts b/frontend/taipy-gui/base/src/exports.ts index 9f702a4390..ff1f70f524 100644 --- a/frontend/taipy-gui/base/src/exports.ts +++ b/frontend/taipy-gui/base/src/exports.ts @@ -1,7 +1,7 @@ +import { TaipyApp, createApp, OnChangeHandler, OnInitHandler } from "./app"; import { WsAdapter } from "./wsAdapter"; -// import { TaipyApp } from "./app"; +import { ModuleData } from "./dataManager"; -export { - WsAdapter, - // TaipyApp, -}; +export default TaipyApp; +export { TaipyApp, createApp, WsAdapter }; +export type { OnChangeHandler, OnInitHandler, ModuleData }; diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index ee9a76411c..3966d59eff 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -85,6 +85,10 @@ export interface WsMessage { module_context: string; ack_id?: string; } +export declare abstract class WsAdapter { + abstract supportedMessageTypes: string[]; + abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean; +} export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void; @@ -163,7 +167,11 @@ export declare class TaipyApp { getWsStatus(): string[]; getBaseUrl(): string; } -export declare abstract class WsAdapter { - abstract supportedMessageTypes: string[]; - abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean; -} +export declare const createApp: ( + onInit?: OnInitHandler, + onChange?: OnChangeHandler, + path?: string, + socket?: Socket, +) => TaipyApp; + +export { TaipyApp as default }; From cdc9561fe43092e21159fd6189f73eb59f6cde87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:34:48 +0200 Subject: [PATCH 10/10] chat support markdown pre raw (#1810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support markdown pre in chat messages * test * test lazy markdown * jest with react-markdown * do not ignore react-markdown take 2 * test with react-markdown --------- Co-authored-by: Fred Lefévère-Laoide --- frontend/taipy-gui/jest.config.js | 3 +- .../src/components/Taipy/Chat.spec.tsx | 39 +++++++---- .../taipy-gui/src/components/Taipy/Chat.tsx | 65 +++++++++++++------ .../src/components/Taipy/Field.spec.tsx | 12 +++- taipy/gui/_renderers/factory.py | 1 + taipy/gui/viselements.json | 6 ++ 6 files changed, 91 insertions(+), 35 deletions(-) diff --git a/frontend/taipy-gui/jest.config.js b/frontend/taipy-gui/jest.config.js index 81649289f3..28381eb754 100644 --- a/frontend/taipy-gui/jest.config.js +++ b/frontend/taipy-gui/jest.config.js @@ -26,6 +26,7 @@ module.exports = { ], coverageReporters: ["json", "html", "text"], modulePathIgnorePatterns: ["/packaging/"], - transformIgnorePatterns: ["/node_modules/(?!react-jsx-parser/)"], + moduleNameMapper: {"react-markdown": "/node_modules/react-markdown/react-markdown.min.js"}, + transformIgnorePatterns: ["/node_modules/(?!react-jsx-parser|react-markdown/)"], ...createJsWithTsPreset() }; diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx index aaebc2b912..7348c411eb 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx @@ -12,7 +12,7 @@ */ import React from "react"; -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; @@ -39,48 +39,63 @@ const searchMsg = messages[valueKey].data[0][1]; describe("Chat Component", () => { it("renders", async () => { - const { getByText, getByLabelText } = render(); + const { getByText, getByLabelText } = render(); const elt = getByText(searchMsg); expect(elt.tagName).toBe("DIV"); const input = getByLabelText("message (taipy)"); expect(input.tagName).toBe("INPUT"); }); it("uses the class", async () => { - const { getByText } = render(); + const { getByText } = render(); const elt = getByText(searchMsg); expect(elt.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.parentElement).toHaveClass("taipy-chat"); }); it("can display an avatar", async () => { - const { getByAltText } = render(); + const { getByAltText } = render(); const elt = getByAltText("Fred.png"); expect(elt.tagName).toBe("IMG"); }); it("is disabled", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).toHaveClass("Mui-disabled")); }); it("is enabled by default", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); it("is enabled by active", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); it("can hide input", async () => { - render(); + render(); const elt = document.querySelector(".taipy-chat input"); expect(elt).toBeNull(); }); + it("renders markdown by default", async () => { + render(); + const elt = document.querySelector(".taipy-chat .taipy-chat-received .MuiPaper-root"); + await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull()); + }); + it("can render pre", async () => { + render(); + const elt = document.querySelector(".taipy-chat .taipy-chat-received .MuiPaper-root pre"); + expect(elt).toBeInTheDocument(); + }); + it("can render raw", async () => { + render(); + const elt = document.querySelector(".taipy-chat .taipy-chat-received div.MuiPaper-root"); + expect(elt).toBeInTheDocument(); + }); it("dispatch a well formed message by Keyboard", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByLabelText } = render( - + ); const elt = getByLabelText("message (taipy)"); @@ -92,7 +107,7 @@ describe("Chat Component", () => { context: undefined, payload: { action: undefined, - args: ["Enter", "varname", "new message", "taipy"], + args: ["Enter", "varName", "new message", "taipy"], }, }); }); @@ -101,7 +116,7 @@ describe("Chat Component", () => { const state: TaipyState = INITIAL_STATE; const { getByLabelText, getByRole } = render( - + ); const elt = getByLabelText("message (taipy)"); @@ -114,7 +129,7 @@ describe("Chat Component", () => { context: undefined, payload: { action: undefined, - args: ["click", "varname", "new message", "taipy"], + args: ["click", "varName", "new message", "taipy"], }, }); }); diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.tsx index 55acb528c2..44449b99a8 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chat.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chat.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { useMemo, useCallback, KeyboardEvent, MouseEvent, useState, useRef, useEffect, ReactNode } from "react"; +import React, { useMemo, useCallback, KeyboardEvent, MouseEvent, useState, useRef, useEffect, ReactNode, lazy } from "react"; import { SxProps, Theme, darken, lighten } from "@mui/material/styles"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; @@ -28,8 +28,6 @@ import Send from "@mui/icons-material/Send"; import ArrowDownward from "@mui/icons-material/ArrowDownward"; import ArrowUpward from "@mui/icons-material/ArrowUpward"; -// import InfiniteLoader from "react-window-infinite-loader"; - import { createRequestInfiniteTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers"; import { TaipyActiveProps, disableColor, getSuffixedClassNames } from "./utils"; import { useClassNames, useDispatch, useDynamicProperty, useElementVisible, useModule } from "../../utils/hooks"; @@ -39,6 +37,8 @@ import { emptyArray, getInitials } from "../../utils"; import { RowType, TableValueType } from "./tableUtils"; import { Stack } from "@mui/material"; +const Markdown = lazy(() => import("react-markdown")); + interface ChatProps extends TaipyActiveProps { messages?: TableValueType; withInput?: boolean; @@ -50,6 +50,7 @@ interface ChatProps extends TaipyActiveProps { defaultKey?: string; // for testing purposes only pageSize?: number; showSender?: boolean; + mode?: string; } const ENTER_KEY = "Enter"; @@ -66,7 +67,13 @@ const gridSx = { pb: "1em", mt: "unset", flex: 1, overflow: "auto" }; const loadMoreSx = { width: "fit-content", marginLeft: "auto", marginRight: "auto" }; const inputSx = { maxWidth: "unset" }; const leftNameSx = { fontSize: "0.6em", fontWeight: "bolder", pl: `${indicWidth}em` }; -const rightNameSx: SxProps = { ...leftNameSx, pr: `${2 * indicWidth}em`, width: "100%", display: "flex", justifyContent: "flex-end" }; +const rightNameSx: SxProps = { + ...leftNameSx, + pr: `${2 * indicWidth}em`, + width: "100%", + display: "flex", + justifyContent: "flex-end", +}; const senderPaperSx = { pr: `${indicWidth}em`, pl: `${indicWidth}em`, @@ -127,10 +134,11 @@ interface ChatRowProps { getAvatar: (id: string, sender: boolean) => ReactNode; index: number; showSender: boolean; + mode?: string; } const ChatRow = (props: ChatRowProps) => { - const { senderId, message, name, className, getAvatar, index, showSender } = props; + const { senderId, message, name, className, getAvatar, index, showSender, mode } = props; const sender = senderId == name; const avatar = getAvatar(name, sender); @@ -149,14 +157,26 @@ const ChatRow = (props: ChatRowProps) => { {name} - {message} + {mode == "pre" ? ( +
{message}
+ ) : mode == "raw" ? ( + message + ) : ( + {message} + )}
{sender ? {avatar} : null} ) : ( - {message} + {mode == "pre" ? ( +
{message}
+ ) : mode == "raw" ? ( + message + ) : ( + {message} + )}
)}
@@ -385,6 +405,7 @@ const Chat = (props: ChatProps) => { getAvatar={getAvatar} index={idx} showSender={showSender} + mode={props.mode} /> ) : null )} @@ -406,20 +427,22 @@ const Chat = (props: ChatProps) => { label={`message (${senderId})`} disabled={!active} onKeyDown={handleAction} - slotProps={{input: { - endAdornment: ( - - - - - - ), - }}} + slotProps={{ + input: { + endAdornment: ( + + + + + + ), + }, + }} sx={inputSx} /> ) : null} diff --git a/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx index cb16a03fc3..d42de9e77f 100644 --- a/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Field.spec.tsx @@ -12,7 +12,7 @@ */ import React from "react"; -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import Field from "./Field"; @@ -60,4 +60,14 @@ describe("Field Component", () => { const elt = getByText("titi"); expect(elt).toHaveStyle("width: 500px"); }); + it("can render markdown", async () => { + render(); + const elt = document.querySelector(".taipy-text"); + await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull()); + }); + it("can render pre", async () => { + render(); + const elt = document.querySelector("pre.taipy-text"); + expect(elt).toBeInTheDocument(); + }); }); diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 88144c7c8c..d1f3301559 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -99,6 +99,7 @@ class _Factory: ("height",), ("page_size", PropertyType.number, 50), ("show_sender", PropertyType.boolean, False), + ("mode",), ] ), "chart": lambda gui, control_type, attrs: _Builder( diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 33051cdeee..d1838d2aa1 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1661,6 +1661,12 @@ "type": "bool", "default_value": "False", "doc": "If True, the sender avatar and name are displayed." + }, + { + "name": "mode", + "type": "str", + "default_value": "\"markdown\"", + "doc": "Define the way the messages are processed:\n
  • "raw" no processing
  • "pre": keeps spaces and new lines
  • "markdown" or "md": basic support for Markdown.
" } ] }