Repository: hgrecco/pint

Issue:
Add case (in)senstive option to registry and extend to Unit and Quantity parsing
[In our quest to handle CF compliant/UDUNITS style units in MetPy](https://github.com/Unidata/MetPy/issues/1362), we have come across a couple issues regarding case sensitivity. For example, we need to be able to parse `Meter` as `meter` and [`Celcius` as an alias of `degrees_Celcius`](https://github.com/hgrecco/pint/issues/1081). To enable this within Pint (as an option of course and not the default [to maintain standards](https://github.com/hgrecco/pint/issues/1081#issuecomment-615238512)), I would like to propose the following to extend the case insensitive handling already existing in Pint:

1) Add a `case_sensitive` option to `BaseRegistry` which defaults to `True`
2) Modify the existing `case_sensitive` options on `get_name`, `parse_unit_name`, `parse_pattern`, and `parse_expression` to default to `None` instead of `True`, and have `None` be replaced within the methods with the registry's `case_sensitive` option
3) Add analogous `case_sensitive` kwarg to `parse_units`
4) Optional: add analogous `case_sensitive` kwarg to any other public registry method that ends up using `parse_unit_name` (or something that uses it, recursively) so that it can be appropriately passed along.

This would allow something like the following to become valid, so that a greater extent of CF unit compliance can be achieved:

```python
import pint

ureg = pint.UnitRegistry(case_sensitive=False)

q = ureg("1 Meter")

print(q)
```

```
1 meter
```

@hgrecco Is this compatible enough with your philosophy for Pint in https://github.com/hgrecco/pint/issues/1081#issuecomment-618681303 and seem like a reasonable enough way forward in implementation?

Discussion:
hgrecco: This looks good. Just one question. Why you decided to put the 
```python
case_sensitive = (
            self.case_sensitive if case_sensitive is None else case_sensitive
        )
```

in `parse_unit_name` and not in `_parse_unit_name`?
jthielen: > Why you decided to put the `...` in `parse_unit_name` and not in `_parse_unit_name`?

I believe that was because I started with it at the highest level (in each public function with the `case_sensitive` kwarg), and then realized that they all ended up being routed through `parse_unit_name`, so that was the only one needed. Indeed, I can move it to `_parse_unit_name` just as well. Let me know if you'd want me to move it there or not.
hgrecco: I think it makes sense to put it in `_parse_unit_name` so that any future method that uses internal functions can rely on that and avoid code duplication.

