Repository: hgrecco/pint

Issue:
Context.define for currency conversions
I'm trying to use pint to perform currency conversions.
Currency FX rates change by the minute and may be different in different markets/situations - which would be modelled perfectly by a pint.Context.

However, while Context allows defining custom conversion functions between different dimensions, it doesn't seem to allow customizing conversions within the same dimension.

I'm currently using throwaway UnitRegistry objects (very expensive to create):
```python
import pint

ureg_day1 = pint.UnitRegistry()
ureg_day1.define("USD = [currency]")
ureg_day1.define("EUR = 1.11 USD")
ureg_day1.define("GBP = 1.22 USD")

ureg_day2 = pint.UnitRegistry()
ureg_day2.define("USD = [currency]")
ureg_day2.define("EUR = 1.13 USD")
ureg_day2.define("GBP = 1.28 USD")

print((100 * ureg_day1("EUR")).to(ureg_day1("GBP")))
print((100 * ureg_day2("EUR")).to(ureg_day2("GBP")))
```
output:
```
90.98360655737706 GBP
88.28124999999999 GBP
```

I could not find a way to convert 100 EUR to USD on day1 and then convert them back to EUR on day2. This doesn't work:
```python
x = 100 * ureg_day1.EUR
x = x.to(ureg_day1.USD)
x = x.to(ureg_day2.EUR)
x
```
Output:
```
100.0 EUR  # Wrong - it should be 98.23 EUR!
```
Workaround:
```python
x = 100 * ureg_day1.EUR
x = x.to(ureg_day1.USD)
x = x.magnitude * ureg_day2.USD
x = x.to(ureg_day2.EUR)
x
```
Output:
```
98.23008849557525 EUR
```


It would make sense to have instead something like:
```python
import pint

ureg = pint.UnitRegistry()
ureg.define("USD = [currency]")
ureg.define("EUR = <TODO: valid currency, but crash if you attempt conversion>")
ureg.define("GBP = <TODO: valid currency, but crash if you attempt conversion>")

ctx_day1 = pint.Context("")
ctx_day1.define("EUR = 1.11 USD")
ctx_day1.define("GBP = 1.22 USD")

ctx_day2 = pint.Context("")
ctx_day2.define("EUR = 1.13 USD")
ctx_day2.define("GBP = 1.28 USD")

x = 100 * ureg.EUR
print(x.to(ureg.GBP, ctx_day1))
print(x.to(ureg.GBP, ctx_day2))

# x = x.to(ureg.USD)  # crash
x = x.to(ureg.USD, ctx_day1)
x = x.to(ureg.EUR, ctx_day2)
print(x)
```
Output:
```
90.98360655737706 GBP
88.28124999999999 GBP
98.23008849557525 EUR
```

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 95c26c8..b74f5bf 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,8 +4,19 @@ Pint Changelog
 0.10 (unreleased)
 -----------------
 
