Changed Systems and Groups to be aware of the Registry in which they are.

Additionally used SourceIterator and improved error reporting
This commit is contained in:
Hernan Grecco
2016-01-07 20:44:36 -03:00
parent 1fce8bd058
commit 5363f6d2f9
2 changed files with 190 additions and 162 deletions

View File

@@ -13,35 +13,42 @@ from __future__ import division, unicode_literals, print_function, absolute_impo
import re
from .unit import Definition, UnitDefinition, DefinitionSyntaxError
from .util import to_units_container, UnitsContainer
from .unit import Definition, UnitDefinition, DefinitionSyntaxError, RedefinitionError
from .util import to_units_container, SharedRegistryObject, SourceIterator
class Group(object):
"""A group is a list of units.
class _Group(SharedRegistryObject):
"""A group is a set of units.
Units can be added directly or by including other groups.
Members are computed dynamically, that is if a unit is added to a group X
all groups that include X are affected.
The group belongs to one Registry.
It can be specified in the definition file as:
@group <name> [using <group 1>, ..., <group N>]
<definition 1>
...
<definition N>
@end
"""
#: Regex to match the header parts of a context.
#: Regex to match the header parts of a definition.
_header_re = re.compile('@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*')
def __init__(self, name, groups_systems):
def __init__(self, name):
"""
:param name: Name of the group
:param name: Name of the group. If not given, a root Group will be created.
:type name: str
:param groups_systems: dictionary containing groups and system.
The newly created group will be added after creation.
:type groups_systems: dict[str, Group | System]
:param groups: dictionary like object groups and system.
The newly created group will be added after creation.
:type groups: dict[str | Group]
"""
if name in groups_systems:
t = 'group' if isinstance(groups_systems['name'], Group) else 'system'
raise ValueError('The system name already in use by a %s' % t)
# The name of the group.
#: type: str
self.name = name
@@ -58,21 +65,19 @@ class Group(object):
#: :type: set[str]
self._used_by = set()
#: Maps group name to Group.
#: :type: dict[str, Group]
self._groups_systems = groups_systems
self._groups_systems[self.name] = self
# Add this group to the group dictionary
self._REGISTRY._groups[self.name] = self
if name != 'root':
# All groups are added to root group
groups_systems['root'].add_groups(name)
self._REGISTRY._groups['root'].add_groups(name)
#: A cache of the included units.
#: None indicates that the cache has been invalidated.
#: :type: frozenset[str] | None
self._computed_members = None
@property
def members(self):
"""Names of the units that are members of the group.
@@ -95,13 +100,13 @@ class Group(object):
"""Invalidate computed members in this Group and all parent nodes.
"""
self._computed_members = None
d = self._groups_systems
d = self._REGISTRY._groups
for name in self._used_by:
d[name].invalidate_members()
def iter_used_groups(self):
pending = set(self._used_groups)
d = self._groups_systems
d = self._REGISTRY._groups
while pending:
name = pending.pop()
group = d[name]
@@ -124,6 +129,10 @@ class Group(object):
self.invalidate_members()
@property
def non_inherited_unit_names(self):
return frozenset(self._unit_names)
def remove_units(self, *unit_names):
"""Remove units from group.
@@ -139,7 +148,7 @@ class Group(object):
:type group_names: str
"""
d = self._groups_systems
d = self._REGISTRY._groups
for group_name in group_names:
grp = d[group_name]
@@ -157,28 +166,26 @@ class Group(object):
:type group_names: str
"""
d = self._groups_systems
d = self._REGISTRY._groups
for group_name in group_names:
grp = d[group_name]
self._used_groups.remove(group_name)
grp._used_by.remove(self.name)
self.invalidate_members()
@classmethod
def from_lines(cls, lines, define_func, group_dict):
def from_lines(cls, lines, define_func):
"""Return a Group object parsing an iterable of lines.
:param lines: iterable
:type lines: list[str]
:param define_func: Function to define a unit in the registry.
:type define_func: str -> None
:param group_dict: Maps group name to Group.
:type group_dict: dict[str, Group]
"""
header, lines = lines[0], lines[1:]
lines = SourceIterator(lines)
lineno, header = next(lines)
r = cls._header_re.search(header)
name = r.groupdict()['name'].strip()
@@ -189,19 +196,26 @@ class Group(object):
group_names = ()
unit_names = []
for line in lines:
for lineno, line in lines:
if '=' in line:
# Is a definition
definition = Definition.from_string(line)
if not isinstance(definition, UnitDefinition):
raise DefinitionSyntaxError('Only UnitDefinition are valid inside _used_groups, '
'not %s' % type(definition))
define_func(definition)
'not %s' % type(definition), lineno=lineno)
try:
define_func(definition)
except (RedefinitionError, DefinitionSyntaxError) as ex:
if ex.lineno is None:
ex.lineno = lineno
raise ex
unit_names.append(definition.name)
else:
unit_names.append(line.strip())
grp = cls(name, group_dict)
grp = cls(name)
grp.add_units(*unit_names)
@@ -210,34 +224,46 @@ class Group(object):
return grp
def __getattr__(self, item):
return self._REGISTRY
class System(object):
class _System(SharedRegistryObject):
"""A system is a Group plus a set of base units.
@system <name> [using <group 1>, ..., <group N>]
<rule 1>
...
<rule N>
@end
Members are computed dynamically, that is if a unit is added to a group X
all groups that include X are affected.
The System belongs to one Registry.
It can be specified in the definition file as:
@system <name> [using <group 1>, ..., <group N>]
<rule 1>
...
<rule N>
@end
The syntax for the rule is:
new_unit_name : old_unit_name
where:
- old_unit_name: a root unit part which is going to be removed from the system.
- new_unit_name: a non root unit which is going to replace the old_unit.
If the new_unit_name and the old_unit_name, the later and the colon can be ommited.
"""
#: Regex to match the header parts of a context.
_header_re = re.compile('@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*')
def __init__(self, name, groups_systems):
def __init__(self, name):
"""
:param name: Name of the group
:type name: str
:param groups_systems: dictionary containing groups and system.
The newly created group will be added after creation.
:type groups_systems: dict[str, Group | System]
"""
if name in groups_systems:
t = 'group' if isinstance(groups_systems[name], Group) else 'system'
raise ValueError('The system name (%s) already in use by a %s' % (name, t))
#: Name of the system
#: :type: str
self.name = name
@@ -257,18 +283,17 @@ class System(object):
#: :type: frozenset | None
self._computed_members = None
#: Maps group name to Group.
self._group_systems_dict = groups_systems
self._group_systems_dict[self.name] = self
# Add this system to the system dictionary
self._REGISTRY._systems[self.name] = self
@property
def members(self):
d = self._REGISTRY._groups
if self._computed_members is None:
self._computed_members = set()
for group_name in self._used_groups:
self._computed_members |= self._group_systems_dict[group_name].members
self._computed_members |= d[group_name].members
self._computed_members = frozenset(self._computed_members)
@@ -298,8 +323,10 @@ class System(object):
self.invalidate_members()
@classmethod
def from_lines(cls, lines, get_root_func, group_dict):
header, lines = lines[0], lines[1:]
def from_lines(cls, lines, get_root_func):
lines = SourceIterator(lines)
lineno, header = next(lines)
r = cls._header_re.search(header)
name = r.groupdict()['name'].strip()
@@ -313,7 +340,7 @@ class System(object):
base_unit_names = {}
derived_unit_names = []
for line in lines:
for lineno, line in lines:
line = line.strip()
# We would identify a
@@ -360,7 +387,7 @@ class System(object):
base_unit_names[old_unit] = {new_unit: 1./value}
system = cls(name, group_dict)
system = cls(name)
system.add_groups(*group_names)
@@ -370,54 +397,19 @@ class System(object):
return system
class GSManager(object):
def build_group_class(registry):
def __init__(self):
#: :type: dict[str, Group | System]
self._groups_systems = dict()
self._root_group = Group('root', self._groups_systems)
class Group(_Group):
pass
def add_system_from_lines(self, lines, get_root_func):
System.from_lines(lines, get_root_func, self._groups_systems)
Group._REGISTRY = registry
return Group
def add_group_from_lines(self, lines, define_func):
Group.from_lines(lines, define_func, self._groups_systems)
def get_group(self, name, create_if_needed=True):
"""Return a Group.
def build_system_class(registry):
:param name: Name of the group.
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: Group
"""
try:
return self._groups_systems[name]
except KeyError:
if create_if_needed:
if name == 'root':
raise ValueError('The name root is reserved.')
return Group(name, self._groups_systems)
else:
raise KeyError('No group %s found.' % name)
class System(_System):
pass
def get_system(self, name, create_if_needed=True):
"""Return a Group.
:param name: Name of the system
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: System
"""
try:
return self._groups_systems[name]
except KeyError:
if create_if_needed:
return System(name, self._groups_systems)
else:
raise KeyError('No system found named %s.' % name)
def __getitem__(self, item):
if item in self._groups_systems:
return self._groups_systems[item]
raise KeyError('No group or system found named %s.' % item)
System._REGISTRY = registry
return System

View File

@@ -29,7 +29,7 @@ from .util import (logger, pi_theorem, solve_dependencies, ParserHelper,
string_preprocessor, find_connected_nodes,
find_shortest_path, UnitsContainer, _is_dim,
SharedRegistryObject, to_units_container,
fix_str_conversions)
fix_str_conversions, SourceIterator)
from .compat import tokenizer, string_types, NUMERIC_TYPES, long_type, zip_longest
from .formatting import siunitx_format_unit
@@ -43,19 +43,6 @@ from .pint_eval import build_eval_tree
from . import systems
def _capture_till_end(ifile):
context = []
for no, line in ifile:
line = line.strip()
if line.startswith('@end'):
break
elif line.startswith('@'):
raise DefinitionSyntaxError('cannot nest @ directives', lineno=no)
context.append(line)
return context
@fix_str_conversions
class _Unit(SharedRegistryObject):
"""Implements a class to describe a unit supporting math operations.
@@ -68,8 +55,7 @@ class _Unit(SharedRegistryObject):
default_format = ''
def __reduce__(self):
from . import _build_unit
return _build_unit, (self._units)
return self.Unit, (self._units)
def __new__(cls, units):
inst = object.__new__(cls)
@@ -283,6 +269,7 @@ class UnitRegistry(object):
def __init__(self, filename='', force_ndarray=False, default_as_delta=True,
autoconvert_offset_to_baseunit=False,
on_redefinition='warn', system=None):
self.Unit = build_unit_class(self)
self.Quantity = build_quantity_class(self, force_ndarray)
self.Measurement = build_measurement_class(self, force_ndarray)
@@ -290,11 +277,23 @@ class UnitRegistry(object):
#: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore'
self._on_redefinition = on_redefinition
#: Map between name (string) and value (string) of defaults stored in the definitions file.
self._defaults = {}
#: Map dimension name (string) to its definition (DimensionDefinition).
self._dimensions = {}
#: :type: systems.GSManager
self._gsmanager = systems.GSManager()
#: Map system name to system.
#: :type: dict[ str | System]
self._systems = {}
#: Map group name to group.
#: :type: dict[ str | Group]
self._groups = {}
self.Group = systems.build_group_class(self)
self._groups['root'] = self.Group('root')
self.System = systems.build_system_class(self)
#: Map unit name (string) to its definition (UnitDefinition).
#: Might contain prefixed units.
@@ -336,9 +335,6 @@ class UnitRegistry(object):
#: non-multiplicative units as their *delta* counterparts.
self.default_as_delta = default_as_delta
#: System name to be used by default.
self._default_system_name = None
# Determines if quantities with offset units are converted to their
# base units on multiplication and division.
self.autoconvert_offset_to_baseunit = autoconvert_offset_to_baseunit
@@ -350,9 +346,15 @@ class UnitRegistry(object):
self.define(UnitDefinition('pi', 'π', (), ScaleConverter(math.pi)))
self._build_cache()
#: Copy units in root group to the default group
if 'group' in self._defaults:
grp = self.get_group(self._defaults['group'], True)
grp.add_units(*self.get_group('root', False).non_inherited_unit_names)
self.set_default_system(system)
#: System name to be used by default.
self._system = system or self._defaults.get('system', None)
self._build_cache()
def __name__(self):
return 'UnitRegistry'
@@ -390,24 +392,46 @@ class UnitRegistry(object):
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: Group
"""
return self._gsmanager.get_group(name, create_if_needed)
if name in self._groups:
return self._groups[name]
if not create_if_needed:
raise ValueError('Unkown group %s' % name)
return self.Group(name)
@property
def systems(self):
return set(self._systems.keys())
@property
def system(self):
return self._system
@system.setter
def system(self, name):
if name:
if name not in self._systems:
raise ValueError('Unknown system %s' % name)
self._base_units_cache = {}
self._system = name
def get_system(self, name, create_if_needed=True):
"""Return a Group.
:param registry:
:param name: Name of the group to be
:param create_if_needed: Create a group if not Found. If False, raise an Exception.
:return: System
"""
return self._gsmanager.get_system(name, create_if_needed)
if name in self._systems:
return self._systems[name]
def set_default_system(self, name):
# Test if exists
if name:
system = self._gsmanager.get_system(name, False)
self._default_system_name = name
self._base_units_cache = {}
if not create_if_needed:
raise ValueError('Unkown system %s' % name)
return self.System(name)
def add_context(self, context):
"""Add a context object to the registry.
@@ -531,12 +555,11 @@ class UnitRegistry(object):
# the added contexts are removed from the active one.
self.disable_contexts(len(names))
def define(self, definition):
def define(self, definition, add_to_root_group=False):
"""Add unit to the registry.
"""
if isinstance(definition, string_types):
definition = Definition.from_string(definition)
if isinstance(definition, DimensionDefinition):
d, di = self._dimensions, None
elif isinstance(definition, UnitDefinition):
@@ -550,6 +573,10 @@ class UnitRegistry(object):
self.define(DimensionDefinition(dimension, '', (), None, is_base=True))
# We add all units to the root group
if add_to_root_group:
self.get_group('root').add_units(definition.name)
elif isinstance(definition, PrefixDefinition):
d, di = self._prefixes, None
else:
@@ -596,7 +623,8 @@ class UnitRegistry(object):
self.define(UnitDefinition(d_name, d_symbol, d_aliases,
ScaleConverter(definition.converter.scale),
d_reference, definition.is_base))
d_reference, definition.is_base),
add_to_root_group=True)
def load_definitions(self, file, is_resource=False):
"""Add units and prefixes defined in a definition text file.
@@ -619,11 +647,8 @@ class UnitRegistry(object):
msg = getattr(e, 'message', '') or str(e)
raise ValueError('While opening {0}\n{1}'.format(file, msg))
ifile = enumerate(file, 1)
ifile = SourceIterator(file)
for no, line in ifile:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('@import'):
if is_resource:
path = line[7:].strip()
@@ -634,27 +659,30 @@ class UnitRegistry(object):
path = os.getcwd()
path = os.path.join(path, os.path.normpath(line[7:].strip()))
self.load_definitions(path, is_resource)
elif line.startswith('@defaults'):
next(ifile)
for lineno, part in ifile.block_iter():
k, v = part.split('=')
self._defaults[k.strip()] = v.strip()
elif line.startswith('@context'):
context = [line, ] + _capture_till_end(ifile)
try:
self.add_context(Context.from_lines(context, self.get_dimensionality))
except KeyError as e:
raise DefinitionSyntaxError('unknown dimension {0} in context'.format(str(e)), lineno=no)
elif line.startswith('@system'):
context = [line, ] + _capture_till_end(ifile)
try:
self._gsmanager.add_system_from_lines(context, self.get_root_units)
self.add_context(Context.from_lines(ifile.block_iter(),
self.get_dimensionality))
except KeyError as e:
raise DefinitionSyntaxError('unknown dimension {0} in context'.format(str(e)), lineno=no)
elif line.startswith('@group'):
context = [line, ] + _capture_till_end(ifile)
try:
self._gsmanager.add_group_from_lines(context, self.define)
except KeyError as e:
raise DefinitionSyntaxError('unknown dimension {0} in context'.format(str(e)), lineno=no)
self.Group.from_lines(ifile.block_iter(), self.define)
elif line.startswith('@system'):
self.System.from_lines(ifile.block_iter(), self.get_root_units)
else:
try:
self.define(Definition.from_string(line))
self.define(Definition.from_string(line),
add_to_root_group=True)
except (RedefinitionError, DefinitionSyntaxError) as ex:
if ex.lineno is None:
ex.lineno = no
@@ -881,15 +909,15 @@ class UnitRegistry(object):
:return:
"""
# The cache is only done for check_nonmult=True
if check_nonmult and input_units in self._base_units_cache:
if system is None:
system = self._system
# The cache is only done for check_nonmult=True and the current system.
if check_nonmult and system == self._system and input_units in self._base_units_cache:
return self._base_units_cache[input_units]
factor, units = self.get_root_units(input_units, check_nonmult)
if system is None:
system = self._default_system_name
if not system:
return factor, units
@@ -898,7 +926,7 @@ class UnitRegistry(object):
destination_units = UnitsContainer()
bu = self._gsmanager.get_system(system, False).base_units
bu = self.get_system(system, False).base_units
for unit, value in units.items():
if unit in bu:
@@ -933,11 +961,14 @@ class UnitRegistry(object):
"""
input_units = to_units_container(input_units)
if group_or_system is None:
group_or_system = self._system
equiv = self._get_compatible_units(input_units, group_or_system)
return frozenset(self.Unit(eq) for eq in equiv)
def _get_compatible_units(self, input_units, group_or_system=None):
def _get_compatible_units(self, input_units, group_or_system):
"""
"""
if not input_units:
@@ -955,7 +986,12 @@ class UnitRegistry(object):
ret |= self._dimensional_equivalents[node]
if group_or_system:
members = self._gsmanager[group_or_system].members
if group_or_system in self._systems:
members = self._systems[group_or_system].members
elif group_or_system in self._groups:
members = self._groups[group_or_system].members
else:
raise ValueError("Unknown Group o System with name '%s'" % group_or_system)
return frozenset(ret.intersection(members))
return ret