diff options
-rw-r--r-- | AUTHORS | 2 | ||||
-rw-r--r-- | pint/context.py | 20 | ||||
-rw-r--r-- | pint/formatting.py | 2 | ||||
-rw-r--r-- | pint/registry.py | 124 | ||||
-rw-r--r-- | pint/systems.py | 4 | ||||
-rw-r--r-- | pint/testsuite/helpers.py | 12 | ||||
-rw-r--r-- | pint/testsuite/test_measurement.py | 6 | ||||
-rw-r--r-- | pint/testsuite/test_unit.py | 24 |
8 files changed, 132 insertions, 62 deletions
@@ -10,6 +10,7 @@ Other contributors, listed alphabetically, are: * Brend Wanders <b.wanders@utwente.nl> * choloepus * coutinho <coutinho@esrf.fr> +* Clément Pit-Claudel <clement.pitclaudel@live.com> * Daniel Sokolowski <daniel.sokolowski@danols.com> * Dave Brooks <dave@bcs.co.nz> * David Linke @@ -19,6 +20,7 @@ Other contributors, listed alphabetically, are: * Felix Hummel <felix@felixhummel.de> * Francisco Couzo <franciscouzo@gmail.com> * Giel van Schijndel <me@mortis.eu> +* Guido Imperiale <crusaderky@gmail.com> * Ignacio Fdez. Galván <jellby@yahoo.com> * James Rowe <jnrowe@gmail.com> * Jim Turner <jturner314@gmail.com> diff --git a/pint/context.py b/pint/context.py index 071e89e..78e763f 100644 --- a/pint/context.py +++ b/pint/context.py @@ -18,10 +18,10 @@ from .util import (ParserHelper, UnitsContainer, from .errors import DefinitionSyntaxError #: Regex to match the header parts of a context. -_header_re = re.compile('@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*') +_header_re = re.compile(r'@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*') #: Regex to match variable names in an equation. -_varname_re = re.compile('[A-Za-z_][A-Za-z0-9_]*') +_varname_re = re.compile(r'[A-Za-z_][A-Za-z0-9_]*') def _expression_to_function(eq): @@ -85,7 +85,7 @@ class Context: newdef = dict(context.defaults, **defaults) c = cls(context.name, context.aliases, newdef) c.funcs = context.funcs - for edge in context.funcs.keys(): + for edge in context.funcs: c.relation_to_context[edge] = c return c return context @@ -194,8 +194,8 @@ class ContextChain(ChainMap): to transform from one dimension to another. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self): + super().__init__() self._graph = None self._contexts = [] @@ -220,7 +220,7 @@ class ContextChain(ChainMap): @property def defaults(self): if self: - return list(self.maps[0].values())[0].defaults + return next(iter(self.maps[0].values())).defaults return {} @property @@ -240,3 +240,11 @@ class ContextChain(ChainMap): :raises: KeyError if the rule is not found. """ return self[(src, dst)].transform(src, dst, registry, value) + + def context_ids(self): + """Hashable unique identifier of the current contents of the context chain. This + is not implemented as ``__hash__`` as doing so on a mutable object can provoke + unpredictable behaviour, as interpreter-level optimizations can cache the output + of ``__hash__``. + """ + return tuple(id(ctx) for ctx in self._contexts) diff --git a/pint/formatting.py b/pint/formatting.py index 24a44d5..cd23de8 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -14,7 +14,7 @@ import re from .babel_names import _babel_units, _babel_lengths from .compat import Loc -__JOIN_REG_EXP = re.compile("\{\d*\}") +__JOIN_REG_EXP = re.compile(r"\{\d*\}") def _join(fmt, iterable): diff --git a/pint/registry.py b/pint/registry.py index c79f78b..80454d1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -78,6 +78,24 @@ class RegistryMeta(type): return obj +class RegistryCache: + """Cache to speed up unit registries + """ + + #: Maps dimensionality (UnitsContainer) to Units (str) + def __init__(self): + + self.dimensional_equivalents = {} + #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) + self.root_units = {} + + #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) + self.dimensionality = {} + + #: Cache the unit name associated to user input. ('mV' -> 'millivolt') + self.parse_unit = {} + + class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. @@ -99,6 +117,8 @@ class BaseRegistry(metaclass=RegistryMeta): 'warn', 'raise', 'ignore' :type on_redefinition: str :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. + :param preprocessors: list of callables which are iteratively ran on any input expression + or unit string """ #: Map context prefix to function @@ -114,13 +134,15 @@ class BaseRegistry(metaclass=RegistryMeta): 'parse_unit_name', 'parse_units', 'parse_expression', 'convert'] - def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', auto_reduce_dimensions=False): + def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', + auto_reduce_dimensions=False, preprocessors=None): self._register_parsers() self._init_dynamic_classes() self._filename = filename self.force_ndarray = force_ndarray + self.preprocessors = preprocessors or [] #: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore' self._on_redefinition = on_redefinition @@ -149,17 +171,8 @@ class BaseRegistry(metaclass=RegistryMeta): #: Map suffix name (string) to canonical , and unit alias to canonical unit name self._suffixes = {'': None, 's': ''} - #: Maps dimensionality (UnitsContainer) to Units (str) - self._dimensional_equivalents = dict() - - #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) - self._root_units_cache = dict() - - #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) - self._dimensionality_cache = dict() - - #: Cache the unit name associated to user input. ('mV' -> 'millivolt') - self._parse_unit_cache = dict() + #: Map contexts to RegistryCache + self._cache = RegistryCache() self._initialized = False @@ -337,7 +350,7 @@ class BaseRegistry(metaclass=RegistryMeta): if self._on_redefinition == 'raise': raise RedefinitionError(key, type(value)) elif self._on_redefinition == 'warn': - logger.warning("Redefining '%s' (%s)", key, type(value)) + logger.warning("Redefining '%s' (%s)" % (key, type(value))) unit_dict[key] = value if casei_unit_dict is not None: @@ -430,7 +443,7 @@ class BaseRegistry(metaclass=RegistryMeta): def _build_cache(self): """Build a cache of dimensionality and base units. """ - self._dimensional_equivalents = dict() + self._cache = RegistryCache() deps = { name: definition.reference.keys() if definition.reference else set() @@ -454,14 +467,14 @@ class BaseRegistry(metaclass=RegistryMeta): bu = self._get_root_units(uc) di = self._get_dimensionality(uc) - self._root_units_cache[uc] = bu - self._dimensionality_cache[uc] = di + self._cache.root_units[uc] = bu + self._cache.dimensionality[uc] = di if not prefixed: - if di not in self._dimensional_equivalents: - self._dimensional_equivalents[di] = set() - - self._dimensional_equivalents[di].add(self._units[base_name]._name) + 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)) @@ -556,8 +569,12 @@ class BaseRegistry(metaclass=RegistryMeta): if not input_units: return UnitsContainer() - if input_units in self._dimensionality_cache: - return self._dimensionality_cache[input_units] + cache = self._cache.dimensionality + + try: + return cache[input_units] + except KeyError: + pass accumulator = defaultdict(float) self._get_dimensionality_recurse(input_units, 1.0, accumulator) @@ -567,7 +584,7 @@ class BaseRegistry(metaclass=RegistryMeta): dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0.0}) - self._dimensionality_cache[input_units] = dims + cache[input_units] = dims return dims @@ -643,8 +660,12 @@ class BaseRegistry(metaclass=RegistryMeta): return 1., UnitsContainer() # The cache is only done for check_nonmult=True - if check_nonmult and input_units in self._root_units_cache: - return self._root_units_cache[input_units] + cache = self._cache.root_units + if check_nonmult: + try: + return cache[input_units] + except KeyError: + pass accumulators = [1., defaultdict(float)] self._get_root_units_recurse(input_units, 1.0, accumulators) @@ -659,7 +680,7 @@ class BaseRegistry(metaclass=RegistryMeta): return None, units if check_nonmult: - self._root_units_cache[input_units] = factor, units + cache[input_units] = factor, units return factor, units @@ -708,10 +729,7 @@ class BaseRegistry(metaclass=RegistryMeta): return frozenset() src_dim = self._get_dimensionality(input_units) - - ret = self._dimensional_equivalents[src_dim] - - return ret + return self._cache.dimensional_equivalents[src_dim] def convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. @@ -812,17 +830,20 @@ class BaseRegistry(metaclass=RegistryMeta): :class:`pint.UndefinedUnitError` if a unit is not in the registry :class:`ValueError` if the expression is invalid. """ + for p in self.preprocessors: + input_string = p(input_string) units = self._parse_units(input_string, as_delta) return self.Unit(units) - def _parse_units(self, input_string, as_delta=None): + def _parse_units(self, input_string, as_delta=True): """ """ - if as_delta is None: - as_delta = True - - if as_delta and input_string in self._parse_unit_cache: - return self._parse_unit_cache[input_string] + cache = self._cache.parse_unit + if as_delta: + try: + return cache[input_string] + except KeyError: + pass if not input_string: return UnitsContainer() @@ -850,7 +871,7 @@ class BaseRegistry(metaclass=RegistryMeta): ret = UnitsContainer(ret) if as_delta: - self._parse_unit_cache[input_string] = ret + cache[input_string] = ret return ret @@ -880,6 +901,8 @@ class BaseRegistry(metaclass=RegistryMeta): if not input_string: return self.Quantity(1) + for p in self.preprocessors: + input_string = p(input_string) input_string = string_preprocessor(input_string) gen = tokenizer(input_string) @@ -1058,13 +1081,14 @@ class ContextRegistry(BaseRegistry): """ def __init__(self, **kwargs): - super().__init__(**kwargs) - #: Map context name (string) or abbreviation to context. self._contexts = {} - #: Stores active contexts. self._active_ctx = ContextChain() + #: Map context chain to cache + self._caches = {} + + super().__init__(**kwargs) def _register_parsers(self): super()._register_parsers() @@ -1109,6 +1133,14 @@ class ContextRegistry(BaseRegistry): return context + def _build_cache(self): + key = self._active_ctx.context_ids() + try: + self._cache = self._caches[key] + except KeyError: + super()._build_cache() + self._caches[key] = self._cache + def enable_contexts(self, *names_or_contexts, **kwargs): """Enable contexts provided by name or by object. @@ -1279,7 +1311,7 @@ class ContextRegistry(BaseRegistry): nodes = find_connected_nodes(self._active_ctx.graph, src_dim) if nodes: for node in nodes: - ret |= self._dimensional_equivalents[node] + ret |= self._cache.dimensional_equivalents[node] return ret @@ -1515,20 +1547,24 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): 'warn', 'raise', 'ignore' :type on_redefinition: str :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. + :param preprocessors: list of callables which are iteratively ran on any input expression + or unit string """ def __init__(self, filename='', force_ndarray=False, default_as_delta=True, autoconvert_offset_to_baseunit=False, on_redefinition='warn', system=None, - auto_reduce_dimensions=False): + auto_reduce_dimensions=False, preprocessors=None): super().__init__( - filename=filename, force_ndarray=force_ndarray, + filename=filename, + force_ndarray=force_ndarray, on_redefinition=on_redefinition, default_as_delta=default_as_delta, autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, system=system, - auto_reduce_dimensions=auto_reduce_dimensions + auto_reduce_dimensions=auto_reduce_dimensions, + preprocessors=preprocessors ) def pi_theorem(self, quantities): diff --git a/pint/systems.py b/pint/systems.py index 3daf527..f0ecac5 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -38,7 +38,7 @@ class Group(SharedRegistryObject): """ #: Regex to match the header parts of a definition. - _header_re = re.compile('@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*') + _header_re = re.compile(r'@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*') def __init__(self, name): """ @@ -261,7 +261,7 @@ class System(SharedRegistryObject): """ #: Regex to match the header parts of a context. - _header_re = re.compile('@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*') + _header_re = re.compile(r'@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*') def __init__(self, name): """ diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index f7774af..cb6d610 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -40,14 +40,14 @@ def requires_not_uncertainties(): return unittest.skipIf(HAS_UNCERTAINTIES, 'Requires Uncertainties is not installed.') -_number_re = '([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' -_q_re = re.compile('<Quantity\(' + '\s*' + '(?P<magnitude>%s)' % _number_re + - '\s*,\s*' + "'(?P<unit>.*)'" + '\s*' + '\)>') +_number_re = r'([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' +_q_re = re.compile(r'<Quantity\(' + r'\s*' + r'(?P<magnitude>%s)' % _number_re + + r'\s*,\s*' + r"'(?P<unit>.*)'" + r'\s*' + r'\)>') -_sq_re = re.compile('\s*' + '(?P<magnitude>%s)' % _number_re + - '\s' + "(?P<unit>.*)") +_sq_re = re.compile(r'\s*' + r'(?P<magnitude>%s)' % _number_re + + r'\s' + r"(?P<unit>.*)") -_unit_re = re.compile('<Unit\((.*)\)>') +_unit_re = re.compile(r'<Unit\((.*)\)>') class PintOutputChecker(doctest.OutputChecker): diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 389cd20..ac4260f 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -55,7 +55,7 @@ class TestMeasurement(QuantityTestCase): self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}') self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second<sup>2</sup>') self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') - self.assertEqual('{0:.1fLx}'.format(m), '\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') + self.assertEqual('{0:.1fLx}'.format(m), r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') def test_format_paru(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') @@ -75,8 +75,8 @@ class TestMeasurement(QuantityTestCase): self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}') self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second<sup>2</sup>') self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') - self.assertEqual('{0:.3uLx}'.format(m), '\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') - self.assertEqual('{0:.1uLx}'.format(m), '\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') + self.assertEqual('{0:.3uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') + self.assertEqual('{0:.1uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') def test_format_percu(self): self.test_format_perce() diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 2026c9a..82dada1 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import copy +import functools import math +import re from pint import DimensionalityError, UndefinedUnitError from pint.compat import np @@ -272,6 +274,28 @@ class TestRegistry(QuantityTestCase): self.assertEqual(parse('kelvin*meter', as_delta=True), UnitsContainer(kelvin=1, meter=1)) self.assertEqual(parse('kelvin*meter', as_delta=False), UnitsContainer(kelvin=1, meter=1)) + def test_parse_expression_with_preprocessor(self): + # Add parsing of UDUNITS-style power + self.ureg.preprocessors.append(functools.partial( + re.sub, r'(?<=[A-Za-z])(?![A-Za-z])(?<![0-9\-][eE])(?<![0-9\-])(?=[0-9\-])', '**')) + # Test equality + self.assertEqual(self.ureg.parse_expression('42 m2'), self.Q_(42, UnitsContainer(meter=2.))) + self.assertEqual(self.ureg.parse_expression('1e6 Hz s-2'), self.Q_(1e6, UnitsContainer(second=-3.))) + self.assertEqual(self.ureg.parse_expression('3 metre3'), self.Q_(3, UnitsContainer(meter=3.))) + # Clean up and test previously expected value + self.ureg.preprocessors.pop() + self.assertEqual(self.ureg.parse_expression('1e6 Hz s-2'), self.Q_(999998., UnitsContainer())) + + def test_parse_unit_with_preprocessor(self): + # Add parsing of UDUNITS-style power + self.ureg.preprocessors.append(functools.partial( + re.sub, r'(?<=[A-Za-z])(?![A-Za-z])(?<![0-9\-][eE])(?<![0-9\-])(?=[0-9\-])', '**')) + # Test equality + self.assertEqual(self.ureg.parse_units('m2'), UnitsContainer(meter=2.)) + self.assertEqual(self.ureg.parse_units('m-2'), UnitsContainer(meter=-2.)) + # Clean up + self.ureg.preprocessors.pop() + def test_name(self): self.assertRaises(UndefinedUnitError, self.ureg.get_name, 'asdf') |