1
0
Эх сурвалжийг харах

Merge branch 'develop' into feature/#547-remove-src-folder

Fred Lefévère-Laoide 1 жил өмнө
parent
commit
2deb174e1c
34 өөрчлөгдсөн 2736 нэмэгдсэн , 0 устгасан
  1. 10 0
      tests/config/__init__.py
  2. 10 0
      tests/config/checker/__init__.py
  3. 10 0
      tests/config/checker/checkers/__init__.py
  4. 27 0
      tests/config/checker/checkers/test_checker.py
  5. 81 0
      tests/config/checker/checkers/test_config_checker.py
  6. 22 0
      tests/config/checker/test_default_config_checker.py
  7. 94 0
      tests/config/checker/test_issue_collector.py
  8. 10 0
      tests/config/common/__init__.py
  9. 138 0
      tests/config/common/test_argparser.py
  10. 27 0
      tests/config/common/test_classproperty.py
  11. 50 0
      tests/config/common/test_scope.py
  12. 199 0
      tests/config/common/test_template_handler.py
  13. 47 0
      tests/config/common/test_validate_id.py
  14. 49 0
      tests/config/conftest.py
  15. 10 0
      tests/config/global_app/__init__.py
  16. 46 0
      tests/config/global_app/test_global_app_config.py
  17. 152 0
      tests/config/test_compilation.py
  18. 355 0
      tests/config/test_config_comparator.py
  19. 48 0
      tests/config/test_default_config.py
  20. 58 0
      tests/config/test_env_file_config.py
  21. 42 0
      tests/config/test_file_config.py
  22. 200 0
      tests/config/test_override_config.py
  23. 47 0
      tests/config/test_section.py
  24. 169 0
      tests/config/test_section_registration.py
  25. 477 0
      tests/config/test_section_serialization.py
  26. 10 0
      tests/config/utils/__init__.py
  27. 18 0
      tests/config/utils/checker_for_tests.py
  28. 28 0
      tests/config/utils/named_temporary_file.py
  29. 69 0
      tests/config/utils/section_for_tests.py
  30. 98 0
      tests/config/utils/section_of_sections_list_for_tests.py
  31. 70 0
      tests/config/utils/unique_section_for_tests.py
  32. 10 0
      tests/logger/__init__.py
  33. 27 0
      tests/logger/logger.conf
  34. 28 0
      tests/logger/test_logger.py

+ 10 - 0
tests/config/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 10 - 0
tests/config/checker/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 10 - 0
tests/config/checker/checkers/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 27 - 0
tests/config/checker/checkers/test_checker.py

@@ -0,0 +1,27 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+from unittest import mock
+from unittest.mock import MagicMock
+
+from src.taipy.config import Config
+from src.taipy.config.checker._checker import _Checker
+from src.taipy.config.checker.issue_collector import IssueCollector
+from tests.config.utils.checker_for_tests import CheckerForTest
+
+
+def test_register_checker():
+    checker = CheckerForTest
+    checker._check = MagicMock()
+    _Checker.add_checker(checker)
+    Config.check()
+    checker._check.assert_called_once()

+ 81 - 0
tests/config/checker/checkers/test_config_checker.py

@@ -0,0 +1,81 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import logging
+from unittest import mock
+
+from src.taipy.config._config import _Config
+from src.taipy.config.checker._checkers._config_checker import _ConfigChecker
+from src.taipy.config.checker.issue import Issue
+from src.taipy.config.checker.issue_collector import IssueCollector
+
+
+class MyCustomChecker(_ConfigChecker):
+    def _check(self) -> IssueCollector:  # type: ignore
+        pass
+
+
+def test__error():
+    with mock.patch.object(logging.Logger, "error"):
+        collector = IssueCollector()
+        assert len(collector.all) == 0
+        _ConfigChecker(_Config(), collector)._error("field", 17, "my message")
+        assert len(collector.all) == 1
+        assert len(collector.errors) == 1
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert collector.errors[0] == Issue(IssueCollector._ERROR_LEVEL, "field", 17, "my message", "_ConfigChecker")
+
+        MyCustomChecker(_Config(), collector)._error("foo", "bar", "baz")
+        assert len(collector.all) == 2
+        assert len(collector.errors) == 2
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert collector.errors[0] == Issue(IssueCollector._ERROR_LEVEL, "field", 17, "my message", "_ConfigChecker")
+        assert collector.errors[1] == Issue(IssueCollector._ERROR_LEVEL, "foo", "bar", "baz", "MyCustomChecker")
+
+
+def test__warning():
+    collector = IssueCollector()
+    assert len(collector.all) == 0
+    _ConfigChecker(_Config(), collector)._warning("field", 17, "my message")
+    assert len(collector.all) == 1
+    assert len(collector.warnings) == 1
+    assert len(collector.errors) == 0
+    assert len(collector.infos) == 0
+    assert collector.warnings[0] == Issue(IssueCollector._WARNING_LEVEL, "field", 17, "my message", "_ConfigChecker")
+
+    MyCustomChecker(_Config(), collector)._warning("foo", "bar", "baz")
+    assert len(collector.all) == 2
+    assert len(collector.warnings) == 2
+    assert len(collector.errors) == 0
+    assert len(collector.infos) == 0
+    assert collector.warnings[0] == Issue(IssueCollector._WARNING_LEVEL, "field", 17, "my message", "_ConfigChecker")
+    assert collector.warnings[1] == Issue(IssueCollector._WARNING_LEVEL, "foo", "bar", "baz", "MyCustomChecker")
+
+
+def test__info():
+    collector = IssueCollector()
+    assert len(collector.all) == 0
+    _ConfigChecker(_Config(), collector)._info("field", 17, "my message")
+    assert len(collector.all) == 1
+    assert len(collector.infos) == 1
+    assert len(collector.errors) == 0
+    assert len(collector.warnings) == 0
+    assert collector.infos[0] == Issue(IssueCollector._INFO_LEVEL, "field", 17, "my message", "_ConfigChecker")
+
+    MyCustomChecker(_Config(), collector)._info("foo", "bar", "baz")
+    assert len(collector.all) == 2
+    assert len(collector.infos) == 2
+    assert len(collector.errors) == 0
+    assert len(collector.warnings) == 0
+    assert collector.infos[0] == Issue(IssueCollector._INFO_LEVEL, "field", 17, "my message", "_ConfigChecker")
+    assert collector.infos[1] == Issue(IssueCollector._INFO_LEVEL, "foo", "bar", "baz", "MyCustomChecker")

+ 22 - 0
tests/config/checker/test_default_config_checker.py

@@ -0,0 +1,22 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from src.taipy.config._config import _Config
+from src.taipy.config.checker._checker import _Checker
+
+
+class TestDefaultConfigChecker:
+    def test_check_default_config(self):
+        config = _Config._default_config()
+        collector = _Checker._check(config)
+        assert len(collector._errors) == 0
+        assert len(collector._infos) == 0
+        assert len(collector._warnings) == 0

+ 94 - 0
tests/config/checker/test_issue_collector.py

@@ -0,0 +1,94 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from src.taipy.config.checker.issue import Issue
+from src.taipy.config.checker.issue_collector import IssueCollector
+
+
+class TestIssueCollector:
+    def test_add_error(self):
+        collector = IssueCollector()
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 0
+        collector._add_error("field", "value", "message", "checker")
+        assert len(collector.errors) == 1
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 1
+        assert collector.all[0] == Issue(IssueCollector._ERROR_LEVEL, "field", "value", "message", "checker")
+        collector._add_error("field", "value", "message", "checker")
+        assert len(collector.errors) == 2
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 2
+        assert collector.all[0] == Issue(IssueCollector._ERROR_LEVEL, "field", "value", "message", "checker")
+        assert collector.all[1] == Issue(IssueCollector._ERROR_LEVEL, "field", "value", "message", "checker")
+
+    def test_add_warning(self):
+        collector = IssueCollector()
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 0
+        collector._add_warning("field", "value", "message", "checker")
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 1
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 1
+        assert collector.all[0] == Issue(IssueCollector._WARNING_LEVEL, "field", "value", "message", "checker")
+        collector._add_warning("field", "value", "message", "checker")
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 2
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 2
+        assert collector.all[0] == Issue(IssueCollector._WARNING_LEVEL, "field", "value", "message", "checker")
+        assert collector.all[1] == Issue(IssueCollector._WARNING_LEVEL, "field", "value", "message", "checker")
+
+    def test_add_info(self):
+        collector = IssueCollector()
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 0
+        assert len(collector.all) == 0
+        collector._add_info("field", "value", "message", "checker")
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 1
+        assert len(collector.all) == 1
+        assert collector.all[0] == Issue(IssueCollector._INFO_LEVEL, "field", "value", "message", "checker")
+        collector._add_info("field", "value", "message", "checker")
+        assert len(collector.errors) == 0
+        assert len(collector.warnings) == 0
+        assert len(collector.infos) == 2
+        assert len(collector.all) == 2
+        assert collector.all[0] == Issue(IssueCollector._INFO_LEVEL, "field", "value", "message", "checker")
+        assert collector.all[1] == Issue(IssueCollector._INFO_LEVEL, "field", "value", "message", "checker")
+
+    def test_all(self):
+        collector = IssueCollector()
+        collector._add_info("foo", "bar", "baz", "qux")
+        assert collector.all[0] == Issue(IssueCollector._INFO_LEVEL, "foo", "bar", "baz", "qux")
+        collector._add_warning("foo2", "bar2", "baz2", "qux2")
+        assert collector.all[0] == Issue(IssueCollector._WARNING_LEVEL, "foo2", "bar2", "baz2", "qux2")
+        assert collector.all[1] == Issue(IssueCollector._INFO_LEVEL, "foo", "bar", "baz", "qux")
+        collector._add_warning("foo3", "bar3", "baz3", "qux3")
+        assert collector.all[0] == Issue(IssueCollector._WARNING_LEVEL, "foo2", "bar2", "baz2", "qux2")
+        assert collector.all[1] == Issue(IssueCollector._WARNING_LEVEL, "foo3", "bar3", "baz3", "qux3")
+        assert collector.all[2] == Issue(IssueCollector._INFO_LEVEL, "foo", "bar", "baz", "qux")
+        collector._add_info("field", "value", "message", "checker")
+        collector._add_error("field", "value", "message", "checker")
+        assert collector.all[0] == Issue(IssueCollector._ERROR_LEVEL, "field", "value", "message", "checker")
+        assert collector.all[1] == Issue(IssueCollector._WARNING_LEVEL, "foo2", "bar2", "baz2", "qux2")
+        assert collector.all[2] == Issue(IssueCollector._WARNING_LEVEL, "foo3", "bar3", "baz3", "qux3")
+        assert collector.all[3] == Issue(IssueCollector._INFO_LEVEL, "foo", "bar", "baz", "qux")
+        assert collector.all[4] == Issue(IssueCollector._INFO_LEVEL, "field", "value", "message", "checker")

+ 10 - 0
tests/config/common/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 138 - 0
tests/config/common/test_argparser.py

