From 46cfc558ecbb3bc8fe0c01f991613777eb258fb7 2020-03-28 17:36:56 From: Brett Smith Date: 2020-03-28 17:36:56 Subject: [PATCH] plugin: Link checkers use Metadata class. --- diff --git a/conservancy_beancount/errors.py b/conservancy_beancount/errors.py index c42ac318370249d628530ba7f53a5746d60700b3..3d9bdd207e5137e3060a8fc1677ececad1cdaa13 100644 --- a/conservancy_beancount/errors.py +++ b/conservancy_beancount/errors.py @@ -67,13 +67,17 @@ class ConfigurationError(Error): class InvalidMetadataError(Error): - def __init__(self, txn, key, value=None, post=None, source=None): + def __init__(self, txn, key, value=None, post=None, need_type=str, source=None): if post is None: srcname = 'transaction' else: srcname = post.account if value is None: msg = "{} missing {}".format(srcname, key) - else: + elif isinstance(value, need_type): msg = "{} has invalid {}: {}".format(srcname, key, value) + else: + msg = "{} has wrong type of {}: expected {} but is a {}".format( + srcname, key, need_type.__name__, type(value).__name__, + ) super().__init__(msg, txn, source) diff --git a/conservancy_beancount/plugin/meta_repo_links.py b/conservancy_beancount/plugin/meta_repo_links.py index 4c1bdf0e1c8b580b93ed06a43669b03b41cd437a..f9f281b084a1032eca811892eed84a5acdf90cb5 100644 --- a/conservancy_beancount/plugin/meta_repo_links.py +++ b/conservancy_beancount/plugin/meta_repo_links.py @@ -23,11 +23,13 @@ from .. import errors as errormod from ..beancount_types import ( MetaKey, MetaValue, + Posting, Transaction, ) from typing import ( - Mapping, + MutableMapping, + Optional, ) class MetaRepoLinks(core.TransactionHook): @@ -41,19 +43,26 @@ class MetaRepoLinks(core.TransactionHook): self.repo_path = repo_path def _check_links(self, + meta: MutableMapping[MetaKey, MetaValue], txn: Transaction, - meta: Mapping[MetaKey, MetaValue], + post: Optional[Posting]=None, ) -> errormod.Iter: - for key in data.LINK_METADATA.intersection(meta): - for link in str(meta[key]).split(): - 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) + metadata = data.Metadata(meta) + for key in data.LINK_METADATA: + try: + 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, txn.meta) + yield from self._check_links(txn.meta, txn) for post in txn.postings: if post.meta is not None: - yield from self._check_links(txn, post.meta) + 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 591e7c0342196d21c0d1c1fd68a8ac54f3b4e5a2..b91f4ec45a4a70d18fa149c08fc4a9044dd9cc57 100644 --- a/conservancy_beancount/plugin/meta_rt_links.py +++ b/conservancy_beancount/plugin/meta_rt_links.py @@ -21,11 +21,13 @@ from .. import errors as errormod from ..beancount_types import ( MetaKey, MetaValue, + Posting, Transaction, ) from typing import ( - Mapping, + MutableMapping, + Optional, ) class MetaRTLinks(core.TransactionHook): @@ -39,19 +41,26 @@ class MetaRTLinks(core.TransactionHook): self.rt = rt_wrapper def _check_links(self, + meta: MutableMapping[MetaKey, MetaValue], txn: Transaction, - meta: Mapping[MetaKey, MetaValue], + post: Optional[Posting]=None, ) -> errormod.Iter: - for key in self.LINK_METADATA.intersection(meta): - for link in str(meta[key]).split(): - 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) + metadata = data.Metadata(meta) + for key in self.LINK_METADATA: + try: + 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, txn.meta) + yield from self._check_links(txn.meta, txn) for post in txn.postings: if post.meta is not None: - yield from self._check_links(txn, post.meta) + yield from self._check_links(post.meta, txn, post) diff --git a/tests/test_meta_repo_links.py b/tests/test_meta_repo_links.py index b174c54c0f4b03cd96e7fc946935bbbb65352bf5..4c44e46c5292bc0a63fd0fb1c866d913553c0814 100644 --- a/tests/test_meta_repo_links.py +++ b/tests/test_meta_repo_links.py @@ -100,6 +100,18 @@ def test_bad_post_links(hook): actual = {error.message for error in hook.run(txn)} assert expected == actual +@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 + @pytest.mark.parametrize('ext_doc', [ 'rt:123', 'rt:456/789', diff --git a/tests/test_meta_rt_links.py b/tests/test_meta_rt_links.py index 21f4b5692257dbc98489637e9ac184f9ccc0823a..6f95c2e3c5ac8cf187a658a9de42f0d188b4ea12 100644 --- a/tests/test_meta_rt_links.py +++ b/tests/test_meta_rt_links.py @@ -119,6 +119,18 @@ def test_bad_post_links(hook, link_source, format_error): actual = {error.message for error in hook.run(txn)} assert expected == actual +@pytest.mark.parametrize('value', testutil.NON_STRING_METADATA_VALUES) +def test_bad_metadata_type(hook, value): + txn = testutil.Transaction(**{'rt-id': value}, postings=[ + ('Income:Donations', -5), + ('Assets:Cash', 5), + ]) + expected = {'transaction has wrong type of rt-id: expected str but is a {}'.format( + type(value).__name__, + )} + actual = {error.message for error in hook.run(txn)} + assert expected == actual + @pytest.mark.parametrize('ext_doc', [ 'statement.txt', 'https://example.org/',