rewrite: Support `metakey in value
` conditions.
For now, works exactly like ``.account in value``.
This commit is contained in:
parent
ea69d58957
commit
69f597a47c
2 changed files with 49 additions and 2 deletions
|
@ -82,6 +82,13 @@ Conditions can always use Python's basic comparison operators:
|
||||||
account names. The condition matches when the posting's
|
account names. The condition matches when the posting's
|
||||||
account is any of those named accounts, or any of their
|
account is any of those named accounts, or any of their
|
||||||
respective subaccounts.
|
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
|
Action Operators
|
||||||
|
@ -141,9 +148,11 @@ accounting equation, Equity = Assets - Liabilities.
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import enum
|
import enum
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import operator as opmod
|
import operator as opmod
|
||||||
import re
|
import re
|
||||||
|
@ -290,9 +299,18 @@ class DateTest(Tester[datetime.date]):
|
||||||
|
|
||||||
|
|
||||||
class MetadataTest(Tester[Optional[MetaValue]]):
|
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:
|
def __init__(self, key: MetaKey, operator: str, operand: str) -> None:
|
||||||
super().__init__(operator, operand)
|
|
||||||
self.key = key
|
self.key = key
|
||||||
|
if operator == 'in':
|
||||||
|
self._get_hierarchy = self._GETTERS[key]
|
||||||
|
self.under_args = operand.split()
|
||||||
|
else:
|
||||||
|
super().__init__(operator, operand)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_operand(operand: str) -> str:
|
def parse_operand(operand: str) -> str:
|
||||||
|
@ -301,6 +319,18 @@ class MetadataTest(Tester[Optional[MetaValue]]):
|
||||||
def post_get(self, post: data.Posting) -> Optional[MetaValue]:
|
def post_get(self, post: data.Posting) -> Optional[MetaValue]:
|
||||||
return post.meta.get(self.key)
|
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]):
|
class NumberTest(Tester[Decimal]):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -76,6 +76,24 @@ def test_metadata_condition(value, operator):
|
||||||
tester = rewrite.MetadataTest(key, operator, operand)
|
tester = rewrite.MetadataTest(key, operator, operand)
|
||||||
assert tester(post) == eval(f'value {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('value', ['4.5', '4.75', '5'])
|
||||||
@pytest.mark.parametrize('operator', CMP_OPS)
|
@pytest.mark.parametrize('operator', CMP_OPS)
|
||||||
def test_number_condition(value, operator):
|
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
|
'.date in 1990-9-9', # Bad operator
|
||||||
'.number > 0xff', # Bad operand
|
'.number > 0xff', # Bad operand
|
||||||
'.number in 16', # Bad operator
|
'.number in 16', # Bad operator
|
||||||
'testkey in foo', # Bad operator
|
|
||||||
'units.number == 5', # Bad subject (syntax)
|
'units.number == 5', # Bad subject (syntax)
|
||||||
'.units == 5', # Bad subject (unknown)
|
'.units == 5', # Bad subject (unknown)
|
||||||
])
|
])
|
||||||
|
|
Loading…
Add table
Reference in a new issue