Source code for reproman.cmdline.main

# ex: set sts=4 ts=4 sw=4 noet:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the reproman package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
""""""

__docformat__ = 'restructuredtext'

import logging
lgr = logging.getLogger('reproman.cmdline')

lgr.log(5, "Importing cmdline.main")

import argparse
import sys
import textwrap
from importlib import import_module

import reproman

from reproman.cmdline import helpers
from reproman.support.exceptions import InsufficientArgumentsError, MissingConfigFileError
from ..utils import setup_exceptionhook, chpwd
from ..dochelpers import exc_str


def _license_info():
    return """\
Copyright (c) 2016- ReproMan developers (parts 2013-2016 DataLad developers)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""


# TODO:  OPT look into making setup_parser smarter to become faster
# Now it seems to take up to 200ms to do all the parser setup
# even though it might not be necessary to know about all the commands etc.
# I wondered if it could somehow decide on what commands to worry about etc
# by going through sys.args first
[docs]def setup_parser( formatter_class=argparse.RawDescriptionHelpFormatter, return_subparsers=False): lgr.log(5, "Starting to setup_parser") # delay since it can be a heavy import from ..interface.base import dedent_docstring, get_interface_groups, \ get_cmdline_command_name, alter_interface_docs_for_cmdline # setup cmdline args parser parts = {} # main parser parser = argparse.ArgumentParser( fromfile_prefix_chars='@', description=dedent_docstring("""\ ReproMan aims to ease construction and execution of computation environments based on collected provenance data."""), epilog='"Reproducibly Manage Your Environments"', formatter_class=formatter_class, add_help=False) # common options helpers.parser_add_common_opt(parser, 'help') helpers.parser_add_common_opt(parser, 'log_level') helpers.parser_add_common_opt( parser, 'version', version='reproman %s\n\n%s' % (reproman.__version__, _license_info())) if __debug__: parser.add_argument( '--dbg', action='store_true', dest='common_debug', help="enter Python debugger when uncaught exception happens") parser.add_argument( '--idbg', action='store_true', dest='common_idebug', help="enter IPython debugger when uncaught exception happens") parser.add_argument( '-C', action='append', dest='change_path', metavar='PATH', help="""run as if reproman were started in <path> instead of the current working directory. When multiple -C options are given, each subsequent non-absolute -C <path> is interpreted relative to the preceding -C <path>. This option affects the interpretations of the path names in that they are made relative to the working directory caused by the -C option""") parser.add_argument( "-c", "--config", metavar="CONFIG", action="append", help="""path to ReproMan configuration file. This option can be given multiple times, in which case values in the later files override previous ones.""") # yoh: atm we only dump to console. Might adopt the same separation later on # and for consistency will call it --verbose-level as well for now # log-level is set via common_opts ATM # parser.add_argument('--log-level', # choices=('critical', 'error', 'warning', 'info', 'debug'), # dest='common_log_level', # help="""level of verbosity in log files. By default # everything, including debug messages is logged.""") #parser.add_argument('-l', '--verbose-level', # choices=('critical', 'error', 'warning', 'info', 'debug'), # dest='common_verbose_level', # help="""level of verbosity of console output. By default # only warnings and errors are printed.""") # subparsers subparsers = parser.add_subparsers() # auto detect all available interfaces and generate a function-based # API from them grp_short_descriptions = [] interface_groups = get_interface_groups() for grp_name, grp_descr, _interfaces in interface_groups: # for all subcommand modules it can find cmd_short_descriptions = [] for _intfspec in _interfaces: # turn the interface spec into an instance lgr.log(5, "Importing module %s " % _intfspec[0]) _mod = import_module(_intfspec[0], package='reproman') _intf = getattr(_mod, _intfspec[1]) cmd_name = get_cmdline_command_name(_intfspec) # deal with optional parser args if hasattr(_intf, 'parser_args'): parser_args = _intf.parser_args else: parser_args = dict(formatter_class=formatter_class) # use class description, if no explicit description is available parser_args['description'] = alter_interface_docs_for_cmdline( _intf.__doc__) # create subparser, use module suffix as cmd name subparser = subparsers.add_parser(cmd_name, add_help=False, **parser_args) # all subparser can report the version helpers.parser_add_common_opt( subparser, 'version', version='reproman %s %s\n\n%s' % (cmd_name, reproman.__version__, _license_info())) # our own custom help for all commands helpers.parser_add_common_opt(subparser, 'help') helpers.parser_add_common_opt(subparser, 'log_level') # let module configure the parser _intf.setup_parser(subparser) # logger for command # configure 'run' function for this command plumbing_args = dict( func=_intf.call_from_parser, logger=logging.getLogger(_intf.__module__), subparser=subparser) if hasattr(_intf, 'result_renderer_cmdline'): plumbing_args['result_renderer'] = _intf.result_renderer_cmdline subparser.set_defaults(**plumbing_args) # store short description for later sdescr = getattr(_intf, 'short_description', parser_args['description'].split('\n')[0]) cmd_short_descriptions.append((cmd_name, sdescr)) parts[cmd_name] = subparser grp_short_descriptions.append(cmd_short_descriptions) # create command summary cmd_summary = [] for i, grp in enumerate(interface_groups): grp_descr = grp[1] grp_cmds = grp_short_descriptions[i] cmd_summary.append('\n*%s*\n' % (grp_descr,)) for cd in grp_cmds: cmd_summary.append(' - %s: %s' % (cd[0], textwrap.fill( cd[1].rstrip(' .'), 75, #initial_indent=' ' * 4, subsequent_indent=' ' * 8))) # we need one last formal section to not have the trailed be # confused with the last command group cmd_summary.append('\n*General information*\n') parser.description = '%s\n%s\n\n%s' \ % (parser.description, '\n'.join(cmd_summary), textwrap.fill(dedent_docstring("""\ Detailed usage information for individual commands is available via command-specific --help, i.e.: reproman <command> --help"""), 75, initial_indent='', subsequent_indent='')) parts['reproman'] = parser lgr.log(5, "Finished setup_parser") if return_subparsers: return parts else: return parser
# yoh: arn't used # def generate_api_call(cmdlineargs=None): # parser = setup_parser() # # parse cmd args # cmdlineargs = parser.parse_args(cmdlineargs) # # convert cmdline args into API call spec # functor, args, kwargs = cmdlineargs.func(cmdlineargs) # return cmdlineargs, functor, args, kwargs
[docs]def main(args=None): lgr.log(5, "Starting main(%r)", args) # PYTHON_ARGCOMPLETE_OK parser = setup_parser() try: import argcomplete argcomplete.autocomplete(parser) except ImportError: pass # parse cmd args cmdlineargs = parser.parse_args(args) if not cmdlineargs.change_path is None: for path in cmdlineargs.change_path: chpwd(path) if cmdlineargs.config is not None: # In this case, we're unnecessarily instantiating ConfigManager twice, # at import and now, but it might not be worth the effort to # restructure things to delay the import. from reproman.config import ConfigManager reproman.cfg = ConfigManager(cmdlineargs.config, load_default=False) if not hasattr(cmdlineargs, 'func'): lgr.info("No command given, returning") return ret = None if cmdlineargs.common_debug or cmdlineargs.common_idebug: # so we could see/stop clearly at the point of failure setup_exceptionhook(ipython=cmdlineargs.common_idebug) ret = cmdlineargs.func(cmdlineargs) else: # otherwise - guard and only log the summary. Postmortem is not # as convenient if being caught in this ultimate except try: ret = cmdlineargs.func(cmdlineargs) except InsufficientArgumentsError as exc: # if the func reports inappropriate usage, give help output lgr.error('%s (%s)' % (exc_str(exc), exc.__class__.__name__)) cmdlineargs.subparser.print_usage() sys.exit(1) except MissingConfigFileError as exc: # TODO: ConfigManager is not finding files in the default locations. lgr.error('%s (%s)' % (exc_str(exc), exc.__class__.__name__)) sys.exit(1) except Exception as exc: # print('%s (%s)' % (exc_str(exc), exc.__class__.__name__)) lgr.error('%s (%s)' % (exc_str(exc), exc.__class__.__name__)) sys.exit(1) if hasattr(cmdlineargs, 'result_renderer'): return cmdlineargs.result_renderer(ret)
lgr.log(5, "Done importing cmdline.main")