Source code for tracktable.script_helpers.argument_groups.utilities

#
# Copyright (c) 2014-2017 National Technology and Engineering
# Solutions of Sandia, LLC. Under the terms of Contract DE-NA0003525
# with National Technology and Engineering Solutions of Sandia, LLC,
# the U.S. Government retains certain rights in this software.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""INTERNAL: Implementations for manipulating argument groups

"""

from __future__ import print_function

import string
import sys
import textwrap

__all__ = [ '_create_argument_group', '_add_argument', '_available_argument_groups', '_use_argument_group', '_extract_arguments' ]

_ARGUMENT_GROUPS = {}

def _create_argument_group(group_name,
                           title=None,
                           description=None):
    """Register a new, empty group of arguments

    Arguments to configure different capabilities tend to come in
    groups.  For example, movie-making includes frames per second,
    movie duration, encoder type and encoder options.  This function
    lets you create such subject-related groups.

    Note that this function only *creates* the group.  You still have to
    populate it using register_argument (q.v.).

    Args:
       group_name (string): Name of the group you want to add.
       title (string): Title of the group.  Not required but highly recommended.
       description (string): Description / help text.  Not required but highly recommended.

    Returns:
       The name of the group just added.

    """

    global _ARGUMENT_GROUPS
    group = _ARGUMENT_GROUPS.get(group_name, None)
    if group is None:
        group = { 'name': group_name, 'args': dict() }
        _ARGUMENT_GROUPS[group_name] = group

    group['title'] = title
    group['description'] = description

    return group_name

# ----------------------------------------------------------------------

def _add_argument(group_name,
                  option_names,
                  **kwargs):

    """Add a single command-line argument to a group.

    Args:
       group_name (string):  Name for conceptual group of arguments (such as 'movie' for movie-making parameters)
       option_names (list):  A list of command-line options that can be used to specify this argument (such as [ '--frame-rate', '-f' ])

       All other arguments will be passed straight to argparse.add_argument() when this argument group is requested.

    Returns:
       The name of the argument just added.

    Raises:
       KeyError: the specified argument group does not exist.

    Examples:

       >>> add_argument('movies', [ '--frame-rate', '-f' ],
                        help='Desired frame rate for movie',
                        type=int,
                        default=30)
       '--frame-rate'

       >>> add_argument('nonexistent_group', [ '--foo', '-g' ])
       XXX INSERT ERROR MESSAGES

    """

    global _ARGUMENT_GROUPS

    group = _ARGUMENT_GROUPS[group_name]

    main_name = option_names[0]
    group['args'][main_name] = { 'names': option_names, 'info': kwargs }

    return main_name

# ----------------------------------------------------------------------

def _use_argument_group(group_name, parser):
    """Add a group of arguments to a parser.

    Args:
       group_name: Name of the desired group of arguments
       parser:      An instance of argparse.ArgumentParser

    Returns:
       Parser after the arguments have been added

    Raises:
       KeyError: the desired argument group does not exist

    Once you have registered one or more arguments in a group, call
    use_arguments to add them to a parser.  They will be added to a
    group in that parser.

    Examples:
    XXX TODO
    """

    global _ARGUMENT_GROUPS
    arg_group = _ARGUMENT_GROUPS[group_name]

    subparser = parser.add_argument_group(title=arg_group['title'],
                                          description=arg_group['description'])

    for (main_name, info) in arg_group['args'].items():
        arg_names = info['names']
        arg_parameters = info['info']
        subparser.add_argument(*arg_names, **arg_parameters)

    return parser


# ----------------------------------------------------------------------

def _available_argument_groups():
    """Return a list of all available argument groups.

    Args:
      None

    Returns:
      A list of strings, each the name of a registered argument group
    """

    global _ARGUMENT_GROUPS
    return _ARGUMENT_GROUPS.keys()

# ----------------------------------------------------------------------

def _extract_arguments(group_name, parsed_args, switch_character='-'):
    """Extract a group of arguments from a Namespace into a dict

    Argument groups make it easy to include a batch of options all at
    once.  This function is the next step: after you've used
    argparse.ArgumentParser to parse a set of command-line arguments,
    you use extract_arguments to extract a group of arguments
    into a dict.  This dict can then be passed on to a function as a
    set of keyword arguments.

    The difference between this and calling vals() on the namespace
    returned by parse_args() is that this pulls out just the arguments
    associated with the specified group.

    Args:
       group_name:       A string naming the group to extract
       parsed_args:      A Namespace object returned from argparse.ArgumentParser.parse_args()
       switch_character: Character used to prefix switches, e.g. '-' for options like '--foo' and '-f'

    Returns:
       A dict() whose names are the destinations (*) associated with
       the command-line arguments and whose values are the values from
       the command line

    Raises:
       KeyError: The desired argument group doesn't exist

    """


    result = {}

    global _ARGUMENT_GROUPS
    arg_group = _ARGUMENT_GROUPS.get(group_name, None)
    if arg_group is None:
        raise KeyError("extract_arguments: No such group '{}'.".format(group_name))

    if isinstance(parsed_args, dict):
        parsed_values = dict(parsed_args)
    else:
        parsed_values = vars(parsed_args)

    for arg in arg_group['args'].values():
        destvar_name = _arg_to_destvar(arg)
        if destvar_name not in parsed_values:
            raise KeyError("WARNING: extract_arguments: Destination '{}' not found for argument '{}' in group '{}'\n".format(destvar_name, arg['names'][0], group_name))
            continue
        result[destvar_name] = parsed_values[destvar_name]

    return result

# ----------------------------------------------------------------------

def _arg_to_destvar(arg, switch_character='-'):
    """INTERNAL: Look up the destination variable an argument writes into

    The argparse library allows you to specify the name of the
    property into which an argument's value will be stored.  This
    function abstracts that away.  When you call it you will get back
    our best guess at the property that will contain values for the
    specified argument.

    Args:
      arg (dict): Entry from argument group
      switch_character (character): Optional switch character, defaults to '-'

    Returns:
      String containing name of property for argument
    """

    arg_info = arg['info']

    if 'dest' in arg_info:
        return arg_info['dest']
    else:
        original_name = arg['names'][0]
        destvar_name = original_name.lstrip(switch_character).replace('-', '_')
        return destvar_name