Repository: eventbrite/conformity

Issue:
Improve Currint fields to more accurately support requests and responses
Currently, we have an `Amount` and an `AmountDictionary`. `Amount` is fine. `AmountDictionary` works only for our API requests, and not for all of them.

- We need an `AmountString` that accepts either `CUR,VALUE` or `CUR:VALUE`
- We need an `AmountResponseDictionary` that has `value`, `currency`, `major_value`, and `display`

Discussion:

Patch:
diff --git a/conformity/fields/currency.py b/conformity/fields/currency.py
index 0659a86..c3f4b90 100644
--- a/conformity/fields/currency.py
+++ b/conformity/fields/currency.py
@@ -3,13 +3,16 @@ from __future__ import (
     unicode_literals,
 )
 
+import re
 from typing import (  # noqa: F401 TODO Python 3
     AbstractSet,
     Any as AnyType,
     Iterable,
     List as ListType,
     Optional,
+    Tuple as TupleType,
 )
+import warnings
 
 import attr
 import currint
@@ -24,6 +27,7 @@ from conformity.fields.basic import (  # noqa: F401 TODO Python 3
     Constant,
     Integer,
     Introspection,
+    UnicodeString,
 )
 from conformity.fields.structures import Dictionary
 from conformity.utils import (
@@ -36,6 +40,42 @@ from conformity.utils import (
 )
 
 
+__all__ = (
+    'Amount',
+    'AmountRequestDictionary',
+    'AmountResponseDictionary',
+    'AmountString',
+)
+
+
+DEFAULT_CURRENCY_CODES = frozenset(currint.currencies.keys())
+
+
+def _get_errors_for_currency_amount(
+    currency_code,  # type: six.text_type
+    value,  # type: int
+    valid_currencies,  # type: AbstractSet[six.text_type]
+    gt,  # type: Optional[int]
+    gte,  # type: Optional[int]
+    lt,  # type: Optional[int]
+    lte,  # type: Optional[int]
+):
+    errors = []
+
+    if currency_code not in valid_currencies:
+        errors.append(Error('Not a valid currency code', code=ERROR_CODE_INVALID))
+    if gt is not None and value <= gt:
+        errors.append(Error('Value not > {}'.format(gt), code=ERROR_CODE_INVALID))
+    if lt is not None and value >= lt:
+        errors.append(Error('Value not < {}'.format(lt), code=ERROR_CODE_INVALID))
+    if gte is not None and value < gte:
+        errors.append(Error('Value not >= {}'.format(gte), code=ERROR_CODE_INVALID))
+    if lte is not None and value > lte:
+        errors.append(Error('Value not <= {}'.format(lte), code=ERROR_CODE_INVALID))
+
+    return errors
+
+
 @attr.s
 class Amount(Base):
     """
@@ -45,6 +85,7 @@ class Amount(Base):
     """
 
     introspect_type = 'currint.Amount'
+
     valid_currencies = attr.ib(
         default=frozenset(),
         validator=attr_is_iterable(attr_is_string(), attr_is_set()),
@@ -57,7 +98,7 @@ class Amount(Base):
 
     def __attrs_post_init__(self):  # type: () -> None
         if not self.valid_currencies:
-            self.valid_currencies = frozenset(currint.currencies.keys())
+            self.valid_currencies = DEFAULT_CURRENCY_CODES
 
     def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, currint.Amount):
@@ -66,44 +107,23 @@ class Amount(Base):
                 code=ERROR_CODE_INVALID,
             )]
 