@@ -0,0 +1,138 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import argparse
+import re
+import sys
+
+import pytest
+
+from src.taipy._cli._base_cli import _CLI
+
+if sys.version_info >= (3, 10):
+    argparse_options_str = "options:"
+else:
+    argparse_options_str = "optional arguments:"
+
+
+def preprocess_stdout(stdout):
+    stdout = stdout.replace("\n", " ").replace("\t", " ")
+    return re.sub(" +", " ", stdout)
+
+
+def remove_subparser(name: str):
+    """Remove a subparser from argparse."""
+    _CLI._sub_taipyparsers.pop(name, None)
+
+    if _CLI._subparser_action:
+        _CLI._subparser_action._name_parser_map.pop(name, None)
+
+        for action in _CLI._subparser_action._choices_actions:
+            if action.dest == name:
+                _CLI._subparser_action._choices_actions.remove(action)
+
+
+@pytest.fixture(autouse=True, scope="function")
+def clean_argparser():
+    _CLI._parser = argparse.ArgumentParser(conflict_handler="resolve")
+    _CLI._arg_groups = {}
+    subcommands = list(_CLI._sub_taipyparsers.keys())
+    for subcommand in subcommands:
+        remove_subparser(subcommand)
+
+    yield
+
+
+def test_subparser(capfd):
+    subcommand_1 = _CLI._add_subparser("subcommand_1", help="subcommand_1 help")
+    subcommand_1.add_argument("--foo", "-f", help="foo help")
+    subcommand_1.add_argument("--bar", "-b", help="bar help")
+
+    subcommand_2 = _CLI._add_subparser("subcommand_2", help="subcommand_2 help")
+    subcommand_2.add_argument("--doo", "-d", help="doo help")
+    subcommand_2.add_argument("--baz", "-z", help="baz help")
+
+    expected_subcommand_1_help_message = f"""subcommand_1 [-h] [--foo FOO] [--bar BAR]
+
+{argparse_options_str}
+  -h, --help         show this help message and exit
+  --foo FOO, -f FOO  foo help
+  --bar BAR, -b BAR  bar help
+    """
+
+    subcommand_1.print_help()
+    stdout, _ = capfd.readouterr()
+    assert preprocess_stdout(expected_subcommand_1_help_message) in preprocess_stdout(stdout)
+
+    expected_subcommand_2_help_message = f"""subcommand_2 [-h] [--doo DOO] [--baz BAZ]
+
+{argparse_options_str}
+  -h, --help         show this help message and exit
+  --doo DOO, -d DOO  doo help
+  --baz BAZ, -z BAZ  baz help
+    """
+
+    subcommand_2.print_help()
+    stdout, _ = capfd.readouterr()
+    assert preprocess_stdout(expected_subcommand_2_help_message) in preprocess_stdout(stdout)
+
+
+def test_duplicate_subcommand():
+    subcommand_1 = _CLI._add_subparser("subcommand_1", help="subcommand_1 help")
+    subcommand_1.add_argument("--foo", "-f", help="foo help")
+
+    subcommand_2 = _CLI._add_subparser("subcommand_1", help="subcommand_2 help")
+    subcommand_2.add_argument("--bar", "-b", help="bar help")
+
+    # The title of subcommand_2 is duplicated with  subcommand_1, and therefore
+    # there will be no new subcommand created
+    assert len(_CLI._sub_taipyparsers) == 1
+
+
+def test_groupparser(capfd):
+    group_1 = _CLI._add_groupparser("group_1", "group_1 desc")
+    group_1.add_argument("--foo", "-f", help="foo help")
+    group_1.add_argument("--bar", "-b", help="bar help")
+
+    group_2 = _CLI._add_groupparser("group_2", "group_2 desc")
+    group_2.add_argument("--doo", "-d", help="doo help")
+    group_2.add_argument("--baz", "-z", help="baz help")
+
+    expected_help_message = """
+group_1:
+  group_1 desc
+
+  --foo FOO, -f FOO  foo help
+  --bar BAR, -b BAR  bar help
+
+group_2:
+  group_2 desc
+
+  --doo DOO, -d DOO  doo help
+  --baz BAZ, -z BAZ  baz help
+    """.strip()
+
+    _CLI._parser.print_help()
+    stdout, _ = capfd.readouterr()
+
+    assert expected_help_message in stdout
+
+
+def test_duplicate_group():
+    group_1 = _CLI._add_groupparser("group_1", "group_1 desc")
+    group_1.add_argument("--foo", "-f", help="foo help")
+
+    group_2 = _CLI._add_groupparser("group_1", "group_2 desc")
+    group_2.add_argument("--bar", "-b", help="bar help")
+
+    # The title of group_2 is duplicated with  group_1, and therefore
+    # there will be no new group created
+    assert len(_CLI._arg_groups) == 1

+ 27 - 0
tests/config/common/test_classproperty.py

@@ -0,0 +1,27 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config.common._classproperty import _Classproperty
+
+
+class TestClassProperty:
+    def test_class_property(self):
+        class TestClass:
+            @_Classproperty
+            def test_property(cls):
+                return "test_property"
+
+        assert TestClass.test_property == "test_property"
+        assert TestClass().test_property == "test_property"
+        with pytest.raises(TypeError):
+            TestClass.test_property()

+ 50 - 0
tests/config/common/test_scope.py

@@ -0,0 +1,50 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config.common.scope import Scope
+
+
+def test_scope():
+    # Test __ge__ method
+    assert Scope.GLOBAL >= Scope.GLOBAL
+    assert Scope.GLOBAL >= Scope.CYCLE
+    assert Scope.CYCLE >= Scope.CYCLE
+    assert Scope.GLOBAL >= Scope.SCENARIO
+    assert Scope.CYCLE >= Scope.SCENARIO
+    assert Scope.SCENARIO >= Scope.SCENARIO
+    with pytest.raises(TypeError):
+        assert Scope.SCENARIO >= "testing string"
+
+    # Test __gt__ method
+    assert Scope.GLOBAL > Scope.CYCLE
+    assert Scope.GLOBAL > Scope.SCENARIO
+    assert Scope.CYCLE > Scope.SCENARIO
+    with pytest.raises(TypeError):
+        assert Scope.SCENARIO > "testing string"
+
+    # Test __le__ method
+    assert Scope.GLOBAL <= Scope.GLOBAL
+    assert Scope.CYCLE <= Scope.GLOBAL
+    assert Scope.CYCLE <= Scope.CYCLE
+    assert Scope.SCENARIO <= Scope.GLOBAL
+    assert Scope.SCENARIO <= Scope.CYCLE
+    assert Scope.SCENARIO <= Scope.SCENARIO
+    with pytest.raises(TypeError):
+        assert Scope.SCENARIO <= "testing string"
+
+    # Test __lt__ method
+    assert Scope.SCENARIO < Scope.GLOBAL
+    assert Scope.SCENARIO < Scope.GLOBAL
+    assert Scope.SCENARIO < Scope.CYCLE
+    with pytest.raises(TypeError):
+        assert Scope.SCENARIO < "testing string"

+ 199 - 0
tests/config/common/test_template_handler.py

@@ -0,0 +1,199 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import datetime
+import os
+from unittest import mock
+
+import pytest
+
+from src.taipy.config.common._template_handler import _TemplateHandler
+from src.taipy.config.common.frequency import Frequency
+from src.taipy.config.common.scope import Scope
+from src.taipy.config.exceptions.exceptions import InconsistentEnvVariableError
+
+
+def test_replace_if_template():
+    assert_does_not_change("123")
+    assert_does_not_change("foo")
+    assert_does_not_change("_foo")
+    assert_does_not_change("_foo_")
+    assert_does_not_change("foo_")
+    assert_does_not_change("foo")
+    assert_does_not_change("foo_1")
+    assert_does_not_change("1foo_1")
+    assert_does_not_change("env(foo)")
+    assert_does_not_change("env<foo>")
+    assert_does_not_change("env[foo]")
+    assert_does_not_change("Env[foo]")
+    assert_does_not_change("ENV[1foo]")
+
+    assert_does_not_change("123:bool")
+    assert_does_not_change("foo:bool")
+    assert_does_not_change("_foo:bool")
+    assert_does_not_change("_foo_:bool")
+    assert_does_not_change("foo_:bool")
+    assert_does_not_change("foo:bool")
+    assert_does_not_change("foo_1:bool")
+    assert_does_not_change("1foo_1:bool")
+    assert_does_not_change("env(foo):bool")
+    assert_does_not_change("env<foo>:bool")
+    assert_does_not_change("env[foo]:bool")
+    assert_does_not_change("Env[foo]:bool")
+    assert_does_not_change("ENV[1foo]:bool")
+
+    assert_does_not_change("ENV[foo]:")
+    assert_does_not_change("ENV[_foo]:")
+    assert_does_not_change("ENV[foo_]:")
+    assert_does_not_change("ENV[foo0]:")
+    assert_does_not_change("ENV[foo_0]:")
+    assert_does_not_change("ENV[_foo_0]:")
+
+    assert_does_not_change("ENV[foo]:foo")
+    assert_does_not_change("ENV[_foo]:foo")
+    assert_does_not_change("ENV[foo_]:foo")
+    assert_does_not_change("ENV[foo0]:foo")
+    assert_does_not_change("ENV[foo_0]:foo")
+    assert_does_not_change("ENV[_foo_0]:foo")
+
+    assert_does_replace("ENV[foo]", "foo", "VALUE", str)
+    assert_does_replace("ENV[_foo]", "_foo", "VALUE", str)
+    assert_does_replace("ENV[foo_]", "foo_", "VALUE", str)
+    assert_does_replace("ENV[foo0]", "foo0", "VALUE", str)
+    assert_does_replace("ENV[foo_0]", "foo_0", "VALUE", str)
+    assert_does_replace("ENV[_foo_0]", "_foo_0", "VALUE", str)
+
+    assert_does_replace("ENV[foo]:str", "foo", "VALUE", str)
+    assert_does_replace("ENV[_foo]:str", "_foo", "VALUE", str)
+    assert_does_replace("ENV[foo_]:str", "foo_", "VALUE", str)
+    assert_does_replace("ENV[foo0]:str", "foo0", "VALUE", str)
+    assert_does_replace("ENV[foo_0]:str", "foo_0", "VALUE", str)
+    assert_does_replace("ENV[_foo_0]:str", "_foo_0", "VALUE", str)
+
+    assert_does_replace("ENV[foo]:int", "foo", "1", int)
+    assert_does_replace("ENV[_foo]:int", "_foo", "1", int)
+    assert_does_replace("ENV[foo_]:int", "foo_", "1", int)
+    assert_does_replace("ENV[foo0]:int", "foo0", "1", int)
+    assert_does_replace("ENV[foo_0]:int", "foo_0", "1", int)
+    assert_does_replace("ENV[_foo_0]:int", "_foo_0", "1", int)
+
+    assert_does_replace("ENV[foo]:float", "foo", "1.", float)
+    assert_does_replace("ENV[_foo]:float", "_foo", "1.", float)
+    assert_does_replace("ENV[foo_]:float", "foo_", "1.", float)
+    assert_does_replace("ENV[foo0]:float", "foo0", "1.", float)
+    assert_does_replace("ENV[foo_0]:float", "foo_0", "1.", float)
+    assert_does_replace("ENV[_foo_0]:float", "_foo_0", "1.", float)
+
+    assert_does_replace("ENV[foo]:bool", "foo", "True", bool)
+    assert_does_replace("ENV[_foo]:bool", "_foo", "True", bool)
+    assert_does_replace("ENV[foo_]:bool", "foo_", "True", bool)
+    assert_does_replace("ENV[foo0]:bool", "foo0", "True", bool)
+    assert_does_replace("ENV[foo_0]:bool", "foo_0", "True", bool)
+    assert_does_replace("ENV[_foo_0]:bool", "_foo_0", "True", bool)
+
+
+def assert_does_replace(template, env_variable_name, replaced_by, as_type):
+    with mock.patch.dict(os.environ, {env_variable_name: replaced_by}):
+        tpl = _TemplateHandler()
+        assert tpl._replace_templates(template) == as_type(replaced_by)
+
+
+def assert_does_not_change(template):
+    tpl = _TemplateHandler()
+    assert tpl._replace_templates(template) == template
+
+
+def test_replace_tuple_list_dict():
+    with mock.patch.dict(os.environ, {"FOO": "true", "BAR": "3", "BAZ": "qux"}):
+        tpl = _TemplateHandler()
+        now = datetime.datetime.now()
+        actual = tpl._replace_templates(("ENV[FOO]:bool", now, "ENV[BAR]:int", "ENV[BAZ]", "quz"))
+        assert actual == (True, now, 3, "qux", "quz")
+        actual = tpl._replace_templates(("ENV[FOO]:bool", now, "ENV[BAR]:int", "ENV[BAZ]", "quz"))
+        assert actual == (True, now, 3, "qux", "quz")
+
+
+def test_to_bool():
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_bool("okhds")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_bool("no")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_bool("tru")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_bool("tru_e")
+
+    assert _TemplateHandler._to_bool("true")
+    assert _TemplateHandler._to_bool("True")
+    assert _TemplateHandler._to_bool("TRUE")
+    assert _TemplateHandler._to_bool("TruE")
+    assert _TemplateHandler._to_bool("TrUE")
+
+    assert not _TemplateHandler._to_bool("false")
+    assert not _TemplateHandler._to_bool("False")
+    assert not _TemplateHandler._to_bool("FALSE")
+    assert not _TemplateHandler._to_bool("FalSE")
+    assert not _TemplateHandler._to_bool("FalSe")
+
+
+def test_to_int():
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_int("okhds")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_int("_45")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_int("12.5")
+
+    assert 12 == _TemplateHandler._to_int("12")
+    assert 0 == _TemplateHandler._to_int("0")
+    assert -2 == _TemplateHandler._to_int("-2")
+    assert 156165 == _TemplateHandler._to_int("156165")
+
+
+def test_to_float():
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_float("okhds")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_float("_45")
+
+    assert 12.5 == _TemplateHandler._to_float("12.5")
+    assert 2.0 == _TemplateHandler._to_float("2")
+    assert 0.0 == _TemplateHandler._to_float("0")
+    assert -2.1 == _TemplateHandler._to_float("-2.1")
+    assert 156165.3 == _TemplateHandler._to_float("156165.3")
+
+
+def test_to_scope():
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_scope("okhds")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_scope("plop")
+
+    assert Scope.GLOBAL == _TemplateHandler._to_scope("global")
+    assert Scope.GLOBAL == _TemplateHandler._to_scope("GLOBAL")
+    assert Scope.SCENARIO == _TemplateHandler._to_scope("SCENARIO")
+    assert Scope.CYCLE == _TemplateHandler._to_scope("cycle")
+
+
+def test_to_frequency():
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_frequency("okhds")
+    with pytest.raises(InconsistentEnvVariableError):
+        _TemplateHandler._to_frequency("plop")
+
+    assert Frequency.DAILY == _TemplateHandler._to_frequency("DAILY")
+    assert Frequency.DAILY == _TemplateHandler._to_frequency("Daily")
+    assert Frequency.WEEKLY == _TemplateHandler._to_frequency("weekly")
+    assert Frequency.WEEKLY == _TemplateHandler._to_frequency("WEEKLY")
+    assert Frequency.MONTHLY == _TemplateHandler._to_frequency("Monthly")
+    assert Frequency.MONTHLY == _TemplateHandler._to_frequency("MONThLY")
+    assert Frequency.QUARTERLY == _TemplateHandler._to_frequency("QuaRtERlY")
+    assert Frequency.YEARLY == _TemplateHandler._to_frequency("Yearly")