Patch:
diff --git a/CHANGES b/CHANGES
index 13947da..076b3fc 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,8 @@ Pint Changelog
 - Change `Quantity` and `Unit` HTML (i.e., Jupyter notebook) repr away from LaTeX to a
   simpler, more performant pretty-text and table based repr inspired by Sparse and Dask.
   (Issue #654)
+- Add `case_sensitive` option to registry for case (in)sensitive handling when parsing
+  units (Issue #1145)
 - Implement Dask collection interface to support Pint Quantity wrapped Dask arrays.
 - Started automatically testing examples in the documentation
 - Fixed right operand power for dimensionless Quantity to reflect numpy behavior. (Issue #1136)
diff --git a/pint/registry.py b/pint/registry.py
index a059376..2686ee9 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -167,6 +167,8 @@ class BaseRegistry(metaclass=RegistryMeta):
         locale identifier string, used in `format_babel`
     non_int_type : type
         numerical type used for non integer values. (Default: float)
+    case_sensitive : bool, optional
+        Control default case sensitivity of unit parsing. (Default: True)
 
     """
 
@@ -187,6 +189,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         preprocessors=None,
         fmt_locale=None,
         non_int_type=float,
+        case_sensitive=True,
     ):
         self._register_parsers()
         self._init_dynamic_classes()
@@ -208,6 +211,9 @@ class BaseRegistry(metaclass=RegistryMeta):
         #: Numerical type used for non integer values.
         self.non_int_type = non_int_type
 
+        #: Default unit case sensitivity
+        self.case_sensitive = case_sensitive
+
         #: Map between name (string) and value (string) of defaults stored in the
         #: definitions file.
         self._defaults = {}
@@ -619,7 +625,7 @@ class BaseRegistry(metaclass=RegistryMeta):
                 except Exception as exc:
                     logger.warning(f"Could not resolve {unit_name}: {exc!r}")
 
-    def get_name(self, name_or_alias, case_sensitive=True):
+    def get_name(self, name_or_alias, case_sensitive=None):
         """Return the canonical name of a unit.
         """
 
@@ -645,7 +651,7 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         if prefix:
             name = prefix + unit_name
-            symbol = self.get_symbol(name)
+            symbol = self.get_symbol(name, case_sensitive)
             prefix_def = self._prefixes[prefix]
             self._units[name] = UnitDefinition(
                 name,
@@ -658,10 +664,10 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         return unit_name
 
-    def get_symbol(self, name_or_alias):
+    def get_symbol(self, name_or_alias, case_sensitive=None):
         """Return the preferred alias for a unit.
         """
-        candidates = self.parse_unit_name(name_or_alias)
+        candidates = self.parse_unit_name(name_or_alias, case_sensitive)
         if not candidates:
             raise UndefinedUnitError(name_or_alias)
         elif len(candidates) == 1:
@@ -994,7 +1000,7 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         return value
 
-    def parse_unit_name(self, unit_name, case_sensitive=True):
+    def parse_unit_name(self, unit_name, case_sensitive=None):
         """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
@@ -1004,8 +1010,9 @@ class BaseRegistry(metaclass=RegistryMeta):
         ----------
         unit_name :
 
-        case_sensitive :
-             (Default value = True)
+        case_sensitive : bool or None
+            Control if unit lookup is case sensitive. Defaults to None, which uses the
+            registry's case_sensitive setting
 
         Returns
         -------
@@ -1016,9 +1023,12 @@ class BaseRegistry(metaclass=RegistryMeta):
             self._parse_unit_name(unit_name, case_sensitive=case_sensitive)
         )
 
-    def _parse_unit_name(self, unit_name, case_sensitive=True):
+    def _parse_unit_name(self, unit_name, case_sensitive=None):
         """Helper of parse_unit_name.
         """
+        case_sensitive = (
+            self.case_sensitive if case_sensitive is None else case_sensitive
+        )
         stw = unit_name.startswith
         edw = unit_name.endswith
         for suffix, prefix in itertools.product(self._suffixes, self._prefixes):
@@ -1062,7 +1072,7 @@ class BaseRegistry(metaclass=RegistryMeta):
                 candidates.pop(("", cp + cu, ""), None)
         return tuple(candidates)
 
-    def parse_units(self, input_string, as_delta=None):
+    def parse_units(self, input_string, as_delta=None, case_sensitive=None):
         """Parse a units expression and returns a UnitContainer with
         the canonical names.
 
@@ -1074,6 +1084,9 @@ class BaseRegistry(metaclass=RegistryMeta):
         as_delta : bool or None
             if the expression has multiple units, the parser will
             interpret non multiplicative units as their `delta_` counterparts. (Default value = None)
+        case_sensitive : bool or None
+            Control if unit parsing is case sensitive. Defaults to None, which uses the
+            registry's setting.
 
         Returns
         -------
@@ -1081,10 +1094,10 @@ class BaseRegistry(metaclass=RegistryMeta):
         """
         for p in self.preprocessors:
             input_string = p(input_string)
-        units = self._parse_units(input_string, as_delta)
+        units = self._parse_units(input_string, as_delta, case_sensitive)
         return self.Unit(units)
 
-    def _parse_units(self, input_string, as_delta=True):
+    def _parse_units(self, input_string, as_delta=True, case_sensitive=None):
         """Parse a units expression and returns a UnitContainer with
         the canonical names.
         """
@@ -1109,7 +1122,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         ret = {}
         many = len(units) > 1
         for name in units:
-            cname = self.get_name(name)
+            cname = self.get_name(name, case_sensitive=case_sensitive)
             value = units[name]
             if not cname:
                 continue
@@ -1126,7 +1139,7 @@ class BaseRegistry(metaclass=RegistryMeta):
 
         return ret
 
-    def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values):
+    def _eval_token(self, token, case_sensitive=None, use_decimal=False, **values):
 
         # TODO: remove this code when use_decimal is deprecated
         if use_decimal:
@@ -1156,7 +1169,7 @@ class BaseRegistry(metaclass=RegistryMeta):
             raise Exception("unknown token type")
 
     def parse_pattern(
-        self, input_string, pattern, case_sensitive=True, use_decimal=False, many=False
+        self, input_string, pattern, case_sensitive=None, use_decimal=False, many=False
     ):
         """Parse a string with a given regex pattern and returns result.
 
@@ -1167,7 +1180,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         pattern_string:
              The regex parse string
         case_sensitive :
-             (Default value = True)
+             (Default value = None, which uses registry setting)
         use_decimal :
              (Default value = False)
         many :
@@ -1212,7 +1225,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         return results
 
     def parse_expression(
-        self, input_string, case_sensitive=True, use_decimal=False, **values
+        self, input_string, case_sensitive=None, use_decimal=False, **values
     ):
         """Parse a mathematical expression including units and return a quantity object.
 
@@ -1224,7 +1237,7 @@ class BaseRegistry(metaclass=RegistryMeta):
         input_string :
 
         case_sensitive :
-             (Default value = True)
+             (Default value = None, which uses registry setting)
         use_decimal :
              (Default value = False)
         **values :
@@ -1289,13 +1302,13 @@ class NonMultiplicativeRegistry(BaseRegistry):
         # base units on multiplication and division.
         self.autoconvert_offset_to_baseunit = autoconvert_offset_to_baseunit
 
-    def _parse_units(self, input_string, as_delta=None):
+    def _parse_units(self, input_string, as_delta=None, case_sensitive=None):
         """
         """
         if as_delta is None:
             as_delta = self.default_as_delta
 
-        return super()._parse_units(input_string, as_delta)
+        return super()._parse_units(input_string, as_delta, case_sensitive)
 
     def _define(self, definition):
         """Add unit to the registry.
@@ -2099,6 +2112,8 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry):
         or unit string
     fmt_locale :
         locale identifier string, used in `format_babel`. Default to None
+    case_sensitive : bool, optional
+        Control default case sensitivity of unit parsing. (Default: True)
     """
 
     def __init__(
@@ -2114,6 +2129,7 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry):
         preprocessors=None,
         fmt_locale=None,
         non_int_type=float,
+        case_sensitive=True,
     ):
 
         super().__init__(
@@ -2128,6 +2144,7 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry):
             preprocessors=preprocessors,
             fmt_locale=fmt_locale,
             non_int_type=non_int_type,
