diff --git a/mock/local_test.py b/mock/local_test.py index 0cfd245..a9c4c13 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,5 +1,3 @@ -import argparse +arg = '-repeat' -parser = argparse.ArgumentParser(prog='myprogram') -_ = parser.add_argument('--foo', help='foo of the %(prog)s program') -print(vars(parser.parse_args())) \ No newline at end of file +print(arg[:arg.rfind('-')+1]) \ No newline at end of file diff --git a/mock/mock_app/main.py b/mock/mock_app/main.py index af42553..84664dd 100644 --- a/mock/mock_app/main.py +++ b/mock/mock_app/main.py @@ -3,19 +3,16 @@ from mock.mock_app.routers import work_router from argenta import App, Orchestrator from argenta.app import PredefinedMessages, DynamicDividingLine, AutoCompleter from argenta.orchestrator import ArgParser -from argenta.orchestrator.argparser import BooleanArgument, ValueArgument, RequiredArgument +from argenta.orchestrator.argparser import BooleanArgument, ValueArgument arg_parser: ArgParser = ArgParser(processed_args=[BooleanArgument(name="repeat", is_deprecated=True), - ValueArgument(name="value", possible_values=["cat", "dog"]), - RequiredArgument(name="required", is_required=True)]) + ValueArgument(name="required", is_required=True)]) app: App = App( dividing_line=DynamicDividingLine(), autocompleter=AutoCompleter(), ) -orchestrator: Orchestrator = Orchestrator(arg_parser) - -print(orchestrator.get_input_args()) +orchestrator: Orchestrator = Orchestrator() def main(): app.include_router(work_router) @@ -23,7 +20,8 @@ def main(): app.add_message_on_startup(PredefinedMessages.USAGE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.HELP) - # orchestrator.start_polling(app) + + orchestrator.start_polling(app) if __name__ == "__main__": diff --git a/mock/mock_app/routers.py b/mock/mock_app/routers.py index 030fe51..5d36e1f 100644 --- a/mock/mock_app/routers.py +++ b/mock/mock_app/routers.py @@ -12,8 +12,9 @@ flag = Flag('csdv', possible_values=PossibleValues.NEITHER) Command("get", description="Get Help", aliases=["help", "Get_help"], - flags=Flags([PredefinedFlags.PORT, - PredefinedFlags.HOST]))) + flags=Flags([PredefinedFlags.PORT, PredefinedFlags.HOST]) + ) +) def command_help(response: Response): print(response.status) print(response.input_flags.flags) diff --git a/src/argenta/__init__.py b/src/argenta/__init__.py index 6498ca5..94239fd 100644 --- a/src/argenta/__init__.py +++ b/src/argenta/__init__.py @@ -1,10 +1,3 @@ -__all__ = [ - 'App', - 'Orchestrator', - 'Router', -] - - -from argenta.app import App -from argenta.orchestrator import Orchestrator -from argenta.router import Router +from argenta.app.models import App as App +from argenta.orchestrator.entity import Orchestrator as Orchestrator +from argenta.router.entity import Router as Router diff --git a/src/argenta/orchestrator/__init__.py b/src/argenta/orchestrator/__init__.py index b658f94..62e8e21 100644 --- a/src/argenta/orchestrator/__init__.py +++ b/src/argenta/orchestrator/__init__.py @@ -1,8 +1,5 @@ __all__ = [ - "Orchestrator", - "ArgParser" + "ArgParser", ] - -from argenta.orchestrator.entity import Orchestrator from argenta.orchestrator.argparser.entity import ArgParser diff --git a/src/argenta/orchestrator/argparser/__init__.py b/src/argenta/orchestrator/argparser/__init__.py index 4a69e78..4f59581 100644 --- a/src/argenta/orchestrator/argparser/__init__.py +++ b/src/argenta/orchestrator/argparser/__init__.py @@ -1,12 +1,9 @@ __all__ = [ "ArgParser", - "RequiredArgument", - "ValueArgument", - "BooleanArgument" + "BooleanArgument", + "ValueArgument" ] from argenta.orchestrator.argparser.entity import ArgParser -from argenta.orchestrator.argparser.arguments import (BooleanArgument, - RequiredArgument, - ValueArgument) +from argenta.orchestrator.argparser.arguments import BooleanArgument, ValueArgument diff --git a/src/argenta/orchestrator/argparser/arguments/__init__.py b/src/argenta/orchestrator/argparser/arguments/__init__.py index 011299b..0308877 100644 --- a/src/argenta/orchestrator/argparser/arguments/__init__.py +++ b/src/argenta/orchestrator/argparser/arguments/__init__.py @@ -1,8 +1,8 @@ -__all__ = ["BooleanArgument", "RequiredArgument", "ValueArgument"] +__all__ = ["BooleanArgument", "ValueArgument", "InputArgument"] from argenta.orchestrator.argparser.arguments.models import ( BooleanArgument, - RequiredArgument, ValueArgument, + InputArgument ) diff --git a/src/argenta/orchestrator/argparser/arguments/models.py b/src/argenta/orchestrator/argparser/arguments/models.py index 8cc2bca..32d6f02 100644 --- a/src/argenta/orchestrator/argparser/arguments/models.py +++ b/src/argenta/orchestrator/argparser/arguments/models.py @@ -1,11 +1,14 @@ +from typing import Literal + + class BaseArgument: """ Private. Base class for all arguments """ def __init__(self, name: str, *, help: str, - is_required: bool, - is_deprecated: bool): + is_deprecated: bool, + prefix: Literal["-", "--", "---"]): """ Public. Boolean argument, does not require a value :param name: name of the argument @@ -15,42 +18,26 @@ class BaseArgument: """ self.name: str = name self.help: str = help - self.is_required: bool = is_required self.is_deprecated: bool = is_deprecated - - -class RequiredArgument(BaseArgument): - def __init__(self, name: str, *, - help: str = "Help for required argument", - default: str | None = None, - possible_values: list[str] | None = None, - is_required: bool = True, - is_deprecated: bool = False): - """ - Public. Required argument at startup - :param name: name of the argument, must not start with minus (-) - :param help: help message for the argument - :param default: default value for the argument - :param possible_values: list of possible values for the argument - :param is_required: whether the argument is required - :param is_deprecated: whether the argument is deprecated - """ - self.default: str | None = default - self.possible_values: list[str] | None = possible_values - self.action: str = "store" - super().__init__(name, help=help, is_required=is_required, is_deprecated=is_deprecated) + self.prefix: Literal["-", "--", "---"] = prefix + + @property + def string_entity(self) -> str: + return self.prefix + self.name class ValueArgument(BaseArgument): def __init__(self, name: str, *, - help: str = "Help message for the value argument", - possible_values: list[str] | None = None, - default: str | None = None, - is_required: bool = False, - is_deprecated: bool = False): + prefix: Literal["-", "--", "---"] = "--", + help: str = "Help message for the value argument", + possible_values: list[str] | None = None, + default: str | None = None, + is_required: bool = False, + is_deprecated: bool = False): """ Public. Value argument, must have the value :param name: name of the argument + :param prefix: prefix for the argument :param help: help message for the argument :param possible_values: list of possible values for the argument :param default: default value for the argument @@ -59,15 +46,16 @@ class ValueArgument(BaseArgument): """ self.default: str | None = default self.possible_values: list[str] | None = possible_values + self.is_required: bool = is_required self.action: str = "store" - super().__init__(name, help=help, is_required=is_required, is_deprecated=is_deprecated) + super().__init__(name, prefix=prefix, help=help, is_deprecated=is_deprecated) class BooleanArgument(BaseArgument): def __init__(self, name: str, *, - help: str = "Help message for the boolean argument", - is_required: bool = False, - is_deprecated: bool = False): + prefix: Literal["-", "--", "---"] = "--", + help: str = "Help message for the boolean argument", + is_deprecated: bool = False): """ Public. Boolean argument, does not require a value :param name: name of the argument @@ -76,7 +64,7 @@ class BooleanArgument(BaseArgument): :param is_deprecated: whether the argument is deprecated """ self.action: str = "store_true" - super().__init__(name, help=help, is_required=is_required, is_deprecated=is_deprecated) + super().__init__(name, prefix=prefix, help=help, is_deprecated=is_deprecated) class InputArgument: @@ -88,4 +76,7 @@ class InputArgument: self.founder_class: type[BaseArgument] = founder_class def __str__(self) -> str: - return f"{self.name}={self.value}" + return f"InputArgument({self.name}={self.value})" + + def __repr__(self) -> str: + return f"InputArgument" diff --git a/src/argenta/orchestrator/argparser/entity.py b/src/argenta/orchestrator/argparser/entity.py index 520bf8c..b49e426 100644 --- a/src/argenta/orchestrator/argparser/entity.py +++ b/src/argenta/orchestrator/argparser/entity.py @@ -1,12 +1,11 @@ from argparse import ArgumentParser, Namespace -from typing import Self +from typing import Never, Self from argenta.orchestrator.argparser.arguments.models import ( BaseArgument, BooleanArgument, InputArgument, - ValueArgument, - RequiredArgument, + ValueArgument ) @@ -16,19 +15,30 @@ class ArgSpace: @classmethod def from_namespace(cls, namespace: Namespace, - processed_args: list[RequiredArgument | ValueArgument | BooleanArgument]) -> Self: + processed_args: list[ValueArgument | BooleanArgument]) -> Self: name_type_paired_args: dict[str, type[BaseArgument]] = { arg.name: type(arg) for arg in processed_args } - return cls([InputArgument(name, value, name_type_paired_args[name]) + return cls([InputArgument(name=name, + value=value, + founder_class=name_type_paired_args[name]) for name, value in vars(namespace).items()]) - + + def get_by_name(self, name: str) -> InputArgument | None: + for arg in self.all_arguments: + if arg.name == name: + return arg + return None + + def get_by_type(self, arg_type: type[BaseArgument]) -> list[InputArgument] | list[Never]: + return [arg for arg in self.all_arguments if arg.founder_class is arg_type] + class ArgParser: def __init__( self, - processed_args: list[RequiredArgument | ValueArgument | BooleanArgument], *, + processed_args: list[ValueArgument | BooleanArgument], *, name: str = "Argenta", description: str = "Argenta available arguments", epilog: str = "github.com/koloideal/Argenta | made by kolo", @@ -40,22 +50,21 @@ class ArgParser: :param epilog: the epilog of the ArgParse instance :param processed_args: registered and processed arguments """ - self._name: str = name - self._description: str = description - self._epilog: str = epilog + self.name: str = name + self.description: str = description + self.epilog: str = epilog + self.processed_args: list[ValueArgument | BooleanArgument] = processed_args - self._entity: ArgumentParser = ArgumentParser(prog=name, description=description, epilog=epilog) - self._processed_args: list[RequiredArgument | ValueArgument | BooleanArgument] = processed_args + self._core: ArgumentParser = ArgumentParser(prog=name, description=description, epilog=epilog) for arg in processed_args: if isinstance(arg, BooleanArgument): - _ = self._entity.add_argument(arg.name, + _ = self._core.add_argument(arg.string_entity, action=arg.action, help=arg.help, - required=arg.is_required, deprecated=arg.is_deprecated) else: - _ = self._entity.add_argument(arg.name, + _ = self._core.add_argument(arg.string_entity, action=arg.action, help=arg.help, default=arg.default, @@ -64,6 +73,6 @@ class ArgParser: deprecated=arg.is_deprecated) def parse_args(self) -> ArgSpace: - return ArgSpace.from_namespace(namespace=self._entity.parse_args(), - processed_args=self._processed_args) + return ArgSpace.from_namespace(namespace=self._core.parse_args(), + processed_args=self.processed_args) \ No newline at end of file diff --git a/src/argenta/orchestrator/entity.py b/src/argenta/orchestrator/entity.py index e9d3dba..3719bc1 100644 --- a/src/argenta/orchestrator/entity.py +++ b/src/argenta/orchestrator/entity.py @@ -3,14 +3,17 @@ from argenta.orchestrator.argparser import ArgParser from argenta.orchestrator.argparser.entity import ArgSpace +DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[]) + + class Orchestrator: - def __init__(self, arg_parser: ArgParser | None = None): + def __init__(self, arg_parser: ArgParser = DEFAULT_ARGPARSER): """ Public. An orchestrator and configurator that defines the behavior of an integrated system, one level higher than the App :param arg_parser: Cmd argument parser and configurator at startup :return: None """ - self._arg_parser: ArgParser | None = arg_parser + self._arg_parser: ArgParser = arg_parser def start_polling(self, app: App) -> None: """ @@ -18,8 +21,5 @@ class Orchestrator: :param app: a running application :return: None """ - if self._arg_parser is not None: - parsed_argspace: ArgSpace = self._arg_parser.parse_args() - app.run_polling(argspace=parsed_argspace) - else: - app.run_polling(argspace=None) + parsed_argspace: ArgSpace = self._arg_parser.parse_args() + app.run_polling(argspace=parsed_argspace) diff --git a/tests/system_tests/test_system_handling_non_standard_behavior.py b/tests/system_tests/test_system_handling_non_standard_behavior.py index 67ba794..7bab541 100644 --- a/tests/system_tests/test_system_handling_non_standard_behavior.py +++ b/tests/system_tests/test_system_handling_non_standard_behavior.py @@ -4,12 +4,10 @@ from unittest import TestCase import io import re -from argenta.app import App from argenta.command import Command, PredefinedFlags from argenta.command.flag.models import ValidationStatus -from argenta.router import Router from argenta.command.flag.flags.models import Flags -from argenta.orchestrator import Orchestrator +from argenta import Orchestrator, App, Router from argenta.response import Response diff --git a/tests/system_tests/test_system_handling_normal_behavior.py b/tests/system_tests/test_system_handling_normal_behavior.py index 29fa790..c99659c 100644 --- a/tests/system_tests/test_system_handling_normal_behavior.py +++ b/tests/system_tests/test_system_handling_normal_behavior.py @@ -4,12 +4,10 @@ from unittest import TestCase import io import re -from argenta.app import App from argenta.command import Command, PredefinedFlags from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.response import Response -from argenta.router import Router -from argenta.orchestrator import Orchestrator +from argenta import Orchestrator, App, Router from argenta.command.flag import Flag from argenta.command.flag.flags import Flags diff --git a/tests/unit_tests/test_argparser.py b/tests/unit_tests/test_argparser.py new file mode 100644 index 0000000..da86049 --- /dev/null +++ b/tests/unit_tests/test_argparser.py @@ -0,0 +1,164 @@ +import unittest +from unittest.mock import MagicMock, patch +from argparse import Namespace + +from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace +from argenta.orchestrator.argparser.arguments.models import ( + ValueArgument, + BooleanArgument, + InputArgument, + BaseArgument +) + + +class TestArgumentClasses(unittest.TestCase): + def test_value_argument_creation(self): + arg = ValueArgument( + name="test_arg", + prefix="--", + help="A test argument.", + possible_values=["one", "two"], + default="one", + is_required=True, + is_deprecated=False + ) + self.assertEqual(arg.name, "test_arg") + self.assertEqual(arg.prefix, "--") + self.assertEqual(arg.help, "A test argument.") + self.assertEqual(arg.possible_values, ["one", "two"]) + self.assertEqual(arg.default, "one") + self.assertTrue(arg.is_required) + self.assertFalse(arg.is_deprecated) + self.assertEqual(arg.action, "store") + self.assertEqual(arg.string_entity, "--test_arg") + + def test_boolean_argument_creation(self): + arg = BooleanArgument( + name="verbose", + prefix="-", + help="Enable verbose mode.", + is_deprecated=True + ) + self.assertEqual(arg.name, "verbose") + self.assertEqual(arg.prefix, "-") + self.assertEqual(arg.help, "Enable verbose mode.") + self.assertTrue(arg.is_deprecated) + self.assertEqual(arg.action, "store_true") + self.assertEqual(arg.string_entity, "-verbose") + + def test_input_argument_creation(self): + arg = InputArgument( + name="file", + value="/path/to/file", + founder_class=ValueArgument + ) + self.assertEqual(arg.name, "file") + self.assertEqual(arg.value, "/path/to/file") + self.assertEqual(arg.founder_class, ValueArgument) + + +class TestArgParser(unittest.TestCase): + def setUp(self): + self.value_arg = ValueArgument(name="config", help="Path to config file") + self.bool_arg = BooleanArgument(name="debug", help="Enable debug mode") + self.processed_args = [self.value_arg, self.bool_arg] + + def test_argparser_initialization(self): + parser = ArgParser( + processed_args=self.processed_args, + name="TestApp", + description="A test application.", + epilog="Test epilog." + ) + self.assertEqual(parser.name, "TestApp") + self.assertEqual(parser.description, "A test application.") + self.assertEqual(parser.epilog, "Test epilog.") + self.assertEqual(parser.processed_args, self.processed_args) + + @patch('argenta.orchestrator.argparser.entity.ArgumentParser.parse_args') + def test_parse_args(self, mock_parse_args: MagicMock): + mock_namespace = Namespace(config='config.json', debug=True) + mock_parse_args.return_value = mock_namespace + + parser = ArgParser(processed_args=self.processed_args) + arg_space = parser.parse_args() + + self.assertIsInstance(arg_space, ArgSpace) + self.assertEqual(len(arg_space.all_arguments), 2) + + config_arg = arg_space.get_by_name('config') + debug_arg = arg_space.get_by_name('debug') + + self.assertIsNotNone(config_arg) + if config_arg: + self.assertEqual(config_arg.value, 'config.json') + self.assertEqual(config_arg.founder_class, ValueArgument) + + self.assertIsNotNone(debug_arg) + if debug_arg: + self.assertTrue(debug_arg.value) + self.assertEqual(debug_arg.founder_class, BooleanArgument) + + +class TestArgSpace(unittest.TestCase): + def setUp(self): + self.input_arg1 = InputArgument(name="arg1", value="val1", founder_class=ValueArgument) + self.input_arg2 = InputArgument(name="arg2", value="val2", founder_class=BooleanArgument) + self.input_arg3 = InputArgument(name="arg3", value="val3", founder_class=ValueArgument) + self.arg_space = ArgSpace(all_arguments=[self.input_arg1, self.input_arg2, self.input_arg3]) + + def test_argspace_initialization(self): + self.assertEqual(len(self.arg_space.all_arguments), 3) + self.assertIn(self.input_arg1, self.arg_space.all_arguments) + self.assertIn(self.input_arg2, self.arg_space.all_arguments) + self.assertIn(self.input_arg3, self.arg_space.all_arguments) + + def test_get_by_name(self): + found_arg = self.arg_space.get_by_name("arg1") + self.assertIsNotNone(found_arg) + if found_arg: + self.assertEqual(found_arg, self.input_arg1) + + def test_get_by_name_not_found(self): + found_arg = self.arg_space.get_by_name("non_existent_arg") + self.assertIsNone(found_arg) + + def test_get_by_type(self): + value_args = self.arg_space.get_by_type(ValueArgument) + self.assertEqual(len(value_args), 2) + self.assertIn(self.input_arg1, value_args) + self.assertIn(self.input_arg3, value_args) + + bool_args = self.arg_space.get_by_type(BooleanArgument) + self.assertEqual(len(bool_args), 1) + self.assertIn(self.input_arg2, bool_args) + + def test_get_by_type_not_found(self): + class OtherArgument(BaseArgument): + pass + + other_args = self.arg_space.get_by_type(OtherArgument) + self.assertEqual(len(other_args), 0) + + def test_from_namespace(self): + namespace = Namespace(arg1="val1", debug=True) + processed_args = [ + ValueArgument(name="arg1", prefix="--"), + BooleanArgument(name="debug", prefix="-") + ] + + arg_space = ArgSpace.from_namespace(namespace, processed_args) + self.assertEqual(len(arg_space.all_arguments), 2) + + arg1 = arg_space.get_by_name('arg1') + debug_arg = arg_space.get_by_name('debug') + + self.assertIsNotNone(arg1) + if arg1: + self.assertEqual(arg1.value, "val1") + self.assertEqual(arg1.founder_class, ValueArgument) + + self.assertIsNotNone(debug_arg) + if debug_arg: + self.assertTrue(debug_arg.value) + self.assertEqual(debug_arg.founder_class, BooleanArgument)