Skip to content
Snippets Groups Projects
Commit 08f602f9 authored by Pavel Kácha's avatar Pavel Kácha
Browse files

Added dynamically defined OpenTypedDict class

parent 56356a0b
No related branches found
No related tags found
No related merge requests found
......@@ -4,25 +4,25 @@
# Copyright (c) 2016, CESNET, z. s. p. o.
# Use of this source is governed by an ISC license, see LICENSE file.
from typedcols import TypedDict, TypedList, KeyNotAllowed, KeysRequired, Discard, Any
from typedcols import TypedDict, OpenTypedDict, TypedList, KeyNotAllowed, KeysRequired, Discard, Any
from sys import version_info
import unittest
class AddressDict(TypedDict):
typedef = {
address_typedef = {
"street": {"type": str},
"num": {"type": int, "description": "Street number"},
"city": str,
"state": {"type": str, "required": True}
}
class AddressDict(TypedDict):
typedef = address_typedef
allow_unknown = True
def raise_discard(x=None):
raise Discard
class PersonDict(TypedDict):
typedef = {
person_typedef = {
"name": {"type": str, "default": "_Default_Value_"},
"age": int,
"address": {"type": AddressDict},
......@@ -35,18 +35,8 @@ class PersonDict(TypedDict):
"discard_default1": {"type": Any, "default": Discard}, # Same as no default
"discard_default2": {"type": Any, "default": raise_discard}, # Same as no default
}
allow_unknown = False
# Monkeypatching for cheap Py 2 & 3 compatibility
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
class TestTypedDict(unittest.TestCase):
def setUp(self):
self.person = PersonDict({
person_init_data = {
"age": "34",
"note": None,
"address": {
......@@ -58,7 +48,22 @@ class TestTypedDict(unittest.TestCase):
"discard2": "garbage",
"discard3": "rubbish",
"discard4": "scrap"
})
}
class PersonDict(TypedDict):
typedef = person_typedef
allow_unknown = False
# Monkeypatching for cheap Py 2 & 3 compatibility
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
class TestTypedDict(unittest.TestCase):
def setUp(self):
self.person = PersonDict(person_init_data)
def testTypedefNormalization(self):
self.assertEqual(self.person.typedef["age"], {"type": int})
......@@ -127,6 +132,20 @@ class TestTypedDict(unittest.TestCase):
("note", None)
])
class TestOpenTypedDict(TestTypedDict):
def setUp(self):
open_address_dict = OpenTypedDict(typedef=address_typedef.copy())
open_person_typedef = person_typedef.copy()
open_person_typedef["address"] = open_address_dict
open_person_dict = OpenTypedDict(typedef=open_person_typedef)
self.person = open_person_dict(person_init_data)
class IntList(TypedList):
item_type = int
......
......@@ -10,7 +10,7 @@ Defines TypedDict and TypedList, which enforce inserted types based on simple
type definition.
"""
__version__ = '0.1.9'
__version__ = '0.1.10'
__author__ = 'Pavel Kácha <pavel.kacha@cesnet.cz>'
import collections
......@@ -42,24 +42,26 @@ def Any(v):
return v
class TypedDictMetaclass(abc.ABCMeta):
""" Metaclass for TypedDict, allowing simplified typedefs - if typedef is not
dict, simple type object is assumed and correct dict is created.
Metaclassed to be run just once for the class, not for each instance.
"""
def dictifyTypedef(self, typedef):
def dictify_typedef(typedef):
for key in typedef:
tdef = typedef[key]
if not isinstance(tdef, collections.Mapping):
if callable(tdef):
typedef[key] = {"type": tdef}
typedef[key].setdefault("type", Any)
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)
cls.dictifyTypedef(cls.typedef)
dictify_typedef(cls.typedef)
class TypedDict(collections.MutableMapping):
class TypedDictBase(collections.MutableMapping):
""" Dictionary type abstract class, which supports checking of inserted
types, based on simple type definition.
......@@ -159,7 +161,7 @@ class TypedDict(collections.MutableMapping):
.arg will be message from "description" field in type definition
"""
tdef = self.getTypedef(key)
valuetype = tdef.get("type", Any)
valuetype = tdef["type"]
if valuetype is Discard:
return
try:
......@@ -211,7 +213,70 @@ class TypedDict(collections.MutableMapping):
# 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,), {})
TypedDict = TypedDictMetaclass("TypedDict", (TypedDictBase,), {})
class TypedefSetter(object):
""" Setter for OpenTypedDict.typedef value, which forces typedef canonicalisation.
Implemented as setter only class, as it does not intercept and slow down read
access.
"""
def __set__(self, obj, value):
dictify_typedef(value)
obj.__dict__["typedef"] = value
class OpenTypedDict(TypedDictBase):
""" Dictionary type class, which supports checking of inserted types, based on
simple type definition, which must be provided in constructor and is changeable
by assigning instance.typedef variable.
Note however that changing already populated OpenTypedDict's typedef to
incompatible definition may lead to undefined results and data inconsistent
with definition.
"""
def __init__(self, init_data=None, typedef=None, allow_unknown=False, dict_class=dict):
""" init_data: initial values
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
"""
self.allow_unknown = allow_unknown
self.dict_class = dict_class
self.typedef = typedef or {}
super(OpenTypedDict, self).__init__(init_data)
typedef = TypedefSetter()
def __call__(self, data):
""" Instances are made callable so they can be used in nested "type"
definitions. Note however that these classes are mutable, so
assigning new values replaces old ones.
"""
self.update(data)
return self
class TypedList(collections.MutableSequence):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment