Changeset - 18eebbc0ed28
[Not reviewed]
0 4 0
Brett Smith - 6 years ago 2017-12-19 15:19:56
brettcsmith@brettcsmith.org
hooks: run() return value controls processing of entry data.

Instead of using in-band signaling with the entry_data dict.
I don't know why I didn't think of this in the first place.
4 files changed with 23 insertions and 11 deletions:
0 comments (0 inline, 0 general)
CODE.rst
Show inline comments
...
 
@@ -48,13 +48,19 @@ Hooks
 
Hooks make arbitrary transformations to entry data dicts.  Every entry data dict generated by an importer is run through every hook before being output.
 

	
 
``__init__(config)``
 
  Initializes the hook with the user's configuration.
 

	
 
``run(entry_data)``
 
  This method makes the hook's transformations to the entry data dict, if any.  If this method sets ``entry_data['_hook_cancel']`` to a truthy value, that entry will not be output.
 
  This method can make arbitrary transformations to the entry data, or filter it so it isn't output.
 

	
 
  If this method returns ``None``, processing the entry data continues normally.  Most hooks should do this, and just transform entry data in place.
 

	
 
  If this method returns ``False``, processing the entry data stops immediately.  The entry will not appear in the program output.
 

	
 
  If this method returns any other value, the program replaces the entry data with the return value, and continues processing.
 

	
 
Templates
 
~~~~~~~~~
 

	
 
Templates receive entry data dicts and format them into final output entries.
 

	
...
 
@@ -83,15 +89,20 @@ At a high level, import2ledger handles each input file this way::
 
  usable_importers = importers where can_handle(input_file) returns true
 
  for importer_class in usable_importers:
 
    template = built from importer_class.TEMPLATE_KEY
 
    input_file.seek(0)
 
    for entry_data in importer_class(input_file):
 
      for hook in all_hooks:
 
        hook.run(entry_data)
 
      if entry_data:
 
        template.render(entry_data)
 
        hook_return = hook.run(entry_data)
 
        if hook_return is False:
 
          break
 
        elif hook_return is not None:
 
          entry_data = hook_return
 
      else:
 
        if entry_data:
 
          template.render(entry_data)
 

	
 
Note in particular that multiple importers can handle the same input file.  This helps support inputs like Patreon's earnings CSV, where completely different transactions are generated from the same source.
 

	
 
Running tests
 
-------------
 

	
import2ledger/__main__.py
Show inline comments
...
 
@@ -46,19 +46,21 @@ class FileImporter:
 
            else:
 
                out_file = exit_stack.enter_context(output_path.open('a'))
 
            for importer, template in importers:
 
                default_date = self.config.get_default_date()
 
                in_file.seek(0)
 
                for entry_data in importer(in_file):
 
                    entry_data['_hook_cancel'] = False
 
                    for hook in self.hooks:
 
                        hook.run(entry_data)
 
                        if entry_data['_hook_cancel']:
 
                        hook_retval = hook.run(entry_data)
 
                        if hook_retval is None:
 
                            pass
 
                        elif hook_retval is False:
 
                            break
 
                        else:
 
                            entry_data = hook_retval
 
                    else:
 
                        del entry_data['_hook_cancel']
 
                        render_vars = collections.ChainMap(entry_data, source_vars)
 
                        print(template.render(render_vars), file=out_file, end='')
 

	
 
    def import_path(self, in_path):
 
        if in_path is None:
 
            raise errors.UserInputFileError("only seekable files are supported", '<stdin>')
import2ledger/hooks/filter_by_date.py
Show inline comments
...
 
@@ -6,7 +6,7 @@ class FilterByDateHook:
 
        try:
 
            date = entry_data['date']
 
        except KeyError:
 
            pass
 
        else:
 
            if not self.config.date_in_want_range(date):
 
                entry_data['_hook_cancel'] = True
 
                return False
tests/test_hooks.py
Show inline comments
...
 
@@ -54,14 +54,13 @@ class DateRangeConfig:
 
    (datetime.date(2016, 1, 31), datetime.date(2016, 2, 1), None, False),
 
    (datetime.date(2016, 12, 1), None, datetime.date(2016, 11, 30), False),
 
])
 
def test_filter_by_date(entry_date, start_date, end_date, allowed):
 
    entry_data = {'date': entry_date}
 
    hook = filter_by_date.FilterByDateHook(DateRangeConfig(start_date, end_date))
 
    hook.run(entry_data)
 
    assert entry_data.get('_hook_cancel', False) == (not allowed)
 
    assert hook.run(entry_data) is (None if allowed else False)
 

	
 
class DefaultDateConfig:
 
    ONE_DAY = datetime.timedelta(days=1)
 

	
 
    def __init__(self, start_date=None):
 
        if start_date is None:
0 comments (0 inline, 0 general)