Changeset - 76f2707aacf7
[Not reviewed]
0 5 1
Brett Smith - 6 years ago 2017-12-31 17:35:20
brettcsmith@brettcsmith.org
hooks.ledger_entry: New hook to output Ledger entries.

This is roughly the smallest diff necessary to move output to a hook.
There's a lot of code reorganization that should still happen to bring it
better in line with this new structure.
6 files changed with 100 insertions and 28 deletions:
0 comments (0 inline, 0 general)
import2ledger/__main__.py
Show inline comments
...
 
@@ -19,48 +19,36 @@ class FileImporter:
 
        if in_path is None:
 
            in_path = pathlib.Path(in_file.name)
 
        importers = []
 
        for importer in self.importers:
 
            in_file.seek(0)
 
            if importer.can_import(in_file):
 
                try:
 
                    template = self.config.get_template(importer.TEMPLATE_KEY)
 
                except errors.UserInputConfigurationError as error:
 
                    if error.strerror.startswith('template not defined '):
 
                        have_template = False
 
                    else:
 
                        raise
 
                else:
 
                    have_template = not template.is_empty()
 
                if have_template:
 
                    importers.append((importer, template))
 
                importers.append(importer)
 
        if not importers:
 
            raise errors.UserInputFileError("no importers available", in_file.name)
 
        source_vars = {
 
            'source_abspath': in_path.absolute().as_posix(),
 
            'source_absdir': in_path.absolute().parent.as_posix(),
 
            'source_dir': in_path.parent.as_posix(),
 
            'source_name': in_path.name,
 
            'source_path': in_path.as_posix(),
 
            'source_stem': in_path.stem,
 
        }
 
        with self.config.open_output_file() as out_file:
 
            for importer, template in importers:
 
                in_file.seek(0)
 
                for entry_data in importer(in_file):
 
                    for hook in self.hooks:
 
                        hook_retval = hook.run(entry_data)
 
                        if hook_retval is None:
 
                            pass
 
                        elif hook_retval is False:
 
                            break
 
                        else:
 
                            entry_data = hook_retval
 
        for importer in importers:
 
            in_file.seek(0)
 
            source_vars['template'] = importer.TEMPLATE_KEY
 
            for entry_data in importer(in_file):
 
                entry_data = collections.ChainMap(entry_data, source_vars)
 
                for hook in self.hooks:
 
                    hook_retval = hook.run(entry_data)
 
                    if hook_retval is None:
 
                        pass
 
                    elif hook_retval is False:
 
                        break
 
                    else:
 
                        render_vars = collections.ChainMap(entry_data, source_vars)
 
                        print(template.render(render_vars), file=out_file, end='')
 
                        entry_data = hook_retval
 

	
 
    def import_path(self, in_path):
 
        if in_path is None:
 
            raise errors.UserInputFileError("only seekable files are supported", '<stdin>')
 
        with in_path.open(errors='replace') as in_file:
 
            if not in_file.seekable():
import2ledger/hooks/__init__.py
Show inline comments
...
 
@@ -16,12 +16,14 @@ HOOK_KINDS = enum.Enum('HOOK_KINDS', [
 
    # DATA_MUNGER hooks should add or change data in the entry based on what's
 
    # already in it.
 
    'DATA_MUNGER',
 
    # DATA_FILTER hooks make a decision about whether or not to proceed with
 
    # processing the entry.
 
    'DATA_FILTER',
 
    # OUTPUT hooks run last, sending the data somewhere else.
 
    'OUTPUT',
 
])
 

	
 
def load_all():
 
    hooks = list(dynload.submodule_items_named(__file__, operator.methodcaller('endswith', 'Hook')))
 
    hooks.sort(key=operator.attrgetter('KIND.value'))
 
    return hooks
import2ledger/hooks/ledger_entry.py
Show inline comments
 
new file 100644
 
from . import HOOK_KINDS
 

	
 
from .. import errors
 

	
 
class LedgerEntryHook:
 
    KIND = HOOK_KINDS.OUTPUT
 

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

	
 
    def run(self, entry_data):
 
        try:
 
            template = self.config.get_template(entry_data['template'])
 
        except errors.UserInputConfigurationError as error:
 
            if error.strerror.startswith('template not defined '):
 
                have_template = False
 
            else:
 
                raise
 
        else:
 
            have_template = not template.is_empty()
 
        if have_template:
 
            with self.config.open_output_file() as out_file:
 
                print(template.render(entry_data), file=out_file, end='')
