Changeset - 536b50b478d8
[Not reviewed]
0 10 0
Brett Smith - 4 years ago 2020-05-11 13:52:05
brettcsmith@brettcsmith.org
plugin: Don't validate transactions flagged with !. RT#10591.
10 files changed with 77 insertions and 17 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/plugin/core.py
Show inline comments
...
 
@@ -62,28 +62,24 @@ class Hook(Generic[Entry], metaclass=abc.ABCMeta):
 
    DIRECTIVE: Type[Directive]
 
    HOOK_GROUPS: FrozenSet[HookName] = frozenset()
 

	
 
    def __init__(self, config: configmod.Config) -> None:
 
        pass
 
        # Subclasses that need configuration should override __init__ to check
 
        # and store it.
 

	
 
    @abc.abstractmethod
 
    def run(self, entry: Entry) -> errormod.Iter: ...
 

	
 

	
 
class TransactionHook(Hook[Transaction]):
 
    DIRECTIVE = Transaction
 

	
 

	
 
### HELPER CLASSES
 

	
 
class LessComparable(metaclass=abc.ABCMeta):
 
    @abc.abstractmethod
 
    def __le__(self, other: Any) -> bool: ...
 

	
 
    @abc.abstractmethod
 
    def __lt__(self, other: Any) -> bool: ...
 

	
 

	
 
CT = TypeVar('CT', bound=LessComparable)
 
class _GenericRange(Generic[CT]):
...
 