-        errors = []
-        if value.currency.code not in self.valid_currencies:
-            errors.append(Error(
-                'Not a valid currency code',
-                code=ERROR_CODE_INVALID,
-                pointer='currency.code',
-            ))
-        if self.gt is not None and value.value <= self.gt:
-            errors.append(Error(
-                'Value not > {}'.format(self.gt),
-                code=ERROR_CODE_INVALID,
-                pointer='value',
-            ))
-        if self.lt is not None and value.value >= self.lt:
-            errors.append(Error(
-                'Value not < {}'.format(self.lt),
-                code=ERROR_CODE_INVALID,
-                pointer='value',
-            ))
-        if self.gte is not None and value.value < self.gte:
-            errors.append(Error(
-                'Value not >= {}'.format(self.gte),
-                code=ERROR_CODE_INVALID,
-                pointer='value',
-            ))
-        if self.lte is not None and value.value > self.lte:
-            errors.append(Error(
-                'Value not <= {}'.format(self.lte),
-                code=ERROR_CODE_INVALID,
-                pointer='value',
-            ))
-        return errors
+        return _get_errors_for_currency_amount(
+            value.currency.code,
+            value.value,
+            self.valid_currencies,
+            self.gt,
+            self.gte,
+            self.lt,
+            self.lte,
+        )
 
     def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
-            'valid_currencies': sorted(self.valid_currencies),
+            'valid_currencies': (
+                '(all currencies)' if self.valid_currencies is DEFAULT_CURRENCY_CODES else sorted(self.valid_currencies)
+            ),
             'gt': self.gt,
             'gte': self.gte,
             'lt': self.lt,
@@ -111,24 +131,38 @@ class Amount(Base):
         })
 
 
-class AmountDictionary(Dictionary):
+class AmountRequestDictionary(Dictionary):
     """
     Conformity field that ensures that the value is a dictionary containing exactly fields `'currency'` and `'value'`
     and optionally enforces boundaries for those values with the `valid_currencies`, `gt`, `gte`, `lt`, and `lte`
-    arguments. This field requires that Currint be installed.
+    arguments. This field requires that Currint be installed. No other arguments are supported; `*args` and `**kwargs`
+    are deprecated and will be removed in Conformity 2.0.0.
     """
 
     def __init__(
         self,
         valid_currencies=None,  # type: Iterable[six.text_type]
-        gt=None,  # type: int
-        gte=None,  # type: int
-        lt=None,  # type: int
-        lte=None,  # type: int
+        gt=None,  # type: Optional[int]
+        gte=None,  # type: Optional[int]
+        lt=None,  # type: Optional[int]
+        lte=None,  # type: Optional[int]
+        description=None,  # type: Optional[six.text_type]
         *args,  # type: AnyType
         **kwargs  # type: AnyType
     ):
         # type: (...) -> None
