Module refinery.lib.argparser

Provides a customized argument parser that is used by all refinery Units.

Expand source code Browse git
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Provides a customized argument parser that is used by all refinery `refinery.units.Unit`s.
"""
from __future__ import annotations

from typing import IO, Optional, List, Dict, Any
from argparse import ArgumentParser, ArgumentError, ArgumentTypeError, Action, RawDescriptionHelpFormatter

import sys

from refinery.lib.tools import terminalfit, get_terminal_size


class ArgparseError(ValueError):
    """
    This custom exception type is thrown from the custom argument parser of
    `refinery.units.Unit` rather than terminating program execution immediately.
    The `parser` parameter is a reference to the argument parser that threw
    the original argument parsing exception with the given `message`.
    """
    def __init__(self, parser, message):
        self.parser = parser
        super().__init__(message)


class LineWrapRawTextHelpFormatter(RawDescriptionHelpFormatter):
    """
    The refinery help text formatter uses the full width of the terminal and prints argument
    options only once after the long name of the option.
    """

    def __init__(self, prog, indent_increment=2, max_help_position=30, width=None):
        super().__init__(prog, indent_increment, max_help_position, width=get_terminal_size())

    def add_text(self, text):
        if isinstance(text, str):
            text = terminalfit(text, width=get_terminal_size())
        return super().add_text(text)

    def _format_action_invocation(self, action):
        if not action.option_strings:
            metavar, = self._metavar_formatter(action, action.dest)(1)
            return metavar
        else:
            parts = []
            if action.nargs == 0:
                parts.extend(action.option_strings)
            else:
                default = action.dest.upper()
                args_string = self._format_args(action, default)
                for option_string in action.option_strings:
                    parts.append(str(option_string))
                parts[-1] += F' {args_string}'
            return ', '.join(parts)


class ArgumentParserWithKeywordHooks(ArgumentParser):
    """
    The refinery argument parser remembers the order of arguments in the property `order`.
    Furthermore, the parser can be initialized with a given set of keywords which will be
    parsed as if they had been passed as keyword arguments on the command line.
    """

    order: List[str]
    keywords: Dict[str, Any]

    class RememberOrder:
        __wrapped__: Action

        def __init__(self, action: Action):
            super().__setattr__('__wrapped__', action)

        def __setattr__(self, name, value):
            return setattr(self.__wrapped__, name, value)

        def __getattr__(self, name):
            return getattr(self.__wrapped__, name)

        def __call__(self, parser: ArgumentParserWithKeywordHooks, *args, **kwargs):
            destination = self.__wrapped__.dest
            if destination not in parser.order:
                parser.order.append(destination)
            return self.__wrapped__(parser, *args, **kwargs)

    def __init__(self, keywords, prog=None, description=None, add_help=True):
        super().__init__(prog=prog, description=description, add_help=add_help,
            formatter_class=LineWrapRawTextHelpFormatter)
        self.keywords = keywords
        self.order = []

    def print_help(self, file: Optional[IO[str]] = None) -> None:
        if file is None:
            sys.stdout.close()
            file = sys.stderr
        return super().print_help(file=file)

    def _add_action(self, action: Action):
        keywords = self.keywords
        if action.dest in keywords:
            action.required = False
            if callable(getattr(action, 'type', None)):
                value = keywords[action.dest]
                if value is not None and isinstance(value, str) and action.type is not str:
                    keywords[action.dest] = action.type(keywords[action.dest])
        return super()._add_action(self.RememberOrder(action))

    def _parse_optional(self, arg_string):
        if isinstance(arg_string, str):
            return super()._parse_optional(arg_string)

    def error_commandline(self, message):
        super().error(message)

    def error(self, message):
        raise ArgparseError(self, message)

    def parse_args_with_nesting(self, args: List[str], namespace=None):
        self.order = []
        args = list(args)
        keywords = self.keywords
        if args and args[~0] and isinstance(args[~0], str):
            nestarg = args[~0]
            nesting = len(nestarg)
            if nestarg.startswith('[]'):
                self.set_defaults(squeeze=True)
                nestarg = nestarg[2:]
                nesting = nesting - 2
            if nestarg == ']' * nesting:
                self.set_defaults(nesting=-nesting)
                del args[~0:]
            elif nestarg == '[' * nesting:
                self.set_defaults(nesting=nesting)
                del args[~0:]
        self.set_defaults(**self.keywords)
        try:
            parsed = self.parse_args(args=args, namespace=namespace)
        except (ArgumentError, ArgumentTypeError, ArgparseError) as e:
            self.error(str(e))
        except Exception as e:
            self.error(F'Failed to parse arguments: {args!r}, {e}, {type(e).__name__}')
        for name in keywords:
            param = getattr(parsed, name, None)
            if param != keywords[name]:
                self.error(
                    F'parameter "{name}" duplicated with conflicting '
                    F'values {param} and {keywords[name]}'
                )
        for name in vars(parsed):
            if name not in self.order:
                self.order.append(name)
        return parsed

Classes

class ArgparseError (parser, message)

This custom exception type is thrown from the custom argument parser of Unit rather than terminating program execution immediately. The parser parameter is a reference to the argument parser that threw the original argument parsing exception with the given message.

Expand source code Browse git
class ArgparseError(ValueError):
    """
    This custom exception type is thrown from the custom argument parser of
    `refinery.units.Unit` rather than terminating program execution immediately.
    The `parser` parameter is a reference to the argument parser that threw
    the original argument parsing exception with the given `message`.
    """
    def __init__(self, parser, message):
        self.parser = parser
        super().__init__(message)

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class LineWrapRawTextHelpFormatter (prog, indent_increment=2, max_help_position=30, width=None)

The refinery help text formatter uses the full width of the terminal and prints argument options only once after the long name of the option.

Expand source code Browse git
class LineWrapRawTextHelpFormatter(RawDescriptionHelpFormatter):
    """
    The refinery help text formatter uses the full width of the terminal and prints argument
    options only once after the long name of the option.
    """

    def __init__(self, prog, indent_increment=2, max_help_position=30, width=None):
        super().__init__(prog, indent_increment, max_help_position, width=get_terminal_size())

    def add_text(self, text):
        if isinstance(text, str):
            text = terminalfit(text, width=get_terminal_size())
        return super().add_text(text)

    def _format_action_invocation(self, action):
        if not action.option_strings:
            metavar, = self._metavar_formatter(action, action.dest)(1)
            return metavar
        else:
            parts = []
            if action.nargs == 0:
                parts.extend(action.option_strings)
            else:
                default = action.dest.upper()
                args_string = self._format_args(action, default)
                for option_string in action.option_strings:
                    parts.append(str(option_string))
                parts[-1] += F' {args_string}'
            return ', '.join(parts)

Ancestors

  • argparse.RawDescriptionHelpFormatter
  • argparse.HelpFormatter

Methods

def add_text(self, text)
Expand source code Browse git
def add_text(self, text):
    if isinstance(text, str):
        text = terminalfit(text, width=get_terminal_size())
    return super().add_text(text)
class ArgumentParserWithKeywordHooks (keywords, prog=None, description=None, add_help=True)

The refinery argument parser remembers the order of arguments in the property order. Furthermore, the parser can be initialized with a given set of keywords which will be parsed as if they had been passed as keyword arguments on the command line.

Expand source code Browse git
class ArgumentParserWithKeywordHooks(ArgumentParser):
    """
    The refinery argument parser remembers the order of arguments in the property `order`.
    Furthermore, the parser can be initialized with a given set of keywords which will be
    parsed as if they had been passed as keyword arguments on the command line.
    """

    order: List[str]
    keywords: Dict[str, Any]

    class RememberOrder:
        __wrapped__: Action

        def __init__(self, action: Action):
            super().__setattr__('__wrapped__', action)

        def __setattr__(self, name, value):
            return setattr(self.__wrapped__, name, value)

        def __getattr__(self, name):
            return getattr(self.__wrapped__, name)

        def __call__(self, parser: ArgumentParserWithKeywordHooks, *args, **kwargs):
            destination = self.__wrapped__.dest
            if destination not in parser.order:
                parser.order.append(destination)
            return self.__wrapped__(parser, *args, **kwargs)

    def __init__(self, keywords, prog=None, description=None, add_help=True):
        super().__init__(prog=prog, description=description, add_help=add_help,
            formatter_class=LineWrapRawTextHelpFormatter)
        self.keywords = keywords
        self.order = []

    def print_help(self, file: Optional[IO[str]] = None) -> None:
        if file is None:
            sys.stdout.close()
            file = sys.stderr
        return super().print_help(file=file)

    def _add_action(self, action: Action):
        keywords = self.keywords
        if action.dest in keywords:
            action.required = False
            if callable(getattr(action, 'type', None)):
                value = keywords[action.dest]
                if value is not None and isinstance(value, str) and action.type is not str:
                    keywords[action.dest] = action.type(keywords[action.dest])
        return super()._add_action(self.RememberOrder(action))

    def _parse_optional(self, arg_string):
        if isinstance(arg_string, str):
            return super()._parse_optional(arg_string)

    def error_commandline(self, message):
        super().error(message)

    def error(self, message):
        raise ArgparseError(self, message)

    def parse_args_with_nesting(self, args: List[str], namespace=None):
        self.order = []
        args = list(args)
        keywords = self.keywords
        if args and args[~0] and isinstance(args[~0], str):
            nestarg = args[~0]
            nesting = len(nestarg)
            if nestarg.startswith('[]'):
                self.set_defaults(squeeze=True)
                nestarg = nestarg[2:]
                nesting = nesting - 2
            if nestarg == ']' * nesting:
                self.set_defaults(nesting=-nesting)
                del args[~0:]
            elif nestarg == '[' * nesting:
                self.set_defaults(nesting=nesting)
                del args[~0:]
        self.set_defaults(**self.keywords)
        try:
            parsed = self.parse_args(args=args, namespace=namespace)
        except (ArgumentError, ArgumentTypeError, ArgparseError) as e:
            self.error(str(e))
        except Exception as e:
            self.error(F'Failed to parse arguments: {args!r}, {e}, {type(e).__name__}')
        for name in keywords:
            param = getattr(parsed, name, None)
            if param != keywords[name]:
                self.error(
                    F'parameter "{name}" duplicated with conflicting '
                    F'values {param} and {keywords[name]}'
                )
        for name in vars(parsed):
            if name not in self.order:
                self.order.append(name)
        return parsed

Ancestors

  • argparse.ArgumentParser
  • argparse._AttributeHolder
  • argparse._ActionsContainer

Subclasses

Class variables

var order
var keywords
var RememberOrder

Methods

def print_help(self, file=None)
Expand source code Browse git
def print_help(self, file: Optional[IO[str]] = None) -> None:
    if file is None:
        sys.stdout.close()
        file = sys.stderr
    return super().print_help(file=file)
def error_commandline(self, message)
Expand source code Browse git
def error_commandline(self, message):
    super().error(message)
def error(self, message)

error(message: string)

Prints a usage message incorporating the message to stderr and exits.

If you override this in a subclass, it should not return – it should either exit or raise an exception.

Expand source code Browse git
def error(self, message):
    raise ArgparseError(self, message)
def parse_args_with_nesting(self, args, namespace=None)
Expand source code Browse git
def parse_args_with_nesting(self, args: List[str], namespace=None):
    self.order = []
    args = list(args)
    keywords = self.keywords
    if args and args[~0] and isinstance(args[~0], str):
        nestarg = args[~0]
        nesting = len(nestarg)
        if nestarg.startswith('[]'):
            self.set_defaults(squeeze=True)
            nestarg = nestarg[2:]
            nesting = nesting - 2
        if nestarg == ']' * nesting:
            self.set_defaults(nesting=-nesting)
            del args[~0:]
        elif nestarg == '[' * nesting:
            self.set_defaults(nesting=nesting)
            del args[~0:]
    self.set_defaults(**self.keywords)
    try:
        parsed = self.parse_args(args=args, namespace=namespace)
    except (ArgumentError, ArgumentTypeError, ArgparseError) as e:
        self.error(str(e))
    except Exception as e:
        self.error(F'Failed to parse arguments: {args!r}, {e}, {type(e).__name__}')
    for name in keywords:
        param = getattr(parsed, name, None)
        if param != keywords[name]:
            self.error(
                F'parameter "{name}" duplicated with conflicting '
                F'values {param} and {keywords[name]}'
            )
    for name in vars(parsed):
        if name not in self.order:
            self.order.append(name)
    return parsed