diff --git a/.travis.yml b/.travis.yml index 3c72244..b47f73a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python python: - - 2.6 - 2.7 - 3.5 + - 3.6 # command to install dependencies before_install: - pip install -r src/test-requirements.txt @@ -11,6 +11,8 @@ install: - cd src - python setup.py install # command to run tests -script: py.test cmdtree --cov=cmdtree +script: + - make test + - make flake8 after_success: - coveralls \ No newline at end of file diff --git a/doc/ParserFlow.md b/doc/ParserFlow.md new file mode 100644 index 0000000..9a73714 --- /dev/null +++ b/doc/ParserFlow.md @@ -0,0 +1,15 @@ + +## Error Type + ++ No such command ++ Argument Required ++ Invalid Argument Type + +``` +cmd_path = get_cmd_path() +parser = get_parser(cmd_path) +find command (argv, cmd_path)-> + validate argument-length + find command(child_cmd_path, argv[command_2:]) + +``` diff --git a/src/.flake8 b/src/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/src/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/src/Makefile b/src/Makefile index 03bb371..0d8b0ac 100644 --- a/src/Makefile +++ b/src/Makefile @@ -8,7 +8,10 @@ clean: rm -fr ./dist ./build ./cmdtree.egg-info ./.cache test: - py.test --cov=cmdtree --cov-report=term-missing cmdtree - + py.test tests --cov=cmdtree --cov-report=term-missing cmdtree +flake8: + flake8 cmdtree + flake8 tests + flake8 examples develop: python setup.py develop diff --git a/src/cmdtree/tests/__init__.py b/src/__init__.py similarity index 100% rename from src/cmdtree/tests/__init__.py rename to src/__init__.py diff --git a/src/cmdtree/__init__.py b/src/cmdtree/__init__.py index c257f94..2ce6cb2 100644 --- a/src/cmdtree/__init__.py +++ b/src/cmdtree/__init__.py @@ -1,4 +1,3 @@ -from cmdtree.parser import AParser from cmdtree.registry import env from cmdtree.shortcuts import ( argument, @@ -20,6 +19,20 @@ ) # globals and entry point -env.parser = AParser() entry = env.entry +__all__ = ( + "argument", + "option", + "command", + "group", + "STRING", + "INT", + "FLOAT", + "BOOL", + "UUID", + "Choices", + "IntRange", + "File", + "entry", +) diff --git a/src/cmdtree/_compat.py b/src/cmdtree/_compat.py index a13fb06..b571540 100644 --- a/src/cmdtree/_compat.py +++ b/src/cmdtree/_compat.py @@ -7,10 +7,11 @@ def get_filesystem_encoding(): return sys.getfilesystemencoding() or sys.getdefaultencoding() + if WIN: def _get_argv_encoding(): import locale return locale.getpreferredencoding() else: def _get_argv_encoding(): - return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() \ No newline at end of file + return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() diff --git a/src/cmdtree/constants.py b/src/cmdtree/constants.py new file mode 100644 index 0000000..9d3cf7d --- /dev/null +++ b/src/cmdtree/constants.py @@ -0,0 +1 @@ +ROOT_NODE_NAME = "root" diff --git a/src/cmdtree/decorators.py b/src/cmdtree/decorators.py new file mode 100644 index 0000000..3025fa9 --- /dev/null +++ b/src/cmdtree/decorators.py @@ -0,0 +1,23 @@ +from functools import wraps + +from cmdtree import echo +from cmdtree.exceptions import ParserError +from cmdtree.format import format_node_help + + +def format_error(func): + + @wraps(func) + def wrapped(*args, **kwargs): + try: + return func(*args, **kwargs) + except ParserError as e: + assert hasattr(args[0], "tree") + node_dict = args[0].tree.get_node_by_path( + e.node.cmd_path + ) + node_help = format_node_help(node_dict) + echo.error("Error: %s" % str(e.format_error(node_help))) + exit(1) + + return wrapped diff --git a/src/cmdtree/echo.py b/src/cmdtree/echo.py new file mode 100644 index 0000000..6973b82 --- /dev/null +++ b/src/cmdtree/echo.py @@ -0,0 +1,6 @@ +import sys + + +def error(error_msg): + sys.stderr.write(error_msg) + sys.stderr.write("\n") diff --git a/src/cmdtree/exceptions.py b/src/cmdtree/exceptions.py index 328f862..8ab4809 100644 --- a/src/cmdtree/exceptions.py +++ b/src/cmdtree/exceptions.py @@ -1,2 +1,70 @@ -class ArgumentParseError(ValueError): - pass \ No newline at end of file + +class ParserError(ValueError): + DEFAULT_TPL = ( + "{error}\n\n" + "{cmd_ref}" + "{help}" + "{sub_cmd_help}" + ) + + def __init__(self, *args, **kwargs): + super(ParserError, self).__init__(*args) + self.node = kwargs.pop("node", None) + + def format_error(self, sub_cmd_help=None): + cmd_ref = "Help message for '{name}':\n\n" + node_help = "" + _cmd_ref = "" + if sub_cmd_help is not None: + sub_cmd_help = sub_cmd_help + "\n" + else: + sub_cmd_help = "" + if self.node is not None: + node_help = self.node.format_help() + if node_help: + node_help += "\n\n" + _cmd_ref = cmd_ref.format( + name=self.node.name + ) + return self.DEFAULT_TPL.format( + error=str(self), + cmd_ref=_cmd_ref, + sub_cmd_help=sub_cmd_help, + help=node_help, + ) + + +class DevelopmentError(ValueError): + pass + + +class ArgumentRepeatedRegister(DevelopmentError): + pass + + +class ArgumentTypeError(DevelopmentError): + pass + + +class CmdRepeatedRegister(DevelopmentError): + pass + + +class NodeDoesExist(DevelopmentError): + pass + + +class NoSuchCommand(ParserError): + pass + + +class InvalidCommand(ParserError): + pass + + +class ArgumentError(ParserError): + pass + + +class OptionError(ParserError): + pass diff --git a/src/cmdtree/format.py b/src/cmdtree/format.py new file mode 100644 index 0000000..f71ab44 --- /dev/null +++ b/src/cmdtree/format.py @@ -0,0 +1,88 @@ +from cmdtree.templates import E_NO_CMD_GIVEN_TPL + +try: + from textwrap import indent +except ImportError: + import textwrap + + def indent(text, indent_with): + wrapper = textwrap.TextWrapper( + initial_indent=indent_with, + subsequent_indent=indent_with + ) + return wrapper.fill(text) + + +INDENT_1 = " " * 4 + + +def _format_arg_help(argument): + """ + :type argument: cmdtree.parser.Argument + """ + tpl = "{name}: {help}" + _help = argument.help + if argument.help is None: + _help = "argument" + return tpl.format( + name=argument.name, + help=_help, + ) + + +def _format_cmd_help(cmd_node): + tpl = "{name}: {help}" + _help = cmd_node.help + if cmd_node.help is None: + _help = cmd_node.name + return tpl.format( + name=cmd_node.name, + help=_help, + ) + + +def _format_cmd_choice(parent_name, cmd_node_list): + help_msg = "\n".join( + _format_cmd_help(ele) + for ele in cmd_node_list + ) + return E_NO_CMD_GIVEN_TPL.format( + name=parent_name, + cmds=indent(help_msg, INDENT_1) + ) + + +def format_node_help(tree_node): + """ + :type tree_node: dict + """ + node = tree_node + _help = "" + if not node['cmd'].callable(): + if node['cmd'].help is not None: + _help += node['cmd'].help + cmds = tuple( + value['cmd'] + for value in node['children'].values() + ) + if len(cmds) >= 0: + _help += _format_cmd_choice(node['name'], cmds) + return _help if len(_help) > 0 else None + + +def format_arg_help(title, arguments): + """ + :type arguments: iterable[cmdtree.parser.Argument] + :type title: str + """ + if len(arguments) == 0: + return + details = tuple( + _format_arg_help(arg) + for arg in arguments + ) + details = indent("\n".join(details), INDENT_1) + return ( + "{title}\n" + "{details}" + ).format(title=title, details=details) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 18a6be4..0864b8c 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -1,104 +1,386 @@ -from argparse import ArgumentParser import sys -import six +from copy import deepcopy -from cmdtree.exceptions import ArgumentParseError +from cmdtree.echo import error +from cmdtree.exceptions import ( + ParserError, ArgumentRepeatedRegister, + ArgumentTypeError, + ArgumentError, + OptionError, + NodeDoesExist, + NoSuchCommand, + InvalidCommand +) +from cmdtree.decorators import format_error + +from cmdtree.format import format_arg_help from cmdtree.registry import env +from cmdtree.templates import E_MISSING_ARGUMENT +from cmdtree.types import ParamTypeFactory, STRING def _normalize_arg_name(arg_name): + name_list = list(arg_name) + new_name_list = [] + for index, ele in enumerate(name_list): + prev_index = index - 1 if index != 0 else index + prev_ele = name_list[prev_index] + if prev_ele == ele == "-": + continue + new_name_list.append(ele) + arg_name = "".join(new_name_list) return arg_name.replace("-", "_") -def vars_(object=None): - """ - Clean all of the property starts with "_" then - return result of vars(object). - """ - filtered_vars = {} - vars_dict = vars(object) - for key, value in six.iteritems(vars_dict): - if key.startswith("_"): - continue - filtered_vars[_normalize_arg_name(key)] = value - return filtered_vars +def _assert_type_valid(name, type_): + if not isinstance(type_, ParamTypeFactory): + raise ArgumentTypeError( + ( + "Invalid type of argument {}, " + "should be instance of {}" + ).format( + name, + ParamTypeFactory, + ) + ) -class AParser(ArgumentParser): - """ - Arg-parse wrapper for sub command and convenient arg parse. - """ - def __init__(self, *args, **kwargs): - self.subparsers = None - super(AParser, self).__init__(*args, **kwargs) +class Argument(object): + def __init__(self, name, type_, help=None): + self.name = name + self.type = type_ + self.help = help - def add_cmd(self, name, help=None, func=None): - """ - If func is None, this is regarded as a sub-parser which can contains - sub-command. - Else, this is a leaf node in cmd tree which can not add sub-command. - :rtype: AParser - """ - if self.subparsers is None: - self.subparsers = self.add_subparsers( - title="sub-commands", - help=help or 'sub-commands', + def get_value(self, value): + return self.type.convert(value) + + +class Option(object): + def __init__(self, name, help=None, is_flag=False, default=None, type_=None): + self.name = name + self.default = default + if is_flag: + self.default = bool(default) + self.is_flag = is_flag + self.help = help + self.type = type_ + # TODO: implement is_flag parse + + def get_value(self, value): + return self.type.convert(value) + + +class ArgumentMgr(object): + + def __init__(self): + self.arg_names = [] + self.arg_map = {} + self.parsed_values = {} + + def assert_filled(self): + if self.num_args > len(self.parsed_values): + missed_args = [ + name + for name in self.arg_names + if name not in self.parsed_values + ] + msg = E_MISSING_ARGUMENT.format( + args=", ".join(missed_args) ) + return False, msg + return True, None - parser = self.subparsers.add_parser( - name, + def add(self, name, type_=None, help=None): + type_ = type_ or STRING + if name in self.arg_names: + raise ArgumentRepeatedRegister( + "Argument {} registered more than once.".format( + name + ) + ) + _assert_type_valid(name, type_) + self.arg_names.append(name) + self.arg_map[name] = Argument( + name=name, + type_=type_, help=help, ) - if func is not None: - parser.set_defaults(_func=func) - return parser - def run(self, args=None, namespace=None): - args = self.parse_args(args, namespace) - _func = getattr(args, "_func", None) + def format_help(self): + return format_arg_help( + "Positional arguments:", + tuple( + self.arg_map[name] + for name in self.arg_names + ), + ) - if _func: - return args._func(**vars_(args)) - else: - raise ValueError( - "No function binding for args `{args}`".format( - args=args + @property + def num_args(self): + return len(self.arg_names) + + def add_value(self, index, value): + self.parsed_values[self.arg_names[index]] = value + + @property + def kwargs(self): + kwargs = {} + for name, value in self.parsed_values.items(): + argument = self.arg_map[name] + new_name = _normalize_arg_name(name) + kwargs[new_name] = argument.get_value(value) + return kwargs + + +class OptionMgr(object): + def __init__(self): + self.opts_names = [] + self.opts_map = {} + self.parsed_values = {} + + def add(self, name, help=None, is_flag=False, default=None, type_=None): + type_ = type_ or STRING + if name in self.opts_names: + raise ArgumentRepeatedRegister( + "Argument {} registered more than once.".format( + name ) ) + _assert_type_valid(name, type_) + self.opts_names.append(name) + self.opts_map[name] = Option( + name=name, + type_=type_, + help=help, + is_flag=is_flag, + default=default + ) + + def get_option_or_none(self, name): + """ + :rtype: Option + """ + return self.opts_map.get(name) + + def add_value(self, name, value): + self.parsed_values[name] = value + + def format_help(self): + return format_arg_help( + "Optional arguments:", + tuple( + self.opts_map[name] + for name in self.opts_names + ), + ) + + @property + def kwargs(self): + kwargs = {} + for name, opt in self.opts_map.items(): + new_name = _normalize_arg_name(name) + kwargs[new_name] = opt.default + + for name, value in self.parsed_values.items(): + argument = self.opts_map[name] + new_name = _normalize_arg_name(name) + kwargs[new_name] = argument.get_value(value) + return kwargs + + +class CommandNode(object): + """ + Arg-parse wrapper for sub command and convenient arg parse. + """ + def __init__(self, cmd_path, help=None, func=None, is_root=False): + self.is_root = is_root + self.name = cmd_path[-1] + self.cmd_path = cmd_path + if is_root: + self.abs_path = [] + self.help = help + self.arg_mgr = ArgumentMgr() + self.opt_mgr = OptionMgr() + self.func = func + + def format_help(self): + msg = "" + arg_help = self.arg_mgr.format_help() + opt_help = self.opt_mgr.format_help() + if arg_help is not None: + msg += "%s" % arg_help + if opt_help is not None: + msg += "\n\n%s" % opt_help + return msg + + @property + def kwargs(self): + kwargs = {} + kwargs.update(self.arg_mgr.kwargs) + kwargs.update(self.opt_mgr.kwargs) + return kwargs + + @classmethod + def _is_option(cls, arg_str): + return arg_str.startswith("-") + + def parse_args(self, possible_args): + count = 0 + index = -1 + args_len = len(possible_args) + while True: + index += 1 + if index >= args_len: + break + current_arg = possible_args[index] + if self._is_option(current_arg): + option = self.opt_mgr.get_option_or_none( + current_arg + ) + if option is None: + raise OptionError( + "No such option '%s'" % current_arg, + node=self, + ) + if option.is_flag: + self.opt_mgr.add_value( + name=option.name, + value=not option.default, + ) + continue + try: + self.opt_mgr.add_value( + name=option.name, + value=possible_args[index + 1], + ) + except IndexError: + raise ArgumentError( + "No value for argument %s" % option.name, + node=self, + ) + index += 1 + continue + count += 1 + if count > self.arg_mgr.num_args: + index -= 1 + break + self.arg_mgr.add_value( + count - 1, + value=current_arg + ) + filled, msg = self.arg_mgr.assert_filled() + if not filled: + raise ArgumentError( + msg, + node=self, + ) + left_args = possible_args[index + 1:] + eaten_length = args_len - len(left_args) + return eaten_length, left_args + + def callable(self): + return self.func is not None + + def run(self, kwargs): + if self.callable(): + return self.func(**kwargs) def exit(self, status=0, message=None): if message: - self._print_message(message, sys.stderr) + error(message) if env.silent_exit: sys.exit(status) else: - raise ArgumentParseError(message) + raise ParserError(message) def argument(self, name, help=None, type=None): - kwargs = {"help": help} if name.startswith("-"): raise ValueError( "positional argument [{0}] can not contains `-` in".format(name) ) - if type is not None: - kwargs.update( - type() - ) - return self.add_argument( - name, **kwargs + return self.arg_mgr.add( + name=name, + help=help, + type_=type, ) def option(self, name, help=None, is_flag=False, default=None, type=None): _name = name if not name.startswith("-"): _name = "--" + name - kwargs = dict(help=help) - if is_flag: - kwargs['action'] = "store_true" - if default is not None: - kwargs['default'] = default - if type is not None: - kwargs.update(type()) - return self.add_argument(_name, **kwargs) \ No newline at end of file + return self.opt_mgr.add( + _name, + help=help, + is_flag=is_flag, + default=default, + type_=type, + ) + + +class RawArgsParser(object): + + def __init__(self, args, tree): + """ + :type args: list[str] + :type tree: cmdtree.tree.CmdTree + """ + self.raw_args = args + self.tree = tree + self.cmd_nodes = [] + + def parse2cmd(self, raw_args, tree): + cmd_nodes = [] + full_cmd_path = [] + left_args = deepcopy(raw_args) + cmd_start_index = 0 + node = None + while True: + cmd2find = left_args[cmd_start_index:cmd_start_index + 1] + cmd_path2find = full_cmd_path + cmd2find + try: + node = tree.get_node_by_path(cmd_path2find) + except NodeDoesExist: + error_parent = node if node is not None else tree.root + raise NoSuchCommand( + "Command %s does not exist." + % ( + str( + cmd_path2find[-1] + if cmd_path2find + else sys.argv[0] + ), + ), + node=error_parent['cmd'], + ) + cmd = node['cmd'] + left_args = left_args[cmd_start_index + 1:] + index_offset, left_args = cmd.parse_args( + left_args, + ) + full_cmd_path = cmd_path2find + cmd_nodes.append(node) + if len(left_args) <= 0: + break + return cmd_nodes, full_cmd_path + + @format_error + def run(self): + self.cmd_nodes, cmd_path = self.parse2cmd( + self.raw_args, + self.tree, + ) + kwargs = {} + for node in self.cmd_nodes: + kwargs.update( + node['cmd'].kwargs + ) + node = self.cmd_nodes[-1] + cmd = node['cmd'] + if not cmd.callable(): + raise InvalidCommand( + "%s is a command-group" % node['name'], + node=node['cmd'] + ) + return cmd.run(kwargs) diff --git a/src/cmdtree/proxy.py b/src/cmdtree/proxy.py new file mode 100644 index 0000000..f9348c6 --- /dev/null +++ b/src/cmdtree/proxy.py @@ -0,0 +1,196 @@ +from cmdtree import env +from cmdtree.utils import get_func_name, get_cmd_path + + +CMD_META_NAME = "meta" + + +class CmdMeta(object): + __slots__ = ( + "full_path", + "name", + "parser", + ) + + def __init__(self, name=None, full_path=None, parser=None): + """ + :param full_path: should always be tuple to avoid + unexpected changes from outside. + """ + self.full_path = tuple(full_path) if full_path else tuple() + self.name = name + self.parser = parser + + +class ParserProxy(object): + __slots__ = ( + "options", + "arguments", + ) + + def __init__(self): + self.options = [] + self.arguments = [] + + def option(self, *args, **kwargs): + self.options.append((args, kwargs)) + + def argument(self, *args, **kwargs): + self.arguments.append((args, kwargs)) + + +class CmdProxy(object): + """ + Used to store original cmd info for cmd build proxy. + """ + __slots__ = ( + "func", + "meta", + ) + + def __init__(self, func): + self.func = func + self.meta = CmdMeta(parser=ParserProxy()) + + +class Group(object): + def __init__(self, func, name, parser, help=None, full_path=None): + """ + :type func: callable + :type name: str + :type parser: cmdtree.parser.CommandNode + :type help: str + :type full_path: tuple or list + """ + self.func = func + self.meta = CmdMeta( + name=name, + full_path=full_path, + parser=parser, + ) + self.help = help + + def __call__(self, *args, **kwargs): + # TODO(winkidney): This func will not work in + # any case now.Be left now for possible call. + return self.func(*args, **kwargs) + + def command(self, name=None, help=None): + return _mk_cmd(name, help=help, path_prefix=self.meta.full_path) + + def group(self, name=None, help=None): + return _mk_group(name, help=help, path_prefix=self.meta.full_path) + + +class Cmd(object): + def __init__(self, func, name, parser, help=None, full_path=None): + self.func = func + self.meta = CmdMeta( + name=name, + full_path=full_path, + parser=parser, + ) + self.help = help + + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + + +def _apply2parser(arguments, options, parser): + """ + :return the parser itself + :type arguments: list[list[T], dict[str, T]] + :type options: list[list[T], dict[str, T]] + :type parser: cmdtree.parser.CommandNode + :rtype: cmdtree.parser.CommandNode + """ + for args, kwargs in options: + parser.option(*args, **kwargs) + for args, kwargs in arguments: + parser.argument(*args, **kwargs) + return parser + + +def apply2parser(cmd_proxy, parser): + """ + Apply a CmdProxy's arguments and options + to a parser of argparse. + :type cmd_proxy: callable or cmdtree.proxy.CmdProxy + :type parser: cmdtree.parser.CommandNode + :rtype: cmdtree.parser.CommandNode + """ + if isinstance(cmd_proxy, CmdProxy): + parser_proxy = cmd_proxy.meta.parser + _apply2parser( + parser_proxy.arguments, + parser_proxy.options, + parser, + ) + return parser + + +def _mk_group(name, help=None, path_prefix=None): + + def wrapper(func): + if isinstance(func, Group): + raise ValueError( + "You can not register group `{name}` more than once".format( + name=name + ) + ) + _name = name + _func = func + + if isinstance(func, CmdProxy): + _func = func.func + + if name is None: + _name = get_func_name(_func) + + full_path = get_cmd_path(path_prefix, _name) + + tree = env.tree + parser = tree.add_parent_commands(full_path, help=help)['cmd'] + _group = Group( + _func, + _name, + parser, + help=help, + full_path=full_path, + ) + apply2parser(func, parser) + return _group + return wrapper + + +def _mk_cmd(name, help=None, path_prefix=None): + def wrapper(func): + if isinstance(func, Cmd): + raise ValueError( + "You can not register a command more than once: {0}".format( + func + ) + ) + _func = func + + if isinstance(func, CmdProxy): + _func = func.func + + _name = name + if name is None: + _name = get_func_name(_func) + + full_path = get_cmd_path(path_prefix, _name) + tree = env.tree + parser = tree.add_commands(full_path, _func, help=help) + _cmd = Cmd( + _func, + _name, + parser, + help=help, + full_path=full_path, + ) + apply2parser(func, parser) + + return _cmd + return wrapper diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index aea98d6..70ef2c1 100644 --- a/src/cmdtree/registry.py +++ b/src/cmdtree/registry.py @@ -1,3 +1,6 @@ +import sys + + class ENV(object): __slots__ = ( "silent_exit", @@ -7,13 +10,18 @@ class ENV(object): def __init__(self): """ - :type parser: cmdtree.parser.AParser + :type parser: cmdtree.parser.CommandNode """ self.silent_exit = True self._tree = None - def entry(self, args=None, namespace=None): - return self.tree.root.run(args, namespace) + def entry(self, args=None): + from cmdtree.parser import RawArgsParser + + if args is None: + args = sys.argv[1:] + parser = RawArgsParser(args, self.tree) + return parser.run() @property def tree(self): @@ -25,8 +33,5 @@ def tree(self): self._tree = CmdTree() return self._tree - @property - def root(self): - return self.tree.root -env = ENV() \ No newline at end of file +env = ENV() diff --git a/src/cmdtree/shortcuts.py b/src/cmdtree/shortcuts.py index 33db35e..a0935a2 100644 --- a/src/cmdtree/shortcuts.py +++ b/src/cmdtree/shortcuts.py @@ -1,211 +1,4 @@ -from cmdtree.registry import env - - -CMD_META_NAME = "meta" - - -def _get_cmd_path(path_prefix, cmd_name): - if path_prefix is None: - full_path = (cmd_name, ) - else: - full_path = tuple(path_prefix) + (cmd_name, ) - return full_path - - -def _apply2parser(arguments, options, parser): - """ - :return the parser itself - :type arguments: list[list[T], dict[str, T]] - :type options: list[list[T], dict[str, T]] - :type parser: cmdtree.parser.AParser - :rtype: cmdtree.parser.AParser - """ - for args, kwargs in options: - parser.option(*args, **kwargs) - for args, kwargs in arguments: - parser.argument(*args, **kwargs) - return parser - - -def apply2parser(cmd_proxy, parser): - """ - Apply a CmdProxy's arguments and options - to a parser of argparse. - :type cmd_proxy: callable or CmdProxy - :type parser: cmdtree.parser.AParser - :rtype: cmdtree.parser.AParser - """ - if isinstance(cmd_proxy, CmdProxy): - parser_proxy = cmd_proxy.meta.parser - _apply2parser( - parser_proxy.arguments, - parser_proxy.options, - parser, - ) - return parser - - -def _mk_group(name, help=None, path_prefix=None): - - def wrapper(func): - if isinstance(func, Group): - raise ValueError( - "You can not register group `{name}` more than once".format( - name=name - ) - ) - _name = name - _func = func - - if isinstance(func, CmdProxy): - _func = func.func - - if name is None: - _name = _get_func_name(_func) - - full_path = _get_cmd_path(path_prefix, _name) - - tree = env.tree - parser = tree.add_parent_commands(full_path, help=help)['cmd'] - _group = Group( - _func, - _name, - parser, - help=help, - full_path=full_path, - ) - apply2parser(func, parser) - return _group - return wrapper - - -def _mk_cmd(name, help=None, path_prefix=None): - def wrapper(func): - if isinstance(func, Cmd): - raise ValueError( - "You can not register a command more than once: {0}".format( - func - ) - ) - _func = func - - if isinstance(func, CmdProxy): - _func = func.func - - _name = name - if name is None: - _name = _get_func_name(_func) - - full_path = _get_cmd_path(path_prefix, _name) - tree = env.tree - parser = tree.add_commands(full_path, _func, help=help) - _cmd = Cmd( - _func, - _name, - parser, - help=help, - full_path=full_path, - ) - apply2parser(func, parser) - - return _cmd - return wrapper - - -class CmdMeta(object): - __slots__ = ( - "full_path", - "name", - "parser", - ) - - def __init__(self, name=None, full_path=None, parser=None): - """ - :param full_path: should always be tuple to avoid - unexpected changes from outside. - """ - self.full_path = tuple(full_path) if full_path else tuple() - self.name = name - self.parser = parser - - -class ParserProxy(object): - __slots__ = ( - "options", - "arguments", - ) - - def __init__(self): - self.options = [] - self.arguments = [] - - def option(self, *args, **kwargs): - self.options.append((args, kwargs)) - - def argument(self, *args, **kwargs): - self.arguments.append((args, kwargs)) - - -class CmdProxy(object): - """ - Used to store original cmd info for cmd build proxy. - """ - __slots__ = ( - "func", - "meta", - ) - - def __init__(self, func): - self.func = func - self.meta = CmdMeta(parser=ParserProxy()) - - -class Group(object): - def __init__(self, func, name, parser, help=None, full_path=None): - """ - :type func: callable - :type name: str - :type parser: cmdtree.parser.AParser - :type help: str - :type full_path: tuple or list - """ - self.func = func - self.meta = CmdMeta( - name=name, - full_path=full_path, - parser=parser, - ) - self.help = help - - def __call__(self, *args, **kwargs): - # TODO(winkidney): This func will not work in - # any case now.Be left now for possible call. - return self.func(*args, **kwargs) - - def command(self, name=None, help=None): - return _mk_cmd(name, help=help, path_prefix=self.meta.full_path) - - def group(self, name=None, help=None): - return _mk_group(name, help=help, path_prefix=self.meta.full_path) - - -class Cmd(object): - def __init__(self, func, name, parser, help=None, full_path=None): - self.func = func - self.meta = CmdMeta( - name=name, - full_path=full_path, - parser=parser, - ) - self.help = help - - def __call__(self, *args, **kwargs): - return self.func(*args, **kwargs) - - -def _get_func_name(func): - assert callable(func) - return func.__name__ +from cmdtree.proxy import CmdProxy, Group, Cmd, _mk_group, _mk_cmd def group(name=None, help=None): @@ -259,4 +52,4 @@ def wrapper(func): type=type, ) return meta_cmd - return wrapper \ No newline at end of file + return wrapper diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py new file mode 100644 index 0000000..a4b0e1b --- /dev/null +++ b/src/cmdtree/templates.py @@ -0,0 +1,5 @@ +E_MISSING_ARGUMENT = """Missing positional arguments: {args}""" + +E_NO_CMD_GIVEN_TPL = """Sub-commands for {name}: +{cmds} +""" diff --git a/src/cmdtree/tests/unittest/test_parser.py b/src/cmdtree/tests/unittest/test_parser.py deleted file mode 100644 index 83c499f..0000000 --- a/src/cmdtree/tests/unittest/test_parser.py +++ /dev/null @@ -1,197 +0,0 @@ -import mock -import pytest -import six - -from cmdtree import parser -from cmdtree.exceptions import ArgumentParseError - - -def mk_obj(property_dict): - class TestObject(object): - pass - obj = TestObject() - for key, value in six.iteritems(property_dict): - setattr(obj, key, value) - return obj - - -@pytest.fixture() -def aparser(): - from cmdtree.parser import AParser - return AParser() - - -@pytest.fixture() -def test_func(): - def func(): - return "result" - return func - - -@pytest.mark.parametrize( - "arg_name, expected", - ( - ("hello_world", "hello_world"), - ("hello-world", "hello_world"), - ) -) -def test_normalize_arg_name(arg_name, expected): - from cmdtree.parser import _normalize_arg_name - assert _normalize_arg_name(arg_name) == expected - - -@pytest.mark.parametrize( - "p_dict, expected", - ( - ({"_k": "v", "k": "v"}, {"k": "v"}), - ({"__k": "v", "k": "v"}, {"k": "v"}), - ({"k1": "v", "k": "v"}, {"k": "v", "k1": "v"}), - ) -) -def test_vars_should_return_right_dict(p_dict, expected): - obj = mk_obj(p_dict) - assert parser.vars_( - obj - ) == expected - - -class TestAParser: - def test_should_execute_func(self, aparser, test_func): - aparser.add_cmd("test", func=test_func) - assert aparser.run(["test"]) == "result" - - def test_should_execute_child_cmd(self, aparser, test_func): - parent = aparser.add_cmd("parent") - parent.add_cmd("child", func=test_func) - assert aparser.run(['parent', 'child']) == "result" - - @pytest.mark.parametrize( - "cmd_func, exception", - ( - (None, ValueError), - (lambda *args, **kwargs: "str", None), - ) - ) - def test_should_execute_without_func(self, cmd_func, exception, aparser): - parent = aparser.add_cmd("parent") - parent.add_cmd("child", func=cmd_func) - if exception is not None: - with pytest.raises(exception): - aparser.run(['parent', 'child']) - else: - assert aparser.run(['parent', 'child']) == "str" - - @pytest.mark.parametrize( - "silent_exit, exception", - ( - (False, ArgumentParseError), - (True, SystemExit) - ) - ) - def test_should_parent_cmd_exit_or_raise_error(self, silent_exit, exception, test_func, aparser): - from cmdtree.registry import env - env.silent_exit = silent_exit - parent = aparser.add_cmd("parent") - parent.add_cmd("child", func=test_func) - with pytest.raises(exception): - aparser.run(['parent']) - - @pytest.mark.parametrize( - "arg_name, exception", - ( - ('--name', ValueError), - ('-name', ValueError), - ('name', None), - ) - ) - def test_should_argument_starts_with_valid_string(self, arg_name, exception, test_func, aparser): - cmd = aparser.add_cmd("execute", func=test_func) - with mock.patch.object(cmd, "add_argument") as mocked_add: - if exception is not None: - with pytest.raises(exception): - cmd.argument(arg_name) - else: - cmd.argument(arg_name) - mocked_add.assert_called_with(arg_name, help=None) - - @pytest.mark.parametrize( - "arg_name, expected_name", - ( - ('--name', '--name'), - ('-name', '-name'), - ('name', '--name'), - ) - ) - def test_option_should_starts_with_hyphen(self, arg_name, expected_name, test_func, aparser): - cmd = aparser.add_cmd("execute", func=test_func) - with mock.patch.object(cmd, "add_argument") as mocked_add: - cmd.option(arg_name) - mocked_add.assert_called_with(expected_name, help=None) - - @pytest.mark.parametrize( - "is_flag", - ( - True, - False, - ) - ) - def test_option_should_work_with_is_flag(self, is_flag, test_func, aparser): - cmd = aparser.add_cmd("execute", func=test_func) - with mock.patch.object(cmd, "add_argument") as mocked_add: - cmd.option("name", is_flag=is_flag) - if is_flag: - mocked_add.assert_called_with("--name", help=None, action="store_true") - else: - mocked_add.assert_called_with("--name", help=None) - - @pytest.mark.parametrize( - "default", - ( - None, - 1, - ) - ) - def test_option_should_work_with_default_value(self, default, aparser): - cmd = aparser.add_cmd("execute", func=test_func) - with mock.patch.object(cmd, "add_argument") as mocked_add: - cmd.option("name", default=default) - if default is None: - mocked_add.assert_called_with("--name", help=None) - else: - mocked_add.assert_called_with("--name", help=None, default=default) - - @pytest.mark.parametrize( - "type_func, kwargs", - ( - (mock.Mock(), {"help": None, "type": int}), - (None, {"help": None}), - ) - ) - def test_add_argument_work_with_type( - self, type_func, kwargs, aparser - ): - if type_func is not None: - type_func.return_value = {"type": int} - with mock.patch.object(aparser, "add_argument") as mocked_add: - aparser.argument("name", type=type_func) - if type_func is not None: - assert type_func.called - mocked_add.assert_called_with("name", **kwargs) - - @pytest.mark.parametrize( - "type_func, kwargs", - ( - (mock.Mock(), {"help": None, "type": int}), - (None, {"help": None}), - ) - ) - def test_add_option_work_with_type( - self, type_func, kwargs, aparser - ): - if type_func is not None: - type_func.return_value = {"type": int} - with mock.patch.object(aparser, "add_argument") as mocked_add: - aparser.option("name", type=type_func) - if type_func is not None: - assert type_func.called - mocked_add.assert_called_with("--name", **kwargs) diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 3f33358..4fb6e74 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -1,4 +1,7 @@ -from cmdtree.parser import AParser +import sys + +from cmdtree.exceptions import NodeDoesExist +from cmdtree.parser import CommandNode def _mk_cmd_node(cmd_name, cmd_obj): @@ -17,19 +20,30 @@ class CmdTree(object): def __init__(self, root_parser=None): """ - :type root_parser: cmdtree.parser.AParser + :type root_parser: cmdtree.parser.CommandNode """ if root_parser is not None: - self.root = root_parser + root = root_parser else: - self.root = AParser() - self.tree = { - "name": "root", - "cmd": self.root, + root = CommandNode( + cmd_path=sys.argv[:1], + is_root=True, + ) + assert root.is_root is True + self.root = { + "name": root.name, + "cmd": root, "children": {} } def get_cmd_by_path(self, existed_cmd_path): + """ + :rtype: CommandNode + """ + node = self.get_node_by_path(existed_cmd_path) + return node['cmd'] + + def get_node_by_path(self, existed_cmd_path): """ :return: { @@ -38,12 +52,14 @@ def get_cmd_by_path(self, existed_cmd_path): "children": {} } """ - parent = self.tree + parent = self.root + if len(existed_cmd_path) == 0: + return self.root for cmd_name in existed_cmd_path: try: parent = parent['children'][cmd_name] except KeyError: - raise ValueError( + raise NodeDoesExist( "Given key [%s] in path %s does not exist in tree." % (cmd_name, existed_cmd_path) ) @@ -53,11 +69,20 @@ def _add_node(self, cmd_node, cmd_path): """ :type cmd_path: list or tuple """ - parent = self.tree - for cmd_key in cmd_path: + parent = self.root + path_len = len(cmd_path) + index = 0 + for index, cmd_key in enumerate(cmd_path): if cmd_key not in parent['children']: break parent = parent['children'][cmd_key] + if path_len != 0: + if (index + 1) != path_len: + raise ValueError( + "Failed to add node {path}, parent not existed.".format( + path=cmd_path + ) + ) parent["children"][cmd_node['name']] = cmd_node return cmd_node @@ -70,10 +95,12 @@ def _get_paths(full_path, end_index): return new_path, existed_path def add_commands(self, cmd_path, func, help=None): - cmd_name = cmd_path[-1] - parent = self.add_parent_commands(cmd_path[:-1]) - sub_command = parent['cmd'].add_cmd(name=cmd_name, func=func, help=help) - node = _mk_cmd_node(cmd_name, sub_command) + sub_command = CommandNode( + cmd_path=cmd_path, + func=func, + help=help + ) + node = _mk_cmd_node(sub_command.name, sub_command) self._add_node(node, cmd_path=cmd_path) return sub_command @@ -88,21 +115,24 @@ def add_parent_commands(self, cmd_path, help=None): cmd_path, existed_cmd_end_index, ) - parent_node = self.get_cmd_by_path(existed_path) + parent_node = self.get_node_by_path(existed_path) last_one_index = 1 new_path_len = len(new_path) _kwargs = {} - for cmd_name in new_path: + for index, _ in enumerate(new_path): + current_path = existed_path + new_path[:index + 1] + parent_path = existed_path + new_path[:index] if last_one_index >= new_path_len: _kwargs['help'] = help - sub_cmd = parent_node['cmd'].add_cmd( - cmd_name, **_kwargs + sub_cmd = CommandNode( + cmd_path=current_path, + **_kwargs ) - parent_node = _mk_cmd_node(cmd_name, sub_cmd) + parent_node = _mk_cmd_node(sub_cmd.name, sub_cmd) self._add_node( parent_node, - existed_path + new_path[:new_path.index(cmd_name)] + parent_path, ) last_one_index += 1 return parent_node @@ -113,7 +143,7 @@ def index_in_tree(self, cmd_path): :type cmd_path: list or tuple :return: None if cmd_path already indexed in tree. """ - current_tree = self.tree + current_tree = self.root for key in cmd_path: if key in current_tree['children']: current_tree = current_tree['children'][key] diff --git a/src/cmdtree/types.py b/src/cmdtree/types.py index 0c48c1a..7a79937 100644 --- a/src/cmdtree/types.py +++ b/src/cmdtree/types.py @@ -1,4 +1,4 @@ -from argparse import ArgumentTypeError as ArgTypeError, FileType +from argparse import FileType, ArgumentTypeError from six import text_type, PY2 @@ -27,7 +27,7 @@ def convert(self, value): @staticmethod def fail(msg): - raise ArgTypeError(msg) + raise ArgumentTypeError(msg) class UnprocessedParamType(ParamTypeFactory): @@ -200,4 +200,4 @@ def __call__(self): BOOL = BoolParamType() -UUID = UUIDParameterType() \ No newline at end of file +UUID = UUIDParameterType() diff --git a/src/cmdtree/utils.py b/src/cmdtree/utils.py new file mode 100644 index 0000000..e6446c2 --- /dev/null +++ b/src/cmdtree/utils.py @@ -0,0 +1,11 @@ +def get_cmd_path(path_prefix, cmd_name): + if path_prefix is None: + full_path = (cmd_name, ) + else: + full_path = tuple(path_prefix) + (cmd_name, ) + return full_path + + +def get_func_name(func): + assert callable(func) + return func.__name__ diff --git a/src/examples/arg_types.py b/src/examples/arg_types.py index 1c06e1e..0390387 100644 --- a/src/examples/arg_types.py +++ b/src/examples/arg_types.py @@ -14,4 +14,4 @@ def run_docker(host, port): if __name__ == "__main__": - entry() \ No newline at end of file + entry() diff --git a/src/examples/command.py b/src/examples/command.py index bc1bacc..35a9057 100644 --- a/src/examples/command.py +++ b/src/examples/command.py @@ -15,6 +15,7 @@ def run_server(host, reload, port): ) ) + if __name__ == "__main__": from cmdtree import entry - entry() \ No newline at end of file + entry() diff --git a/src/examples/command_group.py b/src/examples/command_group.py index 228d266..c1db339 100644 --- a/src/examples/command_group.py +++ b/src/examples/command_group.py @@ -42,4 +42,4 @@ def image_create(ip, name): if __name__ == "__main__": - entry() \ No newline at end of file + entry() diff --git a/src/examples/global_argument.py b/src/examples/global_argument.py index b30a24e..bf66188 100644 --- a/src/examples/global_argument.py +++ b/src/examples/global_argument.py @@ -12,6 +12,6 @@ def run_server(host): ) ) -if __name__ == "__main__": - entry() \ No newline at end of file +if __name__ == "__main__": + entry() diff --git a/src/examples/low-level/command_from_path.py b/src/examples/low-level/command_from_path.py index 82cef13..96e0297 100644 --- a/src/examples/low-level/command_from_path.py +++ b/src/examples/low-level/command_from_path.py @@ -1,3 +1,6 @@ +import sys + +from cmdtree.parser import RawArgsParser from cmdtree.tree import CmdTree tree = CmdTree() @@ -20,13 +23,13 @@ def delete(disk_id): # get the parser in any place, any time tree.add_commands(["computer", "show"], show) -tree_node = tree.get_cmd_by_path(["computer", "show"]) +tree_node = tree.get_node_by_path(["computer", "show"]) show_parser = tree_node['cmd'] show_parser.argument("disk_id") -# Add delete command +# Add deleted command delete3 = tree.add_commands(["computer", "delete"], delete) delete3.argument("disk_id") # run your tree -tree.root.run() \ No newline at end of file +RawArgsParser(sys.argv, tree=tree).run() diff --git a/src/runtest.sh b/src/runtest.sh deleted file mode 100755 index b44edee..0000000 --- a/src/runtest.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -py.test --cov=cmdtree --cov-report=term-missing cmdtree diff --git a/src/setup.py b/src/setup.py index 051a79e..18bc8ce 100644 --- a/src/setup.py +++ b/src/setup.py @@ -5,7 +5,6 @@ HERE = os.path.abspath(os.path.dirname(__file__)) install_requires = ( - "argparse", "six>=1.10.0", ) diff --git a/src/test-requirements.txt b/src/test-requirements.txt index 6d8762c..6cef430 100644 --- a/src/test-requirements.txt +++ b/src/test-requirements.txt @@ -1,3 +1,4 @@ mock>=2.0.0 pytest>=2.9.0 -pytest-cov \ No newline at end of file +pytest-cov +flake8 diff --git a/src/cmdtree/tests/functional/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/cmdtree/tests/functional/__init__.py rename to src/tests/__init__.py diff --git a/src/cmdtree/tests/unittest/__init__.py b/src/tests/functional/__init__.py similarity index 100% rename from src/cmdtree/tests/unittest/__init__.py rename to src/tests/functional/__init__.py diff --git a/src/cmdtree/tests/functional/test_command.py b/src/tests/functional/test_command.py similarity index 98% rename from src/cmdtree/tests/functional/test_command.py rename to src/tests/functional/test_command.py index 86e43d0..7357ce1 100644 --- a/src/cmdtree/tests/functional/test_command.py +++ b/src/tests/functional/test_command.py @@ -1,4 +1,3 @@ -import pytest from cmdtree import INT, entry from cmdtree import command, argument, option @@ -59,4 +58,4 @@ def hello(feed, config): assert entry( ["test_order", "--feed", "fake"] - ) == "fake" \ No newline at end of file + ) == "fake" diff --git a/src/cmdtree/tests/functional/test_group.py b/src/tests/functional/test_group.py similarity index 88% rename from src/cmdtree/tests/functional/test_group.py rename to src/tests/functional/test_group.py index 46844f6..959d57c 100644 --- a/src/cmdtree/tests/functional/test_group.py +++ b/src/tests/functional/test_group.py @@ -1,7 +1,4 @@ -from cmdtree import Choices -from cmdtree import command from cmdtree import group, argument, entry -from cmdtree import option @group("docker") @@ -37,4 +34,3 @@ def test_nested_group_works(): assert entry( ["docker", "0.0.0.0", "image", "create", "test_image"] ) == ("0.0.0.0", "test_image") - diff --git a/src/tests/unittest/__init__.py b/src/tests/unittest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py new file mode 100644 index 0000000..ea88dd1 --- /dev/null +++ b/src/tests/unittest/test_parser.py @@ -0,0 +1,52 @@ +import pytest +import six + +from cmdtree.parser import CommandNode +from cmdtree.constants import ROOT_NODE_NAME + + +def mk_obj(property_dict): + class TestObject(object): + pass + obj = TestObject() + for key, value in six.iteritems(property_dict): + setattr(obj, key, value) + return obj + + +@pytest.fixture() +def cmd_node(): + return CommandNode( + cmd_path=[ROOT_NODE_NAME] + ) + + +@pytest.fixture() +def test_func(): + def func(): + return "result" + return func + + +@pytest.mark.parametrize( + "arg_name, expected", + ( + ("hello_world", "hello_world"), + ("hello-world", "hello_world"), + ("--hello-world", "hello_world"), + ("---hello-world", "hello_world"), + ("-hello-world", "hello_world"), + ) +) +def test_normalize_arg_name(arg_name, expected): + from cmdtree.parser import _normalize_arg_name + assert _normalize_arg_name(arg_name) == expected + + +class TestCmdNode: + def test_should_execute_func(self, test_func): + cmd_node = CommandNode( + cmd_path=[ROOT_NODE_NAME, ], + func=test_func, + ) + assert cmd_node.run({}) == "result" diff --git a/src/cmdtree/tests/unittest/test_registry.py b/src/tests/unittest/test_registry.py similarity index 88% rename from src/cmdtree/tests/unittest/test_registry.py rename to src/tests/unittest/test_registry.py index e8613bf..4ff1e3a 100644 --- a/src/cmdtree/tests/unittest/test_registry.py +++ b/src/tests/unittest/test_registry.py @@ -4,4 +4,4 @@ def test_get_tree_always_get_the_same_one(): tree1 = env.tree tree2 = env.tree assert isinstance(tree1, CmdTree) - assert tree1 is tree2 \ No newline at end of file + assert tree1 is tree2 diff --git a/src/cmdtree/tests/unittest/test_shortcuts.py b/src/tests/unittest/test_shortcuts.py similarity index 69% rename from src/cmdtree/tests/unittest/test_shortcuts.py rename to src/tests/unittest/test_shortcuts.py index 966705f..1187f8d 100644 --- a/src/cmdtree/tests/unittest/test_shortcuts.py +++ b/src/tests/unittest/test_shortcuts.py @@ -1,7 +1,8 @@ import mock import pytest -from cmdtree import shortcuts +from cmdtree import utils +from cmdtree import proxy @pytest.fixture() @@ -20,12 +21,12 @@ def mocked_parser(): @pytest.fixture() def parser_proxy(): - return shortcuts.ParserProxy() + return proxy.ParserProxy() @pytest.fixture() def group(mocked_parser, do_nothing): - return shortcuts.Group( + return proxy.Group( do_nothing, "do_nothing", mocked_parser, @@ -35,7 +36,7 @@ def group(mocked_parser, do_nothing): @pytest.fixture() def cmd(mocked_parser, do_nothing): - return shortcuts.Cmd( + return proxy.Cmd( do_nothing, "do_nothing", mocked_parser, @@ -43,32 +44,10 @@ def cmd(mocked_parser, do_nothing): ) -@pytest.mark.parametrize( - "path_prefix, cmd_name, expected", - ( - ( - ("parent", "child"), - "execute", - ("parent", "child", "execute") - ), - ( - ["parent", "child"], - "execute", - ("parent", "child", "execute") - ), - (None, "execute", ("execute", )), - ) -) -def test_get_cmd_path(path_prefix, cmd_name, expected): - assert shortcuts._get_cmd_path( - path_prefix, cmd_name - ) == expected - - def test_should_apply2user_called_correctly(mocked_parser): option = mocked_parser.option = mock.Mock() argument = mocked_parser.argument = mock.Mock() - shortcuts._apply2parser( + proxy._apply2parser( [["cmd1", {}], ], [["cmd1", {}], ["cmd1", {}], ], mocked_parser @@ -80,7 +59,7 @@ def test_should_apply2user_called_correctly(mocked_parser): @pytest.mark.parametrize( "cmd_proxy, expected", ( - (shortcuts.CmdProxy(lambda x: x), True), + (proxy.CmdProxy(lambda x: x), True), (lambda x: x, False), ) ) @@ -88,9 +67,9 @@ def test_should_apply2parser_be_called_with_cmd_proxy( cmd_proxy, expected, mocked_parser, ): with mock.patch.object( - shortcuts, "_apply2parser" + proxy, "_apply2parser" ) as mocked_apply: - shortcuts.apply2parser(cmd_proxy, mocked_parser) + proxy.apply2parser(cmd_proxy, mocked_parser) assert mocked_apply.called is expected @@ -98,26 +77,26 @@ class TestMkGroup: def test_should_return_group_with_group(self, do_nothing): assert isinstance( - shortcuts._mk_group("hello")(do_nothing), - shortcuts.Group + proxy._mk_group("hello")(do_nothing), + proxy.Group ) def test_should_raise_value_error_if_group_inited( self, do_nothing, mocked_parser ): - group = shortcuts.Group(do_nothing, "test", mocked_parser) + group = proxy.Group(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - shortcuts._mk_group("test")(group) + proxy._mk_group("test")(group) def test_should_get_func_name_called_if_no_name_given( self, do_nothing ): with mock.patch.object( - shortcuts, "_get_func_name" + proxy, "get_func_name" ) as mocked_get_name: - shortcuts._mk_group(None)(do_nothing) + proxy._mk_group(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -125,10 +104,10 @@ def test_should_call_apply2parser_for_meta_cmd( ): with mock.patch.object( - shortcuts, "apply2parser", + proxy, "apply2parser", ) as apply2parser: - cmd_proxy = shortcuts.CmdProxy(do_nothing) - shortcuts._mk_group("name")(cmd_proxy) + cmd_proxy = proxy.CmdProxy(do_nothing) + proxy._mk_group("name")(cmd_proxy) assert apply2parser.called @@ -136,26 +115,26 @@ class TestMkCmd: def test_should_return_cmd_with_cmd(self, do_nothing): assert isinstance( - shortcuts._mk_cmd("hello")(do_nothing), - shortcuts.Cmd + proxy._mk_cmd("hello")(do_nothing), + proxy.Cmd ) def test_should_raise_value_error_if_cmd_inited( self, do_nothing, mocked_parser ): - cmd = shortcuts.Cmd(do_nothing, "test", mocked_parser) + cmd = proxy.Cmd(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - shortcuts._mk_cmd("test")(cmd) + proxy._mk_cmd("test")(cmd) def test_should_get_func_name_called_if_no_name_given( self, do_nothing ): with mock.patch.object( - shortcuts, "_get_func_name" + proxy, "get_func_name" ) as mocked_get_name: - shortcuts._mk_cmd(None)(do_nothing) + proxy._mk_cmd(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -163,15 +142,15 @@ def test_should_call_apply2parser_for_meta_cmd( ): with mock.patch.object( - shortcuts, "apply2parser", + proxy, "apply2parser", ) as apply2parser: - cmd_proxy = shortcuts.CmdProxy(do_nothing) - shortcuts._mk_cmd("name")(cmd_proxy) + cmd_proxy = proxy.CmdProxy(do_nothing) + proxy._mk_cmd("name")(cmd_proxy) assert apply2parser.called def test_cmd_meta_should_handle_none_value_of_path_to_tuple(): - cmd_meta = shortcuts.CmdMeta() + cmd_meta = proxy.CmdMeta() assert cmd_meta.full_path == tuple() @@ -198,7 +177,7 @@ def test_should_full_path_be_none_if_path_is_none(self, group): def test_should_command_call_mk_command(self, group): with mock.patch.object( - shortcuts, "_mk_cmd" + proxy, "_mk_cmd" ) as mocked_mk: group.command("name") mocked_mk.assert_called_with( @@ -209,7 +188,7 @@ def test_should_command_call_mk_command(self, group): def test_should_group_call_mk_group(self, group): with mock.patch.object( - shortcuts, "_mk_group" + proxy, "_mk_group" ) as mocked_mk: group.group("name") mocked_mk.assert_called_with( @@ -228,4 +207,4 @@ def test_should_full_path_be_none_if_path_is_none(self, cmd): def test_get_func_name(do_nothing): - assert shortcuts._get_func_name(do_nothing) == "func" \ No newline at end of file + assert utils.get_func_name(do_nothing) == "func" diff --git a/src/cmdtree/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py similarity index 83% rename from src/cmdtree/tests/unittest/test_tree.py rename to src/tests/unittest/test_tree.py index a77ddd8..559943a 100644 --- a/src/cmdtree/tests/unittest/test_tree.py +++ b/src/tests/unittest/test_tree.py @@ -30,7 +30,9 @@ def cmd_tree(mocked_resource): @pytest.fixture def mocked_resource(): - return mock.Mock() + root = mock.Mock() + root.is_root = True + return root @pytest.fixture @@ -56,8 +58,8 @@ def test_should_cmd_tree_add_node_create_right_index( self, cmd_tree, mocked_resource, cmd_node, cmd_node2 ): cmd_tree._add_node(cmd_node, ['new_cmd']) - assert cmd_tree.tree == { - "name": "root", + assert cmd_tree.root == { + "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": cmd_node} } @@ -65,8 +67,8 @@ def test_should_cmd_tree_add_node_create_right_index( cmd_tree._add_node(cmd_node2, ['new_cmd', 'child_cmd']) expected_cmd_node = deepcopy(cmd_node) expected_cmd_node['children']['child_cmd'] = cmd_node2 - assert cmd_tree.tree == { - "name": "root", + assert cmd_tree.root == { + "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": expected_cmd_node} } @@ -75,7 +77,7 @@ def test_should_cmd_tree_get_cmd_by_path_get_parent( self, cmd_tree_with_tree, cmd_node, cmd_node2 ): tree = cmd_tree_with_tree - ret = tree.get_cmd_by_path(['new_cmd']) + ret = tree.get_node_by_path(['new_cmd']) expected_cmd_node = deepcopy(cmd_node) expected_cmd_node['children']['child_cmd'] = cmd_node2 assert ret == expected_cmd_node @@ -84,7 +86,7 @@ def test_should_cmd_tree_get_cmd_by_path_get_child( self, cmd_tree_with_tree, cmd_node2 ): tree = cmd_tree_with_tree - ret = tree.get_cmd_by_path(['new_cmd', 'child_cmd']) + ret = tree.get_node_by_path(['new_cmd', 'child_cmd']) assert ret == cmd_node2 @pytest.mark.parametrize( @@ -122,23 +124,21 @@ def test_should_cmd_tree_add_parent_commands_return_the_last( cmd_tree ): cmd_tree.add_parent_commands(['new_cmd', 'hello']) - assert "hello" in \ - cmd_tree.tree['children']['new_cmd']['children'] - assert {} == \ - cmd_tree.tree['children']['new_cmd']['children']["hello"]['children'] + assert "hello" in cmd_tree.root['children']['new_cmd']['children'] + assert cmd_tree.root['children']['new_cmd']['children']["hello"]['children'] == {} def test_should_cmd_tree_get_cmd_by_path_got_obj( self, cmd_tree_with_tree ): - assert cmd_tree_with_tree.get_cmd_by_path(['new_cmd']) is not None - assert cmd_tree_with_tree.get_cmd_by_path( + assert cmd_tree_with_tree.get_node_by_path(['new_cmd']) is not None + assert cmd_tree_with_tree.get_node_by_path( ['new_cmd', "child_cmd"]) is not None with pytest.raises(ValueError) as excinfo: - cmd_tree_with_tree.get_cmd_by_path(['new_cmd', "fuck"]) + cmd_tree_with_tree.get_node_by_path(['new_cmd', "fuck"]) msg = "Given key [fuck] in path ['new_cmd', 'fuck'] does not exist in tree." assert str(excinfo.value) == msg def test_should_add_parent_cmd_not_repeat_add(self, cmd_tree_with_tree): orig_node = cmd_tree_with_tree.add_parent_commands(['test_nested', 'child']) new_node = cmd_tree_with_tree.add_parent_commands(['test_nested', 'child']) - assert id(orig_node['cmd']) == id(new_node['cmd']) \ No newline at end of file + assert id(orig_node['cmd']) == id(new_node['cmd']) diff --git a/src/cmdtree/tests/unittest/test_types.py b/src/tests/unittest/test_types.py similarity index 99% rename from src/cmdtree/tests/unittest/test_types.py rename to src/tests/unittest/test_types.py index 3310711..bcda6cb 100644 --- a/src/cmdtree/tests/unittest/test_types.py +++ b/src/tests/unittest/test_types.py @@ -218,4 +218,4 @@ def test_should_return_keyword_argument(self, choices, type_): assert instance() == { "type": type_ or instance.convert, "choices": choices - } \ No newline at end of file + } diff --git a/src/tests/unittest/test_utils.py b/src/tests/unittest/test_utils.py new file mode 100644 index 0000000..a7ce4a0 --- /dev/null +++ b/src/tests/unittest/test_utils.py @@ -0,0 +1,25 @@ +import pytest + +from cmdtree import utils + + +@pytest.mark.parametrize( + "path_prefix, cmd_name, expected", + ( + ( + ("parent", "child"), + "execute", + ("parent", "child", "execute") + ), + ( + ["parent", "child"], + "execute", + ("parent", "child", "execute") + ), + (None, "execute", ("execute", )), + ) +) +def test_get_cmd_path(path_prefix, cmd_name, expected): + assert utils.get_cmd_path( + path_prefix, cmd_name + ) == expected