Browse Source

Merge branch 'develop' into bug/#1703-number-visual-element-does-not-update-when-using-arrow-to-change-value

namnguyen 9 months ago
parent
commit
3d7889b18e
48 changed files with 597 additions and 421 deletions
  1. 4 3
      .github/workflows/build-and-release-single-package.yml
  2. 0 6
      .github/workflows/build-and-release.yml
  3. 1 1
      .github/workflows/trigger-benchmark.yml
  4. 1 1
      Pipfile
  5. 56 57
      frontend/taipy-gui/package-lock.json
  6. 26 21
      frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
  7. 2 14
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx
  8. 23 19
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
  9. 80 91
      frontend/taipy/package-lock.json
  10. 2 1
      frontend/taipy/src/DataNodeTable.tsx
  11. 5 2
      frontend/taipy/src/JobSelector.tsx
  12. 9 4
      taipy/config/config.py
  13. 1 1
      taipy/core/common/warn_if_inputs_not_ready.py
  14. 35 0
      taipy/core/config/checkers/_scenario_config_checker.py
  15. 0 6
      taipy/core/cycle/cycle.py
  16. 2 8
      taipy/core/data/data_node.py
  17. 1 1
      taipy/core/data/parquet.py
  18. 7 0
      taipy/core/exceptions/exceptions.py
  19. 23 10
      taipy/core/scenario/scenario.py
  20. 13 4
      taipy/core/sequence/sequence.py
  21. 13 3
      taipy/core/task/task.py
  22. 86 70
      taipy/gui_core/_context.py
  23. 69 11
      tests/core/config/checkers/test_scenario_config_checker.py
  24. 5 4
      tests/core/cycle/test_cycle.py
  25. 1 1
      tests/core/cycle/test_cycle_manager.py
  26. 4 4
      tests/core/data/test_csv_data_node.py
  27. 2 2
      tests/core/data/test_data_manager.py
  28. 0 1
      tests/core/data/test_data_node.py
  29. 18 18
      tests/core/data/test_excel_data_node.py
  30. 0 1
      tests/core/data/test_json_data_node.py
  31. 4 4
      tests/core/data/test_parquet_data_node.py
  32. 1 1
      tests/core/data/test_read_excel_data_node.py
  33. 5 5
      tests/core/data/test_sql_data_node.py
  34. 3 3
      tests/core/data/test_sql_table_data_node.py
  35. 26 7
      tests/core/scenario/test_scenario.py
  36. 23 6
      tests/core/sequence/test_sequence.py
  37. 1 1
      tests/core/sequence/test_sequence_manager.py
  38. 17 1
      tests/core/task/test_task.py
  39. 2 2
      tests/gui_core/test_context_is_deletable.py
  40. 8 8
      tests/gui_core/test_context_is_editable.py
  41. 2 2
      tests/gui_core/test_context_is_promotable.py
  42. 10 10
      tests/gui_core/test_context_is_readable.py
  43. 1 1
      tools/packages/pipfiles/Pipfile3.10.max
  44. 1 1
      tools/packages/pipfiles/Pipfile3.11.max
  45. 1 1
      tools/packages/pipfiles/Pipfile3.12.max
  46. 1 1
      tools/packages/pipfiles/Pipfile3.8.max
  47. 1 1
      tools/packages/pipfiles/Pipfile3.9.max
  48. 1 1
      tools/packages/taipy-gui/setup.requirements.txt

+ 4 - 3
.github/workflows/build-and-release-single-package.yml

@@ -51,8 +51,6 @@ jobs:
           ${{ github.event.inputs.target_package }} >> $GITHUB_OUTPUT
 
   build-and-release-package:
-    permissions:
-      contents: write
     needs: [fetch-versions]
     timeout-minutes: 20
     runs-on: ubuntu-latest
@@ -159,6 +157,9 @@ jobs:
         working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
           python -m build
+
+      - name: Rename files
+        run: |
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
       - name: Create tag and release
@@ -175,7 +176,7 @@ jobs:
 
       - name: Ensure Taipy release is marked as latest
         run: |
-          gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
+           gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 0 - 6
.github/workflows/build-and-release.yml

@@ -44,12 +44,6 @@ jobs:
         run: |
           python tools/release/setup_version.py ALL ${{ github.event.inputs.release_type }} ${{ github.event.inputs.target_version }} ${{ steps.extract_branch.outputs.branch }} >> $GITHUB_OUTPUT
 
