diff --git a/import2ledger/config.py b/import2ledger/config.py index a961b6a692b1bc120c68f74aa2e93eb47e941b06..acc924dbdd81e0b2d5f3f22f3df5ad0e5889e4a1 100644 --- a/import2ledger/config.py +++ b/import2ledger/config.py @@ -74,20 +74,6 @@ class Configuration: '--output-path', '-O', metavar='PATH', help="Path of file to append entries to, or '-' for stdout (default).", ) - out_args.add_argument( - '--signed-currency', '--sign', metavar='CODE', - action='append', dest='signed_currencies', - help="Currency code to use currency sign for in Ledger entry amounts. " - "Can be specified multiple times.", - ) - out_args.add_argument( - '--signed-currency-format', '--sign-format', '-S', metavar='FORMAT', - help="Unicode number pattern to use for signed currencies in Ledger entry amounts", - ) - out_args.add_argument( - '--unsigned-currency-format', '--unsign-format', '-U', metavar='FORMAT', - help="Unicode number pattern to use for unsigned currencies in Ledger entry amounts", - ) return parser @@ -97,10 +83,6 @@ class Configuration: defaults={ 'loglevel': 'WARNING', 'output_path': '-', - 'signed_currencies': ','.join(babel.numbers.get_territory_currencies( - self.LOCALE.territory, start_date=self.TODAY)), - 'signed_currency_format': '¤#,##0.###;¤-#,##0.###', - 'unsigned_currency_format': '#,##0.### ¤¤', }) def _read_conffiles(self): diff --git a/import2ledger/hooks/ledger_entry.py b/import2ledger/hooks/ledger_entry.py index ddb08cf79edfbdaa815b53169aed7b2ffa492486..ef03f704561ecd70cb98438161d8148851c88e93 100644 --- a/import2ledger/hooks/ledger_entry.py +++ b/import2ledger/hooks/ledger_entry.py @@ -277,46 +277,52 @@ class LedgerEntryHook: def __init__(self, config): self.config = config + self.config_section = config.get_section('Ledger output') + if not any(value and not value.isspace() + for key, value in self.config_section.items() + if key.endswith(' ledger entry')): + raise errors.NotConfiguredError("no Ledger entries in config", None) + try: + signed_currencies = (code.strip().upper() for code in + self.config_section['signed currencies'].split(',')) + except KeyError: + territory = self.config.LOCALE.territory + if territory is None: + signed_currencies = [] + else: + signed_currencies = babel.numbers.get_territory_currencies( + territory, start_date=self.config.TODAY) + self.template_kwargs = { + 'date_fmt': self.config_section['date format'], + 'signed_currencies': frozenset(signed_currencies), + 'signed_currency_fmt': self.config_section.get( + 'signed currency format', Template.SIGNED_CURRENCY_FMT), + 'unsigned_currency_fmt': self.config_section.get( + 'unsigned currency format', Template.UNSIGNED_CURRENCY_FMT), + } @staticmethod @functools.lru_cache() - def _load_template(config, section_name, config_key): - section_config = config.get_section(section_name) - try: - template_s = section_config[config_key] - except KeyError: - raise errors.UserInputConfigurationError( - "Ledger template not defined in [{}]".format(section_name), - config_key, - ) - return Template( - template_s, - date_fmt=section_config['date format'], - signed_currencies=[code.strip().upper() for code in section_config['signed_currencies'].split(',')], - signed_currency_fmt=section_config['signed_currency_format'], - unsigned_currency_fmt=section_config['unsigned_currency_format'], - template_name=config_key, - ) + def _load_template(template_s, signed_currencies, + date_fmt, + signed_currency_fmt, + unsigned_currency_fmt, + template_name): + return Template(**locals()) def run(self, entry_data): try: - template_key = entry_data['ledger template'] + template_name = entry_data['ledger template'] except KeyError: - template_key = '{} {} ledger entry'.format( + template_name = '{} {} ledger entry'.format( strparse.rslice_words(entry_data['importer_module'], -1, '.', 1), entry_data['importer_class'][:-8].lower(), ) - try: - template = self._load_template(self.config, None, template_key) - except errors.UserInputConfigurationError as error: - if error.strerror.startswith('Ledger template not defined '): - have_template = False - else: - raise - else: - have_template = not template.is_empty() - if not have_template: - logger.warning("no Ledger template defined as %r", template_key) + template_s = self.config_section.get(template_name, '') + template = self._load_template( + template_s, template_name=template_name, **self.template_kwargs) + if template.is_empty(): + logger.warning("no Ledger template defined as %r", template_name) else: with self.config.open_output_file() as out_file: print(template.render(entry_data), file=out_file, end='') diff --git a/tests/__init__.py b/tests/__init__.py index 1f2a59a2c164e11f1240b36ec11002ac429922f2..de0e2376fb11a404a34862de5e73c6666b75c895 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,19 +1,26 @@ import collections import configparser +import datetime import decimal +import operator import pathlib import re +import babel.core from import2ledger import __main__ as i2lmain decimal.setcontext(i2lmain.decimal_context()) DATA_DIR = pathlib.Path(__file__).with_name('data') +START_DATE = datetime.date.today() def normalize_whitespace(s): return re.sub(r'(\t| {3,})', ' ', s) class Config: + LOCALE = babel.core.Locale('en_US_POSIX') + TODAY = START_DATE + def __init__(self, options_dict=None): self.config = configparser.ConfigParser( defaults={ @@ -29,3 +36,5 @@ class Config: return self.config[section_name] except KeyError: return self.config[configparser.DEFAULTSECT] + + __getitem__ = property(operator.attrgetter('config.__getitem__')) diff --git a/tests/data/templates.ini b/tests/data/templates.ini deleted file mode 100644 index 7e29cff291308a612fa44bfeae591dc32fac4744..0000000000000000000000000000000000000000 --- a/tests/data/templates.ini +++ /dev/null @@ -1,53 +0,0 @@ -[DEFAULT] -date format = %%Y-%%m-%%d -signed_currencies = USD, CAD -signed_currency_format = ¤#,##0.### -unsigned_currency_format = #,##0.### ¤¤ - -[Simplest] -template = Accrued:Accounts Receivable {amount} - Income:Donations -{amount} - -[FiftyFifty] -template = - Accrued:Accounts Receivable {amount} - Income:Donations -.5 * {amount} - Income:Sales -.5*{amount} - -[Complex] -template = - ;Tag: Value - ;TransactionID: {txid} - Accrued:Accounts Receivable {amount} - ;Entity: Supplier - Income:Donations:{program} -.955* {amount} - ;Program: {program} - ;Entity: {entity} - Income:Donations:General -.045 * {amount} - ;Entity: {entity} - -[Multivalue] -template = - Expenses:Taxes {tax} - ;TaxAuthority: IRS - Accrued:Accounts Receivable {amount} - {tax} - Income:RBI -.1*{amount} - Income:Donations -.9*{amount} - -[Custom Payee] -template = {custom_date} {payee} - Custom - Accrued:Accounts Receivable {amount} - Income:Donations -{amount} - -[Multisplit] -template = - Assets:Cash {amount} - Income:Sales -{amount} + {item_sales} - ; :NonItem: - Income:Sales -{item_sales} - ; :Item: - -[Empty] -template = - -[Nonexistent] diff --git a/tests/data/templates.yml b/tests/data/templates.yml new file mode 100644 index 0000000000000000000000000000000000000000..95aac7305f64ad645d946789219947240b542295 --- /dev/null +++ b/tests/data/templates.yml @@ -0,0 +1,40 @@ +--- +DEFAULT: + date format: "%%Y-%%m-%%d" +Ledger output: + signed currencies: USD, CAD + signed currency format: "¤#,##0.###" + Simplest ledger entry: | + Accrued:Accounts Receivable {amount} + Income:Donations -{amount} + FiftyFifty ledger entry: | + Accrued:Accounts Receivable {amount} + Income:Donations -.5 * {amount} + Income:Sales -.5*{amount} + Complex ledger entry: | + ;Tag: Value + ;TransactionID: {txid} + Accrued:Accounts Receivable {amount} + ;Entity: Supplier + Income:Donations:{program} -.955* {amount} + ;Program: {program} + ;Entity: {entity} + Income:Donations:General -.045 * {amount} + ;Entity: {entity} + Multivalue ledger entry: | + Expenses:Taxes {tax} + ;TaxAuthority: IRS + Accrued:Accounts Receivable {amount} - {tax} + Income:RBI -.1*{amount} + Income:Donations -.9*{amount} + Custom Payee ledger entry: | + {custom_date} {payee} - Custom + Accrued:Accounts Receivable {amount} + Income:Donations -{amount} + Multisplit ledger entry: | + Assets:Cash {amount} + Income:Sales -{amount} + {item_sales} + ; :NonItem: + Income:Sales -{item_sales} + ; :Item: + Empty ledger entry: "" diff --git a/tests/data/test_main.ini b/tests/data/test_main.ini index 8a209e2ab5cb27739672f560e57e669e2bfd93b5..c6f9de2a8281ecdb57ede21add3ac8e1bf74fca3 100644 --- a/tests/data/test_main.ini +++ b/tests/data/test_main.ini @@ -1,9 +1,9 @@ [DEFAULT] date format = %%Y/%%m/%%d loglevel = critical -signed_currencies = USD +signed currencies = USD -[One] +[Ledger output] patreon cardfees ledger entry = Accrued:Accounts Receivable -{amount} Expenses:Fees:Credit Card {amount} diff --git a/tests/test_config.py b/tests/test_config.py index 4ba9b162d8d50407549b528371035cb172fe3f87..a71825b672c5bf0664846230d08d827cfb86cfc7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,14 +5,12 @@ import logging import os import pathlib -START_DATE = datetime.date.today() - from unittest import mock import pytest from import2ledger import config, errors, strparse -from . import DATA_DIR +from . import DATA_DIR, START_DATE def config_from_file(path, arglist=[], stdout=None, stderr=None, *, init=True): path = pathlib.Path(path) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 3e4599f58e6343e4398c61fb3dca4d3185b9acf7..62308259696677dc2fe12bf715731da645629ba7 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -17,7 +17,9 @@ def test_load_no_config_needed(): def test_load_all(): config_dict = date_hooks.DateHookTestBase().new_config() - return _run_order_test(Config(config_dict), [ + config = Config(config_dict) + config['DEFAULT']['test ledger entry'] = 'Income {amount}' + return _run_order_test(config, [ default_date.DefaultDateHook, add_entity.AddEntityHook, filter_by_date.FilterByDateHook, diff --git a/tests/test_hooks_ledger_entry.py b/tests/test_hooks_ledger_entry.py index feb876c05f24892e5b0338ecbd7f9068b5ef5334..f418c93a20322f75a9fec73dfc3afdff16681cb3 100644 --- a/tests/test_hooks_ledger_entry.py +++ b/tests/test_hooks_ledger_entry.py @@ -1,5 +1,4 @@ import collections -import configparser import contextlib import datetime import decimal @@ -7,19 +6,31 @@ import io import pathlib import pytest +import yaml from import2ledger import errors from import2ledger.hooks import ledger_entry -from . import DATA_DIR, normalize_whitespace +from . import DATA_DIR, normalize_whitespace, Config as BaseConfig -DATE = datetime.date(2015, 3, 14) +with pathlib.Path(DATA_DIR, 'templates.yml').open() as conffile: + _config_dict = yaml.load(conffile) + +class Config(BaseConfig): + def __init__(self, options_dict=_config_dict): + super().__init__(options_dict) + self.stdout = io.StringIO() -config = configparser.ConfigParser(comment_prefixes='#') -with pathlib.Path(DATA_DIR, 'templates.ini').open() as conffile: - config.read_file(conffile) + @contextlib.contextmanager + def open_output_file(self): + yield self.stdout + + +DATE = datetime.date(2015, 3, 14) -def template_from(section_name, *args, **kwargs): - return ledger_entry.Template(config[section_name]['template'], *args, **kwargs) +def template_from(template_name, *args, **kwargs): + section = Config().get_section('Ledger output') + template_s = section['{} ledger entry'.format(template_name)] + return ledger_entry.Template(template_s, *args, **kwargs) def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None): call_vars = { @@ -27,7 +38,6 @@ def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None): 'currency': currency, 'date': date, 'payee': payee, - 'ledger template': 'template', } if other_vars is None: return call_vars @@ -204,24 +214,12 @@ def test_bad_amount_expression(amount_expr): with pytest.raises(errors.UserInputError): ledger_entry.Template(" Income " + amount_expr) -class Config: - def __init__(self, use_section): - self.section_name = use_section - self.stdout = io.StringIO() - - @contextlib.contextmanager - def open_output_file(self): - yield self.stdout - - def get_section(self, name=None): - return config[self.section_name] - - -def run_hook(entry_data, config_section): - hook_config = Config(config_section) - hook = ledger_entry.LedgerEntryHook(hook_config) +def run_hook(entry_data, template_name): + config = Config() + entry_data['ledger template'] = '{} ledger entry'.format(template_name) + hook = ledger_entry.LedgerEntryHook(config) assert hook.run(entry_data) is None - stdout = hook_config.stdout.getvalue() + stdout = config.stdout.getvalue() return normalize_whitespace(stdout).splitlines() def test_hook_renders_template(): @@ -242,3 +240,7 @@ def test_hook_handles_template_undefined(): entry_data = template_vars('DD', 1) assert not run_hook(entry_data, 'Nonexistent') +def test_unconfigured(): + config = Config({}) + with pytest.raises(errors.NotConfiguredError): + ledger_entry.LedgerEntryHook(config) diff --git a/tests/test_main.py b/tests/test_main.py index d515aedf35657834295dd71fb9d493321c2668e3..be9544b9e1848972b6a8ac23bc228e0084add901 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -54,10 +54,7 @@ def path_vars(path): def test_fees_import(): source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv') - arglist = ARGLIST + [ - '-c', 'One', - source_path.as_posix(), - ] + arglist = ARGLIST + [source_path.as_posix()] exitcode, stdout, _ = run_main(arglist) assert exitcode == 0 actual = list(format_entries(stdout)) @@ -67,7 +64,6 @@ def test_fees_import(): def test_date_range_import(): source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv') arglist = ARGLIST + [ - '-c', 'One', '--date-range', '2017/10/01-', source_path.as_posix(), ]