From 536b50b478d889b89982057bf026f32446ea4949 2020-05-11 13:52:05 From: Brett Smith Date: 2020-05-11 13:52:05 Subject: [PATCH] plugin: Don't validate transactions flagged with !. RT#10591. --- diff --git a/conservancy_beancount/plugin/core.py b/conservancy_beancount/plugin/core.py index e296b158aadf18fb40dc6a9b72604c47566c8ae8..57db6a5a572ae1600716568fe93a1477938a2425 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 658bd9dbe4f6270286ec85d44dcea8053b884fd3..3c68cfc5991abd467e1dea5f8ee2110a6c8e3ef5 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 2d14e86b8bb172c4532f6810c609f6e17c7af029..57cc114acd70310e07004c66f74d1d0c9d5af0d8 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 5d1778555ae79b893fe6831aba08a7fb34bd66c6..76179ba52551f7f52f09e662f3ca39481dfc2087 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 1d23aff2ead09c748289c81b206ad86f09ad9f11..17c69946066f0b180f0ff85bd1eef1ab475ad25e 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 7cabd8ecc5f2f84e528cfaa4f80c77b64e72d4d9..d0b85d65fc5e3b6c6ebaaf0a1ed3010ef899fad8 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 af7a37025cec0929239a0d76eff0dbfa99fbfa1c..daf2a79f94f658417be9afcf8eb0eec6aa3d5314 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 ad41abb6a1b35a7b7fd9e17d9210fb9bde8e4ad3..705e4d54f46c1fde890a9ec86cf4c3514ff5ca37 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 4c44e46c5292bc0a63fd0fb1c866d913553c0814..5c0b0050c5ef4b68d7362ce1005c1aeb2d12f38e 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 6f95c2e3c5ac8cf187a658a9de42f0d188b4ea12..fa6f2bfa63169d3f04fee2c2778a96303f454cdf 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),