Skip to content

Parser

Argufy is an inspection based CLI parser.

Parser

Provide CLI parser for function.

__init__(self, *args, **kwargs) special

Initialize parser.

str

The name of the program

str

The string describing the program usage

str

Text to display before the argument help

str

Text to display after the argument help

list

A list of ArgumentParser objects whose arguments should also be included

Object

A class for customizing the help output

char

The set of characters that prefix optional arguments

None

The set of characters that prefix files from which additional arguments should be read

None

The global default value for arguments

Object

The strategy for resolving conflicting optionals

str

Add a -h/--help option to the parser

bool

Allows long options to be abbreviated if the abbreviation is unambiguous

Source code in argufy/parser.py
def __init__(self, *args: str, **kwargs: str) -> None:
    '''Initialize parser.

    Parameters
    ----------
    prog: str
        The name of the program
    usage: str
        The string describing the program usage
    description: str
        Text to display before the argument help
    epilog: str
        Text to display after the argument help
    parents: list
        A list of ArgumentParser objects whose arguments should also
        be included
    formatter_class: Object
        A class for customizing the help output
    prefix_chars: char
        The set of characters that prefix optional arguments
    fromfile_prefix_chars: None
        The set of characters that prefix files from which additional
        arguments should be read
    argument_default: None
        The global default value for arguments
    conflict_handler: Object
        The strategy for resolving conflicting optionals
    add_help: str
        Add a -h/--help option to the parser
    allow_abbrev: bool
        Allows long options to be abbreviated if the abbreviation is
        unambiguous

    '''
    # TODO: handle environment variables

    module = self.__get_parent_module()
    if module:
        docstring = parse(module.__doc__)
        if not kwargs.get('description'):
            kwargs['description'] = docstring.short_description

        if 'prog' not in kwargs:
            kwargs['prog'] = module.__name__.split('.')[0]

    if 'version' in kwargs:
        self.prog_version = kwargs.pop('version')

    # if 'prefix' in kwargs:
    #     self.prefix = kwargs.pop('prefix')
    # else:
    #     self.prefix = kwargs['prog'].upper()
    # log.debug(self.prefix)

    if 'log_level' in kwargs:
        log.setLevel(getattr(logging, kwargs.pop('log_level').upper()))
    if 'log_handler' in kwargs:
        log_handler = kwargs.pop('log_handler')
        log.addHandler(logging.StreamHandler(log_handler))  # type: ignore

    self.use_module_args = kwargs.pop('use_module_args', False)
    self.command_type = kwargs.pop('command_type', None)
    self.command_scheme = kwargs.pop('command_scheme', None)

    if 'formatter_class' not in kwargs:
        self.formatter_class = ArgufyHelpFormatter

    super().__init__(**kwargs)  # type: ignore

    # NOTE: cannot move to formatter
    self._positionals.title = ArgufyHelpFormatter.font(
        self._positionals.title or 'arguments'
    )
    self._optionals.title = ArgufyHelpFormatter.font(
        self._optionals.title or 'flags'
    )

    if hasattr(self, 'prog_version'):
        self.add_argument(
            '--version',
            action='version',
            version=f"%(prog)s {self.prog_version}",
            help='display package version',
        )

add_arguments(self, obj, parser=None)

Add arguments to parser/subparser.

Any

Verious module, function, or arguments that can be inspected.

ArgumentParser, optional

Parser/Subparser that arguments will be added.

Self

Return object itself to allow chaining functions.

Source code in argufy/parser.py
def add_arguments(
    self, obj: Any, parser: Optional[ArgumentParser] = None
) -> 'Parser':
    '''Add arguments to parser/subparser.

    Parameters
    ----------
    obj: Any
        Verious module, function, or arguments that can be inspected.
    parser: ArgumentParser, optional
        Parser/Subparser that arguments will be added.

    Returns
    -------
    self:
        Return object itself to allow chaining functions.

    '''
    if not parser:
        parser = self

    docstring = parse(obj.__doc__)
    signature = inspect.signature(obj)

    # determine keyword arguments from docstring
    for arg in signature.parameters:
        description = self.__get_description(arg, docstring)
        # TODO fix splat arguments
        param = signature.parameters[arg]
        # log.debug(f"{param}, {param.kind}")
        if not param.kind == inspect.Parameter.VAR_KEYWORD:
            arguments = self.__get_args(Argument(description, param))
            name = arguments.pop('name')
            parser.add_argument(*name, **arguments)

    # log.debug(f"params {params}")
    for arg in self.__get_keyword_args(signature, docstring):
        description = self.__get_description(arg, docstring)
        arguments = self.__get_args(Argument(description))
        parser.add_argument(f"--{arg}", **arguments)
    # log.debug(f"arguments {arguments}")
    # TODO for any docstring not collected parse here (args, kwargs)
    # log.debug('docstring params', docstring.params)
    return self

add_commands(self, module, parser=None, exclude_prefixes=(), command_type=None)

Add commands.

ModuleType,

Module used to import functions for CLI commands.

ArgumentParser, optional

Parser used to append subparsers to create subcommands.

tuple,

Methods from a module that should be excluded.

str, optional

Choose format type of commands to be created.

Self

Return object itself to allow chaining functions.

