Changeset - 547ae657808f
[Not reviewed]
0 6 0
Brett Smith - 4 years ago 2020-03-08 15:32:03
brettcsmith@brettcsmith.org
plugin.core: _meta_set properly handles when post.meta is None.

post is a NamedTuple, so attribute assignment is not allowed.
Instead we have to construct a whole new Posting.
6 files changed with 23 insertions and 20 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/plugin/__init__.py
Show inline comments
...
 
@@ -77,17 +77,17 @@ class HookRegistry:
 
HOOK_REGISTRY = HookRegistry()
 
HOOK_REGISTRY.import_hooks('.meta_expense_allocation', 'MetaExpenseAllocation')
 
HOOK_REGISTRY.import_hooks('.meta_tax_implication', 'MetaTaxImplication')
 

	
 
def run(entries, options_map, config='', hook_registry=HOOK_REGISTRY):
 
    errors = []
 
    hooks = hook_registry.group_by_directive(config)
 
    for entry in entries:
 
        entry_type = type(entry).__name__
 
        for hook in hooks[entry_type]:
 
            errors.extend(hook.run(entry))
 
        if entry_type == 'Transaction':
 
            for post in entry.postings:
 
            for index, post in enumerate(entry.postings):
 
                for hook in hooks['Posting']:
 
                    errors.extend(hook.run(entry, post))
 
                    errors.extend(hook.run(entry, post, index))
 
    return entries, errors
 

	
conservancy_beancount/plugin/core.py
Show inline comments
...
 
@@ -123,28 +123,29 @@ class PostingChecker:
 

	
 
    HOOK_GROUPS = frozenset(['Posting', 'metadata'])
 
    ACCOUNTS = ('',)
 
    TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE)
 
    VALUES_ENUM = {}
 

	
 
    def _meta_get(self, txn, post, key, default=None):
 
        try:
 
            return post.meta[key]
 
        except (KeyError, TypeError):
 
            return txn.meta.get(key, default)
 

	
 
    def _meta_set(self, post, key, value):
 
    def _meta_set(self, txn, post, post_index, key, value):
 
        if post.meta is None:
 
            post.meta = {}
 
        post.meta[key] = value
 
            txn.postings[post_index] = Posting(*post[:5], {key: value})
 
        else:
 
            post.meta[key] = value
 

	
 
    # If the posting does not specify METADATA_KEY, the hook calls
 
    # _default_value to get a default. This method should either return
 
    # a value string from METADATA_ENUM, or else raise InvalidMetadataError.
 
    # This base implementation does the latter.
 
    def _default_value(self, txn, post):
 
        raise errormod.InvalidMetadataError(txn, post, self.METADATA_KEY)
 

	
 
    # The hook calls _should_check on every posting and only checks postings
 
    # when the method returns true. This base method checks the transaction
 
    # date is in TXN_DATE_RANGE, and the posting account name matches ACCOUNTS.
 
    def _should_check(self, txn, post):
tests/test_meta_expenseAllocation.py
Show inline comments
...
 
