cliutil: Add ExtendAction.

This is a user affordance we've wanted for a while, and
query-report *really* wants it.
This commit is contained in:
Brett Smith 2021-02-24 15:31:23 -05:00
parent 6c89febb90
commit 95f7524b00
4 changed files with 85 additions and 12 deletions

View file

@ -46,6 +46,7 @@ from typing import (
IO, IO,
Iterable, Iterable,
Iterator, Iterator,
List,
NamedTuple, NamedTuple,
NoReturn, NoReturn,
Optional, Optional,
@ -195,6 +196,42 @@ class ExitCode(enum.IntEnum):
BeancountErrors = 63 BeancountErrors = 63
class ExtendAction(argparse.Action):
"""argparse action to let a user build a list from a string
This is a fancier version of argparse's built-in ``action='append'``.
The user's input is turned into a list of strings, split by a regexp
pattern you provide. Typical usage looks like::
parser = argparse.ArgumentParser()
parser.add_argument(
'--option', ...,
action=ExtendAction,
const=regexp_pattern, # default is r'\s*,\s*'
...,
)
"""
DEFAULT_PATTERN = r'\s*,\s*'
def __call__(self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[Sequence[Any], str, None]=None,
option_string: Optional[str]=None,
) -> None:
pattern: str = self.const or self.DEFAULT_PATTERN
value: Optional[List[str]] = getattr(namespace, self.dest, None)
if value is None:
value = []
setattr(namespace, self.dest, value)
if values is None:
values = []
elif isinstance(values, str):
values = [values]
for s in values:
value.extend(re.split(pattern, s))
class InfoAction(argparse.Action): class InfoAction(argparse.Action):
def __call__(self, def __call__(self,
parser: argparse.ArgumentParser, parser: argparse.ArgumentParser,

View file

@ -338,10 +338,10 @@ The default is one month after the start date.
'--account', '-a', '--account', '-a',
dest='accounts', dest='accounts',
metavar='ACCOUNT', metavar='ACCOUNT',
action='append', action=cliutil.ExtendAction,
help="""Reconcile this account. You can specify this option help="""Reconcile this account. You can specify multiple
multiple times. You can specify a part of the account hierarchy, or an account comma-separated accounts or classifications, and/or specify this option
classification from metadata. Default adapts to your search criteria. multiple times. Default adapts to your search criteria.
""") """)
parser.add_argument( parser.add_argument(
'--id-metadata-key', '-i', '--id-metadata-key', '-i',

View file

@ -739,8 +739,9 @@ date was also not specified.
'--account', '-a', '--account', '-a',
dest='accounts', dest='accounts',
metavar='ACCOUNT', metavar='ACCOUNT',
action='append', action=cliutil.ExtendAction,
help="""Show this account in the report. You can specify this option help="""Show this account in the report. You can specify multiple
comma-separated accounts or classifications, and/or specify this option
multiple times. You can specify a part of the account hierarchy, or an account multiple times. You can specify a part of the account hierarchy, or an account
classification from metadata. If not specified, the default set adapts to your classification from metadata. If not specified, the default set adapts to your
search criteria. search criteria.
@ -749,19 +750,21 @@ search criteria.
parser.add_argument( parser.add_argument(
'--show-totals', '-S', '--show-totals', '-S',
metavar='ACCOUNT', metavar='ACCOUNT',
action='append', action=cliutil.ExtendAction,
help="""When entries for this account appear in the report, include help="""When entries for this account appear in the report, include
account balance(s) as well. You can specify this option multiple times. Pass in account balance(s) as well. You can specify multiple comma-separated parts of
a part of the account hierarchy. The default is all accounts. the account hierarchy, and/or specify this option multiple times.
The default is all accounts.
""") """)
parser.add_argument( parser.add_argument(
'--add-totals', '-T', '--add-totals', '-T',
metavar='ACCOUNT', metavar='ACCOUNT',
action='append', action=cliutil.ExtendAction,
help="""When an account could be included in the report but does not help="""When an account could be included in the report but does not
have any entries in the date range, include a header and account balance(s) for have any entries in the date range, include a header and account balance(s) for
it. You can specify this option multiple times. Pass in a part of the account it. You can specify multiple comma-separated parts of the account hierarchy,
hierarchy. The default set adapts to your search criteria. and/or specify this option multiple times. The default set adapts to your
search criteria.
""") """)
parser.add_argument( parser.add_argument(
'--sheet-size', '--size', '--sheet-size', '--size',

View file

@ -298,3 +298,36 @@ def test_enum_arg_choices_str_defaults(arg_enum):
def test_enum_arg_choices_str_args(arg_enum): def test_enum_arg_choices_str_args(arg_enum):
sep = '/' sep = '/'
assert arg_enum.choices_str(sep, '{}') == sep.join(c.value for c in ArgChoices) assert arg_enum.choices_str(sep, '{}') == sep.join(c.value for c in ArgChoices)
@pytest.mark.parametrize('values,sep', testutil.combine_values(
[['foo'], ['bar', 'baz'], ['qu', 'quu', 'quux']],
[',', ', ', ' ,', ' , '],
))
def test_extend_action_once(values, sep):
action = cliutil.ExtendAction(['-t'], 'result')
args = argparse.Namespace()
action(None, args, sep.join(values), '-t')
assert args.result == values
def test_extend_action_multi():
action = cliutil.ExtendAction(['-t'], 'result')
args = argparse.Namespace()
action(None, args, 'foo,bar', '-t')
action(None, args, 'baz, quux', '-t')
assert args.result == ['foo', 'bar', 'baz', 'quux']
def test_extend_action_from_default():
action = cliutil.ExtendAction(['-t'], 'result')
args = argparse.Namespace(result=['foo'])
action(None, args, 'bar , baz', '-t')
assert args.result == ['foo', 'bar', 'baz']
@pytest.mark.parametrize('pattern,expected', [
(',', ['foo', ' bar']),
(r'\s+', ['foo,', 'bar']),
])
def test_extend_action_custom_pattern(pattern, expected):
action = cliutil.ExtendAction(['-t'], 'result', const=pattern)
args = argparse.Namespace()
action(None, args, 'foo, bar', '-t')
assert args.result == expected