Changeset - 95f7524b000e
[Not reviewed]
0 4 0
Brett Smith - 3 years ago 2021-02-24 20:31:23
brettcsmith@brettcsmith.org
cliutil: Add ExtendAction.

This is a user affordance we've wanted for a while, and
query-report *really* wants it.
4 files changed with 85 insertions and 12 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/cliutil.py
Show inline comments
...
 
@@ -37,24 +37,25 @@ from . import rtutil
 

	
 
from typing import (
 
    cast,
 
    Any,
 
    BinaryIO,
 
    Callable,
 
    Container,
 
    Generic,
 
    Hashable,
 
    IO,
 
    Iterable,
 
    Iterator,
 
    List,
 
    NamedTuple,
 
    NoReturn,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Type,
 
    TypeVar,
 
    Union,
 
)
 
from .beancount_types import (
 
    MetaKey,
 
)
...
 
@@ -186,24 +187,60 @@ class ExitCode(enum.IntEnum):
 
    NoConfiguration = os.EX_CONFIG
 
    NoConfig = NoConfiguration
 
    NoDataFiltered = os.EX_DATAERR
 
    NoDataLoaded = os.EX_NOINPUT
 
    OK = os.EX_OK
 
    Ok = OK
 
    RewriteRulesError = os.EX_DATAERR
 

	
 
    # Our own exit codes, working down from that range
 
    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):
 
    def __call__(self,
 
                 parser: argparse.ArgumentParser,
 
                 namespace: argparse.Namespace,
 
                 values: Union[Sequence[Any], str, None]=None,
 
                 option_string: Optional[str]=None,
 
    ) -> NoReturn:
 
        if isinstance(self.const, str):
 
            info = self.const
 
            exitcode = 0
 
        else:
 
            info, exitcode = self.const
conservancy_beancount/reconcile/statement.py
Show inline comments
...
 
@@ -329,28 +329,28 @@ def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace
 
    parser.add_argument(
 
        '--end', '--stop', '-e',
 
        dest='stop_date',
 
        metavar='DATE',
 
        type=cliutil.date_arg,
 
        help="""Date to end reconciliation, exclusive, in YYYY-MM-DD format.
 
The default is one month after the start date.
 
""")
 
    parser.add_argument(
 
        '--account', '-a',
 
        dest='accounts',
 
        metavar='ACCOUNT',
 
        action='append',
 
        help="""Reconcile this account. You can specify this option
 
multiple times. You can specify a part of the account hierarchy, or an account
 
classification from metadata. Default adapts to your search criteria.
 
        action=cliutil.ExtendAction,
 
        help="""Reconcile this account. You can specify multiple
 
comma-separated accounts or classifications, and/or specify this option
 
multiple times. Default adapts to your search criteria.
 
""")
 
    parser.add_argument(
 
        '--id-metadata-key', '-i',
 
        metavar='METAKEY',
 
        help="""Show the named metadata as a posting identifier.
 
Default varies by account.
 
""")
 
    parser.add_argument(
 
        '--statement-metadata-key', '-s',
 
        metavar='METAKEY',
 
        help="""Use the named metadata to determine which postings have already
 
been reconciled. Default varies by account.
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -730,47 +730,50 @@ date was also not specified.
 
""")
 
    # --transactions got merged into --report-type; this is backwards compatibility.
 
    parser.add_argument(
 
        '--transactions',
 
        dest=report_type_arg.dest,
 
        type=cast(Callable, report_type_arg.type),
 
        help=argparse.SUPPRESS,
 
    )
 
    parser.add_argument(
 
        '--account', '-a',
 
        dest='accounts',
 
        metavar='ACCOUNT',
 
        action='append',
 
        help="""Show this account in the report. You can specify this option
 
        action=cliutil.ExtendAction,
 
        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
 
classification from metadata. If not specified, the default set adapts to your
 
search criteria.
 
""")
 
    cliutil.add_rewrite_rules_argument(parser)
 
    parser.add_argument(
 
        '--show-totals', '-S',
 
        metavar='ACCOUNT',
 
        action='append',
 
        action=cliutil.ExtendAction,
 
        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
 
a part of the account hierarchy. The default is all accounts.
 
account balance(s) as well. You can specify multiple comma-separated parts of
 
the account hierarchy, and/or specify this option multiple times.
 
The default is all accounts.
 
""")
 
    parser.add_argument(
 
        '--add-totals', '-T',
 
        metavar='ACCOUNT',
 
        action='append',
 
        action=cliutil.ExtendAction,
 
        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
 
it. You can specify this option multiple times. Pass in a part of the account
 
hierarchy. The default set adapts to your search criteria.
 
it. You can specify multiple comma-separated parts of the account hierarchy,
 
and/or specify this option multiple times. The default set adapts to your
 
search criteria.
 
""")
 
    parser.add_argument(
 
        '--sheet-size', '--size',
 
        metavar='SIZE',
 
        type=int,
 
        default=LedgerODS.SHEET_SIZE,
 
        help="""Try to limit sheets to this many rows. The report will
 
automatically create new sheets to make this happen. When that's not possible,
 
it will issue a warning.
 
""")
 
    parser.add_argument(
 
        '--output-file', '-O',
tests/test_cliutil.py
Show inline comments
...
 
@@ -289,12 +289,45 @@ def test_enum_arg_value_type(arg_enum, name, choice):
 

	
 
@pytest.mark.parametrize('arg', 'az\0')
 
def test_enum_arg_no_value_match(arg_enum, arg):
 
    with pytest.raises(ValueError):
 
        arg_enum.value_type(arg)
 

	
 
def test_enum_arg_choices_str_defaults(arg_enum):
 
    assert arg_enum.choices_str() == ', '.join(repr(c.value) for c in ArgChoices)
 

	
 
def test_enum_arg_choices_str_args(arg_enum):
 
    sep = '/'
 
    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
0 comments (0 inline, 0 general)