Source code for argparseinator

# -*- coding: utf-8 -*-
"""
    ArgParseInator.
    silly but funny thing thats can help you to manage argparse and functions
"""
from __future__ import print_function
# try to import the builtin module
try:
    import __builtin__
except ImportError:
    import builtins as __builtin__
from importlib import import_module
import sys
import types
import inspect
from os import linesep
import os
import re
import argparse
import six
from argparseinator import utils
from argparseinator import exceptions
__version__ = "1.0.17"
EXIT_OK = 0
fun_check = re.compile(r'(?m)^.*?:\n\s+').search
fun_comment = re.compile(r'^\s*\.\.').search
fun_rst_title = re.compile(
    r'(?m)(?:^[=\-_\*]+\n(.+)\n[=\-_\*]+)|(?:^(.+)\n[=\-_\*]+)').search

class ARPIFormatter(argparse.HelpFormatter):
    """
    ArgParseInator Help Formatter

    Try to extract title form a **rst** formatted title.
    Try to split lines when found :\n\s (colon newline tab/spaces) sequence.
        list here:
            - apple
            - banana
            - pasta
    Try to remove lines **rst** comments (two points ..)
    Formats default values.
    """

    def _split_lines(self, text, width):
        if fun_check(text):
            return [l for l in text.splitlines(True) if not fun_comment(l)]
        return [
            l for l in argparse.HelpFormatter._split_lines(self, text, width)
            if not fun_comment(l)]

    def _fill_text(self, text, width, indent):
        pars = text.split("\n\n", True)
        newtext = []
        for par in pars:
            title = fun_rst_title(par)
            if title:
                par = "".join([g for g in title.groups() if g])
            newtext += self._split_lines(par, width) + ['\n\n']
        newtext.pop()
        return ''.join([indent + line for line in newtext])


def formatter_factory(show_defaults=True):
    """Formatter factory"""
    def get_help_string(self, action):
        lhelp = action.help
        if isinstance(show_defaults, (list, tuple)):
            if "-" + action.dest in show_defaults:
                return lhelp
        if '%(default)' not in action.help:
            if action.default is not argparse.SUPPRESS:
                defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
                if action.option_strings or action.nargs in defaulting_nargs:
                    lhelp += ' (default: %(default)s)'
        return lhelp

    def default_help_string(self, action):
        return action.help
    if show_defaults is True:
        ARPIFormatter._get_help_string = classmethod(get_help_string)
    else:
        ARPIFormatter._get_help_string = classmethod(default_help_string)
    return ARPIFormatter


class ArgParseInated(object):  # pylint: disable=too-few-public-methods
    """
    Class for deriving from
    """
    __shared_arguments__ = []
    __arguments__ = []

    def __init__(self, parseinator, **new_attributes):
        # update the class attributes
        self.__dict__.update(**new_attributes)
        # set the parseinator reference
        self.parseinator = parseinator
        # and some shortcut
        self.args = parseinator.args
        self.cfg = parseinator.cfg or {}
        self.write = parseinator.write
        self.writeln = parseinator.writeln
        # clall the initialization function
        self.__preinator__()

    def __preinator__(self):
        """
        Initialization to setup something at the __init__ end.
        """
        pass


