Changeset - 89378cbf90fc
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-01-10 18:27:34
brettcsmith@brettcsmith.org
hooks.ledger_entry: Clean up whitespace in strings.

The hook now ensures it does not output whitespace that could be
significant to Ledger, either because it's a newline or an
account-amount separator.
3 files changed with 44 insertions and 6 deletions:
0 comments (0 inline, 0 general)
import2ledger/hooks/ledger_entry.py
Show inline comments
...
 
@@ -213,27 +213,28 @@ class AccountSplitter:
 
                    account_s, ' ' * sep_len, amount_s,
 
                    metadata.format_map(template_vars),
 
                )
 

	
 
    def render_next(self, template_vars):
 
        if template_vars is not self._last_template_vars:
 
            self._split_iter = self._iter_splits(template_vars)
 
            self._last_template_vars = template_vars
 
        return next(self._split_iter)
 

	
 

	
 
class Template:
 
    ACCOUNT_SPLIT_RE = re.compile(r'(?:\t|  )\s*')
 
    ACCOUNT_SPLIT_RE = re.compile(r'(?: ?\t|  )[ \t]*')
 
    DATE_FMT = '%Y/%m/%d'
 
    PAYEE_LINE_RE = re.compile(r'^\{(\w*_)*date\}\s')
 
    NEWLINE_RE = re.compile(r'[\f\n\r\v\u0085\u2028\u2029]')
 
    SIGNED_CURRENCY_FMT = '¤#,##0.###;¤-#,##0.###'
 
    UNSIGNED_CURRENCY_FMT = '#,##0.### ¤¤'
 

	
 
    def __init__(self, template_s, signed_currencies=frozenset(),
 
                 date_fmt=DATE_FMT,
 
                 signed_currency_fmt=SIGNED_CURRENCY_FMT,
 
                 unsigned_currency_fmt=UNSIGNED_CURRENCY_FMT,
 
                 template_name='<template>'):
 
        self.date_fmt = date_fmt
 
        self.date_field = 'date'
 
        self.splitter = AccountSplitter(
 
            signed_currencies, signed_currency_fmt, unsigned_currency_fmt, template_name)
...
 
@@ -292,30 +293,35 @@ class Template:
 
        else:
 
            self.splitter.set_metadata(str_flat)
 

	
 
    def render(self, template_vars):
 
        # template_vars must have these keys.  Raise a KeyError if not.
 
        template_vars['currency']
 
        template_vars['payee']
 
        if template_vars.get(self.date_field) is None:
 
            raise errors.UserInputConfigurationError(
 
                "entry needs {} field but that's not set by the importer".format(
 
                    self.date_field,
 
                ), self.splitter.template_name)
 
        render_vars = {
 
            'amount': strparse.currency_decimal(template_vars['amount']),
 
        }
 
        render_vars = {}
 
        for key, value in template_vars.items():
 
            if value is not None and (key == 'date' or key.endswith('_date')):
 
            if value is None:
 
                pass
 
            elif key == 'date' or key.endswith('_date'):
 
                render_vars[key] = value.strftime(self.date_fmt)
 
            elif isinstance(value, str):
 
                value = self.NEWLINE_RE.sub(' ', value)
 
                value = self.ACCOUNT_SPLIT_RE.sub(' ', value)
 
                render_vars[key] = value
 
        render_vars['amount'] = strparse.currency_decimal(template_vars['amount'])
 
        all_vars = collections.ChainMap(render_vars, template_vars)
 
        return ''.join(f(all_vars) for f in self.format_funcs)
 

	
 
    def is_empty(self):
 
        return not self.format_funcs
 

	
 

	
 
class LedgerEntryHook:
 
    KIND = HOOK_KINDS.OUTPUT
 

	
 
    def __init__(self, config):
 
        self.config = config
setup.py
Show inline comments
...
 
@@ -21,24 +21,24 @@ all_extras_require = [
 
]
 

	
 
REQUIREMENTS['extras_require']['all_importers'] = all_extras_require
 
REQUIREMENTS['tests_require'] = [
 
    'pytest',
 
    'PyYAML',
 
    *all_extras_require,
 
]
 

	
 
setup(
 
    name='import2ledger',
 
    description="Import different sources of financial data to Ledger",
 
    version='0.11.0',
 
    version='0.11.1',
 
    author='Brett Smith',
 
    author_email='brettcsmith@brettcsmith.org',
 
    license='GNU AGPLv3+',
 

	
 
    packages=find_packages(include=['import2ledger', 'import2ledger.*']),
 
    entry_points={
 
        'console_scripts': ['import2ledger = import2ledger.__main__:main'],
 
    },
 

	
 
    **REQUIREMENTS,
 
)
tests/test_hook_ledger_entry.py
Show inline comments
...
 
@@ -83,24 +83,56 @@ def test_complex_template():
 
        "2015-03-14 TT",
 
        "  ;Tag: Value",
 
        "  ;TransactionID: ABCDEF",
 
        "  Accrued:Accounts Receivable  $125.50",
 
        "  ;Entity: Supplier",
 
        "  Income:Donations:Spectrum Defense  $-119.85",
 
        "  ;Program: Spectrum Defense",
 
        "  ;Entity: T-T",
 
        "  Income:Donations:General  $-5.65",
 
        "  ;Entity: T-T",
 
    ]
 

	
 
def test_variable_whitespace_cleaned():
 
    # There are two critical parts of this to avoid making malformed Ledger
 
    # entries:
 
    # * Ensure newlines become non-newline whitespace so we don't write
 
    #   malformed lines.
 
    # * Collapse multiple spaces into one so variables in account names
 
    #   can't make malformed account lines by having a premature split
 
    #   between the account name and amount.
 
    render_vars = template_vars('W\t\tS', '125.50', other_vars={
 
        'entity': 'W\fS',
 
        'program': 'Spectrum\r\nDefense',
 
        'txid': 'ABC\v\tDEF',
 
    })
 
    lines = render_lines(
 
        render_vars, 'Complex',
 
        date_fmt='%Y-%m-%d',
 
        signed_currencies=['USD'],
 
    )
 
    assert lines == [
 
        "",
 
        "2015-03-14 W S",
 
        "  ;Tag: Value",
 
        "  ;TransactionID: ABC DEF",
 
        "  Accrued:Accounts Receivable  $125.50",
 
        "  ;Entity: Supplier",
 
        "  Income:Donations:Spectrum Defense  $-119.85",
 
        "  ;Program: Spectrum Defense",
 
        "  ;Entity: W S",
 
        "  Income:Donations:General  $-5.65",
 
        "  ;Entity: W S",
 
    ]
 

	
 
def test_balancing():
 
    lines = render_lines(template_vars('FF', '1.01'), 'FiftyFifty')
 
    assert lines == [
 
        "",
 
        "2015/03/14 FF",
 
        "  Accrued:Accounts Receivable  1.01 USD",
 
        "  Income:Donations  -0.50 USD",
 
        "  Income:Sales  -0.51 USD",
 
    ]
 

	
 
def test_multivalue():
 
    render_vars = template_vars('DD', '150.00', other_vars={
0 comments (0 inline, 0 general)