From 08073f752bc84428f3b5bf4b6fe75fe6ef0dfd5b Mon Sep 17 00:00:00 2001
From: Brett Smith <brettcsmith@brettcsmith.org>
Date: Wed, 17 May 2017 11:32:05 -0400
Subject: [PATCH] Configuration: Parse more conversion arguments for historical
 subcommand.

---
 oxrlib/config.py            | 48 +++++++++++++++++++++++++++++++++++--
 tests/test_Configuration.py | 34 ++++++++++++++++++++++++++
 2 files changed, 80 insertions(+), 2 deletions(-)

diff --git a/oxrlib/config.py b/oxrlib/config.py
index d748861..675d410 100644
--- a/oxrlib/config.py
+++ b/oxrlib/config.py
@@ -1,6 +1,7 @@
 import argparse
 import configparser
 import datetime
+import decimal
 import os.path
 import pathlib
 
@@ -13,7 +14,7 @@ base=USD
 """
 
 def currency_code(s):
-    if not (len(s) == 3) and s.isalpha():
+    if not ((len(s) == 3) and s.isalpha()):
         raise ValueError("bad currency code: {!r}".format(s))
     return s.upper()
 
@@ -24,6 +25,7 @@ def date_from(fmt_s):
 
 class Configuration:
     DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini')
+    PREPOSITIONS = frozenset(['in', 'to', 'into'])
 
     def __init__(self, arglist):
         argparser = self._build_argparser()
@@ -56,15 +58,24 @@ class Configuration:
         subparsers = prog_parser.add_subparsers()
 
         hist_parser = subparsers.add_parser('historical', aliases=['hist'])
-        hist_parser.set_defaults(command='historical')
+        hist_parser.set_defaults(
+            command='historical',
+            amount=None,
+            from_currency=None,
+        )
         hist_parser.add_argument(
             '--base',
+            type=currency_code,
             help="Base currency (default USD)",
         )
         hist_parser.add_argument(
             'date',
             type=date_from('%Y-%m-%d'), metavar='YYYY-MM-DD',
         )
+        hist_parser.add_argument(
+            'remainder',
+            nargs=argparse.REMAINDER,
+        )
 
         return prog_parser
 
@@ -73,9 +84,42 @@ class Configuration:
         conffile.read_string(CONFFILE_SEED)
         return conffile
 
+    def _convert_or_error(self, argtype, s_value, argname=None, typename=None):
+        try:
+            return argtype(s_value)
+        except (decimal.InvalidOperation, TypeError, ValueError):
+            errmsg = []
+            if argname:
+                errmsg.append("argument {}".format(argname))
+            if typename is None:
+                typename = argtype.__name__.replace('_', ' ')
+            errmsg.append("invalid {} value".format(typename))
+            errmsg.append(repr(s_value))
+            self.error(': '.join(errmsg))
+
     def _post_hook_historical(self):
         if self.args.base is None:
             self.args.base = self.conffile.get('Historical', 'base')
+        self.args.to_currency = self.args.base
+        remain_len = len(self.args.remainder)
+        if (remain_len > 3) and (self.args.remainder[2].lower() in self.PREPOSITIONS):
+            del self.args.remainder[2]
+            remain_len -= 1
+        if remain_len == 0:
+            pass
+        elif remain_len == 1:
+            self.args.from_currency = self._convert_or_error(
+                currency_code, self.args.remainder[0])
+        elif remain_len < 4:
+            self.args.amount = self._convert_or_error(
+                decimal.Decimal, self.args.remainder[0])
+            self.args.from_currency = self._convert_or_error(
+                currency_code, self.args.remainder[1])
+            if remain_len == 3:
+                self.args.to_currency = self._convert_or_error(
+                    currency_code, self.args.remainder[2])
+        else:
+            self.error("too many arguments")
 
     def _build_cache_loader(self):
         kwargs = dict(self.conffile.items('Cache'))
diff --git a/tests/test_Configuration.py b/tests/test_Configuration.py
index 00d5ddf..b59d3c0 100644
--- a/tests/test_Configuration.py
+++ b/tests/test_Configuration.py
@@ -1,3 +1,4 @@
+import decimal
 import os
 
 import pytest
@@ -42,3 +43,36 @@ def test_historical_default_base(ini_filename, expected_currency, use_switch, an
     arglist.append(any_date.isoformat())
     config = config_from(ini_filename, arglist)
     assert config.args.base == expected_currency
+
+@pytest.mark.parametrize('amount,from_curr,preposition,to_curr', [
+    (None, 'JPY', None, None),
+    (decimal.Decimal('1000'), 'chf', None, None),
+    (decimal.Decimal('999'), 'Eur', None, 'Chf'),
+    (decimal.Decimal('12.34'), 'gbp', 'IN', 'eur'),
+])
+def test_historical_argparsing_success(amount, from_curr, preposition, to_curr, any_date):
+    arglist = ['historical', any_date.isoformat()]
+    arglist.extend(str(s) for s in [amount, from_curr, preposition, to_curr]
+                   if s is not None)
+    config = config_from(os.devnull, arglist)
+    assert config.args.amount == amount
+    assert config.args.from_currency == from_curr.upper()
+    if to_curr is not None:
+        assert config.args.to_currency == to_curr.upper()
+
+@pytest.mark.parametrize('arglist', [
+    ['100'],
+    ['120', 'dollars'],
+    ['to', '42', 'usd'],
+    ['99', 'usd', 'minus', 'jpy'],
+    ['usdjpy'],
+    ['44', 'eur', 'in', 'chf', 'pronto'],
+])
+def test_historical_argparsing_failure(arglist, any_date):
+    arglist = ['historical', any_date.isoformat()] + arglist
+    try:
+        config = config_from(os.devnull, arglist)
+    except SystemExit:
+        pass
+    else:
+        assert not vars(config.args), "bad arglist succeeded"