+        """
+        Construct the field.
+
+        :param valid_currencies: An iterable of valid currencies (if not specified, all valid currencies will be used)
+        :param gt: If specified, the value must be greater than this
+        :param gte: If specified, the value must be greater than or equal to this
+        :param lt: If specified, the value must be less than this
+        :param lte: If specified, the value must be less than or equal to this
+        :param description: The description for documentation
+        :param args: Deprecated, unused, and will be removed in version 2.0.0
+        :param kwargs: Deprecated, unused, and will be removed in version 2.0.0
+        """
         if valid_currencies is not None and (
             not hasattr(valid_currencies, '__iter__') or
             not all(isinstance(c, six.text_type) for c in valid_currencies)
@@ -144,7 +178,152 @@ class AmountDictionary(Dictionary):
         if lte is not None and not isinstance(lte, int):
             raise TypeError("'lte' must be an int")
 
-        super(AmountDictionary, self).__init__({
-            'currency': Constant(*(valid_currencies or currint.currencies.keys())),
-            'value': Integer(gt=gt, gte=gte, lt=lt, lte=lte),
-        }, *args, **kwargs)
+        if args or kwargs:
+            warnings.warn(
+                '*args and **kwargs are deprecated in AmountRequestDictionary and will be removed in Conformity 2.0.',
+                DeprecationWarning,
+            )
+
+        super(AmountRequestDictionary, self).__init__(
+            {
+                'currency': Constant(*(valid_currencies or DEFAULT_CURRENCY_CODES)),
+                'value': Integer(gt=gt, gte=gte, lt=lt, lte=lte),
+            },
+            optional_keys=(),
+            allow_extra_keys=False,
+            description=description,
+        )
+
+
+class AmountDictionary(AmountRequestDictionary):
+    """
+    :deprecated:
+    """
+    def __init__(
+        self,
+        valid_currencies=None,  # type: Iterable[six.text_type]
+        gt=None,  # type: Optional[int]
+        gte=None,  # type: Optional[int]
+        lt=None,  # type: Optional[int]
+        lte=None,  # type: Optional[int]
+        description=None,  # type: Optional[six.text_type]
+        *args,  # type: AnyType
+        **kwargs  # type: AnyType
+    ):
+        warnings.warn(
+            'AmountDictionary is deprecated and will be removed in Conformity 2.0. '
+            'Use AmountRequestDictionary, instead.',
+            DeprecationWarning,
+        )
+
+        # type ignored due to MyPy bug https://github.com/python/mypy/issues/2582
+        super(AmountDictionary, self).__init__(  # type: ignore
+            valid_currencies=valid_currencies,
+            gt=gt,
+            gte=gte,
+            lt=lt,
+            lte=lte,
+            description=description,
+            *args,
+            **kwargs
+        )
+
+
+@attr.s
+class AmountString(Base):
+    """
+    Conformity field that ensures that the value is a unicode string matching the format CUR,1234 or CUR:1234, where
+    the part before the delimiter is a valid currency and the part after the delimiter is an integer. It also optionally
+    enforces boundaries for those values with the `valid_currencies`, `gt`, `gte`, `lt`, and `lte` arguments. This
+    field requires that Currint be installed.
+    """
+
+    _format = re.compile(r'[,:]')
+
+    introspect_type = 'currency_amount_string'
+
+    valid_currencies = attr.ib(
+        default=frozenset(),
+        validator=attr_is_iterable(attr_is_string(), attr_is_set()),
+    )  # type: AbstractSet[six.text_type]
+    gt = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
+    gte = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
+    lt = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
+    lte = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
+    description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
+
+    def __attrs_post_init__(self):  # type: () -> None
+        if not self.valid_currencies:
+            self.valid_currencies = DEFAULT_CURRENCY_CODES
+
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
+        if not isinstance(value, six.text_type):
+            return [Error('Not a unicode string currency amount')]
+
+        parts = self._format.split(value)
+        if len(parts) != 2:
+            return [Error('Currency string does not match format CUR,1234 or CUR:1234')]
+
+        currency = parts[0]
+        try:
+            value = int(parts[1])
+        except ValueError:
+            return [Error('Currency amount {} cannot be converted to an integer'.format(parts[1]))]
+
+        return _get_errors_for_currency_amount(
+            currency,
+            value,
+            self.valid_currencies,
+            self.gt,
+            self.gte,
+            self.lt,
+            self.lte,
+        )
+
+    def introspect(self):  # type: () -> Introspection
+        return strip_none({
+            'type': self.introspect_type,
+            'description': self.description,
+            'valid_currencies': (
+                '(all currencies)' if self.valid_currencies is DEFAULT_CURRENCY_CODES else sorted(self.valid_currencies)
+            ),
+            'gt': self.gt,
+            'gte': self.gte,
+            'lt': self.lt,
+            'lte': self.lte,
+        })
+
+
+class AmountResponseDictionary(Dictionary):
+    """
+    Conformity field that ensures that the value is a dictionary containing at least fields `'currency'` and `'value'`
+    and optionally fields `'major_value'` and `'display'`. This field requires that Currint be installed.
+    """
+
+    def __init__(self, description=None, major_value_required=True, display_required=True):
+        # type: (Optional[six.text_type], bool, bool) -> None
+        """
+        Construct the field.
+
+        :param description: The description for documentation
+        :param major_value_required: By default, `'major_value'` is a required field in the response, but setting this
+                                     to `False` makes it optional
+        :param display_required: By default, `'display'` is a required field in the response, but setting this to
+                                 `False` makes it optional
+        """
+        optional_keys = ()  # type: TupleType[six.text_type, ...]
+        if not major_value_required:
+            optional_keys += ('major_value', )
+        if not display_required:
+            optional_keys += ('display', )
+        super(AmountResponseDictionary, self).__init__(
+            {
+                'currency': Constant(*DEFAULT_CURRENCY_CODES),
+                'value': Integer(),
+                'major_value': UnicodeString(),
+                'display': UnicodeString(),
+            },
+            optional_keys=optional_keys,
+            allow_extra_keys=False,
+            description=description,
+        )
diff --git a/conformity/fields/email.py b/conformity/fields/email.py
index 9890678..dc4cbb6 100644
--- a/conformity/fields/email.py
+++ b/conformity/fields/email.py
@@ -9,6 +9,7 @@ from typing import (  # noqa: F401  TODO Python 3
     Iterable,
     List as ListType,
 )
+import warnings
 
 import six
 
@@ -67,6 +68,12 @@ class EmailAddress(UnicodeString):
         ):
             raise TypeError("'whitelist' must be an iterable of unicode strings")
 
