diff --git a/import2ledger/__main__.py b/import2ledger/__main__.py index c03c71dcf0ab608433d32f567c76054f54baa467..57ec1e9c608b0c8f7f158f625cd87c115d33ea0f 100644 --- a/import2ledger/__main__.py +++ b/import2ledger/__main__.py @@ -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: diff --git a/import2ledger/hooks/__init__.py b/import2ledger/hooks/__init__.py index 0e7caa1090b0cb1a8708a486cbf619d890cb79d4..87a345f4c98f486bfb856d64d475198b909a30ad 100644 --- a/import2ledger/hooks/__init__.py +++ b/import2ledger/hooks/__init__.py @@ -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(): diff --git a/import2ledger/hooks/ledger_entry.py b/import2ledger/hooks/ledger_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..f1b10dea4d7f68afa76a963a2fd262c5fb6b2232 --- /dev/null +++ b/import2ledger/hooks/ledger_entry.py @@ -0,0 +1,23 @@ +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='') diff --git a/tests/data/templates.ini b/tests/data/templates.ini index 26e7f94322f1f37b6f615095c9af6b291ff78769..fff0c75aa3ab5d9141b6e2f6305f415832730c5c 100644 --- a/tests/data/templates.ini +++ b/tests/data/templates.ini @@ -40,3 +40,8 @@ template = ; :NonItem: Income:Sales -{item_sales} ; :Item: + +[Empty] +template = + +[Nonexistent] diff --git a/tests/test_hooks.py b/tests/test_hooks.py index d5d61d7187f2dc1a968df48cc671af5193de58d9..95c2e60388d4089135a04442386d7900102d6e99 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -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'), diff --git a/tests/test_templates.py b/tests/test_templates.py index b0a1740d3db2a2cd6fd6419afbcef39c113f060c..5cc03dc87cfeea2c3b1ad4d918c8e353c2070394 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,11 +1,14 @@ 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) +