+            case_sensitive=case_sensitive,
         )
 
     def pi_theorem(self, quantities):


Test patch:
diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py
index 1fbc233..6877221 100644
--- a/pint/testsuite/__init__.py
+++ b/pint/testsuite/__init__.py
@@ -130,6 +130,14 @@ class QuantityTestCase(BaseTestCase):
             self.assertLessEqual(abs(m1 - m2), atol + rtol * abs(m2), msg=msg)
 
 
+class CaseInsensitveQuantityTestCase(QuantityTestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.ureg = UnitRegistry(case_sensitive=False)
+        cls.Q_ = cls.ureg.Quantity
+        cls.U_ = cls.ureg.Unit
+
+
 def testsuite():
     """A testsuite that has all the pint tests."""
     suite = unittest.TestLoader().discover(os.path.dirname(__file__))
diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py
index 749a60f..dcb3e97 100644
--- a/pint/testsuite/test_unit.py
+++ b/pint/testsuite/test_unit.py
@@ -11,7 +11,7 @@ from pint import (
 )
 from pint.compat import np
 from pint.registry import LazyRegistry, UnitRegistry
-from pint.testsuite import QuantityTestCase, helpers
+from pint.testsuite import CaseInsensitveQuantityTestCase, QuantityTestCase, helpers
 from pint.testsuite.parameterized import ParameterizedTestCase
 from pint.util import ParserHelper, UnitsContainer
 
@@ -704,6 +704,48 @@ class TestRegistry(QuantityTestCase):
             ],
         )
 
+    def test_case_sensitivity(self):
+        ureg = self.ureg
+        # Default
+        self.assertRaises(UndefinedUnitError, ureg.parse_units, "Meter")
+        self.assertRaises(UndefinedUnitError, ureg.parse_units, "j")
+        # Force True
+        self.assertRaises(
+            UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True
+        )
+        self.assertRaises(
+            UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True
+        )
+        # Force False
+        self.assertEqual(
+            ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1)
+        )
+        self.assertEqual(
+            ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1)
+        )
+
+
+class TestCaseInsensitiveRegistry(CaseInsensitveQuantityTestCase):
+    def test_case_sensitivity(self):
+        ureg = self.ureg
+        # Default
+        self.assertEqual(ureg.parse_units("Meter"), UnitsContainer(meter=1))
+        self.assertEqual(ureg.parse_units("j"), UnitsContainer(joule=1))
+        # Force True
+        self.assertRaises(
+            UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True
+        )
+        self.assertRaises(
+            UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True
+        )
+        # Force False
+        self.assertEqual(
+            ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1)
+        )
+        self.assertEqual(
+            ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1)
+        )
+
 
 class TestCompatibleUnits(QuantityTestCase):
     FORCE_NDARRAY = False