+        if message is not None or code is not None:
+            warnings.warn(
+                'Arguments `message` and `code` are deprecated in EmailAddress and will be removed in Conformity 2.0.',
+                DeprecationWarning,
+            )
+
         super(EmailAddress, self).__init__(**kwargs)
         if whitelist is not None:
             self.domain_whitelist = whitelist if isinstance(whitelist, frozenset) else frozenset(whitelist)
diff --git a/conformity/fields/temporal.py b/conformity/fields/temporal.py
index ccedbec..c798a28 100644
--- a/conformity/fields/temporal.py
+++ b/conformity/fields/temporal.py
@@ -13,6 +13,7 @@ from typing import (  # noqa: F401 TODO Python 3
     Type,
     Union,
 )
+import warnings
 
 import attr
 import six  # noqa: F401 TODO Python 3
@@ -161,3 +162,13 @@ class TZInfo(TemporalBase):
     valid_isinstance = datetime.tzinfo
     valid_noun = 'datetime.tzinfo'
     introspect_type = 'tzinfo'
+
+    def __attrs_post_init__(self):  # type: () -> None
+        if self.gt is not None or self.gte is not None or self.lt is not None or self.lte is not None:
+            warnings.warn(
+                'Arguments `gt`, `gte`, `lt`, and `lte` are deprecated in TZInfo and will be removed in '
+                'Conformity 2.0.',
+                DeprecationWarning,
+            )
+
+        super(TZInfo, self).__attrs_post_init__()
diff --git a/conformity/sphinx_ext/autodoc.py b/conformity/sphinx_ext/autodoc.py
index 0dfa679..86b2fb8 100644
--- a/conformity/sphinx_ext/autodoc.py
+++ b/conformity/sphinx_ext/autodoc.py
@@ -108,6 +108,11 @@ def _clean_literals(documentation: str) -> str:  # noqa: E999
         new_documentation.append(c)
         last = c
 
+    if len(new_documentation) > 3 and started and new_documentation[-1] == '`' and new_documentation[-2] != '`':
+        # Our code literal ended at the end of a line, so we didn't loop again to finish it off.
+        new_documentation.insert(position_to_insert_backtick + add_to_i, '`')
+        new_documentation.append('`')
+
     return ''.join(new_documentation)
 
 
diff --git a/docs/fields.rst b/docs/fields.rst
index 6072594..f6a75a6 100644
--- a/docs/fields.rst
+++ b/docs/fields.rst
@@ -298,14 +298,15 @@ In order to use ``CountryCodeField``, you must specify the ``country`` extras de
     # With Pipfile
     conformity = {version="*", extras=["country"]}
 
-There are two other fields that make use of `Currint`_ types if you specify the ``currint`` extras dependency:
+There are four other fields that make use of `Currint`_ types if you specify the ``currint`` extras dependency:
 
 - `currency.Amount <reference.html#conformity.fields.currency.Amount>`_: This field ensures that the value is an
   instance of ``currint.Amount``. It provides an optional ``valid_currencies`` argument which, by default, is the set
   of all ISO 4217 currency codes recognized by Currint. It also provides optional integer ``gt``, ``gte``, ``lt``, and
   ``lte`` boundary arguments that will be compared against the ``currint.Amount.value`` attribute.