@@ -33,97 +33,97 @@ INVALID_VALUES = {
 
    'adimn',
 
    'fundrasing',
 
    '',
 
}
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_postings(src_value, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Cash', -25),
 
        ('Expenses:General', 25, {'expenseAllocation': src_value}),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert txn.postings[-1].meta.get('expenseAllocation') == set_value
 

	
 
@pytest.mark.parametrize('src_value', INVALID_VALUES)
 
def test_invalid_values_on_postings(src_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Cash', -25),
 
        ('Expenses:General', 25, {'expenseAllocation': src_value}),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert errors
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_transactions(src_value, set_value):
 
    txn = testutil.Transaction(expenseAllocation=src_value, postings=[
 
        ('Assets:Cash', -25),
 
        ('Expenses:General', 25),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert txn.postings[-1].meta.get('expenseAllocation') == set_value
 

	
 
@pytest.mark.parametrize('src_value', INVALID_VALUES)
 
def test_invalid_values_on_transactions(src_value):
 
    txn = testutil.Transaction(expenseAllocation=src_value, postings=[
 
        ('Assets:Cash', -25),
 
        ('Expenses:General', 25),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert errors
 

	
 
@pytest.mark.parametrize('account', [
 
    'Accrued:AccountsReceivable',
 
    'Assets:Cash',
 
    'Income:Donations',
 
    'Liabilities:CreditCard',
 
    'UnearnedIncome:Donations',
 
])
 
def test_non_expense_accounts_skipped(account):
 
    txn = testutil.Transaction(postings=[
 
        (account, -25),
 
        ('Expenses:General', 25, {'expenseAllocation': 'program'}),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[0])
 
    errors = checker.run(txn, txn.postings[0], 0)
 
    assert not errors
 

	
 
@pytest.mark.parametrize('account,set_value', [
 
    ('Expenses:Services:Accounting', 'administration'),
 
    ('Expenses:Services:Administration', 'administration'),
 
    ('Expenses:Services:Advocacy', 'program'),
 
    ('Expenses:Services:Development', 'program'),
 
    ('Expenses:Services:Fundraising', 'fundraising'),
 
])
 
def test_default_values(account, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Liabilites:CreditCard', -25),
 
        (account, 25),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert txn.postings[-1].meta['expenseAllocation'] == set_value
 

	
 
@pytest.mark.parametrize('date,set_value', [
 
    (testutil.EXTREME_FUTURE_DATE, False),
 
    (testutil.FUTURE_DATE, True),
 
    (testutil.FY_START_DATE, True),
 
    (testutil.FY_MID_DATE, True),
 
    (testutil.PAST_DATE, False),
 
])
 
def test_default_value_set_in_date_range(date, set_value):
 
    txn = testutil.Transaction(date=date, postings=[
 
        ('Liabilites:CreditCard', -25),
 
        ('Expenses:General', 25),
 
    ])
 
    checker = meta_expense_allocation.MetaExpenseAllocation()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    got_value = (txn.postings[-1].meta or {}).get('expenseAllocation')
 
    assert bool(got_value) == bool(set_value)
tests/test_meta_taxImplication.py
Show inline comments
...
 
@@ -45,86 +45,86 @@ INVALID_VALUES = {
 
    'W3',
 
    'Payrol',
 
    '',
 
}
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_postings(src_value, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Accrued:AccountsPayable', 25),
 
        ('Assets:Cash', -25, {'taxImplication': src_value}),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert txn.postings[-1].meta.get('taxImplication') == set_value
 

	
 
@pytest.mark.parametrize('src_value', INVALID_VALUES)
 
def test_invalid_values_on_postings(src_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Accrued:AccountsPayable', 25),
 
        ('Assets:Cash', -25, {'taxImplication': src_value}),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert errors
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_transactions(src_value, set_value):
 
    txn = testutil.Transaction(taxImplication=src_value, postings=[
 
        ('Accrued:AccountsPayable', 25),
 
        ('Assets:Cash', -25),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert txn.postings[-1].meta.get('taxImplication') == set_value
 

	
 
@pytest.mark.parametrize('src_value', INVALID_VALUES)
 
def test_invalid_values_on_transactions(src_value):
 
    txn = testutil.Transaction(taxImplication=src_value, postings=[
 
        ('Accrued:AccountsPayable', 25),
 
        ('Assets:Cash', -25),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert errors
 

	
 
@pytest.mark.parametrize('account', [
 
    'Accrued:AccountsPayable',
 
    'Expenses:General',
 
    'Liabilities:CreditCard',
 
])
 
def test_non_asset_accounts_skipped(account):
 
    txn = testutil.Transaction(postings=[
 
        (account, 25),
 
        ('Assets:Cash', -25, {'taxImplication': 'USA-Corporation'}),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[0])
 
    errors = checker.run(txn, txn.postings[0], 0)
 
    assert not errors
 

	
 
def test_asset_credits_skipped():
 
    txn = testutil.Transaction(postings=[
 
        ('Income:Donations', -25),
 
        ('Assets:Cash', 25),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert not errors
 
    assert not txn.postings[-1].meta
 

	
 
@pytest.mark.parametrize('date,need_value', [
 
    (testutil.EXTREME_FUTURE_DATE, False),
 
    (testutil.FUTURE_DATE, True),
 
    (testutil.FY_START_DATE, True),
 
    (testutil.FY_MID_DATE, True),
 
    (testutil.PAST_DATE, False),
 
])
 
def test_default_value_set_in_date_range(date, need_value):
 
    txn = testutil.Transaction(date=date, postings=[
 
        ('Liabilites:CreditCard', 25),
 
        ('Assets:Cash', -25),
 
    ])
 
    checker = meta_tax_implication.MetaTaxImplication()
 
    errors = checker.run(txn, txn.postings[-1])
 
    errors = checker.run(txn, txn.postings[-1], -1)
 
    assert bool(errors) == bool(need_value)
tests/test_plugin_run.py
Show inline comments
...
 
@@ -26,25 +26,25 @@ HOOK_REGISTRY = plugin.HookRegistry()
 
@HOOK_REGISTRY.add_hook
 
class TransactionCounter:
 
    HOOK_GROUPS = frozenset(['Transaction', 'counter'])
 

	
 
    def run(self, txn):
 
        return ['txn:{}'.format(id(txn))]
 

	
 

	
 
@HOOK_REGISTRY.add_hook
 
class PostingCounter(TransactionCounter):
 
    HOOK_GROUPS = frozenset(['Posting', 'counter'])
 

	
 
    def run(self, txn, post):
 
    def run(self, txn, post, post_index):
 
        return ['post:{}'.format(id(post))]
 

	
 

	
 
def map_errors(errors):
 
    retval = {}
 
    for errkey in errors:
 
        key, _, errid = errkey.partition(':')
 
        retval.setdefault(key, set()).add(errid)
 
    return retval
 

	
 
def test_with_multiple_hooks():
 
    in_entries = [
tests/testutil.py
Show inline comments
...
 
@@ -24,24 +24,26 @@ from decimal import Decimal
 
EXTREME_FUTURE_DATE = datetime.date(datetime.MAXYEAR, 12, 30)
 
FUTURE_DATE = datetime.date.today() + datetime.timedelta(days=365 * 99)
 
FY_START_DATE = datetime.date(2020, 3, 1)
 
FY_MID_DATE = datetime.date(2020, 9, 1)
 
PAST_DATE = datetime.date(2000, 1, 1)
 

	
 
def parse_date(s, fmt='%Y-%m-%d'):
 
    return datetime.datetime.strptime(s, fmt).date()
 

	
 
def Posting(account, number,
 
            currency='USD', cost=None, price=None, flag=None,
 
            **meta):
 
    if not meta:
 
        meta = None
 
    return bc_data.Posting(
 
        account,
 
        bc_amount.Amount(Decimal(number), currency),
 
        cost,
 
        price,
 
        flag,
 
        meta,
 
    )
 

	
 
class Transaction:
 
    def __init__(self,
 
                 date=FY_MID_DATE, flag='*', payee=None,
0 comments (0 inline, 0 general)