data: Add PostingMeta.detached() method.

This commit is contained in:
Brett Smith 2020-08-29 10:26:21 -04:00
parent dfdb9b65d5
commit 52adf1f0a5
2 changed files with 36 additions and 9 deletions

View file

@ -512,10 +512,9 @@ class PostingMeta(Metadata):
self.txn = txn self.txn = txn
self.index = index self.index = index
self.post = post self.post = post
if post.meta is None: self.meta: collections.ChainMap = collections.ChainMap(txn.meta)
self.meta = self.txn.meta if post.meta is not None:
else: self.meta = self.meta.new_child(post.meta)
self.meta = collections.ChainMap(post.meta, txn.meta)
def __getitem__(self, key: MetaKey) -> MetaValue: def __getitem__(self, key: MetaKey) -> MetaValue:
try: try:
@ -527,17 +526,16 @@ class PostingMeta(Metadata):
raise raise
def __setitem__(self, key: MetaKey, value: MetaValue) -> None: def __setitem__(self, key: MetaKey, value: MetaValue) -> None:
if self.post.meta is None: if len(self.meta.maps) == 1:
self.post = self.post._replace(meta={key: value}) self.post = self.post._replace(meta={key: value})
assert self.post.meta is not None
self.txn.postings[self.index] = self.post self.txn.postings[self.index] = self.post
# mypy complains that self.post.meta could be None, but we know self.meta = self.meta.new_child(self.post.meta)
# from two lines up that it's not.
self.meta = collections.ChainMap(self.post.meta, self.txn.meta) # type:ignore[arg-type]
else: else:
super().__setitem__(key, value) super().__setitem__(key, value)
def __delitem__(self, key: MetaKey) -> None: def __delitem__(self, key: MetaKey) -> None:
if self.post.meta is None: if len(self.meta.maps) == 1:
raise KeyError(key) raise KeyError(key)
else: else:
super().__delitem__(key) super().__delitem__(key)
@ -550,6 +548,17 @@ class PostingMeta(Metadata):
def date(self) -> datetime.date: def date(self) -> datetime.date:
return self.txn.date return self.txn.date
def detached(self) -> 'PostingMeta':
"""Create a copy of this PostingMeta detached from the original post
Changes you make to the detached copy will not propagate to the
underlying data structures. This is mostly useful for reporting code
that may want to "split" and manipulate the metadata multiple times.
"""
retval = type(self)(self.txn, self.index, self.post)
retval.meta = retval.meta.new_child()
return retval
class Posting(BasePosting): class Posting(BasePosting):
"""Enhanced Posting objects """Enhanced Posting objects

View file

@ -126,6 +126,24 @@ def test_date(date):
for index, post in enumerate(txn.postings): for index, post in enumerate(txn.postings):
assert data.PostingMeta(txn, index, post).date == date assert data.PostingMeta(txn, index, post).date == date
def test_mutable_copy():
txn = testutil.Transaction(
filename='f', lineno=130, txnkey='one', postings=[
('Assets:Cash', 18),
('Income:Donations', -18),
])
meta = data.PostingMeta(txn, 1).detached()
meta['layerkey'] = 'two'
assert dict(meta) == {
'filename': 'f',
'lineno': 130,
'txnkey': 'one',
'layerkey': 'two',
}
assert 'layerkey' not in txn.meta
assert all(post.meta is None for post in txn.postings)
assert meta.date == txn.date
# The .get() tests are arguably testing the stdlib, but they're short and # The .get() tests are arguably testing the stdlib, but they're short and
# they confirm that we're using the stdlib as we intend. # they confirm that we're using the stdlib as we intend.
def test_get_with_meta_value(simple_txn): def test_get_with_meta_value(simple_txn):