-- `currency.AmountDictionary <reference.html#conformity.fields.currency.AmountDictionary>`_: A special extension of
-  ``Dictionary`` that enforces the standard JSON-compatible representation of a ``currint.Amount`` value:
+- `currency.AmountRequestDictionary <reference.html#conformity.fields.currency.AmountRequestDictionary>`_: A special
+  extension of ``Dictionary`` that enforces the standard JSON-compatible representation of a ``currint.Amount`` input
+  value, which must have a string ``'currency'`` key and an integer ``'value'`` key:
 
   .. code-block:: json
 
@@ -316,6 +317,22 @@ There are two other fields that make use of `Currint`_ types if you specify the
 
   This object, for example, represents USD 12.00. Like ``Amount``, it also has ``valid_currencies``, ``gt``, ``gte``,
   ``lt``, and ``lte`` optional arguments.
+- `currency.AmountResponseDictionary <reference.html#conformity.fields.currency.AmountResponseDictionary>`_: A special
+  extension of ``Dictionary`` that enforces the standard JSON-compatible representation of a ``currint.Amount``
+  response value, which must have a string ``'currency'`` key and an integer ``'value'`` key and may optionally have
+  string keys ``'major_value'`` and ``'display'``.
+
+  .. code-block:: json
+
+      {
+          "currency": "USD",
+          "value": 1200,
+          "major_value": "12.00",
+          "display": "12.00 USD",
+      }
+- `currency.AmountString <reference.html#conformity.fields.currency.AmountString>`_: A unicode string field (which does
+  not extend ``UnicodeString``) that enforces the value meet the currency format ``'CUR,1234'`` or ``'CUR:1234'``, and,
+  like ``Amount``, supports ``valid_currencies``, ``gt``, ``gte``, ``lt``, and ``lte`` optional arguments.
 
 
 Advanced Fields


Test patch:
diff --git a/tests/sphinx_ext/test_autodoc.py b/tests/sphinx_ext/test_autodoc.py
index 8e09f2a..95e0791 100644
--- a/tests/sphinx_ext/test_autodoc.py
+++ b/tests/sphinx_ext/test_autodoc.py
@@ -309,6 +309,9 @@ def test_autodoc_process_docstring_backticks():
         'This is the first line of `documentation` which should be ``modified`` but `only` if the ',
         'contents ```warrant``` modification. We especially do not `want` to mess ``with`` ',
         '`links to other titles`_ or `links to other pages <hello>`_ because that would be `bad`.',
+        "Conformity field that ensures that the value is a dictionary containing at least "
+        "fields `'currency'` and `'value'`",
+        "and optionally fields `'major_value'` and `'display'`. This field requires that Currint be installed.",
         'isort:skip_file',
     ]
 
@@ -318,6 +321,9 @@ def test_autodoc_process_docstring_backticks():
         'This is the first line of ``documentation`` which should be ``modified`` but ``only`` if the ',
         'contents ```warrant``` modification. We especially do not ``want`` to mess ``with`` ',
         '`links to other titles`_ or `links to other pages <hello>`_ because that would be ``bad``.',
+        "Conformity field that ensures that the value is a dictionary containing at least "
+        "fields ``'currency'`` and ``'value'``",
+        "and optionally fields ``'major_value'`` and ``'display'``. This field requires that Currint be installed.",
         '',
     ]
 
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 265676f..d75c5a7 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -11,6 +11,7 @@ from typing import (  # noqa: F401 TODO Python 3
     Tuple as TupleType,
 )
 import unittest
+import warnings
 
 import freezegun
 import pytest
@@ -777,3 +778,18 @@ class FieldTests(unittest.TestCase):
         assert schema.errors('foo') == [Error('Validation not implemented on base type')]
         with pytest.raises(NotImplementedError):
             schema.introspect()
