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