Source code for utils.cli_interface

"""
Module
------

    cli_interface.py

Description
-----------

    This module contains functions to be used for any command line
    interface (CLI) argument collection(s) and schema validation(s).

Classes
-------

   CLIParser()

       This is the base-class object for the command-line interface
       (CLI) argument parsing for each Python SimpleNamespace object
       for the CLI argument(s) valid schema keys can be found at
       https://tinyurl.com/argparse-objects.

Functions
---------

    __checkschema__(parser, options_obj, schema_path, write_table = False,
                    logger_method = "info")

        This function validates the CLI argument schema.

    __get_knownargs__(parser)

        This function collects the known (i.e., mandatory) arguments
        from the CLI.

    __get_otherargs__(parser, options_obj)

        This function collects any ancillary arguments from the CLI
        and updates the `options_obj` SimpleNamespace.

    init(args_objs, description = None, prog = None, epilog = None,
        formatter_class=RichHelpFormatter)

        This function initializes a Python ArgumentParser object in
        accordance with the specified argument SimpleNamespace objects
        and the respective keyword attributes.

    options(options(parser, validate_schema=False, schema_path=None)

        This function defines a Python SimpleNamespace object
        containing both the mandatory and the ancillary CLI arguments.

Requirements
------------

- rich_argparse; https://github.com/hamdanal/rich-argparse

Author(s)
---------

    Henry R. Winterbottom; 04 June 2023

History
-------

    2023-06-04: Henry Winterbottom -- Initial implementation.

"""

# ----

# pylint: disable=too-few-public-methods

# ----

import os
from argparse import ArgumentParser
from collections import OrderedDict
from pydoc import locate
from types import SimpleNamespace
from typing import Any, Generic, Tuple

from confs.yaml_interface import YAML
from rich_argparse import RichHelpFormatter
from tools import fileio_interface, parser_interface

from utils import schema_interface
from utils.exceptions_interface import CLIInterfaceError

# ----

# Define all available module properties.
__all__ = ["CLIParser", "init", "options"]

# ----

# Set the default ArgumentParser formatter class attributes.
RichHelpFormatter.styles["argparse.args"] = "green"
RichHelpFormatter.styles["argparse.metavar"] = "cyan"
RichHelpFormatter.styles["argparse.text"] = "default"
RichHelpFormatter.styles["argparse.help"] = "blue_violet"

# ----


class CLIParser:
    """
    Description
    -----------

    This is the base-class object for the command-line interface (CLI)
    argument parsing for each Python SimpleNamespace object for the
    CLI argument(s) valid schema keys can be found at
    https://tinyurl.com/argparse-objects.

    """

    def __init__(self: Generic):
        """
        Description
        -----------

        Creates a new CLIParser object.

        """

        # Define the base-class attributes.
        if parser_interface.enviro_get(envvar="CLI_SCHEMA") is not None:
            cli_yaml = parser_interface.enviro_get(envvar="CLI_SCHEMA")
        else:
            cli_yaml = os.path.join(os.getcwd(), "cli_schema.yaml")
        if not fileio_interface.fileexist(path=cli_yaml):
            msg = f"The CLI schema file {cli_yaml} does not exist. Aborting!!!"
            raise CLIInterfaceError(msg=msg)
        self.cli_obj = YAML().read_yaml(yaml_file=cli_yaml, return_obj=True)

    def build(self: Generic) -> Tuple[SimpleNamespace]:
        """
        Description
        -----------

        This method defines and defines all CLI argument
        SimpleNamespace object tuples.

        Returns
        -------

        args_objs: ``Tuple[SimpleNamespace]``

            A Python tuple of SimpleNamespace objects containing the
            CLI argument(s) attributes.

        """

        # Collect all CLI attributes from the specified schema.
        args_tuple = ()
        for cli_arg in vars(self.cli_obj):
            args_cli_obj = parser_interface.object_define()
            args_obj = parser_interface.dict_toobject(
                in_dict=parser_interface.object_getattr(
                    object_in=self.cli_obj, key=cli_arg
                )
            )
            args_cli_obj = parser_interface.object_setattr(
                object_in=args_cli_obj, key=cli_arg, value=args_obj
            )
            args_tuple = args_tuple + (args_cli_obj,)

        return args_tuple


