"""
Implementation of argument parsing.
Should not be imported directly by user code.
"""
import argparse
import typing
import attr
import pyrsistent
def _convert(name):
return pyrsistent.pvector(name.replace('_', '-').split())
@attr.s(frozen=True)
class _Command(object):
"""
Top-level command or subcommand
"""
_name = attr.ib()
_args = attr.ib()
_options = attr.ib()
def rename(self, new_name):
"""
Return a new command with a different name
"""
return attr.evolve(self, name=_convert(new_name))
def _make_parser(self):
my_options = self._options
subcommands = pyrsistent.m()
for thing in self._args:
my_options += thing.get_options()
for thing in self._args:
for name, subcommand in thing.add_to(self._name, my_options):
subcommands = subcommands.set(name, subcommand)
return _Parser(subcommands)
def get_options(self):
"""
Options that should only be inherited
Returns:
empty immutable iterable
"""
return pyrsistent.v()
def add_to(self, parent_name, my_options):
"""
Command-line details to add to subparser
Args:
parent_name (List[str]): name of parent
my_options (Dict[str,thing]): inherited options
Returns:
a tuple: (full_name, options)
"""
full_name = parent_name + self._name
for thing in self._args:
my_options += thing.get_options()
for thing in self._args:
for name, suboptions in thing.add_to(full_name, my_options):
yield name, suboptions
if self._name:
yield full_name, my_options + self._options
def parse(self, args):
"""
Parse command-line
Args:
args (List[str]): command-line arguments
Returns:
immutable map with __caparg_subcommand__ as one of the keys
"""
parser = self._make_parser()
return parser.parse_args(args)
[docs]@attr.s(frozen=True)
class ParseError(ValueError):
"""
Command-line arguments are invalid
"""
message = attr.ib()
class _RaisingArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise ParseError(message)
@attr.s(frozen=True)
class _Parser(object):
_subcommands = attr.ib()
def parse_args(self, args):
"""
Parse arguments
Args:
args (List[str]): command-line arguments
Returns:
immutable map, where one of the keys is __caparg_subcommand__
"""
args = pyrsistent.pvector(args)
candidates = [i
for i in range(1, len(args)+1)
if args[:i] in self._subcommands]
if not candidates:
raise ParseError(self._make_help())
parts = max(candidates)
subcommand, rest = self._subcommands[args[:parts]], args[parts:]
parser = _RaisingArgumentParser(' '.join(args[:parts]))
for thing in subcommand:
thing.add_argument(parser)
namespace = parser.parse_args(rest)
ret = pyrsistent.m(__caparg_subcommand__=args[:parts])
for thing in subcommand:
ret = ret.update(thing.get_value(namespace))
return ret
def _make_help(self):
parts = ["Usage:\n"]
for key in sorted(self._subcommands):
parts.append(" " + " ".join(key) + "\n")
return ''.join(parts)
@attr.s(frozen=True)
class _PreOption(object):
"""
Something that could be turned into an option
An option that lacks a name.
"""
_type = attr.ib()
_required = attr.ib(default=False)
_have_default = attr.ib(default=False)
@attr.s(frozen=True)
class Option(object):
"""
An option
"""
_type = attr.ib()
_required = attr.ib()
_have_default = attr.ib()
_name = attr.ib()
_MISSING = object()
def add_argument(self, parser):
"""
Add ourselves to an argument parser
Args:
parser (argparse.ArgumentParser): the parser to add to
"""
opt_name = '--' + self._name.replace('_', '-')
if self._type == str:
parser.add_argument(opt_name,
type=str,
required=self._required,
default=self._MISSING)
elif self._type == bool:
parser.add_argument(opt_name, action='store_true',
default=False)
elif self._type == typing.List[str]:
parser.add_argument(opt_name, action='append')
else:
raise NotImplementedError("cannot add to parser",
self, parser) # pragma: no cover
def get_value(self, namespace):
"""
Get value out of a namespace
Args:
namespace (argparse.Namespace): the namespace
Returns:
a value
"""
value = getattr(namespace, self._name, self._MISSING)
ret = pyrsistent.m()
if value is None and self._type == typing.List[str]:
value = self._MISSING
if value is not self._MISSING:
ret = ret.set(self._name, value)
elif self._have_default is True:
if self._type == str:
ret = ret.set(self._name, '')
elif self._type == typing.List[str]:
ret = ret.set(self._name, pyrsistent.v())
else: # pragma: no cover
raise NotImplementedError("cannot default value",
self._name, self._type)
return ret
def with_name(self, name):
"""
Save a name
Args:
name (str): The name of the option
Returns:
something with add_argument and get_value
"""
return self.Option(name=name,
type=self._type,
required=self._required,
have_default=self._have_default)
[docs]def command(_name, *args, **kwargs):
"""
A command (or a subcommand)
Args:
_name (str): the name of the subcommand (for top-level, '')
*args (tuple): commands and options
**kwargs (dict): options by name
"""
_name = _convert(_name)
my_options = pyrsistent.pvector(value.with_name(key)
for key, value in kwargs.items())
return _Command(_name, args, my_options)
# pylint: disable=redefined-builtin
[docs]def option(type, required=False, have_default=False):
"""
An option
Note that an option does not know its name. It will usually be used
in a dictionary where the name is specified as its key, and it has
a method :code:`with_name` to add the name when processed.
Args:
type (class): the expected input type
required (bool): whether option name is expected
have_default (bool): whether to auto-create a default based on the type
"""
return _PreOption(type, required=required, have_default=have_default)
# pylint: enable=redefined-builtin
@attr.s(frozen=True)
class _OptionList(object):
"""List of options"""
_options = attr.ib(converter=lambda x:
pyrsistent.pvector(value.with_name(key)
for key, value in x.items()))
def get_options(self):
"""
Return options for current command and subcommands.
Returns:
immutable iterable of things with add_argument and get_value
"""
return self._options
def add_to(self, _parent_name, _options):
"""
Return subcommands to be added
Args:
parent_name (List[str]): full name of the parent
options (List[option interface?]): list of options to inherit
Returns:
empty immutable iterable
"""
return pyrsistent.v()
[docs]def options(**kwargs):
"""
Wrap options
This is used to be able to put options at the beginning of the
argument list of a command. Since options are given by keywords,
they must follow positional arguments."
Args:
**kwargs (Dict[str, option]): Mapping
"""
return _OptionList(pyrsistent.pmap(kwargs))
@attr.s(frozen=True)
class _Positional(object):
"""
Positional argument
"""
_name = attr.ib()
_type = attr.ib()
_required = attr.ib()
_have_default = attr.ib()
_MISSING = object()
def get_options(self):
"""
Return options to be added to current and sub-parsers
Returns:
immutable iterable containing self
"""
return pyrsistent.v(self)
def add_to(self, _parent_name, _options):
"""
Return subcommands to be added
Args:
parent_name (List[str]): full name of the parent
options (List[option interface?]): list of options to inherit
Returns:
empty immutable iterable
"""
return pyrsistent.v()
def add_argument(self, parser):
"""
Add ourselves to a :code:`argparse.ArgumentParser`
Add details of class to an :code:`ArgumentParser` which
will parse this positional.
Args:
parser (argparse.ArgumentParser): the parser
"""
if self._have_default:
raise NotImplementedError("cannot have defaults in positionals",
self._name) # pragma: no cover
if self._type == str:
parser.add_argument(self._name, type=str, default=self._MISSING)
return
raise NotImplementedError("cannot add to parser",
self, parser) # pragma: no cover
def get_value(self, namespace):
"""
Get the value from a 'namespace'.
Args:
namespace (object): something with potentially the named attribute
Returns:
an immutable mapping which is either empty, or has one element
"""
value = getattr(namespace, self._name, self._MISSING)
ret = pyrsistent.m()
if value is not self._MISSING:
ret = ret.set(self._name, value)
# argparse doesn't allow positionals to have defaults
# elif self._have_default is True:
# if self._type == str:
# ret = ret.set(self._name, '')
# else:
# raise NotImplementedError("cannot default value",
# self._name, self._type)
return ret
# pylint: disable=redefined-builtin
[docs]def positional(name, type, required=False, have_default=False):
"""
A positional argument
Args:
name (str): name of argument
type (type): expected type
required (boolean): argument is required (default :code:`False`)
have_default (boolean): if argument is not given, generate
a default (default :code:`False`)
Returns:
Something with add_to and get_value
"""
return _Positional(name, type, required, have_default)
# pylint: enable=redefined-builtin