Source code for typedcols

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, CESNET, z. s. p. o.
# Use of this source is governed by an ISC license, see LICENSE file.

"""Simple typed collections library.

Defines TypedDict and TypedList, which enforce inserted types based on simple
type definition.
"""

__version__ = '0.1.13'
__author__ = 'Pavel Kácha <pavel.kacha@cesnet.cz>'

import collections
import abc


[docs]class KeyNotAllowed(LookupError): """ Raised when untyped key is inserted on type, which does not allow for untyped keys. """
[docs]class KeysRequired(LookupError): """ Raised when required keys are missing in dictionary (usually on the call of checkRequired method. """
[docs]class Discard(Exception): """ Sentinel class to signal expected dropping of the key. Can be returned or raised from type enforcing callable, and can itself be used as type enforcing callable. """ @classmethod def __call__(cls, x=None): return cls
[docs]def Any(v): return v
[docs]def dictify_typedef(typedef): for key in typedef: tdef = typedef[key] if callable(tdef): typedef[key] = {"type": tdef} typedef[key].setdefault("type", Any)
[docs]class TypedDictMetaclass(abc.ABCMeta): """ Metaclass for TypedDict, allowing simplified typedefs - if typedef is callable, simple type object is assumed and correct dict is created. Metaclassed to be run just once for the class, not for each instance. """ def __init__(cls, name, bases, dct): super(TypedDictMetaclass, cls).__init__(name, bases, dct) dictify_typedef(cls.typedef)
class TypedDict(collections.MutableMapping): """ Dictionary type abstract class, which supports checking of inserted types, based on simple type definition. Must be subclassed, and subclass must populate 'typedef' dict, and may also reassign allow_unknown and dict_class class attributes. typedef: dictionary with keys and their type definitions. Type definition may be simple callable (int, string, check_func, AnotherTypedDict), or dict with the following members: "type": type enforcing callable. If callable returns, raises or is Discard, key will be silently discarded "default": new TypedDict subclass will be initialized with keys with this value; deleted keys will also revert to it "required": bool, checkRequired method will report the key if not present "description": string, explaining field type in human readable terms (will be used in exception explanations) Type enforcing callable must take one argument, and return value, coerced to expected type. Coercion may even be conversion, for example arbitrary date string, converted to DateTime. allow_unknown: boolean, specifies whether dictionary allows unknown keys, that means keys, which are not defined in 'typedef' dict_class: class or factory for underlying dict implementation """ typedef = {} allow_unknown = False dict_class = dict def __init__(self, init_data=None): self.data = self.dict_class() self.clear() self.update(init_data or {}) def clear(self): self.data.clear() for key in self.typedef.keys(): self.initItemDefault(key) def initItemDefault(self, key): """ Sets 'key' to the default value from typedef (if defined) """ tdef = self.getTypedef(key) try: default = tdef["default"] except KeyError: pass else: if default is Discard: return try: # Call if callable default = default() except Discard: return except TypeError: pass self[key] = default def getTypedef(self, key): """ Get type definition for 'key'. If key is not defined and allow_unknown is True, empty definition is returned, otherwise KeyNotAllowed gets raised. """ tdef = {"type": Any} try: tdef = self.typedef[key] except KeyError: if not self.allow_unknown: raise KeyNotAllowed(key) return tdef def checkRequired(self, recursive=False): """ The class does not check missing items by itself (we need it to be incomplete during creation and manipulation), so this checks and return list of missing required keys explicitly. Note that the check is not recursive (as instance dictionary may contain another subclasses of TypedDict), so care must be taken if there is such concern. """ missing = () for key, tdef in self.typedef.items(): if tdef.get("required", False) and not key in self.data: missing = missing + ((key,),) elif recursive and issubclass(tdef["type"], TypedDict): try: self.data[key].checkRequired(recursive) except KeysRequired as e: subkeys = tuple((key,) + subkey for subkey in e.args[0]) missing = missing + subkeys if missing: raise KeysRequired(missing) def __setitem__(self, key, value): """ Setter with type coercion. Any exception, raised from type enforcing callable, will get modified - first .arg will be tuple of key hierarchy, last .arg will be message from "description" field in type definition """ tdef = self.getTypedef(key) valuetype = tdef["type"] if valuetype is Discard: return try: fvalue = valuetype(value) except Discard: return except Exception as e: if isinstance(e.args[0], tuple): e.args = ((key,) + e.args[0],) + e.args[1:] else: e.args = ((key,),) + e.args if len(e.args) < 3: desc = tdef.get("description", None) if desc is not None: e.args = e.args + (desc,) raise if fvalue is Discard: return self.data[key] = fvalue def __getitem__(self, key): return self.data[key] def __delitem__(self, key): """ Deleter with reverting to defaults """ del self.data[key] self.initItemDefault(key) # Following definitions are not strictly necessary as MutableMapping # already defines them, however we can override them by calling to # possibly more optimized underlying implementations. def __iter__(self): return iter(self.data) def itervalues(self): return self.data.itervalues() def iteritems(self): return self.data.iteritems() def keys(self): return self.data.keys() def values(self): return self.data.values() def __len__(self): return len(self.data) def __str__(self): return "%s(%s)" % (type(self).__name__, str(self.data)) def __repr__(self): return "%s(%s)" % (type(self).__name__, repr(self.data)) # Py 2 requires metaclassing by __metaclass__ attribute, whereas Py 3 # needs metaclass argument. What actually happens is the following, # so we will do it explicitly, to be compatible with both versions. TypedDict = TypedDictMetaclass("TypedDict", (TypedDict,), {})
[docs]class TypedList(collections.MutableSequence): """ List type abstract class, which supports checking of inserted items type. Must be subclassed, and subclass must populate 'item_type' class variable, and may also reassign list_class class attributes. item_type: type enforcing callable, wich must take one argument, and return value, coerced to expected type. Coercion may even be conversion, for example arbitrary date string, converted to DateTime. Because defined within class, Python authomatically makes it object method, so it must be wrapped in staticmethod(...) explicitly. list_class: class or factory for underlying list implementation """ item_type = staticmethod(Any) list_class = list def __init__(self, iterable): self.data = self.list_class() self.extend(iterable) def __getitem__(self, val): return self.data[val] def __delitem__(self, val): del self.data[val] def __len__(self): return len(self.data) def __setitem__(self, idx, val): tval = self.item_type(val) self.data[idx] = tval
[docs] def insert(self, idx, val): tval = self.item_type(val) self.data.insert(idx, tval)
# Following definitions are not strictly necessary as MutableSequence # already defines them, however we can override them by calling to # possibly more optimized underlying implementations. def __contains__(self, val): tval = self.item_type(val) return tval in self.data
[docs] def index(self, val): tval = self.item_type(val) return self.data.index(tval)
[docs] def count(self, val): tval = self.item_type(val) return self.data.count(tval)
def __iter__(self): return iter(self.data)
[docs] def reverse(self): return self.data.reverse()
def __reversed__(self): return reversed(self.data)
[docs] def pop(self, index=-1): return self.data.pop(index)
def __str__(self): return "%s(%s)" % (type(self).__name__, str(self.data)) def __repr__(self): return "%s(%s)" % (type(self).__name__, repr(self.data))
[docs]def typed_list(name, item_type): """ Helper for oneshot type definition """ return type(name, (TypedList,), dict(item_type=staticmethod(item_type)))
[docs]def typed_dict(name, allow_unknown, typedef): """ Helper for oneshot type definition """ return type(name, (TypedDict,), dict(allow_unknown=allow_unknown, typedef=typedef))