# ----


def __checkschema__(
    options_obj: SimpleNamespace,
    schema_path: str,
    write_table: bool = False,
    logger_method: str = "info",
) -> OrderedDict:
    """
    Description
    -----------

    This function validates the CLI argument schema.

    Parameters
    ----------

    options_obj: ``SimpleNamespace``

        A Python SimpleNamespace containing the specified CLI
        arguments.

    schema_path: ``str``

        A Python string specifying the path to the YAML-formatted file
        containing the CLI argument schema.

    Keywords
    ---------

    write_table: ``bool``, optional

        A Python boolean valued variable specifying whether to write
        the schema attributes table using the specified logger method.

    logger_method: ``str``, optional

        A Python string specifying the logger method to be usedf to
        write the schema attributes table.

    Returns
    -------

    options_obj: OrderdDict

        A Python OrderedDict object containing the arguments/options
        collected via the CLI.

    """

    # Validate the CLI argument schema.
    cls_schema = schema_interface.build_schema(YAML().read_yaml(yaml_file=schema_path))
    cls_opts = parser_interface.object_todict(object_in=options_obj)
    options_obj = schema_interface.validate_schema(
        cls_schema=cls_schema,
        cls_opts=cls_opts,
        write_table=write_table,
        logger_method=logger_method.lower(),
    )

    return options_obj


# ----


def __get_knownargs__(parser: ArgumentParser) -> SimpleNamespace:
    """
    Description
    -----------

    This function collects the known (i.e., mandatory) arguments from
    the CLI.

    Parameters
    ----------

    parser: ``ArgumentParser``

        A Python ArgumentParser object containing the CLI arguments.

    Returns
    -------

    options_obj: ``SimpleNamespace``

        A Python SimpleNamespace containing the (any) mandatory CLI
        arguments.

    """

    # Collect any mandatory arguments from the CLI.
    options_obj = parser_interface.object_define()
    args_obj = parser.parse_known_args()[0]
    for opt in vars(args_obj):
        options_obj = parser_interface.object_setattr(
            object_in=options_obj,
            key=opt,
            value=parser_interface.object_getattr(
                object_in=args_obj, key=opt, force=True
            ),
        )

    return options_obj


# ----


def __get_otherargs__(
    parser: ArgumentParser, options_obj: SimpleNamespace
) -> SimpleNamespace:
    """
    Description
    -----------

    This function collects any ancillary arguments from the CLI and
    updates the `options_obj` SimpleNamespace.

    Parameters
    ----------

    parser: ``ArgumentParser``

        A Python ArgumentParser object containing the CLI arguments.

    options_obj: ``SimpleNamespace``

        A Python SimpleNamespace containing the (any) mandatory CLI
        arguments.

    Returns
    -------

    options_obj: ``SimpleNamespace``

        A Python SimpleNamespace object specified upon entry and
        updated with any ancillary CLI arguments.

    """

    # Get the ancillary arguments from the CLI.
    otherargs_list = parser.parse_known_intermixed_args()[1]
    for idx in range(0, len(otherargs_list), 1):
        options_obj = parser_interface.object_setattr(
            object_in=options_obj,
            key="".join(otherargs_list[idx].split("-")[::-1]),
            value=otherargs_list[idx],
        )

    return options_obj


# ----


