Repository: Avaiga/taipy

Issue:
Why Taipy is blocking config update in development version
### Description

I am in development version. I don't want to keep my .taipy and files generated by user_data.
 
I change from a Taipy version to another but receive this message:

```
[2025-01-22 11:11:00.209][Taipy][ERROR] The version 4.1.0.dev0 of Taipy's entities does not match version of the Taipy Version management 4.0.2. Please update the core entities to be compatible with Taipy Core 4.0.2 using the `taipy migrate ...` command. For more information, please run `taipy help migrate`
```

Why Taipy is blocking config update in development version?

### Acceptance Criteria

- [ ] Any new code is covered by a unit tested.
- [ ] Check code coverage is at least 90%.


### Code of Conduct

- [x] I have checked the [existing issues](https://github.com/Avaiga/taipy/issues?q=is%3Aissue+).
- [ ] I am willing to work on this issue (optional)

Hints from issue comments:
@trgiangdo: @jrobinAV @FlorianJacta This is happening because of the `ConfigCoreVersionMismatched` exception is raised here because of the `_check_compatibility()` method.

Should we skip the `_check_compatibility()` method when in development mode?@trgiangdo: The `ConfigCoreVersionMismatched` exception is raised before the `clean_all_entity_by_version()` method, which is trying to load the old _Version entity and the old Config.

Proposal:
Update the `_version_manager.py` lines 138 - 149 to:
```python
        try:
            if version_number == cls._get_development_version():
                if cls._exists(version_number):
                    return version_number
            else:
                if cls._get(version_number):
                    return version_number
        except InconsistentEnvVariableError:  # The version exist but the Config is alternated
            return version_number
        except ConfigCoreVersionMismatched as e:
            cls._logger.error(e.message)
            raise SystemExit() from e
```

The main difference is only calling the `_get` when it's not development mode, otherwise call the `_exists` method@trgiangdo: To test the problem:

1. Install taipy 4.0.0
2. Run an application that create some entities in taipy 4.0.0 in both development mode and experiment mode (so we have entities for both dev and experiment versions)
3. Install taipy 4.1.0.dev1
4. Rerun the application in development mode. For the current code, it will log an error.
    - Expected behavior: The application should run
6. Rerun the application in experiment mode. For the current code, the exception is raised with the whole backlog
    - Expected behavior: The error should be logged properly.

Solution patch:
diff --git a/taipy/core/_version/_version_manager.py b/taipy/core/_version/_version_manager.py
index f49af7dab..93660edb8 100644
--- a/taipy/core/_version/_version_manager.py
+++ b/taipy/core/_version/_version_manager.py
@@ -136,8 +136,13 @@ class _VersionManager(_Manager[_Version]):
             return ""
 
         try:
-            if version := cls._get(version_number):
-                return version.id
+            if version_number == cls._get_development_version():
+                if cls._exists(version_number):
+                    return version_number
+            else:
+                if cls._get(version_number):
+                    return version_number
+
         except InconsistentEnvVariableError:  # The version exist but the Config is alternated
             return version_number
         except ConfigCoreVersionMismatched as e:
@@ -183,6 +188,9 @@ class _VersionManager(_Manager[_Version]):
                 cls._set_experiment_version(current_version_number, Config.core.force)
             except VersionAlreadyExistsAsDevelopment as err:
                 raise SystemExit(err.message) from None
+            except ConfigCoreVersionMismatched as e:
+                cls._logger.error(e.message)
+                raise SystemExit() from e
 
         else:
             raise SystemExit(f"Undefined execution mode: {Config.core.mode}.")
diff --git a/taipy/gui/_renderers/_markdown/factory.py b/taipy/gui/_renderers/_markdown/factory.py
index fa47e3baf..a00afb8ba 100644
--- a/taipy/gui/_renderers/_markdown/factory.py
+++ b/taipy/gui/_renderers/_markdown/factory.py
@@ -22,11 +22,11 @@ class _MarkdownFactory(_Factory):
     _TAIPY_BLOCK_TAGS = ["layout", "part", "expandable", "dialog", "pane"]
 
     @staticmethod
-    def create_element(gui, control_type: str, all_properties: str) -> t.Union[t.Any, str]:
+    def create_element(gui, element_type: str, all_properties: str) -> t.Union[t.Any, str]:
         # Create properties dict from all_properties
         property_pairs = _Factory._PROPERTY_RE.findall(all_properties)
         properties = {property[0]: property[1] for property in property_pairs}
-        builder_md = _Factory.call_builder(gui, control_type, properties)
+        builder_md = _Factory.call_builder(gui, element_type, properties)
         if builder_md is None:
-            return f"<|INVALID SYNTAX - Control is '{control_type}'|>"
+            return f"<|UNKNOWN ELEMENT TYPE '{element_type}'|>"
         return builder_md
diff --git a/taipy/gui/utils/_adapter.py b/taipy/gui/utils/_adapter.py
index 8a2de2d80..55faf411b 100644
--- a/taipy/gui/utils/_adapter.py
+++ b/taipy/gui/utils/_adapter.py
@@ -70,7 +70,8 @@ class _Adapter:
         adapter = self.__get_for_var(var_name, value)
         if isclass(lov) and issubclass(lov, Enum):
             lov = list(lov)
-        if isinstance(lov, (list, tuple)):
+        # list or Lov value
+        if isinstance(lov, list) or (isinstance(lov, tuple) and not id_only):
             res = []
             for elt in lov:
                 v = self._run(adapter, elt, var_name, id_only)


Test patch:
diff --git a/tests/core/config/test_core_version.py b/tests/core/config/test_core_version.py
index 03a8d8e3d..6a18c091c 100644
--- a/tests/core/config/test_core_version.py
+++ b/tests/core/config/test_core_version.py
@@ -14,24 +14,30 @@ from unittest import mock
 import pytest
 
 from taipy.common.config import Config
+from taipy.core import Orchestrator, taipy
 from taipy.core._init_version import _read_version
 from taipy.core.config.core_section import CoreSection
 from taipy.core.exceptions import ConfigCoreVersionMismatched
+from taipy.core.scenario._scenario_manager import _ScenarioManager
 from tests.core.utils.named_temporary_file import NamedTemporaryFile
 
 _MOCK_CORE_VERSION = "3.1.1"
 
 
+def patch_core_version(mock_core_version: str):
+    with mock.patch("taipy.core.config.core_section._read_version") as mock_read_version:
+        mock_read_version.return_value = mock_core_version
+
+    CoreSection._CURRENT_CORE_VERSION = mock_core_version
+    Config._default_config._unique_sections[CoreSection.name] = CoreSection.default_config()
+    Config._python_config._unique_sections[CoreSection.name] = CoreSection.default_config()
+
+
 @pytest.fixture(scope="function", autouse=True)
 def mock_core_version():
-    with mock.patch("taipy.core.config.core_section._read_version") as mock_read_version:
-        mock_read_version.return_value = _MOCK_CORE_VERSION
-        CoreSection._CURRENT_CORE_VERSION = _MOCK_CORE_VERSION
-        Config.unique_sections[CoreSection.name] = CoreSection.default_config()
-        Config._default_config._unique_sections[CoreSection.name] = CoreSection.default_config()
-        Config._python_config._unique_sections[CoreSection.name] = CoreSection.default_config()
+    patch_core_version(_MOCK_CORE_VERSION)
 
-        yield
+    yield
 
     CoreSection._CURRENT_CORE_VERSION = _read_version()
 
@@ -137,3 +143,57 @@ class TestCoreVersionInCoreSectionConfig:
         )
         Config.load(file_config.filename)
         assert Config.unique_sections[CoreSection.name]._core_version == _MOCK_CORE_VERSION