Source code in argufy/parser.py
def add_commands(
    self,
    module: ModuleType,
    parser: Optional[ArgumentParser] = None,
    exclude_prefixes: tuple = tuple(),
    command_type: Optional[str] = None,
) -> 'Parser':
    '''Add commands.

    Parameters
    ----------
    module: ModuleType,
        Module used to import functions for CLI commands.
    parser: ArgumentParser, optional
        Parser used to append subparsers to create subcommands.
    exclude_prefixes: tuple,
        Methods from a module that should be excluded.
    command_type: str, optional
        Choose format type of commands to be created.

    Returns
    -------
    self:
        Return object itself to allow chaining functions.

    '''
    # use self or an existing parser
    if not parser:
        parser = self

    module_name = module.__name__.split('.')[-1]
    docstring = parse(module.__doc__)

    # use exsiting subparser or create a new one
    if not any(isinstance(x, _SubParsersAction) for x in parser._actions):
        parser.add_subparsers(dest=module_name, parser_class=Parser)

    # check if command exists
    command = next(
        (x for x in parser._actions if isinstance(x, _SubParsersAction)),
        None,
    )
    excludes = Parser._get_excludes(exclude_prefixes)

    # set command name scheme
    if command_type is None:
        command_type = self.command_type

    # create subcommand for command
    if command_type == 'subcommand':
        if command:
            msg = docstring.short_description
            subcommand = command.add_parser(
                module_name.replace('_', '-'),
                description=msg,
                formatter_class=ArgufyHelpFormatter,
                help=msg,
            )
            subcommand.set_defaults(mod=module)
            parser.formatter_class = ArgufyHelpFormatter

        # append subcommand to exsiting command or create a new one
        return self.add_commands(
            module=module,
            parser=subcommand,
            exclude_prefixes=Parser._get_excludes(exclude_prefixes),
            command_type='command',
        )

    for name, value in inspect.getmembers(module):
        # TODO: Possible singledispatch candidate
        if not name.startswith(excludes):
            # skip classes for now
            if inspect.isclass(value):
                continue  # pragma: no cover
            # create commands from functions
            elif inspect.isfunction(value):  # or inspect.ismethod(value):
                # TODO: Turn parameter-less function into switch
                if (
                    module.__name__ == value.__module__
                    and not name.startswith(', '.join(excludes))
                ):
                    if command:
                        # control command name format
                        if self.command_scheme == 'chain':
                            cmd_name = f"{module_name}.{name}"
                        else:
                            cmd_name = name
                        msg = parse(value.__doc__).short_description
                        cmd = command.add_parser(
                            cmd_name.replace('_', '-'),
                            description=msg,
                            formatter_class=ArgufyHelpFormatter,
                            help=msg,
                        )
                        cmd.set_defaults(fn=value)
                        parser.formatter_class = ArgufyHelpFormatter
                    # log.debug(f"command {name} {value} {cmd}")
                    self.add_arguments(value, cmd)
            # create arguments from module varibles
            elif (
                self.use_module_args
                and value.__class__.__module__ == 'builtins'
            ):
                # TODO: Reconcile inspect parameters with dict
                arguments = self.__get_args(
                    Argument(
                        self.__get_description(name, docstring),
                        self.__generate_parameter(name, module),
                    )
                )
                name = arguments.pop('name')
                parser.add_argument(*name, **arguments)
    return self

dispatch(self, args=['--line-by-line'], ns=None)

Call command with arguments.

Sequence[str]

Command line arguments passed to the parser.

Optional[Namespace]

Argparse namespace object for a command.

Namespace

Argparse namespace object with command arguments.

Source code in argufy/parser.py
def dispatch(
    self,
    args: Sequence[str] = sys.argv[1:],
    ns: Optional[Namespace] = None,
) -> Optional[Callable[[F], F]]:
    '''Call command with arguments.

    Paramters
    ---------
    args: Sequence[str]
        Command line arguments passed to the parser.
    ns: Optional[Namespace]
        Argparse namespace object for a command.

    Returns
    -------
    List[str]:
        Argparse remaining unparse arguments.
    Namespace:
        Argparse namespace object with command arguments.

    '''
    # parse variables
    arguments, namespace = self.retrieve(args, ns)
    log.debug("%s %s", arguments, namespace)

    # call function with variables
    if 'fn' in namespace:
        fn = vars(namespace).pop('fn')
        namespace = self.__set_module_arguments(fn, namespace)
        fn(**vars(namespace))
    return self.dispatch(arguments) if arguments != [] else None

retrieve(self, args=['--line-by-line'], ns=None)

Retrieve values from CLI.

Sequence[str]

Command line arguments passed to the parser.

Optional[Namespace]

Argparse namespace object for a command.

Namespace

Argparse namespace object with command arguments.

Source code in argufy/parser.py
def retrieve(
    self,
    args: Sequence[str] = sys.argv[1:],
    ns: Optional[Namespace] = None,
) -> Tuple[List[str], Namespace]:
    '''Retrieve values from CLI.

    Paramters
    ---------
    args: Sequence[str]
        Command line arguments passed to the parser.
    ns: Optional[Namespace]
        Argparse namespace object for a command.

    Returns
    -------
    List[str]:
        Argparse remaining unparse arguments.
    Namespace:
        Argparse namespace object with command arguments.

    '''
    # TODO: handle invalid argument

    # show help when no arguments provided
    if args == []:
        args = ['--help']  # pragma: no cover
    main_ns, main_args = self.parse_known_args(args, ns)
    if main_args == [] and 'fn' in vars(main_ns):
        return main_args, main_ns
    else:
        # default to help message for subcommand
        if 'mod' in vars(main_ns):
            a = []
            a.append(vars(main_ns)['mod'].__name__.split('.')[-1])
            a.append('--help')
            self.parse_args(a)
        return main_args, main_ns