+
+
+@pytest.mark.parametrize(('kwarg', ), (('gt', ), ('lt', ), ('gte', ), ('lte', )))
+def test_tzinfo_deprecated_arguments(kwarg):
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always', DeprecationWarning)
+
+        TZInfo(**{kwarg: pytz.timezone('America/Chicago')})  # type: ignore
+
+    assert w
+    assert len(w) == 1
+    assert issubclass(w[-1].category, DeprecationWarning)
+    assert (
+        'Arguments `gt`, `gte`, `lt`, and `lte` are deprecated in TZInfo and will be removed in Conformity 2.0.'
+    ) in str(w[-1].message)
diff --git a/tests/test_fields_currency.py b/tests/test_fields_currency.py
index 9cbce56..18ae324 100644
--- a/tests/test_fields_currency.py
+++ b/tests/test_fields_currency.py
@@ -4,12 +4,15 @@ from __future__ import (
 )
 
 import unittest
+import warnings
 
 from currint import (
     Amount,
     Currency,
+    currencies,
 )
 import pytest
+import six
 
 from conformity.error import (
     ERROR_CODE_INVALID,
@@ -18,7 +21,7 @@ from conformity.error import (
 from conformity.fields import currency as currency_fields
 
 
-class AmountFieldTests(unittest.TestCase):
+class TestAmount(unittest.TestCase):
     """
     Tests the Conformity Amount field
     """
@@ -91,10 +94,6 @@ class AmountFieldTests(unittest.TestCase):
             error.message,
             'Value not > 100',
         )
-        self.assertEqual(
-            error.pointer,
-            'value',
-        )
 
     def test_operator_greater_than_or_equal_to(self):  # type: () -> None
         field = currency_fields.Amount(gte=100)
@@ -112,10 +111,6 @@ class AmountFieldTests(unittest.TestCase):
             error.message,
             'Value not >= 101',
         )
-        self.assertEqual(
-            error.pointer,
-            'value',
-        )
 
     def test_operator_less_than(self):  # type: () -> None
         field = currency_fields.Amount(lt=101)
@@ -133,10 +128,6 @@ class AmountFieldTests(unittest.TestCase):
             error.message,
             'Value not < 100',
         )
-        self.assertEqual(
-            error.pointer,
-            'value',
-        )
 
     def test_operator_less_than_or_equal_to(self):  # type: () -> None
         field = currency_fields.Amount(lte=100)
@@ -154,10 +145,6 @@ class AmountFieldTests(unittest.TestCase):
             error.message,
             'Value not <= 99',
         )
-        self.assertEqual(
-            error.pointer,
-            'value',
-        )
 
     def test_introspect(self):  # type: () -> None
         self.field.valid_currencies = frozenset({'USD'})
@@ -171,7 +158,7 @@ class AmountFieldTests(unittest.TestCase):
         )
 
 
-class AmountDictionaryFieldTests(unittest.TestCase):
+class TestAmountRequestDictionaryField(unittest.TestCase):
     """
     Tests the AmountDictionary field
     """
@@ -181,7 +168,7 @@ class AmountDictionaryFieldTests(unittest.TestCase):
             'currency': 'USD',
             'value': 100,
         }
-        self.field = currency_fields.AmountDictionary(
+        self.field = currency_fields.AmountRequestDictionary(
             description='An amount',
             valid_currencies=['JPY', 'USD'],
         )
@@ -189,27 +176,43 @@ class AmountDictionaryFieldTests(unittest.TestCase):
     def test_constructor(self):  # type: () -> None
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(valid_currencies=1234)  # type: ignore
+            currency_fields.AmountRequestDictionary(valid_currencies=1234)  # type: ignore
 
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(valid_currencies=[1, 2, 3, 4])  # type: ignore
+            currency_fields.AmountRequestDictionary(valid_currencies=[1, 2, 3, 4])  # type: ignore
 
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(gt='not an int')  # type: ignore
+            currency_fields.AmountRequestDictionary(gt='not an int')  # type: ignore
 
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(gte='not an int')  # type: ignore
+            currency_fields.AmountRequestDictionary(gte='not an int')  # type: ignore
 
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(lt='not an int')  # type: ignore
+            currency_fields.AmountRequestDictionary(lt='not an int')  # type: ignore
 
         with pytest.raises(TypeError):
             # noinspection PyTypeChecker
-            currency_fields.AmountDictionary(lte='not an int')  # type: ignore
+            currency_fields.AmountRequestDictionary(lte='not an int')  # type: ignore
+
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always', DeprecationWarning)
+
+            currency_fields.AmountRequestDictionary(
+                description='An amount',
+                valid_currencies=['JPY', 'USD'],
+                allow_extra_keys=True,
+            )
+
+        assert w
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert (
+            '*args and **kwargs are deprecated in AmountRequestDictionary and will be removed in Conformity 2.0.'
+        ) in str(w[-1].message)
 
     def test_valid(self):  # type: () -> None
         self.assertEqual(self.field.errors(self.value), [])
@@ -229,10 +232,10 @@ class AmountDictionaryFieldTests(unittest.TestCase):
         )
 
     def test_operator_greater_than(self):  # type: () -> None
