Changeset - 0d370c445b9e
[Not reviewed]
0 8 0
Brett Smith - 4 years ago 2020-03-19 21:23:27
brettcsmith@brettcsmith.org
plugin: User configuration is passed to hooks on initialization.
8 files changed with 62 insertions and 9 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/errors.py
Show inline comments
...
 
@@ -26,19 +26,31 @@ class Error(Exception):
 

	
 
    def __repr__(self):
 
        return "{clsname}<{source[filename]}:{source[lineno]}: {message}>".format(
 
            clsname=type(self).__name__,
 
            message=self.message,
 
            source=self.source,
 
        )
 

	
 
    def _fill_source(self, source, filename='conservancy_beancount', lineno=0):
 
        source.setdefault('filename', filename)
 
        source.setdefault('lineno', lineno)
 

	
 

	
 
Iter = Iterable[Error]
 

	
 
class ConfigurationError(Error):
 
    def __init__(self, message, entry=None, source=None):
 
        if source is None:
 
            source = {}
 
        self._fill_source(source)
 
        super().__init__(message, entry, source)
 

	
 

	
 
class InvalidMetadataError(Error):
 
    def __init__(self, txn, post, key, value=None, source=None):
 
        if value is None:
 
            msg_fmt = "{post.account} missing {key}"
 
        else:
 
            msg_fmt = "{post.account} has invalid {key}: {value}"
 
        super().__init__(
 
            msg_fmt.format(post=post, key=key, value=value),
conservancy_beancount/plugin/__init__.py
Show inline comments
...
 
@@ -27,16 +27,17 @@ from typing import (
 
    Set,
 
    Tuple,
 
    Type,
 
)
 
from ..beancount_types import (
 
    ALL_DIRECTIVES,
 
    Directive,
 
)
 
from .. import config as configmod
 
from .core import (
 
    Hook,
 
    HookName,
 
)
 
from ..errors import (
 
    Error,
 
)
 

	
...
 
@@ -95,16 +96,22 @@ HOOK_REGISTRY.import_hooks('.meta_tax_implication', 'MetaTaxImplication')
 
def run(
 
        entries: List[Directive],
 
        options_map: Dict[str, Any],
 
        config: str='',
 
        hook_registry: HookRegistry=HOOK_REGISTRY,
 
) -> Tuple[List[Directive], List[Error]]:
 
    errors: List[Error] = []
 
    hooks: Dict[HookName, List[Hook]] = {}
 
    user_config = configmod.Config()
 
    for key, hook_type in hook_registry.group_by_directive(config):
 
        hooks.setdefault(key, []).append(hook_type())
 
        try:
 
            hook = hook_type(user_config)
 
        except Error as error:
 
            errors.append(error)
 
        else:
 
            hooks.setdefault(key, []).append(hook)
 
    for entry in entries:
 
        entry_type = type(entry).__name__
 
        for hook in hooks[entry_type]:
 
            errors.extend(hook.run(entry))
 
    return entries, errors
 

	
conservancy_beancount/plugin/core.py
Show inline comments
...
 
@@ -13,16 +13,17 @@
 
#
 
# You should have received a copy of the GNU Affero General Public License
 
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 

	
 
import abc
 
import datetime
 
import re
 

	
 
from .. import config as configmod
 
from .. import data
 
from .. import errors as errormod
 

	
 
from typing import (
 
    Any,
 
    Dict,
 
    FrozenSet,
 
    Generic,
...
 
@@ -55,16 +56,21 @@ DEFAULT_STOP_DATE: datetime.date = datetime.date(datetime.MAXYEAR, 1, 1)
 

	
 
HookName = str
 

	
 
Entry = TypeVar('Entry', bound=Directive)
 
class Hook(Generic[Entry], metaclass=abc.ABCMeta):
 
    DIRECTIVE: Type[Directive]
 
    HOOK_GROUPS: FrozenSet[HookName] = frozenset()
 

	
 
    def __init__(self, config: configmod.Config) -> None:
 
        pass
 
        # Subclasses that need configuration should override __init__ to check
 
        # and store it.
 

	
 
    @abc.abstractmethod
 
    def run(self, entry: Entry) -> errormod.Iter: ...
 

	
 
    def __init_subclass__(cls):
 
        cls.DIRECTIVE = cls.__orig_bases__[0].__args__[0]
 

	
 

	
 
TransactionHook = Hook[Transaction]
tests/test_meta_expense_allocation.py
Show inline comments
...
 
@@ -34,17 +34,18 @@ INVALID_VALUES = {
 
    'fundrasing',
 
    '',
 
}
 

	
 
TEST_KEY = 'expense-allocation'
 

	
 
@pytest.fixture(scope='module')
 
def hook():
 
    return meta_expense_allocation.MetaExpenseAllocation()
 
    config = testutil.TestConfig()
 
    return meta_expense_allocation.MetaExpenseAllocation(config)
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_postings(hook, src_value, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Cash', -25),
 
        ('Expenses:General', 25, {TEST_KEY: src_value}),
 
    ])
 
    errors = list(hook.run(txn))
tests/test_meta_income_type.py
Show inline comments
...
 
@@ -34,17 +34,18 @@ INVALID_VALUES = {
 
    'UTBI',
 
    '',
 
}
 

	
 
TEST_KEY = 'income-type'
 

	
 
@pytest.fixture(scope='module')
 
def hook():
 
    return meta_income_type.MetaIncomeType()
 
    config = testutil.TestConfig()
 
    return meta_income_type.MetaIncomeType(config)
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_postings(hook, src_value, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Cash', 25),
 
        ('Income:Other', -25, {TEST_KEY: src_value}),
 
    ])
 
    errors = list(hook.run(txn))