[docs]class ArgParseInator(object): # pylint: disable=too-many-instance-attributes """ ArgParseInator class. """ # we will use a Singleton __metaclass__ = utils.Singleton __reinit__ = True # setup the standard output _output = sys.stdout # the main parser object parser = None # parsed status _is_parsed = False # single command _single = False add_output = False write_mode = 'wb' never_single = False # help formatter formatter_class = formatter_factory args = None argparse_args = {} # commands commands = {} # subparsers subparsers = None # parsed arguments ap_args = [] # authorizations auths = {} # authorization phrase auth_phrase = None # write name _write_name = 'write' # write line name _write_line_name = 'writeln' # global ArgParseInator instance name _argpi_name = '__argpi__' # auto_exit auto_exit = True msg_on_error_only = False cmd_name = None default_cmd = None setup = [] appendvars = {} error = None cfg_file = None _cfg_factory = None cfg = None # default encoding encoding = 'utf-8' _plugins = {} def __init__( # pylint: disable=too-many-arguments self, add_output=None, argpi_name=None, args=None, auth_phrase=None, auto_exit=None, config=None, default_cmd=None, error=None, ff_prefix=None, formatter_class=None, msg_on_error_only=None, never_single=None, setup=None, skip_init=False, write_line_name=None, write_name=None, write_mode=None, show_defaults=True, **argparse_args): # setup the global ArgParseInator instance name self._argpi_name = argpi_name or self._argpi_name # set the global reference to th ArgParseInator instance setattr(__builtin__, self._argpi_name, self) if skip_init: return self.auth_phrase = auth_phrase or self.auth_phrase self.never_single = never_single or self.never_single self.add_output = add_output or self.add_output self.write_mode = write_mode or self.write_mode self.ap_args = args or self.ap_args self.auto_exit = auto_exit if auto_exit is not None else self.auto_exit self.msg_on_error_only = msg_on_error_only or self.msg_on_error_only if ff_prefix is True: argparse_args['fromfile_prefix_chars'] = '@' elif ff_prefix is not None: argparse_args['fromfile_prefix_chars'] = ff_prefix # update the arguments self.argparse_args.update(**argparse_args) # setup formatter clas if formatter_class: self.formatter_class = formatter_class else: self.formatter_class = formatter_factory(show_defaults=show_defaults) # setup the name for the write function self._write_name = write_name or self._write_name # setup the name for the writeln funcion self._write_line_name = write_line_name or self._write_line_name # setup the default command self.default_cmd = default_cmd or self.default_cmd # setup the setup function self.setup = setup or self.setup # setup the error functino self.error = error or self.error if isinstance(config, dict): self.cfg = config elif isinstance(config, (list, tuple)): # if we have a config tuple/list # setup the config file self.cfg_file = config[0] # setup the config factory cfg_factory = config[1] setattr(self, '_cfg_factory', cfg_factory) if len(config) > 2: # if config has 3 elements setup the config error too setattr(self, '_cfg_error', types.MethodType(config[2], self)) def _compile(self, module=None): """ Compile functions for argparsing. """ # get the main module mod = module or sys.modules['__main__'] self.mod = mod # setup the script version if hasattr(mod, '__version__'): version = "%(prog)s " + mod.__version__ self.ap_args.append( ap_arg('-v', '--version', action='version', version=version)) # setup the script description if module is None: if 'description' not in self.argparse_args and hasattr(mod, '__doc__'): self.argparse_args['description'] = mod.__doc__ # setup main parser self.parser = argparse.ArgumentParser( formatter_class=self.formatter_class, **self.argparse_args) if self.error: # setup the error method if we have one setattr( self.parser, 'error', types.MethodType(self.error, self.parser)) if self.add_output: # add the output options if we have the add_output true if isinstance(self.add_output, basestring): odefault = self.add_output else: odefault = None self.parser.add_argument( '-o', '--output', metavar="FILE", default=odefault, help="Output to file") self.parser.add_argument( '--encoding', default="utf-8", help="Encoding for output file.") self.parser.add_argument( '--write-mode', default=self.write_mode, help="Write mode") if self._cfg_factory: # if we have a config factory add the config options self.parser.add_argument( '-c', '--config', metavar="FILE", default=self.cfg_file, help="Configuration file") if self.auths: # if we have authorizations enabled add the auth options self.parser.add_argument( '--auth', help="Authorization phrase for special commands.") # let's exexcute plugins for plugin in ArgParseInator._plugins.values(): plugin(self) if self.ap_args is not None: # if we have argment parser args we will add them to the main parser for aargs, akargs in self.ap_args: self.parser.add_argument(*aargs, **akargs) if len(self.commands) == 1 and self.never_single is False: # if we have only one command ad never_single is False # setup the command as the only command. func = six.next(six.itervalues(self.commands)) # add the arguments to the main parser for args, kwargs in func.__arguments__: self.parser.add_argument(*args, **kwargs) # shortcut for the single command self._single = func if not self.parser.description: # replace the description if we dont' have one self.parser.description = func.__doc__ or self.parser.description else: # or add to the main description if func.__doc__: self.parser.description += linesep + linesep + func.__doc__ if not self.parser.epilog: # replace the description if we dont' have one self.parser.epilog = getattr( func, "__epilog__", self.parser.epilog) else: # or add to the main description if hasattr(func, '__epilog__'): self.parser.epilog += linesep + linesep + func.__epilog__ utils.set_subcommands(func, self.parser) else: # if we have more than one command or we don't want a single command # set the single to None self._single = None # setup the subparsers self.subparsers = self.parser.add_subparsers( title=utils.COMMANDS_LIST_TITLE, dest='command', description=utils.COMMANDS_LIST_DESCRIPTION) # add all the commands for func in self.commands.values(): parser = utils.get_parser(func, self.subparsers) utils.set_subcommands(func, parser) return self.parser def parse_args(self): """ Parse our arguments. """ # compile the parser self._compile() # clear the args self.args = None self._self_event('before_parse', 'parse', *sys.argv[1:], **{}) # list commands/subcommands in argv cmds = [cmd for cmd in sys.argv[1:] if not cmd.startswith("-")] if (len(cmds) > 0 and not utils.check_help() and self.default_cmd and cmds[0] not in self.commands): # if we have at least one command which is not an help command # and we have a default command and the first command in arguments # is not in commands we insert the default command as second # argument (actually the first command) sys.argv.insert(1, self.default_cmd) # let's parse the arguments self.args = self.parser.parse_args() # set up the output. if self.args: # if we have some arguments if self.add_output and self.args.output is not None: # If add_output is True and we have an output file # setup the encoding self.encoding = self.args.encoding if self.args.encoding.lower() == 'raw': # if we have passed a raw encoding we will write directly # to the output file. self._output = open(self.args.output, self.args.write_mode) else: # else we will use the codecs module to write to the # output file. import codecs self._output = codecs.open( self.args.output, self.args.write_mode, encoding=self.args.encoding) if self._cfg_factory: # if we have a config factory setup the config file with the # right param self.cfg_file = self.args.config # now is parsed. self._is_parsed = True return self def check_auth(self, name): """ Check the authorization for the command """ if name in self.auths: # if the command name is in the **need authorization list** # get the authorization for the command auth = self.auths[name] if self.args.auth is None: # if we didn't pass the authorization phrase raise the # appropriate exception raise exceptions.ArgParseInatorAuthorizationRequired elif ((auth is True and self.args.auth != self.auth_phrase) or (auth is not True and self.args.auth != auth)): # else if the authorization phrase is wrong raise exceptions.ArgParseInatorNotValidAuthorization return True
[docs] def check_command(self, **new_attributes): """ Check if 'was passed a valid action in the command line and if so, executes it by passing parameters and returning the result. :return: (Any) Return the result of the called function, if provided, or None. """ # let's parse arguments if we didn't before. if not self._is_parsed: self.parse_args() if not self.commands: # if we don't have commands raise an Exception raise exceptions.ArgParseInatorNoCommandsFound elif self._single: # if we have a single function we get it directly func = self._single else: if not self.args.command: self.parser.error("too few arguments") # get the right command func = self.commands[self.args.command] if hasattr(func, '__subcommands__') and func.__subcommands__: # if we have subcommands get the command from them command = func.__subcommands__[self.args.subcommand] else: # else the command IS the function command = func # get the command name self.cmd_name = command.__cmd_name__ # check authorization if not self.check_auth(id(command)): return 0 # let's execute the command. return self._execute(func, command, **new_attributes)
def _cfg_error(self, error): """Config error""" raise exceptions.ArgParseInatorConfigError(str(error)) def _execute(self, func, command, **new_attributes): """ Execute command. """ if self._cfg_factory: # if we have a cfg_factory try: # we try to load a config with the factory if self.cfg_file: self.cfg = self._cfg_factory(self.cfg_file) except StandardError as error: # raise se exception self._cfg_error(error) # let's get command(function) argspec arg_specs = inspect.getargspec(command) if arg_specs.defaults: # if we have defaults # count defaults arguments count = len(arg_specs.defaults) # get arguments names args_names = arg_specs.args[:count] # get keyword arguments names kwargs_name = arg_specs.args[count:] else: # else all names are the args only args_names = arg_specs.args # and keyword arguments is empty kwargs_name = [] pargs = [] kwargs = {} # for every argument in argument names for name in args_names: if name == 'args': # if argument name is *special name* **args** # we append a reference to self.args pargs.append(self.args) elif name == 'self': # else if argment name is *special name* **self** if ArgParseInated in inspect.getmro(func.__cls__): # if the class that holds the function is subclass of # ArgParseInated we'll instantiate it, passing some # parameter pargs.append(func.__cls__(self, **new_attributes)) else: # else we'll instatiate the class without parameters pargs.append(func.__cls__()) else: # else we'll append the argument getting it from the self.args pargs.append(getattr(self.args, name)) # for every argument in keyword arguments for name in kwargs_name: if name == 'args': # if argument name is *special name* **args** # we set for the arg a reference to self.args kwargs[name] = self.args elif name in self.args: # else if name is in self.args we'll set the relative value. kwargs[name] = getattr(self.args, name) # set the **global** write function setattr(__builtin__, self._write_name, self.write) # set the **global** write line function setattr(__builtin__, self._write_line_name, self.writeln) # let's setup something. for setup_func in self.setup: setup_func(self) # call event before_execute self._self_event('before_execute', command, *pargs, **kwargs) # if events returns a non None value we use it as retrval. retval, pargs, kwargs = self._call_event( 'before_execute', command, pargs, kwargs) # if before_execute event returns None go on with command if retval is None: # let's execute the command and assign the returned value to retval retval = command(*pargs, **kwargs) # call event after_execute self._call_event('after_execute', command, pargs, kwargs) self._self_event('after_execute', command, *pargs, **kwargs) if self.auto_exit: # if we have auto_exit is True if retval is None: self._self_event( 'before_exit_ok', command, retval=EXIT_OK, *pargs, **kwargs) # if retval is None we'll assume it's EXIT_OK self.exit(EXIT_OK) elif isinstance(retval, basestring): self._self_event('before_exit_ok', command, retval=retval, *pargs, **kwargs) # else if retval is a string we will exit with the message and # ERRORCODE is equal to 0 self.exit(EXIT_OK, retval) elif isinstance(retval, int): if retval == EXIT_OK: self._self_event('before_exit_ok', command, retval=retval, *pargs, **kwargs) else: self._self_event('before_exit_error', command, retval=retval, *pargs, **kwargs) # else if retval is an integer we'll exits with it as ERRORCODE self.exit(retval) elif isinstance(retval, (tuple, list,)): self._self_event('before_exit_error', command, retval=retval, *pargs, **kwargs) # if retval is a tuple or a list we'll exist with ERRORCODE and # message self.exit(retval[0], retval[1]) self._self_event('before_exit', command, retval=retval, *pargs, **kwargs) self.exit() else: # else if auto_exit is not True # we'll simply return retval return retval def _call_event(self, event_name, cmd, pargs, kwargs, **kws): """ Try to call events for cmd. """ def get_result_params(res): """return the right list of params""" if not isinstance(res, (list, tuple)): return res, pargs, kwargs elif len(res) == 2: return res, pargs, kwargs return res[0], (pargs[0], ) + tuple(res[1]), kwargs if hasattr(cmd, event_name): return get_result_params( getattr(cmd, event_name)(pargs[0], *pargs[1:], **kwargs)) elif hasattr(cmd.__cls__, event_name): return get_result_params( getattr(cmd.__cls__, event_name)( pargs[0], cmd.__cmd_name__ or cmd.__name__, *pargs[1:], **kwargs)) return None, pargs, kwargs def _self_event(self, event_name, cmd, *pargs, **kwargs): """Call self event""" if hasattr(self, event_name): getattr(self, event_name)(cmd, *pargs, **kwargs) @classmethod def add_event(cls, event, event_name=None): """Add events""" # setattr(cls, event_name, event) event_name = event_name or event.__name__ setattr(cls, event_name, types.MethodType(event, cls)) def _tounicode(self, string): """Silly converter""" if not isinstance(string, unicode): # if string is not unicode we'll comverte it string = unicode(string, self.encoding) return string
[docs] def write(self, *string): """ Writes to the output """ self._output.write(' '.join([self._tounicode(s) for s in string])) return self
[docs] def writeln(self, *string): """ Wrtie lines to the output """ self._output.write(' '.join([ self._tounicode(s) for s in string]) + linesep) return self
def exit(self, status=EXIT_OK, message=None): """ Terminate the script. """ if not self.parser: self.parser = argparse.ArgumentParser() if self.msg_on_error_only: # if msg_on_error_only is True if status != EXIT_OK: # if we have an error we'll exit with the message also. self.parser.exit(status, message) else: # else we'll exit with the status ongly self.parser.exit(status, None) else: # else if msg_on_error_only is not True # we'll exit with the status and the message self.parser.exit(status, message)
def extend_with(func): """Extends with class or function""" if not func.__name__ in ArgParseInator._plugins: ArgParseInator._plugins[func.__name__] = func def arg(*args, **kwargs): """ Dcorates a function or a class method to add to the argument parser """ def decorate(func): """ Decorate """ # we'll set the command name with the passed cmd_name argument, if # exist, else the command name will be the function name func.__cmd_name__ = kwargs.pop( 'cmd_name', getattr(func, '__cmd_name__', func.__name__)) # retrieve the class (SillyClass) func.__cls__ = utils.check_class() if not hasattr(func, '__arguments__'): # if the funcion hasn't the __arguments__ yet, we'll setup them # using get_functarguments. func.__arguments__ = utils.get_functarguments(func) if len(args) or len(kwargs): # if we have some argument or keyword argument # we'll try to get the destination name from the kwargs ('dest') # else we'll use the last arg name as destination arg_name = kwargs.get( 'dest', args[-1].lstrip('-').replace('-', '_')) try: # we try to get the command index. idx = func.__named__.index(arg_name) # and delete it from the named list del func.__named__[idx] # and delete it from the arguments list del func.__arguments__[idx] except ValueError: pass # append the args and kwargs to the function arguments list func.__arguments__.append((args, kwargs,)) if func.__cls__ is None and isinstance(func, types.FunctionType): # if the function don't have a class and is a FunctionType # we'll add it directly to he commands list. ap_ = ArgParseInator(skip_init=True) if func.__cmd_name__ not in ap_.commands: # we'll add it if not exists ap_.commands[func.__cmd_name__] = func return func return decorate def class_args(cls): """ Decorates a class to handle the arguments parser. """ # get the Singleton ap_ = ArgParseInator(skip_init=True) # collect special vars (really need?) utils.collect_appendvars(ap_, cls) # set class reference cls.__cls__ = cls cmds = {} # get eventual class arguments cls.__arguments__ = getattr(cls, '__arguments__', []) # cycle through class functions for func in [f for f in cls.__dict__.values() if hasattr(f, '__cmd_name__') and not inspect.isclass(f)]: # clear subcommands func.__subcommands__ = None # set the parent class func.__cls__ = cls # assign to commands dict cmds[func.__cmd_name__] = func if hasattr(cls, '__cmd_name__') and cls.__cmd_name__ not in ap_.commands: # if che class has the __cmd_name__ attribute and is not already present # in the ArgParseInator commands # set the class subcommands cls.__subcommands__ = cmds # add the class as ArgParseInator command ap_.commands[cls.__cmd_name__] = cls else: # else if we don't have a __cmd_name__ # we will add all the functions directly to the ArgParseInator commands # if it don't already exists. for name, func in cmds.items(): if name not in ap_.commands: ap_.commands[name] = func return cls def ap_arg(*args, **kwargs): """ Silly function to return a tuple for add_argument parser. """ return args, kwargs def cmd_auth(auth_phrase=None): """ set authorization for command or subcommand. """ def decorate(func): """ decorates the funcion """ # get the Singleton ap_ = ArgParseInator(skip_init=True) # set the authorization name auth_name = id(func) if auth_phrase is None: # if we don't have a specific auth_phrase we set the # **authorization needed** to True ap_.auths[auth_name] = True else: # else if we have a specific auth_phrase we set it for the # command authorization ap_.auths[auth_name] = str(auth_phrase) return func return decorate def get_compiled(): """ :return: The compiled parser. :rtype: parser Return the compiled parser. """ return ArgParseInator()._compile() def get_compiled_factory(module): """ :module module: Referred module. :return: The compiled parser. :rtype: function Return a function that will compile the parser using module as refer. """ def _get_compiled(): return ArgParseInator()._compile(sys.modules[module]) return _get_compiled