+- It is now possible to redefine units within a context, and use pint for currency
+  conversions. Read
+
+  - https://pint.readthedocs.io/en/latest/contexts.html
+  - https://pint.readthedocs.io/en/latest/currencies.html
+
+  (Issue #938, Thanks Guido Imperiale)
+- NaN (any capitalization) in a definitions file is now treated as a number
+  (Issue #938, Thanks Guido Imperiale)
 - Added slinch to Avoirdupois group
   (Issue #936, Thanks awcox21)
+- Fix bug where ureg.disable_contexts() would fail to fully disable throwaway contexts
+  (Issue #932, Thanks Guido Imperiale)
 - Use black, flake8, and isort on the project
   (Issues #929, #931, and #937, Thanks Guido Imperiale)
 - Auto-increase package version at every commit when pint is installed from the git tip,
@@ -40,8 +51,9 @@ Pint Changelog
   (Issue #915, Thanks Guido Imperiale)
 - TODO #913
 - TODO #911
-- Prevent 1500x slowdown when using .to() with a context
-  (Issues #909 and #923, Thanks Guido Imperiale)
+- Context activation and deactivation is now instantaneous; drastically reduced memory
+  footprint of a context (it used to be ~1.6MB per context; now it's a few bytes)
+  (Issues #909 / #923 / #938, Thanks Guido Imperiale)
 - **BREAKING CHANGE**:
   Drop support for Python < 3.6, numpy < 1.14, and uncertainties < 3.0;
   if you still need them, please install pint 0.9.
diff --git a/docs/contexts.rst b/docs/contexts.rst
index 4221fe7..27cb6ab 100644
--- a/docs/contexts.rst
+++ b/docs/contexts.rst
@@ -202,3 +202,126 @@ It is also possible to create anonymous contexts without invoking add_context:
    ...
    >>> ureg("1 s").to("km", c)
    299792.458 kilometer
+
+Using contexts for unit redefinition
+------------------------------------
+
+The exact definition of a unit of measure can change slightly depending on the country,
+year, and more in general convention. For example, the ISO board released over the years
+several revisions of its whitepapers, which subtly change the value of some of the more
+obscure units. And as soon as one steps out of the SI system and starts wandering into
+imperial and colonial measuring systems, the same unit may start being defined slightly
+differently every time - with no clear 'right' or 'wrong' definition.
+
+The default pint definitions file (default_en.txt) tries to mitigate the problem by
+offering multiple variants of the same unit by calling them with different names; for
+example, one will find multiple definitions of a "BTU"::
+
+    british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso
+    international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it
+    thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th
+
+That's sometimes insufficient, as Wikipedia reports `no less than 6 different
+definitions <https://en.wikipedia.org/wiki/British_thermal_unit>`_ for BTU, and it's
+entirely possible that some companies in the energy sector, or even individual energy
+contracts, may redefine it to something new entirely, e.g. with a different rounding.
+
+Pint allows changing the definition of a unit within the scope of a context.
+This allows layering; in the example above, a company may use the global definition
+of BTU from default_en.txt above, then override it with a customer-specific one in
+a context, and then override it again with a contract-specific one on top of it.
+
+A redefinition follows the following syntax::
+
+    <unit name> = <new definition>
+
+where <unit name> can be the base unit name or one of its aliases.
+For example::
+
+    BTU = 1055 J
+
+
+Programmatically:
+
+.. code-block:: python
+
+    >>> ureg = pint.UnitRegistry()
+    >>> q = ureg.Quantity("1 BTU")
+    >>> q.to("J")
+    1055.056 joule
+    >>> ctx = pint.Context()
+    >>> ctx.redefine("BTU = 1055 J")
+    >>> q.to("J", ctx)
+    1055.0 joule
+    # When the context is disabled, pint reverts to the base definition
+    >>> q.to("J")
+    1055.056 joule
+
+Or with a definitions file::
+
+    @context somecontract
+        BTU = 1055 J
+    @end
+
+.. code-block:: python
+
+    >>> ureg = pint.UnitRegistry()
+    >>> ureg.load_definitions("somefile.txt")
+    >>> q = ureg.Quantity("1 BTU")
+    >>> q.to("J")
+    1055.056 joule
+    >>> q.to("J", "somecontract")
+    1055.0 joule
+
+
+.. note::
+   Redefinitions are transitive; if the registry defines B as a function of A
+   and C as a function of B, redefining B will also impact the conversion from C to A.
+
+**Limitations**
+
+- You can't create brand new units ; all units must be defined outside of the context
+  first.
+- You can't change the dimensionality of a unit within a context. For example, you
+  can't define a context that redefines grams as a force instead of a mass (but see
+  the unit ``force_gram`` in default_en.txt).
+- You can't redefine a unit with a prefix; e.g. you can redefine a liter, but not a
+  decaliter.
+- You can't redefine a base unit, such as grams.
+- You can't add or remove aliases, or change the symbol. Symbol and aliases are
+  automatically inherited from the UnitRegistry.
+- You can't redefine dimensions or prefixes.
+
+Working without a default definition
+------------------------------------
+
+In some cases, the definition of a certain unit may be so volatile to make it unwise to
+define a default conversion rate in the UnitRegistry.
+
+This can be solved by using 'NaN' (any capitalization) instead of a conversion rate rate
+in the UnitRegistry, and then override it in contexts::
+
+    truckload = nan kg
+
+    @context Euro_TIR
+        truckload = 2000 kg
+    @end
+
+    @context British_grocer
+        truckload = 500 lb
+    @end
+
+This allows you, before any context is activated, to define quantities and perform
+dimensional analysis:
+
+.. code-block:: python
+
+    >>> ureg.truckload.dimensionality
+    [mass]
+    >>> q = ureg.Quantity("2 truckloads")
+    >>> q.to("kg")
+    nan kg
+    >>> q.to("kg", "Euro_TIR")
+    4000 kilogram
+    >>> q.to("kg", "British_grocer")
+    453.59237 kilogram
diff --git a/docs/currencies.rst b/docs/currencies.rst
new file mode 100644
index 0000000..26b66b5
--- /dev/null
+++ b/docs/currencies.rst
@@ -0,0 +1,86 @@
+.. _currencies:
+
+Using Pint for currency conversions
+===================================
+Currency conversion tends to be substantially more complex than physical units.
+The exact exchange rate between two currencies:
+
+- changes every minute,
+- changes depending on the place,
+- changes depending on who you are and who changes the money,
+- may be not reversible, e.g. EUR->USD may not be the same as 1/(USD->EUR),
+- different rates may apply to different amounts of money, e.g. in a BUY/SELL ledger,
+- frequently involves fees, whose calculation can be more or less sophisticated.
+  For example, a typical credit card contract may state that the bank will charge you a
+  fee on all purchases in foreign currency of 1 USD or 2%, whichever is higher, for all
+  amounts less than 1000 USD, and then 1.5% for anything in excess.
+
+You may implement currencies in two ways, both of which require you to be familiar
+with :ref:`contexts`.
+
+Simplified model
+----------------
+
+This model implies a few strong assumptions:
+
+- There are no conversion fees
+- All exchange rates are reversible
+- Any amount of money can be exchanged at the same rate
+- All exchanges can happen at the same time, between the same actors.
+
+In this simplified scenario, you can perform any round-trip across currencies
+and always come back with the original money; e.g.
+1 USD -> EUR -> JPY -> GBP -> USD will always give you 1 USD.
+
+In reality, these assumptions almost never happen but can be a reasonable approximation,
+for example in the case of large financial institutions, which can use interbank
+exchange rates and have nearly-limitless liquidity and sub-second trading systems.
+
+This can be implemented by putting all currencies on the same dimension, with a
+default conversion rate of NaN, and then setting the rate within contexts::
+
+    USD = [currency]
+    EUR = nan USD
+    JPY = nan USD
+    GBP = nan USD
+
+    @context FX
+        EUR = 1.11254 USD
+        GBP = 1.16956 EUR
+    @end
+
+Note how, in the example above:
+
+- USD is our *base currency*. It is arbitrary, only matters for the purpose
+  of invoking ``to_base_units()``, and can be changed with :ref:`systems`.
+- We did not set a value for JPY - maybe because the trader has no availability, or
+  because the data source was for some reason missing up-to-date data.
+  Any conversion involving JPY will return NaN.
+- We redefined GBP to be a function of EUR instead of USD. This is fine as long as there
+  is a path between two currencies.
+
+Full model
+----------
+
+If any of the assumptions of the simplified model fails, one can resort to putting each
+currency on its own dimension, and then implement transformations::
+
+    EUR = [currency_EUR]
+    GBP = [currency_GBP]
+
+    @context FX
+        GBP -> EUR: value * 1.11108 EUR/GBP
+        EUR -> GBP: value * 0.81227 GBP/EUR
+    @end
+
+.. code-block:: python
+
+    >>> q = ureg.Quantity("1 EUR")
+    >>> with ureg.context("FX"):
+    ... q = q.to("GBP").to("EUR")
+    >>> q
+    0.9024969516 EUR
+
+More sophisticated formulas, e.g. dealing with flat fees and thresholds, can be
+implemented with arbitrary python code by programmatically defining a context (see
+:ref:`contexts`).
diff --git a/docs/index.rst b/docs/index.rst
index bb76a72..b3245ab 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -127,6 +127,7 @@ User Guide
     defining
     performance
     systems
+    currencies
     pint-pandas.ipynb
 
 More information
diff --git a/pint/compat.py b/pint/compat.py
index 6f64acb..4ec3633 100644
--- a/pint/compat.py
+++ b/pint/compat.py
@@ -15,9 +15,8 @@ from numbers import Number
 
 def tokenizer(input_string):
     for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline):
-        if tokinfo.type == tokenize.ENCODING:
-            continue
-        yield tokinfo
+        if tokinfo.type != tokenize.ENCODING:
+            yield tokinfo
 
 
 # TODO: remove this warning after v0.10
diff --git a/pint/context.py b/pint/context.py
index db57336..dce298d 100644
--- a/pint/context.py
+++ b/pint/context.py
@@ -12,6 +12,7 @@ import re
 import weakref
 from collections import ChainMap, defaultdict
 
+from .definitions import Definition, UnitDefinition
 from .errors import DefinitionSyntaxError
 from .util import ParserHelper, SourceIterator, to_units_container
 
@@ -58,6 +59,13 @@ class Context:
         >>> c.transform(timedim, spacedim, 2)
         2
 
+    Additionally, a context may host redefinitions:
+
+        >>> c.redefine("pound = 0.5 kg")
+
+    A redefinition must be performed among units that already exist in the registry. It
+    cannot change the dimensionality of a unit. The symbol and aliases are automatically
+    inherited from the registry.
     """
 
     def __init__(self, name=None, aliases=(), defaults=None):
@@ -71,6 +79,12 @@ class Context:
         #: Maps defaults variable names to values
         self.defaults = defaults or {}
 
+        # Store Definition objects that are context-specific
+        self.redefinitions = []
+
+        # Flag set to True by the Registry the first time the context is enabled
+        self.checked = False
+
         #: Maps (src, dst) -> self
         #: Used as a convenience dictionary to be composed by ContextChain
         self.relation_to_context = weakref.WeakValueDictionary()
@@ -87,6 +101,7 @@ class Context:
             newdef = dict(context.defaults, **defaults)
             c = cls(context.name, context.aliases, newdef)
             c.funcs = context.funcs
+            c.redefinitions = context.redefinitions
             for edge in context.funcs:
                 c.relation_to_context[edge] = c
             return c
@@ -135,6 +150,10 @@ class Context:
 
         names = set()
         for lineno, line in lines:
+            if "=" in line:
+                ctx.redefine(line)
+                continue
+
             try:
                 rel, eq = line.split(":")
                 names.update(_varname_re.findall(eq))
@@ -195,6 +214,26 @@ class Context:
         _key = self.__keytransform__(src, dst)
         return self.funcs[_key](registry, value, **self.defaults)
 
+    def redefine(self, definition: str) -> None:
+        """Override the definition of a unit in the registry.
+
+        :param definition:
+            ``<unit> = <new definition>``, e.g. ``pound = 0.5 kg``
+        """
+        for line in definition.splitlines():
+            d = Definition.from_string(line)
+            if not isinstance(d, UnitDefinition):
+                raise DefinitionSyntaxError(
+                    "Expected <unit> = <converter>; got %s" % line.strip()
+                )
+            if d.symbol != d.name or d.aliases:
+                raise DefinitionSyntaxError(
+                    "Can't change a unit's symbol or aliases within a context"
+                )
+            if d.is_base:
+                raise DefinitionSyntaxError("Can't define base units within a context")
+            self.redefinitions.append(d)
+
     def hashable(self):
         """Generate a unique hashable and comparable representation of self, which can
         be used as a key in a dict. This class cannot define ``__hash__`` because it is
@@ -205,6 +244,7 @@ class Context:
             tuple(self.aliases),
             frozenset((k, id(v)) for k, v in self.funcs.items()),
             frozenset(self.defaults.items()),
+            tuple(self.redefinitions),
         )
 
 
@@ -215,8 +255,9 @@ class ContextChain(ChainMap):
 
     def __init__(self):
         super().__init__()
+        self.contexts = []
+        self.maps.clear()  # Remove default empty map
         self._graph = None
-        self._contexts = []
 
     def insert_contexts(self, *contexts):
         """Insert one or more contexts in reversed order the chained map.
@@ -225,15 +266,15 @@ class ContextChain(ChainMap):
         To facilitate the identification of the context with the matching rule,
         the *relation_to_context* dictionary of the context is used.
         """
-        self._contexts = list(reversed(contexts)) + self._contexts
+        self.contexts = list(reversed(contexts)) + self.contexts
         self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps
         self._graph = None
 
-    def remove_contexts(self, n):
+    def remove_contexts(self, n: int = None):
         """Remove the last n inserted contexts from the chain.
         """
-        self._contexts = self._contexts[n:]
-        self.maps = self.maps[n:]
+        del self.contexts[:n]
+        del self.maps[:n]
         self._graph = None
 
     @property
@@ -265,4 +306,4 @@ class ContextChain(ChainMap):
         be used as a key in a dict. This class cannot define ``__hash__`` because it is
         mutable, and the Python interpreter does cache the output of ``__hash__``.
         """
-        return tuple(ctx.hashable() for ctx in self._contexts)
+        return tuple(ctx.hashable() for ctx in self.contexts)
diff --git a/pint/definitions.py b/pint/definitions.py
index 0ca7ed4..4188124 100644
--- a/pint/definitions.py
+++ b/pint/definitions.py
@@ -9,6 +9,7 @@
 """
 
 from .converters import OffsetConverter, ScaleConverter
+from .errors import DefinitionSyntaxError
 from .util import ParserHelper, UnitsContainer, _is_dim
 
 
@@ -126,7 +127,7 @@ class UnitDefinition(Definition):
             elif all(_is_dim(key) for key in converter.keys()):
                 self.is_base = True
             else:
-                raise ValueError(
+                raise DefinitionSyntaxError(
                     "Cannot mix dimensions and units in the same definition. "
                     "Base units must be referenced only to dimensions. "
                     "Derived units must be referenced only to units."
@@ -154,7 +155,7 @@ class DimensionDefinition(Definition):
             elif all(_is_dim(key) for key in converter.keys()):
                 self.is_base = False
             else:
-                raise ValueError(
+                raise DefinitionSyntaxError(
                     "Base dimensions must be referenced to None. "
                     "Derived dimensions must only be referenced "
                     "to dimensions."
diff --git a/pint/registry.py b/pint/registry.py
index 444b363..dc85dfe 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -37,7 +37,7 @@ import itertools
 import math
 import os
 import re
-from collections import defaultdict
+from collections import ChainMap, defaultdict
 from contextlib import closing, contextmanager
 from decimal import Decimal
 from fractions import Fraction
@@ -108,6 +108,18 @@ class RegistryCache:
         self.parse_unit = {}
 
 
+class ContextCacheOverlay:
+    """Layer on top of the base UnitRegistry cache, specific to a combination of
+    active contexts which contain unit redefinitions.
+    """
+
+    def __init__(self, registry_cache: RegistryCache):
+        self.dimensional_equivalents = registry_cache.dimensional_equivalents
+        self.root_units = {}
+        self.dimensionality = registry_cache.dimensionality
+        self.parse_unit = registry_cache.parse_unit
+
+
 class BaseRegistry(metaclass=RegistryMeta):
     """Base class for all registries.
 
@@ -204,7 +216,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         self._prefixes = {"": PrefixDefinition("", "", (), 1)}
 
         #: Map suffix name (string) to canonical , and unit alias to canonical unit name
-        self._suffixes = {"": None, "s": ""}
+        self._suffixes = {"": "", "s": ""}
 
         #: Map contexts to RegistryCache
         self._cache = RegistryCache()
@@ -514,13 +526,12 @@ class BaseRegistry(metaclass=RegistryMeta):
             for unit_name in unit_names:
                 if "[" in unit_name:
                     continue
-                parsed_names = tuple(self.parse_unit_name(unit_name))
-                _prefix = None
+                parsed_names = self.parse_unit_name(unit_name)
                 if parsed_names:
-                    _prefix, base_name, _suffix = parsed_names[0]
+                    prefix, base_name, _ = parsed_names[0]
                 else:
-                    base_name = unit_name
-                prefixed = True if _prefix else False
+                    prefix, base_name = "", unit_name
+
                 try:
                     uc = ParserHelper.from_word(base_name)
 
@@ -530,34 +541,14 @@ class BaseRegistry(metaclass=RegistryMeta):
                     self._cache.root_units[uc] = bu
                     self._cache.dimensionality[uc] = di
 
-                    if not prefixed:
+                    if not prefix:
                         dimeq_set = self._cache.dimensional_equivalents.setdefault(
                             di, set()
                         )
                         dimeq_set.add(self._units[base_name]._name)
 
-                except Exception as e:
-                    logger.warning("Could not resolve {0}: {1!r}".format(unit_name, e))
-
-    def _dedup_candidates(self, candidates):
-        """Given a list of unit triplets (prefix, name, suffix),
-        remove those with different names but equal value.
-
-            e.g. ('kilo', 'gram', '') and ('', 'kilogram', '')
-        """
-        candidates = tuple(candidates)
-        if len(candidates) < 2:
-            return candidates
-
-        unique = [candidates[0]]
-        for c in candidates[2:]:
-            for u in unique:
-                if c == u:
-                    break
-            else:
-                unique.append(c)
-
-        return tuple(unique)
+                except Exception as exc:
+                    logger.warning(f"Could not resolve {unit_name}: {exc!r}")
 
     def get_name(self, name_or_alias, case_sensitive=True):
         """Return the canonical name of a unit.
@@ -571,9 +562,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         except KeyError:
             pass
 
-        candidates = self._dedup_candidates(
-            self.parse_unit_name(name_or_alias, case_sensitive)
-        )
+        candidates = self.parse_unit_name(name_or_alias, case_sensitive)
         if not candidates:
             raise UndefinedUnitError(name_or_alias)
         elif len(candidates) == 1:
@@ -599,7 +588,7 @@ class BaseRegistry(metaclass=RegistryMeta):
     def get_symbol(self, name_or_alias):
         """Return the preferred alias for a unit
         """
-        candidates = self._dedup_candidates(self.parse_unit_name(name_or_alias))
+        candidates = self.parse_unit_name(name_or_alias)
         if not candidates:
             raise UndefinedUnitError(name_or_alias)
         elif len(candidates) == 1:
@@ -726,13 +715,11 @@ class BaseRegistry(metaclass=RegistryMeta):
         if not input_units:
             return 1.0, UnitsContainer()
 
-        # The cache is only done for check_nonmult=True
         cache = self._cache.root_units
-        if check_nonmult:
-            try:
-                return cache[input_units]
-            except KeyError:
-                pass
+        try:
+            return cache[input_units]
+        except KeyError:
+            pass
 
         accumulators = [1.0, defaultdict(float)]
         self._get_root_units_recurse(input_units, 1.0, accumulators)
@@ -742,13 +729,10 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         # Check if any of the final units is non multiplicative and return None instead.
         if check_nonmult:
-            for unit in units:
-                if not self._units[unit].converter.is_multiplicative:
-                    return None, units
-
-        if check_nonmult:
-            cache[input_units] = factor, units
+            if any(not self._units[unit].converter.is_multiplicative for unit in units):
+                factor = None
 
+        cache[input_units] = factor, units
         return factor, units
 
     def get_base_units(self, input_units, check_nonmult=True, system=None):
@@ -768,7 +752,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         return self.get_root_units(input_units, check_nonmult)
 
     def _get_root_units_recurse(self, ref, exp, accumulators):
-        for key in sorted(ref):
+        for key in ref:
             exp2 = exp * ref[key]
             key = self.get_name(key)
             reg = self._units[key]
@@ -841,7 +825,7 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         # Here src and dst have only multiplicative units left. Thus we can
         # convert with a factor.
-        factor, units = self._get_root_units(src / dst)
+        factor, _ = self._get_root_units(src / dst)
 
         # factor is type float and if our magnitude is type Decimal then
         # must first convert to Decimal before we can '*' the values
@@ -860,8 +844,20 @@ class BaseRegistry(metaclass=RegistryMeta):
     def parse_unit_name(self, unit_name, case_sensitive=True):
         """Parse a unit to identify prefix, unit name and suffix
         by walking the list of prefix and suffix.
+        In case of equivalent combinations (e.g. ('kilo', 'gram', '') and
+        ('', 'kilogram', ''), prefer those with prefix.
+
+        :returns:
+            all non-equivalent combinations of (prefix, unit name, suffix)
+        :rtype:
+            ((str, str, str), ...)
+        """
+        return self._dedup_candidates(
+            self._parse_unit_name(unit_name, case_sensitive=case_sensitive)
+        )
 
-        :rtype: (str, str, str)
+    def _parse_unit_name(self, unit_name, case_sensitive=True):
+        """Helper of parse_unit_name
         """
         stw = unit_name.startswith
         edw = unit_name.endswith
@@ -887,6 +883,25 @@ class BaseRegistry(metaclass=RegistryMeta):
                             self._suffixes[suffix],
                         )
 
+    @staticmethod
+    def _dedup_candidates(candidates):
+        """Helper of parse_unit_name.
+
+        Given an iterable of unit triplets (prefix, name, suffix), remove those with
+        different names but equal value, preferring those with a prefix.
+
+        e.g. ('kilo', 'gram', '') and ('', 'kilogram', '')
+        """
+        candidates = dict.fromkeys(candidates)  # ordered set
+        for cp, cu, cs in list(candidates):
+            assert isinstance(cp, str)
+            assert isinstance(cu, str)
+            if cs != "":
+                raise NotImplementedError("non-empty suffix")
+            if cp:
+                candidates.pop(("", cp + cu, ""), None)
+        return tuple(candidates)
+
     def parse_units(self, input_string, as_delta=None):
         """Parse a units expression and returns a UnitContainer with
         the canonical names.
@@ -1052,10 +1067,11 @@ class NonMultiplicativeRegistry(BaseRegistry):
         # If the unit is not in the registry might be because it is not
         # registered with its prefixed version.
         # TODO: Might be better to register them.
-        candidates = self._dedup_candidates(self.parse_unit_name(u))
+        names = self.parse_unit_name(u)
+        assert len(names) == 1
+        _, base_name, _ = names[0]
         try:
-            u = candidates[0][1]
-            return self._units[u].is_multiplicative
+            return self._units[base_name].is_multiplicative
         except KeyError:
             raise UndefinedUnitError(u)
 
@@ -1165,15 +1181,20 @@ class ContextRegistry(BaseRegistry):
     """
 
     def __init__(self, **kwargs):
-        #: Map context name (string) or abbreviation to context.
+        # Map context name (string) or abbreviation to context.
         self._contexts = {}
-        #: Stores active contexts.
+        # Stores active contexts.
         self._active_ctx = ContextChain()
-        #: Map context chain to cache
+        # Map context chain to cache
         self._caches = {}
+        # Map context chain to units override
+        self._context_units = {}
 
         super().__init__(**kwargs)
 
+        # Allow contexts to add override layers to the units
+        self._units = ChainMap(self._units)
+
     def _register_parsers(self):
         super()._register_parsers()
         self._register_parser("@context", self._parse_context)
@@ -1186,7 +1207,7 @@ class ContextRegistry(BaseRegistry):
         except KeyError as e:
             raise DefinitionSyntaxError(f"unknown dimension {e} in context")
 
-    def add_context(self, context):
+    def add_context(self, context: Context) -> None:
         """Add a context object to the registry.
 
         The context will be accessible by its name and aliases.
@@ -1208,7 +1229,7 @@ class ContextRegistry(BaseRegistry):
                 )
             self._contexts[alias] = context
 
-    def remove_context(self, name_or_alias):
+    def remove_context(self, name_or_alias: str) -> Context:
         """Remove a context from the registry and return it.
 
         Notice that this methods will not disable the context. Use `disable_contexts`.
@@ -1221,15 +1242,91 @@ class ContextRegistry(BaseRegistry):
 
         return context
 
-    def _build_cache(self):
+    def _build_cache(self) -> None:
+        super()._build_cache()
+        self._caches[()] = self._cache
+
+    def _switch_context_cache_and_units(self) -> None:
+        """If any of the active contexts redefine units, create variant self._cache
+        and self._units specific to the combination of active contexts.
+        The next time this method is invoked with the same combination of contexts,
+        reuse the same variant self._cache and self._units as in the previous time.
+        """
+        del self._units.maps[:-1]
+        units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts)
+        if not units_overlay:
+            # Use the default _cache and _units
+            self._cache = self._caches[()]
+            return
+
         key = self._active_ctx.hashable()
         try:
             self._cache = self._caches[key]
+            self._units.maps.insert(0, self._context_units[key])
+        except KeyError:
+            pass
+
+        # First time using this specific combination of contexts and it contains
+        # unit redefinitions
+        base_cache = self._caches[()]
+        self._caches[key] = self._cache = ContextCacheOverlay(base_cache)
+
+        self._context_units[key] = units_overlay = {}
+        self._units.maps.insert(0, units_overlay)
+
+        on_redefinition_backup = self._on_redefinition
+        self._on_redefinition = "ignore"
+        try:
+            for ctx in self._active_ctx.contexts:
+                for definition in ctx.redefinitions:
+                    self._redefine(definition)
+        finally:
+            self._on_redefinition = on_redefinition_backup
+
+    def _redefine(self, definition: UnitDefinition) -> None:
+        """Redefine a unit from a context
+        """
+        # Find original definition in the UnitRegistry
+        candidates = self.parse_unit_name(definition.name)
+        if not candidates:
+            raise UndefinedUnitError(definition.name)
+        candidates_no_prefix = [c for c in candidates if not c[0]]
+        if not candidates_no_prefix:
+            raise ValueError(f"Can't redefine a unit with a prefix: {definition.name}")
+        assert len(candidates_no_prefix) == 1
+        _, name, _ = candidates_no_prefix[0]
+        try:
+            basedef = self._units[name]
         except KeyError:
-            super()._build_cache()
-            self._caches[key] = self._cache
+            raise UndefinedUnitError(name)
+
+        # Rebuild definition as a variant of the base
+        if basedef.is_base:
+            raise ValueError("Can't redefine a base unit to a derived one")
+
+        dims_old = self._get_dimensionality(basedef.reference)
+        dims_new = self._get_dimensionality(definition.reference)
+        if dims_old != dims_new:
+            raise ValueError(
+                f"Can't change dimensionality of {basedef.name} "
+                f"from {dims_old} to {dims_new} in a context"
+            )
+
+        # Do not modify in place the original definition, as (1) the context may
+        # be shared by other registries, and (2) it would alter the cache key
+        definition = UnitDefinition(
+            name=basedef.name,
+            symbol=basedef.symbol,
+            aliases=basedef.aliases,
+            is_base=False,
+            reference=definition.reference,
+            converter=definition.converter,
+        )
 
-    def enable_contexts(self, *names_or_contexts, **kwargs):
+        # Write into the context-specific self._units.maps[0] and self._cache.root_units
+        self.define(definition)
+
+    def enable_contexts(self, *names_or_contexts, **kwargs) -> None:
         """Enable contexts provided by name or by object.
 
         :param names_or_contexts: sequence of the contexts or contexts names/alias
@@ -1249,7 +1346,7 @@ class ContextRegistry(BaseRegistry):
         # Check if the contexts have been checked first, if not we make sure
         # that dimensions are expressed in terms of base dimensions.
         for ctx in ctxs:
-            if getattr(ctx, "_checked", False):
+            if ctx.checked:
                 continue
             for (src, dst), func in ctx.funcs.items():
                 src_ = self._get_dimensionality(src)
@@ -1257,22 +1354,20 @@ class ContextRegistry(BaseRegistry):
                 if src != src_ or dst != dst_:
                     ctx.remove_transformation(src, dst)
                     ctx.add_transformation(src_, dst_, func)
-            ctx._checked = True
+            ctx.checked = True
 
         # and create a new one with the new defaults.
         ctxs = tuple(Context.from_context(ctx, **kwargs) for ctx in ctxs)
 
         # Finally we add them to the active context.
         self._active_ctx.insert_contexts(*ctxs)
-        self._build_cache()
+        self._switch_context_cache_and_units()
 
-    def disable_contexts(self, n=None):
+    def disable_contexts(self, n: int = None) -> None:
         """Disable the last n enabled contexts.
         """
-        if n is None:
-            n = len(self._contexts)
         self._active_ctx.remove_contexts(n)
-        self._build_cache()
+        self._switch_context_cache_and_units()
 
     @contextmanager
     def context(self, *names, **kwargs):
@@ -1400,6 +1495,7 @@ class ContextRegistry(BaseRegistry):
         ret = super()._get_compatible_units(input_units, group_or_system)
 
         if self._active_ctx:
+            ret = ret.copy()  # Do not alter self._cache
             nodes = find_connected_nodes(self._active_ctx.graph, src_dim)
             if nodes:
                 for node in nodes:
diff --git a/pint/util.py b/pint/util.py
index 3b6c467..8d4f0e3 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -9,6 +9,7 @@
 """
 
 import logging
+import math
 import operator
 import re
 from collections.abc import Mapping
@@ -188,12 +189,13 @@ def pi_theorem(quantities, registry=None):
 def solve_dependencies(dependencies):
     """Solve a dependency graph.
 
-    :param dependencies: dependency dictionary. For each key, the value is
-                         an iterable indicating its dependencies.
-    :return: list of sets, each containing keys of independents tasks dependent
-                           only of the previous tasks in the list.
+    :param dependencies:
+        dependency dictionary. For each key, the value is an iterable indicating its
+        dependencies.
+    :return:
+        iterator of sets, each containing keys of independents tasks dependent only of
+        the previous tasks in the list.
     """
-    r = []
     while dependencies:
         # values not in keys (items without dep)
         t = {i for v in dependencies.values() for i in v} - dependencies.keys()
@@ -206,10 +208,9 @@ def solve_dependencies(dependencies):
                     ", ".join(repr(x) for x in dependencies.items())
                 )
             )
-        r.append(t)
         # and cleaned up
         dependencies = {k: v - t for k, v in dependencies.items() if v}
-    return r
+        yield t
 
 
 def find_shortest_path(graph, start, end, path=None):
@@ -442,10 +443,6 @@ class ParserHelper(UnitsContainer):
         """
         return cls(1, [(input_word, 1)])
 
-    @classmethod
-    def from_string(cls, input_string):
-        return cls._from_string(input_string)
-
     @classmethod
     def eval_token(cls, token, use_decimal=False):
         token_type = token.type
@@ -464,9 +461,8 @@ class ParserHelper(UnitsContainer):
 
     @classmethod
     @lru_cache()
-    def _from_string(cls, input_string):
+    def from_string(cls, input_string):
         """Parse linear expression mathematical units and return a quantity object.
-
         """
         if not input_string:
             return cls()
@@ -486,16 +482,21 @@ class ParserHelper(UnitsContainer):
         if isinstance(ret, Number):
             return ParserHelper(ret)
 
-        if not reps:
-            return ret
+        if reps:
+            ret = ParserHelper(
+                ret.scale,
+                {
+                    key.replace("__obra__", "[").replace("__cbra__", "]"): value
+                    for key, value in ret.items()
+                },
+            )
 
-        return ParserHelper(
-            ret.scale,
-            {
-                key.replace("__obra__", "[").replace("__cbra__", "]"): value
-                for key, value in ret.items()
-            },
-        )
+        for k in list(ret):
+            if k.lower() == "nan":
+                del ret._d[k]
+                ret.scale = math.nan
+
+        return ret
 
     def __copy__(self):
         new = super().__copy__()
@@ -618,7 +619,6 @@ _pretty_exp_re = re.compile(r"⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+(?:\.[⁰¹²³
 
 
 def string_preprocessor(input_string):
-
     input_string = input_string.replace(",", "")
     input_string = input_string.replace(" per ", "/")
 
@@ -722,11 +722,10 @@ def to_units_container(unit_like, registry=None):
 def infer_base_unit(q):
     """Return UnitsContainer of q with all prefixes stripped."""
     d = udict()
-    parse = q._REGISTRY.parse_unit_name
     for unit_name, power in q._units.items():
-        completely_parsed_unit = list(parse(unit_name))[-1]
-
-        _, base_unit, __ = completely_parsed_unit
+        candidates = q._REGISTRY.parse_unit_name(unit_name)
+        assert len(candidates) == 1
+        _, base_unit, _ = candidates[0]
         d[base_unit] += power
 
     # remove values that resulted in a power of 0


Test patch:
diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py
index 1b07275..bc575ed 100644
--- a/pint/testsuite/test_contexts.py
+++ b/pint/testsuite/test_contexts.py
@@ -1,7 +1,13 @@
 import itertools
+import math
 from collections import defaultdict
 
-from pint import DefinitionSyntaxError, DimensionalityError, UnitRegistry
+from pint import (
+    DefinitionSyntaxError,
+    DimensionalityError,
+    UndefinedUnitError,
+    UnitRegistry,
+)
 from pint.context import Context
 from pint.testsuite import QuantityTestCase
 from pint.util import UnitsContainer
@@ -486,13 +492,15 @@ class TestContexts(QuantityTestCase):
         self.assertEqual(len(ureg._contexts), nctx)
 
     def test_parse_invalid(self):
-        s = [
-            "@context longcontextname",
+        for badrow in (
             "[length] = 1 / [time]: c / value",
             "1 / [time] = [length]: c / value",
-        ]
-
-        self.assertRaises(DefinitionSyntaxError, Context.from_lines, s)
+            "[length] <- [time] = c / value",
+            "[length] - [time] = c / value",
+        ):
+            with self.subTest(badrow):
+                with self.assertRaises(DefinitionSyntaxError):
+                    Context.from_lines(["@context c", badrow])
 
     def test_parse_simple(self):
 
@@ -724,3 +732,222 @@ class TestDefinedContexts(QuantityTestCase):
 
         self.assertEqual(b, f(a))
         self.assertEqual(b, g(a))
+
+
+class TestContextRedefinitions(QuantityTestCase):
+    def test_redefine(self):
+        ureg = UnitRegistry(
+            """
+            foo = [d] = f = foo_alias
+            bar = 2 foo = b = bar_alias
+            baz = 3 bar = _ = baz_alias
+            asd = 4 baz
+
+            @context c
+                # Note how we're redefining a symbol, not the base name, as a
+                # function of another name
+                b = 5 f
+            """.splitlines()
+        )
+        # Units that are somehow directly or indirectly defined as a function of the
+        # overridden unit are also affected
+        foo = ureg.Quantity(1, "foo")
+        bar = ureg.Quantity(1, "bar")
+        asd = ureg.Quantity(1, "asd")
+
+        # Test without context before and after, to verify that the cache and units have
+        # not been polluted
+        for enable_ctx in (False, True, False):
+            with self.subTest(enable_ctx):
+                if enable_ctx:
+                    ureg.enable_contexts("c")
+                    k = 5
+                else:
+                    k = 2
+
+                self.assertEqual(foo.to("b").magnitude, 1 / k)
+                self.assertEqual(foo.to("bar").magnitude, 1 / k)
+                self.assertEqual(foo.to("bar_alias").magnitude, 1 / k)
+                self.assertEqual(foo.to("baz").magnitude, 1 / k / 3)
+                self.assertEqual(bar.to("foo").magnitude, k)
+                self.assertEqual(bar.to("baz").magnitude, 1 / 3)
+                self.assertEqual(asd.to("foo").magnitude, 4 * 3 * k)
+                self.assertEqual(asd.to("bar").magnitude, 4 * 3)
+                self.assertEqual(asd.to("baz").magnitude, 4)
+
+            ureg.disable_contexts()
+
+    def test_define_nan(self):
+        ureg = UnitRegistry(
+            """
+            USD = [currency]
+            EUR = nan USD
+            GBP = nan USD
+
+            @context c
+                EUR = 1.11 USD
+                # Note that we're changing which unit GBP is defined against
+                GBP = 1.18 EUR
+            @end
+            """.splitlines()
+        )
+
+        q = ureg.Quantity("10 GBP")
+        self.assertEquals(q.magnitude, 10)
+        self.assertEquals(q.units.dimensionality, {"[currency]": 1})
+        self.assertEquals(q.to("GBP").magnitude, 10)
+        self.assertTrue(math.isnan(q.to("USD").magnitude))
+        self.assertAlmostEqual(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11)
+
+    def test_non_multiplicative(self):
+        ureg = UnitRegistry(
+            """
+            kelvin = [temperature]
+            fahrenheit = 5 / 9 * kelvin; offset: 255
+            bogodegrees = 9 * kelvin
+
+            @context nonmult_to_nonmult
+                fahrenheit = 7 * kelvin; offset: 123
+            @end
+            @context nonmult_to_mult
+                fahrenheit = 123 * kelvin
+            @end
+            @context mult_to_nonmult
+                bogodegrees = 5 * kelvin; offset: 123
+            @end
+            """.splitlines()
+        )
+        k = ureg.Quantity(100, "kelvin")
+
+        with self.subTest("baseline"):
+            self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 255) * 9 / 5)
+            self.assertAlmostEqual(k.to("bogodegrees").magnitude, 100 / 9)
+
+        with self.subTest("nonmult_to_nonmult"):
+            with ureg.context("nonmult_to_nonmult"):
+                self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 123) / 7)
+
+        with self.subTest("nonmult_to_mult"):
+            with ureg.context("nonmult_to_mult"):
+                self.assertAlmostEqual(k.to("fahrenheit").magnitude, 100 / 123)
+
+        with self.subTest("mult_to_nonmult"):
+            with ureg.context("mult_to_nonmult"):
+                self.assertAlmostEqual(k.to("bogodegrees").magnitude, (100 - 123) / 5)
+
+    def test_err_to_base_unit(self):
+        with self.assertRaises(DefinitionSyntaxError) as e:
+            Context.from_lines(["@context c", "x = [d]"])
+        self.assertEquals(str(e.exception), "Can't define base units within a context")
+
+    def test_err_change_base_unit(self):
+        ureg = UnitRegistry(
+            """
+            foo = [d1]
+            bar = [d2]
+
+            @context c
+                bar = foo
+            @end
+            """.splitlines()
+        )
+
+        with self.assertRaises(ValueError) as e:
+            ureg.enable_contexts("c")
+        self.assertEquals(
+            str(e.exception), "Can't redefine a base unit to a derived one"
+        )
+
+    def test_err_change_dimensionality(self):
+        ureg = UnitRegistry(
+            """
+            foo = [d1]
+            bar = [d2]
+            baz = foo
+
+            @context c
+                baz = bar
+            @end
+            """.splitlines()
+        )
+        with self.assertRaises(ValueError) as e:
+            ureg.enable_contexts("c")
+        self.assertEquals(
+            str(e.exception),
+            "Can't change dimensionality of baz from [d1] to [d2] in a context",
+        )
+
+    def test_err_cyclic_dependency(self):
+        ureg = UnitRegistry(
+            """
+            foo = [d]
+            bar = foo
+            baz = bar
+
+            @context c
+                bar = baz
+            @end
+            """.splitlines()
+        )
+        # TODO align this exception and the one you get when you implement a cyclic
+        #      dependency within the base registry. Ideally this exception should be
+        #      raised by enable_contexts.
+        ureg.enable_contexts("c")
+        q = ureg.Quantity("bar")
+        with self.assertRaises(RecursionError):
+            q.to("foo")
+
+    def test_err_dimension_redefinition(self):
+        with self.assertRaises(DefinitionSyntaxError) as e:
+            Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
+        self.assertEquals(
+            str(e.exception), "Expected <unit> = <converter>; got [d1] = [d2] * [d3]"
+        )
+
+    def test_err_prefix_redefinition(self):
+        with self.assertRaises(DefinitionSyntaxError) as e:
+            Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
+        self.assertEquals(
+            str(e.exception), "Expected <unit> = <converter>; got [d1] = [d2] * [d3]"
+        )
+
+    def test_err_redefine_alias(self):
+        for s in ("foo = bar = f", "foo = bar = _ = baz"):
+            with self.subTest(s):
+                with self.assertRaises(DefinitionSyntaxError) as e:
+                    Context.from_lines(["@context c", s])
+                self.assertEquals(
+                    str(e.exception),
+                    "Can't change a unit's symbol or aliases within a context",
+                )
+
+    def test_err_redefine_with_prefix(self):
+        ureg = UnitRegistry(
+            """
+            kilo- = 1000
+            gram = [mass]
+            pound = 454 gram
+
+            @context c
+                kilopound = 500000 gram
+            @end
+            """.splitlines()
+        )
+        with self.assertRaises(ValueError) as e:
+            ureg.enable_contexts("c")
+        self.assertEquals(
+            str(e.exception), "Can't redefine a unit with a prefix: kilopound"
+        )
+
+    def test_err_new_unit(self):
+        ureg = UnitRegistry(
+            """
+            foo = [d]
+            @context c
+                bar = foo
+            @end
+            """.splitlines()
+        )
+        with self.assertRaises(UndefinedUnitError) as e:
+            ureg.enable_contexts("c")
+        self.assertEquals(str(e.exception), "'bar' is not defined in the unit registry")
diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py
index 421bdb8..e80622f 100644
--- a/pint/testsuite/test_definitions.py
+++ b/pint/testsuite/test_definitions.py
@@ -6,14 +6,17 @@ from pint.definitions import (
     PrefixDefinition,
     UnitDefinition,
 )
+from pint.errors import DefinitionSyntaxError
 from pint.testsuite import BaseTestCase
 from pint.util import UnitsContainer
 
 
 class TestDefinition(BaseTestCase):
     def test_invalid(self):
-        self.assertRaises(ValueError, Definition.from_string, "x = [time] * meter")
-        self.assertRaises(ValueError, Definition.from_string, "[x] = [time] * meter")
+        with self.assertRaises(DefinitionSyntaxError):
+            Definition.from_string("x = [time] * meter")
+        with self.assertRaises(DefinitionSyntaxError):
+            Definition.from_string("[x] = [time] * meter")
 
     def test_prefix_definition(self):
         for definition in ("m- = 1e-3", "m- = 10**-3", "m- = 0.001"):
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 145d0cc..0ff532b 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -3,7 +3,7 @@ import math
 import pprint
 import unittest
 
-from pint import DimensionalityError, UnitRegistry
+from pint import Context, DimensionalityError, UnitRegistry
 from pint.compat import np
 from pint.testsuite import QuantityTestCase, helpers
 from pint.unit import UnitsContainer
@@ -679,3 +679,14 @@ class TestIssues(QuantityTestCase):
         meter_units = ureg.get_compatible_units(ureg.meter)
         hertz_units = ureg.get_compatible_units(ureg.hertz)
         pprint.pformat(meter_units | hertz_units)
+
+    def test_issue932(self):
+        ureg = UnitRegistry()
+        q = ureg.Quantity("1 kg")
+        with self.assertRaises(DimensionalityError):
+            q.to("joule")
+        ureg.enable_contexts("energy", *(Context() for _ in range(20)))
+        q.to("joule")
+        ureg.disable_contexts()
+        with self.assertRaises(DimensionalityError):
+            q.to("joule")
diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py
index d27a4eb..52c7c82 100644
--- a/pint/testsuite/test_util.py
+++ b/pint/testsuite/test_util.py
@@ -1,5 +1,6 @@
 import collections
 import copy
+import math
 import operator as op
 from decimal import Decimal
 
@@ -203,6 +204,13 @@ class TestParseHelper(BaseTestCase):
         # integer numbers are represented as ints, not Decimals
         self._test_eval_token(1000, "1000", use_decimal=True)
 
+    def test_nan(self):
+        for s in ("nan", "NAN", "NaN", "123 NaN nan NAN 456"):
+            with self.subTest(s):
+                p = ParserHelper.from_string(s + " kg")
+                assert math.isnan(p.scale)
+                self.assertEqual(dict(p), {"kg": 1})
+
 
 class TestStringProcessor(BaseTestCase):
     def _test(self, bef, aft):