tests/test_meta_tax_implication.py
Show inline comments
...
 
@@ -46,17 +46,18 @@ INVALID_VALUES = {
 
    'Payrol',
 
    '',
 
}
 

	
 
TEST_KEY = 'tax-implication'
 

	
 
@pytest.fixture(scope='module')
 
def hook():
 
    return meta_tax_implication.MetaTaxImplication()
 
    config = testutil.TestConfig()
 
    return meta_tax_implication.MetaTaxImplication(config)
 

	
 
@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
 
def test_valid_values_on_postings(hook, src_value, set_value):
 
    txn = testutil.Transaction(postings=[
 
        ('Accrued:AccountsPayable', 25),
 
        ('Assets:Cash', -25, {TEST_KEY: src_value}),
 
    ])
 
    errors = list(hook.run(txn))
tests/test_plugin.py
Show inline comments
...
 
@@ -13,42 +13,58 @@
 
#
 
# You should have received a copy of the GNU Affero General Public License
 
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 

	
 
import pytest
 

	
 
from . import testutil
 

	
 
from conservancy_beancount import beancount_types, plugin
 
from conservancy_beancount import beancount_types, errors as errormod, plugin
 

	
 
HOOK_REGISTRY = plugin.HookRegistry()
 

	
 
class NonError(errormod.Error):
 
    pass
 

	
 

	
 
class TransactionHook:
 
    DIRECTIVE = beancount_types.Transaction
 
    HOOK_GROUPS = frozenset()
 

	
 
    def __init__(self, config):
 
        self.config = config
 

	
 
    def run(self, txn):
 
        assert False, "something called base class run method"
 

	
 

	
 
@HOOK_REGISTRY.add_hook
 
class ConfigurationError(TransactionHook):
 
    HOOK_GROUPS = frozenset(['unconfigured'])
 

	
 
    def __init__(self, config):
 
        raise errormod.ConfigurationError("testing error")
 

	
 

	
 
@HOOK_REGISTRY.add_hook
 
class TransactionError(TransactionHook):
 
    HOOK_GROUPS = frozenset(['configured'])
 

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

	
 

	
 
@HOOK_REGISTRY.add_hook
 
class PostingError(TransactionHook):
 
    HOOK_GROUPS = frozenset(['configured', 'posting'])
 

	
 
    def run(self, txn):
 
        return ['post:{}'.format(id(post)) for post in txn.postings]
 
        return [NonError('post:{}'.format(id(post)), txn)
 
                for post in txn.postings]
 

	
 

	
 
@pytest.fixture
 
def config_map():
 
    return {}
 

	
 
@pytest.fixture
 
def easy_entries():
...
 
@@ -60,18 +76,18 @@ def easy_entries():
 
        testutil.Transaction(postings=[
 
            ('Expenses:General', 10),
 
            ('Liabilites:CreditCard', -10),
 
        ]),
 
    ]
 

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

	
 
@pytest.mark.parametrize('group_str,expected', [
 
    (None, [TransactionError, PostingError]),
 
    ('', [TransactionError, PostingError]),
 
    ('all', [TransactionError, PostingError]),
 
    ('Transaction', [TransactionError, PostingError]),
tests/testutil.py
Show inline comments
...
 
@@ -15,16 +15,17 @@
 
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 

	
 
import datetime
 

	
 
import beancount.core.amount as bc_amount
 
import beancount.core.data as bc_data
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
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 check_post_meta(txn, *expected_meta, default=None):
...
 
@@ -89,8 +90,16 @@ class Transaction:
 
        elif args:
 
            if isinstance(args[-1], dict):
 
                kwargs = args[-1]
 
                args = args[:-1]
 
            posting = Posting(arg, *args, **kwargs)
 
        else:
 
            posting = arg
 
        self.postings.append(posting)
 

	
 

	
 
class TestConfig:
 
    def __init__(self, repo_path=None):
 
        self.repo_path = None if repo_path is None else Path(repo_path)
 

	
 
    def repository_path(self):
 
        return self.repo_path
0 comments (0 inline, 0 general)