@@ -169,36 +165,46 @@ class MetadataEnum:
 
        """
 
        try:
 
            return self[key]
 
        except KeyError:
 
            if default_key is None:
 
                return None
 
            else:
 
                return self[default_key]
 

	
 

	
 
### 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
 

	
 
    def run(self, txn: Transaction) -> errormod.Iter:
 
        if self._run_on_txn(txn):
 
            for post in data.Posting.from_txn(txn):
 
                if self._run_on_post(txn, post):
 
                    yield from self.post_run(txn, post)
 

	
 
    @abc.abstractmethod
 
    def post_run(self, txn: Transaction, post: data.Posting) -> errormod.Iter: ...
 

	
conservancy_beancount/plugin/meta_repo_links.py
Show inline comments
...
 
@@ -54,16 +54,17 @@ class MetaRepoLinks(core.TransactionHook):
 
                links = metadata.get_links(key)
 
            except TypeError:
 
                yield errormod.InvalidMetadataError(txn, key, meta[key], post)
 
            else:
 
                for link in links:
 
                    match = self.PATH_PUNCT_RE.search(link)
 
                    if match and match.group(0) == ':':
 
                        pass
 
                    elif not (self.repo_path / link).exists():
 
                        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)
conservancy_beancount/plugin/meta_rt_links.py
Show inline comments
...
 
@@ -50,16 +50,17 @@ class MetaRTLinks(core.TransactionHook):
 
                links = metadata.get_links(key)
 
            except TypeError:
 
                yield errormod.InvalidMetadataError(txn, key, meta[key], post)
 
            else:
 
                for link in links:
 
                    if not link.startswith('rt:'):
 
                        continue
 
                    parsed = self.rt.parse(link)
 
                    if parsed is None or not self.rt.exists(*parsed):
 
                        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)
tests/test_meta_approval.py
Show inline comments
...
 
@@ -171,12 +171,19 @@ def test_approval_not_required_for_asset_transfers(hook, tax_implication, other_
 
    assert not list(hook.run(txn))
 

	
 
def test_approval_required_for_partial_transfer(hook):
 
    # I'm not sure this ever comes up in reality, but just being thorough
 
    # out of an abundance of precaution.
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Checking', -250, {'tax-implication': 'Bank-Transfer'}),
 
        ('Assets:Savings', 225),
 
        ('Expenses:BankingFees', 25),
 
    ])
 
    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))
tests/test_meta_invoice.py
Show inline comments
...
 
@@ -135,12 +135,23 @@ def test_bad_type_values_on_transaction(hook, acct1, acct2, value):
 
))
 
def test_missing_invoice(hook, acct1, acct2):
 
    txn = testutil.Transaction(postings=[
 
        (acct2, -25),
 
        (acct1, 25),
 
    ])
 
    actual = {error.message for error in hook.run(txn)}
 
    assert actual == {"{} missing {}".format(acct1, TEST_KEY)}
 

	
 
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))
tests/test_meta_payable_documentation.py
Show inline comments
...
 
@@ -162,12 +162,15 @@ def test_paid_accts_not_checked(hook):
 
])
 
def test_does_not_apply_to_other_accounts(hook, account):
 
    meta = seed_meta()
 
    check(hook, None, account, post_meta=meta)
 

	
 
def test_not_required_on_opening(hook):
 
    txn = testutil.Transaction(postings=[
 
        ('Liabilities:Payable:Accounts', -15),
 
        ('Liabilities:Payable:Vacation', -25),
 
        (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': '!'})
tests/test_meta_receipt.py
Show inline comments
...
 
@@ -340,12 +340,19 @@ def test_fallback_on_zero_amount_postings(hook, test_acct, other_acct, value):
 
        ('Income:Donations', '-.1'),
 
        ('Expenses:BankingFees', '.1'),
 
        (test_acct.name, 0, {test_acct.fallback_meta: value}),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('test_acct,equity_acct', testutil.combine_values(
 
    ACCOUNTS,
 
    testutil.OPENING_EQUITY_ACCOUNTS,
 
))
 
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': '!'})
tests/test_meta_receivable_documentation.py
Show inline comments
...
 
@@ -204,12 +204,16 @@ def test_does_not_apply_to_other_accounts(hook, account):
 
def test_configuration_error_without_rt():
 
    config = testutil.TestConfig()
 
    with pytest.raises(errormod.ConfigurationError):
 
        meta_receivable_documentation.MetaReceivableDocumentation(config)
 

	
 
def test_not_required_on_opening(hook):
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Receivable:Accounts', 100),
 
        ('Assets:Receivable:Loans', 200),
 
        (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)
tests/test_meta_repo_links.py
Show inline comments
...
 
@@ -91,24 +91,34 @@ def test_bad_txn_links(hook):
 
    assert expected == actual
 

	
 
def test_bad_post_links(hook):
 
    meta = build_meta(None, BAD_LINKS)
 
    txn = testutil.Transaction(postings=[
 
        ('Income:Donations', -5, meta.copy()),
 
        ('Assets:Cash', 5),
 
    ])
 
    expected = {NOT_FOUND_MSG(key, value) for key, value in meta.items()}
 
    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=[
 
        ('Income:Donations', -5),
 
        ('Assets:Cash', 5),
 
    ])
 
    expected = {'transaction has wrong type of check: expected str but is a {}'.format(
 
        type(value).__name__,
 
    )}
 
    actual = {error.message for error in hook.run(txn)}
 
    assert expected == actual
 

	
tests/test_meta_rt_links.py
Show inline comments
...
 
@@ -137,24 +137,34 @@ def test_bad_metadata_type(hook, value):
 
])
 
def test_docs_outside_rt_not_checked(hook, ext_doc):
 
    txn = testutil.Transaction(
 
        receipt='{} {} {}'.format(GOOD_LINKS[0], ext_doc, MALFORMED_LINKS[1]),
 
        postings=[
 
            ('Income:Donations', -5),
 
            ('Assets:Cash', 5),
 
        ])
 
    expected = {MALFORMED_MSG('receipt', MALFORMED_LINKS[1])}
 
    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),
 
        contract='{} {}'.format(MALFORMED_LINKS[0], GOOD_LINKS[1]),
 
        postings=[
 
            ('Income:Donations', -5, {'invoice': '{} {}'.format(*NOT_FOUND_LINKS)}),
 
            ('Assets:Cash', 5, {'statement': '{} {}'.format(GOOD_LINKS[0], MALFORMED_LINKS[1])}),
 
        ])
 
    expected = {
 
        MALFORMED_MSG('contract', MALFORMED_LINKS[0]),
 
        NOT_FOUND_MSG('invoice', NOT_FOUND_LINKS[0]),
 
        NOT_FOUND_MSG('invoice', NOT_FOUND_LINKS[1]),
0 comments (0 inline, 0 general)