+ 47 - 0
tests/config/common/test_validate_id.py

@@ -0,0 +1,47 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config.common._validate_id import _validate_id
+from src.taipy.config.exceptions.exceptions import InvalidConfigurationId
+
+
+class TestId:
+    def test_validate_id(self):
+        s = _validate_id("foo")
+        assert s == "foo"
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("1foo")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("foo bar")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("foo/foo$")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id(" ")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("class")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("def")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("with")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("CYCLE")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("SCENARIO")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("SEQUENCE")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("TASK")
+        with pytest.raises(InvalidConfigurationId):
+            _validate_id("DATANODE")

+ 49 - 0
tests/config/conftest.py

@@ -0,0 +1,49 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config._config import _Config
+from src.taipy.config._config_comparator._config_comparator import _ConfigComparator
+from src.taipy.config._serializer._toml_serializer import _TomlSerializer
+from src.taipy.config.checker.issue_collector import IssueCollector
+from src.taipy.config.config import Config
+from src.taipy.config.section import Section
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+@pytest.fixture(scope="function", autouse=True)
+def reset():
+    reset_configuration_singleton()
+    register_test_sections()
+
+
+def reset_configuration_singleton():
+    Config.unblock_update()
+    Config._default_config = _Config()._default_config()
+    Config._python_config = _Config()
+    Config._file_config = _Config()
+    Config._env_file_config = _Config()
+    Config._applied_config = _Config()
+    Config._collector = IssueCollector()
+    Config._serializer = _TomlSerializer()
+    Config._comparator = _ConfigComparator()
+
+
+def register_test_sections():
+    Config._register_default(UniqueSectionForTest("default_attribute"))
+    Config.configure_unique_section_for_tests = UniqueSectionForTest._configure
+    Config.unique_section_name = Config.unique_sections[UniqueSectionForTest.name]
+
+    Config._register_default(SectionForTest(Section._DEFAULT_KEY, "default_attribute", prop="default_prop", prop_int=0))
+    Config.configure_section_for_tests = SectionForTest._configure
+    Config.section_name = Config.sections[SectionForTest.name]

+ 10 - 0
tests/config/global_app/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 46 - 0
tests/config/global_app/test_global_app_config.py

@@ -0,0 +1,46 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+from unittest import mock
+
+import pytest
+
+from src.taipy.config.config import Config
+from src.taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked
+
+
+def test_global_config_with_env_variable_value():
+    with mock.patch.dict(os.environ, {"FOO": "bar", "BAZ": "qux"}):
+        Config.configure_global_app(foo="ENV[FOO]", bar="ENV[BAZ]")
+        assert Config.global_config.foo == "bar"
+        assert Config.global_config.bar == "qux"
+
+
+def test_default_global_app_config():
+    global_config = Config.global_config
+    assert global_config is not None
+    assert not global_config.notification
+    assert len(global_config.properties) == 0
+
+
+def test_block_update_global_app_config():
+    Config.block_update()
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        Config.configure_global_app(foo="bar")
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        Config.global_config.properties = {"foo": "bar"}
+
+    # Test if the global_config stay as default
+    assert Config.global_config.foo is None
+    assert len(Config.global_config.properties) == 0

+ 152 - 0
tests/config/test_compilation.py

@@ -0,0 +1,152 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config.config import Config
+from src.taipy.config.section import Section
+from tests.config.utils.named_temporary_file import NamedTemporaryFile
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.section_of_sections_list_for_tests import SectionOfSectionsListForTest
+
+
+@pytest.fixture
+def _init_list_section_for_test():
+    Config._register_default(SectionOfSectionsListForTest(Section._DEFAULT_KEY, [], prop="default_prop", prop_int=0))
+    Config.configure_list_section_for_tests = SectionOfSectionsListForTest._configure
+    Config.list_section_name = Config.sections[SectionOfSectionsListForTest.name]
+
+
+def test_applied_config_compilation_does_not_change_other_configs():
+    assert len(Config._default_config._unique_sections) == 1
+    assert Config._default_config._unique_sections["unique_section_name"] is not None
+    assert Config._default_config._unique_sections["unique_section_name"].attribute == "default_attribute"
+    assert Config._default_config._unique_sections["unique_section_name"].prop is None
+    assert len(Config._python_config._unique_sections) == 0
+    assert len(Config._file_config._unique_sections) == 0
+    assert len(Config._env_file_config._unique_sections) == 0
+    assert len(Config._applied_config._unique_sections) == 1
+    assert Config._applied_config._unique_sections["unique_section_name"] is not None
+    assert Config._applied_config._unique_sections["unique_section_name"].attribute == "default_attribute"
+    assert Config._applied_config._unique_sections["unique_section_name"].prop is None
+    assert len(Config.unique_sections) == 1
+    assert Config.unique_sections["unique_section_name"] is not None
+    assert Config.unique_sections["unique_section_name"].attribute == "default_attribute"
+    assert Config.unique_sections["unique_section_name"].prop is None
+    assert (
+        Config._applied_config._unique_sections["unique_section_name"]
+        is not Config._default_config._unique_sections["unique_section_name"]
+    )
+
+    Config.configure_unique_section_for_tests("qwe", prop="rty")
+
+    assert len(Config._default_config._unique_sections) == 1
+    assert Config._default_config._unique_sections["unique_section_name"] is not None
+    assert Config._default_config._unique_sections["unique_section_name"].attribute == "default_attribute"
+    assert Config._default_config._unique_sections["unique_section_name"].prop is None
+    assert len(Config._python_config._unique_sections) == 1
+    assert Config._python_config._unique_sections["unique_section_name"] is not None
+    assert Config._python_config._unique_sections["unique_section_name"].attribute == "qwe"
+    assert Config._python_config._unique_sections["unique_section_name"].prop == "rty"
+    assert (
+        Config._python_config._unique_sections["unique_section_name"]
+        != Config._default_config._unique_sections["unique_section_name"]
+    )
+    assert len(Config._file_config._unique_sections) == 0
+    assert len(Config._env_file_config._unique_sections) == 0
+    assert len(Config._applied_config._unique_sections) == 1
+    assert Config._applied_config._unique_sections["unique_section_name"] is not None
+    assert Config._applied_config._unique_sections["unique_section_name"].attribute == "qwe"
+    assert Config._applied_config._unique_sections["unique_section_name"].prop == "rty"
+    assert (
+        Config._python_config._unique_sections["unique_section_name"]
+        != Config._applied_config._unique_sections["unique_section_name"]
+    )
+    assert (
+        Config._default_config._unique_sections["unique_section_name"]
+        != Config._applied_config._unique_sections["unique_section_name"]
+    )
+    assert len(Config.unique_sections) == 1
+    assert Config.unique_sections["unique_section_name"] is not None
+    assert Config.unique_sections["unique_section_name"].attribute == "qwe"
+    assert Config.unique_sections["unique_section_name"].prop == "rty"
+
+
+def test_nested_section_instance_in_python(_init_list_section_for_test):
+    s1_cfg = Config.configure_section_for_tests("s1", attribute="foo")
+    s2_cfg = Config.configure_section_for_tests("s2", attribute="bar")
+    ss_cfg = Config.configure_list_section_for_tests("ss", attribute="foo", sections_list=[s1_cfg, s2_cfg])
+
+    s1_config_applied_instance = Config.section_name["s1"]
+    s1_config_python_instance = Config._python_config._sections[SectionForTest.name]["s1"]
+
+    s2_config_applied_instance = Config.section_name["s2"]
+    s2_config_python_instance = Config._python_config._sections[SectionForTest.name]["s2"]
+
+    assert ss_cfg.sections_list[0] is s1_config_applied_instance
+    assert ss_cfg.sections_list[0] is not s1_config_python_instance
+    assert ss_cfg.sections_list[1] is s2_config_applied_instance
+    assert ss_cfg.sections_list[1] is not s2_config_python_instance
+
+
+def _configure_in_toml():
+    return NamedTemporaryFile(
+        content="""
+[TAIPY]
+
+[section_name.s1]
+attribute = "foo"
+
+[section_name.s2]
+attribute = "bar"
+
+[list_section_name.ss]
+sections_list = [ "foo", "s1:SECTION", "s2:SECTION"]
+    """
+    )
+
+
+def test_nested_section_instance_load_toml(_init_list_section_for_test):
+    toml_config = _configure_in_toml()
+    Config.load(toml_config)
+
+    s1_config_applied_instance = Config.section_name["s1"]
+    s1_config_python_instance = Config._python_config._sections[SectionForTest.name]["s1"]
+
+    s2_config_applied_instance = Config.section_name["s2"]
+    s2_config_python_instance = Config._python_config._sections[SectionForTest.name]["s2"]
+
+    ss_cfg = Config.list_section_name["ss"]
+
+    assert ss_cfg.sections_list[0] == "foo"
+    assert ss_cfg.sections_list[1] is s1_config_applied_instance
+    assert ss_cfg.sections_list[1] is not s1_config_python_instance
+    assert ss_cfg.sections_list[2] is s2_config_applied_instance
+    assert ss_cfg.sections_list[2] is not s2_config_python_instance
+
+
+def test_nested_section_instance_override_toml(_init_list_section_for_test):
+    toml_config = _configure_in_toml()
+    Config.override(toml_config)
+
+    s1_config_applied_instance = Config.section_name["s1"]
+    s1_config_python_instance = Config._file_config._sections[SectionForTest.name]["s1"]
+
+    s2_config_applied_instance = Config.section_name["s2"]
+    s2_config_python_instance = Config._file_config._sections[SectionForTest.name]["s2"]
+
+    ss_cfg = Config.list_section_name["ss"]
+
+    assert ss_cfg.sections_list[0] == "foo"
+    assert ss_cfg.sections_list[1] is s1_config_applied_instance
+    assert ss_cfg.sections_list[1] is not s1_config_python_instance
+    assert ss_cfg.sections_list[2] is s2_config_applied_instance
+    assert ss_cfg.sections_list[1] is not s2_config_python_instance

