浏览代码

refactor Taipy CLI

trgiangdo 1 年之前
父节点
当前提交
63eb6946c2

+ 0 - 2
taipy/_cli/_base_cli/__init__.py

@@ -8,5 +8,3 @@
 # 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 ._cli import _CLI

+ 64 - 0
taipy/_cli/_base_cli/_abstract_cli.py

@@ -0,0 +1,64 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import sys
+from abc import abstractmethod
+from difflib import SequenceMatcher
+from typing import List, Optional
+
+from taipy.logger._taipy_logger import _TaipyLogger
+
+from ._taipy_parser import _TaipyParser
+
+
+class _AbstractCLI:
+    _logger = _TaipyLogger._get_logger()
+
+    _COMMAND_NAME: Optional[str] = None
+    _ARGUMENTS: List[str] = []
+    SIMILARITY_THRESHOLD = 0.8
+
+    @classmethod
+    @abstractmethod
+    def create_parser(cls):
+        raise NotImplementedError
+
+    @classmethod
+    @abstractmethod
+    def handle_command(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def _parse_arguments(cls):
+        args, unknown_args = _TaipyParser._parser.parse_known_args()
+
+        if getattr(args, "which", None) != cls._COMMAND_NAME:
+            return
+
+        if unknown_args:
+            _TaipyParser._sub_taipyparsers.get(cls._COMMAND_NAME).print_help()
+            for unknown_arg in unknown_args:
+                if not unknown_arg.startswith("-"):
+                    continue
+
+                error_message = f"Unknown arguments: {unknown_arg}."
+                similar_args = [
+                    arg
+                    for arg in cls._ARGUMENTS
+                    if SequenceMatcher(None, unknown_arg, arg).ratio() > cls.SIMILARITY_THRESHOLD
+                ]
+                if similar_args:
+                    error_message += f" Did you mean: {' or '.join(similar_args)}?"
+                cls._logger.error(error_message)
+            cls._logger.error("Please refer to the help message above for more information.")
+            sys.exit(1)
+
+        return args

+ 1 - 7
taipy/_cli/_base_cli/_cli.py → taipy/_cli/_base_cli/_taipy_parser.py

@@ -13,7 +13,7 @@ import argparse
 from typing import Dict
 
 
-class _CLI:
+class _TaipyParser:
     """Argument parser for Taipy application."""
 
     # The conflict_handler is set to "resolve" to override conflict arguments
@@ -51,12 +51,6 @@ class _CLI:
         cls._arg_groups[title] = groupparser
         return groupparser
 
-    @classmethod
-    def _parse(cls):
-        """Parse and return only known arguments."""
-        args, _ = cls._parser.parse_known_args()
-        return args
-
     @classmethod
     def _remove_argument(cls, arg: str):
         """Remove an argument from the parser. Note that the `arg` must be without --.

+ 21 - 16
taipy/_cli/_help_cli.py

@@ -11,31 +11,36 @@
 
 import sys
 
-from taipy._cli._base_cli import _CLI
-from taipy.logger._taipy_logger import _TaipyLogger
+from ._base_cli._abstract_cli import _AbstractCLI
+from ._base_cli._taipy_parser import _TaipyParser
 
 
-class _HelpCLI:
-    __logger = _TaipyLogger._get_logger()
+class _HelpCLI(_AbstractCLI):
+    _COMMAND_NAME = "help"
 
     @classmethod
     def create_parser(cls):
-        create_parser = _CLI._add_subparser("help", help="Show the Taipy help message.", add_help=False)
+        create_parser = _TaipyParser._add_subparser(
+            cls._COMMAND_NAME,
+            help="Show the Taipy help message.",
+            add_help=False,
+        )
         create_parser.add_argument(
             "command", nargs="?", type=str, const="", default="", help="Show the help message of the command."
         )
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
-
-        if getattr(args, "which", None) == "help":
-            if args.command:
-                if args.command in _CLI._sub_taipyparsers.keys():
-                    _CLI._sub_taipyparsers.get(args.command).print_help()
-                else:
-                    cls.__logger.error(f"{args.command} is not a valid command.")
+    def handle_command(cls):
+        args = cls._parse_arguments()
+        if not args:
+            return
+
+        if args.command:
+            if args.command in _TaipyParser._sub_taipyparsers.keys():
+                _TaipyParser._sub_taipyparsers.get(args.command).print_help()
             else:
-                _CLI._parser.print_help()
+                cls._logger.error(f"{args.command} is not a valid command.")
+        else:
+            _TaipyParser._parser.print_help()
 
-            sys.exit(0)
+        sys.exit(0)

+ 31 - 27
taipy/_cli/_run_cli.py

@@ -12,13 +12,16 @@
 import subprocess
 import sys
 
-from taipy._cli._base_cli import _CLI
+from ._base_cli._abstract_cli import _AbstractCLI
+from ._base_cli._taipy_parser import _TaipyParser
 
 
-class _RunCLI:
+class _RunCLI(_AbstractCLI):
+    _COMMAND_NAME = "run"
+
     @classmethod
     def create_parser(cls):
-        run_parser = _CLI._add_subparser("run", help="Run a Taipy application.")
+        run_parser = _TaipyParser._add_subparser(cls._COMMAND_NAME, help="Run a Taipy application.")
         run_parser.add_argument(
             "application_main_file",
         )
@@ -33,27 +36,28 @@ class _RunCLI:
         )
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
-
-        if getattr(args, "which", None) == "run":
-            all_args = sys.argv[3:]  # First 3 args are always (1) Python executable, (2) run, (3) Python file
-
-            external_args = []
-            try:
-                external_args_index = all_args.index("external-args")
-            except ValueError:
-                pass
-            else:
-                external_args.extend(all_args[external_args_index + 1 :])
-                all_args = all_args[:external_args_index]
-
-            taipy_args = [f"--taipy-{arg[2:]}" if arg.startswith("--") else arg for arg in all_args]
-
-            subprocess.run(
-                [sys.executable, args.application_main_file, *(external_args + taipy_args)],
-                stdout=sys.stdout,
-                stderr=sys.stdout,
-            )
-
-            sys.exit(0)
+    def handle_command(cls):
+        args = cls._parse_arguments()
+        if not args:
+            return
+
+        all_args = sys.argv[3:]  # First 3 args are always (1) Python executable, (2) run, (3) Python file
+
+        external_args = []
+        try:
+            external_args_index = all_args.index("external-args")
+        except ValueError:
+            pass
+        else:
+            external_args.extend(all_args[external_args_index + 1 :])
+            all_args = all_args[:external_args_index]
+
+        taipy_args = [f"--taipy-{arg[2:]}" if arg.startswith("--") else arg for arg in all_args]
+
+        subprocess.run(
+            [sys.executable, args.application_main_file, *(external_args + taipy_args)],
+            stdout=sys.stdout,
+            stderr=sys.stdout,
+        )
+
+        sys.exit(0)

+ 17 - 9
taipy/_cli/_scaffold_cli.py

@@ -15,17 +15,24 @@ import sys
 from cookiecutter.main import cookiecutter
 
 import taipy
-from taipy._cli._base_cli import _CLI
 
+from ._base_cli._abstract_cli import _AbstractCLI
+from ._base_cli._taipy_parser import _TaipyParser
 
-class _ScaffoldCLI:
+
+class _ScaffoldCLI(_AbstractCLI):
     __TAIPY_PATH = pathlib.Path(taipy.__file__).parent.resolve() / "templates"
+    _TEMPLATE_MAP = {str(x.name): str(x) for x in __TAIPY_PATH.iterdir() if x.is_dir() and not x.name.startswith("_")}
 
-    _TEMPLATE_MAP = {str(x.name): str(x) for x in __TAIPY_PATH.iterdir() if x.is_dir()}
+    _COMMAND_NAME = "create"
+    _ARGUMENTS = ["--template"]
 
     @classmethod
     def create_parser(cls):
-        create_parser = _CLI._add_subparser("create", help="Create a new Taipy application.")
+        create_parser = _TaipyParser._add_subparser(
+            cls._COMMAND_NAME,
+            help="Create a new Taipy application using pre-defined templates.",
+        )
         create_parser.add_argument(
             "--template",
             choices=list(cls._TEMPLATE_MAP.keys()),
@@ -34,9 +41,10 @@ class _ScaffoldCLI:
         )
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
+    def handle_command(cls):
+        args = cls._parse_arguments()
+        if not args:
+            return
 
-        if getattr(args, "which", None) == "create":
-            cookiecutter(cls._TEMPLATE_MAP[args.template])
-            sys.exit(0)
+        cookiecutter(cls._TEMPLATE_MAP[args.template])
+        sys.exit(0)

+ 15 - 10
taipy/_entrypoint.py

@@ -13,7 +13,7 @@ import os
 import sys
 from importlib.util import find_spec
 
-from taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 from taipy.core._core_cli import _CoreCLI
 from taipy.core._entity._migrate_cli import _MigrateCLI
 from taipy.core._version._cli._version_cli import _VersionCLI
@@ -29,7 +29,12 @@ def _entrypoint():
     # Add the current working directory to path to execute version command on FS repo
     sys.path.append(os.path.normpath(os.getcwd()))
 
-    _CLI._parser.add_argument("-v", "--version", action="store_true", help="Print the current Taipy version and exit.")
+    _TaipyParser._parser.add_argument(
+        "-v",
+        "--version",
+        action="store_true",
+        help="Print the current Taipy version and exit.",
+    )
 
     _RunCLI.create_parser()
     _GuiCLI.create_run_parser()
@@ -45,16 +50,16 @@ def _entrypoint():
 
         _enterprise_entrypoint()
 
-    args = _CLI._parse()
+    args, _ = _TaipyParser._parser.parse_known_args()
     if args.version:
         print(f"Taipy {_get_version()}")  # noqa: T201
         sys.exit(0)
 
-    _RunCLI.parse_arguments()
-    _HelpCLI.parse_arguments()
-    _VersionCLI.parse_arguments()
-    _MigrateCLI.parse_arguments()
-    _ScaffoldCLI.parse_arguments()
+    _RunCLI.handle_command()
+    _HelpCLI.handle_command()
+    _VersionCLI.handle_command()
+    _MigrateCLI.handle_command()
+    _ScaffoldCLI.handle_command()
 
-    _CLI._remove_argument("help")
-    _CLI._parser.print_help()
+    _TaipyParser._remove_argument("help")
+    _TaipyParser._parser.print_help()

+ 1 - 1
taipy/core/_core.py

@@ -103,7 +103,7 @@ class Core:
     def __update_core_section(cls):
         cls.__logger.info("Updating configuration with command-line arguments...")
         _CoreCLI.create_parser()
-        Config._applied_config._unique_sections[CoreSection.name]._update(_CoreCLI.parse_arguments())
+        Config._applied_config._unique_sections[CoreSection.name]._update(_CoreCLI.handle_command())
 
     @classmethod
     def __manage_version(cls):

+ 8 - 6
taipy/core/_core_cli.py

@@ -11,12 +11,13 @@
 
 from typing import Dict
 
-from taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._abstract_cli import _AbstractCLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 
 from .config import CoreSection
 
 
-class _CoreCLI:
+class _CoreCLI(_AbstractCLI):
     """Command-line interface for Taipy Core application."""
 
     __MODE_ARGS: Dict[str, Dict] = {
@@ -69,7 +70,7 @@ class _CoreCLI:
 
     @classmethod
     def create_parser(cls):
-        core_parser = _CLI._add_groupparser("Taipy Core", "Optional arguments for Taipy Core service")
+        core_parser = _TaipyParser._add_groupparser("Taipy Core", "Optional arguments for Taipy Core service")
 
         mode_group = core_parser.add_mutually_exclusive_group()
         for mode_arg, mode_arg_dict in cls.__MODE_ARGS.items():
@@ -81,7 +82,7 @@ class _CoreCLI:
 
     @classmethod
     def create_run_parser(cls):
-        run_parser = _CLI._add_subparser("run", help="Run a Taipy application.")
+        run_parser = _TaipyParser._add_subparser("run", help="Run a Taipy application.")
         mode_group = run_parser.add_mutually_exclusive_group()
         for mode_arg, mode_arg_dict in cls.__MODE_ARGS.items():
             mode_group.add_argument(mode_arg, **mode_arg_dict)
@@ -91,8 +92,9 @@ class _CoreCLI:
             force_group.add_argument(force_arg, **force_arg_dict)
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
+    def handle_command(cls):
+        args, _ = _TaipyParser._parser.parse_known_args()
+
         as_dict = {}
         if args.taipy_development:
             as_dict[CoreSection._MODE_KEY] = CoreSection._DEVELOPMENT_MODE

+ 19 - 15
taipy/core/_entity/_migrate_cli.py

@@ -12,9 +12,9 @@
 import sys
 from typing import List
 
-from taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._abstract_cli import _AbstractCLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 from taipy.config.config import Config
-from taipy.logger._taipy_logger import _TaipyLogger
 
 from ._migrate import (
     _migrate_fs_entities,
@@ -29,19 +29,19 @@ from ._migrate import (
 )
 
 
-class _MigrateCLI:
-    __logger = _TaipyLogger._get_logger()
+class _MigrateCLI(_AbstractCLI):
+    _COMMAND_NAME = "migrate"
+    _ARGUMENTS = ["--repository-type", "--skip-backup", "--restore", "--remove-backup"]
 
     @classmethod
     def create_parser(cls):
-        migrate_parser = _CLI._add_subparser(
-            "migrate",
+        migrate_parser = _TaipyParser._add_subparser(
+            cls._COMMAND_NAME,
             help="Migrate entities created from old taipy versions to be compatible with the current taipy version. "
             " The entity migration should be performed only after updating taipy code to the current version.",
         )
         migrate_parser.add_argument(
             "--repository-type",
-            required=True,
             nargs="+",
             help="The type of repository to migrate. If filesystem or sql, a path to the database folder/.sqlite file "
             "should be informed. In case of mongo host, port, user and password must be informed, if left empty it "
@@ -64,12 +64,16 @@ class _MigrateCLI:
         )
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
-
-        if getattr(args, "which", None) != "migrate":
+    def handle_command(cls):
+        args = cls._parse_arguments()
+        if not args:
             return
 
+        if not args.repository_type:
+            _TaipyParser._sub_taipyparsers.get(cls._COMMAND_NAME).print_help()
+            cls._logger.error("The following arguments are required: --repository-type")
+            sys.exit(1)
+
         repository_type = args.repository_type[0]
         repository_args = args.repository_type[1:] if len(args.repository_type) > 1 else [None]
 
@@ -78,7 +82,7 @@ class _MigrateCLI:
         if args.remove_backup:
             cls.__handle_remove_backup(repository_type, repository_args)
 
-        do_backup =  not args.skip_backup
+        do_backup = not args.skip_backup
         cls.__migrate_entities(repository_type, repository_args, do_backup)
         sys.exit(0)
 
@@ -95,7 +99,7 @@ class _MigrateCLI:
             if not _remove_backup_mongo_entities():
                 sys.exit(1)
         else:
-            cls.__logger.error(f"Unknown repository type {repository_type}")
+            cls._logger.error(f"Unknown repository type {repository_type}")
             sys.exit(1)
 
         sys.exit(0)
@@ -114,7 +118,7 @@ class _MigrateCLI:
             if not _restore_migrate_mongo_entities(*mongo_args):
                 sys.exit(1)
         else:
-            cls.__logger.error(f"Unknown repository type {repository_type}")
+            cls._logger.error(f"Unknown repository type {repository_type}")
             sys.exit(1)
         sys.exit(0)
 
@@ -134,5 +138,5 @@ class _MigrateCLI:
             _migrate_mongo_entities(*mongo_args, backup=do_backup)  # type: ignore
 
         else:
-            cls.__logger.error(f"Unknown repository type {repository_type}")
+            cls._logger.error(f"Unknown repository type {repository_type}")
             sys.exit(1)

+ 17 - 17
taipy/core/_version/_cli/_version_cli.py

@@ -11,10 +11,10 @@
 
 import sys
 
-from taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._abstract_cli import _AbstractCLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 from taipy.config import Config
 from taipy.config.exceptions.exceptions import InconsistentEnvVariableError
-from taipy.logger._taipy_logger import _TaipyLogger
 
 from ...data._data_manager_factory import _DataManagerFactory
 from ...exceptions.exceptions import VersionIsNotProductionVersion
@@ -27,14 +27,15 @@ from .._version_manager_factory import _VersionManagerFactory
 from ._bcolor import _Bcolors
 
 
-class _VersionCLI:
+class _VersionCLI(_AbstractCLI):
     """Command-line interface of the versioning system."""
 
-    __logger = _TaipyLogger._get_logger()
+    _COMMAND_NAME = "manage-versions"
+    _ARGUMENTS = ["-l", "--list", "--rename", "--compare-config", "-d", "--delete", "-dp", "--delete-production"]
 
     @classmethod
     def create_parser(cls):
-        version_parser = _CLI._add_subparser("manage-versions", help="Taipy version control system.")
+        version_parser = _TaipyParser._add_subparser(cls._COMMAND_NAME, help="Taipy version control system.")
 
         version_parser.add_argument(
             "-l", "--list", action="store_true", help="List all existing versions of the Taipy application."
@@ -64,10 +65,9 @@ class _VersionCLI:
         )
 
     @classmethod
-    def parse_arguments(cls):
-        args = _CLI._parse()
-
-        if getattr(args, "which", None) != "manage-versions":
+    def handle_command(cls):
+        args = cls._parse_arguments()
+        if not args:
             return
 
         if args.list:
@@ -78,13 +78,13 @@ class _VersionCLI:
             try:
                 cls.__rename_version(args.rename[0], args.rename[1])
             except InconsistentEnvVariableError as error:
-                cls.__logger.error(
+                cls._logger.error(
                     f"Fail to rename version {args.rename[0]} to {args.rename[1]} due to outdated Configuration."
                     f"Detail: {str(error)}"
                 )
                 sys.exit(1)
 
-            cls.__logger.info(f"Successfully renamed version '{args.rename[0]}' to '{args.rename[1]}'.")
+            cls._logger.info(f"Successfully renamed version '{args.rename[0]}' to '{args.rename[1]}'.")
             sys.exit(0)
 
         if args.compare_config:
@@ -94,7 +94,7 @@ class _VersionCLI:
         if args.delete_production:
             try:
                 _VersionManagerFactory._build_manager()._delete_production_version(args.delete_production)
-                cls.__logger.info(
+                cls._logger.info(
                     f"Successfully delete version {args.delete_production} from the production version list."
                 )
                 sys.exit(0)
@@ -103,7 +103,7 @@ class _VersionCLI:
 
         if args.delete:
             if clean_all_entities(args.delete):
-                cls.__logger.info(f"Successfully delete version {args.delete}.")
+                cls._logger.info(f"Successfully delete version {args.delete}.")
             else:
                 sys.exit(1)
 
@@ -154,13 +154,13 @@ class _VersionCLI:
 
         # Check if the new version already exists, return an error
         if _version_manager._get(new_version):
-            cls.__logger.error(f"Version name '{new_version}' is already used.")
+            cls._logger.error(f"Version name '{new_version}' is already used.")
             sys.exit(1)
 
         # Make sure that all entities of the old version are exists and loadable
         version_entity = _version_manager._get(old_version)
         if version_entity is None:
-            cls.__logger.error(f"Version '{old_version}' does not exist.")
+            cls._logger.error(f"Version '{old_version}' does not exist.")
             sys.exit(1)
 
         jobs = _JobManagerFactory._build_manager()._get_all(version_number=old_version)
@@ -208,12 +208,12 @@ class _VersionCLI:
     def __compare_version_config(cls, version_1: str, version_2: str):
         version_entity_1 = _VersionManagerFactory._build_manager()._get(version_1)
         if version_entity_1 is None:
-            cls.__logger.error(f"Version '{version_1}' does not exist.")
+            cls._logger.error(f"Version '{version_1}' does not exist.")
             sys.exit(1)
 
         version_entity_2 = _VersionManagerFactory._build_manager()._get(version_2)
         if version_entity_2 is None:
-            cls.__logger.error(f"Version '{version_2}' does not exist.")
+            cls._logger.error(f"Version '{version_2}' does not exist.")
             sys.exit(1)
 
         Config._comparator._compare(  # type: ignore[attr-defined]

+ 8 - 6
taipy/gui/_gui_cli.py

@@ -11,10 +11,11 @@
 
 from typing import Dict, Tuple
 
-from taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._abstract_cli import _AbstractCLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 
 
-class _GuiCLI:
+class _GuiCLI(_AbstractCLI):
     """Command-line interface of GUI."""
 
     __GUI_ARGS: Dict[Tuple, Dict] = {
@@ -72,7 +73,7 @@ class _GuiCLI:
 
     @classmethod
     def create_parser(cls):
-        gui_parser = _CLI._add_groupparser("Taipy GUI", "Optional arguments for Taipy GUI service")
+        gui_parser = _TaipyParser._add_groupparser("Taipy GUI", "Optional arguments for Taipy GUI service")
 
         for args, arg_dict in cls.__GUI_ARGS.items():
             taipy_arg = (args[0], cls.__add_taipy_prefix(args[0]), *args[1:])
@@ -88,7 +89,7 @@ class _GuiCLI:
 
     @classmethod
     def create_run_parser(cls):
-        run_parser = _CLI._add_subparser("run", help="Run a Taipy application.")
+        run_parser = _TaipyParser._add_subparser("run", help="Run a Taipy application.")
         for args, arg_dict in cls.__GUI_ARGS.items():
             run_parser.add_argument(*args, **arg_dict)
 
@@ -101,8 +102,9 @@ class _GuiCLI:
             reloader_group.add_argument(reloader_arg, **reloader_arg_dict)
 
     @classmethod
-    def parse_arguments(cls):
-        return _CLI._parse()
+    def handle_command(cls):
+        args, _ = _TaipyParser._parser.parse_known_args()
+        return args
 
     @classmethod
     def __add_taipy_prefix(cls, key: str):

+ 1 - 1
taipy/gui/config.py

@@ -185,7 +185,7 @@ class _Config(object):
 
     def _handle_argparse(self):
         _GuiCLI.create_parser()
-        args = _GuiCLI.parse_arguments()
+        args = _GuiCLI.handle_command()
 
         config = self.config
         if args.taipy_port:

+ 21 - 0
tests/cli/conftest.py

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

+ 12 - 41
tests/config/common/test_argparser.py → tests/cli/test_argparser.py

@@ -9,13 +9,10 @@
 # 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 taipy._cli._base_cli import _CLI
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 
 if sys.version_info >= (3, 10):
     argparse_options_str = "options:"
@@ -28,38 +25,12 @@ def preprocess_stdout(stdout):
     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
-
-    _CLI._subparser_action = None
-    _CLI._sub_taipyparsers = {}
-
-
 def test_subparser(capfd):
-    subcommand_1 = _CLI._add_subparser("subcommand_1", help="subcommand_1 help")
+    subcommand_1 = _TaipyParser._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 = _TaipyParser._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")
 
@@ -89,23 +60,23 @@ def test_subparser(capfd):
 
 
 def test_duplicate_subcommand():
-    subcommand_1 = _CLI._add_subparser("subcommand_1", help="subcommand_1 help")
+    subcommand_1 = _TaipyParser._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 = _TaipyParser._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
+    assert len(_TaipyParser._sub_taipyparsers) == 1
 
 
 def test_groupparser(capfd):
-    group_1 = _CLI._add_groupparser("group_1", "group_1 desc")
+    group_1 = _TaipyParser._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 = _TaipyParser._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")
 
@@ -123,19 +94,19 @@ group_2:
   --baz BAZ, -z BAZ  baz help
     """.strip()
 
-    _CLI._parser.print_help()
+    _TaipyParser._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 = _TaipyParser._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 = _TaipyParser._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
+    assert len(_TaipyParser._arg_groups) == 1

+ 1 - 30
tests/cli/test_cli.py → tests/cli/test_help_cli.py

@@ -9,13 +9,11 @@
 # 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
 from unittest.mock import patch
 
 import pytest
 
-from taipy._cli._base_cli import _CLI
 from taipy._entrypoint import _entrypoint
 
 
@@ -24,40 +22,13 @@ def preprocess_stdout(stdout):
     return re.sub(" +", " ", stdout)
 
 
-def remove_subparser(name: str):
-    """Remove a subparser from the _CLI class."""
-    _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._subparser_action = None
-    _CLI._arg_groups = {}
-    subcommands = list(_CLI._sub_taipyparsers.keys())
-    for subcommand in subcommands:
-        remove_subparser(subcommand)
-
-    yield
-
-    _CLI._subparser_action = None
-    _CLI._sub_taipyparsers = {}
-
-
 expected_help = """{run,manage-versions,create,migrate,help} ...
 
 positional arguments:
   {run,manage-versions,create,migrate,help}
     run                 Run a Taipy application.
     manage-versions     Taipy version control system.
-    create              Create a new Taipy application.
+    create              Create a new Taipy application using pre-defined templates.
     migrate             Migrate entities created from old taipy versions to be compatible with the current taipy
     version. The entity migration should be performed only after updating taipy code to the current version.
     help                Show the Taipy help message.

+ 28 - 0
tests/conftest.py

@@ -9,8 +9,11 @@
 # 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 pytest
 
+from taipy._cli._base_cli._taipy_parser import _TaipyParser
 from taipy.config import _inject_section
 from taipy.config._config import _Config
 from taipy.config._config_comparator._config_comparator import _ConfigComparator
@@ -36,6 +39,31 @@ def e2e_port(request):
     return request.config.getoption("--e2e-port")
 
 
+def remove_subparser(name: str):
+    """Remove a subparser from argparse."""
+    _TaipyParser._sub_taipyparsers.pop(name, None)
+
+    if _TaipyParser._subparser_action:
+        _TaipyParser._subparser_action._name_parser_map.pop(name, None)
+
+        for action in _TaipyParser._subparser_action._choices_actions:
+            if action.dest == name:
+                _TaipyParser._subparser_action._choices_actions.remove(action)
+
+
+@pytest.fixture
+def clean_argparser():
+    def _clean_argparser():
+        _TaipyParser._parser = argparse.ArgumentParser(conflict_handler="resolve")
+        _TaipyParser._subparser_action = None
+        _TaipyParser._arg_groups = {}
+        subcommands = list(_TaipyParser._sub_taipyparsers.keys())
+        for subcommand in subcommands:
+            remove_subparser(subcommand)
+
+    return _clean_argparser
+
+
 @pytest.fixture
 def reset_configuration_singleton():
     def _reset_configuration_singleton():

+ 44 - 29
tests/core/_entity/test_migrate_cli.py

@@ -19,9 +19,24 @@ from unittest.mock import patch
 import mongomock
 import pytest
 
+from taipy._entrypoint import _entrypoint
 from taipy.core._entity._migrate_cli import _MigrateCLI
 
 
+def test_migrate_cli_with_wrong_repository_type_arguments(caplog):
+    with patch("sys.argv", ["prog", "migrate", "--reposiory-tyep", "filesystem"]):
+        with pytest.raises(SystemExit):
+            _entrypoint()
+        assert "Unknown arguments: --reposiory-tyep. Did you mean: --repository-type?" in caplog.text
+
+
+def test_migrate_cli_with_wrong_skip_backup_arguments(caplog):
+    with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", "--slip-backup"]):
+        with pytest.raises(SystemExit):
+            _entrypoint()
+        assert "Unknown arguments: --slip-backup. Did you mean: --skip-backup?" in caplog.text
+
+
 @pytest.fixture(scope="function", autouse=True)
 def clean_data_folder():
     if os.path.exists("tests/core/_entity/.data"):
@@ -37,7 +52,7 @@ def test_migrate_fs_default(caplog):
     # Test migrate with default .data folder
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", "--skip-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert "Starting entity migration from '.taipy/' folder" in caplog.text
 
 
@@ -53,7 +68,7 @@ def test_migrate_fs_specified_folder(caplog, mocker):
     # Run with --skip-backup to only test the migration
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path, "--skip-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Starting entity migration from '{data_path}' folder" in caplog.text
 
     # Compare migrated .data folder with data_sample_migrated
@@ -77,7 +92,7 @@ def test_migrate_fs_backup_and_remove(caplog, mocker):
     # Remove backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path, "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup folder '{backup_path}' does not exist." in caplog.text
     assert not os.path.exists(backup_path)
@@ -85,7 +100,7 @@ def test_migrate_fs_backup_and_remove(caplog, mocker):
     # Run without --skip-backup to create the backup folder
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Backed up entities from '{data_path}' to '{backup_path}' folder before migration." in caplog.text
 
     assert os.path.exists(backup_path)
@@ -93,7 +108,7 @@ def test_migrate_fs_backup_and_remove(caplog, mocker):
     # Remove backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path, "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Removed backup entities from the backup folder '{backup_path}'." in caplog.text
     assert not os.path.exists(backup_path)
 
@@ -111,7 +126,7 @@ def test_migrate_fs_backup_and_restore(caplog, mocker):
     # Restore backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path, "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup folder '{backup_path}' does not exist." in caplog.text
     assert not os.path.exists(backup_path)
@@ -119,14 +134,14 @@ def test_migrate_fs_backup_and_restore(caplog, mocker):
     # Run without --skip-backup to create the backup folder
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert os.path.exists(backup_path)
 
     # restore the backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", data_path, "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Restored entities from the backup folder '{backup_path}' to '{data_path}'." in caplog.text
     assert not os.path.exists(backup_path)
 
@@ -143,7 +158,7 @@ def test_migrate_fs_non_existing_folder(caplog):
     # Test migrate with a non-existing folder
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "filesystem", "non-existing-folder"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert "Folder 'non-existing-folder' does not exist." in caplog.text
 
@@ -155,7 +170,7 @@ def test_migrate_sql_specified_path(_migrate_sql_entities_mock, tmp_sqlite):
     # Test the _migrate_sql_entities is called once with the correct path
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite, "--skip-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert _migrate_sql_entities_mock.assert_called_once_with(path=tmp_sqlite)
 
 
@@ -172,7 +187,7 @@ def test_migrate_sql_backup_and_remove(caplog, tmp_sqlite):
     # Remove backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite, "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup database '{backup_sqlite}' does not exist." in caplog.text
     assert not os.path.exists(backup_sqlite)
@@ -180,14 +195,14 @@ def test_migrate_sql_backup_and_remove(caplog, tmp_sqlite):
     # Run without --skip-backup to create the backup database
     with pytest.raises((SystemExit, OperationalError)):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert os.path.exists(backup_sqlite)
 
     # Remove backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite, "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Removed backup entities from the backup database '{backup_sqlite}'." in caplog.text
     assert not os.path.exists(backup_sqlite)
 
@@ -206,7 +221,7 @@ def test_migrate_sql_backup_and_restore(caplog, tmp_sqlite):
     # Restore backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite, "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup database '{backup_sqlite}' does not exist." in caplog.text
     assert not os.path.exists(backup_sqlite)
@@ -214,14 +229,14 @@ def test_migrate_sql_backup_and_restore(caplog, tmp_sqlite):
     # Run without --skip-backup to create the backup database
     with pytest.raises((SystemExit, OperationalError)):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert os.path.exists(backup_sqlite)
 
     # Restore the backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", tmp_sqlite, "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Restored entities from the backup database '{backup_sqlite}' to '{tmp_sqlite}'." in caplog.text
     assert not os.path.exists(backup_sqlite)
 
@@ -232,7 +247,7 @@ def test_migrate_sql_non_existing_path(caplog):
     # Test migrate without providing a path
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert err.value.code == 1
     assert "Missing the required sqlite path." in caplog.text
@@ -242,7 +257,7 @@ def test_migrate_sql_non_existing_path(caplog):
     # Test migrate with a non-existing-path.sqlite file
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "sql", "non-existing-path.sqlite"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert "File 'non-existing-path.sqlite' does not exist." in caplog.text
 
@@ -253,12 +268,12 @@ def test_call_to_migrate_mongo(_migrate_mongo_entities_mock):
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert _migrate_mongo_entities_mock.assert_called_once_with()
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo", "host", "port", "user", "password"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert _migrate_mongo_entities_mock.assert_called_once_with("host", "port", "user", "password")
 
 
@@ -271,7 +286,7 @@ def test_migrate_mongo_backup_and_remove(caplog):
     # Remove backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo", "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup folder '{mongo_backup_path}' does not exist." in caplog.text
     assert not os.path.exists(mongo_backup_path)
@@ -279,14 +294,14 @@ def test_migrate_mongo_backup_and_remove(caplog):
     # Run without --skip-backup to create the backup database
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert os.path.exists(mongo_backup_path)
 
     # Remove backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo", "--remove-backup"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Removed backup entities from the backup folder '{mongo_backup_path}'." in caplog.text
     assert not os.path.exists(mongo_backup_path)
 
@@ -300,7 +315,7 @@ def test_migrate_mongo_backup_and_restore(caplog):
     # Restore backup when it does not exist should raise an error
     with pytest.raises(SystemExit) as err:
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo", "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert err.value.code == 1
     assert f"The backup folder '{mongo_backup_path}' does not exist." in caplog.text
     assert not os.path.exists(mongo_backup_path)
@@ -308,14 +323,14 @@ def test_migrate_mongo_backup_and_restore(caplog):
     # Run without --skip-backup to create the backup database
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
 
     assert os.path.exists(mongo_backup_path)
 
     # Restore the backup
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "mongo", "--restore"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
     assert f"Restored entities from the backup folder '{mongo_backup_path}'." in caplog.text
     assert not os.path.exists(mongo_backup_path)
 
@@ -325,15 +340,15 @@ def test_not_provide_valid_repository_type(caplog):
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert "the following arguments are required: --repository-type" in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert "argument --repository-type: expected at least one argument" in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "migrate", "--repository-type", "invalid-repository-type"]):
-            _MigrateCLI.parse_arguments()
+            _MigrateCLI.handle_command()
             assert "Unknown repository type invalid-repository-type" in caplog.text

+ 3 - 1
tests/core/conftest.py

@@ -315,7 +315,8 @@ def tmp_sqlite(tmpdir_factory):
 
 
 @pytest.fixture(scope="function", autouse=True)
-def clean_repository(init_config, init_managers, init_orchestrator, init_notifier):
+def clean_repository(init_config, init_managers, init_orchestrator, init_notifier, clean_argparser):
+    clean_argparser()
     close_all_sessions()
     init_config()
     init_orchestrator()
@@ -326,6 +327,7 @@ def clean_repository(init_config, init_managers, init_orchestrator, init_notifie
     with patch("sys.argv", ["prog"]):
         yield
 
+    clean_argparser()
     close_all_sessions()
     init_orchestrator()
     init_managers()

+ 21 - 13
tests/core/version/test_version_cli.py

@@ -14,6 +14,7 @@ from unittest.mock import patch
 
 import pytest
 
+from taipy._entrypoint import _entrypoint
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.scope import Scope
 from taipy.config.config import Config
@@ -27,6 +28,13 @@ from taipy.core.sequence._sequence_manager import _SequenceManager
 from taipy.core.task._task_manager import _TaskManager
 
 
+def test_version_cli_with_wrong_arguments(caplog):
+    with patch("sys.argv", ["prog", "manage-versions", "--lits"]):
+        with pytest.raises(SystemExit):
+            _entrypoint()
+        assert "Unknown arguments: --lits. Did you mean: --list?" in caplog.text
+
+
 def test_delete_version(caplog):
     scenario_config = config_scenario()
 
@@ -87,7 +95,7 @@ def test_delete_version(caplog):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete", "1.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert "Successfully delete version 1.0." in caplog.text
     all_versions = [version.id for version in _VersionManager._get_all()]
@@ -97,13 +105,13 @@ def test_delete_version(caplog):
     # Test delete a non-existed version
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete", "non_exist_version"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     # Test delete production version will change the version from production to experiment
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete-production", "1.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert "Successfully delete version 1.1 from the production version list." in caplog.text
     all_versions = [version.id for version in _VersionManager._get_all()]
@@ -114,7 +122,7 @@ def test_delete_version(caplog):
     # Test delete a non-existed production version
     with pytest.raises(SystemExit) as e:
         with patch("sys.argv", ["prog", "manage-versions", "--delete-production", "non_exist_version"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert str(e.value) == "Version 'non_exist_version' is not a production version."
 
@@ -158,7 +166,7 @@ def test_list_versions(capsys):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--list"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     out, _ = capsys.readouterr()
     version_list = str(out).strip().split("\n")
@@ -194,20 +202,20 @@ def test_rename_version(caplog):
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "non_exist_version", "1.1"]):
             # This should raise an exception since version "non_exist_version" does not exist
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "1.0", "2.0"]):
             # This should raise an exception since 2.0 already exists
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version name '2.0' is already used." in caplog.text
 
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "1.0", "1.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert _VersionManager._get("1.0") is None
     assert [version.id for version in _VersionManager._get_all()].sort() == [dev_ver, "1.1", "2.0"].sort()
     # All entities are assigned to the new version
@@ -220,7 +228,7 @@ def test_rename_version(caplog):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "2.0", "2.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert _VersionManager._get("2.0") is None
     assert [version.id for version in _VersionManager._get_all()].sort() == [dev_ver, "1.1", "2.1"].sort()
     assert _VersionManager._get_production_versions() == ["2.1"]
@@ -258,23 +266,23 @@ def test_compare_version_config(caplog, init_config):
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "non_exist_version", "2.0"]):
             # This should raise an exception since version "non_exist_version" does not exist
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "non_exist_version"]):
             # This should raise an exception since 2.0 already exists
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "1.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "There is no difference between version 1.0 Configuration and version 1.0 Configuration." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "2.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     expected_message = """Differences between version 1.0 Configuration and version 2.0 Configuration:
 \tDATA_NODE "d2" has attribute "default_path" modified: foo.csv -> bar.csv"""
     assert expected_message in caplog.text

+ 13 - 13
tests/core/version/test_version_cli_with_sql_repo.py

@@ -90,7 +90,7 @@ def test_delete_version(caplog, init_sql_repo):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete", "1.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert "Successfully delete version 1.0." in caplog.text
     all_versions = [version.id for version in _VersionManager._get_all()]
@@ -100,13 +100,13 @@ def test_delete_version(caplog, init_sql_repo):
     # Test delete a non-existed version
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete", "non_exist_version"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     # Test delete production version will change the version from production to experiment
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--delete-production", "1.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert "Successfully delete version 1.1 from the production version list." in caplog.text
     all_versions = [version.id for version in _VersionManager._get_all()]
@@ -117,7 +117,7 @@ def test_delete_version(caplog, init_sql_repo):
     # Test delete a non-existed production version
     with pytest.raises(SystemExit) as e:
         with patch("sys.argv", ["prog", "manage-versions", "--delete-production", "non_exist_version"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     assert str(e.value) == "Version 'non_exist_version' is not a production version."
 
@@ -163,7 +163,7 @@ def test_list_versions(capsys, init_sql_repo):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--list"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
 
     out, _ = capsys.readouterr()
     version_list = str(out).strip().split("\n")
@@ -201,20 +201,20 @@ def test_rename_version(caplog, init_sql_repo):
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "non_exist_version", "1.1"]):
             # This should raise an exception since version "non_exist_version" does not exist
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "1.0", "2.0"]):
             # This should raise an exception since 2.0 already exists
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version name '2.0' is already used." in caplog.text
 
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "1.0", "1.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert _VersionManager._get("1.0") is None
     assert [version.id for version in _VersionManager._get_all()].sort() == [dev_ver, "1.1", "2.0"].sort()
     # All entities are assigned to the new version
@@ -227,7 +227,7 @@ def test_rename_version(caplog, init_sql_repo):
     _VersionCLI.create_parser()
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--rename", "2.0", "2.1"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert _VersionManager._get("2.0") is None
     assert [version.id for version in _VersionManager._get_all()].sort() == [dev_ver, "1.1", "2.1"].sort()
     assert _VersionManager._get_production_versions() == ["2.1"]
@@ -269,23 +269,23 @@ def test_compare_version_config(caplog, init_sql_repo, init_config):
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "non_exist_version", "2.0"]):
             # This should raise an exception since version "non_exist_version" does not exist
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "non_exist_version"]):
             # This should raise an exception since 2.0 already exists
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "Version 'non_exist_version' does not exist." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "1.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     assert "There is no difference between version 1.0 Configuration and version 1.0 Configuration." in caplog.text
 
     with pytest.raises(SystemExit):
         with patch("sys.argv", ["prog", "manage-versions", "--compare-config", "1.0", "2.0"]):
-            _VersionCLI.parse_arguments()
+            _VersionCLI.handle_command()
     expected_message = """Differences between version 1.0 Configuration and version 2.0 Configuration:
 \tDATA_NODE "d2" has attribute "default_path" modified: foo.csv -> bar.csv"""
     assert expected_message in caplog.text

+ 23 - 0
tests/templates/test_template_cli.py

@@ -0,0 +1,23 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from unittest.mock import patch
+
+import pytest
+
+from taipy._entrypoint import _entrypoint
+
+
+def test_create_cli_with_wrong_arguments(caplog):
+    with patch("sys.argv", ["prog", "create", "--teamplaet", "default"]):
+        with pytest.raises(SystemExit):
+            _entrypoint()
+        assert "Unknown arguments: --teamplaet. Did you mean: --template?" in caplog.text