From 536b50b478d889b89982057bf026f32446ea4949 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Mon, 11 May 2020 09:52:05 -0400 Subject: [PATCH] plugin: Don't validate transactions flagged with !. RT#10591. --- conservancy_beancount/plugin/core.py | 24 ++++++++++++------- .../plugin/meta_repo_links.py | 9 +++---- conservancy_beancount/plugin/meta_rt_links.py | 9 +++---- tests/test_meta_approval.py | 7 ++++++ tests/test_meta_invoice.py | 11 +++++++++ tests/test_meta_payable_documentation.py | 3 +++ tests/test_meta_receipt.py | 7 ++++++ tests/test_meta_receivable_documentation.py | 4 ++++ tests/test_meta_repo_links.py | 10 ++++++++ tests/test_meta_rt_links.py | 10 ++++++++ 10 files changed, 77 insertions(+), 17 deletions(-) diff --git a/conservancy_beancount/plugin/core.py b/conservancy_beancount/plugin/core.py index e296b15..57db6a5 100644 --- a/conservancy_beancount/plugin/core.py +++ b/conservancy_beancount/plugin/core.py @@ -71,10 +71,6 @@ class Hook(Generic[Entry], metaclass=abc.ABCMeta): def run(self, entry: Entry) -> errormod.Iter: ... -class TransactionHook(Hook[Transaction]): - DIRECTIVE = Transaction - - ### HELPER CLASSES class LessComparable(metaclass=abc.ABCMeta): @@ -178,18 +174,28 @@ class MetadataEnum: ### HOOK SUBCLASSES -class _PostingHook(TransactionHook, metaclass=abc.ABCMeta): +class TransactionHook(Hook[Transaction]): + DIRECTIVE = Transaction TXN_DATE_RANGE: _GenericRange = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE) - def __init_subclass__(cls) -> None: - cls.HOOK_GROUPS = cls.HOOK_GROUPS.union(['posting']) - def _run_on_txn(self, txn: Transaction) -> bool: + """Check whether we should run on a given transaction + + This method implements our usual checks for whether or not a hook + should run on a given transaction. It's here for subclasses to use in + their own implementations. See _PostingHook below for an example. + """ return ( - txn.date in self.TXN_DATE_RANGE + txn.flag != '!' + and txn.date in self.TXN_DATE_RANGE and not data.is_opening_balance_txn(txn) ) + +class _PostingHook(TransactionHook, metaclass=abc.ABCMeta): + def __init_subclass__(cls) -> None: + cls.HOOK_GROUPS = cls.HOOK_GROUPS.union(['posting']) + def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool: return True diff --git a/conservancy_beancount/plugin/meta_repo_links.py b/conservancy_beancount/plugin/meta_repo_links.py index 658bd9d..3c68cfc 100644 --- a/conservancy_beancount/plugin/meta_repo_links.py +++ b/conservancy_beancount/plugin/meta_repo_links.py @@ -63,7 +63,8 @@ class MetaRepoLinks(core.TransactionHook): yield errormod.BrokenLinkError(txn, key, link) def run(self, txn: Transaction) -> errormod.Iter: - yield from self._check_links(txn.meta, txn) - for post in txn.postings: - if post.meta is not None: - yield from self._check_links(post.meta, txn, post) + if self._run_on_txn(txn): + yield from self._check_links(txn.meta, txn) + for post in txn.postings: + if post.meta is not None: + yield from self._check_links(post.meta, txn, post) diff --git a/conservancy_beancount/plugin/meta_rt_links.py b/conservancy_beancount/plugin/meta_rt_links.py index 2d14e86..57cc114 100644 --- a/conservancy_beancount/plugin/meta_rt_links.py +++ b/conservancy_beancount/plugin/meta_rt_links.py @@ -59,7 +59,8 @@ class MetaRTLinks(core.TransactionHook): yield errormod.BrokenRTLinkError(txn, key, link, parsed) def run(self, txn: Transaction) -> errormod.Iter: - yield from self._check_links(txn.meta, txn) - for post in txn.postings: - if post.meta is not None: - yield from self._check_links(post.meta, txn, post) + if self._run_on_txn(txn): + yield from self._check_links(txn.meta, txn) + for post in txn.postings: + if post.meta is not None: + yield from self._check_links(post.meta, txn, post) diff --git a/tests/test_meta_approval.py b/tests/test_meta_approval.py index 5d17785..76179ba 100644 --- a/tests/test_meta_approval.py +++ b/tests/test_meta_approval.py @@ -180,3 +180,10 @@ def test_approval_required_for_partial_transfer(hook): ]) actual = {error.message for error in hook.run(txn)} assert actual == {"Assets:Checking missing {}".format(TEST_KEY)} + +def test_not_required_on_flagged(hook): + txn = testutil.Transaction(flag='!', postings=[ + ('Assets:Checking', -25), + ('Liabilities:Payable:Accounts', 25), + ]) + assert not list(hook.run(txn)) diff --git a/tests/test_meta_invoice.py b/tests/test_meta_invoice.py index 1d23aff..17c6994 100644 --- a/tests/test_meta_invoice.py +++ b/tests/test_meta_invoice.py @@ -144,3 +144,14 @@ def test_missing_invoice(hook, acct1, acct2): def test_not_required_on_opening(hook): txn = testutil.OpeningBalance() assert not list(hook.run(txn)) + +@pytest.mark.parametrize('acct1,acct2', testutil.combine_values( + REQUIRED_ACCOUNTS, + NON_REQUIRED_ACCOUNTS, +)) +def test_not_required_on_flagged(acct1, acct2, hook): + txn = testutil.Transaction(flag='!', postings=[ + (acct1, 25), + (acct2, -25), + ]) + assert not list(hook.run(txn)) diff --git a/tests/test_meta_payable_documentation.py b/tests/test_meta_payable_documentation.py index 7cabd8e..d0b85d6 100644 --- a/tests/test_meta_payable_documentation.py +++ b/tests/test_meta_payable_documentation.py @@ -171,3 +171,6 @@ def test_not_required_on_opening(hook): (next(testutil.OPENING_EQUITY_ACCOUNTS), 40), ]) assert not list(hook.run(txn)) + +def test_not_required_on_flagged(hook): + check(hook, None, txn_meta={'flag': '!'}) diff --git a/tests/test_meta_receipt.py b/tests/test_meta_receipt.py index af7a370..daf2a79 100644 --- a/tests/test_meta_receipt.py +++ b/tests/test_meta_receipt.py @@ -349,3 +349,10 @@ def test_fallback_on_zero_amount_postings(hook, test_acct, other_acct, value): )) def test_not_required_on_opening(hook, test_acct, equity_acct): check(hook, test_acct, equity_acct, None) + +@pytest.mark.parametrize('test_acct,other_acct', testutil.combine_values( + ACCOUNTS, + NOT_REQUIRED_ACCOUNTS, +)) +def test_not_required_on_flagged(hook, test_acct, other_acct): + check(hook, test_acct, other_acct, None, txn_meta={'flag': '!'}) diff --git a/tests/test_meta_receivable_documentation.py b/tests/test_meta_receivable_documentation.py index ad41abb..705e4d5 100644 --- a/tests/test_meta_receivable_documentation.py +++ b/tests/test_meta_receivable_documentation.py @@ -213,3 +213,7 @@ def test_not_required_on_opening(hook): (next(testutil.OPENING_EQUITY_ACCOUNTS), -300), ]) assert not list(hook.run(txn)) + +def test_not_required_on_flagged(hook): + post_meta = seed_meta() + check(hook, None, txn_meta={'flag': '!'}, post_meta=post_meta) diff --git a/tests/test_meta_repo_links.py b/tests/test_meta_repo_links.py index 4c44e46..5c0b005 100644 --- a/tests/test_meta_repo_links.py +++ b/tests/test_meta_repo_links.py @@ -100,6 +100,16 @@ def test_bad_post_links(hook): actual = {error.message for error in hook.run(txn)} assert expected == actual +def test_flagged_txn_not_checked(hook): + keys = iter(METADATA_KEYS) + txn_meta = build_meta(keys, BAD_LINKS) + txn_meta['flag'] = '!' + txn = testutil.Transaction(**txn_meta, postings=[ + ('Income:Donations', -5, build_meta(keys, BAD_LINKS)), + ('Assets:Checking', 5, build_meta(keys, BAD_LINKS)), + ]) + assert not list(hook.run(txn)) + @pytest.mark.parametrize('value', testutil.NON_STRING_METADATA_VALUES) def test_bad_metadata_type(hook, value): txn = testutil.Transaction(**{'check': value}, postings=[ diff --git a/tests/test_meta_rt_links.py b/tests/test_meta_rt_links.py index 6f95c2e..fa6f2bf 100644 --- a/tests/test_meta_rt_links.py +++ b/tests/test_meta_rt_links.py @@ -146,6 +146,16 @@ def test_docs_outside_rt_not_checked(hook, ext_doc): actual = {error.message for error in hook.run(txn)} assert expected == actual +def test_flagged_txn_not_checked(hook): + txn_meta = build_meta(None, MALFORMED_LINKS) + txn_meta['flag'] = '!' + keys = iter(METADATA_KEYS) + txn = testutil.Transaction(**txn_meta, postings=[ + ('Income:Donations', -5, build_meta(keys, MALFORMED_LINKS)), + ('Assets:Checking', 5, build_meta(keys, NOT_FOUND_LINKS)), + ]) + assert not list(hook.run(txn)) + def test_mixed_results(hook): txn = testutil.Transaction( approval='{} {}'.format(*GOOD_LINKS),