rewrite: Support `metakey in value` conditions.

For now, works exactly like ``.account in value``.
This commit is contained in:
Brett Smith 2020-10-24 11:43:11 -04:00
parent ea69d58957
commit 69f597a47c
2 changed files with 49 additions and 2 deletions

View file

@ -82,6 +82,13 @@ Conditions can always use Python's basic comparison operators:
account names. The condition matches when the posting's
account is any of those named accounts, or any of their
respective subaccounts.
---------------- -------------------------------------------------------
``metakey in`` Works analogously for metadata values that are
structured as a colon-separated hierarchy like account
names. The operand is parsed as a space-separated list
of parts of the hierarchy. The condition matches when
the posting's ``metakey`` metadata is under any of
those parts of the hierarchy.
================ =======================================================
Action Operators
@ -141,9 +148,11 @@ accounting equation, Equity = Assets - Liabilities.
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import abc
import collections
import datetime
import decimal
import enum
import functools
import logging
import operator as opmod
import re
@ -290,9 +299,18 @@ class DateTest(Tester[datetime.date]):
class MetadataTest(Tester[Optional[MetaValue]]):
_GETTERS: Mapping[MetaKey, Callable[[str], data.Account]] = \
collections.defaultdict(lambda: functools.lru_cache()(data.Account))
# We use this class variable to share one cache per metadata key being
# tested.
def __init__(self, key: MetaKey, operator: str, operand: str) -> None:
super().__init__(operator, operand)
self.key = key
if operator == 'in':
self._get_hierarchy = self._GETTERS[key]
self.under_args = operand.split()
else:
super().__init__(operator, operand)
@staticmethod
def parse_operand(operand: str) -> str:
@ -301,6 +319,18 @@ class MetadataTest(Tester[Optional[MetaValue]]):
def post_get(self, post: data.Posting) -> Optional[MetaValue]:
return post.meta.get(self.key)
def __call__(self, post: data.Posting) -> bool:
try:
self.under_args
except AttributeError:
return super().__call__(post)
else:
meta_value = self.post_get(post)
if isinstance(meta_value, str):
return self._get_hierarchy(meta_value).is_under(*self.under_args) is not None
else:
return False
class NumberTest(Tester[Decimal]):
@staticmethod

View file

@ -76,6 +76,24 @@ def test_metadata_condition(value, operator):
tester = rewrite.MetadataTest(key, operator, operand)
assert tester(post) == eval(f'value {operator} operand')
@pytest.mark.parametrize('value,expected', [
('Root', False),
('Root:Branch', True),
('Root:Branch:Leaf', True),
('Branch', False),
('RootBranch:Leaf', False),
(None, False),
])
def test_metadata_in_condition(value, expected):
key = 'testkey'
meta = {} if value is None else {key: value}
txn = testutil.Transaction(postings=[
('Income:Other', -5, meta),
])
post, = data.Posting.from_txn(txn)
tester = rewrite.MetadataTest(key, 'in', 'Root:Branch')
assert tester(post) == expected
@pytest.mark.parametrize('value', ['4.5', '4.75', '5'])
@pytest.mark.parametrize('operator', CMP_OPS)
def test_number_condition(value, operator):
@ -118,7 +136,6 @@ def test_parse_good_condition(subject, operator, operand):
'.date in 1990-9-9', # Bad operator
'.number > 0xff', # Bad operand
'.number in 16', # Bad operator
'testkey in foo', # Bad operator
'units.number == 5', # Bad subject (syntax)
'.units == 5', # Bad subject (unknown)
])