[docs]def init( args_objs: Tuple[SimpleNamespace], description: str = None, prog: str = None, epilog: str = None, formatter_class: Any = RichHelpFormatter, ) -> ArgumentParser: """ Description ----------- This function initializes a Python ArgumentParser object in accordance with the specified argument SimpleNamespace objects and the respective keyword attributes. Parameters ---------- args_objs: ``Tuple[SimpleNamespace]`` A Python tuple of SimpleNamespace objects containing the mandatory, optional, and task arguments. Keywords -------- description: ``str``, optional A Python string defining the purpose of the respective application/program. prog: ``str``, optional A Python string specifying the program name. epilog: ``str``, optional A Python string specifying text to be provided at the bottom of a `help` type message. formatter_class: ``Any``, optional A Python Argparse customizing class. Returns ------- parser: ``ArgumentParser`` A Python ArgumentParser object. Raises ------ CLIInterfaceError: - raised if the attribute `longname` has not been specified in an `args_obj` SimpleNamespace object. - raised if an exception is encountered while initializing the CLI. """ # Initialize the CLI. parser = ArgumentParser( prog=prog, description=description, epilog=epilog, formatter_class=formatter_class, ) # Define the respective mandatory, optional, and task application # attributes accordingly. for args_item_obj in args_objs: # Define the attributes for the respective CLI argument. arg_obj = parser_interface.object_define() arg_key = list(parser_interface.object_todict(object_in=args_item_obj).keys())[ 0 ] arg_dict = dict( [key, value] for (key, value) in vars( parser_interface.object_getattr( object_in=args_item_obj, key=arg_key, force=True ) ).items() if key not in ["action", "longname", "required", "shortname", "type"] ) arg_obj.required = parser_interface.object_getattr( object_in=parser_interface.object_getattr( object_in=args_item_obj, key=arg_key ), key="required", force=True, ) if arg_obj.required is None: arg_obj.required = False # Define the minimal argument attributes. arg_obj.longname = parser_interface.object_getattr( object_in=args_item_obj, key=arg_key, force=True ).longname try: arg_dict["type"] = locate( parser_interface.object_getattr( object_in=args_item_obj, key=arg_key, force=True ).type ) except AttributeError: try: arg_dict["action"] = parser_interface.object_getattr( object_in=args_item_obj, key=arg_key, force=True ).action except Exception as errmsg: msg = ( f"Defining the parser attributes for {arg_key} failed with " f"error {errmsg}. Aborting!!!" ) raise CLIInterfaceError(msg=msg) from errmsg try: arg_obj.shortname = parser_interface.object_getattr( object_in=args_item_obj, key=arg_key, force=True ).shortname except AttributeError: arg_obj.shortname = None try: if arg_obj.required: parser.add_argument(f"{arg_obj.longname}", **arg_dict) if not arg_obj.required: if (arg_obj.longname is not None) and (arg_obj.shortname is not None): parser.add_argument( f"--{arg_obj.longname}", f"-{arg_obj.shortname}", **arg_dict ) if (arg_obj.longname is not None) and (arg_obj.shortname is None): parser.add_argument(f"-{arg_obj.longname}", **arg_dict) except Exception as errmsg: msg = f"Initializing the CLI failed with error {errmsg}. Aborting!!!" raise CLIInterfaceError(msg=msg) from errmsg return parser
# ----
[docs]def options( parser: ArgumentParser, validate_schema: bool = False, schema_path: str = None ) -> SimpleNamespace: """ Description ----------- This function defines a Python SimpleNamespace object containing both the mandatory and the ancillary CLI arguments. Parameters ---------- parser: ``ArgumentParser`` A Python ArgumentParser object containing the CLI arguments. Keywords -------- validate_schema: ``bool``, optional A Python boolean valued variable specifying whether to validate the schema for the respective CLI arguments; if `True` upon entry, either the CLI arguments must contain the attribute `schema` or the `schema_path` must be specified upon entry. schema_path: ``str``, optional A Python string specifying the path to the YAML-formatted file containing the CLI argument schema. Raises ------ CLIInterfaceError: - raised if the `schema_path` attribute is NoneType when attempting to validate the CLI argument schema. """ # Define the CLI arguments. options_obj = __get_knownargs__(parser=parser) options_obj = __get_otherargs__(parser=parser, options_obj=options_obj) # Validate the CLI argument schema; proceed accordingly. if validate_schema: if options_obj.schema is not None: schema_path = options_obj.schema if schema_path is None: msg = ( "The CLI arguments cannot be validated without a specified " "schema. Aborting!!!" ) raise CLIInterfaceError(msg=msg) options_dict = __checkschema__(options_obj=options_obj, schema_path=schema_path) options_obj = parser_interface.dict_toobject(in_dict=options_dict) return options_obj