-      - uses: stefanzweifel/git-auto-commit-action@v4
-        with:
-          commit_message: Update version to ${{ steps.version-setup.outputs.NEW_VERSION }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
   build-and-release-taipy-packages:
     needs: [fetch-versions]
     timeout-minutes: 20

+ 1 - 1
.github/workflows/trigger-benchmark.yml

@@ -7,7 +7,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - name: Trigger taipy-integration-testing
+      - name: Trigger taipy-benchmark computation
         uses: peter-evans/repository-dispatch@v1
         with:
           token: ${{secrets.TAIPY_INTEGRATION_TESTING_ACCESS_TOKEN}}

+ 1 - 1
Pipfile

@@ -31,7 +31,7 @@ pytz = "==2021.3"
 simple-websocket = "==0.10.1"
 sqlalchemy = "==2.0.16"
 toml = "==0.10"
-twisted = "==23.8.0"
+twisted = "==24.7.0"
 tzlocal = "==3.0"
 boto3 = "==1.29.1"
 watchdog = "==4.0.0"

+ 56 - 57
frontend/taipy-gui/package-lock.json

@@ -191,9 +191,9 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.4.tgz",
-      "integrity": "sha512-NFtZmZsyzDPJnk9Zg3BbTfKKc9UlHYzD0E//p2Z3B9nCwwtJW9T0gVbCz8+fBngnn4zf1Dr3IK8PHQQHq0lDQw==",
+      "version": "7.25.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
+      "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
       "dependencies": {
         "@babel/types": "^7.25.4",
         "@jridgewell/gen-mapping": "^0.3.5",
@@ -2097,14 +2097,14 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.13.0.tgz",
-      "integrity": "sha512-cmpAfkzOjUgL4I8WenU4elm1QJO8vWpGmIPCezT3Q9wFjGL1QApQhJ5gMZ+X4tM6Gha9AhIWNQX5eXHKbSoyFQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.14.0.tgz",
+      "integrity": "sha512-3xI3xYVxqPU4//KfE4FcR+Zs7UT4kkDPvA+IDOcQdRUyVwmcXCjBuJZgKgJMqSCNK/KIJZQQrpmy5XGHOKTbdA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -2161,12 +2161,12 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.13.0.tgz",
-      "integrity": "sha512-eUK7iykkDWU+wBfTzE/S0qh4awgVgsORfrpvuPbUp+E6qUj1Xhu9M/WKzbwz0CPFnTJZwBQ9KYrxpGXnPBEpRQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.14.0.tgz",
+      "integrity": "sha512-+qWIHLgt2vgH6bKmf7IwRvS86UbZRWKAdDY/yTQJaqzCzyesUvQhD+WRxe1kpdCK8UE061S9/Ju7hLkM4kjRNA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/utils": "^5.16.5"
+        "@mui/utils": "^5.16.6"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -2180,15 +2180,15 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.13.0.tgz",
-      "integrity": "sha512-ADixvp85a0iZ7AOzBuCPQ+yl+gMq0BlIWhg3GfbX+57sMhjcdOEUUxcGIcIt6pw1V05bVXE2/QP+5qzDamiGPw==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.14.0.tgz",
+      "integrity": "sha512-j1sK0tLrsiCu0FxwTJQkVm2nbLYc1tRLwmPDAXcQ3nuzGDzn0x/IA28dBjxse/+oNy4j2cpJz3k/mSz/a4ZLjA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@mui/x-internals": "7.13.0",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@mui/x-internals": "7.14.0",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -2490,13 +2490,12 @@
       }
     },
     "node_modules/@testing-library/jest-dom": {
-      "version": "6.4.8",
-      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz",
-      "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==",
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
+      "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
       "dev": true,
       "dependencies": {
         "@adobe/css-tools": "^4.4.0",
-        "@babel/runtime": "^7.9.2",
         "aria-query": "^5.0.0",
         "chalk": "^3.0.0",
         "css.escape": "^1.5.1",
@@ -2760,9 +2759,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.11",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz",
-      "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==",
+      "version": "8.56.12",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+      "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
       "dev": true,
       "dependencies": {
         "@types/estree": "*",
@@ -3950,9 +3949,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.7.4",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
-      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
+      "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
       "dependencies": {
         "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
@@ -4340,9 +4339,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001651",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
-      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "version": "1.0.30001653",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
+      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
       "funding": [
         {
           "type": "opencollective",
@@ -4544,9 +4543,9 @@
       }
     },
     "node_modules/cjs-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz",
+      "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==",
       "dev": true
     },
     "node_modules/clamp": {
@@ -5957,9 +5956,9 @@
       }
     },
     "node_modules/emoji-regex": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
-      "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+      "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
       "dev": true
     },
     "node_modules/end-of-stream": {
@@ -12041,9 +12040,9 @@
       ]
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "dependencies": {
         "braces": "^3.0.3",
@@ -15015,20 +15014,20 @@
       }
     },
     "node_modules/ts-jest": {
-      "version": "29.2.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz",
-      "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==",
+      "version": "29.2.5",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
+      "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==",
       "dev": true,
       "dependencies": {
-        "bs-logger": "0.x",
+        "bs-logger": "^0.2.6",
         "ejs": "^3.1.10",
-        "fast-json-stable-stringify": "2.x",
+        "fast-json-stable-stringify": "^2.1.0",
         "jest-util": "^29.0.0",
         "json5": "^2.2.3",
-        "lodash.memoize": "4.x",
-        "make-error": "1.x",
-        "semver": "^7.5.3",
-        "yargs-parser": "^21.0.1"
+        "lodash.memoize": "^4.1.2",
+        "make-error": "^1.3.6",
+        "semver": "^7.6.3",
+        "yargs-parser": "^21.1.1"
       },
       "bin": {
         "ts-jest": "cli.js"
@@ -15162,9 +15161,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
     },
     "node_modules/type": {
       "version": "2.7.3",
@@ -15314,9 +15313,9 @@
       }
     },
     "node_modules/typedoc-plugin-markdown": {
-      "version": "4.2.5",
-      "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.5.tgz",
-      "integrity": "sha512-ZWIfc0OqwEtQfuaqbmM1kesMi/Fhc++W+5f3TDEm1Tsi28pHSoZk4WCOm4lNuN30WtEImwAHhhXC4DIWki1DiA==",
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.6.tgz",
+      "integrity": "sha512-k33o2lZSGpL3GjH28eW+RsujzCYFP0L5GNqpK+wa4CBcMOxpj8WV7SydNRLS6eSa2UvaPvNVJTaAZ6Tm+8GXoA==",
       "dev": true,
       "engines": {
         "node": ">= 18"

+ 26 - 21
frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

@@ -228,7 +228,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             const nr = newValue.data as RowType[];
             if (Array.isArray(nr) && nr.length > newValue.start) {
                 setRows(nr);
-                newValue.comp && setCompRows(newValue.comp as RowType[])
+                newValue.comp && setCompRows(newValue.comp as RowType[]);
                 promise && promise.resolve();
             } else {
                 promise && promise.reject();
@@ -279,34 +279,36 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         if (baseColumns) {
             try {
                 let filter = false;
-                Object.values(baseColumns).forEach((col) => {
-                    if (typeof col.filter != "boolean") {
-                        col.filter = !!props.filter;
+                const newCols: Record<string, ColumnDesc> = {};
+                Object.entries(baseColumns).forEach(([cId, cDesc]) => {
+                    const nDesc = (newCols[cId] = { ...cDesc });
+                    if (typeof nDesc.filter != "boolean") {
+                        nDesc.filter = !!props.filter;
                     }
-                    filter = filter || col.filter;
-                    if (typeof col.notEditable != "boolean") {
-                        col.notEditable = !editable;
+                    filter = filter || nDesc.filter;
+                    if (typeof nDesc.notEditable != "boolean") {
+                        nDesc.notEditable = !editable;
                     }
-                    if (col.tooltip === undefined) {
-                        col.tooltip = props.tooltip;
+                    if (nDesc.tooltip === undefined) {
+                        nDesc.tooltip = props.tooltip;
                     }
                 });
                 addDeleteColumn(
                     (active && editable && (onAdd || onDelete) ? 1 : 0) +
                         (active && filter ? 1 : 0) +
                         (active && downloadable ? 1 : 0),
-                    baseColumns
+                    newCols
                 );
-                const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
+                const colsOrder = Object.keys(newCols).sort(getsortByIndex(newCols));
                 const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
-                    if (baseColumns[col].style) {
+                    if (newCols[col].style) {
                         pv.styles = pv.styles || {};
-                        pv.styles[baseColumns[col].dfid] = baseColumns[col].style as string;
+                        pv.styles[newCols[col].dfid] = newCols[col].style as string;
                     }
-                    hNan = hNan || !!baseColumns[col].nanValue;
-                    if (baseColumns[col].tooltip) {
+                    hNan = hNan || !!newCols[col].nanValue;
+                    if (newCols[col].tooltip) {
                         pv.tooltips = pv.tooltips || {};
-                        pv.tooltips[baseColumns[col].dfid] = baseColumns[col].tooltip as string;
+                        pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
                     }
                     return pv;
                 }, {});
@@ -314,7 +316,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                     styTt.styles = styTt.styles || {};
                     styTt.styles[LINE_STYLE] = props.lineStyle;
                 }
-                return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
+                return [colsOrder, newCols, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
                 console.info("ATable.columns: " + ((e as Error).message || e));
             }
@@ -407,7 +409,9 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                         afs,
                         compare ? onCompare : undefined,
                         updateVars && getUpdateVar(updateVars, "comparedatas"),
-                        typeof userData == "object" ? (userData as Record<string, Record<string, unknown>>).context : undefined
+                        typeof userData == "object"
+                            ? (userData as Record<string, Record<string, unknown>>).context
+                            : undefined
                     )
                 );
             });
@@ -429,7 +433,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             onCompare,
             dispatch,
             module,
-            userData
+            userData,
         ]
     );
 
@@ -537,7 +541,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             selection: selected,
             formatConfig: formatConfig,
             onValidation: active && onEdit ? onCellValidation : undefined,
-            onDeletion: active && onDelete ? onRowDeletion : undefined,
+            onDeletion: active && editable && onDelete ? onRowDeletion : undefined,
             onRowSelection: active && onAction ? onRowSelection : undefined,
             onRowClick: active && onAction ? onRowClick : undefined,
             lineStyle: props.lineStyle,
@@ -553,6 +557,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             columns,
             selected,
             formatConfig,
+            editable,
             onEdit,
             onCellValidation,
             onDelete,
@@ -584,7 +589,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [
-                                                    active && onAdd ? (
+                                                    active && editable && onAdd ? (
                                                         <Tooltip title="Add a row" key="addARow">
                                                             <IconButton
                                                                 onClick={onAddRowClick}

+ 2 - 14
frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx

@@ -173,10 +173,6 @@ const styledColumns = JSON.stringify({
     },
 });
 
-const invalidColumns = JSON.stringify({
-    invalid: true,
-});
-
 describe("PaginatedTable Component", () => {
     it("renders", async () => {
         const { getByText } = render(<PaginatedTable data={undefined} defaultColumns={tableColumns} />);
@@ -655,7 +651,7 @@ describe("PaginatedTable Component", () => {
                 col: "int",
                 index: 1,
                 reason: "click",
-                value: undefined
+                value: undefined,
             },
             type: "SEND_ACTION_ACTION",
         });
@@ -692,7 +688,7 @@ describe("PaginatedTable Component", () => {
                 col: "Code",
                 index: 0,
                 reason: "button",
-                value: "button action"
+                value: "button action",
             },
             type: "SEND_ACTION_ACTION",
         });
@@ -714,14 +710,6 @@ describe("PaginatedTable Component", () => {
         const elt = document.querySelector('table[aria-labelledby="tableTitle"]');
         expect(elt).toBeInTheDocument();
     });
-    it("logs error when baseColumns prop is invalid", () => {
-        // Mock console.info to check if it gets called
-        console.info = jest.fn();
-        // Render the component with invalid baseColumns prop
-        render(<PaginatedTable defaultColumns={invalidColumns} />);
-        // Check if console.info was called
-        expect(console.info).toHaveBeenCalled();
-    });
     it("should sort the table in ascending order", async () => {
         await waitFor(() => {
             render(<PaginatedTable data={tableValue} defaultColumns={tableColumns} />);

+ 23 - 19
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -136,34 +136,36 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         if (baseColumns) {
             try {
                 let filter = false;
-                Object.values(baseColumns).forEach((col) => {
-                    if (typeof col.filter != "boolean") {
-                        col.filter = !!props.filter;
+                const newCols: Record<string, ColumnDesc> = {};
+                Object.entries(baseColumns).forEach(([cId, cDesc]) => {
+                    const nDesc = (newCols[cId] = { ...cDesc });
+                    if (typeof nDesc.filter != "boolean") {
+                        nDesc.filter = !!props.filter;
                     }
-                    filter = filter || col.filter;
-                    if (typeof col.notEditable != "boolean") {
-                        col.notEditable = !editable;
+                    filter = filter || nDesc.filter;
+                    if (typeof nDesc.notEditable != "boolean") {
+                        nDesc.notEditable = !editable;
                     }
-                    if (col.tooltip === undefined) {
-                        col.tooltip = props.tooltip;
+                    if (nDesc.tooltip === undefined) {
+                        nDesc.tooltip = props.tooltip;
                     }
                 });
                 addDeleteColumn(
                     (active && editable && (onAdd || onDelete) ? 1 : 0) +
                         (active && filter ? 1 : 0) +
                         (active && downloadable ? 1 : 0),
-                    baseColumns
+                    newCols
                 );
-                const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
+                const colsOrder = Object.keys(newCols).sort(getsortByIndex(newCols));
                 const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
-                    if (baseColumns[col].style) {
+                    if (newCols[col].style) {
                         pv.styles = pv.styles || {};
-                        pv.styles[baseColumns[col].dfid] = baseColumns[col].style as string;
+                        pv.styles[newCols[col].dfid] = newCols[col].style as string;
                     }
-                    hNan = hNan || !!baseColumns[col].nanValue;
-                    if (baseColumns[col].tooltip) {
+                    hNan = hNan || !!newCols[col].nanValue;
+                    if (newCols[col].tooltip) {
                         pv.tooltips = pv.tooltips || {};
-                        pv.tooltips[baseColumns[col].dfid] = baseColumns[col].tooltip as string;
+                        pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
                     }
                     return pv;
                 }, {});
@@ -171,7 +173,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                     styTt.styles = styTt.styles || {};
                     styTt.styles[LINE_STYLE] = props.lineStyle;
                 }
-                return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
+                return [colsOrder, newCols, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
                 console.info("PaginatedTable.columns: ", (e as Error).message || e);
             }
@@ -293,7 +295,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         module,
         compare,
         onCompare,
-        userData
+        userData,
     ]);
 
     const onSort = useCallback(
@@ -480,7 +482,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [
-                                                    active && onAdd ? (
+                                                    active && editable && onAdd ? (
                                                         <Tooltip title="Add a row" key="addARow">
                                                             <IconButton
                                                                 onClick={onAddRowClick}
@@ -592,7 +594,9 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                                             ? onCellValidation
                                                             : undefined
                                                     }
-                                                    onDeletion={active && onDelete ? onRowDeletion : undefined}
+                                                    onDeletion={
+                                                        active && editable && onDelete ? onRowDeletion : undefined
+                                                    }
                                                     onSelection={active && onAction ? onRowSelection : undefined}
                                                     nanValue={columns[col].nanValue || props.nanValue}
                                                     tooltip={getTooltip(row, columns[col].tooltip, col)}

+ 80 - 91
frontend/taipy/package-lock.json

@@ -56,11 +56,11 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
-      "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
+      "version": "7.25.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
+      "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
       "dependencies": {
-        "@babel/types": "^7.25.0",
+        "@babel/types": "^7.25.4",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -112,11 +112,11 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
-      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz",
+      "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==",
       "dependencies": {
-        "@babel/types": "^7.25.2"
+        "@babel/types": "^7.25.4"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -126,9 +126,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
-      "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz",
+      "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -150,15 +150,15 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
-      "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz",
+      "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.25.0",
-        "@babel/parser": "^7.25.3",
+        "@babel/generator": "^7.25.4",
+        "@babel/parser": "^7.25.4",
         "@babel/template": "^7.25.0",
-        "@babel/types": "^7.25.2",
+        "@babel/types": "^7.25.4",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -167,9 +167,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
-      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
+      "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
       "dependencies": {
         "@babel/helper-string-parser": "^7.24.8",
         "@babel/helper-validator-identifier": "^7.24.7",
@@ -237,14 +237,14 @@
       "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
     },
     "node_modules/@emotion/react": {
-      "version": "11.13.0",
-      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz",
-      "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==",
+      "version": "11.13.3",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
+      "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
       "dependencies": {
         "@babel/runtime": "^7.18.3",
         "@emotion/babel-plugin": "^11.12.0",
         "@emotion/cache": "^11.13.0",
-        "@emotion/serialize": "^1.3.0",
+        "@emotion/serialize": "^1.3.1",
         "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
         "@emotion/utils": "^1.4.0",
         "@emotion/weak-memoize": "^0.4.0",
@@ -260,13 +260,13 @@
       }
     },
     "node_modules/@emotion/serialize": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz",
-      "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz",
+      "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==",
       "dependencies": {
         "@emotion/hash": "^0.9.2",
         "@emotion/memoize": "^0.9.0",
-        "@emotion/unitless": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
         "@emotion/utils": "^1.4.0",
         "csstype": "^3.0.2"
       }
@@ -299,9 +299,9 @@
       }
     },
     "node_modules/@emotion/unitless": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz",
-      "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ=="
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
     },
     "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
       "version": "1.1.0",
@@ -880,14 +880,14 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.12.1.tgz",
-      "integrity": "sha512-Zj8kt3SCQbJp1qhMi+A3I4KqB8i5OY2Q11mdOEathFhqN/SQm1sUjIa1G09cGP1dPDgK1a6KM6qJGNtcw/nuWA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.14.0.tgz",
+      "integrity": "sha512-3xI3xYVxqPU4//KfE4FcR+Zs7UT4kkDPvA+IDOcQdRUyVwmcXCjBuJZgKgJMqSCNK/KIJZQQrpmy5XGHOKTbdA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -944,12 +944,12 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.12.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.12.0.tgz",
-      "integrity": "sha512-zgu/JqSXBflSvtzfFN8lNi5Wxw79czBv6V/crOrXqCCOzxAIsrcup2FZlwvXlzetm3otS7o/Tzfo/O5dE68NkA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.14.0.tgz",
+      "integrity": "sha512-+qWIHLgt2vgH6bKmf7IwRvS86UbZRWKAdDY/yTQJaqzCzyesUvQhD+WRxe1kpdCK8UE061S9/Ju7hLkM4kjRNA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/utils": "^5.16.5"
+        "@mui/utils": "^5.16.6"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -963,15 +963,15 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.12.1.tgz",
-      "integrity": "sha512-WEejS6mzKQzwm0vKT5W1XqlHxqIFv0AV/MYDgvru39WwaCUCyip32sjvl7cDNwrsC8CkwyBCaEvNDEE9Jx0BkA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.14.0.tgz",
+      "integrity": "sha512-j1sK0tLrsiCu0FxwTJQkVm2nbLYc1tRLwmPDAXcQ3nuzGDzn0x/IA28dBjxse/+oNy4j2cpJz3k/mSz/a4ZLjA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@mui/x-internals": "7.12.0",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@mui/x-internals": "7.14.0",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -1124,25 +1124,15 @@
       "dev": true
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.11",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz",
-      "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==",
+      "version": "8.56.12",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+      "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
       "dev": true,
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
       }
     },
-    "node_modules/@types/eslint-scope": {
-      "version": "3.7.7",
-      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
-      "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
-      "dev": true,
-      "dependencies": {
-        "@types/eslint": "*",
-        "@types/estree": "*"
-      }
-    },
     "node_modules/@types/estree": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -1189,12 +1179,12 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "22.3.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz",
-      "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==",
+      "version": "22.5.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
+      "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
       "dev": true,
       "dependencies": {
-        "undici-types": "~6.18.2"
+        "undici-types": "~6.19.2"
       }
     },
     "node_modules/@types/parse-json": {
@@ -1208,9 +1198,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     "node_modules/@types/react": {
-      "version": "18.3.3",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
-      "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
+      "version": "18.3.4",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
+      "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
       "dependencies": {
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
@@ -2013,9 +2003,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001651",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
-      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "version": "1.0.30001653",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
+      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
       "dev": true,
       "funding": [
         {
@@ -2354,9 +2344,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.7",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.7.tgz",
-      "integrity": "sha512-6FTNWIWMxMy/ZY6799nBlPtF1DFDQ6VQJ7yyDP27SJNt5lwtQ5ufqVvHylb3fdQefvRcgA3fKcFMJi9OLwBRNw==",
+      "version": "1.5.13",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
+      "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
       "dev": true
     },
     "node_modules/enhanced-resolve": {
@@ -3628,9 +3618,9 @@
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.15.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
-      "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+      "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
       "dependencies": {
         "hasown": "^2.0.2"
       },
@@ -4251,9 +4241,9 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "dependencies": {
         "braces": "^3.0.3",
@@ -5596,9 +5586,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
     },
     "node_modules/type-check": {
       "version": "0.4.0",
@@ -5726,9 +5716,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.18.2",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz",
-      "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==",
+      "version": "6.19.8",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
       "dev": true
     },
     "node_modules/update-browserslist-db": {
@@ -5784,12 +5774,11 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.93.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz",
-      "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==",
+      "version": "5.94.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
+      "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
       "dev": true,
       "dependencies": {
-        "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
         "@webassemblyjs/ast": "^1.12.1",
         "@webassemblyjs/wasm-edit": "^1.12.1",
@@ -5798,7 +5787,7 @@
         "acorn-import-attributes": "^1.9.5",
         "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.17.0",
+        "enhanced-resolve": "^5.17.1",
         "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",

+ 2 - 1
frontend/taipy/src/DataNodeTable.tsx

@@ -215,7 +215,8 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                 updateVarName={props.updateVarName}
                 data={props.data}
                 userData={userData}
-                onEdit={tableEdit ? props.onEdit : undefined}
+                onEdit={props.onEdit}
+                editable={tableEdit}
                 filter={true}
                 libClassName="taipy-table"
                 pageSize={25}

+ 5 - 2
frontend/taipy/src/JobSelector.tsx

@@ -77,8 +77,8 @@ interface JobSelectorProps {
     updateJbVars?: string;
 }
 
-// job id, job name, empty list, entity id, entity name, submit id, creation date, status
-type Job = [string, string, [], string, string, string, string, number];
+// job id, job name, empty list, entity id, entity name, submit id, creation date, status, not deletable, not readable, not editable
+type Job = [string, string, [], string, string, string, string, number, string, string, string];
 type Jobs = Array<Job>;
 
 enum JobProps {
@@ -90,6 +90,9 @@ enum JobProps {
     submission_id,
     creation_date,
     status,
+    not_deletable,
+    not_readable,
+    not_editable
 }
 const JobLength = Object.keys(JobProps).length / 2;
 

+ 9 - 4
taipy/config/config.py

@@ -207,10 +207,15 @@ class Config:
 
     @classmethod
     def _override_env_file(cls):
-        if config_filename := os.environ.get(cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH):
-            cls.__logger.info(f"Loading configuration provided by environment variable. Filename: '{config_filename}'")
-            cls._env_file_config = cls._serializer._read(config_filename)
-            cls.__logger.info(f"Configuration '{config_filename}' successfully loaded.")
+        if cfg_filename := os.environ.get(cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH):
+            if not os.path.exists(cfg_filename):
+                cls.__logger.error(f"File '{cfg_filename}' provided by environment variable "
+                                   f"'{cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH}' does not exist. "
+                                   f"No configuration will be loaded from environment variable.")
+                return
+            cls.__logger.info(f"Loading configuration provided by environment variable. Filename: '{cfg_filename}'")
+            cls._env_file_config = cls._serializer._read(cfg_filename)
+            cls.__logger.info(f"Configuration '{cfg_filename}' successfully loaded.")
 
     @classmethod
     def _compile_configs(cls):

+ 1 - 1
taipy/core/common/warn_if_inputs_not_ready.py

@@ -34,7 +34,7 @@ def _warn_if_inputs_not_ready(inputs: Iterable[DataNode]):
             ]:
                 logger.warning(
                     f"{dn.id} cannot be read because it has never been written. "
-                    f"Hint: The data node may refer to a wrong path : {dn.path} "
+                    f"Hint: The data node may refer to a wrong path : {dn.properties['path']} "
                 )
             else:
                 logger.warning(f"{dn.id} cannot be read because it has never been written.")

+ 35 - 0
taipy/core/config/checkers/_scenario_config_checker.py

@@ -46,6 +46,31 @@ class _ScenarioConfigChecker(_ConfigChecker):
     def _check_if_children_config_id_is_overlapping_with_properties(
         self, scenario_config_id: str, scenario_config: ScenarioConfig
     ):
+        if scenario_config.sequences:
+            for sequence in scenario_config.sequences:
+                if sequence in scenario_config.properties:
+                    self._error(
+                        sequence,
+                        scenario_config.sequences[sequence],
+                        f"The sequence name `{sequence}` is overlapping with the "
+                        f"property `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                    )
+                if scenario_config.data_nodes:
+                    if sequence in [dn.id for dn in scenario_config.data_nodes if isinstance(dn, DataNodeConfig)]:
+                        self._error(
+                            sequence,
+                            scenario_config.sequences[sequence],
+                            f"The sequence name `{sequence}` is overlapping with the "
+                            f"data node `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
+                if scenario_config.tasks:
+                    if sequence in [task.id for task in scenario_config.tasks if isinstance(task, TaskConfig)]:
+                        self._error(
+                            sequence,
+                            scenario_config.sequences[sequence],
+                            f"The sequence name `{sequence}` is overlapping with the "
+                            f"task `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
         if scenario_config.tasks:
             for task in scenario_config.tasks:
                 if isinstance(task, TaskConfig) and task.id in scenario_config.properties:
@@ -55,6 +80,16 @@ class _ScenarioConfigChecker(_ConfigChecker):
                         f"The id of the TaskConfig `{task.id}` is overlapping with the "
                         f"property `{task.id}` of ScenarioConfig `{scenario_config_id}`.",
                     )
+                if scenario_config.data_nodes:
+                    if isinstance(task, TaskConfig) and task.id in [
+                        dn.id for dn in scenario_config.data_nodes if isinstance(dn, DataNodeConfig)
+                    ]:
+                        self._error(
+                            TaskConfig._ID_KEY,
+                            task.id,
+                            f"The id of the TaskConfig `{task.id}` is overlapping with the "
+                            f"data node `{task.id}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
         if scenario_config.data_nodes:
             for data_node in scenario_config.data_nodes:
                 if isinstance(data_node, DataNodeConfig) and data_node.id in scenario_config.properties:

+ 0 - 6
taipy/core/cycle/cycle.py

@@ -211,12 +211,6 @@ class Cycle(_Entity, _Labeled):
 
         return CycleId(_get_valid_filename(Cycle.__SEPARATOR.join([Cycle._ID_PREFIX, name, str(uuid.uuid4())])))
 
-    def __getattr__(self, attribute_name):
-        protected_attribute_name = attribute_name
-        if protected_attribute_name in self._properties:
-            return self._properties[protected_attribute_name]
-        raise AttributeError(f"{attribute_name} is not an attribute of cycle {self.id}")
-
     def __eq__(self, other):
         return isinstance(other, Cycle) and self.id == other.id
 

+ 2 - 8
taipy/core/data/data_node.py

@@ -134,7 +134,7 @@ class DataNode(_Entity, _Labeled):
 
     _ID_PREFIX = "DATANODE"
     __ID_SEPARATOR = "_"
-    __logger = _TaipyLogger._get_logger()
+    _logger = _TaipyLogger._get_logger()
     _REQUIRED_PROPERTIES: List[str] = []
     _MANAGER_NAME: str = "data"
     _PATH_KEY = "path"
@@ -347,12 +347,6 @@ class DataNode(_Entity, _Labeled):
     def __setstate__(self, state):
         vars(self).update(state)
 
-    def __getattr__(self, attribute_name):
-        protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return self._properties[protected_attribute_name]
-        raise AttributeError(f"{attribute_name} is not an attribute of data node {self.id}")
-
     @classmethod
     def _get_last_modified_datetime(cls, path: Optional[str] = None) -> Optional[datetime]:
         if path and os.path.isfile(path):
@@ -397,7 +391,7 @@ class DataNode(_Entity, _Labeled):
         try:
             return self.read_or_raise()
         except NoData:
-            self.__logger.warning(
+            self._logger.warning(
                 f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             return None

+ 1 - 1
taipy/core/data/parquet.py

@@ -189,7 +189,7 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
 
         # return None if data was never written
         if not self.last_edit_date:
-            self._DataNode__logger.warning(
+            self._logger.warning(
                 f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             return None

+ 7 - 0
taipy/core/exceptions/exceptions.py

@@ -383,3 +383,10 @@ class SQLQueryCannotBeExecuted(Exception):
 
 class _SuspiciousFileOperation(Exception):
     pass
+
+
+class AttributeKeyAlreadyExisted(Exception):
+    """Raised when an attribute key already existed."""
+
+    def __init__(self, key: str):
+        self.message = f"Attribute key '{key}' already existed."

+ 23 - 10
taipy/core/scenario/scenario.py

@@ -16,7 +16,6 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 
 import networkx as nx
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 
 from .._entity._entity import _Entity
@@ -31,6 +30,7 @@ from ..cycle.cycle import Cycle
 from ..data.data_node import DataNode
 from ..data.data_node_id import DataNodeId
 from ..exceptions.exceptions import (
+    AttributeKeyAlreadyExisted,
     InvalidSequence,
     NonExistingDataNode,
     NonExistingSequence,
@@ -117,6 +117,7 @@ class Scenario(_Entity, Submittable, _Labeled):
     _SEQUENCE_TASKS_KEY = "tasks"
     _SEQUENCE_PROPERTIES_KEY = "properties"
     _SEQUENCE_SUBSCRIBERS_KEY = "subscribers"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -155,6 +156,7 @@ class Scenario(_Entity, Submittable, _Labeled):
             )
 
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
+        self._init_done = True
 
     @staticmethod
     def _new_id(config_id: str) -> ScenarioId:
@@ -176,20 +178,28 @@ class Scenario(_Entity, Submittable, _Labeled):
     def __eq__(self, other):
         return isinstance(other, Scenario) and self.id == other.id
 
-    def __getattr__(self, attribute_name):
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
+    def __getattr__(self, attribute_name) -> Union[Sequence, Task, DataNode]:
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
-
         sequences = self._get_sequences()
         if protected_attribute_name in sequences:
             return sequences[protected_attribute_name]
-        tasks = self.tasks
+        tasks = self.__get_tasks()
         if protected_attribute_name in tasks:
             return tasks[protected_attribute_name]
-        data_nodes = self.data_nodes
+        data_nodes = self.__get_data_nodes()
         if protected_attribute_name in data_nodes:
             return data_nodes[protected_attribute_name]
+
         raise AttributeError(f"{attribute_name} is not an attribute of scenario {self.id}")
 
     @property
@@ -458,14 +468,17 @@ class Scenario(_Entity, Submittable, _Labeled):
     def _get_set_of_tasks(self) -> Set[Task]:
         return set(self.tasks.values())
 
-    @property  # type: ignore
-    @_self_reload(_MANAGER_NAME)
-    def data_nodes(self) -> Dict[str, DataNode]:
+    def __get_data_nodes(self) -> Dict[str, DataNode]:
         data_nodes_dict = self.__get_additional_data_nodes()
         for _, task in self.__get_tasks().items():
             data_nodes_dict.update(task.data_nodes)
         return data_nodes_dict
 
+    @property  # type: ignore
+    @_self_reload(_MANAGER_NAME)
+    def data_nodes(self) -> Dict[str, DataNode]:
+        return self.__get_data_nodes()
+
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def creation_date(self):

+ 13 - 4
taipy/core/sequence/sequence.py

@@ -15,7 +15,6 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 
 import networkx as nx
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 
 from .._entity._entity import _Entity
@@ -27,7 +26,7 @@ from .._version._version_manager_factory import _VersionManagerFactory
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..data.data_node import DataNode
-from ..exceptions.exceptions import NonExistingTask
+from ..exceptions.exceptions import AttributeKeyAlreadyExisted, NonExistingTask
 from ..job.job import Job
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..submission.submission import Submission
@@ -126,6 +125,7 @@ class Sequence(_Entity, Submittable, _Labeled):
     _ID_PREFIX = "SEQUENCE"
     _SEPARATOR = "_"
     _MANAGER_NAME = "sequence"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -144,6 +144,7 @@ class Sequence(_Entity, Submittable, _Labeled):
         self._parent_ids = parent_ids or set()
         self._properties = _Properties(self, **properties)
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
+        self._init_done = True
 
     @staticmethod
     def _new_id(sequence_name: str, scenario_id) -> SequenceId:
@@ -156,10 +157,18 @@ class Sequence(_Entity, Submittable, _Labeled):
     def __eq__(self, other):
         return isinstance(other, Sequence) and self.id == other.id
 
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
     def __getattr__(self, attribute_name):
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
         tasks = self._get_tasks()
         if protected_attribute_name in tasks:
             return tasks[protected_attribute_name]

+ 13 - 3
taipy/core/task/task.py

@@ -12,7 +12,6 @@
 import uuid
 from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 from taipy.config.common.scope import Scope
 
@@ -22,6 +21,7 @@ from .._entity._properties import _Properties
 from .._entity._reload import _Reloader, _self_reload, _self_setter
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..data.data_node import DataNode
+from ..exceptions import AttributeKeyAlreadyExisted
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..submission.submission import Submission
 from .task_id import TaskId
@@ -97,6 +97,7 @@ class Task(_Entity, _Labeled):
     _ID_PREFIX = "TASK"
     __ID_SEPARATOR = "_"
     _MANAGER_NAME = "task"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -121,6 +122,7 @@ class Task(_Entity, _Labeled):
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
         self._skippable = skippable
         self._properties = _Properties(self, **properties)
+        self._init_done = True
 
     def __hash__(self):
         return hash(self.id)
@@ -134,10 +136,18 @@ class Task(_Entity, _Labeled):
     def __setstate__(self, state):
         vars(self).update(state)
 
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
     def __getattr__(self, attribute_name):
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
         if protected_attribute_name in self.input:
             return self.input[protected_attribute_name]
         if protected_attribute_name in self.output:

+ 86 - 70
taipy/gui_core/_context.py

@@ -54,11 +54,13 @@ from taipy.core import submit as core_submit
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
+from taipy.core.reason import ReasonCollection
 from taipy.core.submission.submission_status import SubmissionStatus
 from taipy.core.taipy import can_create
 from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
+from taipy.gui.utils._map_dict import _MapDict
 
 from ._adapters import (
     CustomScenarioFilter,
@@ -125,7 +127,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         else None
                     )
                     if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids:  # type: ignore
-                        self.broadcast_core_changed({"scenario": list(sequence.parent_ids)})
+                        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:
@@ -420,8 +422,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if update:
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             if delete:
-                if not is_deletable(scenario_id):
-                    state.assign(error_var, f"Scenario. {scenario_id} is not deletable.")
+                if not (reason := is_deletable(scenario_id)):
+                    state.assign(error_var, f"Scenario. {scenario_id} is not deletable: {_get_reason(reason)}.")
                     return
                 try:
                     core_delete(scenario_id)
@@ -507,12 +509,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
                     try:
                         var_name, _ = gui._get_real_var_name(sel_scenario_var)
-                        self.gui._update_var(var_name, scenario or user_scenario, on_change= args[2])
+                        self.gui._update_var(var_name, scenario or user_scenario, on_change=args[2])
                     except Exception as e:  # pragma: no cover
                         _warn("Can't find value variable name in context", e)
         if scenario:
-            if not is_editable(scenario):
-                state.assign(error_var, f"Scenario {scenario_id or name} is not editable.")
+            if not (reason := is_editable(scenario)):
+                state.assign(error_var, f"Scenario {scenario_id or name} is not editable: {_get_reason(reason)}.")
                 return
             with scenario as sc:
                 sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
@@ -555,9 +557,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     else:
                         primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                         if primary is True:
-                            if not is_promotable(scenario):
+                            if not (reason := is_promotable(scenario)):
                                 _GuiCoreContext.__assign_var(
-                                    state, error_var, f"Scenario {entity_id} is not promotable."
+                                    state, error_var, f"Scenario {entity_id} is not promotable: {_get_reason(reason)}."
                                 )
                                 return
                             set_primary(scenario)
@@ -602,7 +604,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     state,
                     error_var,
                     f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable: "
-                    + reason.reasons,
+                    + f"{_get_reason(reason)}.",
                 )
                 return
             if entity:
@@ -809,9 +811,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         job.submit_id,
                         job.creation_date,
                         job.status.value,
-                        is_deletable(job),
-                        is_readable(job),
-                        is_editable(job),
+                        _get_reason(is_deletable(job)),
+                        _get_reason(is_readable(job)),
+                        _get_reason(is_editable(job)),
                     )
         except Exception as e:
             _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
@@ -829,11 +831,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
             errs = []
             if job_action == "delete":
                 for job_id in job_ids:
-                    if not is_readable(job_id):
-                        errs.append(f"Job {job_id} is not readable.")
+                    if not (reason := is_readable(job_id)):
+                        errs.append(f"Job {job_id} is not readable: {_get_reason(reason)}.")
                         continue
-                    if not is_deletable(job_id):
-                        errs.append(f"Job {job_id} is not deletable.")
+                    if not (reason := is_deletable(job_id)):
+                        errs.append(f"Job {job_id} is not deletable: {_get_reason(reason)}.")
                         continue
                     try:
                         delete_job(core_get(job_id))
@@ -841,11 +843,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         errs.append(f"Error deleting job. {e}")
             elif job_action == "cancel":
                 for job_id in job_ids:
-                    if not is_readable(job_id):
-                        errs.append(f"Job {job_id} is not readable.")
+                    if not (reason := is_readable(job_id)):
+                        errs.append(f"Job {job_id} is not readable: {_get_reason(reason)}.")
                         continue
-                    if not is_editable(job_id):
-                        errs.append(f"Job {job_id} is not cancelable.")
+                    if not (reason := is_editable(job_id)):
+                        errs.append(f"Job {job_id} is not cancelable: {_get_reason(reason)}.")
                         continue
                     try:
                         cancel_job(job_id)
@@ -947,8 +949,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 job_id = e.get("job_id")
                 job: t.Optional[Job] = None
                 if job_id:
-                    if not is_readable(job_id):
-                        job_id += " not readable"
+                    if not (reason := is_readable(job_id)):
+                        job_id += f" is not readable: {_get_reason(reason)}."
                     else:
                         job = core_get(job_id)
                 res.append(
@@ -964,11 +966,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return _DoNotUpdate()
 
     def __check_readable_editable(self, state: State, id: str, ent_type: str, var: t.Optional[str]):
-        if not is_readable(t.cast(ScenarioId, id)):
-            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable.")
+        if not (reason := is_readable(t.cast(ScenarioId, id))):
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable: {_get_reason(reason)}.")
             return False
-        if not is_editable(t.cast(ScenarioId, id)):
-            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable.")
+        if not (reason := is_editable(t.cast(ScenarioId, id))):
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable: {_get_reason(reason)}.")
             return False
         return True
 
@@ -1001,7 +1003,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode value. {e}")
             _GuiCoreContext.__assign_var(state, payload.get("data_id"), entity_id)  # this will update the data value
 
-    def tabular_data_edit(self, state: State, var_name: str, payload: dict):
+    def tabular_data_edit(self, state: State, var_name: str, payload: dict):  # noqa:C901
         self.__lazy_start()
         error_var = payload.get("error_id")
         user_data = payload.get("user_data", {})
@@ -1028,6 +1030,23 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     elif isinstance(data, pd.Series):
                         data.at[idx] = val
                     new_data = data
+                elif isinstance(data, (dict, _MapDict)):
+                    row = data.get(col, None)
+                    data_tuple = False
+                    if isinstance(row, tuple):
+                        row = list(row)
+                        data_tuple = True
+                    if isinstance(row, list):
+                        row[idx] = val
+                        if data_tuple:
+                            data[col] = tuple(row)
+                        new_data = data
+                    else:
+                        _GuiCoreContext.__assign_var(
+                            state,
+                            error_var,
+                            "Error updating Datanode: dict values must be list or tuple.",
+                        )
                 else:
                     data_tuple = False
                     if isinstance(data, tuple):
@@ -1088,55 +1107,44 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def get_data_node_tabular_data(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                value = self.__read_tabular_data(dn)
-                if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
-                    return value
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    value = self.__read_tabular_data(dn)
+                    if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
+                        return value
+                except Exception:
+                    return None
         return None
 
     def get_data_node_tabular_columns(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                value = self.__read_tabular_data(dn)
-                if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
-                    return self.gui._tbl_cols(
-                        True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=value
-                    )
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    value = self.__read_tabular_data(dn)
+                    if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
+                        return self.gui._tbl_cols(
+                            True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=value
+                        )
+                except Exception:
+                    return None
         return None
 
     def get_data_node_chart_config(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                return self.gui._chart_conf(
-                    True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=self.__read_tabular_data(dn)
-                )
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    return self.gui._chart_conf(
+                        True,
+                        True,
+                        "{}",
+                        json.dumps({"data": "tabular_data"}),
+                        tabular_data=self.__read_tabular_data(dn),
+                    )
+                except Exception:
+                    return None
         return None
 
     def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]):
@@ -1147,7 +1155,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
         on_action_function = self.gui._get_user_function(args[1]) if args[1] else None
         if callable(on_action_function):
             try:
-                entity = core_get(args[0]) if is_readable(args[0]) else f"unredable({args[0]})"
+                entity = (
+                    core_get(args[0])
+                    if (reason := is_readable(args[0]))
+                    else f"{args[0]} is not readable: {_get_reason(reason)}"
+                )
                 self.gui._call_function_with_state(
                     on_action_function,
                     [entity],
@@ -1160,4 +1172,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def get_creation_reason(self):
         self.__lazy_start()
-        return "" if (reason := can_create()) else f"Cannot create scenario: {reason.reasons}"
+        return "" if (reason := can_create()) else f"Cannot create scenario: {_get_reason(reason)}"
+
+
+def _get_reason(reason: t.Union[bool, ReasonCollection]):
+    return reason.reasons if isinstance(reason, ReasonCollection) else " "

+ 69 - 11
tests/core/config/checkers/test_scenario_config_checker.py

@@ -88,42 +88,100 @@ class TestScenarioConfigChecker:
         Config._compile_configs()
         input_dn_config = DataNodeConfig("input_dn")
         output_dn_config = DataNodeConfig("output_dn")
-        test_dn_config = DataNodeConfig("test")
+        additional_dn_config = DataNodeConfig("additional_dn")
         task_config = TaskConfig("bar", print, [input_dn_config], [output_dn_config])
-        test_task_config = TaskConfig("test", print, [test_dn_config], [output_dn_config])
 
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
-        config._sections[ScenarioConfig.name]["new"]._properties["test"] = "test"
         config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
         Config._collector = IssueCollector()
         Config.check()
         assert len(Config._collector.errors) == 0
 
-        config._sections[ScenarioConfig.name]["new"]._tasks = [test_task_config]
+        config._sections[ScenarioConfig.name]["new"]._properties["bar"] = "bar"
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
+        assert (
+            "The id of the TaskConfig `bar` is overlapping with the property `bar` of ScenarioConfig `new`."
+            in caplog.text
+        )
+
+        config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [additional_dn_config]
+        config._sections[ScenarioConfig.name]["new"]._properties["additional_dn"] = "additional_dn"
         with pytest.raises(SystemExit):
             Config._collector = IssueCollector()
             Config.check()
         assert len(Config._collector.errors) == 2
         assert (
-            "The id of the TaskConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the DataNodeConfig `additional_dn` is overlapping"
+            " with the property `additional_dn` of ScenarioConfig `new`." in caplog.text
+        )
+
+        config._sections[ScenarioConfig.name]["new"].add_sequences({"sq": [task_config]})
+        config._sections[ScenarioConfig.name]["new"]._properties["sq"] = "sq"
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 3
+        assert "The sequence name `sq` is overlapping with the property `sq` of ScenarioConfig `new`." in caplog.text
+
+    def test_check_if_children_ids_are_overlapping(self, caplog):
+        config = Config._applied_config
+        Config._compile_configs()
+        input_dn_config = DataNodeConfig("input_dn")
+        output_dn_config = DataNodeConfig("output_dn")
+        test_dn_config = DataNodeConfig("test")
+        bar_dn_config = DataNodeConfig("bar")
+        test_task_config = TaskConfig("test", print, [test_dn_config], [output_dn_config])
+        bar_task_config = TaskConfig("bar", print, [input_dn_config], [output_dn_config])
+
+        config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
+        config._sections[ScenarioConfig.name]["new"]._tasks = [bar_task_config]
+        Config._collector = IssueCollector()
+        Config.check()
+        assert len(Config._collector.errors) == 0
+
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [bar_dn_config]
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
+        assert (
+            "The id of the TaskConfig `bar` is overlapping with the data node `bar` of ScenarioConfig `new`."
             in caplog.text
         )
+
+        config._sections[ScenarioConfig.name]["new"]._tasks = [test_task_config]
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
         assert (
-            "The id of the DataNodeConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the TaskConfig `test` is overlapping with the data node `test` of ScenarioConfig `new`."
             in caplog.text
         )
 
-        config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
-        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [test_dn_config]
+        config._sections[ScenarioConfig.name]["new"]._tasks = [bar_task_config]
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [bar_dn_config]
         with pytest.raises(SystemExit):
             Config._collector = IssueCollector()
             Config.check()
         assert len(Config._collector.errors) == 1
         assert (
-            "The id of the DataNodeConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the TaskConfig `bar` is overlapping with the data node `bar` of ScenarioConfig `new`."
             in caplog.text
         )
 
+        config._sections[ScenarioConfig.name]["new"].add_sequences({"bar": [bar_task_config]})
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 3
+        assert "The sequence name `bar` is overlapping with the data node `bar` of ScenarioConfig `new`." in caplog.text
+        assert "The sequence name `bar` is overlapping with the task `bar` of ScenarioConfig `new`." in caplog.text
+
     def test_check_task_configs(self, caplog):
         Config._collector = IssueCollector()
         config = Config._applied_config
@@ -197,7 +255,7 @@ class TestScenarioConfigChecker:
         assert len(Config._collector.infos) == 0
 
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
-        config._sections[ScenarioConfig.name]["new"]._tasks = [TaskConfig("bar", print)]
+        config._sections[ScenarioConfig.name]["new"]._tasks = [TaskConfig("foo", print)]
         Config._collector = IssueCollector()
         Config.check()
         assert len(Config._collector.errors) == 0
@@ -264,7 +322,7 @@ class TestScenarioConfigChecker:
         output_dn_config = DataNodeConfig("output_dn")
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
         config._sections[ScenarioConfig.name]["new"]._tasks = [
-            TaskConfig("bar", print, [input_dn_config], [output_dn_config])
+            TaskConfig("foo", print, [input_dn_config], [output_dn_config])
         ]
         Config._collector = IssueCollector()
         Config.check()

+ 5 - 4
tests/core/cycle/test_cycle.py

@@ -8,6 +8,7 @@
 # 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 datetime
 from datetime import timedelta
 
@@ -49,7 +50,7 @@ def test_create_cycle_entity(current_datetime):
     assert cycle_1.creation_date == current_datetime
     assert cycle_1.start_date == current_datetime
     assert cycle_1.end_date == current_datetime
-    assert cycle_1.key == "value"
+    assert cycle_1.properties["key"] == "value"
     assert cycle_1.frequency == Frequency.DAILY
 
     cycle_2 = Cycle(Frequency.YEARLY, {}, current_datetime, current_datetime, current_datetime)
@@ -111,13 +112,13 @@ def test_add_property_to_scenario(current_datetime):
         name="foo",
     )
     assert cycle.properties == {"key": "value"}
-    assert cycle.key == "value"
+    assert cycle.properties["key"] == "value"
 
     cycle.properties["new_key"] = "new_value"
 
     assert cycle.properties == {"key": "value", "new_key": "new_value"}
-    assert cycle.key == "value"
-    assert cycle.new_key == "new_value"
+    assert cycle.properties["key"] == "value"
+    assert cycle.properties["new_key"] == "new_value"
 
 
 def test_auto_set_and_reload(current_datetime):

+ 1 - 1
tests/core/cycle/test_cycle_manager.py

@@ -91,7 +91,7 @@ def test_create_and_delete_cycle_entity(tmpdir):
     assert cycle_1.start_date is not None
     assert cycle_1.end_date is not None
     assert cycle_1.start_date < cycle_1.creation_date < cycle_1.end_date
-    assert cycle_1.key == "value"
+    assert cycle_1.properties["key"] == "value"
     assert cycle_1.frequency == Frequency.DAILY
 
     cycle_1_id = cycle_1.id

+ 4 - 4
tests/core/data/test_csv_data_node.py

@@ -65,8 +65,8 @@ class TestCSVDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == default_path
-        assert dn.has_header is False
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["has_header"] is False
+        assert dn.properties["exposed_type"] == "pandas"
 
         csv_dn_config = Config.configure_csv_data_node(
             id="foo", default_path=default_path, has_header=True, exposed_type=MyCustomObject
@@ -74,8 +74,8 @@ class TestCSVDataNode:
         dn = _DataManagerFactory._build_manager()._create_and_set(csv_dn_config, None, None)
         assert dn.storage_type() == "csv"
         assert dn.config_id == "foo"
-        assert dn.has_header is True
-        assert dn.exposed_type == MyCustomObject
+        assert dn.properties["has_header"] is True
+        assert dn.properties["exposed_type"] == MyCustomObject
 
         with pytest.raises(InvalidConfigurationId):
             CSVDataNode(

+ 2 - 2
tests/core/data/test_data_manager.py

@@ -303,14 +303,14 @@ class TestDataManager:
         assert csv_dn.config_id == "foo"
         assert isinstance(csv_dn, CSVDataNode)
         assert csv_dn._path == "path_from_config_file"
-        assert csv_dn.has_header
+        assert csv_dn.properties["has_header"]
 
         csv_dn_cfg = Config.configure_data_node(id="baz", storage_type="csv", path="bar", has_header=True)
         csv_dn = _DataManager._create_and_set(csv_dn_cfg, None, None)
         assert csv_dn.config_id == "baz"
         assert isinstance(csv_dn, CSVDataNode)
         assert csv_dn._path == "bar"
-        assert csv_dn.has_header
+        assert csv_dn.properties["has_header"]
 
     def test_get_if_not_exists(self):
         with pytest.raises(ModelNotFound):

+ 0 - 1
tests/core/data/test_data_node.py

@@ -670,7 +670,6 @@ class TestDataNode:
             dn = _DataManager._bulk_get_or_create([dn_config])[dn_config]
             assert dn._properties.data["prop"] == "ENV[FOO]"
             assert dn.properties["prop"] == "bar"
-            assert dn.prop == "bar"
 
     def test_path_populated_with_config_default_path(self):
         dn_config = Config.configure_data_node("data_node", "pickle", default_path="foo.p")

+ 18 - 18
tests/core/data/test_excel_data_node.py

@@ -94,17 +94,17 @@ class TestExcelDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == path
-        assert dn.has_header is False
-        assert dn.sheet_name == "Sheet1"
+        assert dn.properties["has_header"] is False
+        assert dn.properties["sheet_name"] == "Sheet1"
 
         excel_dn_config_1 = Config.configure_excel_data_node(
             id="baz", default_path=path, has_header=True, sheet_name="Sheet1", exposed_type=MyCustomObject
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_1, None, None)
         assert isinstance(dn_1, ExcelDataNode)
-        assert dn_1.has_header is True
-        assert dn_1.sheet_name == "Sheet1"
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["has_header"] is True
+        assert dn_1.properties["sheet_name"] == "Sheet1"
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
         excel_dn_config_2 = Config.configure_excel_data_node(
             id="baz",
@@ -115,16 +115,16 @@ class TestExcelDataNode:
         )
         dn_2 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_2, None, None)
         assert isinstance(dn_2, ExcelDataNode)
-        assert dn_2.sheet_name == sheet_names
-        assert dn_2.exposed_type == {"Sheet1": "pandas", "Sheet2": "numpy"}
+        assert dn_2.properties["sheet_name"] == sheet_names
+        assert dn_2.properties["exposed_type"] == {"Sheet1": "pandas", "Sheet2": "numpy"}
 
         excel_dn_config_3 = Config.configure_excel_data_node(
             id="baz", default_path=path, has_header=True, sheet_name=sheet_names, exposed_type=MyCustomObject
         )
         dn_3 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_3, None, None)
         assert isinstance(dn_3, ExcelDataNode)
-        assert dn_3.sheet_name == sheet_names
-        assert dn_3.exposed_type == MyCustomObject
+        assert dn_3.properties["sheet_name"] == sheet_names
+        assert dn_3.properties["exposed_type"] == MyCustomObject
 
         excel_dn_config_4 = Config.configure_excel_data_node(
             id="baz",
@@ -135,8 +135,8 @@ class TestExcelDataNode:
         )
         dn_4 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_4, None, None)
         assert isinstance(dn_4, ExcelDataNode)
-        assert dn_4.sheet_name == sheet_names
-        assert dn_4.exposed_type == {"Sheet1": MyCustomObject, "Sheet2": MyCustomObject2}
+        assert dn_4.properties["sheet_name"] == sheet_names
+        assert dn_4.properties["exposed_type"] == {"Sheet1": MyCustomObject, "Sheet2": MyCustomObject2}
 
     def test_get_user_properties(self, excel_file):
         dn_1 = ExcelDataNode("dn_1", Scope.SCENARIO, properties={"path": "data/node/path"})
@@ -204,7 +204,7 @@ class TestExcelDataNode:
             pathlib.Path(__file__).parent.resolve(), "data_sample/example_2.xlsx"
         )  # ["Sheet1", "Sheet2", "Sheet3"]
         dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"default_path": path, "exposed_type": MyCustomObject1})
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         dn.read()
         dn.path = new_path
         dn.read()
@@ -214,7 +214,7 @@ class TestExcelDataNode:
             Scope.SCENARIO,
             properties={"default_path": path, "exposed_type": MyCustomObject1, "sheet_name": ["Sheet4"]},
         )
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         with pytest.raises(NonExistingExcelSheet):
             dn.read()
 
@@ -264,14 +264,14 @@ class TestExcelDataNode:
             "foo", Scope.SCENARIO, properties={"default_path": "notexistyet.xlsx", "exposed_type": MyCustomObject1}
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         dn = ExcelDataNode(
             "foo",
             Scope.SCENARIO,
             properties={"default_path": "notexistyet.xlsx", "exposed_type": [MyCustomObject1, MyCustomObject2]},
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == [MyCustomObject1, MyCustomObject2]
+        assert dn.properties["exposed_type"] == [MyCustomObject1, MyCustomObject2]
         dn = ExcelDataNode(
             "foo",
             Scope.SCENARIO,
@@ -281,12 +281,12 @@ class TestExcelDataNode:
             },
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == {"Sheet1": MyCustomObject1, "Sheet2": MyCustomObject2}
+        assert dn.properties["exposed_type"] == {"Sheet1": MyCustomObject1, "Sheet2": MyCustomObject2}
 
     def test_exposed_type_default(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.xlsx")
         dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"default_path": path, "sheet_name": "Sheet1"})
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["exposed_type"] == "pandas"
         data = dn.read()
         assert isinstance(data, pd.DataFrame)
 
@@ -295,7 +295,7 @@ class TestExcelDataNode:
         dn = ExcelDataNode(
             "foo", Scope.SCENARIO, properties={"default_path": path, "exposed_type": "pandas", "sheet_name": "Sheet1"}
         )
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["exposed_type"] == "pandas"
         data = dn.read()
         assert isinstance(data, pd.DataFrame)
 

+ 0 - 1
tests/core/data/test_json_data_node.py

@@ -108,7 +108,6 @@ class TestJSONDataNode:
         assert isinstance(dn_2, JSONDataNode)
         assert dn_2.storage_type() == "json"
         assert dn_2.properties["encoding"] == "utf-16"
-        assert dn_2.encoding == "utf-16"
 
         json_dn_config_3 = Config.configure_json_data_node(
             id="foo", default_path=path, encoder=MyCustomEncoder, decoder=MyCustomDecoder

+ 4 - 4
tests/core/data/test_parquet_data_node.py

@@ -84,16 +84,16 @@ class TestParquetDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == path
-        assert dn.exposed_type == "pandas"
-        assert dn.compression == "snappy"
-        assert dn.engine == "pyarrow"
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["compression"] == "snappy"
+        assert dn.properties["engine"] == "pyarrow"
 
         parquet_dn_config_1 = Config.configure_parquet_data_node(
             id="bar", default_path=path, compression=compression, exposed_type=MyCustomObject
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(parquet_dn_config_1, None, None)
         assert isinstance(dn_1, ParquetDataNode)
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
         with pytest.raises(InvalidConfigurationId):
             dn = ParquetDataNode("foo bar", Scope.SCENARIO, properties={"path": path, "name": "super name"})

+ 1 - 1
tests/core/data/test_read_excel_data_node.py

@@ -584,7 +584,7 @@ def test_read_multi_sheet_without_header_single_custom_object_exposed_type():
     )
 
     data_custom = excel_data_node_as_custom_object.read()
-    assert excel_data_node_as_custom_object.exposed_type == MyCustomObject1
+    assert excel_data_node_as_custom_object.properties["exposed_type"] == MyCustomObject1
     assert isinstance(data_custom, Dict)
     assert len(data_custom) == 2
     assert all(len(data_custom[sheet_name]) == 6 for sheet_name in sheet_names)

+ 5 - 5
tests/core/data/test_sql_data_node.py

@@ -119,9 +119,9 @@ class TestSQLDataNode:
         assert dn.owner_id is None
         assert dn.job_ids == []
         assert dn.is_ready_for_reading
-        assert dn.exposed_type == "pandas"
-        assert dn.read_query == "SELECT * FROM example"
-        assert dn.write_query_builder == my_write_query_builder_with_pandas
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["read_query"] == "SELECT * FROM example"
+        assert dn.properties["write_query_builder"] == my_write_query_builder_with_pandas
 
         sql_dn_config_1 = Config.configure_sql_data_node(
             id="foo",
@@ -131,8 +131,8 @@ class TestSQLDataNode:
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(sql_dn_config_1, None, None)
         assert isinstance(dn, SQLDataNode)
-        assert dn_1.exposed_type == MyCustomObject
-        assert dn_1.append_query_builder == my_append_query_builder_with_pandas
+        assert dn_1.properties["exposed_type"] == MyCustomObject
+        assert dn_1.properties["append_query_builder"] == my_append_query_builder_with_pandas
 
     @pytest.mark.parametrize("properties", __sql_properties)
     def test_get_user_properties(self, properties):

+ 3 - 3
tests/core/data/test_sql_table_data_node.py

@@ -97,8 +97,8 @@ class TestSQLTableDataNode:
         assert dn.owner_id is None
         assert dn.job_ids == []
         assert dn.is_ready_for_reading
-        assert dn.exposed_type == "pandas"
-        assert dn.table_name == "example"
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["table_name"] == "example"
         assert dn._get_base_read_query() == "SELECT * FROM example"
 
         sql_table_dn_config_1 = Config.configure_sql_table_data_node(
@@ -106,7 +106,7 @@ class TestSQLTableDataNode:
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(sql_table_dn_config_1, None, None)
         assert isinstance(dn_1, SQLTableDataNode)
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
     @pytest.mark.parametrize("properties", __sql_properties)
     def test_get_user_properties(self, properties):

+ 26 - 7
tests/core/scenario/test_scenario.py

@@ -15,14 +15,20 @@ import pytest
 
 from taipy.config import Frequency
 from taipy.config.common.scope import Scope
+from taipy.config.config import Config
 from taipy.config.exceptions.exceptions import InvalidConfigurationId
+from taipy.core import create_scenario
 from taipy.core.common._utils import _Subscriber
 from taipy.core.cycle._cycle_manager_factory import _CycleManagerFactory
 from taipy.core.cycle.cycle import Cycle, CycleId
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.in_memory import DataNode, InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
-from taipy.core.exceptions.exceptions import SequenceAlreadyExists, SequenceTaskDoesNotExistInScenario
+from taipy.core.exceptions.exceptions import (
+    AttributeKeyAlreadyExisted,
+    SequenceAlreadyExists,
+    SequenceTaskDoesNotExistInScenario,
+)
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario_id import ScenarioId
@@ -56,7 +62,7 @@ def test_create_primary_scenario(cycle):
     assert scenario.data_nodes == {}
     assert scenario.sequences == {}
     assert scenario.properties == {"key": "value"}
-    assert scenario.key == "value"
+    assert scenario.properties["key"] == "value"
     assert scenario.creation_date is not None
     assert scenario.is_primary
     assert scenario.cycle == cycle
@@ -156,6 +162,18 @@ def test_create_scenario_and_add_sequences():
     assert scenario.sequences == {"sequence_1": scenario.sequence_1, "sequence_2": scenario.sequence_2}
 
 
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    s_cfg = Config.configure_scenario("foo", additional_data_node_configs=[dn_cfg])
+    scenario = create_scenario(s_cfg)
+
+    scenario.key = "value"
+    assert scenario.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        scenario.bar = "KeyAlreadyUsed"
+
+
 def test_create_scenario_overlapping_sequences():
     input_1 = PickleDataNode("input_1", Scope.SCENARIO)
     output_1 = PickleDataNode("output_1", Scope.SCENARIO)
@@ -453,11 +471,11 @@ def test_update_sequence(data_node):
 
     assert len(scenario.sequences) == 1
     assert scenario.sequences["seq_1"].tasks == {"foo": task_1}
-    assert scenario.sequences["seq_1"].name == "seq_1"
+    assert scenario.sequences["seq_1"].properties["name"] == "seq_1"
     scenario.update_sequence("seq_1", [task_2], {"new_key": "new_value"}, [])
     assert len(scenario.sequences) == 1
     assert scenario.sequences["seq_1"].tasks == {"bar": task_2}
-    assert scenario.sequences["seq_1"].name == "seq_1"
+    assert scenario.sequences["seq_1"].properties["name"] == "seq_1"
     assert scenario.sequences["seq_1"].properties["new_key"] == "new_value"
 
 
@@ -465,6 +483,7 @@ def test_add_rename_and_remove_sequences_within_context(data_node):
     task_1 = Task("task_1", {}, print, output=[data_node])
     task_2 = Task("task_2", {}, print, input=[data_node])
     _TaskManagerFactory._build_manager()._set(task_1)
+    _TaskManagerFactory._build_manager()._set(task_2)
     scenario = Scenario(config_id="scenario", tasks={task_1, task_2}, properties={})
     _ScenarioManagerFactory._build_manager()._set(scenario)
 
@@ -490,13 +509,13 @@ def test_add_rename_and_remove_sequences_within_context(data_node):
 def test_add_property_to_scenario():
     scenario = Scenario("foo", set(), {"key": "value"})
     assert scenario.properties == {"key": "value"}
-    assert scenario.key == "value"
+    assert scenario.properties["key"] == "value"
 
     scenario.properties["new_key"] = "new_value"
 
     assert scenario.properties == {"key": "value", "new_key": "new_value"}
-    assert scenario.key == "value"
-    assert scenario.new_key == "new_value"
+    assert scenario.properties["key"] == "value"
+    assert scenario.properties["new_key"] == "new_value"
 
 
 def test_add_cycle_to_scenario(cycle):

+ 23 - 6
tests/core/sequence/test_sequence.py

@@ -20,6 +20,7 @@ from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.data_node import DataNode
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
+from taipy.core.exceptions import AttributeKeyAlreadyExisted
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.sequence._sequence_manager import _SequenceManager
@@ -38,7 +39,7 @@ def test_sequence_equals():
     sequence_1 = scenario.sequences["print"]
     sequence_id = sequence_1.id
 
-    assert sequence_1.name == "print"
+    assert sequence_1.properties["name"] == "print"
     sequence_2 = _SequenceManager._get(sequence_id)
     # To test if instance is same type
     task = Task("task", {}, print, [], [], sequence_id)
@@ -56,7 +57,7 @@ def test_create_sequence():
     sequence = Sequence({"description": "description"}, [task], sequence_id=SequenceId("name_1"))
     assert sequence.id == "name_1"
     assert sequence.owner_id is None
-    assert sequence.description == "description"
+    assert sequence.properties["description"] == "description"
     assert sequence.foo == input
     assert sequence.bar == output
     assert sequence.baz.id == task.id
@@ -80,7 +81,7 @@ def test_create_sequence():
     )
     assert sequence_1.id == "name_1"
     assert sequence_1.owner_id == "owner_id"
-    assert sequence_1.description == "description"
+    assert sequence_1.properties["description"] == "description"
     assert sequence_1.input == input_1
     assert sequence_1.output == output_1
     assert sequence_1.task_1 == task_1
@@ -109,7 +110,7 @@ def test_create_sequence():
     )
     assert sequence_2.owner_id == "owner_id"
     assert sequence_2.id == "name_2"
-    assert sequence_2.description == "description"
+    assert sequence_2.properties["description"] == "description"
     assert sequence_2.tasks == {task.config_id: task, task_1.config_id: task_1}
     assert sequence_2.data_nodes == {"foo": input, "bar": output, "input": input_1, "output": output_1}
     assert sequence_2.parent_ids == {"parent_id_1", "parent_id_2"}
@@ -122,8 +123,24 @@ def test_create_sequence():
                 return self.label
 
         get_mck.return_value = MockOwner()
-        assert sequence_2.get_label() == "owner_label > " + sequence_2.name
-        assert sequence_2.get_simple_label() == sequence_2.name
+        assert sequence_2.get_label() == "owner_label > " + sequence_2.properties["name"]
+        assert sequence_2.get_simple_label() == sequence_2.properties["name"]
+
+
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    task_config = Config.configure_task("print", print, [dn_cfg], None)
+    scenario_config = Config.configure_scenario("scenario", [task_config])
+
+    scenario = _ScenarioManager._create(scenario_config)
+    scenario.add_sequences({"seq": list(scenario.tasks.values())})
+    sequence = scenario.sequences["seq"]
+
+    sequence.key = "value"
+    assert sequence.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        sequence.bar = "KeyAlreadyUsed"
 
 
 def test_check_consistency():

+ 1 - 1
tests/core/sequence/test_sequence_manager.py

@@ -415,7 +415,7 @@ def test_get_or_create_data():
     scenario.add_sequences({"by_6": list(scenario.tasks.values())})
     sequence = scenario.sequences["by_6"]
 
-    assert sequence.name == "by_6"
+    assert sequence.properties["name"] == "by_6"
 
     assert len(_DataManager._get_all()) == 3
     assert len(_TaskManager._get_all()) == 2

+ 17 - 1
tests/core/task/test_task.py

@@ -21,6 +21,8 @@ from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.csv import CSVDataNode
 from taipy.core.data.data_node import DataNode
 from taipy.core.data.in_memory import InMemoryDataNode
+from taipy.core.exceptions import AttributeKeyAlreadyExisted
+from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.task._task_manager import _TaskManager
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
 from taipy.core.task.task import Task
@@ -94,7 +96,7 @@ def test_create_task():
     assert task.owner_id == "owner_id"
     assert task.parent_ids == {"parent_id_1", "parent_id_2"}
     assert task.name_1ea == abc_dn
-    assert task.name_1ea.path == path
+    assert task.name_1ea.properties["path"] == path
     with pytest.raises(AttributeError):
         _ = task.bar
     with mock.patch("taipy.core.get") as get_mck:
@@ -110,6 +112,20 @@ def test_create_task():
         assert task.get_simple_label() == task.config_id
 
 
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    task_config = Config.configure_task("print", print, [dn_cfg], None)
+    scenario_config = Config.configure_scenario("scenario", [task_config])
+    scenario = _ScenarioManager._create(scenario_config)
+    task = scenario.tasks["print"]
+
+    task.key = "value"
+    assert task.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        task.bar = "KeyAlreadyUsed"
+
+
 def test_can_not_change_task_output(output):
     task = Task("name_1", {}, print, output=output)
 

+ 2 - 2
tests/gui_core/test_context_is_deletable.py

@@ -91,7 +91,7 @@ class TestGuiCoreContext_is_deletable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not deletable.")
+                assert "is not deletable" in str(assign.call_args.args[1])
 
     def test_act_on_jobs(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
@@ -127,4 +127,4 @@ class TestGuiCoreContext_is_deletable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in str(assign.call_args.args[1])

+ 8 - 8
tests/gui_core/test_context_is_editable.py

@@ -88,7 +88,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in str(assign.call_args.args[1])
 
     def test_edit_entity(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -122,7 +122,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in str(assign.call_args.args[1])
 
     def test_act_on_jobs(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
@@ -142,7 +142,7 @@ class TestGuiCoreContext_is_editable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not editable.") == -1
+            assert "is not editable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_editable_false):
@@ -158,7 +158,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_data_node(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -192,7 +192,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_lock_datanode_for_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -228,7 +228,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_update_data(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -264,7 +264,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_tabular_data_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -299,4 +299,4 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]

+ 2 - 2
tests/gui_core/test_context_is_promotable.py

@@ -65,7 +65,7 @@ class TestGuiCoreContext_is_promotable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).endswith("to primary because it doesn't belong to a cycle.")
+            assert "to primary because it doesn't belong to a cycle" in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_promotable", side_effect=mock_is_promotable_false):
@@ -81,4 +81,4 @@ class TestGuiCoreContext_is_promotable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not promotable.")
+                assert "is not promotable" in assign.call_args.args[1]

+ 10 - 10
tests/gui_core/test_context_is_readable.py

@@ -138,7 +138,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_entity(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -172,7 +172,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_submission_status_callback(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
@@ -239,7 +239,7 @@ class TestGuiCoreContext_is_readable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not readable.") == -1
+            assert "is not readable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             gui_core_context.act_on_jobs(
@@ -254,7 +254,7 @@ class TestGuiCoreContext_is_readable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not readable.") == -1
+            assert "is not readable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
@@ -270,7 +270,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
                 assign.reset_mock()
 
                 gui_core_context.act_on_jobs(
@@ -285,7 +285,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_data_node(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -319,7 +319,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_lock_datanode_for_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -355,7 +355,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_get_scenarios_for_owner(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
@@ -402,7 +402,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_tabular_data_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -437,7 +437,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_get_data_node_tabular_data(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:

+ 1 - 1
tools/packages/pipfiles/Pipfile3.10.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/pipfiles/Pipfile3.11.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/pipfiles/Pipfile3.12.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/pipfiles/Pipfile3.8.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/pipfiles/Pipfile3.9.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/taipy-gui/setup.requirements.txt

@@ -13,6 +13,6 @@ python-dotenv>=1.0.0,<=1.0.1
 pytz>=2021.3,<=2024.1
 simple-websocket>=0.10.1,<=1.0.0
 taipy-config
-twisted>=23.8.0,<=24.3.0
+twisted>=24.7.0,<24.8.0
 tzlocal>=3.0,<=5.2
 watchdog>=4.0.0,<=4.0.1