Source code for argdeco.main

""" argdeco.main -- the main function

This module provides :py:class:`~argdeco.main.Main`, which can be used to create main
functions.

For ease it provides common arguments like `debug`, `verbosity` and `quiet`
which control whether you want to print stacktraces, and how verbose the logging
is.  These arguments will not be passed to command handlers or main function
handler.

Usually you will import the global main instance provided in argdeco::

    from argdeco import main

In this case, `main.command` is also provided as global symbol::

    from argdeco import main, command

    @main.command(...)
    def cmd(...):
        ...

    # is equivalent to
    @command(...)
    def cmd(...):
        ...

Bug you can also create an own instance::

   from argdeco.main import Main
   main = Main()

   @main.command('foo', ...)
   def my_cmd(...):
       ...

If you want to make use of the predefined (global) args::

   if __name__ == '__main__':
       main(verbosity=True, debug=True, quiet=True)

"""

import argdeco.command_decorator as command_decorator
import sys, logging
from inspect import isfunction
import argparse

import os
from os.path import expanduser

from .arguments import arg
from .command_decorator import NoAction

PY3 = sys.version_info > (3, 0)
try:
    an_exception = StandardError
except:
    an_exception = Exception

class ArgParseExit(an_exception):
    def __init__(self, error_code, message):
        self.error_code=error_code
        self.message = message

    def __str__(self):
        if self.message:
            return self.message.strip()
        else:
            return ''


