diff --git a/conservancy_beancount/plugin/__init__.py b/conservancy_beancount/plugin/__init__.py index e69de29..217212a 100644 --- a/conservancy_beancount/plugin/__init__.py +++ b/conservancy_beancount/plugin/__init__.py @@ -0,0 +1,48 @@ +"""Beancount plugin entry point for Conservancy""" +# Copyright © 2020 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import beancount.core.data as bc_data + +__plugins__ = ['run'] + +class HookRegistry: + DIRECTIVES = frozenset([ + *(cls.__name__ for cls in bc_data.ALL_DIRECTIVES), + 'Posting', + ]) + + @classmethod + def group_by_directive(cls, hooks_seq): + hooks_map = {key: [] for key in cls.DIRECTIVES} + for hook in hooks_seq: + for key in cls.DIRECTIVES & hook.HOOK_GROUPS: + hooks_map[key].append(hook) + return hooks_map + + +def run(entries, options_map, config): + errors = [] + hooks = HookRegistry.group_by_directive(config) + for entry in entries: + entry_type = type(entry).__name__ + for hook in hooks[entry_type]: + errors.extend(hook.check(entry)) + if entry_type == 'Transaction': + for post in entry.postings: + for hook in hooks['Posting']: + errors.extend(hook.check(entry, post)) + return entries, errors + diff --git a/tests/test_plugin_run.py b/tests/test_plugin_run.py new file mode 100644 index 0000000..731cb68 --- /dev/null +++ b/tests/test_plugin_run.py @@ -0,0 +1,77 @@ +"""Test main plugin run loop""" +# Copyright © 2020 Brett Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest + +from . import testutil + +from conservancy_beancount import plugin + +CONFIG_MAP = {} + +class TransactionCounter: + HOOK_GROUPS = frozenset(['Transaction']) + + def __init__(self): + self.counter = 0 + + def check(self, txn): + self.counter += 1 + return () + + +class PostingCounter(TransactionCounter): + HOOK_GROUPS = frozenset(['Posting']) + + def check(self, txn, post): + return super().check(txn) + + +def test_with_multiple_hooks(): + txn_counter = TransactionCounter() + post_counter = PostingCounter() + in_entries = [ + testutil.Transaction(postings=[ + ('Income:Donations', -25), + ('Assets:Cash', 25), + ]), + testutil.Transaction(postings=[ + ('Expenses:General', 10), + ('Liabilites:CreditCard', -10), + ]), + ] + out_entries, errors = plugin.run(in_entries, CONFIG_MAP, [txn_counter, post_counter]) + assert len(out_entries) == 2 + assert len(errors) == 0 + assert txn_counter.counter == 2 + assert post_counter.counter == 4 + +def test_with_posting_hooks_only(): + post_counter = PostingCounter() + in_entries = [ + testutil.Transaction(postings=[ + ('Income:Donations', -25), + ('Assets:Cash', 25), + ]), + testutil.Transaction(postings=[ + ('Expenses:General', 10), + ('Liabilites:CreditCard', -10), + ]), + ] + out_entries, errors = plugin.run(in_entries, CONFIG_MAP, [post_counter]) + assert len(out_entries) == 2 + assert len(errors) == 0 + assert post_counter.counter == 4