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
...
 
@@ -22,17 +22,7 @@ class FileImporter:
 
        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 = {
...
 
@@ -43,21 +33,19 @@ class FileImporter:
 
            '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:
import2ledger/hooks/__init__.py
Show inline comments
...
 
@@ -19,6 +19,8 @@ HOOK_KINDS = enum.Enum('HOOK_KINDS', [
 
    # 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():
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
...
 
@@ -40,3 +40,8 @@ template =
 
 ; :NonItem:
 
 Income:Sales  -{item_sales}
 
 ; :Item:
 

	
 
[Empty]
 
template =
 

	
 
[Nonexistent]
tests/test_hooks.py
Show inline comments
...
 
@@ -5,13 +5,19 @@ 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'),
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
 

	
...
 
@@ -199,3 +202,48 @@ def test_line1_not_custom_payee():
 
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)