[docs]class Main: """Main function provider An instance of this class can be used as main function for your program. It provides a :py:attribute: :param debug: Set True if you want main to manage the debug arg. (default: False). Set global logging levels to DEBUG and print out full exception stack traces. :param verbosity: Control global logging log levels (default: False) If this is turned on, default log level will be set to ``ERROR`` and following argument are provided: .. option:: -v, --verbose set log level to level ``WARNING`` .. option:: -vv, -v -v, --verbose --verbose Set log level to level ``INFO`` .. option:: -vvv, -v -v -v Set log level to level ``DEBUG`` :param quiet: If you set this to ``True``, argument :option:`--quiet` will be added: .. option:: --quiet If this option is passed, global log level will be set to ``CRITICAL``. :param command: CommandDecorator instance to use. This defaults to None, and for each main instance there will be created a :py:class:`~argdeco.command_decorator.CommandDecorator` instance. :param compile: This parameter is passed :py:class:`~argdeco.command_decorator.CommandDecorator` instance and controls, if arguments passed to handlers are compiled in some way. :param compiler_factory: This parameter is passed :py:class:`~argdeco.command_decorator.CommandDecorator` instance and defines a factory function, which returns a compile function. You may either use ``compile`` or ``compiler_factory``. :param log_format: This parameter is passed to :py:func:`logging.basicConfig` to define log output. (default: ``"%(name)s %(levelname)s %(message)s"``) :param error_code: This is the error code to be returned on an exception (default: 1). :param error_handler: Pass a function, which handles errors. This function will get the error code returned from a command (or main) function and do something with it. Default is :py:func:`sys.exit`. If you do not want to exit the program after running the main funtion you have to set ``error_handler`` to ``None``. If you want to access the managed arguments (quiet, verbosity, debug), you can access them as attributes of the main instance:: if not main.quiet: print("be loud") if main.debug: print("debug is on") """ def __init__(self, debug = False, verbosity = False, quiet = False, compile = None, compiler_factory = None, command = None, log_format = "%(name)-20.20s %(levelname)-10.10s %(message)s", error_handler = sys.exit, error_code = 1, catch_exceptions = (SystemError, AssertionError, ArgParseExit), **kwargs ): # initialize logging logging.basicConfig(format=log_format) logger = logging.getLogger() logger.setLevel(logging.ERROR) # initialize error_handler and error_code self.error_handler = error_handler self.error_code = error_code # initialize managed argument indicators self.arg_debug = debug self.arg_quiet = quiet self.arg_verbosity = verbosity self.debug = False self.verbosity = 0 self.quiet = False self.print_traceback = False # #self.compiled_args = compiled # initialize command if command is None: command = command_decorator.factory(**kwargs) #if command_decorator.command_inst is None: #command_decorator.command_inst = \ #command_decorator.factory(**kwargs) # #command = command_decorator.command_inst self.command = command self.compile = compile self.compiler_factory = compiler_factory self.main_function = None self.catch_exceptions = catch_exceptions
[docs] def configure(self, debug=None, quiet=None, verbosity=None, traceback=None, compile=None, compiler_factory=None, catch_exceptions=None, **kwargs): """configure behaviour of main, e.g. managed args """ if debug is not None: self.arg_debug = debug if quiet is not None: self.arg_quiet = quiet if verbosity is not None: self.arg_verbosity = verbosity if compile is not None: self.compile = compile if compiler_factory is not None: self.compiler_factory = compiler_factory if traceback is not None: self.print_traceback = traceback if catch_exceptions is not None: self.catch_exceptions = catch_exceptions if kwargs: # other keyword arguments update command attribute self.command.update(**kwargs)
def init_managed_args(self): logger = logging.getLogger() _main = self _main.verbosity = 0 if self.arg_debug: @arg('--debug', help="print debug output", metavar='', nargs=0) def debug_arg(self, parser, namespace, values, option_string=None): _main.debug = True logger.setLevel(logging.DEBUG) try: self.command.add_argument(debug_arg) except argparse.ArgumentError: pass if self.arg_verbosity: @arg('-v', '--verbosity', help="verbosity: set loglevel -v warning, -vv info, -vvv debug", nargs=0, metavar=0) def verbosity_arg(self, parser, namespace, values, option_string=None): _main.verbosity += 1 if _main.verbosity == 1: logger.setLevel(logging.WARNING) if _main.verbosity == 2: logger.setLevel(logging.INFO) if _main.verbosity == 3: logger.setLevel(logging.DEBUG) try: self.command.add_argument(verbosity_arg) except argparse.ArgumentError: pass if self.arg_quiet: @arg('--quiet', help="have no output", metavar='', nargs=0) def quiet_arg(self, parser, namespace, values, option_string=None): logger.setLevel(logging.CRITICAL) _main.quiet = True try: self.command.add_argument(quiet_arg) except argparse.ArgumentError: pass def store_args(self, args): if self.arg_debug: del args.debug if self.arg_quiet: del args.quiet if self.arg_verbosity: del args.verbosity self.args = args logger = logging.getLogger('argdeco.main') logger.debug("args: %s", args) if not hasattr(args, 'action'): if self.main_function: args.action = self.main_function else: raise NoAction("You have to specify an action by either using @command or @main decorator")
[docs] def uninstall_bash_completion(self, script_name=None, dest="~/.bashrc"): '''remove line to activate bash_completion for given script_name from given dest You can use this for letting the user uninstall bash_completion:: from argdeco import command, main @command("uninstall-bash-completion", arg('--dest', help="destination", default="~/.bashrc") ) def uninstall_bash_completion(dest): main.uninstall_bash_completion(dest=dest) ''' if 'USERPROFILE' in os.environ and 'HOME' not in os.environ: os.environ['HOME'] = os.environ['USERPROFILE'] dest = expanduser(dest) if script_name is None: script_name = sys.argv[0] lines = [] remove_line = 'register-python-argcomplete %s' % script_name with open(dest, 'r') as f: for line in f: if line.strip().startswith('#'): lines.append(line) continue if remove_line in line: continue lines.append(line) with open(dest, 'w') as f: f.write(''.join(lines))
[docs] def install_bash_completion(self, script_name=None, dest="~/.bashrc"): '''add line to activate bash_completion for given script_name into dest You can use this for letting the user install bash_completion:: from argdeco import command, main @command("install-bash-completion", arg('--dest', help="destination", default="~/.bashrc") ) def install_bash_completion(dest): main.install_bash_completion(dest=dest) ''' if 'USERPROFILE' in os.environ and 'HOME' not in os.environ: os.environ['HOME'] = os.environ['USERPROFILE'] dest = expanduser(dest) if script_name is None: script_name = sys.argv[0] self.uninstall_bash_completion(script_name=script_name, dest=dest) with open(dest, 'a') as f: f.write('eval "$(register-python-argcomplete %s)"\n' % script_name)
[docs] def add_arguments(self, *args): """Explicitely add arguments:: main.add_arguments( arg('--first'), arg('--second') ) This function wraps :py:meth:`argdeco.command_decorator.C` :param \*args: arguments to be added. """ self.command.add_arguments(*args)
[docs] def __call__(self, *args, **kwargs): """ You can call :py:class:`~argdeco.main.Main` instance in various ways. As function or as decorator. As long you did not have decorated a function with this :py:class:`~argdeco.main.Main` instance, you can invoke it as function for confiugration. As soon there is defined some action, invoking the instance, will execute the actions. Configure some global arguments:: main( arg('--global', '-g', help="a global argument"), ) Decorate a function to be called as main function:: @main def my_main(): return 0 if __name__ == "__main__": main() Decorate a function to be main function and define arguments of it:: @main( arg('--first', '-f', help="first argument"), arg('--second', '-s', help="second argument"), ) def main(first, second): return 0 # successful if __name__ == "__main__": main(debug=True) :param \*args: All arguments of type :py:class:`~argdeco.command_decorator.arg` are filtered out and added as global argument to underlying :py:class:`~argdeco.command_decorator.CommandDecorator` instance. All other arguments are collected -- if any to be `argv`. If there are any other parameters, this function switches into regular `main` mode and will execute the main function passing `argv`. If there are no arguments defined, :py:attr:`sys.argv` is used as default. :param \*\*kwargs: You can pass various keyword arguments to tweak behaviour of the main function. :argv: You can set explicitly the ``argv`` vector. This becomes handy, if you want to pass an empty ``argv`` list and do not want to use the default sys.argv. :debug: Turn on debug argument, see :py:class:`~argdeco.main.Main` for more info. :verbosity: Turn on verbose argument, see :py:class:`~argdeco.main.Main` for more info. :quiet: Turn on quiet argument, see :py:class:`~argdeco.main.Main` for more info. :error_handler: Tweak the error handler. This will be only local to this call. :compile: Set compile for this call. :compiler_factory: Set compiler_factory for this call. :return: :Decorator mode: Returns the instance itself, to be invoked as decorator. :Run mode: Returns whatever error_handler returns, when getting the return value of the invoked action function """ error_handler = kwargs.pop('error_handler', self.error_handler) compile = kwargs.pop('compile', self.compile) compiler_factory = kwargs.pop('compiler_factory', self.compiler_factory) def default_error_handler(result): if isinstance(result, int): return result if result is False: return self.error_code return 0 if error_handler is None: error_handler = default_error_handler if hasattr(self, 'arg_debug') and 'debug' in kwargs: setattr(self, 'arg_debug', kwargs.pop('debug')) if hasattr(self, 'arg_verbosity') and 'verbosity' in kwargs: setattr(self, 'arg_verbosity', kwargs.pop('verbosity')) if hasattr(self, 'arg_quiet') and 'quiet' in kwargs: setattr(self, 'arg_quiet', kwargs.pop('quiet')) argv=None if 'argv' in kwargs: argv = kwargs.pop('argv') # other keyword arguments update command attribute self.command.update(**kwargs) # set a custom exit function def _exit(result=0, message=None): raise ArgParseExit(result, message) #return error_handler(result) self.command.update(exit=_exit) # handle case if called as decorator if len(args) == 1 and isfunction(args[0]): self.main_function = args[0] return args[0] # filter out argument definitions from posional arguments and create # argv list, if any for a in args: if isinstance(a, arg): self.command.add_argument(a) else: if argv is None: argv = [] argv.append(a) # if there were no argv args and there is not yet defined a main, # function defined and there are no commands defined yet. Return # this object, that there may be defined a function in a subsequent # call (this is the case if @main(args...) is used). if argv is not None and self.main_function is None and not self.command.has_action(): raise ValueError("Main cannot handle any arguments, when main_function is not yet defined") # at this point we are still in decorating mode if argv is None and self.main_function is None and not self.command.has_action(): return self self.exception = None # right before doing the command execution add the managed args self.init_managed_args() try: return error_handler(self.command.execute(argv, compile=compile, preprocessor=self.store_args, compiler_factory=compiler_factory)) except self.catch_exceptions as e: logger = logging.getLogger() logger.debug("caught exception (self.debug: %s)", self.debug, exc_info=1) if self.verbosity or self.print_traceback: import traceback traceback.print_exc() elif not self.quiet: if PY3: sys.stderr.write("%s\n" % e) else: sys.stderr.write((u"%s\n" % e).encode('utf-8')) self.exception = e if hasattr(e, 'error_code'): return error_handler(e.error_code) else: return error_handler(self.error_code)