tests/data/templates.ini
Show inline comments
...
 
@@ -37,6 +37,11 @@ template =  {custom_date}  {payee} - Custom
 
template =
 
 Assets:Cash  {amount}
 
 Income:Sales  -{amount} + {item_sales}
 
 ; :NonItem:
 
 Income:Sales  -{item_sales}
 
 ; :Item:
 

	
 
[Empty]
 
template =
 

	
 
[Nonexistent]
tests/test_hooks.py
Show inline comments
...
 
@@ -2,19 +2,25 @@ import argparse
 
import datetime
 
import itertools
 

	
 
import pytest
 

	
 
from import2ledger import hooks
 
from import2ledger.hooks import add_entity, default_date, filter_by_date
 
from import2ledger.hooks import add_entity, default_date, filter_by_date, ledger_entry
 

	
 
def test_load_all():
 
    all_hooks = list(hooks.load_all())
 
    positions = {hook: index for index, hook in enumerate(all_hooks)}
 
    assert positions[default_date.DefaultDateHook] < positions[add_entity.AddEntityHook]
 
    assert positions[add_entity.AddEntityHook] < positions[filter_by_date.FilterByDateHook]
 
    expected_order = [
 
        default_date.DefaultDateHook,
 
        add_entity.AddEntityHook,
 
        filter_by_date.FilterByDateHook,
 
        ledger_entry.LedgerEntryHook,
 
    ]
 
    actual_order = list(sorted(expected_order, key=positions.__getitem__))
 
    assert actual_order == expected_order
 

	
 
@pytest.mark.parametrize('in_key,payee,out_key,expected', [
 
    ('payee', 'Alex Smith', 'entity', 'Smith-Alex'),
 
    ('payee', 'Dakota D.  Doe', 'entity', 'Doe-Dakota-D'),
 
    ('payee', 'Björk', 'entity', 'Bjork'),
 
    ('payee', 'Fran Doe-Smith', 'entity', 'Doe-Smith-Fran'),
tests/test_templates.py
Show inline comments
 
import collections
 
import configparser
 
import contextlib
 
import datetime
 
import decimal
 
import io
 
import pathlib
 

	
 
import pytest
 
from import2ledger import errors, template
 
from import2ledger.hooks import ledger_entry
 

	
 
from . import DATA_DIR, normalize_whitespace
 

	
 
DATE = datetime.date(2015, 3, 14)
 

	
 
config = configparser.ConfigParser(comment_prefixes='#')
...
 
@@ -196,6 +199,51 @@ def test_line1_not_custom_payee():
 
    '{Decimal}',
 
    '{FOO}',
 
])
 
def test_bad_amount_expression(amount_expr):
 
    with pytest.raises(errors.UserInputError):
 
        template.Template(" Income  " + amount_expr)
 

	
 
class Config:
 
    def __init__(self):
 
        self.stdout = io.StringIO()
 

	
 
    @contextlib.contextmanager
 
    def open_output_file(self):
 
        yield self.stdout
 

	
 
    def get_template(self, key):
 
        try:
 
            return template_from(key)
 
        except KeyError:
 
            raise errors.UserInputConfigurationError(
 
                "template not defined in test config", key)
 

	
 

	
 
def run_hook(entry_data):
 
    hook_config = Config()
 
    hook = ledger_entry.LedgerEntryHook(hook_config)
 
    assert hook.run(entry_data) is None
 
    stdout = hook_config.stdout.getvalue()
 
    return normalize_whitespace(stdout).splitlines()
 

	
 
def hook_vars(template_key, payee, amount):
 
    return template_vars(payee, amount, other_vars={'template': template_key})
 

	
 
def test_hook_renders_template():
 
    entry_data = hook_vars('Simplest', 'BB', '0.99')
 
    lines = run_hook(entry_data)
 
    assert lines == [
 
        "",
 
        "2015/03/14 BB",
 
        "  Accrued:Accounts Receivable  0.99 USD",
 
        "  Income:Donations  -0.99 USD",
 
    ]
 

	
 
def test_hook_handles_empty_template():
 
    entry_data = hook_vars('Empty', 'CC', 1)
 
    assert not run_hook(entry_data)
 

	
 
def test_hook_handles_template_undefined():
 
    entry_data = hook_vars('Nonexistent', 'DD', 1)
 
    assert not run_hook(entry_data)
 

	
0 comments (0 inline, 0 general)