+ 355 - 0
tests/config/test_config_comparator.py

@@ -0,0 +1,355 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from unittest import mock
+
+from src.taipy.config import Config
+from src.taipy.config._config import _Config
+from src.taipy.config._config_comparator._comparator_result import _ComparatorResult
+from src.taipy.config.global_app.global_app_config import GlobalAppConfig
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+class TestConfigComparator:
+    unique_section_1 = UniqueSectionForTest(attribute="unique_attribute_1", prop="unique_prop_1")
+    unique_section_1b = UniqueSectionForTest(attribute="unique_attribute_1", prop="unique_prop_1b")
+    section_1 = SectionForTest("section_1", attribute="attribute_1", prop="prop_1")
+    section_2 = SectionForTest("section_2", attribute=2, prop="prop_2")
+    section_2b = SectionForTest("section_2", attribute="attribute_2", prop="prop_2b")
+    section_3 = SectionForTest("section_3", attribute=[1, 2, 3, 4], prop=["prop_1"])
+    section_3b = SectionForTest("section_3", attribute=[1, 2], prop=["prop_1", "prop_2", "prop_3"])
+    section_3c = SectionForTest("section_3", attribute=[2, 1], prop=["prop_3", "prop_1", "prop_2"])
+
+    def test_comparator_compare_method_call(self):
+        _config_1 = _Config._default_config()
+        _config_2 = _Config._default_config()
+
+        with mock.patch(
+            "src.taipy.config._config_comparator._config_comparator._ConfigComparator._find_conflict_config"
+        ) as mck:
+            Config._comparator._find_conflict_config(_config_1, _config_2)
+            mck.assert_called_once_with(_config_1, _config_2)
+
+    def test_comparator_without_diff(self):
+        _config_1 = _Config._default_config()
+        _config_2 = _Config._default_config()
+
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+        assert isinstance(config_diff, _ComparatorResult)
+        assert config_diff == {}
+
+    def test_comparator_with_updated_global_config(self):
+        _config_1 = _Config._default_config()
+        _config_1._global_config = GlobalAppConfig(foo="bar")
+
+        _config_2 = _Config._default_config()
+        _config_2._global_config = GlobalAppConfig(foo="baz", bar="foo")
+
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        assert config_diff.get("unconflicted_sections") is None
+        assert config_diff.get("conflicted_sections") is not None
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["modified_items"]) == 1
+        assert conflicted_config_diff["modified_items"][0] == (
+            ("Global Configuration", "foo", None),
+            ("bar", "baz"),
+        )
+        assert len(conflicted_config_diff["added_items"]) == 1
+        assert conflicted_config_diff["added_items"][0] == (
+            ("Global Configuration", "bar", None),
+            "foo",
+        )
+
+    def test_comparator_with_new_section(self):
+        _config_1 = _Config._default_config()
+
+        # The first "section_name" is added to the Config
+        _config_2 = _Config._default_config()
+        _config_2._sections[SectionForTest.name] = {"section_1": self.section_1}
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["added_items"]) == 1
+        assert conflicted_config_diff["added_items"][0] == (
+            ("section_name", None, None),
+            {"section_1": {"attribute": "attribute_1", "prop": "prop_1"}},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("removed_items") is None
+
+        # A new "section_name" is added to the Config
+        _config_3 = _Config._default_config()
+        _config_3._sections[SectionForTest.name] = {"section_1": self.section_1, "section_2": self.section_2}
+        config_diff = Config._comparator._find_conflict_config(_config_2, _config_3)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["added_items"]) == 1
+        assert conflicted_config_diff["added_items"][0] == (
+            ("section_name", "section_2", None),
+            {"attribute": "2:int", "prop": "prop_2"},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("removed_items") is None
+
+    def test_comparator_with_removed_section(self):
+        _config_1 = _Config._default_config()
+
+        # All "section_name" sections are removed from the Config
+        _config_2 = _Config._default_config()
+        _config_2._sections[SectionForTest.name] = {"section_1": self.section_1}
+        config_diff = Config._comparator._find_conflict_config(_config_2, _config_1)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["removed_items"]) == 1
+        assert conflicted_config_diff["removed_items"][0] == (
+            ("section_name", None, None),
+            {"section_1": {"attribute": "attribute_1", "prop": "prop_1"}},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+        # Section "section_1" is removed from the Config
+        _config_3 = _Config._default_config()
+        _config_3._sections[SectionForTest.name] = {"section_1": self.section_1, "section_2": self.section_2}
+        config_diff = Config._comparator._find_conflict_config(_config_3, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["removed_items"]) == 1
+        assert conflicted_config_diff["removed_items"][0] == (
+            ("section_name", "section_2", None),
+            {"attribute": "2:int", "prop": "prop_2"},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+    def test_comparator_with_modified_section(self):
+        _config_1 = _Config._default_config()
+        _config_1._sections[SectionForTest.name] = {"section_2": self.section_2}
+
+        # All "section_name" sections are removed from the Config
+        _config_2 = _Config._default_config()
+        _config_2._sections[SectionForTest.name] = {"section_2": self.section_2b}
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["modified_items"]) == 2
+        assert conflicted_config_diff["modified_items"][0] == (
+            ("section_name", "section_2", "attribute"),
+            ("2:int", "attribute_2"),
+        )
+        assert conflicted_config_diff["modified_items"][1] == (
+            ("section_name", "section_2", "prop"),
+            ("prop_2", "prop_2b"),
+        )
+        assert conflicted_config_diff.get("removed_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+    def test_comparator_with_modified_list_attribute(self):
+        _config_1 = _Config._default_config()
+        _config_1._sections[SectionForTest.name] = {"section_3": self.section_3}
+
+        # All "section_name" sections are removed from the Config
+        _config_2 = _Config._default_config()
+        _config_2._sections[SectionForTest.name] = {"section_3": self.section_3b}
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["modified_items"]) == 2
+        assert conflicted_config_diff["modified_items"][0] == (
+            ("section_name", "section_3", "prop"),
+            (["prop_1"], ["prop_1", "prop_2", "prop_3"]),
+        )
+        assert conflicted_config_diff["modified_items"][1] == (
+            ("section_name", "section_3", "attribute"),
+            (["1:int", "2:int", "3:int", "4:int"], ["1:int", "2:int"]),
+        )
+        assert conflicted_config_diff.get("removed_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+    def test_comparator_with_different_order_list_attributes(self):
+        _config_1 = _Config._default_config()
+        _config_1._unique_sections
+        _config_1._sections[SectionForTest.name] = {"section_3": self.section_3b}
+
+        # Create _config_2 with different order of list attributes
+        _config_2 = _Config._default_config()
+        _config_2._sections[SectionForTest.name] = {"section_3": self.section_3c}
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        # There should be no difference since the order of list attributes is ignored
+        assert config_diff == {}
+
+    def test_comparator_with_new_unique_section(self):
+        _config_1 = _Config._default_config()
+
+        _config_2 = _Config._default_config()
+        _config_2._unique_sections[UniqueSectionForTest.name] = self.unique_section_1
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["added_items"]) == 1
+        assert conflicted_config_diff["added_items"][0] == (
+            ("unique_section_name", None, None),
+            {"attribute": "unique_attribute_1", "prop": "unique_prop_1"},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("removed_items") is None
+
+    def test_comparator_with_removed_unique_section(self):
+        _config_1 = _Config._default_config()
+
+        _config_2 = _Config._default_config()
+        _config_2._unique_sections[UniqueSectionForTest.name] = self.unique_section_1
+        config_diff = Config._comparator._find_conflict_config(_config_2, _config_1)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["removed_items"]) == 1
+        assert conflicted_config_diff["removed_items"][0] == (
+            ("unique_section_name", None, None),
+            {"attribute": "unique_attribute_1", "prop": "unique_prop_1"},
+        )
+        assert conflicted_config_diff.get("modified_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+    def test_comparator_with_modified_unique_section(self):
+        _config_1 = _Config._default_config()
+        _config_1._unique_sections[UniqueSectionForTest.name] = self.unique_section_1
+
+        # All "section_name" sections are removed from the Config
+        _config_2 = _Config._default_config()
+        _config_2._unique_sections[UniqueSectionForTest.name] = self.unique_section_1b
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        conflicted_config_diff = config_diff["conflicted_sections"]
+        assert len(conflicted_config_diff["modified_items"]) == 1
+        assert conflicted_config_diff["modified_items"][0] == (
+            ("unique_section_name", "prop", None),
+            ("unique_prop_1", "unique_prop_1b"),
+        )
+        assert conflicted_config_diff.get("removed_items") is None
+        assert conflicted_config_diff.get("added_items") is None
+
+    def test_unconflicted_section_name_store_statically(self):
+        Config._comparator._add_unconflicted_section("section_name_1")
+        assert Config._comparator._unconflicted_sections == {"section_name_1"}
+
+        Config._comparator._add_unconflicted_section("section_name_2")
+        assert Config._comparator._unconflicted_sections == {"section_name_1", "section_name_2"}
+
+        Config._comparator._add_unconflicted_section("section_name_1")
+        assert Config._comparator._unconflicted_sections == {"section_name_1", "section_name_2"}
+
+    def test_unconflicted_diff_is_stored_separated_from_conflicted_ones(self):
+        _config_1 = _Config._default_config()
+        _config_1._unique_sections[UniqueSectionForTest.name] = self.unique_section_1
+        _config_1._sections[SectionForTest.name] = {"section_2": self.section_2}
+
+        _config_2 = _Config._default_config()
+        _config_2._unique_sections[UniqueSectionForTest.name] = self.unique_section_1b
+        _config_2._sections[SectionForTest.name] = {"section_2": self.section_2b}
+
+        # Compare 2 Configuration
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        assert config_diff.get("unconflicted_sections") is None
+        assert config_diff.get("conflicted_sections") is not None
+        assert len(config_diff["conflicted_sections"]["modified_items"]) == 3
+
+        # Ignore any diff of "section_name" and compare
+        Config._comparator._add_unconflicted_section("section_name")
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+        assert config_diff.get("unconflicted_sections") is not None
+        assert len(config_diff["unconflicted_sections"]["modified_items"]) == 2
+        assert config_diff.get("conflicted_sections") is not None
+        assert len(config_diff["conflicted_sections"]["modified_items"]) == 1
+
+        # Ignore any diff of Global Config and compare
+        Config._comparator._add_unconflicted_section(["unique_section_name"])
+        config_diff = Config._comparator._find_conflict_config(_config_1, _config_2)
+        assert config_diff.get("unconflicted_sections") is not None
+        assert len(config_diff["unconflicted_sections"]["modified_items"]) == 3
+        assert config_diff.get("conflicted_sections") is None
+
+    def test_comparator_log_message(self, caplog):
+        _config_1 = _Config._default_config()
+        _config_1._unique_sections[UniqueSectionForTest.name] = self.unique_section_1
+        _config_1._sections[SectionForTest.name] = {"section_2": self.section_2}
+
+        _config_2 = _Config._default_config()
+        _config_2._unique_sections[UniqueSectionForTest.name] = self.unique_section_1b
+        _config_2._sections[SectionForTest.name] = {"section_2": self.section_2b}
+
+        # Ignore any diff of "section_name" and compare
+        Config._comparator._add_unconflicted_section("section_name")
+        Config._comparator._find_conflict_config(_config_1, _config_2)
+
+        error_messages = caplog.text.strip().split("\n")
+        assert len(error_messages) == 5
+        assert all(
+            t in error_messages[0]
+            for t in [
+                "INFO",
+                "There are non-conflicting changes between the current configuration and the current configuration:",
+            ]
+        )
+        assert 'section_name "section_2" has attribute "attribute" modified: 2:int -> attribute_2' in error_messages[1]
+        assert 'section_name "section_2" has attribute "prop" modified: prop_2 -> prop_2b' in error_messages[2]
+        assert all(
+            t in error_messages[3]
+            for t in [
+                "ERROR",
+                "The current configuration conflicts with the current configuration:",
+            ]
+        )
+        assert 'unique_section_name "prop" was modified: unique_prop_1 -> unique_prop_1b' in error_messages[4]
+
+        caplog.clear()
+
+        Config._comparator._find_conflict_config(_config_1, _config_2, old_version_number="1.0")
+
+        error_messages = caplog.text.strip().split("\n")
+        assert len(error_messages) == 5
+        assert all(
+            t in error_messages[0]
+            for t in [
+                "INFO",
+                "There are non-conflicting changes between the configuration for version 1.0 and the current "
+                "configuration:",
+            ]
+        )
+        assert all(
+            t in error_messages[3]
+            for t in [
+                "ERROR",
+                "The configuration for version 1.0 conflicts with the current configuration:",
+            ]
+        )
+
+        caplog.clear()
+
+        Config._comparator._compare(
+            _config_1,
+            _config_2,
+            version_number_1="1.0",
+            version_number_2="2.0",
+        )
+
+        error_messages = caplog.text.strip().split("\n")
+        assert len(error_messages) == 3
+        assert all(
+            t in error_messages[0]
+            for t in ["INFO", "Differences between version 1.0 Configuration and version 2.0 Configuration:"]
+        )
+
+        caplog.clear()

+ 48 - 0
tests/config/test_default_config.py

@@ -0,0 +1,48 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from src.taipy.config.config import Config
+from src.taipy.config.global_app.global_app_config import GlobalAppConfig
+from src.taipy.config.section import Section
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+def _test_default_global_app_config(global_config: GlobalAppConfig):
+    assert global_config is not None
+    assert not global_config.notification
+    assert len(global_config.properties) == 0
+
+
+def test_default_configuration():
+    default_config = Config._default_config
+    assert default_config._unique_sections is not None
+    assert len(default_config._unique_sections) == 1
+    assert default_config._unique_sections[UniqueSectionForTest.name] is not None
+    assert default_config._unique_sections[UniqueSectionForTest.name].attribute == "default_attribute"
+    assert default_config._sections is not None
+    assert len(default_config._sections) == 1
+
+    _test_default_global_app_config(default_config._global_config)
+    _test_default_global_app_config(Config.global_config)
+    _test_default_global_app_config(GlobalAppConfig().default_config())
+
+
+def test_register_default_configuration():
+    Config._register_default(SectionForTest(Section._DEFAULT_KEY, "default_attribute", prop1="prop1"))
+
+    # Replace the first default section
+    Config._register_default(SectionForTest(Section._DEFAULT_KEY, "default_attribute", prop2="prop2"))
+
+    default_section = Config.sections[SectionForTest.name][Section._DEFAULT_KEY]
+    assert len(default_section.properties) == 1
+    assert default_section.prop2 == "prop2"
+    assert default_section.prop1 is None

+ 58 - 0
tests/config/test_env_file_config.py

@@ -0,0 +1,58 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+
+import pytest
+
+from src.taipy.config.config import Config
+from src.taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked
+from tests.config.utils.named_temporary_file import NamedTemporaryFile
+
+config_from_filename = NamedTemporaryFile(
+    """
+[TAIPY]
+custom_property_not_overwritten = true
+custom_property_overwritten = 10
+"""
+)
+
+config_from_environment = NamedTemporaryFile(
+    """
+[TAIPY]
+custom_property_overwritten = 11
+"""
+)
+
+
+def test_load_from_environment_overwrite_load_from_filename():
+    os.environ[Config._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH] = config_from_environment.filename
+    Config.load(config_from_filename.filename)
+
+    assert Config.global_config.custom_property_not_overwritten is True
+    assert Config.global_config.custom_property_overwritten == 11
+    os.environ.pop(Config._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH)
+
+
+def test_block_load_from_environment_overwrite_load_from_filename():
+    Config.load(config_from_filename.filename)
+    assert Config.global_config.custom_property_not_overwritten is True
+    assert Config.global_config.custom_property_overwritten == 10
+
+    Config.block_update()
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        os.environ[Config._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH] = config_from_environment.filename
+        Config.load(config_from_filename.filename)
+
+    os.environ.pop(Config._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH)
+    assert Config.global_config.custom_property_not_overwritten is True
+    assert Config.global_config.custom_property_overwritten == 10  # The Config.load is failed to override

+ 42 - 0
tests/config/test_file_config.py

@@ -0,0 +1,42 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config.config import Config
+from src.taipy.config.exceptions.exceptions import LoadingError
+from tests.config.utils.named_temporary_file import NamedTemporaryFile
+
+
+def test_node_can_not_appear_twice():
+    config = NamedTemporaryFile(
+        """
+[unique_section_name]
+attribute = "my_attribute"
+
+[unique_section_name]
+attribute = "other_attribute"
+    """
+    )
+
+    with pytest.raises(LoadingError, match="Can not load configuration"):
+        Config.load(config.filename)
+
+
+def test_skip_configuration_outside_nodes():
+    config = NamedTemporaryFile(
+        """
+foo = "bar"
+    """
+    )
+
+    Config.load(config.filename)
+    assert Config.global_config.foo is None

+ 200 - 0
tests/config/test_override_config.py

@@ -0,0 +1,200 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+from unittest import mock
+
+import pytest
+
+from src.taipy.config.config import Config
+from src.taipy.config.exceptions.exceptions import InconsistentEnvVariableError, MissingEnvVariableError
+from tests.config.utils.named_temporary_file import NamedTemporaryFile
+
+
+def test_override_default_configuration_with_code_configuration():
+    assert not Config.global_config.root_folder == "foo"
+
+    assert len(Config.unique_sections) == 1
+    assert Config.unique_sections["unique_section_name"] is not None
+    assert Config.unique_sections["unique_section_name"].attribute == "default_attribute"
+    assert Config.unique_sections["unique_section_name"].prop is None
+
+    assert len(Config.sections) == 1
+    assert len(Config.sections["section_name"]) == 1
+    assert Config.sections["section_name"] is not None
+    assert Config.sections["section_name"]["default"].attribute == "default_attribute"
+
+    Config.configure_global_app(root_folder="foo")
+    assert Config.global_config.root_folder == "foo"
+
+    Config.configure_unique_section_for_tests("foo", prop="bar")
+    assert len(Config.unique_sections) == 1
+    assert Config.unique_sections["unique_section_name"] is not None
+    assert Config.unique_sections["unique_section_name"].attribute == "foo"
+    assert Config.unique_sections["unique_section_name"].prop == "bar"
+
+    Config.configure_section_for_tests("my_id", "baz", prop="qux")
+    assert len(Config.unique_sections) == 1
+    assert Config.sections["section_name"] is not None
+    assert Config.sections["section_name"]["my_id"].attribute == "baz"
+    assert Config.sections["section_name"]["my_id"].prop == "qux"
+
+
+def test_override_default_config_with_code_config_including_env_variable_values():
+    Config.configure_global_app()
+    assert Config.global_config.foo is None
+    Config.configure_global_app(foo="bar")
+    assert Config.global_config.foo == "bar"
+
+    with mock.patch.dict(os.environ, {"FOO": "foo"}):
+        Config.configure_global_app(foo="ENV[FOO]")
+        assert Config.global_config.foo == "foo"
+
+
+def test_override_default_configuration_with_file_configuration():
+    tf = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = "bar"
+
+"""
+    )
+    assert Config.global_config.foo is None
+
+    Config.load(tf.filename)
+
+    assert Config.global_config.foo == "bar"
+
+
+def test_override_default_config_with_file_config_including_env_variable_values():
+    tf = NamedTemporaryFile(
+        """
+[TAIPY]
+foo_attribute = "ENV[FOO]:int"
+bar_attribute = "ENV[BAR]:bool"
+"""
+    )
+    assert Config.global_config.foo_attribute is None
+    assert Config.global_config.bar_attribute is None
+
+    with mock.patch.dict(os.environ, {"FOO": "foo", "BAR": "true"}):
+        with pytest.raises(InconsistentEnvVariableError):
+            Config.load(tf.filename)
+            Config.global_config.foo_attribute
+
+    with mock.patch.dict(os.environ, {"FOO": "5"}):
+        with pytest.raises(MissingEnvVariableError):
+            Config.load(tf.filename)
+            Config.global_config.bar_attribute
+
+    with mock.patch.dict(os.environ, {"FOO": "6", "BAR": "TRUe"}):
+        Config.load(tf.filename)
+        assert Config.global_config.foo_attribute == 6
+        assert Config.global_config.bar_attribute
+
+
+def test_code_configuration_does_not_override_file_configuration():
+    config_from_filename = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = 2
+    """
+    )
+    Config.override(config_from_filename.filename)
+
+    Config.configure_global_app(foo=21)
+
+    assert Config.global_config.foo == 2  # From file config
+
+
+def test_code_configuration_does_not_override_file_configuration_including_env_variable_values():
+    config_from_filename = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = 2
+    """
+    )
+    Config.override(config_from_filename.filename)
+
+    with mock.patch.dict(os.environ, {"FOO": "21"}):
+        Config.configure_global_app(foo="ENV[FOO]")
+        assert Config.global_config.foo == 2  # From file config
+
+
+def test_file_configuration_overrides_code_configuration():
+    config_from_filename = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = 2
+    """
+    )
+    Config.configure_global_app(foo=21)
+    Config.load(config_from_filename.filename)
+
+    assert Config.global_config.foo == 2  # From file config
+
+
+def test_file_configuration_overrides_code_configuration_including_env_variable_values():
+    config_from_filename = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = "ENV[FOO]:int"
+    """
+    )
+    Config.configure_global_app(foo=21)
+
+    with mock.patch.dict(os.environ, {"FOO": "2"}):
+        Config.load(config_from_filename.filename)
+        assert Config.global_config.foo == 2  # From file config
+
+
+def test_override_default_configuration_with_multiple_configurations():
+    file_config = NamedTemporaryFile(
+        """
+[TAIPY]
+foo = 10
+bar = "baz"
+    """
+    )
+    # Default config is applied
+    assert Config.global_config.foo is None
+    assert Config.global_config.bar is None
+
+    # Code config is applied
+    Config.configure_global_app(foo="bar")
+    assert Config.global_config.foo == "bar"
+    assert Config.global_config.bar is None
+
+    # File config is applied
+    Config.load(file_config.filename)
+    assert Config.global_config.foo == 10
+    assert Config.global_config.bar == "baz"
+
+
+def test_override_default_configuration_with_multiple_configurations_including_environment_variable_values():
+    file_config = NamedTemporaryFile(
+        """
+[TAIPY]
+att = "ENV[BAZ]"
+    """
+    )
+
+    with mock.patch.dict(os.environ, {"FOO": "bar", "BAZ": "qux"}):
+        # Default config is applied
+        assert Config.global_config.att is None
+
+        # Code config is applied
+        Config.configure_global_app(att="ENV[FOO]")
+        assert Config.global_config.att == "bar"
+
+        # File config is applied
+        Config.load(file_config.filename)
+        assert Config.global_config.att == "qux"

+ 47 - 0
tests/config/test_section.py

@@ -0,0 +1,47 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+from unittest import mock
+
+import pytest
+
+from src.taipy.config.exceptions.exceptions import InvalidConfigurationId
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+class WrongUniqueSection(UniqueSectionForTest):
+    name = "1wrong_id"
+
+
+class WrongSection(SectionForTest):
+    name = "correct_name"
+
+
+def test_section_uses_valid_id():
+    with pytest.raises(InvalidConfigurationId):
+        WrongUniqueSection(attribute="foo")
+    with pytest.raises(InvalidConfigurationId):
+        WrongSection("wrong id", attribute="foo")
+    with pytest.raises(InvalidConfigurationId):
+        WrongSection("1wrong_id", attribute="foo")
+    with pytest.raises(InvalidConfigurationId):
+        WrongSection("wrong_@id", attribute="foo")
+
+
+def test_templated_properties_are_replaced():
+    with mock.patch.dict(os.environ, {"foo": "bar", "baz": "1"}):
+        u_sect = UniqueSectionForTest(attribute="attribute", tpl_property="ENV[foo]")
+        assert u_sect.tpl_property == "bar"
+
+        sect = SectionForTest(id="my_id", attribute="attribute", tpl_property="ENV[baz]:int")
+        assert sect.tpl_property == 1

+ 169 - 0
tests/config/test_section_registration.py

@@ -0,0 +1,169 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import pytest
+
+from src.taipy.config import Config
+from src.taipy.config.exceptions.exceptions import ConfigurationUpdateBlocked
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+def test_unique_section_registration_and_usage():
+    assert Config.unique_sections is not None
+    assert Config.unique_sections[UniqueSectionForTest.name] is not None
+    assert Config.unique_sections[UniqueSectionForTest.name].attribute == "default_attribute"
+    assert Config.unique_sections[UniqueSectionForTest.name].prop is None
+
+    mySection = Config.configure_unique_section_for_tests(attribute="my_attribute", prop="my_prop")
+
+    assert Config.unique_sections is not None
+    assert Config.unique_sections[UniqueSectionForTest.name] is not None
+    assert mySection is not None
+    assert Config.unique_sections[UniqueSectionForTest.name].attribute == "my_attribute"
+    assert mySection.attribute == "my_attribute"
+    assert Config.unique_sections[UniqueSectionForTest.name].prop == "my_prop"
+    assert mySection.prop == "my_prop"
+
+    myNewSection = Config.configure_unique_section_for_tests(attribute="my_new_attribute", prop="my_new_prop")
+
+    assert Config.unique_sections is not None
+    assert Config.unique_sections[UniqueSectionForTest.name] is not None
+    assert myNewSection is not None
+    assert mySection is not None
+    assert Config.unique_sections[UniqueSectionForTest.name].attribute == "my_new_attribute"
+    assert myNewSection.attribute == "my_new_attribute"
+    assert mySection.attribute == "my_new_attribute"
+    assert Config.unique_sections[UniqueSectionForTest.name].prop == "my_new_prop"
+    assert myNewSection.prop == "my_new_prop"
+    assert mySection.prop == "my_new_prop"
+
+
+def test_sections_exposed_as_attribute():
+    assert Config.unique_section_name.attribute == "default_attribute"
+    Config.configure_unique_section_for_tests("my_attribute")
+    assert Config.unique_section_name.attribute == "my_attribute"
+
+    assert Config.section_name["default"].attribute == "default_attribute"
+    Config.configure_section_for_tests(id="my_id", attribute="my_attribute")
+    assert Config.section_name["my_id"].attribute == "my_attribute"
+
+
+def test_section_registration_and_usage():
+    assert Config.sections is not None
+    assert len(Config.sections) == 1
+    assert Config.sections[SectionForTest.name] is not None
+    assert len(Config.sections[SectionForTest.name]) == 1
+    assert Config.sections[SectionForTest.name]["default"] is not None
+    assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+    assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["default"].foo is None
+
+    myFirstSection = Config.configure_section_for_tests(id="first", attribute="my_attribute", prop="my_prop", foo="bar")
+    assert Config.sections is not None
+    assert len(Config.sections) == 1
+    assert Config.sections[SectionForTest.name] is not None
+    assert len(Config.sections[SectionForTest.name]) == 2
+    assert Config.sections[SectionForTest.name]["default"] is not None
+    assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+    assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["default"].foo is None
+    assert Config.sections[SectionForTest.name]["first"] is not None
+    assert Config.sections[SectionForTest.name]["first"].attribute == "my_attribute"
+    assert Config.sections[SectionForTest.name]["first"].prop == "my_prop"
+    assert Config.sections[SectionForTest.name]["first"].foo == "bar"
+    assert myFirstSection.attribute == "my_attribute"
+    assert myFirstSection.prop == "my_prop"
+    assert myFirstSection.foo == "bar"
+
+    myNewSection = Config.configure_section_for_tests(id="second", attribute="my_new_attribute", prop="my_new_prop")
+    assert Config.sections is not None
+    assert len(Config.sections) == 1
+    assert Config.sections[SectionForTest.name] is not None
+    assert len(Config.sections[SectionForTest.name]) == 3
+    assert Config.sections[SectionForTest.name]["default"] is not None
+    assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+    assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["default"].foo is None
+    assert Config.sections[SectionForTest.name]["first"] is not None
+    assert Config.sections[SectionForTest.name]["first"].attribute == "my_attribute"
+    assert Config.sections[SectionForTest.name]["first"].prop == "my_prop"
+    assert Config.sections[SectionForTest.name]["first"].foo == "bar"
+    assert Config.sections[SectionForTest.name]["second"] is not None
+    assert Config.sections[SectionForTest.name]["second"].attribute == "my_new_attribute"
+    assert Config.sections[SectionForTest.name]["second"].prop == "my_new_prop"
+    assert Config.sections[SectionForTest.name]["second"].foo is None
+    assert myFirstSection.attribute == "my_attribute"
+    assert myFirstSection.prop == "my_prop"
+    assert myFirstSection.foo == "bar"
+    assert myNewSection.attribute == "my_new_attribute"
+    assert myNewSection.prop == "my_new_prop"
+    assert myNewSection.foo is None
+
+    my2ndSection = Config.configure_section_for_tests(id="second", attribute="my_2nd_attribute", prop="my_2nd_prop")
+    assert Config.sections is not None
+    assert len(Config.sections) == 1
+    assert Config.sections[SectionForTest.name] is not None
+    assert len(Config.sections[SectionForTest.name]) == 3
+    assert Config.sections[SectionForTest.name]["default"] is not None
+    assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+    assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["default"].foo is None
+    assert Config.sections[SectionForTest.name]["first"] is not None
+    assert Config.sections[SectionForTest.name]["first"].attribute == "my_attribute"
+    assert Config.sections[SectionForTest.name]["first"].prop == "my_prop"
+    assert Config.sections[SectionForTest.name]["first"].foo == "bar"
+    assert Config.sections[SectionForTest.name]["second"] is not None
+    assert Config.sections[SectionForTest.name]["second"].attribute == "my_2nd_attribute"
+    assert Config.sections[SectionForTest.name]["second"].prop == "my_2nd_prop"
+    assert Config.sections[SectionForTest.name]["second"].foo is None
+    assert myFirstSection.attribute == "my_attribute"
+    assert myFirstSection.prop == "my_prop"
+    assert myFirstSection.foo == "bar"
+    assert myNewSection.attribute == "my_2nd_attribute"
+    assert myNewSection.prop == "my_2nd_prop"
+    assert myNewSection.foo is None
+    assert my2ndSection.attribute == "my_2nd_attribute"
+    assert my2ndSection.prop == "my_2nd_prop"
+    assert my2ndSection.foo is None
+
+
+def test_block_registration():
+    myUniqueSection = Config.configure_unique_section_for_tests(attribute="my_unique_attribute", prop="my_unique_prop")
+    mySection = Config.configure_section_for_tests(id="section_id", attribute="my_attribute", prop="my_prop", foo="bar")
+
+    Config.block_update()
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        Config.configure_unique_section_for_tests(attribute="my_new_unique_attribute", prop="my_new_unique_prop")
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        Config.configure_section_for_tests(id="new", attribute="my_attribute", prop="my_prop", foo="bar")
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        myUniqueSection.attribute = "foo"
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        myUniqueSection.properties = {"foo": "bar"}
+
+    # myUniqueSection stay the same
+    assert myUniqueSection.attribute == "my_unique_attribute"
+    assert myUniqueSection.properties == {"prop": "my_unique_prop"}
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        mySection.attribute = "foo"
+
+    with pytest.raises(ConfigurationUpdateBlocked):
+        mySection.properties = {"foo": "foo"}
+
+    # mySection stay the same
+    assert mySection.attribute == "my_attribute"
+    assert mySection.properties == {"prop": "my_prop", "foo": "bar", "prop_int": 0}

+ 477 - 0
tests/config/test_section_serialization.py

@@ -0,0 +1,477 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import datetime
+import json
+import os
+from unittest import mock
+
+from src.taipy.config import Config
+from src.taipy.config._serializer._json_serializer import _JsonSerializer
+from src.taipy.config.common.frequency import Frequency
+from src.taipy.config.common.scope import Scope
+from tests.config.utils.named_temporary_file import NamedTemporaryFile
+from tests.config.utils.section_for_tests import SectionForTest
+from tests.config.utils.unique_section_for_tests import UniqueSectionForTest
+
+
+def add(a, b):
+    return a + b
+
+
+class CustomClass:
+    a = None
+    b = None
+
+
+class CustomEncoder(json.JSONEncoder):
+    def default(self, o):
+        if isinstance(o, datetime):
+            result = {"__type__": "Datetime", "__value__": o.isoformat()}
+        else:
+            result = json.JSONEncoder.default(self, o)
+        return result
+
+
+class CustomDecoder(json.JSONDecoder):
+    def __init__(self, *args, **kwargs):
+        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
+
+    def object_hook(self, source):
+        if source.get("__type__") == "Datetime":
+            return datetime.fromisoformat(source.get("__value__"))
+        else:
+            return source
+
+
+def test_write_toml_configuration_file():
+    expected_toml_config = """
+[TAIPY]
+
+[unique_section_name]
+attribute = "my_attribute"
+prop = "my_prop"
+prop_int = "1:int"
+prop_bool = "False:bool"
+prop_list = [ "p1", "1991-01-01T00:00:00:datetime", "1d0h0m0s:timedelta",]
+prop_scope = "SCENARIO:SCOPE"
+prop_freq = "QUARTERLY:FREQUENCY"
+baz = "ENV[QUX]"
+quux = "ENV[QUUZ]:bool"
+corge = [ "grault", "ENV[GARPLY]", "ENV[WALDO]:int", "3.0:float",]
+
+[section_name.default]
+attribute = "default_attribute"
+prop = "default_prop"
+prop_int = "0:int"
+
+[section_name.my_id]
+attribute = "my_attribute"
+prop = "default_prop"
+prop_int = "1:int"
+prop_bool = "False:bool"
+prop_list = [ "unique_section_name:SECTION",]
+prop_scope = "SCENARIO"
+baz = "ENV[QUX]"
+    """.strip()
+    tf = NamedTemporaryFile()
+    with mock.patch.dict(
+        os.environ, {"FOO": "in_memory", "QUX": "qux", "QUUZ": "true", "GARPLY": "garply", "WALDO": "17"}
+    ):
+        unique_section = Config.configure_unique_section_for_tests(
+            attribute="my_attribute",
+            prop="my_prop",
+            prop_int=1,
+            prop_bool=False,
+            prop_list=["p1", datetime.datetime(1991, 1, 1), datetime.timedelta(days=1)],
+            prop_scope=Scope.SCENARIO,
+            prop_freq=Frequency.QUARTERLY,
+            baz="ENV[QUX]",
+            quux="ENV[QUUZ]:bool",
+            corge=("grault", "ENV[GARPLY]", "ENV[WALDO]:int", 3.0),
+        )
+        Config.configure_section_for_tests(
+            "my_id",
+            "my_attribute",
+            prop_int=1,
+            prop_bool=False,
+            prop_list=[unique_section],
+            prop_scope="SCENARIO",
+            baz="ENV[QUX]",
+        )
+
+        Config.backup(tf.filename)
+        actual_config = tf.read().strip()
+        assert actual_config == expected_toml_config
+
+
+def test_read_toml_configuration_file():
+    toml_config = """
+[TAIPY]
+foo = "bar"
+
+[unique_section_name]
+attribute = "my_attribute"
+prop = "my_prop"
+prop_int = "1:int"
+prop_bool = "False:bool"
+prop_list = [ "p1", "1991-01-01T00:00:00:datetime", "1d0h0m0s:timedelta",]
+prop_scope = "SCENARIO:SCOPE"
+prop_freq = "QUARTERLY:FREQUENCY"
+baz = "ENV[QUX]"
+quux = "ENV[QUUZ]:bool"
+corge = [ "grault", "ENV[GARPLY]", "ENV[WALDO]:int", "3.0:float",]
+
+[TAIPY.custom_properties]
+bar = "baz"
+
+[section_name.default]
+attribute = "default_attribute"
+prop = "default_prop"
+prop_int = "0:int"
+
+[section_name.my_id]
+attribute = "my_attribute"
+prop = "default_prop"
+prop_int = "1:int"
+prop_bool = "False:bool"
+prop_list = [ "unique_section_name", "section_name.my_id",]
+prop_scope = "SCENARIO:SCOPE"
+baz = "ENV[QUX]"
+    """.strip()
+    tf = NamedTemporaryFile(toml_config)
+    with mock.patch.dict(
+        os.environ, {"FOO": "in_memory", "QUX": "qux", "QUUZ": "true", "GARPLY": "garply", "WALDO": "17"}
+    ):
+        Config.override(tf.filename)
+
+        assert Config.global_config.foo == "bar"
+        assert Config.global_config.custom_properties.get("bar") == "baz"
+
+        assert Config.unique_sections is not None
+        assert Config.unique_sections[UniqueSectionForTest.name] is not None
+        assert Config.unique_sections[UniqueSectionForTest.name].attribute == "my_attribute"
+        assert Config.unique_sections[UniqueSectionForTest.name].prop == "my_prop"
+        assert Config.unique_sections[UniqueSectionForTest.name].prop_int == 1
+        assert Config.unique_sections[UniqueSectionForTest.name].prop_bool is False
+        assert Config.unique_sections[UniqueSectionForTest.name].prop_list == [
+            "p1",
+            datetime.datetime(1991, 1, 1),
+            datetime.timedelta(days=1),
+        ]
+        assert Config.unique_sections[UniqueSectionForTest.name].prop_scope == Scope.SCENARIO
+        assert Config.unique_sections[UniqueSectionForTest.name].prop_freq == Frequency.QUARTERLY
+        assert Config.unique_sections[UniqueSectionForTest.name].baz == "qux"
+        assert Config.unique_sections[UniqueSectionForTest.name].quux is True
+        assert Config.unique_sections[UniqueSectionForTest.name].corge == [
+            "grault",
+            "garply",
+            17,
+            3.0,
+        ]
+
+        assert Config.sections is not None
+        assert len(Config.sections) == 1
+        assert Config.sections[SectionForTest.name] is not None
+        assert len(Config.sections[SectionForTest.name]) == 2
+        assert Config.sections[SectionForTest.name]["default"] is not None
+        assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+        assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+        assert Config.sections[SectionForTest.name]["default"].prop_int == 0
+        assert Config.sections[SectionForTest.name]["my_id"] is not None
+        assert Config.sections[SectionForTest.name]["my_id"].attribute == "my_attribute"
+        assert Config.sections[SectionForTest.name]["my_id"].prop == "default_prop"
+        assert Config.sections[SectionForTest.name]["my_id"].prop_int == 1
+        assert Config.sections[SectionForTest.name]["my_id"].prop_bool is False
+        assert Config.sections[SectionForTest.name]["my_id"].prop_list == ["unique_section_name", "section_name.my_id"]
+        assert Config.sections[SectionForTest.name]["my_id"].prop_scope == Scope.SCENARIO
+        assert Config.sections[SectionForTest.name]["my_id"].baz == "qux"
+
+        tf2 = NamedTemporaryFile()
+        Config.backup(tf2.filename)
+        actual_config_2 = tf2.read().strip()
+        assert actual_config_2 == toml_config
+
+
+def test_read_write_toml_configuration_file_with_function_and_class():
+    expected_toml_config = """
+[TAIPY]
+
+[unique_section_name]
+attribute = "my_attribute"
+prop = "my_prop"
+prop_list = [ "tests.config.test_section_serialization.CustomEncoder:class", \
+"tests.config.test_section_serialization.CustomDecoder:class",]
+
+[section_name.default]
+attribute = "default_attribute"
+prop = "default_prop"
+prop_int = "0:int"
+
+[section_name.my_id]
+attribute = "my_attribute"
+prop = "default_prop"
+prop_int = "0:int"
+prop_fct_list = [ "tests.config.test_section_serialization.add:function",]
+prop_class_list = [ "tests.config.test_section_serialization.CustomClass:class",]
+
+[section_name.my_id_2]
+attribute = "my_attribute_2"
+prop = "default_prop"
+prop_int = "0:int"
+prop_fct_list = [ "builtins.print:function", "builtins.pow:function",]
+    """.strip()
+
+    tf = NamedTemporaryFile()
+    Config.configure_unique_section_for_tests(
+        attribute="my_attribute",
+        prop="my_prop",
+        prop_list=[CustomEncoder, CustomDecoder],
+    )
+    Config.configure_section_for_tests(
+        "my_id",
+        "my_attribute",
+        prop_fct_list=[add],
+        prop_class_list=[CustomClass],
+    )
+
+    Config.configure_section_for_tests(
+        "my_id_2",
+        "my_attribute_2",
+        prop_fct_list=[print, pow],
+    )
+
+    Config.backup(tf.filename)
+    actual_exported_toml = tf.read().strip()
+    assert actual_exported_toml == expected_toml_config
+
+    Config.override(tf.filename)
+    tf2 = NamedTemporaryFile()
+    Config.backup(tf2.filename)
+
+    actual_exported_toml_2 = tf2.read().strip()
+    assert actual_exported_toml_2 == expected_toml_config
+
+
+def test_write_json_configuration_file():
+    expected_json_config = """
+{
+"TAIPY": {},
+"unique_section_name": {
+"attribute": "my_attribute",
+"prop": "my_prop",
+"prop_int": "1:int",
+"prop_bool": "False:bool",
+"prop_list": [
+"p1",
+"1991-01-01T00:00:00:datetime",
+"1d0h0m0s:timedelta"
+],
+"prop_scope": "SCENARIO:SCOPE",
+"prop_freq": "QUARTERLY:FREQUENCY"
+},
+"section_name": {
+"default": {
+"attribute": "default_attribute",
+"prop": "default_prop",
+"prop_int": "0:int"
+},
+"my_id": {
+"attribute": "my_attribute",
+"prop": "default_prop",
+"prop_int": "1:int",
+"prop_bool": "False:bool",
+"prop_list": [
+"unique_section_name:SECTION"
+],
+"prop_scope": "SCENARIO",
+"baz": "ENV[QUX]"
+}
+}
+}
+    """.strip()
+    tf = NamedTemporaryFile()
+    Config._serializer = _JsonSerializer()
+
+    unique_section = Config.configure_unique_section_for_tests(
+        attribute="my_attribute",
+        prop="my_prop",
+        prop_int=1,
+        prop_bool=False,
+        prop_list=["p1", datetime.datetime(1991, 1, 1), datetime.timedelta(days=1)],
+        prop_scope=Scope.SCENARIO,
+        prop_freq=Frequency.QUARTERLY,
+    )
+    Config.configure_section_for_tests(
+        "my_id",
+        "my_attribute",
+        prop_int=1,
+        prop_bool=False,
+        prop_list=[unique_section],
+        prop_scope="SCENARIO",
+        baz="ENV[QUX]",
+    )
+    Config.backup(tf.filename)
+    actual_config = tf.read()
+    assert actual_config == expected_json_config
+
+
+def test_read_json_configuration_file():
+    json_config = """
+{
+"TAIPY": {
+"root_folder": "./taipy/",
+"storage_folder": ".data/",
+"repository_type": "filesystem"
+},
+"unique_section_name": {
+"attribute": "my_attribute",
+"prop": "my_prop",
+"prop_int": "1:int",
+"prop_bool": "False:bool",
+"prop_list": [
+"p1",
+"1991-01-01T00:00:00:datetime",
+"1d0h0m0s:timedelta"
+],
+"prop_scope": "SCENARIO:SCOPE",
+"prop_freq": "QUARTERLY:FREQUENCY"
+},
+"section_name": {
+"default": {
+"attribute": "default_attribute",
+"prop": "default_prop",
+"prop_int": "0:int"
+},
+"my_id": {
+"attribute": "my_attribute",
+"prop": "default_prop",
+"prop_int": "1:int",
+"prop_bool": "False:bool",
+"prop_list": [
+"unique_section_name"
+],
+"prop_scope": "SCENARIO"
+}
+}
+}
+    """.strip()
+    Config._serializer = _JsonSerializer()
+    tf = NamedTemporaryFile(json_config)
+
+    Config.override(tf.filename)
+
+    assert Config.unique_sections is not None
+    assert Config.unique_sections[UniqueSectionForTest.name] is not None
+    assert Config.unique_sections[UniqueSectionForTest.name].attribute == "my_attribute"
+    assert Config.unique_sections[UniqueSectionForTest.name].prop == "my_prop"
+    assert Config.unique_sections[UniqueSectionForTest.name].prop_int == 1
+    assert Config.unique_sections[UniqueSectionForTest.name].prop_bool is False
+    assert Config.unique_sections[UniqueSectionForTest.name].prop_list == [
+        "p1",
+        datetime.datetime(1991, 1, 1),
+        datetime.timedelta(days=1),
+    ]
+    assert Config.unique_sections[UniqueSectionForTest.name].prop_scope == Scope.SCENARIO
+    assert Config.unique_sections[UniqueSectionForTest.name].prop_freq == Frequency.QUARTERLY
+
+    assert Config.sections is not None
+    assert len(Config.sections) == 1
+    assert Config.sections[SectionForTest.name] is not None
+    assert len(Config.sections[SectionForTest.name]) == 2
+    assert Config.sections[SectionForTest.name]["default"] is not None
+    assert Config.sections[SectionForTest.name]["default"].attribute == "default_attribute"
+    assert Config.sections[SectionForTest.name]["default"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["default"].prop_int == 0
+    assert Config.sections[SectionForTest.name]["my_id"] is not None
+    assert Config.sections[SectionForTest.name]["my_id"].attribute == "my_attribute"
+    assert Config.sections[SectionForTest.name]["my_id"].prop == "default_prop"
+    assert Config.sections[SectionForTest.name]["my_id"].prop_int == 1
+    assert Config.sections[SectionForTest.name]["my_id"].prop_bool is False
+    assert Config.sections[SectionForTest.name]["my_id"].prop_list == ["unique_section_name"]
+
+    tf2 = NamedTemporaryFile()
+    Config.backup(tf2.filename)
+    actual_config_2 = tf2.read().strip()
+    assert actual_config_2 == json_config
+
+
+def test_read_write_json_configuration_file_with_function_and_class():
+    expected_json_config = """
+{
+"TAIPY": {},
+"unique_section_name": {
+"attribute": "my_attribute",
+"prop": "my_prop",
+"prop_list": [
+"tests.config.test_section_serialization.CustomEncoder:class",
+"tests.config.test_section_serialization.CustomDecoder:class"
+]
+},
+"section_name": {
+"default": {
+"attribute": "default_attribute",
+"prop": "default_prop",
+"prop_int": "0:int"
+},
+"my_id": {
+"attribute": "my_attribute",
+"prop": "default_prop",
+"prop_int": "0:int",
+"prop_fct_list": [
+"tests.config.test_section_serialization.add:function"
+],
+"prop_class_list": [
+"tests.config.test_section_serialization.CustomClass:class"
+]
+},
+"my_id_2": {
+"attribute": "my_attribute_2",
+"prop": "default_prop",
+"prop_int": "0:int",
+"prop_fct_list": [
+"builtins.print:function",
+"builtins.pow:function"
+]
+}
+}
+}
+    """.strip()
+
+    Config._serializer = _JsonSerializer()
+    tf = NamedTemporaryFile()
+    Config.configure_unique_section_for_tests(
+        attribute="my_attribute",
+        prop="my_prop",
+        prop_list=[CustomEncoder, CustomDecoder],
+    )
+    Config.configure_section_for_tests(
+        "my_id",
+        "my_attribute",
+        prop_fct_list=[add],
+        prop_class_list=[CustomClass],
+    )
+    Config.configure_section_for_tests(
+        "my_id_2",
+        "my_attribute_2",
+        prop_fct_list=[print, pow],
+    )
+
+    Config.backup(tf.filename)
+    actual_exported_json = tf.read().strip()
+    assert actual_exported_json == expected_json_config
+
+    Config.override(tf.filename)
+    tf2 = NamedTemporaryFile()
+    Config.backup(tf2.filename)
+
+    actual_exported_json_2 = tf2.read().strip()
+    assert actual_exported_json_2 == expected_json_config

+ 10 - 0
tests/config/utils/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 18 - 0
tests/config/utils/checker_for_tests.py

@@ -0,0 +1,18 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from src.taipy.config import IssueCollector
+from src.taipy.config.checker._checkers._config_checker import _ConfigChecker
+
+
+class CheckerForTest(_ConfigChecker):
+    def _check(self) -> IssueCollector:
+        return self._collector

+ 28 - 0
tests/config/utils/named_temporary_file.py

@@ -0,0 +1,28 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+import tempfile
+
+
+class NamedTemporaryFile:
+    def __init__(self, content=None):
+        with tempfile.NamedTemporaryFile("w", delete=False) as fd:
+            if content:
+                fd.write(content)
+            self.filename = fd.name
+
+    def read(self):
+        with open(self.filename, "r") as fp:
+            return fp.read()
+
+    def __del__(self):
+        os.unlink(self.filename)

+ 69 - 0
tests/config/utils/section_for_tests.py

@@ -0,0 +1,69 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from copy import copy
+from typing import Any, Dict, Optional
+
+from src.taipy.config import Config, Section
+from src.taipy.config._config import _Config
+from src.taipy.config.common._config_blocker import _ConfigBlocker
+
+
+class SectionForTest(Section):
+    name = "section_name"
+    _MY_ATTRIBUTE_KEY = "attribute"
+
+    def __init__(self, id: str, attribute: Any = None, **properties):
+        self._attribute = attribute
+        super().__init__(id, **properties)
+
+    def __copy__(self):
+        return SectionForTest(self.id, self._attribute, **copy(self._properties))
+
+    @property
+    def attribute(self):
+        return self._replace_templates(self._attribute)
+
+    @attribute.setter  # type: ignore
+    @_ConfigBlocker._check()
+    def attribute(self, val):
+        self._attribute = val
+
+    def _clean(self):
+        self._attribute = None
+        self._properties.clear()
+
+    def _to_dict(self):
+        as_dict = {}
+        if self._attribute is not None:
+            as_dict[self._MY_ATTRIBUTE_KEY] = self._attribute
+        as_dict.update(self._properties)
+        return as_dict
+
+    @classmethod
+    def _from_dict(cls, as_dict: Dict[str, Any], id: str, config: Optional[_Config] = None):
+        as_dict.pop(cls._ID_KEY, id)
+        attribute = as_dict.pop(cls._MY_ATTRIBUTE_KEY, None)
+        return SectionForTest(id=id, attribute=attribute, **as_dict)
+
+    def _update(self, as_dict: Dict[str, Any], default_section=None):
+        self._attribute = as_dict.pop(self._MY_ATTRIBUTE_KEY, self._attribute)
+        if self._attribute is None and default_section:
+            self._attribute = default_section._attribute
+        self._properties.update(as_dict)
+        if default_section:
+            self._properties = {**default_section.properties, **self._properties}
+
+    @staticmethod
+    def _configure(id: str, attribute: str, **properties):
+        section = SectionForTest(id, attribute, **properties)
+        Config._register(section)
+        return Config.sections[SectionForTest.name][id]

+ 98 - 0
tests/config/utils/section_of_sections_list_for_tests.py

@@ -0,0 +1,98 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from copy import copy
+from typing import Any, Dict, List, Optional
+
+from src.taipy.config import Config, Section
+from src.taipy.config._config import _Config
+from src.taipy.config.common._config_blocker import _ConfigBlocker
+
+from .section_for_tests import SectionForTest
+
+
+class SectionOfSectionsListForTest(Section):
+    name = "list_section_name"
+    _MY_ATTRIBUTE_KEY = "attribute"
+    _SECTIONS_LIST_KEY = "sections_list"
+
+    def __init__(self, id: str, attribute: Any = None, sections_list: List = None, **properties):
+        self._attribute = attribute
+        self._sections_list = sections_list if sections_list else []
+        super().__init__(id, **properties)
+
+    def __copy__(self):
+        return SectionOfSectionsListForTest(
+            self.id, self._attribute, copy(self._sections_list), **copy(self._properties)
+        )
+
+    @property
+    def attribute(self):
+        return self._replace_templates(self._attribute)
+
+    @attribute.setter  # type: ignore
+    @_ConfigBlocker._check()
+    def attribute(self, val):
+        self._attribute = val
+
+    @property
+    def sections_list(self):
+        return list(self._sections_list)
+
+    @sections_list.setter  # type: ignore
+    @_ConfigBlocker._check()
+    def sections_list(self, val):
+        self._sections_list = val
+
+    def _clean(self):
+        self._attribute = None
+        self._sections_list = []
+        self._properties.clear()
+
+    def _to_dict(self):
+        as_dict = {}
+        if self._attribute is not None:
+            as_dict[self._MY_ATTRIBUTE_KEY] = self._attribute
+        if self._sections_list:
+            as_dict[self._SECTIONS_LIST_KEY] = self._sections_list
+        as_dict.update(self._properties)
+        return as_dict
+
+    @classmethod
+    def _from_dict(cls, as_dict: Dict[str, Any], id: str, config: Optional[_Config] = None):
+        as_dict.pop(cls._ID_KEY, id)
+        attribute = as_dict.pop(cls._MY_ATTRIBUTE_KEY, None)
+        section_configs = config._sections.get(SectionForTest.name, None) or []  # type: ignore
+        sections_list = []
+        if inputs_as_str := as_dict.pop(cls._SECTIONS_LIST_KEY, None):
+            for section_id in inputs_as_str:
+                if section_id in section_configs:
+                    sections_list.append(section_configs[section_id])
+                else:
+                    sections_list.append(section_id)
+        return SectionOfSectionsListForTest(id=id, attribute=attribute, sections_list=sections_list, **as_dict)
+
+    def _update(self, as_dict: Dict[str, Any], default_section=None):
+        self._attribute = as_dict.pop(self._MY_ATTRIBUTE_KEY, self._attribute)
+        if self._attribute is None and default_section:
+            self._attribute = default_section._attribute
+        self._sections_list = as_dict.pop(self._SECTIONS_LIST_KEY, self._sections_list)
+        if self._sections_list is None and default_section:
+            self._sections_list = default_section._sections_list
+        self._properties.update(as_dict)
+        if default_section:
+            self._properties = {**default_section.properties, **self._properties}
+
+    @staticmethod
+    def _configure(id: str, attribute: str, sections_list: List = None, **properties):
+        section = SectionOfSectionsListForTest(id, attribute, sections_list, **properties)
+        Config._register(section)
+        return Config.sections[SectionOfSectionsListForTest.name][id]

+ 70 - 0
tests/config/utils/unique_section_for_tests.py

@@ -0,0 +1,70 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from copy import copy
+from typing import Any, Dict, Optional
+
+from src.taipy.config import Config
+from src.taipy.config._config import _Config
+from src.taipy.config.common._config_blocker import _ConfigBlocker
+from src.taipy.config.unique_section import UniqueSection
+
+
+class UniqueSectionForTest(UniqueSection):
+    name = "unique_section_name"
+    _MY_ATTRIBUTE_KEY = "attribute"
+
+    def __init__(self, attribute: str = None, **properties):
+        self._attribute = attribute
+        super().__init__(**properties)
+
+    def __copy__(self):
+        return UniqueSectionForTest(self._attribute, **copy(self._properties))
+
+    @property
+    def attribute(self):
+        return self._replace_templates(self._attribute)
+
+    @attribute.setter  # type: ignore
+    @_ConfigBlocker._check()
+    def attribute(self, val):
+        self._attribute = val
+
+    def _clean(self):
+        self._attribute = None
+        self._properties.clear()
+
+    def _to_dict(self):
+        as_dict = {}
+        if self._attribute is not None:
+            as_dict[self._MY_ATTRIBUTE_KEY] = self._attribute
+        as_dict.update(self._properties)
+        return as_dict
+
+    @classmethod
+    def _from_dict(cls, as_dict: Dict[str, Any], id=None, config: Optional[_Config] = None):
+        as_dict.pop(cls._ID_KEY, None)
+        attribute = as_dict.pop(cls._MY_ATTRIBUTE_KEY, None)
+        return UniqueSectionForTest(attribute=attribute, **as_dict)
+
+    def _update(self, as_dict: Dict[str, Any], default_section=None):
+        self._attribute = as_dict.pop(self._MY_ATTRIBUTE_KEY, self._attribute)
+        if self._attribute is None and default_section:
+            self._attribute = default_section._attribute
+        self._properties.update(as_dict)
+        if default_section:
+            self._properties = {**default_section.properties, **self._properties}
+
+    @staticmethod
+    def _configure(attribute: str, **properties):
+        section = UniqueSectionForTest(attribute, **properties)
+        Config._register(section)
+        return Config.unique_sections[UniqueSectionForTest.name]

+ 10 - 0
tests/logger/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.

+ 27 - 0
tests/logger/logger.conf

@@ -0,0 +1,27 @@
+[loggers]
+keys=root,Taipy
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler
+
+[logger_Taipy]
+level=DEBUG
+handlers=consoleHandler
+qualname=Taipy
+propagate=0
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleFormatter]
+format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

+ 28 - 0
tests/logger/test_logger.py

@@ -0,0 +1,28 @@
+# Copyright 2023 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+import pathlib
+from unittest import TestCase, mock
+
+from src.taipy.logger._taipy_logger import _TaipyLogger
+
+
+class TestTaipyLogger(TestCase):
+    def test_taipy_logger(self):
+        _TaipyLogger._get_logger().info("baz")
+        _TaipyLogger._get_logger().debug("qux")
+
+    def test_taipy_logger_configured_by_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "logger.conf")
+        with mock.patch.dict(os.environ, {"TAIPY_LOGGER_CONFIG_PATH": path}):
+            _TaipyLogger._get_logger().info("baz")
+            _TaipyLogger._get_logger().debug("qux")