From a7b56b9d6f2503fd6c4aacfd43faf129cbc371da Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 16 Aug 2020 10:50:29 -0500 Subject: [PATCH] Add case_sensitive regsitry setting and modify case_sensitive kwarg to default to None to use regsitry setting --- CHANGES | 2 ++ pint/registry.py | 55 ++++++++++++++++++++++++------------- pint/testsuite/__init__.py | 8 ++++++ pint/testsuite/test_unit.py | 44 ++++++++++++++++++++++++++++- 4 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CHANGES b/CHANGES index 13947daea..076b3fc6d 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 a05937671..2686ee9ed 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 @@ def __init__( preprocessors=None, fmt_locale=None, non_int_type=float, + case_sensitive=True, ): self._register_parsers() self._init_dynamic_classes() @@ -208,6 +211,9 @@ def __init__( #: 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 @@ def _build_cache(self): 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 @@ def get_name(self, name_or_alias, case_sensitive=True): 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 @@ def get_name(self, name_or_alias, case_sensitive=True): 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 @@ def _convert(self, value, src, dst, inplace=False, check_dimensionality=True): 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 @@ def parse_unit_name(self, unit_name, case_sensitive=True): ---------- 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 @@ def parse_unit_name(self, unit_name, case_sensitive=True): 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 @@ def _dedup_candidates(candidates): 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 @@ def parse_units(self, input_string, as_delta=None): 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 @@ def parse_units(self, input_string, as_delta=None): """ 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 @@ def _parse_units(self, input_string, as_delta=True): 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 @@ def _parse_units(self, input_string, as_delta=True): 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 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): 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 @@ def parse_pattern( 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 @@ def parse_pattern( 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 @@ def parse_expression( input_string : case_sensitive : - (Default value = True) + (Default value = None, which uses registry setting) use_decimal : (Default value = False) **values : @@ -1289,13 +1302,13 @@ def __init__( # 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 @@ def __init__( preprocessors=None, fmt_locale=None, non_int_type=float, + case_sensitive=True, ): super().__init__( @@ -2128,6 +2144,7 @@ def __init__( preprocessors=preprocessors, fmt_locale=fmt_locale, non_int_type=non_int_type, + case_sensitive=case_sensitive, ) def pi_theorem(self, quantities): diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 1fbc233d4..687722134 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -130,6 +130,14 @@ def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None) 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 749a60f5b..dcb3e97a1 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -11,7 +11,7 @@ ) 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 @@ def test_parse_pattern_many_results_two_units(self): ], ) + 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