+
+    def test_run_core_app_with_different_taipy_core_version_in_development_mode(self):
+        with mock.patch("sys.argv", ["prog", "--development"]):
+            run_application()
+
+        # Run the application with a compatible version should NOT raise any error
+        patch_core_version(f"{self.major}.{self.minor}.{self.patch}.dev0")
+        with mock.patch("sys.argv", ["prog", "--development"]):
+            run_application()
+
+        # Run the application with an incompatible version in development mode should NOT raise an error
+        patch_core_version(f"{self.major}.{int(self.minor) + 1}.{self.patch}.dev0")
+        with mock.patch("sys.argv", ["prog", "--development"]):
+            run_application()
+
+    def test_run_core_app_with_different_taipy_core_version_in_experiment_mode(self, caplog):
+        with mock.patch("sys.argv", ["prog", "--experiment", "1.0"]):
+            run_application()
+
+        # Run the application with a compatible version should not raise any error
+        patch_core_version(f"{self.major}.{self.minor}.{int(self.patch) + 1}.dev0")
+        with mock.patch("sys.argv", ["prog", "--experiment", "1.0"]):
+            run_application()
+
+        # Run the application with an incompatible version in experiment mode should raise SystemExit and log the error
+        patch_core_version(f"{self.major}.{int(self.minor) + 1}.{self.patch}.dev0")
+        with mock.patch("sys.argv", ["prog", "--experiment", "1.0"]):
+            with pytest.raises(SystemExit):
+                run_application()
+        assert (
+            f"The version {self.major}.{self.minor}.{self.patch} of Taipy's entities does not match version "
+            f"of the Taipy Version management {self.major}.{int(self.minor) + 1}.{self.patch}.dev0"
+        ) in caplog.text
+
+
+def twice(a):
+    return [a * 2]
+
+
+def run_application():
+    Config.configure_data_node(id="d0")
+    data_node_1_config = Config.configure_data_node(id="d1", storage_type="pickle", default_data="abc")
+    data_node_2_config = Config.configure_data_node(id="d2", storage_type="csv")
+    task_config = Config.configure_task("my_task", twice, data_node_1_config, data_node_2_config)
+    scenario_config = Config.configure_scenario("my_scenario", [task_config])
+    scenario_config.add_sequences({"my_sequence": [task_config]})
+
+    orchestrator = Orchestrator()
+    orchestrator.run()
+
+    scenario = _ScenarioManager._create(scenario_config)
+    taipy.submit(scenario)
+
+    orchestrator.stop()
diff --git a/tests/gui/renderers/test_md_parsing.py b/tests/gui/renderers/test_md_parsing.py
index fcb7d436d..62877cc34 100644
--- a/tests/gui/renderers/test_md_parsing.py
+++ b/tests/gui/renderers/test_md_parsing.py
@@ -14,7 +14,7 @@ from taipy.gui import Gui
 
 def test_invalid_control_name(gui: Gui, helpers):
     md_string = "<|invalid|invalid|>"
-    expected_list = ["INVALID SYNTAX - Control is 'invalid'"]
+    expected_list = ["UNKNOWN ELEMENT TYPE 'invalid'"]
     helpers.test_control_md(gui, md_string, expected_list)