Source code for improver.argparser

# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# (C) British Crown Copyright 2017-2019 Met Office.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * 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.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# 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.
"""Common option utilities for improver CLIs."""

from argparse import ArgumentParser

from improver.profile import profile_hook_enable


[docs]class ArgParser(ArgumentParser): """Argument parser for improver CLIs. The main purpose of this class is to make it easier to create CLIs which have arguments which are selected from centralized collections. To fulfil these requirements, we define 2 class level dictionaries, ArgParser.CENTRALIZED_ARGUMENTS, and ArgParser.COMPULSORY_ARGUMENTS. For these dictionaries, each element has: - a key, which is a string representing the argument name - used internally to refer to a particular argument (which, in the case of the CENTRALIZED_ARUGMENTS may be selected from when creating an instance of the ArgParser) - a value, which is a list containing 2 elements: 1. a list of strings containing the different flags which are associated with the argument (ie.: the first argument to the add_arguments() method, e.g: ['--profile', '-p']) 2. a dictionary containing all of the kwargs which are passed to the add_argument() method (e.g: {'action': 'store_true', 'default': False, 'help': ... }) The CENTRALIZED_ARGUMENTS will be selected from, as necessary, for each of the CLIs that we create, and the COMPULSORY_ARGUMENTS will be automatically added to all CLIs (with no option to exclude them). ArgParser.DEFAULT_CENTRALIZED_ARG_NAMES defines the centralized arguments which are to be included by default when creating instances of this class (i.e: when nothing is explictly passed into the constructor). This is a tuple containing keys associated with the ArgParser.CENTRALIZED_ARGUMENTS dictionary. """ # Ideally, all CLIs should select something from this dictionary: # NB: --help included by default with ArgumentParser CENTRALIZED_ARGUMENTS = { 'input_file': ( ['input_filepath'], {'metavar': 'INPUT_FILE', 'help': 'A path to an input NetCDF file to be processed'}), 'output_file': ( ['output_filepath'], {'metavar': 'OUTPUT_FILE', 'help': 'The output path for the processed NetCDF'}), } # *All* CLIs will use the options here (no option to disable them): COMPULSORY_ARGUMENTS = { 'profile': ( ['--profile'], {'action': 'store_true', 'help': 'Switch on profiling information.'}), 'profile_file': ( ['--profile_file'], {'metavar': 'PROFILE_FILE', 'help': 'Dump profiling info to a file. Implies --profile.'}) } # We can override including these, but options common to everything should # be in a list here: # DEFAULT_CENTRALIZED_ARG_NAMES = ('input_file', 'output_file') DEFAULT_CENTRALIZED_ARG_NAMES = ()
[docs] def __init__(self, central_arguments=DEFAULT_CENTRALIZED_ARG_NAMES, specific_arguments=None, **kwargs): """Create an ArgParse instance, which is a subclass of argparse.ArgumentParser and automatically add all of the arguments. (Note: The ArgParse.COMPULSORY_ARGUMENTS are always added.) Args: central_arguments (list): A list containing the centralized arguments we require. (Keys of the centralized argument dictionary). By default this is set as ArgParse.DEFAULT_CENTRALIZED_ARG_NAMES. specific_arguments (list): A list of argument specifications required to add arguments which are not contained within the centralized argument dictionary. The format of these argument specifications should be the same as the values in the ArgParser.CENTRALIZED_ARGUMENTS dictionary. (For more details, see the add_arguments method). Default is None, which does not add additional arguments. kwargs (dictionary): Additional keyword arguments which are passed to the superclass constructor (argparse.ArgumentParser), e.g: the `description` of the ArgumentParser. """ # Allow either central_arguments or specific_arguments to be None # (or empty lists) if central_arguments is None: central_arguments = [] if specific_arguments is None: specific_arguments = [] # argspecs of the compulsory arguments (no switch here) compulsory_arguments = list(ArgParser.COMPULSORY_ARGUMENTS.values()) # get argspecs of the central arguments from the list of keys passed in central_arguments = [ArgParser.CENTRALIZED_ARGUMENTS[arg_name] for arg_name in central_arguments] # create instance of ArgumentParser (pass along kwargs) super(ArgParser, self).__init__(**kwargs) # all arguments cli_arguments = (compulsory_arguments + central_arguments + specific_arguments) # automatically add all of the arguments self.add_arguments(cli_arguments)
# Done. Now we can get the arguments with self.parse_args()
[docs] def add_arguments(self, argspec_list): """Adds a list of arguments to the ArgumentParser. The input argspec_list is a list of argument specifications, where each element (argument specification) is a tuple/list of length 2. The first element of an argument specification is a list of strings which the name/flags used to add the argument. The second element of the argument spec shall be a dictionary containing the keyword arguments which are passed into the add_argument() method. Args: argspec_list (list): A list containing the specifications required to add the arguments (see above) Raises: AttributeError: Notifies the user if any of the argument specifications has the wrong length (not 2). """ for argspec in argspec_list: if len(argspec) != 2: raise AttributeError( "The argument specification has an unexpected length. " "Each argument specification should be a 2-tuple, of a " "list (of strings) and a dictionary.") argflags, argkwargs = argspec self.add_argument(*argflags, **argkwargs)
[docs] def parse_args(self, args=None, namespace=None): """Wrap in order to implement some compulsory behaviour.""" args = super(ArgParser, self).parse_args(args=args, namespace=namespace) if hasattr(args, 'profile') and (args.profile or args.profile_file): profile_hook_enable(dump_filename=args.profile_file) return args
[docs] def wrong_args_error(self, args, method): """Raise a parser error. Some CLI scripts have multiple methods of carrying out an action, with each method having different arguments. This method provides a standard error to be used when incompatible method-argument combinations are passed in - ie: when there are mutually exclusive groups of arguments. Args: args (string): The incompatible arguments method (string): The method with which the arguments are incompatible Raises: parser.error: To notify user of incompatible method-argument combinations. """ msg = 'Method: {} does not accept arguments: {}' self.error(msg.format(method, args))
[docs]def safe_eval(command, module, allowed): """ A wrapper for the python eval() function that enforces the use of a list of allowable commands and excludes python builtin functions. This enables the use of an eval statement to convert user string input into a function or method without it being readily possible to trigger malicious code. Args: command (string): A string identifying the function/method/object that is to be returned from the provided module. module (module): The python module from within which the function/method/object is to be found. allowed (list): A list of the functions/methods/objects that the user is allowed to request. Returns: function/method/object: The desired function, method, or object. Raises: TypeError if the requested module component is not allowed or does not exist. """ no_builtins = {"__builtins__": None} safe_dict = {k: module.__dict__.get(k, None) for k in allowed} try: result = eval('{}'.format(command), no_builtins, safe_dict) except TypeError: raise TypeError( 'Function/method/object "{}" not available in module {}.'.format( command, module.__name__)) return result