# -*- coding: utf-8 -*-
"""
General utilities and exceptions.
"""
from __future__ import (absolute_import, division, print_function)
from collections import defaultdict, namedtuple, OrderedDict
try:
from collections.abc import ItemsView, Mapping
except ImportError: # Python 2
ItemsView = list
from collections import Mapping
from functools import wraps
from itertools import product
import os
import types
import warnings
from .. import __url__
from .deprecation import Deprecation
[docs]def identity(x):
return x
[docs]class NoConvergence(Exception):
pass
[docs]class ChemPyDeprecationWarning(DeprecationWarning):
pass
[docs]def deprecated(*args, **kwargs):
""" Helper to :class:`Deprecation` for using ChemPyDeprecationWarning. """
return Deprecation(
*args, issues_url=lambda s: __url__ + '/issues/' +
s.lstrip('gh-'), warning=ChemPyDeprecationWarning, **kwargs)
warnings.simplefilter(os.environ.get('CHEMPY_DEPRECATION_FILTER', 'once'),
ChemPyDeprecationWarning)
[docs]class DeferredImport(object):
def __init__(self, modname, arg=None, decorators=None):
self._modname = modname
self._arg = arg
self._decorators = decorators
self._cache = None
@property
def cache(self):
if self._cache is None:
if self._arg is None:
obj = __import__(self._modname)
else:
obj = getattr(__import__(self._modname, globals(), locals(), [self._arg]), self._arg)
if self._decorators is not None:
for deco in self._decorators:
obj = deco(obj)
self._cache = obj
return self._cache
def __getattribute__(self, attr):
if attr in ('_modname', '_arg', '_cache', 'cache', '_decorators'):
return object.__getattribute__(self, attr)
else:
return getattr(self.cache, attr)
def __call__(self, *args, **kwargs):
return self.cache(*args, **kwargs)
[docs]class NameSpace:
""" Used to wrap, e.g. modules.
Parameters
----------
default : module
The underlying module. Acts as a fallback for attribute access.
Examples
--------
>>> import numpy
>>> my_numpy = NameSpace(numpy)
>>> my_numpy.array = lambda *args, **kwargs: list(numpy.array(*args, **kwargs))
>>> isinstance(my_numpy.array([2, 3]), list)
True
>>> isinstance(numpy.array([2, 3]), list)
False
"""
def __init__(self, default):
self._NameSpace_default = default
self._NameSpace_attr_store = {}
def __getattr__(self, attr):
if attr.startswith('_NameSpace_'):
return self.__dict__[attr]
else:
try:
return self._NameSpace_attr_store[attr]
except KeyError:
return getattr(self._NameSpace_default, attr)
def __setattr__(self, attr, val):
if attr.startswith('_NameSpace_'):
self.__dict__[attr] = val
else:
self._NameSpace_attr_store[attr] = val
[docs] def as_dict(self):
items = self._NameSpace_default.__dict__.items()
result = {k: v for k, v in items if not k.startswith('_')}
result.update(self._NameSpace_attr_store)
return result
[docs]class AttributeContainer(object):
""" Used to turn e.g. a dictionary to a module-like object.
Parameters
----------
\\*\\*kwargs : dictionary
Examples
--------
>>> def RT(T, const):
... return T*const.molar_gas_constant
...
>>> from quantities import constants
>>> RT(273.15, constants)
array(273.15) * R
>>> my_constants = AttributeContainer(molar_gas_constant=42)
>>> RT(273.15, my_constants)
11472.3
"""
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
[docs] def as_dict(self):
return self.__dict__.copy()
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, ", ".join(set(dir(self)) - set(dir(object()))))
[docs]class AttrDict(dict):
""" Subclass of dict with attribute access to keys """
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
[docs]class defaultkeydict(defaultdict):
""" defaultdict where default_factory should have the signature key -> value
Examples
--------
>>> d = defaultkeydict(lambda k: '[%s]' % k, {'a': '[a]', 'b': '[B]'})
>>> d['a']
'[a]'
>>> d['b']
'[B]'
>>> d['c']
'[c]'
"""
def __missing__(self, key):
if self.default_factory is None:
raise KeyError("Missing key: %s" % key)
else:
self[key] = self.default_factory(key)
return self[key]
[docs]def defaultnamedtuple(typename, field_names, defaults=()):
""" Generates a new subclass of tuple with default values.
Parameters
----------
typename : string
The name of the class.
field_names : str or iterable
An iterable of splitable string.
defaults : iterable
Default values for ``field_names``, counting ``[-len(defaults):]``.
Examples
--------
>>> Body = defaultnamedtuple('Body', 'x y z density', (1.0,))
>>> Body.__doc__
'Body(x, y, z, density)'
>>> b = Body(10, z=3, y=5)
>>> b._asdict() == dict(x=10, y=5, z=3, density=1.0)
True
Returns
-------
A new tuple subclass named ``typename``
"""
Tuple = namedtuple(typename, field_names)
Tuple.__new__.__defaults__ = (None,) * len(Tuple._fields)
if isinstance(defaults, Mapping):
Tuple.__new__.__defaults__ = tuple(Tuple(**defaults))
else:
nmissing = len(Tuple._fields) - len(defaults)
defaults = (None,)*nmissing + tuple(defaults)
Tuple.__new__.__defaults__ = tuple(Tuple(*defaults))
return Tuple
[docs]def multi_indexed_cases(od, *, dict_=OrderedDict, apply_keys=None, apply_values=None,
apply_return=list, named_index=False):
""" Returns a list of length-2 tuples
Each tuple consist of a multi-index (tuple of integers) and a dictionary.
Parameters
----------
od : OrderedDict
Maps each key to a number of values. Instances of ``list``, ``tuple``,
``types.GeneratorType``, ``collections.abc.ItemsView`` are converted to ``OrderedDict``.
dict_ : type, optional
Used in the result (see ``Returns``).
apply_keys : callable, optional
Transformation of keys.
apply_values : callable, optional
Transformation of values.
apply_return : callable, optional
Applied on return value. ``None`` for generator.
named_index : bool
Tuple of indices will be a ``namedtuple`` (requires all keys to be ``str``).
Examples
--------
>>> from chempy.util.pyutil import multi_indexed_cases
>>> cases = multi_indexed_cases([('a', [1, 2, 3]), ('b', [False, True])])
>>> len(cases)
6
>>> midxs, case_kws = zip(*cases)
>>> midxs[0]
(0, 0)
>>> case_kws[0] == {'a': 1, 'b': False}
True
>>> d = {'a': 'foo bar'.split(), 'b': 'baz qux'.split()}
>>> from chempy.util.pyutil import AttrDict
>>> for nidx, case in multi_indexed_cases(d, dict_=AttrDict, named_index=True):
... if case.a == 'bar' and case.b == 'baz':
... print("{} {}".format(nidx.a, nidx.b))
...
1 0
Returns
-------
List of length-2 tuples, each consisting of one tuple of indices and one dictionary (of type ``dict_``).
"""
if isinstance(od, OrderedDict):
pass
else:
if hasattr(od, 'items'):
od = od.items()
if isinstance(od, (list, tuple, types.GeneratorType, ItemsView)):
od = OrderedDict(od)
else:
raise NotImplementedError("Expected an OrderedDict")
keys, values = tuple(map(apply_keys or identity, od.keys())), tuple(od.values())
MultiIndex = namedtuple('MultiIndex', keys) if named_index else lambda *args: tuple(args)
_generator = ((MultiIndex(*mi), dict_([
(k, (apply_values or identity)(v[i])) for
k, v, i in zip(keys, values, mi)
])) for mi in product(*map(range, map(len, values))))
return (apply_return or identity)(_generator)
[docs]def memoize(max_nargs=0):
def decorator(func):
@wraps(func)
def wrapper(*args):
if max_nargs is not None and len(args) > max_nargs:
raise ValueError("memoization error")
if args not in wrapper.results:
wrapper.results[args] = func(*args)
return wrapper.results[args]
wrapper.results = {}
return wrapper
return decorator