from argparse import ArgumentParser
from textwrap import dedent
from .arguments import arg
import os
import logging, sys, argparse
logging.basicConfig()
logger = logging.getLogger('argdeco.command_decorator')
#logger.setLevel(logging.DEBUG)
logger.debug("module name: %s", __name__)
try:
basestring
except NameError:
basestring = str
class Undefined:
pass
[docs]class NoAction(RuntimeError):
pass
[docs]class CommandDecorator:
"""
Create a decorator to decorate functions with their arguments.
"""
def __init__(self, *args, **kwargs):
"""Initialize CommandDecorator with global arguments
>>> from argdeco import arg, CommandDecorator
>>> command = CommandDecorator(
...
... )
# Arguments
description:
Description in argument, also causes that command's function __doc__
documentation will be also passed as description to add_argument()
by default.
If not specified, the calling modules __doc__ will be used as epilogue
(and all other commands __doc__ will be passed as epilogue).
formatter_class:
Default formatter class is argparse.RawDescriptionHelpFormatter. You
can override this with passing another formatter_class.
preprocessor:
Instead of passing a compile paramter to execute later, you can also
pass a preprocessor function here.
Preprocessor function is getting args object as positional argument
and expanded arguments as kwargs.
It may return
# a tuple of (args, kwargs) to be passed to command function
# a dict, to be passed as kwargs to command function
# a tuple or list (with not per accident having exact two items
beeing a tuple and kwargs) passed as positional args to command
function
argparser:
(internal) You may pass an existing argparser instance.
commands:
(internal) An object returned from argparser.add_subparsers() method.
compiler_factory:
Passing a compiler_factory is another way of providing a argument
compiler.
`compiler_factory` is expected to be a function getting one parameter
-- the args as returned from `argparser.parseargs()``. It is espected
to return a dictionary like object. For each given argument the
name of the argument is computed and the corresponding configuration
item is set.
Example:
>>> command = CommandDecorator(compiler_factory=lambda x: {})
>>> @command('foo', arg('--bar'))
>>> def cmd(cfg):
... assert cfg['foo.bar'] == '1'
>>> command.execute(['foo', '--bar', '1'])
default_action:
Function to be called with the NameSpace object returned by parse_args(),
if there could no action be identified. Usually if you call a
multi-command program without any arguments. If not present, the
usage will be printed and 2 returned.
"""
self.formatter_class = kwargs.get('formatter_class', argparse.RawDescriptionHelpFormatter)
if 'description' in kwargs:
self.doc = 'description'
else:
self.doc = 'epilog'
if 'description' not in kwargs and 'epilog' not in kwargs:
description = sys._getframe().f_back.f_globals.get('__doc__')
logger.debug("frame name: %s", sys._getframe().f_back.f_globals.get('__name__'))
logger.debug("description: %s", description)
kwargs[self.doc] = description
if kwargs[self.doc] is not None:
kwargs[self.doc] = dedent(kwargs[self.doc])
if 'formatter_class' not in kwargs:
kwargs['formatter_class'] = self.formatter_class
if 'preprocessor' in kwargs:
self.preprocessor = kwargs.pop('preprocessor')
else:
self.preprocessor = None
# use epilog or description
#moddoc = sys._getframe().f_back.f_globals.get('__doc__')
#epilog =
for k in ('commands', 'parent', 'name', 'compiler_factory', 'default_action'):
setattr(self, k, kwargs.pop(k, None))
self.config_map = {}
self.compile = None
if 'argparser' in kwargs:
self.argparser = kwargs.pop('argparser')
else:
self.argparser = ArgumentParser(**kwargs)
for a in args:
if isinstance(a, arg):
a.apply(self.argparser, self, self.get_name())
self.children = []
def has_action(self):
if self.commands is not None:
return len(self.commands._name_parser_map) > 0
else:
return False
def add_parser(self, command, *args, **kwargs):
if self.commands is None:
self.commands = self.argparser.add_subparsers()
parser_action = self.commands.add_parser(command, *args, **kwargs)
#parser_action.set_default(action=lambda *a, **k: self.argparser.print_help(None))
return parser_action
def __getitem__(self, name):
if self.commands is None:
raise KeyError(name)
cmd = self
items = name.split('.')
path, name = items[:-1], items[-1]
for k in path:
_map = dict((c.name, c) for c in cmd.children)
if k not in _map:
raise KeyError(name)
cmd = _map[k]
return cmd.commands._name_parser_map[name]
def get_action(self, name):
return self[name].get_default('action')
[docs] def add_subcommands(self, command, *args, **kwargs):
"""add subcommands.
If command already defined, pass args and kwargs to add_subparsers()
method, else to add_parser() method. This behaviour is for convenience,
because I mostly use the sequence:
>>> p = parser.add_parser('foo', help="some help")
>>> subparser = p.add_subparsers()
If you want to configure your sub_parsers, you can do it with:
>>> command.add_subcommands('cmd',
help = "cmd help"
subcommands = dict(
title = "title"
description = "subcommands description"
)
)
"""
subcommands = kwargs.pop('subcommands', None)
if subcommands is not None and 'description' in subcommands:
subcommands['description'] = dedent(subcommands['description'])
if 'description' in kwargs:
kwargs['description'] = dedent(kwargs['description'])
if 'epilog' in kwargs:
kwargs['epilog'] = dedent(kwargs['epilog'])
try:
cmd = self[command]
except KeyError:
if 'formatter_class' not in kwargs:
kwargs['formatter_class'] = self.formatter_class
cmd = self.add_parser(command, *args, **kwargs)
args, kwargs = tuple(), dict()
if subcommands is not None:
kwargs = subcommands
child = CommandDecorator(
argparser = self.argparser,
commands = cmd.add_subparsers(*args, **kwargs),
parent = self,
name = command,
)
cmd.set_defaults(action=lambda *a, **k: cmd.print_help(None))
self.children.append(child)
return child
[docs] def update(self, command=None, **kwargs):
"""update data, which is usually passed in ArgumentParser initialization
e.g. command.update(prog="foo")
"""
if command is None:
argparser = self.argparser
else:
argparser = self[command]
for k,v in kwargs.items():
setattr(argparser, k, v)
def add_argument(self, *args, **kwargs):
logger.debug("add_argument: %s, %s", args, kwargs)
if len(args) == 1 and isinstance(args[0], arg):
self.add_arguments(*args)
else:
self.argparser.add_argument(*args, **kwargs)
# def register_config_map(self, dest):
def register_config_map(self, context, dest, config_name):
logger.debug("register_config_map: context=%s, dest=%s, config_name=%s", context, dest, config_name)
_root = self
while _root.parent is not None:
_root = _root.parent
if context not in _root.config_map:
_root.config_map[context] = {}
_root.config_map[context][dest] = config_name
# map_name = self.get_name()
# logger.debug("map_name=%s", map_name)
# if hasattr(a, 'config_name'):
# config_name = a.config_name
# else:
# config_name = '.'.join([map_name, a.dest])
#
# if map_name not in self.config_map:
# self.config_map[map_name] = {}
def add_arguments(self, *args, **kwargs):
context = kwargs.get('context')
argparser = kwargs.get('argparser')
if context is None:
context = self.get_name()
if argparser is None:
argparser = self.argparser
for a in args:
if isinstance(a, basestring):
a = arg(a)
elif isinstance(a, dict):
a = arg(**a)
elif isinstance(a, (tuple,list)):
a = arg(*a)
elif not isinstance(a, arg):
raise ValueError("cannot convert %s into arg type" % a)
a.apply(argparser, self, context=context)
[docs] def get_config_name(self, action, name=None):
'''get the name for configuration
This returns a name respecting commands and subcommands. So if you
have a command name "index" with subcommand "ls", which has option
"--all", you will pass the action for subcommand "ls" and the options's
dest name ("all" in this case), then this function will return
"index.ls.all" as configuration name for this option.
'''
_name = None
if name is None:
if '.' in action:
action, name = action.rsplit('.', 1)
else:
_name = ''
name = action
if _name is None:
if isinstance(action, basestring):
action = self.get_action(action)
_name = action.argdeco_name
logger.debug("_name=%s", _name)
config_name = Undefined
while True:
logger.debug("check _name=%s, name=%s", repr(_name), repr(name))
if _name in self.config_map:
if name in self.config_map[_name]:
config_name = self.config_map[_name][name]
if config_name is not None:
if config_name.startswith('.'):
config_name = _name + config_name
break
if _name == '':
break
if '.' not in _name:
_name = ''
continue
_name = _name.rsplit('.', 1)[0]
assert config_name is not Undefined, "could not determine config name for %s" % name
# if config_name.startswith('.'):
# config_name = config_name[1:]
return config_name
[docs] def add_command(self, command, *args, **kwargs):
"""add a command.
This is basically a wrapper for add_parser()
"""
cmd = self.add_parser(command, *args, **kwargs)
def get_name(self, name=None):
if name is not None:
path = [name]
else:
path = []
cmd = self
while cmd:
if cmd.name:
path.append(cmd.name)
cmd = cmd.parent
return '.'.join(reversed(path))
def __call__(self, *args, **opts):
def factory(func):
_args = args
help = None
desc = None
if func.__doc__ is not None:
_doc = func.__doc__
if 'help' not in opts:
try:
help, desc = _doc.split("\n\n", 1)
help = help.strip()
except:
help = _doc.strip()
desc = ''
else:
desc = _doc
if 'preprocessor' in opts:
func._argdeco_preprocessor = opts.pop('preprocessor')
if 'compile' in opts:
func._argdeco_compile = opts.pop('compile')
if 'compiler_factory' in opts:
func._argdeco_compiler_factory = opts.pop('compiler_factory')
if desc is not None:
desc = dedent(desc)
kwargs = {
'help': help,
'formatter_class': self.formatter_class,
self.doc: desc,
}
kwargs.update(opts)
if len(_args) and isinstance(_args[0], basestring):
name = _args[0]
_args = _args[1:]
command = self.add_parser(name, **kwargs)
else:
name = None
command = self.argparser
func.argdeco_name = context = self.get_name(name)
self.config_map[func.argdeco_name] = {}
self.add_arguments(*_args, argparser=command, context=context)
command.set_defaults(action=func)
return func
return factory
def compile_args(self, argv=None, compile=None, preprocessor=None,
compiler_factory=None):
logger.debug(
"argv=%s, compile=%s, preprocessor=%s, compiler_factory=%s",
argv, compile, preprocessor, compiler_factory)
if argv is None:
argv = sys.argv[1:]
import argcomplete
argcomplete.autocomplete(self.argparser)
args = self.argparser.parse_args(argv)
try:
action = args.action
except AttributeError:
action = self.default_action
# initialize arg processors
if not preprocessor:
preprocessor = self.preprocessor
if hasattr(action, '_argdeco_preprocessor'):
preprocessor = action._argdeco_preprocessor
if not compile:
compile = self.compile
if hasattr(action, '_argdeco_compile'):
preprocessor = action._argdeco_compile
if not compiler_factory:
compiler_factory = self.compiler_factory
if hasattr(action, '_argdeco_compiler_factory'):
preprocessor = action._argdeco_compiler_factory
assert not (compiler_factory and compile), \
"you can either define a compiler factory or a compile function"
# if 'BASH' in os.environ:
# if '_python_argcomplete_run' not in os.environ:
# logger.warning("Autocomplete is not activated. See https://github.com/kislyuk/argcomplete#activating-global-completion for activating")
if compiler_factory:
compile = compiler_factory(self)
if preprocessor:
result = None
try:
result = preprocessor(args)
except NoAction as e:
if action is None:
return (lambda *a, **k: self.argparser.print_help(None) or 2, tuple(), dict())
# if preprocessor returns a value, this overrules everything
# (e.g. print error message and return exit value)
if result is not None:
return result
opts = vars(args).copy()
del opts['action']
if compile is None or compile == 'kwargs':
(_args, _kwargs) = tuple(), opts
elif compile is True or compile == 'dict':
(_args, _kwargs) = (opts,), dict()
elif compile == 'args':
(_args, _kwargs) = (args,), dict()
else:
compiled = compile(args, **opts)
if isinstance(compiled, dict):
(_args, _kwargs) = tuple(), compiled
elif isinstance(compiled, (tuple,list)) and len(compiled) == 2 and isinstance(compiled[1], dict) and isinstance(compiled[0], (tuple,list)):
(_args, _kwargs) = compiled
elif isinstance(compiled, (tuple,list)):
(_args, _kwargs) = compiled, dict()
else:
raise Exception("Unkown compilation: %s" % compiled)
#logger.debug("compiled: %s", (_args, _kwargs))
return (args.action, _args, _kwargs)
[docs] def execute(self, argv=None, compile=None, preprocessor=None, compiler_factory=None):
"""Parse arguments and execute decorated function
argv: list of arguments
compile:
- None, pass args as keyword args to function
- True, pass args as single dictionary
- function, get args from parse_args() and return a pair of
tuple and dict to be passed as args and kwargs to function
"""
action, args, kwargs = self.compile_args(argv=argv, compile=compile, preprocessor=preprocessor, compiler_factory=compiler_factory)
return action(*args, **kwargs)
def factory(**kwargs):
frame = sys._getframe()
while True:
frame_name = frame.f_globals.get('__name__', '')
if not ('argdeco' in frame_name or 'importlib' in frame_name):
break
frame = frame.f_back
logger.debug("frame_name: %s", frame.f_globals.get('__name__', 'NO NAME'))
doc = frame.f_globals.get('__doc__')
if doc is not None and 'epilog' not in kwargs and 'description' not in kwargs:
kwargs['epilog'] = doc
return CommandDecorator(**kwargs)
#command_inst = None