-        field = currency_fields.AmountDictionary(gt=99)
+        field = currency_fields.AmountRequestDictionary(gt=99)
         self.assertEqual(field.errors(self.value), [])
 
-        field = currency_fields.AmountDictionary(gt=100)
+        field = currency_fields.AmountRequestDictionary(gt=100)
         errors = field.errors(self.value)
         self.assertEqual(len(errors), 1)
         error = errors[0]
@@ -250,10 +253,10 @@ class AmountDictionaryFieldTests(unittest.TestCase):
         )
 
     def test_operator_greater_than_or_equal_to(self):  # type: () -> None
-        field = currency_fields.AmountDictionary(gte=100)
+        field = currency_fields.AmountRequestDictionary(gte=100)
         self.assertEqual(field.errors(self.value), [])
 
-        field = currency_fields.AmountDictionary(gte=101)
+        field = currency_fields.AmountRequestDictionary(gte=101)
         errors = field.errors(self.value)
         self.assertEqual(len(errors), 1)
         error = errors[0]
@@ -271,10 +274,10 @@ class AmountDictionaryFieldTests(unittest.TestCase):
         )
 
     def test_operator_less_than(self):  # type: () -> None
-        field = currency_fields.AmountDictionary(lt=101)
+        field = currency_fields.AmountRequestDictionary(lt=101)
         self.assertEqual(field.errors(self.value), [])
 
-        field = currency_fields.AmountDictionary(lt=100)
+        field = currency_fields.AmountRequestDictionary(lt=100)
         errors = field.errors(self.value)
         self.assertEqual(len(errors), 1)
         error = errors[0]
@@ -292,10 +295,10 @@ class AmountDictionaryFieldTests(unittest.TestCase):
         )
 
     def test_operator_less_than_or_equal_to(self):  # type: () -> None
-        field = currency_fields.AmountDictionary(lte=100)
+        field = currency_fields.AmountRequestDictionary(lte=100)
         self.assertEqual(field.errors(self.value), [])
 
-        field = currency_fields.AmountDictionary(lte=99)
+        field = currency_fields.AmountRequestDictionary(lte=99)
         errors = field.errors(self.value)
         self.assertEqual(len(errors), 1)
         error = errors[0]
@@ -311,3 +314,147 @@ class AmountDictionaryFieldTests(unittest.TestCase):
             error.pointer,
             'value',
         )
