Changeset - 9c335175835c
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-06-11 14:44:05
brettcsmith@brettcsmith.org
data: Add Metadata.first_link() method.
3 files changed with 45 insertions and 5 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/data.py
Show inline comments
...
 
@@ -235,24 +235,36 @@ class Metadata(MutableMapping[MetaKey, MetaValue]):
 
    def get_links(self, key: MetaKey) -> Sequence[str]:
 
        try:
 
            value = self.meta[key]
 
        except KeyError:
 
            return ()
 
        if isinstance(value, str):
 
            return value.split()
 
        else:
 
            raise TypeError("{} metadata is a {}, not str".format(
 
                key, type(value).__name__,
 
            ))
 

	
 
    @overload
 
    def first_link(self, key: MetaKey, default: None=None) -> Optional[str]: ...
 

	
 
    @overload
 
    def first_link(self, key: MetaKey, default: str) -> str: ...
 

	
 
    def first_link(self, key: MetaKey, default: Optional[str]=None) -> Optional[str]:
 
        try:
 
            return self.get_links(key)[0]
 
        except (IndexError, TypeError):
 
            return default
 

	
 

	
 
class PostingMeta(Metadata):
 
    """Combined access to posting metadata with its parent transaction metadata
 

	
 
    This lets you access posting metadata through a single dict-like object.
 
    If you try to look up metadata that doesn't exist on the posting, it will
 
    look for the value in the parent transaction metadata instead.
 

	
 
    You can set and delete metadata as well. Changes only affect the metadata
 
    of the posting, never the transaction. Changes are propagated to the
 
    underlying Beancount data structures.
 

	
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -211,29 +211,25 @@ class AccrualPostings(core.RelatedPostings):
 
        for post in self:
 
            if pred(post):
 
                try:
 
                    entity = post.meta['entity']
 
                except KeyError:
 
                    pass
 
                else:
 
                    if entity not in seen:
 
                        yield entity
 
                        seen.add(entity)
 

	
 
    def first_links(self, key: MetaKey, default: Optional[str]=None) -> Iterator[Optional[str]]:
 
        for post in self:
 
            try:
 
                yield post.meta.get_links(key)[0]
 
            except (IndexError, TypeError):
 
                yield default
 
        return (post.meta.first_link(key, default) for post in self)
 

	
 
    def make_consistent(self) -> Iterator[Tuple[MetaValue, 'AccrualPostings']]:
 
        account_ok = isinstance(self.account, str)
 
        entity_ok = isinstance(self.entity, str)
 
        # `'/' in self.invoice` is just our heuristic to ensure that the
 
        # invoice metadata is "unique enough," and not just a placeholder
 
        # value like "FIXME". It can be refined if needed.
 
        invoice_ok = isinstance(self.invoice, str) and '/' in self.invoice
 
        if account_ok and entity_ok and invoice_ok:
 
            yield (self.invoice, self)
 
            return
 
        groups = collections.defaultdict(list)
tests/test_data_metadata.py
Show inline comments
...
 
@@ -48,12 +48,44 @@ def test_get_links(value):
 
    meta = data.Metadata({'key': value})
 
    assert list(meta.get_links('key')) == value.split()
 

	
 
def test_get_links_missing():
 
    meta = data.Metadata({})
 
    assert not meta.get_links('key')
 

	
 
@pytest.mark.parametrize('value', testutil.NON_STRING_METADATA_VALUES)
 
def test_get_links_bad_type(value):
 
    meta = data.Metadata({'key': value})
 
    with pytest.raises(TypeError):
 
        meta.get_links('key')
 

	
 
def test_first_link_from_txn(simple_txn):
 
    meta = data.PostingMeta(simple_txn, 0)
 
    assert meta.first_link('note') == 'txn'
 

	
 
def test_first_link_from_post_override(simple_txn):
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('note') == 'donation'
 

	
 
def test_first_link_is_only_link(simple_txn):
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('extra') == 'Extra'
 

	
 
def test_first_link_nonexistent_metadata(simple_txn):
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('Nonexistent') is None
 

	
 
def test_first_link_nonexistent_default(simple_txn):
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('Nonexistent', 'missing') == 'missing'
 

	
 
@pytest.mark.parametrize('meta_value', testutil.NON_STRING_METADATA_VALUES)
 
def test_first_link_bad_type_metadata(simple_txn, meta_value):
 
    simple_txn.meta['badmeta'] = meta_value
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('badmeta') is None
 

	
 
@pytest.mark.parametrize('meta_value', testutil.NON_STRING_METADATA_VALUES)
 
def test_first_link_bad_type_default(simple_txn, meta_value):
 
    simple_txn.meta['badmeta'] = meta_value
 
    meta = data.PostingMeta(simple_txn, 1)
 
    assert meta.first_link('badmeta', '_') == '_'
0 comments (0 inline, 0 general)