From bc673757ac94a0f29196a0e901f9db65505744e8 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 15 Dec 2017 10:13:33 +0000 Subject: [PATCH 01/32] Feature: Refactor with own command-arg parser The basic version works now + Unittest + More work on exception message + help data format + Command Group validation --- doc/ParserFlow.md | 15 + src/cmdtree/__init__.py | 4 +- src/cmdtree/arg_parser.py | 62 ++++ src/cmdtree/echo.py | 10 + src/cmdtree/exceptions.py | 40 ++- src/cmdtree/parser.py | 291 +++++++++++++++---- src/cmdtree/proxy.py | 196 +++++++++++++ src/cmdtree/registry.py | 14 +- src/cmdtree/shortcuts.py | 209 +------------ src/cmdtree/templates.py | 3 + src/cmdtree/tests/unittest/test_parser.py | 8 +- src/cmdtree/tests/unittest/test_shortcuts.py | 48 +-- src/cmdtree/tests/unittest/test_tree.py | 10 +- src/cmdtree/tree.py | 45 ++- src/cmdtree/types.py | 4 +- src/cmdtree/utils.py | 11 + src/examples/low-level/command_from_path.py | 2 +- 17 files changed, 663 insertions(+), 309 deletions(-) create mode 100644 doc/ParserFlow.md create mode 100644 src/cmdtree/arg_parser.py create mode 100644 src/cmdtree/echo.py create mode 100644 src/cmdtree/proxy.py create mode 100644 src/cmdtree/templates.py create mode 100644 src/cmdtree/utils.py 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/cmdtree/__init__.py b/src/cmdtree/__init__.py index c257f94..f980afa 100644 --- a/src/cmdtree/__init__.py +++ b/src/cmdtree/__init__.py @@ -1,4 +1,4 @@ -from cmdtree.parser import AParser +from cmdtree.parser import CommandNode from cmdtree.registry import env from cmdtree.shortcuts import ( argument, @@ -20,6 +20,6 @@ ) # globals and entry point -env.parser = AParser() +# env.parser = CommandNode() entry = env.entry diff --git a/src/cmdtree/arg_parser.py b/src/cmdtree/arg_parser.py new file mode 100644 index 0000000..3167a25 --- /dev/null +++ b/src/cmdtree/arg_parser.py @@ -0,0 +1,62 @@ +from copy import deepcopy +import sys + +from cmdtree.exceptions import NodeDoesExist, NoSuchCommand +from cmdtree.tree import get_help + + +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 = [] + + @staticmethod + def parse2cmd(raw_args, tree): + cmd_nodes = [] + full_cmd_path = [] + left_args = deepcopy(raw_args) + cmd_start_index = 0 + 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: + raise NoSuchCommand( + "Command %s does not exist." + % str( + full_cmd_path[0] + if full_cmd_path + else sys.argv[0] + ) + ) + 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 + + 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 cmd.callable(): + cmd.run(kwargs) + get_help(node) + diff --git a/src/cmdtree/echo.py b/src/cmdtree/echo.py new file mode 100644 index 0000000..3b20877 --- /dev/null +++ b/src/cmdtree/echo.py @@ -0,0 +1,10 @@ +import sys + + +def error(error_msg): + sys.stderr.write(error_msg) + sys.stderr.write("\n") + + +def format_list(the_list): + return "\n".join(str(ele) for ele in the_list) \ No newline at end of file diff --git a/src/cmdtree/exceptions.py b/src/cmdtree/exceptions.py index 328f862..b3ec31b 100644 --- a/src/cmdtree/exceptions.py +++ b/src/cmdtree/exceptions.py @@ -1,2 +1,38 @@ -class ArgumentParseError(ValueError): - pass \ No newline at end of file +class ParserError(ValueError): + pass + + +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/parser.py b/src/cmdtree/parser.py index 18a6be4..d22131c 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -1,16 +1,44 @@ -from argparse import ArgumentParser import sys import six -from cmdtree.exceptions import ArgumentParseError +from cmdtree.echo import error +from cmdtree.exceptions import ( + ParserError, ArgumentRepeatedRegister, + ArgumentTypeError, + ArgumentError, + OptionError +) 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 = [] + prev = name_list[0] + for ele in name_list: + if prev == ele == "-": + continue + new_name_list.append(ele) + arg_name = "".join(new_name_list) return arg_name.replace("-", "_") +def _assert_type_valid(name, type_): + if not isinstance(type_, ParamTypeFactory): + raise ArgumentTypeError( + ( + "Invalid type of argument {}, " + "should be instance of {}" + ).format( + name, + ParamTypeFactory, + ) + ) + + def vars_(object=None): """ Clean all of the property starts with "_" then @@ -25,80 +53,241 @@ def vars_(object=None): return filtered_vars -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 ParserTypes(object): + LEAF = "leaf" + ROOT = "root" - 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', + +class Argument(object): + def __init__(self, name, type_, help=None): + self.name = name + self.type = type_ + self.help = help + + 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) + ) + raise ArgumentError( + msg ) - 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) + @property + def num_args(self): + return len(self.arg_names) - if _func: - return args._func(**vars_(args)) - else: - raise ValueError( - "No function binding for args `{args}`".format( - args=args + 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 + + @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, name, help=None, func=None): + self.name = name + self.help = help + self.arg_mgr = ArgumentMgr() + self.opt_mgr = OptionMgr() + self.func = func + + @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 + ) + 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 + ) + index += 1 + continue + count += 1 + if count > self.arg_mgr.num_args: + break + self.arg_mgr.add_value( + count - 1, + value=current_arg + ) + self.arg_mgr.assert_filled() + 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, + ) diff --git a/src/cmdtree/proxy.py b/src/cmdtree/proxy.py new file mode 100644 index 0000000..a1fc0de --- /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 \ No newline at end of file diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index aea98d6..7fe6e96 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.arg_parser import RawArgsParser + + if args is None: + args = sys.argv[1:] + parser = RawArgsParser(args, self.tree) + parser.run() @property def tree(self): diff --git a/src/cmdtree/shortcuts.py b/src/cmdtree/shortcuts.py index 33db35e..e52db30 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): diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py new file mode 100644 index 0000000..0b9b3ea --- /dev/null +++ b/src/cmdtree/templates.py @@ -0,0 +1,3 @@ +E_MISSING_ARGUMENT = """ +Missing positional arguments: {args} +""" \ No newline at end of file diff --git a/src/cmdtree/tests/unittest/test_parser.py b/src/cmdtree/tests/unittest/test_parser.py index 83c499f..6daeeeb 100644 --- a/src/cmdtree/tests/unittest/test_parser.py +++ b/src/cmdtree/tests/unittest/test_parser.py @@ -3,7 +3,7 @@ import six from cmdtree import parser -from cmdtree.exceptions import ArgumentParseError +from cmdtree.exceptions import ParserError def mk_obj(property_dict): @@ -17,8 +17,8 @@ class TestObject(object): @pytest.fixture() def aparser(): - from cmdtree.parser import AParser - return AParser() + from cmdtree.parser import CommandNode + return CommandNode() @pytest.fixture() @@ -84,7 +84,7 @@ def test_should_execute_without_func(self, cmd_func, exception, aparser): @pytest.mark.parametrize( "silent_exit, exception", ( - (False, ArgumentParseError), + (False, ParserError), (True, SystemExit) ) ) diff --git a/src/cmdtree/tests/unittest/test_shortcuts.py b/src/cmdtree/tests/unittest/test_shortcuts.py index 966705f..60fa901 100644 --- a/src/cmdtree/tests/unittest/test_shortcuts.py +++ b/src/cmdtree/tests/unittest/test_shortcuts.py @@ -2,6 +2,8 @@ import pytest from cmdtree import shortcuts +import cmdtree.proxy +import cmdtree.utils @pytest.fixture() @@ -20,12 +22,12 @@ def mocked_parser(): @pytest.fixture() def parser_proxy(): - return shortcuts.ParserProxy() + return cmdtree.proxy.ParserProxy() @pytest.fixture() def group(mocked_parser, do_nothing): - return shortcuts.Group( + return cmdtree.proxy.Group( do_nothing, "do_nothing", mocked_parser, @@ -35,7 +37,7 @@ def group(mocked_parser, do_nothing): @pytest.fixture() def cmd(mocked_parser, do_nothing): - return shortcuts.Cmd( + return cmdtree.proxy.Cmd( do_nothing, "do_nothing", mocked_parser, @@ -60,7 +62,7 @@ def cmd(mocked_parser, do_nothing): ) ) def test_get_cmd_path(path_prefix, cmd_name, expected): - assert shortcuts._get_cmd_path( + assert cmdtree.utils._get_cmd_path( path_prefix, cmd_name ) == expected @@ -68,7 +70,7 @@ def test_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( + cmdtree.proxy._apply2parser( [["cmd1", {}], ], [["cmd1", {}], ["cmd1", {}], ], mocked_parser @@ -80,7 +82,7 @@ def test_should_apply2user_called_correctly(mocked_parser): @pytest.mark.parametrize( "cmd_proxy, expected", ( - (shortcuts.CmdProxy(lambda x: x), True), + (cmdtree.proxy.CmdProxy(lambda x: x), True), (lambda x: x, False), ) ) @@ -90,7 +92,7 @@ def test_should_apply2parser_be_called_with_cmd_proxy( with mock.patch.object( shortcuts, "_apply2parser" ) as mocked_apply: - shortcuts.apply2parser(cmd_proxy, mocked_parser) + cmdtree.proxy.apply2parser(cmd_proxy, mocked_parser) assert mocked_apply.called is expected @@ -98,18 +100,18 @@ class TestMkGroup: def test_should_return_group_with_group(self, do_nothing): assert isinstance( - shortcuts._mk_group("hello")(do_nothing), - shortcuts.Group + cmdtree.proxy._mk_group("hello")(do_nothing), + cmdtree.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 = cmdtree.proxy.Group(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - shortcuts._mk_group("test")(group) + cmdtree.proxy._mk_group("test")(group) def test_should_get_func_name_called_if_no_name_given( self, do_nothing @@ -117,7 +119,7 @@ def test_should_get_func_name_called_if_no_name_given( with mock.patch.object( shortcuts, "_get_func_name" ) as mocked_get_name: - shortcuts._mk_group(None)(do_nothing) + cmdtree.proxy._mk_group(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -127,8 +129,8 @@ def test_should_call_apply2parser_for_meta_cmd( with mock.patch.object( shortcuts, "apply2parser", ) as apply2parser: - cmd_proxy = shortcuts.CmdProxy(do_nothing) - shortcuts._mk_group("name")(cmd_proxy) + cmd_proxy = cmdtree.proxy.CmdProxy(do_nothing) + cmdtree.proxy._mk_group("name")(cmd_proxy) assert apply2parser.called @@ -136,18 +138,18 @@ class TestMkCmd: def test_should_return_cmd_with_cmd(self, do_nothing): assert isinstance( - shortcuts._mk_cmd("hello")(do_nothing), - shortcuts.Cmd + cmdtree.proxy._mk_cmd("hello")(do_nothing), + cmdtree.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 = cmdtree.proxy.Cmd(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - shortcuts._mk_cmd("test")(cmd) + cmdtree.proxy._mk_cmd("test")(cmd) def test_should_get_func_name_called_if_no_name_given( self, do_nothing @@ -155,7 +157,7 @@ def test_should_get_func_name_called_if_no_name_given( with mock.patch.object( shortcuts, "_get_func_name" ) as mocked_get_name: - shortcuts._mk_cmd(None)(do_nothing) + cmdtree.proxy._mk_cmd(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -165,13 +167,13 @@ def test_should_call_apply2parser_for_meta_cmd( with mock.patch.object( shortcuts, "apply2parser", ) as apply2parser: - cmd_proxy = shortcuts.CmdProxy(do_nothing) - shortcuts._mk_cmd("name")(cmd_proxy) + cmd_proxy = cmdtree.proxy.CmdProxy(do_nothing) + cmdtree.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 = cmdtree.proxy.CmdMeta() assert cmd_meta.full_path == tuple() @@ -228,4 +230,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 cmdtree.utils._get_func_name(do_nothing) == "func" \ No newline at end of file diff --git a/src/cmdtree/tests/unittest/test_tree.py b/src/cmdtree/tests/unittest/test_tree.py index a77ddd8..16b856c 100644 --- a/src/cmdtree/tests/unittest/test_tree.py +++ b/src/cmdtree/tests/unittest/test_tree.py @@ -75,7 +75,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 +84,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( @@ -130,11 +130,11 @@ def test_should_cmd_tree_add_parent_commands_return_the_last( 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 diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 3f33358..70a8bed 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -1,4 +1,7 @@ -from cmdtree.parser import AParser +from cmdtree import echo +from cmdtree.echo import format_list +from cmdtree.exceptions import NodeDoesExist +from cmdtree.parser import CommandNode def _mk_cmd_node(cmd_name, cmd_obj): @@ -9,6 +12,24 @@ def _mk_cmd_node(cmd_name, cmd_obj): } +_NO_CMD_GIVEN_TPL = """ +No command given, please select from commands below: +{ + %s +} +""" + + +def get_help(node): + if not node['cmd'].callable(): + sub_cmds = [ + node['name'] for node in node['children'].values() + ] + return echo.error( + _NO_CMD_GIVEN_TPL % format_list(sub_cmds) + ) + + class CmdTree(object): """ A tree that manages the command references by cmd path like @@ -17,12 +38,12 @@ 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 else: - self.root = AParser() + self.root = CommandNode("root") self.tree = { "name": "root", "cmd": self.root, @@ -30,6 +51,13 @@ def __init__(self, root_parser=None): } 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: { @@ -39,11 +67,13 @@ def get_cmd_by_path(self, existed_cmd_path): } """ parent = self.tree + if len(existed_cmd_path) == 0: + return self.tree 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) ) @@ -71,8 +101,7 @@ def _get_paths(full_path, end_index): 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) + sub_command = CommandNode(name=cmd_name, func=func, help=help) node = _mk_cmd_node(cmd_name, sub_command) self._add_node(node, cmd_path=cmd_path) return sub_command @@ -88,7 +117,7 @@ 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) @@ -96,7 +125,7 @@ def add_parent_commands(self, cmd_path, help=None): for cmd_name in new_path: if last_one_index >= new_path_len: _kwargs['help'] = help - sub_cmd = parent_node['cmd'].add_cmd( + sub_cmd = CommandNode( cmd_name, **_kwargs ) parent_node = _mk_cmd_node(cmd_name, sub_cmd) diff --git a/src/cmdtree/types.py b/src/cmdtree/types.py index 0c48c1a..5cea4bd 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): diff --git a/src/cmdtree/utils.py b/src/cmdtree/utils.py new file mode 100644 index 0000000..afe00e3 --- /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__ \ No newline at end of file diff --git a/src/examples/low-level/command_from_path.py b/src/examples/low-level/command_from_path.py index 82cef13..47bd2b8 100644 --- a/src/examples/low-level/command_from_path.py +++ b/src/examples/low-level/command_from_path.py @@ -20,7 +20,7 @@ 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") From 92745516f7b515895a092e9f2959235f04f33160 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Dec 2017 15:15:40 +0000 Subject: [PATCH 02/32] Fix: Never display help message if function call success --- src/cmdtree/arg_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmdtree/arg_parser.py b/src/cmdtree/arg_parser.py index 3167a25..947df97 100644 --- a/src/cmdtree/arg_parser.py +++ b/src/cmdtree/arg_parser.py @@ -57,6 +57,6 @@ def run(self): node = self.cmd_nodes[-1] cmd = node['cmd'] if cmd.callable(): - cmd.run(kwargs) + return cmd.run(kwargs) get_help(node) From 90140952aabba322cd97d24494755f532138477b Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 07:14:22 +0000 Subject: [PATCH 03/32] Feature: Move all tests out of package --- src/{cmdtree => }/tests/__init__.py | 0 src/{cmdtree => }/tests/functional/__init__.py | 0 src/{cmdtree => }/tests/functional/test_command.py | 0 src/{cmdtree => }/tests/functional/test_group.py | 0 src/{cmdtree => }/tests/unittest/__init__.py | 0 src/{cmdtree => }/tests/unittest/test_parser.py | 0 src/{cmdtree => }/tests/unittest/test_registry.py | 0 src/{cmdtree => }/tests/unittest/test_shortcuts.py | 0 src/{cmdtree => }/tests/unittest/test_tree.py | 0 src/{cmdtree => }/tests/unittest/test_types.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename src/{cmdtree => }/tests/__init__.py (100%) rename src/{cmdtree => }/tests/functional/__init__.py (100%) rename src/{cmdtree => }/tests/functional/test_command.py (100%) rename src/{cmdtree => }/tests/functional/test_group.py (100%) rename src/{cmdtree => }/tests/unittest/__init__.py (100%) rename src/{cmdtree => }/tests/unittest/test_parser.py (100%) rename src/{cmdtree => }/tests/unittest/test_registry.py (100%) rename src/{cmdtree => }/tests/unittest/test_shortcuts.py (100%) rename src/{cmdtree => }/tests/unittest/test_tree.py (100%) rename src/{cmdtree => }/tests/unittest/test_types.py (100%) diff --git a/src/cmdtree/tests/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/cmdtree/tests/__init__.py rename to src/tests/__init__.py diff --git a/src/cmdtree/tests/functional/__init__.py b/src/tests/functional/__init__.py similarity index 100% rename from src/cmdtree/tests/functional/__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 100% rename from src/cmdtree/tests/functional/test_command.py rename to src/tests/functional/test_command.py diff --git a/src/cmdtree/tests/functional/test_group.py b/src/tests/functional/test_group.py similarity index 100% rename from src/cmdtree/tests/functional/test_group.py rename to src/tests/functional/test_group.py diff --git a/src/cmdtree/tests/unittest/__init__.py b/src/tests/unittest/__init__.py similarity index 100% rename from src/cmdtree/tests/unittest/__init__.py rename to src/tests/unittest/__init__.py diff --git a/src/cmdtree/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py similarity index 100% rename from src/cmdtree/tests/unittest/test_parser.py rename to src/tests/unittest/test_parser.py diff --git a/src/cmdtree/tests/unittest/test_registry.py b/src/tests/unittest/test_registry.py similarity index 100% rename from src/cmdtree/tests/unittest/test_registry.py rename to src/tests/unittest/test_registry.py diff --git a/src/cmdtree/tests/unittest/test_shortcuts.py b/src/tests/unittest/test_shortcuts.py similarity index 100% rename from src/cmdtree/tests/unittest/test_shortcuts.py rename to src/tests/unittest/test_shortcuts.py diff --git a/src/cmdtree/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py similarity index 100% rename from src/cmdtree/tests/unittest/test_tree.py rename to src/tests/unittest/test_tree.py diff --git a/src/cmdtree/tests/unittest/test_types.py b/src/tests/unittest/test_types.py similarity index 100% rename from src/cmdtree/tests/unittest/test_types.py rename to src/tests/unittest/test_types.py From 983010e504089b8ada1e05a694e8cc35262db4d7 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 07:30:44 +0000 Subject: [PATCH 04/32] Refactor: Make root-node-name as constants --- src/cmdtree/constants.py | 1 + src/cmdtree/tree.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 src/cmdtree/constants.py 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/tree.py b/src/cmdtree/tree.py index 70a8bed..24da065 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -1,4 +1,5 @@ from cmdtree import echo +from cmdtree.constants import ROOT_NODE_NAME from cmdtree.echo import format_list from cmdtree.exceptions import NodeDoesExist from cmdtree.parser import CommandNode @@ -43,9 +44,9 @@ def __init__(self, root_parser=None): if root_parser is not None: self.root = root_parser else: - self.root = CommandNode("root") + self.root = CommandNode(name=ROOT_NODE_NAME) self.tree = { - "name": "root", + "name": ROOT_NODE_NAME, "cmd": self.root, "children": {} } From 8206e64802e9fac5409af8d04b2ec96086c06d93 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 07:31:48 +0000 Subject: [PATCH 05/32] Refactor: rename aparser to cmd_node --- src/tests/unittest/test_parser.py | 63 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index 6daeeeb..f50f1da 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -3,6 +3,7 @@ import six from cmdtree import parser +from cmdtree.constants import ROOT_NODE_NAME from cmdtree.exceptions import ParserError @@ -16,9 +17,11 @@ class TestObject(object): @pytest.fixture() -def aparser(): +def cmd_node(): from cmdtree.parser import CommandNode - return CommandNode() + return CommandNode( + name=ROOT_NODE_NAME + ) @pytest.fixture() @@ -55,15 +58,15 @@ def test_vars_should_return_right_dict(p_dict, expected): ) == expected -class TestAParser: - def test_should_execute_func(self, aparser, test_func): - aparser.add_cmd("test", func=test_func) - assert aparser.run(["test"]) == "result" +class Testcmd_node: + def test_should_execute_func(self, cmd_node, test_func): + cmd_node.add_cmd("test", func=test_func) + assert cmd_node.run(["test"]) == "result" - def test_should_execute_child_cmd(self, aparser, test_func): - parent = aparser.add_cmd("parent") + def test_should_execute_child_cmd(self, cmd_node, test_func): + parent = cmd_node.add_cmd("parent") parent.add_cmd("child", func=test_func) - assert aparser.run(['parent', 'child']) == "result" + assert cmd_node.run(['parent', 'child']) == "result" @pytest.mark.parametrize( "cmd_func, exception", @@ -72,14 +75,14 @@ def test_should_execute_child_cmd(self, aparser, test_func): (lambda *args, **kwargs: "str", None), ) ) - def test_should_execute_without_func(self, cmd_func, exception, aparser): - parent = aparser.add_cmd("parent") + def test_should_execute_without_func(self, cmd_func, exception, cmd_node): + parent = cmd_node.add_cmd("parent") parent.add_cmd("child", func=cmd_func) if exception is not None: with pytest.raises(exception): - aparser.run(['parent', 'child']) + cmd_node.run(['parent', 'child']) else: - assert aparser.run(['parent', 'child']) == "str" + assert cmd_node.run(['parent', 'child']) == "str" @pytest.mark.parametrize( "silent_exit, exception", @@ -88,13 +91,13 @@ def test_should_execute_without_func(self, cmd_func, exception, aparser): (True, SystemExit) ) ) - def test_should_parent_cmd_exit_or_raise_error(self, silent_exit, exception, test_func, aparser): + def test_should_parent_cmd_exit_or_raise_error(self, silent_exit, exception, test_func, cmd_node): from cmdtree.registry import env env.silent_exit = silent_exit - parent = aparser.add_cmd("parent") + parent = cmd_node.add_cmd("parent") parent.add_cmd("child", func=test_func) with pytest.raises(exception): - aparser.run(['parent']) + cmd_node.run(['parent']) @pytest.mark.parametrize( "arg_name, exception", @@ -104,8 +107,8 @@ def test_should_parent_cmd_exit_or_raise_error(self, silent_exit, exception, tes ('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) + def test_should_argument_starts_with_valid_string(self, arg_name, exception, test_func, cmd_node): + cmd = cmd_node.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): @@ -122,8 +125,8 @@ def test_should_argument_starts_with_valid_string(self, arg_name, exception, tes ('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) + def test_option_should_starts_with_hyphen(self, arg_name, expected_name, test_func, cmd_node): + cmd = cmd_node.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) @@ -135,8 +138,8 @@ def test_option_should_starts_with_hyphen(self, arg_name, expected_name, test_fu False, ) ) - def test_option_should_work_with_is_flag(self, is_flag, test_func, aparser): - cmd = aparser.add_cmd("execute", func=test_func) + def test_option_should_work_with_is_flag(self, is_flag, test_func, cmd_node): + cmd = cmd_node.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: @@ -151,8 +154,8 @@ def test_option_should_work_with_is_flag(self, is_flag, test_func, aparser): 1, ) ) - def test_option_should_work_with_default_value(self, default, aparser): - cmd = aparser.add_cmd("execute", func=test_func) + def test_option_should_work_with_default_value(self, default, cmd_node): + cmd = cmd_node.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: @@ -168,12 +171,12 @@ def test_option_should_work_with_default_value(self, default, aparser): ) ) def test_add_argument_work_with_type( - self, type_func, kwargs, aparser + self, type_func, kwargs, cmd_node ): 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) + with mock.patch.object(cmd_node, "add_argument") as mocked_add: + cmd_node.argument("name", type=type_func) if type_func is not None: assert type_func.called mocked_add.assert_called_with("name", **kwargs) @@ -186,12 +189,12 @@ def test_add_argument_work_with_type( ) ) def test_add_option_work_with_type( - self, type_func, kwargs, aparser + self, type_func, kwargs, cmd_node ): 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) + with mock.patch.object(cmd_node, "add_argument") as mocked_add: + cmd_node.option("name", type=type_func) if type_func is not None: assert type_func.called mocked_add.assert_called_with("--name", **kwargs) From 844f782acf51e78b4864ddf69ab7ec94850072e5 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 07:34:04 +0000 Subject: [PATCH 06/32] Refactor: Remove unused vars and parser-type code --- src/cmdtree/parser.py | 19 ------------------- src/tests/unittest/test_parser.py | 18 +++--------------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index d22131c..0f6013c 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -39,25 +39,6 @@ def _assert_type_valid(name, type_): ) -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 - - -class ParserTypes(object): - LEAF = "leaf" - ROOT = "root" - - class Argument(object): def __init__(self, name, type_, help=None): self.name = name diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index f50f1da..17127fd 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -36,6 +36,9 @@ def func(): ( ("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): @@ -43,21 +46,6 @@ def test_normalize_arg_name(arg_name, expected): 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 Testcmd_node: def test_should_execute_func(self, cmd_node, test_func): cmd_node.add_cmd("test", func=test_func) From 3028fa05468523a9b375290c3d30ea7d565b886a Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 07:55:29 +0000 Subject: [PATCH 07/32] Feature: Remove outdated parser tests / fix bug for normalize-func --- src/cmdtree/parser.py | 7 +- src/tests/unittest/test_parser.py | 147 +----------------------------- 2 files changed, 9 insertions(+), 145 deletions(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 0f6013c..7c875b4 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -17,9 +17,10 @@ def _normalize_arg_name(arg_name): name_list = list(arg_name) new_name_list = [] - prev = name_list[0] - for ele in name_list: - if prev == ele == "-": + 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) diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index 17127fd..28eda34 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -2,7 +2,7 @@ import pytest import six -from cmdtree import parser +from cmdtree import parser, CommandNode from cmdtree.constants import ROOT_NODE_NAME from cmdtree.exceptions import ParserError @@ -18,7 +18,6 @@ class TestObject(object): @pytest.fixture() def cmd_node(): - from cmdtree.parser import CommandNode return CommandNode( name=ROOT_NODE_NAME ) @@ -46,143 +45,7 @@ def test_normalize_arg_name(arg_name, expected): assert _normalize_arg_name(arg_name) == expected -class Testcmd_node: - def test_should_execute_func(self, cmd_node, test_func): - cmd_node.add_cmd("test", func=test_func) - assert cmd_node.run(["test"]) == "result" - - def test_should_execute_child_cmd(self, cmd_node, test_func): - parent = cmd_node.add_cmd("parent") - parent.add_cmd("child", func=test_func) - assert cmd_node.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, cmd_node): - parent = cmd_node.add_cmd("parent") - parent.add_cmd("child", func=cmd_func) - if exception is not None: - with pytest.raises(exception): - cmd_node.run(['parent', 'child']) - else: - assert cmd_node.run(['parent', 'child']) == "str" - - @pytest.mark.parametrize( - "silent_exit, exception", - ( - (False, ParserError), - (True, SystemExit) - ) - ) - def test_should_parent_cmd_exit_or_raise_error(self, silent_exit, exception, test_func, cmd_node): - from cmdtree.registry import env - env.silent_exit = silent_exit - parent = cmd_node.add_cmd("parent") - parent.add_cmd("child", func=test_func) - with pytest.raises(exception): - cmd_node.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, cmd_node): - cmd = cmd_node.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, cmd_node): - cmd = cmd_node.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, cmd_node): - cmd = cmd_node.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, cmd_node): - cmd = cmd_node.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, cmd_node - ): - if type_func is not None: - type_func.return_value = {"type": int} - with mock.patch.object(cmd_node, "add_argument") as mocked_add: - cmd_node.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, cmd_node - ): - if type_func is not None: - type_func.return_value = {"type": int} - with mock.patch.object(cmd_node, "add_argument") as mocked_add: - cmd_node.option("name", type=type_func) - if type_func is not None: - assert type_func.called - mocked_add.assert_called_with("--name", **kwargs) +class TestCmdNode: + def test_should_execute_func(self, test_func): + cmd_node = CommandNode(ROOT_NODE_NAME, func=test_func) + assert cmd_node.run({}) == "result" From 5bde166c1c8a1a1897f357a7d5a3eba1688e4d5f Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:10:06 +0000 Subject: [PATCH 08/32] Refactor: Move help function to tree object --- src/cmdtree/__init__.py | 2 - src/cmdtree/arg_parser.py | 62 ------------------------------- src/cmdtree/parser.py | 62 ++++++++++++++++++++++++++++++- src/cmdtree/registry.py | 2 +- src/cmdtree/templates.py | 7 ++++ src/cmdtree/tree.py | 29 ++++++--------- src/tests/unittest/test_parser.py | 4 +- 7 files changed, 80 insertions(+), 88 deletions(-) delete mode 100644 src/cmdtree/arg_parser.py diff --git a/src/cmdtree/__init__.py b/src/cmdtree/__init__.py index f980afa..4c6c88d 100644 --- a/src/cmdtree/__init__.py +++ b/src/cmdtree/__init__.py @@ -1,4 +1,3 @@ -from cmdtree.parser import CommandNode from cmdtree.registry import env from cmdtree.shortcuts import ( argument, @@ -20,6 +19,5 @@ ) # globals and entry point -# env.parser = CommandNode() entry = env.entry diff --git a/src/cmdtree/arg_parser.py b/src/cmdtree/arg_parser.py deleted file mode 100644 index 947df97..0000000 --- a/src/cmdtree/arg_parser.py +++ /dev/null @@ -1,62 +0,0 @@ -from copy import deepcopy -import sys - -from cmdtree.exceptions import NodeDoesExist, NoSuchCommand -from cmdtree.tree import get_help - - -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 = [] - - @staticmethod - def parse2cmd(raw_args, tree): - cmd_nodes = [] - full_cmd_path = [] - left_args = deepcopy(raw_args) - cmd_start_index = 0 - 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: - raise NoSuchCommand( - "Command %s does not exist." - % str( - full_cmd_path[0] - if full_cmd_path - else sys.argv[0] - ) - ) - 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 - - 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 cmd.callable(): - return cmd.run(kwargs) - get_help(node) - diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 7c875b4..213ca18 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -1,13 +1,15 @@ import sys -import six +from copy import deepcopy from cmdtree.echo import error from cmdtree.exceptions import ( ParserError, ArgumentRepeatedRegister, ArgumentTypeError, ArgumentError, - OptionError + OptionError, + NodeDoesExist, + NoSuchCommand ) from cmdtree.registry import env from cmdtree.templates import E_MISSING_ARGUMENT @@ -273,3 +275,59 @@ def option(self, name, help=None, is_flag=False, default=None, type=None): 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 = [] + + @staticmethod + def parse2cmd(raw_args, tree): + cmd_nodes = [] + full_cmd_path = [] + left_args = deepcopy(raw_args) + cmd_start_index = 0 + 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: + raise NoSuchCommand( + "Command %s does not exist." + % str( + full_cmd_path[0] + if full_cmd_path + else sys.argv[0] + ) + ) + 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 + + 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 cmd.callable(): + return cmd.run(kwargs) + self.tree.show_node_help(node) diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index 7fe6e96..a720402 100644 --- a/src/cmdtree/registry.py +++ b/src/cmdtree/registry.py @@ -16,7 +16,7 @@ def __init__(self): self._tree = None def entry(self, args=None): - from cmdtree.arg_parser import RawArgsParser + from cmdtree.parser import RawArgsParser if args is None: args = sys.argv[1:] diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py index 0b9b3ea..b5ffb7f 100644 --- a/src/cmdtree/templates.py +++ b/src/cmdtree/templates.py @@ -1,3 +1,10 @@ E_MISSING_ARGUMENT = """ Missing positional arguments: {args} +""" + +E_NO_CMD_GIVEN_TPL = """ +No command given, please select from commands below: +{ +%s +} """ \ No newline at end of file diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 24da065..12c35ed 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -3,6 +3,7 @@ from cmdtree.echo import format_list from cmdtree.exceptions import NodeDoesExist from cmdtree.parser import CommandNode +from cmdtree.templates import E_NO_CMD_GIVEN_TPL def _mk_cmd_node(cmd_name, cmd_obj): @@ -13,24 +14,6 @@ def _mk_cmd_node(cmd_name, cmd_obj): } -_NO_CMD_GIVEN_TPL = """ -No command given, please select from commands below: -{ - %s -} -""" - - -def get_help(node): - if not node['cmd'].callable(): - sub_cmds = [ - node['name'] for node in node['children'].values() - ] - return echo.error( - _NO_CMD_GIVEN_TPL % format_list(sub_cmds) - ) - - class CmdTree(object): """ A tree that manages the command references by cmd path like @@ -150,3 +133,13 @@ def index_in_tree(self, cmd_path): else: return cmd_path.index(key) return None + + @classmethod + def show_node_help(cls, node): + if not node['cmd'].callable(): + sub_cmds = [ + node['name'] for node in node['children'].values() + ] + return echo.error( + E_NO_CMD_GIVEN_TPL % format_list(sub_cmds) + ) diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index 28eda34..bb76793 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -1,10 +1,8 @@ -import mock import pytest import six -from cmdtree import parser, CommandNode +from cmdtree import CommandNode from cmdtree.constants import ROOT_NODE_NAME -from cmdtree.exceptions import ParserError def mk_obj(property_dict): From e76f54bc05d636e2547c6ba4a613132f7a01b79f Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:12:21 +0000 Subject: [PATCH 09/32] Fix: Fix reference error for CommandNode --- src/tests/unittest/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index bb76793..991ce9e 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -1,7 +1,7 @@ import pytest import six -from cmdtree import CommandNode +from cmdtree.parser import CommandNode from cmdtree.constants import ROOT_NODE_NAME From 108f08437b9a010710d8258f0530397e9ed062fd Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:24:40 +0000 Subject: [PATCH 10/32] Fix: Use correct comand for cmd-not-found error --- src/cmdtree/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 213ca18..e7d8cc3 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -303,7 +303,7 @@ def parse2cmd(raw_args, tree): raise NoSuchCommand( "Command %s does not exist." % str( - full_cmd_path[0] + cmd_path2find[-1] if full_cmd_path else sys.argv[0] ) From 43d7a05500ddae4cd932e0b4e84c6e5430895ab7 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:29:54 +0000 Subject: [PATCH 11/32] Refacto: Improve error-message for sub-commands --- src/cmdtree/templates.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py index b5ffb7f..4d5f1e8 100644 --- a/src/cmdtree/templates.py +++ b/src/cmdtree/templates.py @@ -2,8 +2,7 @@ Missing positional arguments: {args} """ -E_NO_CMD_GIVEN_TPL = """ -No command given, please select from commands below: +E_NO_CMD_GIVEN_TPL = """Invalid command choice, please select from commands below: { %s } From 65bc6554b032d7f74612338e91e7cffb05ad844b Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:43:14 +0000 Subject: [PATCH 12/32] Fix: Fix all unittest for parser --- src/cmdtree/proxy.py | 10 ++-- src/cmdtree/utils.py | 4 +- src/tests/unittest/test_shortcuts.py | 85 ++++++++++------------------ src/tests/unittest/test_utils.py | 25 ++++++++ 4 files changed, 63 insertions(+), 61 deletions(-) create mode 100644 src/tests/unittest/test_utils.py diff --git a/src/cmdtree/proxy.py b/src/cmdtree/proxy.py index a1fc0de..1b1f116 100644 --- a/src/cmdtree/proxy.py +++ b/src/cmdtree/proxy.py @@ -1,5 +1,5 @@ from cmdtree import env -from cmdtree.utils import _get_func_name, _get_cmd_path +from cmdtree.utils import get_func_name, get_cmd_path CMD_META_NAME = "meta" @@ -145,9 +145,9 @@ def wrapper(func): _func = func.func if name is None: - _name = _get_func_name(_func) + _name = get_func_name(_func) - full_path = _get_cmd_path(path_prefix, _name) + full_path = get_cmd_path(path_prefix, _name) tree = env.tree parser = tree.add_parent_commands(full_path, help=help)['cmd'] @@ -178,9 +178,9 @@ def wrapper(func): _name = name if name is None: - _name = _get_func_name(_func) + _name = get_func_name(_func) - full_path = _get_cmd_path(path_prefix, _name) + full_path = get_cmd_path(path_prefix, _name) tree = env.tree parser = tree.add_commands(full_path, _func, help=help) _cmd = Cmd( diff --git a/src/cmdtree/utils.py b/src/cmdtree/utils.py index afe00e3..1b8d760 100644 --- a/src/cmdtree/utils.py +++ b/src/cmdtree/utils.py @@ -1,4 +1,4 @@ -def _get_cmd_path(path_prefix, cmd_name): +def get_cmd_path(path_prefix, cmd_name): if path_prefix is None: full_path = (cmd_name, ) else: @@ -6,6 +6,6 @@ def _get_cmd_path(path_prefix, cmd_name): return full_path -def _get_func_name(func): +def get_func_name(func): assert callable(func) return func.__name__ \ No newline at end of file diff --git a/src/tests/unittest/test_shortcuts.py b/src/tests/unittest/test_shortcuts.py index 60fa901..8416f15 100644 --- a/src/tests/unittest/test_shortcuts.py +++ b/src/tests/unittest/test_shortcuts.py @@ -1,9 +1,8 @@ import mock import pytest -from cmdtree import shortcuts -import cmdtree.proxy -import cmdtree.utils +from cmdtree import utils +from cmdtree import proxy @pytest.fixture() @@ -22,12 +21,12 @@ def mocked_parser(): @pytest.fixture() def parser_proxy(): - return cmdtree.proxy.ParserProxy() + return proxy.ParserProxy() @pytest.fixture() def group(mocked_parser, do_nothing): - return cmdtree.proxy.Group( + return proxy.Group( do_nothing, "do_nothing", mocked_parser, @@ -37,7 +36,7 @@ def group(mocked_parser, do_nothing): @pytest.fixture() def cmd(mocked_parser, do_nothing): - return cmdtree.proxy.Cmd( + return proxy.Cmd( do_nothing, "do_nothing", mocked_parser, @@ -45,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 cmdtree.utils._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() - cmdtree.proxy._apply2parser( + proxy._apply2parser( [["cmd1", {}], ], [["cmd1", {}], ["cmd1", {}], ], mocked_parser @@ -82,7 +59,7 @@ def test_should_apply2user_called_correctly(mocked_parser): @pytest.mark.parametrize( "cmd_proxy, expected", ( - (cmdtree.proxy.CmdProxy(lambda x: x), True), + (proxy.CmdProxy(lambda x: x), True), (lambda x: x, False), ) ) @@ -90,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: - cmdtree.proxy.apply2parser(cmd_proxy, mocked_parser) + proxy.apply2parser(cmd_proxy, mocked_parser) assert mocked_apply.called is expected @@ -100,26 +77,26 @@ class TestMkGroup: def test_should_return_group_with_group(self, do_nothing): assert isinstance( - cmdtree.proxy._mk_group("hello")(do_nothing), - cmdtree.proxy.Group + proxy._mk_group("hello")(do_nothing), + proxy.Group ) def test_should_raise_value_error_if_group_inited( self, do_nothing, mocked_parser ): - group = cmdtree.proxy.Group(do_nothing, "test", mocked_parser) + group = proxy.Group(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - cmdtree.proxy._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: - cmdtree.proxy._mk_group(None)(do_nothing) + proxy._mk_group(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -127,10 +104,10 @@ def test_should_call_apply2parser_for_meta_cmd( ): with mock.patch.object( - shortcuts, "apply2parser", + proxy, "apply2parser", ) as apply2parser: - cmd_proxy = cmdtree.proxy.CmdProxy(do_nothing) - cmdtree.proxy._mk_group("name")(cmd_proxy) + cmd_proxy = proxy.CmdProxy(do_nothing) + proxy._mk_group("name")(cmd_proxy) assert apply2parser.called @@ -138,26 +115,26 @@ class TestMkCmd: def test_should_return_cmd_with_cmd(self, do_nothing): assert isinstance( - cmdtree.proxy._mk_cmd("hello")(do_nothing), - cmdtree.proxy.Cmd + proxy._mk_cmd("hello")(do_nothing), + proxy.Cmd ) def test_should_raise_value_error_if_cmd_inited( self, do_nothing, mocked_parser ): - cmd = cmdtree.proxy.Cmd(do_nothing, "test", mocked_parser) + cmd = proxy.Cmd(do_nothing, "test", mocked_parser) with pytest.raises(ValueError): - cmdtree.proxy._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: - cmdtree.proxy._mk_cmd(None)(do_nothing) + proxy._mk_cmd(None)(do_nothing) assert mocked_get_name.called def test_should_call_apply2parser_for_meta_cmd( @@ -165,15 +142,15 @@ def test_should_call_apply2parser_for_meta_cmd( ): with mock.patch.object( - shortcuts, "apply2parser", + proxy, "apply2parser", ) as apply2parser: - cmd_proxy = cmdtree.proxy.CmdProxy(do_nothing) - cmdtree.proxy._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 = cmdtree.proxy.CmdMeta() + cmd_meta = proxy.CmdMeta() assert cmd_meta.full_path == tuple() @@ -200,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( @@ -211,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( @@ -230,4 +207,4 @@ def test_should_full_path_be_none_if_path_is_none(self, cmd): def test_get_func_name(do_nothing): - assert cmdtree.utils._get_func_name(do_nothing) == "func" \ No newline at end of file + assert utils.get_func_name(do_nothing) == "func" \ 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 From e0d8a6f695b9f36625016808c0d8ccc3c9b9564a Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:50:48 +0000 Subject: [PATCH 13/32] Fix: index too large if positional argument detected --- src/cmdtree/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index e7d8cc3..ad76589 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -227,6 +227,7 @@ def parse_args(self, possible_args): continue count += 1 if count > self.arg_mgr.num_args: + index -= 1 break self.arg_mgr.add_value( count - 1, From d806f1e777e565640f5d0f63dca18997f7b6a1ad Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:52:42 +0000 Subject: [PATCH 14/32] Fix: Use new path for test command --- src/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Makefile b/src/Makefile index 03bb371..25f35be 100644 --- a/src/Makefile +++ b/src/Makefile @@ -8,7 +8,7 @@ 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 develop: python setup.py develop From 287f5536c404dd1182104b686642aadee5441772 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 08:54:23 +0000 Subject: [PATCH 15/32] Fix: return entry execution result in entry --- src/cmdtree/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index a720402..fbe7573 100644 --- a/src/cmdtree/registry.py +++ b/src/cmdtree/registry.py @@ -21,7 +21,7 @@ def entry(self, args=None): if args is None: args = sys.argv[1:] parser = RawArgsParser(args, self.tree) - parser.run() + return parser.run() @property def tree(self): From 24c53d1ad4ee41bb9e3779c3b94928c37494ab12 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 09:00:16 +0000 Subject: [PATCH 16/32] Fix: Remove unused argparse deps --- src/setup.py | 1 - 1 file changed, 1 deletion(-) 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", ) From e1d32ace00c98a559edf38aa3b76fe4a92928742 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Dec 2017 09:01:45 +0000 Subject: [PATCH 17/32] Fix: Try to fix "no tests found" error on travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c72244..deff2b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: - cd src - python setup.py install # command to run tests -script: py.test cmdtree --cov=cmdtree +script: + - make test after_success: - coveralls \ No newline at end of file From 4c06edaa86175001f122f620d33a1c40358e7eec Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Dec 2017 08:13:09 +0000 Subject: [PATCH 18/32] Feature: Catch and reprint error message --- src/cmdtree/parser.py | 14 ++++++++++---- src/cmdtree/tree.py | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index ad76589..96e2918 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -2,6 +2,7 @@ from copy import deepcopy +from cmdtree import echo from cmdtree.echo import error from cmdtree.exceptions import ( ParserError, ArgumentRepeatedRegister, @@ -321,7 +322,10 @@ def parse2cmd(raw_args, tree): return cmd_nodes, full_cmd_path def run(self): - self.cmd_nodes, cmd_path = self.parse2cmd(self.raw_args, self.tree) + try: + self.cmd_nodes, cmd_path = self.parse2cmd(self.raw_args, self.tree) + except ParserError as e: + echo.error(e.message) kwargs = {} for node in self.cmd_nodes: kwargs.update( @@ -329,6 +333,8 @@ def run(self): ) node = self.cmd_nodes[-1] cmd = node['cmd'] - if cmd.callable(): - return cmd.run(kwargs) - self.tree.show_node_help(node) + if not cmd.callable(): + self.tree.show_node_help(node) + return cmd.run(kwargs) + + diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 12c35ed..8dc5552 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -137,6 +137,10 @@ def index_in_tree(self, cmd_path): @classmethod def show_node_help(cls, node): if not node['cmd'].callable(): + if node['cmd'].help is not None: + echo.error( + node['cmd'].help + ) sub_cmds = [ node['name'] for node in node['children'].values() ] From 840820390cb80af69ec60e53ae08d49ea5d36b6b Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 2 Jan 2018 08:49:48 +0000 Subject: [PATCH 19/32] Feature: Remove unused runtest.sh --- src/runtest.sh | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 src/runtest.sh 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 From 4a7fe45db3b200b307d6158fa13a074ffb4d7cc7 Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 2 Jan 2018 10:46:05 +0000 Subject: [PATCH 20/32] Feature: Add help message for options/arguments/subcommands Some feature about sub-commands's help not ready now. --- src/__init__.py | 0 src/cmdtree/decorators.py | 16 ++++ src/cmdtree/echo.py | 4 - src/cmdtree/exceptions.py | 31 +++++++- src/cmdtree/format.py | 75 +++++++++++++++++++ src/cmdtree/parser.py | 83 ++++++++++++++++----- src/cmdtree/registry.py | 4 - src/cmdtree/templates.py | 9 +-- src/cmdtree/tree.py | 30 ++------ src/examples/low-level/command_from_path.py | 5 +- src/tests/unittest/test_tree.py | 4 +- 11 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/cmdtree/decorators.py create mode 100644 src/cmdtree/format.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cmdtree/decorators.py b/src/cmdtree/decorators.py new file mode 100644 index 0000000..f7727ac --- /dev/null +++ b/src/cmdtree/decorators.py @@ -0,0 +1,16 @@ +from functools import wraps + +from cmdtree import echo +from cmdtree.exceptions import ParserError + + +def format_error(func): + + @wraps(func) + def wrapped(*args, **kwargs): + try: + return func(*args, **kwargs) + except ParserError as e: + echo.error("Error: %s" % str(e.format_error())) + + return wrapped \ No newline at end of file diff --git a/src/cmdtree/echo.py b/src/cmdtree/echo.py index 3b20877..6973b82 100644 --- a/src/cmdtree/echo.py +++ b/src/cmdtree/echo.py @@ -4,7 +4,3 @@ def error(error_msg): sys.stderr.write(error_msg) sys.stderr.write("\n") - - -def format_list(the_list): - return "\n".join(str(ele) for ele in the_list) \ No newline at end of file diff --git a/src/cmdtree/exceptions.py b/src/cmdtree/exceptions.py index b3ec31b..b25c1d2 100644 --- a/src/cmdtree/exceptions.py +++ b/src/cmdtree/exceptions.py @@ -1,5 +1,34 @@ +from cmdtree.format import _format_cmd_choice + + class ParserError(ValueError): - pass + DEFAULT_TPL = ( + "{error}\n\n" + "{sub_cmd_help}" + "{cmd_ref}" + "{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 command '{name}':\n\n" + node_help = "" + _cmd_ref = "" + sub_cmd_help = sub_cmd_help or "" + if self.node is not None: + node_help = self.node.format_help() + _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): diff --git a/src/cmdtree/format.py b/src/cmdtree/format.py new file mode 100644 index 0000000..af551cc --- /dev/null +++ b/src/cmdtree/format.py @@ -0,0 +1,75 @@ +from textwrap import indent + +from cmdtree.templates import E_NO_CMD_GIVEN_TPL + + +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(cmd_node_list): + help_msg = "\n".join( + _format_cmd_help(ele) + for ele in cmd_node_list + ) + return E_NO_CMD_GIVEN_TPL % 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(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 96e2918..02bb018 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -12,6 +12,10 @@ NodeDoesExist, NoSuchCommand ) +from cmdtree.format import format_node_help +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 @@ -83,12 +87,10 @@ def assert_filled(self): if name not in self.parsed_values ] msg = E_MISSING_ARGUMENT.format( - args=" ".join(missed_args) - ) - raise ArgumentError( - msg + args=", ".join(missed_args) ) - + return False, msg + return True, None def add(self, name, type_=None, help=None): type_ = type_ or STRING @@ -106,6 +108,15 @@ def add(self, name, type_=None, help=None): help=help, ) + def format_help(self): + return format_arg_help( + "Positional arguments:", + tuple( + self.arg_map[name] + for name in self.arg_names + ), + ) + @property def num_args(self): return len(self.arg_names) @@ -156,6 +167,15 @@ def get_option_or_none(self, 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 = {} @@ -181,6 +201,18 @@ def __init__(self, name, help=None, func=None): 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 + if len(msg) == 0: + return "No help found now\n" + return msg + @property def kwargs(self): kwargs = {} @@ -207,7 +239,8 @@ def parse_args(self, possible_args): ) if option is None: raise OptionError( - "No such option '%s'" % current_arg + "No such option '%s'" % current_arg, + node=self, ) if option.is_flag: self.opt_mgr.add_value( @@ -222,7 +255,8 @@ def parse_args(self, possible_args): ) except IndexError: raise ArgumentError( - "No value for argument %s" % option.name + "No value for argument %s" % option.name, + node=self, ) index += 1 continue @@ -234,7 +268,12 @@ def parse_args(self, possible_args): count - 1, value=current_arg ) - self.arg_mgr.assert_filled() + 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 @@ -290,25 +329,29 @@ def __init__(self, args, tree): self.tree = tree self.cmd_nodes = [] - @staticmethod - def parse2cmd(raw_args, tree): + 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.tree raise NoSuchCommand( "Command %s does not exist." - % str( - cmd_path2find[-1] - if full_cmd_path - else sys.argv[0] - ) + % ( + 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:] @@ -321,11 +364,9 @@ def parse2cmd(raw_args, tree): break return cmd_nodes, full_cmd_path + @format_error def run(self): - try: - self.cmd_nodes, cmd_path = self.parse2cmd(self.raw_args, self.tree) - except ParserError as e: - echo.error(e.message) + self.cmd_nodes, cmd_path = self.parse2cmd(self.raw_args, self.tree) kwargs = {} for node in self.cmd_nodes: kwargs.update( @@ -334,7 +375,9 @@ def run(self): node = self.cmd_nodes[-1] cmd = node['cmd'] if not cmd.callable(): - self.tree.show_node_help(node) + echo.error( + format_node_help(node) + ) return cmd.run(kwargs) diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index fbe7573..9fcea78 100644 --- a/src/cmdtree/registry.py +++ b/src/cmdtree/registry.py @@ -33,8 +33,4 @@ 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 diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py index 4d5f1e8..8b6eef3 100644 --- a/src/cmdtree/templates.py +++ b/src/cmdtree/templates.py @@ -1,9 +1,6 @@ -E_MISSING_ARGUMENT = """ -Missing positional arguments: {args} -""" +E_MISSING_ARGUMENT = """Missing positional arguments: {args}""" -E_NO_CMD_GIVEN_TPL = """Invalid command choice, please select from commands below: +E_NO_CMD_GIVEN_TPL = """sub-commands: { %s -} -""" \ No newline at end of file +}""" diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 8dc5552..c77801a 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -1,9 +1,7 @@ -from cmdtree import echo -from cmdtree.constants import ROOT_NODE_NAME -from cmdtree.echo import format_list +import sys + from cmdtree.exceptions import NodeDoesExist from cmdtree.parser import CommandNode -from cmdtree.templates import E_NO_CMD_GIVEN_TPL def _mk_cmd_node(cmd_name, cmd_obj): @@ -25,12 +23,12 @@ def __init__(self, root_parser=None): :type root_parser: cmdtree.parser.CommandNode """ if root_parser is not None: - self.root = root_parser + root = root_parser else: - self.root = CommandNode(name=ROOT_NODE_NAME) + root = CommandNode(name=sys.argv[0]) self.tree = { - "name": ROOT_NODE_NAME, - "cmd": self.root, + "name": root.name, + "cmd": root, "children": {} } @@ -85,7 +83,7 @@ def _get_paths(full_path, end_index): def add_commands(self, cmd_path, func, help=None): cmd_name = cmd_path[-1] - sub_command = CommandNode(name=cmd_name, func=func, help=help) + sub_command = CommandNode(name=cmd_name, func=func, help=help) node = _mk_cmd_node(cmd_name, sub_command) self._add_node(node, cmd_path=cmd_path) return sub_command @@ -133,17 +131,3 @@ def index_in_tree(self, cmd_path): else: return cmd_path.index(key) return None - - @classmethod - def show_node_help(cls, node): - if not node['cmd'].callable(): - if node['cmd'].help is not None: - echo.error( - node['cmd'].help - ) - sub_cmds = [ - node['name'] for node in node['children'].values() - ] - return echo.error( - E_NO_CMD_GIVEN_TPL % format_list(sub_cmds) - ) diff --git a/src/examples/low-level/command_from_path.py b/src/examples/low-level/command_from_path.py index 47bd2b8..3655080 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() @@ -29,4 +32,4 @@ def delete(disk_id): 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/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py index 16b856c..b679e44 100644 --- a/src/tests/unittest/test_tree.py +++ b/src/tests/unittest/test_tree.py @@ -57,7 +57,7 @@ def test_should_cmd_tree_add_node_create_right_index( ): cmd_tree._add_node(cmd_node, ['new_cmd']) assert cmd_tree.tree == { - "name": "root", + "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": cmd_node} } @@ -66,7 +66,7 @@ def test_should_cmd_tree_add_node_create_right_index( expected_cmd_node = deepcopy(cmd_node) expected_cmd_node['children']['child_cmd'] = cmd_node2 assert cmd_tree.tree == { - "name": "root", + "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": expected_cmd_node} } From 82f09f4713e8fe329269783f79bf0517e7b76bbb Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 2 Jan 2018 10:59:35 +0000 Subject: [PATCH 21/32] Refactor: Add cmd_path for CommandNode --- src/cmdtree/parser.py | 3 ++- src/cmdtree/tree.py | 22 +++++++++++++++++----- src/tests/unittest/test_parser.py | 6 +++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 02bb018..2f2fcc6 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -194,8 +194,9 @@ class CommandNode(object): """ Arg-parse wrapper for sub command and convenient arg parse. """ - def __init__(self, name, help=None, func=None): + def __init__(self, name, cmd_path, help=None, func=None): self.name = name + self.cmd_path = cmd_path self.help = help self.arg_mgr = ArgumentMgr() self.opt_mgr = OptionMgr() diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index c77801a..0083d60 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -25,7 +25,10 @@ def __init__(self, root_parser=None): if root_parser is not None: root = root_parser else: - root = CommandNode(name=sys.argv[0]) + root = CommandNode( + name=sys.argv[0], + cmd_path=sys.argv[:1] + ) self.tree = { "name": root.name, "cmd": root, @@ -83,7 +86,12 @@ def _get_paths(full_path, end_index): def add_commands(self, cmd_path, func, help=None): cmd_name = cmd_path[-1] - sub_command = CommandNode(name=cmd_name, func=func, help=help) + sub_command = CommandNode( + name=cmd_name, + cmd_path=cmd_path, + func=func, + help=help + ) node = _mk_cmd_node(cmd_name, sub_command) self._add_node(node, cmd_path=cmd_path) return sub_command @@ -104,16 +112,20 @@ def add_parent_commands(self, cmd_path, help=None): last_one_index = 1 new_path_len = len(new_path) _kwargs = {} - for cmd_name in new_path: + for index, cmd_name 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 = CommandNode( - cmd_name, **_kwargs + name=cmd_name, + cmd_path=current_path, + **_kwargs ) parent_node = _mk_cmd_node(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 diff --git a/src/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index 991ce9e..8051291 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -45,5 +45,9 @@ def test_normalize_arg_name(arg_name, expected): class TestCmdNode: def test_should_execute_func(self, test_func): - cmd_node = CommandNode(ROOT_NODE_NAME, func=test_func) + cmd_node = CommandNode( + ROOT_NODE_NAME, + cmd_path=[ROOT_NODE_NAME, ], + func=test_func, + ) assert cmd_node.run({}) == "result" From 4c916fc397c6cd894b5c9bd56d68d0c88c855302 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 04:05:31 +0000 Subject: [PATCH 22/32] Refactor: Use cmd-path instead of cmd-name --- src/cmdtree/parser.py | 13 ++++++++----- src/cmdtree/tree.py | 20 ++++++++------------ src/tests/unittest/test_parser.py | 3 +-- src/tests/unittest/test_tree.py | 8 ++++---- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 2f2fcc6..654eccd 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -194,8 +194,8 @@ class CommandNode(object): """ Arg-parse wrapper for sub command and convenient arg parse. """ - def __init__(self, name, cmd_path, help=None, func=None): - self.name = name + def __init__(self, cmd_path, help=None, func=None): + self.name = cmd_path[-1] self.cmd_path = cmd_path self.help = help self.arg_mgr = ArgumentMgr() @@ -211,7 +211,7 @@ def format_help(self): if opt_help is not None: msg += "\n\n%s" % opt_help if len(msg) == 0: - return "No help found now\n" + return "No help found :)\n" return msg @property @@ -342,7 +342,7 @@ def parse2cmd(self, raw_args, tree): try: node = tree.get_node_by_path(cmd_path2find) except NodeDoesExist: - error_parent = node if node is not None else tree.tree + error_parent = node if node is not None else tree.root raise NoSuchCommand( "Command %s does not exist." % ( @@ -367,7 +367,10 @@ def parse2cmd(self, raw_args, tree): @format_error def run(self): - self.cmd_nodes, cmd_path = self.parse2cmd(self.raw_args, self.tree) + self.cmd_nodes, cmd_path = self.parse2cmd( + self.raw_args, + self.tree, + ) kwargs = {} for node in self.cmd_nodes: kwargs.update( diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index 0083d60..bad625f 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -26,10 +26,9 @@ def __init__(self, root_parser=None): root = root_parser else: root = CommandNode( - name=sys.argv[0], cmd_path=sys.argv[:1] ) - self.tree = { + self.root = { "name": root.name, "cmd": root, "children": {} @@ -51,9 +50,9 @@ def get_node_by_path(self, existed_cmd_path): "children": {} } """ - parent = self.tree + parent = self.root if len(existed_cmd_path) == 0: - return self.tree + return self.root for cmd_name in existed_cmd_path: try: parent = parent['children'][cmd_name] @@ -68,7 +67,7 @@ def _add_node(self, cmd_node, cmd_path): """ :type cmd_path: list or tuple """ - parent = self.tree + parent = self.root for cmd_key in cmd_path: if cmd_key not in parent['children']: break @@ -85,14 +84,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] sub_command = CommandNode( - name=cmd_name, cmd_path=cmd_path, func=func, help=help ) - node = _mk_cmd_node(cmd_name, sub_command) + node = _mk_cmd_node(sub_command.name, sub_command) self._add_node(node, cmd_path=cmd_path) return sub_command @@ -112,17 +109,16 @@ def add_parent_commands(self, cmd_path, help=None): last_one_index = 1 new_path_len = len(new_path) _kwargs = {} - for index, cmd_name in enumerate(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 = CommandNode( - name=cmd_name, 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, parent_path, @@ -136,7 +132,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/tests/unittest/test_parser.py b/src/tests/unittest/test_parser.py index 8051291..ea88dd1 100644 --- a/src/tests/unittest/test_parser.py +++ b/src/tests/unittest/test_parser.py @@ -17,7 +17,7 @@ class TestObject(object): @pytest.fixture() def cmd_node(): return CommandNode( - name=ROOT_NODE_NAME + cmd_path=[ROOT_NODE_NAME] ) @@ -46,7 +46,6 @@ def test_normalize_arg_name(arg_name, expected): class TestCmdNode: def test_should_execute_func(self, test_func): cmd_node = CommandNode( - ROOT_NODE_NAME, cmd_path=[ROOT_NODE_NAME, ], func=test_func, ) diff --git a/src/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py index b679e44..d6e89ce 100644 --- a/src/tests/unittest/test_tree.py +++ b/src/tests/unittest/test_tree.py @@ -56,7 +56,7 @@ 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 == { + assert cmd_tree.root == { "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": cmd_node} @@ -65,7 +65,7 @@ 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 == { + assert cmd_tree.root == { "name": mocked_resource.name, "cmd": mocked_resource, "children": {"new_cmd": expected_cmd_node} @@ -123,9 +123,9 @@ def test_should_cmd_tree_add_parent_commands_return_the_last( ): cmd_tree.add_parent_commands(['new_cmd', 'hello']) assert "hello" in \ - cmd_tree.tree['children']['new_cmd']['children'] + cmd_tree.root['children']['new_cmd']['children'] assert {} == \ - cmd_tree.tree['children']['new_cmd']['children']["hello"]['children'] + 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 From 46e68b270892e86dd618e7978608d606dd4eb4c3 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 04:09:22 +0000 Subject: [PATCH 23/32] Feature: Exit-code should be 1 if error occurs --- src/cmdtree/decorators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmdtree/decorators.py b/src/cmdtree/decorators.py index f7727ac..c902753 100644 --- a/src/cmdtree/decorators.py +++ b/src/cmdtree/decorators.py @@ -12,5 +12,6 @@ def wrapped(*args, **kwargs): return func(*args, **kwargs) except ParserError as e: echo.error("Error: %s" % str(e.format_error())) + exit(1) return wrapped \ No newline at end of file From 2850c905d6d071082b58703955aa270307bc02b6 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 04:43:00 +0000 Subject: [PATCH 24/32] Feature: Add sub-commands help for all command --- src/cmdtree/decorators.py | 8 +++++++- src/cmdtree/exceptions.py | 8 ++++---- src/cmdtree/format.py | 9 ++++++--- src/cmdtree/parser.py | 13 +++++++------ src/cmdtree/templates.py | 7 +++---- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/cmdtree/decorators.py b/src/cmdtree/decorators.py index c902753..11598c5 100644 --- a/src/cmdtree/decorators.py +++ b/src/cmdtree/decorators.py @@ -2,6 +2,7 @@ from cmdtree import echo from cmdtree.exceptions import ParserError +from cmdtree.format import format_node_help def format_error(func): @@ -11,7 +12,12 @@ def wrapped(*args, **kwargs): try: return func(*args, **kwargs) except ParserError as e: - echo.error("Error: %s" % str(e.format_error())) + assert hasattr(args[0], "tree") + node_dict = args[0].tree.get_node_by_path( + e.node.relative_path[1:] + ) + node_help = format_node_help(node_dict) + echo.error("Error: %s" % str(e.format_error(node_help))) exit(1) return wrapped \ No newline at end of file diff --git a/src/cmdtree/exceptions.py b/src/cmdtree/exceptions.py index b25c1d2..ac33323 100644 --- a/src/cmdtree/exceptions.py +++ b/src/cmdtree/exceptions.py @@ -1,12 +1,10 @@ -from cmdtree.format import _format_cmd_choice - class ParserError(ValueError): DEFAULT_TPL = ( "{error}\n\n" - "{sub_cmd_help}" "{cmd_ref}" "{help}" + "{sub_cmd_help}" ) def __init__(self, *args, **kwargs): @@ -17,9 +15,11 @@ def format_error(self, sub_cmd_help=None): cmd_ref = "Help message for command '{name}':\n\n" node_help = "" _cmd_ref = "" - sub_cmd_help = sub_cmd_help or "" + sub_cmd_help = sub_cmd_help + "\n" or "" 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 ) diff --git a/src/cmdtree/format.py b/src/cmdtree/format.py index af551cc..a1f2fdd 100644 --- a/src/cmdtree/format.py +++ b/src/cmdtree/format.py @@ -31,12 +31,15 @@ def _format_cmd_help(cmd_node): ) -def _format_cmd_choice(cmd_node_list): +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 % indent(help_msg, INDENT_1) + return E_NO_CMD_GIVEN_TPL.format( + name=parent_name, + cmds=indent(help_msg, INDENT_1) + ) def format_node_help(tree_node): @@ -53,7 +56,7 @@ def format_node_help(tree_node): for value in node['children'].values() ) if len(cmds) >= 0: - _help += _format_cmd_choice(cmds) + _help += _format_cmd_choice(node['name'], cmds) return _help if len(_help) > 0 else None diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 654eccd..7d2e507 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -10,7 +10,8 @@ ArgumentError, OptionError, NodeDoesExist, - NoSuchCommand + NoSuchCommand, + InvalidCommand ) from cmdtree.format import format_node_help from cmdtree.decorators import format_error @@ -196,7 +197,8 @@ class CommandNode(object): """ def __init__(self, cmd_path, help=None, func=None): self.name = cmd_path[-1] - self.cmd_path = cmd_path + self.abs_path = cmd_path + self.relative_path = cmd_path[1:] self.help = help self.arg_mgr = ArgumentMgr() self.opt_mgr = OptionMgr() @@ -210,8 +212,6 @@ def format_help(self): msg += "%s" % arg_help if opt_help is not None: msg += "\n\n%s" % opt_help - if len(msg) == 0: - return "No help found :)\n" return msg @property @@ -379,8 +379,9 @@ def run(self): node = self.cmd_nodes[-1] cmd = node['cmd'] if not cmd.callable(): - echo.error( - format_node_help(node) + raise InvalidCommand( + "Invalid command %s" % node['name'], + node=node['cmd'] ) return cmd.run(kwargs) diff --git a/src/cmdtree/templates.py b/src/cmdtree/templates.py index 8b6eef3..a4b0e1b 100644 --- a/src/cmdtree/templates.py +++ b/src/cmdtree/templates.py @@ -1,6 +1,5 @@ E_MISSING_ARGUMENT = """Missing positional arguments: {args}""" -E_NO_CMD_GIVEN_TPL = """sub-commands: -{ -%s -}""" +E_NO_CMD_GIVEN_TPL = """Sub-commands for {name}: +{cmds} +""" From 81a9db197d7db94e7d8589b51c3bba847046078c Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 05:00:51 +0000 Subject: [PATCH 25/32] Fix: Fix wrong parent node for command-group's help message --- src/cmdtree/decorators.py | 2 +- src/cmdtree/exceptions.py | 7 +++++-- src/cmdtree/parser.py | 11 ++++++----- src/cmdtree/tree.py | 4 +++- src/tests/unittest/test_tree.py | 4 +++- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/cmdtree/decorators.py b/src/cmdtree/decorators.py index 11598c5..4bb00c2 100644 --- a/src/cmdtree/decorators.py +++ b/src/cmdtree/decorators.py @@ -14,7 +14,7 @@ def wrapped(*args, **kwargs): except ParserError as e: assert hasattr(args[0], "tree") node_dict = args[0].tree.get_node_by_path( - e.node.relative_path[1:] + e.node.cmd_path ) node_help = format_node_help(node_dict) echo.error("Error: %s" % str(e.format_error(node_help))) diff --git a/src/cmdtree/exceptions.py b/src/cmdtree/exceptions.py index ac33323..8ab4809 100644 --- a/src/cmdtree/exceptions.py +++ b/src/cmdtree/exceptions.py @@ -12,10 +12,13 @@ def __init__(self, *args, **kwargs): self.node = kwargs.pop("node", None) def format_error(self, sub_cmd_help=None): - cmd_ref = "Help message for command '{name}':\n\n" + cmd_ref = "Help message for '{name}':\n\n" node_help = "" _cmd_ref = "" - sub_cmd_help = sub_cmd_help + "\n" or "" + 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: diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 7d2e507..1015a05 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -13,7 +13,6 @@ NoSuchCommand, InvalidCommand ) -from cmdtree.format import format_node_help from cmdtree.decorators import format_error from cmdtree.format import format_arg_help @@ -195,10 +194,12 @@ class CommandNode(object): """ Arg-parse wrapper for sub command and convenient arg parse. """ - def __init__(self, cmd_path, help=None, func=None): + def __init__(self, cmd_path, help=None, func=None, is_root=False): + self.is_root = is_root self.name = cmd_path[-1] - self.abs_path = cmd_path - self.relative_path = 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() @@ -380,7 +381,7 @@ def run(self): cmd = node['cmd'] if not cmd.callable(): raise InvalidCommand( - "Invalid command %s" % node['name'], + "%s is a command-group" % node['name'], node=node['cmd'] ) return cmd.run(kwargs) diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index bad625f..aa27bad 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -26,8 +26,10 @@ def __init__(self, root_parser=None): root = root_parser else: root = CommandNode( - cmd_path=sys.argv[:1] + cmd_path=sys.argv[:1], + is_root=True, ) + assert root.is_root is True self.root = { "name": root.name, "cmd": root, diff --git a/src/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py index d6e89ce..eb62638 100644 --- a/src/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 From fe6b3c617a0ee8835a41da9ff3968c28c6c2e9a9 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 05:15:42 +0000 Subject: [PATCH 26/32] Feature: Flake8 --- src/.flake8 | 2 ++ src/Makefile | 5 ++++- src/cmdtree/__init__.py | 15 +++++++++++++++ src/cmdtree/_compat.py | 3 ++- src/cmdtree/decorators.py | 2 +- src/cmdtree/parser.py | 5 +---- src/cmdtree/proxy.py | 2 +- src/cmdtree/registry.py | 3 ++- src/cmdtree/shortcuts.py | 2 +- src/cmdtree/types.py | 2 +- src/cmdtree/utils.py | 2 +- src/examples/arg_types.py | 2 +- src/examples/command.py | 3 ++- src/examples/command_group.py | 2 +- src/examples/global_argument.py | 4 ++-- src/test-requirements.txt | 3 ++- src/tests/functional/test_command.py | 3 +-- src/tests/functional/test_group.py | 4 ---- src/tests/unittest/test_registry.py | 2 +- src/tests/unittest/test_shortcuts.py | 2 +- src/tests/unittest/test_tree.py | 8 +++----- src/tests/unittest/test_types.py | 2 +- 22 files changed, 46 insertions(+), 32 deletions(-) create mode 100644 src/.flake8 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 25f35be..0d8b0ac 100644 --- a/src/Makefile +++ b/src/Makefile @@ -9,6 +9,9 @@ clean: test: 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/__init__.py b/src/cmdtree/__init__.py index 4c6c88d..2ce6cb2 100644 --- a/src/cmdtree/__init__.py +++ b/src/cmdtree/__init__.py @@ -21,3 +21,18 @@ # globals and entry point 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/decorators.py b/src/cmdtree/decorators.py index 4bb00c2..3025fa9 100644 --- a/src/cmdtree/decorators.py +++ b/src/cmdtree/decorators.py @@ -20,4 +20,4 @@ def wrapped(*args, **kwargs): echo.error("Error: %s" % str(e.format_error(node_help))) exit(1) - return wrapped \ No newline at end of file + return wrapped diff --git a/src/cmdtree/parser.py b/src/cmdtree/parser.py index 1015a05..0864b8c 100644 --- a/src/cmdtree/parser.py +++ b/src/cmdtree/parser.py @@ -2,7 +2,6 @@ from copy import deepcopy -from cmdtree import echo from cmdtree.echo import error from cmdtree.exceptions import ( ParserError, ArgumentRepeatedRegister, @@ -25,7 +24,7 @@ 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_index = index - 1 if index != 0 else index prev_ele = name_list[prev_index] if prev_ele == ele == "-": continue @@ -385,5 +384,3 @@ def run(self): node=node['cmd'] ) return cmd.run(kwargs) - - diff --git a/src/cmdtree/proxy.py b/src/cmdtree/proxy.py index 1b1f116..f9348c6 100644 --- a/src/cmdtree/proxy.py +++ b/src/cmdtree/proxy.py @@ -193,4 +193,4 @@ def wrapper(func): apply2parser(func, parser) return _cmd - return wrapper \ No newline at end of file + return wrapper diff --git a/src/cmdtree/registry.py b/src/cmdtree/registry.py index 9fcea78..70ef2c1 100644 --- a/src/cmdtree/registry.py +++ b/src/cmdtree/registry.py @@ -33,4 +33,5 @@ def tree(self): self._tree = CmdTree() return self._tree -env = ENV() \ No newline at end of file + +env = ENV() diff --git a/src/cmdtree/shortcuts.py b/src/cmdtree/shortcuts.py index e52db30..a0935a2 100644 --- a/src/cmdtree/shortcuts.py +++ b/src/cmdtree/shortcuts.py @@ -52,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/types.py b/src/cmdtree/types.py index 5cea4bd..7a79937 100644 --- a/src/cmdtree/types.py +++ b/src/cmdtree/types.py @@ -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 index 1b8d760..e6446c2 100644 --- a/src/cmdtree/utils.py +++ b/src/cmdtree/utils.py @@ -8,4 +8,4 @@ def get_cmd_path(path_prefix, cmd_name): def get_func_name(func): assert callable(func) - return func.__name__ \ No newline at end of file + 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/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/tests/functional/test_command.py b/src/tests/functional/test_command.py index 86e43d0..7357ce1 100644 --- a/src/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/tests/functional/test_group.py b/src/tests/functional/test_group.py index 46844f6..959d57c 100644 --- a/src/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/test_registry.py b/src/tests/unittest/test_registry.py index e8613bf..4ff1e3a 100644 --- a/src/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/tests/unittest/test_shortcuts.py b/src/tests/unittest/test_shortcuts.py index 8416f15..1187f8d 100644 --- a/src/tests/unittest/test_shortcuts.py +++ b/src/tests/unittest/test_shortcuts.py @@ -207,4 +207,4 @@ def test_should_full_path_be_none_if_path_is_none(self, cmd): def test_get_func_name(do_nothing): - assert utils.get_func_name(do_nothing) == "func" \ No newline at end of file + assert utils.get_func_name(do_nothing) == "func" diff --git a/src/tests/unittest/test_tree.py b/src/tests/unittest/test_tree.py index eb62638..559943a 100644 --- a/src/tests/unittest/test_tree.py +++ b/src/tests/unittest/test_tree.py @@ -124,10 +124,8 @@ 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.root['children']['new_cmd']['children'] - assert {} == \ - cmd_tree.root['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 @@ -143,4 +141,4 @@ def test_should_cmd_tree_get_cmd_by_path_got_obj( 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/tests/unittest/test_types.py b/src/tests/unittest/test_types.py index 3310711..bcda6cb 100644 --- a/src/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 + } From 8c2af26be192ed3b1b413ea31aa7b3afb2f34aaf Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 05:16:05 +0000 Subject: [PATCH 27/32] Feature: Add flake8 in travis-ci --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index deff2b4..8291a70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,6 @@ install: # command to run tests script: - make test + - make flake8 after_success: - coveralls \ No newline at end of file From 7aa9fb06926564d2bd99ac63d01d7885bd496eac Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 06:28:58 +0000 Subject: [PATCH 28/32] Feature: Raise exception if parent node does not exist --- src/cmdtree/tree.py | 11 ++++++++++- src/examples/low-level/command_from_path.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cmdtree/tree.py b/src/cmdtree/tree.py index aa27bad..4fb6e74 100644 --- a/src/cmdtree/tree.py +++ b/src/cmdtree/tree.py @@ -70,10 +70,19 @@ def _add_node(self, cmd_node, cmd_path): :type cmd_path: list or tuple """ parent = self.root - for cmd_key in cmd_path: + 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 diff --git a/src/examples/low-level/command_from_path.py b/src/examples/low-level/command_from_path.py index 3655080..96e0297 100644 --- a/src/examples/low-level/command_from_path.py +++ b/src/examples/low-level/command_from_path.py @@ -27,7 +27,7 @@ def delete(disk_id): 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") From 22c55321b408e489db7fbae5ae30220b2e0d4d66 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 06:39:56 +0000 Subject: [PATCH 29/32] Feature: Use textwrap instead in python2 --- src/cmdtree/format.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cmdtree/format.py b/src/cmdtree/format.py index a1f2fdd..bc254be 100644 --- a/src/cmdtree/format.py +++ b/src/cmdtree/format.py @@ -1,7 +1,16 @@ -from textwrap import indent - 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 From 6753e36a90f86d5e6eedf4307cabad5ba1b829af Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 06:57:42 +0000 Subject: [PATCH 30/32] Fix: flake8 --- src/cmdtree/format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmdtree/format.py b/src/cmdtree/format.py index bc254be..f71ab44 100644 --- a/src/cmdtree/format.py +++ b/src/cmdtree/format.py @@ -4,6 +4,7 @@ from textwrap import indent except ImportError: import textwrap + def indent(text, indent_with): wrapper = textwrap.TextWrapper( initial_indent=indent_with, From af75b60955d64f852f0c5ac3f6efdea3023f4618 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 07:01:21 +0000 Subject: [PATCH 31/32] Fix: Remove CI for python-2.6 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8291a70..b592c01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python python: - - 2.6 - 2.7 + - 3.2 - 3.5 + - 3.6 # command to install dependencies before_install: - pip install -r src/test-requirements.txt From f6b3ed39a1402f97179da3904a71eb60defd636b Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 3 Jan 2018 07:05:44 +0000 Subject: [PATCH 32/32] Fix: Remove support for python-3.2 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b592c01..b47f73a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - 2.7 - - 3.2 - 3.5 - 3.6 # command to install dependencies