summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS2
-rw-r--r--pint/context.py20
-rw-r--r--pint/formatting.py2
-rw-r--r--pint/registry.py124
-rw-r--r--pint/systems.py4
-rw-r--r--pint/testsuite/helpers.py12
-rw-r--r--pint/testsuite/test_measurement.py6
-rw-r--r--pint/testsuite/test_unit.py24
8 files changed, 132 insertions, 62 deletions
diff --git a/AUTHORS b/AUTHORS
index df980c2..21ba865 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -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 &plusmn; 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 &plusmn; 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')