+
+
+class TestAmountDictionariesAndStrings(object):
+    def test_amount_response_dict(self):
+        field = currency_fields.AmountResponseDictionary(
+            description='This is a test, yo',
+            major_value_required=False,
+            display_required=False,
+        )
+
+        amount = Amount(currencies['USD'], 1839)
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+        }) == []
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'major_value': amount.currency.format_decimal(amount.value),
+            'display': six.text_type(amount),
+        }) == []
+
+        amount = Amount(currencies['JPY'], 8183728)
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+        }) == []
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'major_value': amount.currency.format_decimal(amount.value),
+            'display': six.text_type(amount),
+        }) == []
+
+        assert field.errors({})
+        assert field.errors({'value': amount.value})
+        assert field.errors({'currency': amount.currency.code})
+        assert field.errors({'value': amount.value, 'currency': amount.currency.code, 'extra': 'Not allowed'})
+
+        field = currency_fields.AmountResponseDictionary(description='Woof', display_required=False)
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'major_value': amount.currency.format_decimal(amount.value),
+        }) == []
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'display': six.text_type(amount),
+        })
+
+        field = currency_fields.AmountResponseDictionary(description='Woof', major_value_required=False)
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'display': six.text_type(amount),
+        }) == []
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'major_value': amount.currency.format_decimal(amount.value),
+        })
+
+        field = currency_fields.AmountResponseDictionary(description='Woof')
+
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'display': six.text_type(amount),
+        })
+        assert field.errors({
+            'value': amount.value,
+            'currency': amount.currency.code,
+            'major_value': amount.currency.format_decimal(amount.value),
+        })
+
+    def test_amount_string(self):
+        field = currency_fields.AmountString(description='Yup yup yup')
+
+        assert field.errors('USD,3819') == []
+        assert field.errors('USD:87173') == []
+        assert field.errors('JPY,6716') == []
+        assert field.errors('JPY:83613') == []
+
+        amount = Amount(currencies['USD'], 1839)
+        assert field.errors('{},{}'.format(amount.currency.code, amount.value)) == []
+
+        assert field.introspect() == {
+            'type': 'currency_amount_string',
+            'description': 'Yup yup yup',
+            'valid_currencies': '(all currencies)',
+        }
+
+        field = currency_fields.AmountString(
+            valid_currencies={'USD'},
+            gte=100,
+            lt=100000,
+            description='US only dude',
+        )
+
+        assert field.errors('USD,3819') == []
+        assert field.errors('USD:87173') == []
+        assert field.errors('USD:99')
+        assert field.errors('USD:100000')
+        assert field.errors('JPF,3819')
+
+        amount = Amount(currencies['JPY'], 1839)
+        assert field.errors('{},{}'.format(amount.currency.code, amount.value))
+
+        assert field.errors(b'USD,3819')
+        assert field.errors('USD.3819')
+        assert field.errors('USD,3819.15')
+
+        assert field.introspect() == {
+            'type': 'currency_amount_string',
+            'description': 'US only dude',
+            'valid_currencies': ['USD'],
+            'gte': 100,
+            'lt': 100000,
+        }
+
+    def test_deprecated_amount_dictionary_constructor_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always', DeprecationWarning)
+
+            # noinspection PyDeprecation
+            currency_fields.AmountDictionary(
+                description='An amount',
+                valid_currencies=['JPY', 'USD'],
+            )
+
+        assert w
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert (
+            'AmountDictionary is deprecated and will be removed in Conformity 2.0. '
+            'Use AmountRequestDictionary, instead.'
+        ) in str(w[-1].message)
diff --git a/tests/test_fields_email.py b/tests/test_fields_email.py
index 1d732c0..45f201d 100644
--- a/tests/test_fields_email.py
+++ b/tests/test_fields_email.py
@@ -5,6 +5,7 @@ from __future__ import (
 )
 
 import unittest
+import warnings
 
 import pytest
 
@@ -57,6 +58,32 @@ class EmailFieldTests(unittest.TestCase):
             'domain_whitelist': ['green.org'],
         }
 
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always', DeprecationWarning)
+
+            # noinspection PyTypeChecker
+            EmailAddress(message='hello')  # type: ignore
+
+        assert w
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert (
+            'Arguments `message` and `code` are deprecated in EmailAddress and will be removed in Conformity 2.0.'
+        ) in str(w[-1].message)
+
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always', DeprecationWarning)
+
+            # noinspection PyTypeChecker
+            EmailAddress(code='')  # type: ignore
+
+        assert w
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert (
+            'Arguments `message` and `code` are deprecated in EmailAddress and will be removed in Conformity 2.0.'
+        ) in str(w[-1].message)
+
     def test_not_unicode(self):  # type: () -> None
         schema = EmailAddress()
         assert schema.errors(self.valid_emails[0].encode('utf-8')) == [Error('Not a unicode string')]