From 520341238e1f44cac37771f79911317c0cd44acf Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 22 May 2012 14:40:36 +0200 Subject: [PATCH] Add a type Registry. It mainly allow to have easy cross-referenced types by setting the attribute datatype to the class name instead of the class instance. It may allow to have separate registry for different apis in the future --- doc/changes.rst | 2 + doc/types.rst | 11 +++ wsme/root.py | 4 + wsme/tests/test_types.py | 26 ++++++- wsme/types.py | 155 ++++++++++++++++++++++++++------------- 5 files changed, 142 insertions(+), 56 deletions(-) diff --git a/doc/changes.rst b/doc/changes.rst index fb1df52..ac084fc 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -8,6 +8,8 @@ Changes * String types handling is clearer. +* Supports cross-referenced types. + * Various bugfixes. * Tests code coverage is now over 95%. diff --git a/doc/types.rst b/doc/types.rst index f2c7475..1a43027 100644 --- a/doc/types.rst +++ b/doc/types.rst @@ -220,3 +220,14 @@ A few things you should know about complex types: For input values, it allows the code to know if the values were, or not, sent by the caller. + + - When 2 complex types refers to each other, their names can be + used as datatypes to avoid adding attributes afterwards: + + :: + + class A(object): + b = wsattr('B') + + class B(object): + a = wsattr(A) diff --git a/wsme/root.py b/wsme/root.py index 100b7ba..107189b 100644 --- a/wsme/root.py +++ b/wsme/root.py @@ -11,6 +11,7 @@ import webob from wsme import exc from wsme.protocols import getprotocol from wsme.api import scan_api +import wsme.types log = logging.getLogger(__name__) @@ -74,6 +75,8 @@ class WSRoot(object): module will be imported and used. """ + __registry__ = wsme.types.registry + def __init__(self, protocols=[], webpath='', transaction=None): self._debug = True self._webpath = webpath @@ -114,6 +117,7 @@ class WSRoot(object): :rtype: list of (path, :class:`FunctionDefinition`) """ if self._api is None: + self.__registry__.resolve_references() self._api = [i for i in scan_api(self)] return self._api diff --git a/wsme/tests/test_types.py b/wsme/tests/test_types.py index 1ec41a5..1290d13 100644 --- a/wsme/tests/test_types.py +++ b/wsme/tests/test_types.py @@ -12,6 +12,9 @@ def gen_class(): class TestTypes(unittest.TestCase): + def setUp(self): + types.registry = types.Registry() + def test_default_usertype(self): class MyType(types.UserType): basetype = str @@ -261,12 +264,29 @@ class TestTypes(unittest.TestCase): self.assertRaises(ValueError, types.register_type, {types.Unset: str}) - def test_list_attribute_auto_register(self): + def test_list_attribute_no_auto_register(self): class MyType(object): aint = int assert not hasattr(MyType, '_wsme_attributes') - types.list_attributes(MyType) + try: + types.list_attributes(MyType) + assert False, "TypeError was not raised" + except TypeError: + pass - assert hasattr(MyType, '_wsme_attributes') + assert not hasattr(MyType, '_wsme_attributes') + + def test_cross_referenced_types(self): + class A(object): + b = types.wsattr('B') + + class B(object): + a = A + + types.register_type(A) + types.register_type(B) + types.registry.resolve_references() + + assert A.b.datatype is B diff --git a/wsme/types.py b/wsme/types.py index 2237556..c891a1d 100644 --- a/wsme/types.py +++ b/wsme/types.py @@ -81,16 +81,6 @@ class Enum(UserType): def frombasetype(self, value): return value -pod_types = six.integer_types + ( - bytes, text, float, bool) -dt_types = (datetime.date, datetime.time, datetime.datetime) -extra_types = (binary, decimal.Decimal) -native_types = pod_types + dt_types + extra_types - -complex_types = [] -array_types = [] -dict_types = [] - class UnsetType(object): if sys.version < '3': @@ -103,6 +93,12 @@ class UnsetType(object): Unset = UnsetType() +pod_types = six.integer_types + ( + bytes, text, float, bool) +dt_types = (datetime.date, datetime.time, datetime.datetime) +extra_types = (binary, decimal.Decimal) +native_types = pod_types + dt_types + extra_types + def iscomplex(datatype): return inspect.isclass(datatype) \ and '_wsme_attributes' in datatype.__dict__ @@ -207,8 +203,7 @@ class wsattr(object): #: The attribute name on the public of the api. #: Defaults to :attr:`key` self.name = name - #: attribute data type - self.datatype = datatype + self._datatype = datatype #: True if the attribute is mandatory self.mandatory = mandatory #: Default value. The attribute will return this instead @@ -235,6 +230,19 @@ class wsattr(object): def __delete__(self, instance): self.__set__(instance, Unset) + def _get_datatype(self): + if isinstance(self._datatype, weakref.ref): + return self._datatype() + return self._datatype + + def _set_datatype(self, datatype): + self._datatype = datatype + + #: attribute data type. Can be either an actual type, + #: or a type name, in which case the actual type will be + #: determined when needed (generaly just before scaning the api). + datatype = property(_get_datatype, _set_datatype) + def iswsattr(attr): if inspect.isfunction(attr) or inspect.ismethod(attr): @@ -319,49 +327,90 @@ def inspect_class(class_): return attributes -def register_type(class_): - """ - Make sure a type is registered. - - It is automatically called by :class:`expose() ` - and :class:`validate() `. - Unless you want to control when the class inspection is done there - is no need to call it. - """ - if class_ is None or \ - class_ in native_types or \ - isusertype(class_) or iscomplex(class_): - return - - if isinstance(class_, list): - if len(class_) != 1: - raise ValueError("Cannot register type %s" % repr(class_)) - register_type(class_[0]) - if class_[0] not in array_types: - array_types.append(class_[0]) - return - - if isinstance(class_, dict): - if len(class_) != 1: - raise ValueError("Cannot register type %s" % repr(class_)) - key_type, value_type = list(class_.items())[0] - if key_type not in pod_types: - raise ValueError("Dictionnaries key can only be a pod type") - register_type(value_type) - if (key_type, value_type) not in dict_types: - dict_types.append((key_type, value_type)) - return - - class_._wsme_attributes = None - class_._wsme_attributes = inspect_class(class_) - - complex_types.append(weakref.ref(class_)) - - def list_attributes(class_): """ Returns a list of a complex type attributes. """ - if not hasattr(class_, '_wsme_attributes'): - register_type(class_) + if not iscomplex(class_): + raise TypeError("%s is not a registered type") return class_._wsme_attributes + + +class Registry(object): + def __init__(self): + self.complex_types = [] + self.array_types = [] + self.dict_types = [] + + def register(self, class_): + """ + Make sure a type is registered. + + It is automatically called by :class:`expose() ` + and :class:`validate() `. + Unless you want to control when the class inspection is done there + is no need to call it. + """ + if class_ is None or \ + class_ in native_types or \ + isusertype(class_) or iscomplex(class_): + return + + if isinstance(class_, list): + if len(class_) != 1: + raise ValueError("Cannot register type %s" % repr(class_)) + register_type(class_[0]) + if class_[0] not in self.array_types: + self.array_types.append(class_[0]) + return + + if isinstance(class_, dict): + if len(class_) != 1: + raise ValueError("Cannot register type %s" % repr(class_)) + key_type, value_type = list(class_.items())[0] + if key_type not in pod_types: + raise ValueError("Dictionnaries key can only be a pod type") + register_type(value_type) + if (key_type, value_type) not in self.dict_types: + self.dict_types.append((key_type, value_type)) + return + + class_._wsme_attributes = None + class_._wsme_attributes = inspect_class(class_) + + self.complex_types.append(weakref.ref(class_)) + + def lookup(self, typename): + modname = None + if '.' in typename: + modname, typename = typename.rsplit('.', 1) + for ct in self.complex_types: + ct = ct() + if typename == ct.__name__ and ( + modname is None or modname == ct.__module__): + return weakref.ref(ct) + + def resolve_type(self, type_): + if isinstance(type_, six.string_types): + return self.lookup(type_) + if isinstance(type_, list): + return [self.resolve_type(type_[0])] + if isinstance(type_, dict): + key_type, value_type = list(type_.items())[0] + return { + key_type: self.resolve_type(value_type) + } + return type_ + + def resolve_references(self): + for ct in self.complex_types: + ct = ct() + for attr in list_attributes(ct): + attr.datatype = self.resolve_type(attr.datatype) + +# Default type registry +registry = Registry() + + +def register_type(class_): + return registry.register(class_)