import logging
from argparse import ArgumentParser
import inspect
from functools import partial
from collections import OrderedDict
from .node import Node
from .leaf import Leaf
from .config import Config
from .argument import Argument
from .exceptions import RootNodeException, NodeException, LeafException, CommandTreeException
logger = logging.getLogger(__name__)
[docs]class CommandTree(object):
"""Define the main API for build a tree with :py:mod:`argparse`.
It defines decorators and other functions to it.
Args:
config (Config): config
"""
def __init__(self, config = None):
self._root = None
self._item_counter = 0
self._config = config or Config()
self._parser = None
@property
def items(self):
return self._root.items
@property
def children(self):
return [item.obj for item in self._root.items]
[docs] def root(self, items = None, **kwargs):
"""Special node decorator; it can used only once.
Args:
items (list): explicit list of all the sub nodes
kwargs: all other keyword arguments will passed to the ArgumentParser.add_subparsers().add_parser function
Returns:
function: wrapper
"""
def wrapper(cls):
self.add_root(cls, items, **kwargs)
return cls
return wrapper
[docs] def node(self, name = None, items = None, **kwargs):
"""Decorator for node creation.
Args:
name (str): the node name
items (list): explicit list of all the sub nodes
kwargs: all other keyword arguments will passed to the ArgumentParser.add_subparsers().add_parser function
Returns:
function: wrapper
"""
def wrapper(cls):
self.add_node(cls, name, items, **kwargs)
return cls
return wrapper
[docs] def node_handler(self, func):
"""Decorator for mark a function to handle the childless nodes."""
func._node_handler = True
return func
[docs] def leaf(self, name = None, **kwargs):
"""Decorator for leaf creation.
Args:
name (str): the node name
kwargs: all other keyword arguments will passed to the ArgumentParser.add_subparsers().add_parser function
Returns:
function: wrapper
"""
def wrapper(func):
self.add_leaf(func, name, **kwargs)
return func
return wrapper
[docs] def argument(self, *args, **kwargs):
"""Decorator for argument creation.
All arguments will passed to the :py:func:`argparse.ArgumentParser.add_argument` function
Returns:
function: wrapper
"""
def wrapper(obj):
self.add_argument(obj, *args, **kwargs)
return obj
return wrapper
[docs] def common_argument(self, *args, **kwargs):
"""Decorator for argument creation.
All arguments will passed to the :py:func:`argparse.ArgumentParser.add_argument` function
Returns:
function: wrapper
"""
def wrapper(obj):
arg = self.add_argument(obj, *args, **kwargs)
arg.set_common()
return obj
return wrapper
[docs] def optional(self, cls):
"""Decorator for optional subparsers."""
if not hasattr(cls, '_item'):
raise CommandTreeException("Use this decorator only on a node. (or maybe used before the node decorator?)")
if not len(cls._item.items):
raise NodeException("This decorator is pointless on a childless node.", cls._item)
cls._item.required = False
return cls
[docs] def subparser_arguments(self, **kwargs):
"""Decorator for set argument to the :py:func:`ArgumentParser.add_subparsers` created by the node.
All keyword argument will passed to the :py:func:`ArgumentParser.add_subparsers`
Returns:
function: wrapper
"""
def wrapper(cls):
if not hasattr(cls, '_item'):
raise CommandTreeException("Use this decorator only on a node. (or maybe used before the node decorator?)")
cls._item.set_subparser_arguments(kwargs)
return cls
return wrapper
[docs] def add_root(self, cls, items = None, **kwargs):
"""Add root node to the tree.
Args:
cls (type): the handler class
items (list): explicit list of all the sub nodes
kwargs: all other keyword arguments will passed to the :py:class:`argparser.ArgumentParser` constructor
Returns:
Node: the item descriptor instance
"""
if self._root is not None:
raise RootNodeException("The root node was already set", self._root)
item = self.add_node(cls, "root", items, **kwargs)
self._root = item
self._root.traverse_for_common_arguments()
return item
[docs] def add_node(self, cls, name = None, items = None, **kwargs):
"""Add node to the tree.
Args:
cls (type): the handler class
name (str): the node name
items (list): explicit list of all the sub nodes
kwargs: all other keyword arguments will passed to the ArgumentParser.add_subparsers().add_parser function
Returns:
Node: the item descriptor instance
"""
item_args = cls._item_arguments if hasattr(cls, "_item_arguments") else OrderedDict()
item = Node(name, cls, self._next_item_id, item_args, items, kwargs, self._config.docstring_parser, self._generate_name_for_item)
cls._item = item
item.fetch()
item.parse_doc_string()
if hasattr(cls.__init__, '__code__'): # maybe this is a mystic object ctor
args = len(inspect.getargspec(cls.__init__).args) - 1
if args != len(item_args):
raise NodeException("The number of argument decorators ({}) is not equal the number of arguments ({})"
" of __init__ of class '{}'.".format(args, len(item_args), cls.__name__), item)
return item
[docs] def generate_name_for_argument(self, args, kwargs, identifier):
long_name = self._generate_name_for_item(identifier)
if self._config.prepend_double_hyphen_prefix_if_arg_has_default and 'default' in kwargs:
long_name = '--' + long_name
res = [long_name]
if self._config.generate_simple_hyphen_name is not False and 'default' in kwargs:
short_name = identifier[0]
hyphen_map = self._config.generate_simple_hyphen_name
if isinstance(hyphen_map, dict) and identifier in hyphen_map:
short_name = hyphen_map[identifier]
short_name = '-' + short_name
if long_name.startswith('--'):
res.insert(0, short_name)
else:
res = [short_name]
kwargs['dest'] = identifier
return res
[docs] def add_leaf(self, func, name = None, **kwargs):
"""Add leaf to the tree.
Args:
func (function): the handler function
name (str): the node name
items (list): explicit list of all the sub nodes
kwargs: all other keyword arguments will passed to the ArgumentParser.add_subparsers().add_parser function
Returns:
Leaf: the item descriptor instance
"""
item_args = func._item_arguments if hasattr(func, "_item_arguments") else OrderedDict()
item = Leaf(name, func, self._next_item_id, item_args, kwargs, self._config.docstring_parser, self._generate_name_for_item)
func._item = item
item.parse_doc_string()
# check this in the node too
func_desc = inspect.getargspec(func)
args = len(func_desc.args) - 1
if args != len(item_args):
raise LeafException("Call {} times the argument decorator on function '{}' before the leaf decor".format(args, func.__name__),
item)
return item
[docs] def add_argument(self, obj, *args, **kwargs):
"""Decorator for argument creation.
Args:
obj: class or function handler
args, kwargs: All other arguments will passed to the ArgumentParser.add_argument function
Returns:
argument.Argument: the argument descriptor instance
"""
if not hasattr(obj, "_item_arguments"):
obj._item_arguments = OrderedDict()
func = None
if inspect.isfunction(obj):
func = obj
else:
func = obj.__init__
func_desc = inspect.getargspec(func)
identifier = func_desc.args[- len(obj._item_arguments) - 1]
# get_default_from_function_param
if 'default' not in kwargs and func_desc.defaults is not None and self._config.get_default_from_function_param:
arg_idx = func_desc.args.index(identifier)
default_idx = arg_idx - (len(func_desc.args) - len(func_desc.defaults))
if default_idx >= 0:
default = func_desc.defaults[default_idx]
kwargs['default'] = default
# get_argument_type_from_function_default_value_type
if 'type' not in kwargs and 'default' in kwargs and 'action' not in kwargs \
and self._config.get_argument_type_from_function_default_value_type and kwargs.get('nargs', '?') == '?':
kwargs['type'] = type(kwargs['default'])
# The argument descriptors must be stored in the object (class or func), because the object descriptor (Node or Leaf) not created
# at this time, because of the CommandTree decorator structure. The "argument" decorator must be under the item decorators
# because of psychological reasons. It's looks better. But because of this, the argument decorators being called before node or leaf
# decorators so argument descriptors must be stored in somewhere, eg in the object.
arg = Argument(identifier, args, kwargs, partial(self.generate_name_for_argument, args, kwargs))
obj._item_arguments[identifier] = arg
# prepend...
obj._item_arguments.move_to_end(identifier, False)
return arg
[docs] def build(self, parser = None):
"""Build the parser tree.
Args:
parser (argparse.ArgumentParser): external parser to build
Returns:
argparse.ArgumentParser: the builded parser
"""
if self._parser and not parser:
return self._parser
parser = parser or ArgumentParser()
if not self._root:
raise RootNodeException("Root node is not defined!")
self._root.build(parser)
self._parser = parser
return parser
[docs] def execute(self, parser = None, args = None):
"""Build the parsers and execute it.
It will be run the handler function chosed by the user.
Args:
parser (argparse.ArgumentParser): external parser built by the CommandTree.build
args (list): external arguments to parse by the argument parser
Returns:
The return value of the leaf handler function
"""
if not self._root:
raise RootNodeException("Root node is not defined!")
parser = parser or self.build()
parsed_args = parser.parse_args(args).__dict__
def iter_item(item, parent = None):
if item is None:
return None
command_key = item.name + '_command'
handle_args = {identifier: parsed_args[arg.action.dest] for identifier, arg in item.arguments.items() if arg.item is item}
logger.debug("Gathered argument values for handle item (%s): %s", item, handle_args)
if command_key in parsed_args:
# it's a node, and it has items in it
item.create_handler_instance(handle_args)
command = parsed_args[command_key]
return iter_item(item[command], item.instance) if command in item else item.handle(handle_args)
elif hasattr(parent, item.obj_name):
# it's a leaf
func = getattr(parent, item.obj_name)
return func(**handle_args)
else:
# node without sub nodes or leafs
item.create_handler_instance(handle_args)
return item.handle(handle_args)
return iter_item(self._root)
@property
def _next_item_id(self):
self._item_counter += 1
return self._item_counter
def _generate_name_for_item(self, source):
source = source.lower()
if self._config.change_underscores_to_hyphens_in_names:
return source.replace('_', '-')
else:
return source