plugin: Link checkers use Metadata class.
This commit is contained in:
		
							parent
							
								
									9b63d898af
								
							
						
					
					
						commit
						46cfc558ec
					
				
					 5 changed files with 70 additions and 24 deletions
				
			
		| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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/',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue