From 87f442c872f28e04313eb1d07110ef77a94e0c8c Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Fri, 28 Oct 2011 17:16:41 +0200 Subject: [PATCH] Now supports user types (non-complex types that are base on native types), the first on being Enum --- wsme/protocols/restxml.py | 8 ++++- wsme/tests/protocol.py | 23 +++++++++++++++ wsme/tests/test_restjson.py | 3 ++ wsme/tests/test_restxml.py | 3 ++ wsme/tests/test_types.py | 22 ++++++++++++++ wsme/types.py | 59 +++++++++++++++++++++++++++++++++---- 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/wsme/protocols/restxml.py b/wsme/protocols/restxml.py index 8c39a37..57ca251 100644 --- a/wsme/protocols/restxml.py +++ b/wsme/protocols/restxml.py @@ -42,7 +42,10 @@ def toxml(datatype, key, value): if value is None: el.set('nil', 'true') else: - if wsme.types.iscomplex(datatype): + if wsme.types.isusertype(datatype): + return toxml(datatype.basetype, + key, datatype.tobasetype(value)) + elif wsme.types.iscomplex(datatype): for attrdef in datatype._wsme_attributes: el.append(toxml(attrdef.datatype, attrdef.key, getattr(value, attrdef.key))) @@ -72,6 +75,9 @@ def fromxml(datatype, element): """ if element.get('nil', False): return None + if wsme.types.isusertype(datatype): + return datatype.frombasetype( + fromxml(datatype.basetype, element)) if wsme.types.iscomplex(datatype): obj = datatype() for attrdef in datatype._wsme_attributes: diff --git a/wsme/tests/protocol.py b/wsme/tests/protocol.py index d0555b3..c0dfe61 100644 --- a/wsme/tests/protocol.py +++ b/wsme/tests/protocol.py @@ -29,6 +29,9 @@ class CallException(RuntimeError): self.faultcode, self.faultstring, self.debuginfo) +myenumtype = wsme.types.Enum(str, set(['v1', 'v2'])) + + class NestedInner(object): aint = int @@ -103,6 +106,10 @@ class ReturnTypes(object): def getnestedarray(self): return [NestedOuter(), NestedOuter()] + @expose(myenumtype) + def getenum(self): + return 'v2' + class ArgTypes(object): @expose(str) @@ -206,6 +213,12 @@ class ArgTypes(object): assert type(value[0]) == NestedOuter return value + @expose(myenumtype) + @validate(myenumtype) + def setenum(self, value): + print value + assert type(value) == str + return value class WithErrors(object): @expose() @@ -328,6 +341,10 @@ class ProtocolTestCase(unittest.TestCase): r = self.call('returntypes/getnestedarray', _rt=[NestedOuter]) assert r == [{'inner': {'aint': 0}}, {'inner': {'aint': 0}}], r + def test_return_enum(self): + r = self.call('returntypes/getenum', _rt=myenumtype) + assert r == 'v2', r + def test_setstr(self): assert self.call('argtypes/setstr', value='astring') == 'astring' @@ -405,6 +422,12 @@ class ProtocolTestCase(unittest.TestCase): _rt=[NestedOuter]) assert r == value + def test_setenum(self): + value = 'v1' + r = self.call('argtypes/setenum', value=value, + _rt=myenumtype) + assert r == value + def test_nested_api(self): r = self.call('nested/inner/deepfunction', _rt=bool) assert r is True diff --git a/wsme/tests/test_restjson.py b/wsme/tests/test_restjson.py index 1f97b6f..f7052bc 100644 --- a/wsme/tests/test_restjson.py +++ b/wsme/tests/test_restjson.py @@ -11,6 +11,7 @@ except: import wsme.protocols.restjson from wsme.protocols.restjson import fromjson from wsme.utils import * +from wsme.types import isusertype def prepare_value(value, datatype): @@ -26,6 +27,8 @@ def prepare_value(value, datatype): def prepare_result(value, datatype): + if isusertype(datatype): + datatype = datatype.basetype if isinstance(datatype, list): return [prepare_result(item, datatype[0]) for item in value] if datatype == datetime.date: diff --git a/wsme/tests/test_restxml.py b/wsme/tests/test_restxml.py index b7e7b6c..0bfa559 100644 --- a/wsme/tests/test_restxml.py +++ b/wsme/tests/test_restxml.py @@ -4,6 +4,7 @@ import base64 import wsme.tests.protocol from wsme.utils import * +from wsme.types import isusertype try: import xml.etree.ElementTree as et @@ -56,6 +57,8 @@ def loadxml(el, datatype): print d return d else: + if isusertype(datatype): + datatype = datatype.basetype if datatype == datetime.date: return parse_isodate(el.text) if datatype == datetime.time: diff --git a/wsme/tests/test_types.py b/wsme/tests/test_types.py index 335a375..a3ab53d 100644 --- a/wsme/tests/test_types.py +++ b/wsme/tests/test_types.py @@ -133,3 +133,25 @@ class TestTypes(unittest.TestCase): assert len(AType._wsme_attributes) == 0 assert AType().test == 'test' + + def test_enum(self): + aenum = types.Enum(str, ['v1', 'v2']) + assert aenum.basetype is str + + class AType(object): + a = aenum + + types.register_type(AType) + + assert AType.a.datatype is aenum + + obj = AType() + obj.a = 'v1' + assert obj.a == 'v1' + + + try: + obj.a = 'v3' + assert False, 'ValueError was not raised' + except ValueError, e: + assert str(e) == "Value 'v3' is invalid (should be one of: v1, v2)" diff --git a/wsme/types.py b/wsme/types.py index 8f66752..7e5449d 100644 --- a/wsme/types.py +++ b/wsme/types.py @@ -5,6 +5,46 @@ import inspect binary = object() + +class UserType(object): + basetype = None + + def validate(self, value): + return + + def tobasetype(self, value): + return value + + def frombasetype(self, value): + return value + +class Enum(UserType): + """ + A simple enumeration type. Can be based on any non-complex type. + + :param basetype: The actual data type + :param values: A set of possible values + + If nullable, 'None' should be added the values set. + """ + def __init__(self, basetype, values): + self.basetype = basetype + self.values = values + + def validate(self, value): + if value not in self.values: + raise ValueError("Value '%s' is invalid (should be one of: %s)" % ( + value, ', '.join(self.values))) + + def tobasetype(self, value): + return value + + def frombasetype(self, value): + return value + +def isusertype(class_): + return isinstance(class_, UserType) + pod_types = [str, unicode, int, float, bool] dt_types = [datetime.date, datetime.time, datetime.datetime] extra_types = [binary, decimal.Decimal] @@ -24,6 +64,17 @@ def iscomplex(datatype): return hasattr(datatype, '_wsme_attributes') +def validate_value(datatype, value): + print datatype + if hasattr(datatype, 'validate'): + return datatype.validate(value) + else: + if value is not None and not isinstance(value, datatype): + raise ValueError( + "Wrong type. Expected '%s', got '%s'" % ( + datatype, type(value) + )) + class wsproperty(property): """ A specialised :class:`property` to define typed-property on complex types. @@ -76,11 +127,7 @@ class wsattr(object): return getattr(instance, '_' + self.key, Unset) def __set__(self, instance, value): - if value is not None and not isinstance(value, self.datatype): - raise ValueError( - "Wrong type for attribute %s. Expected '%s', got '%s'" % ( - self.key, self.datatype, type(value) - )) + validate_value(self.datatype, value) setattr(instance, '_' + self.key, value) def __delete__(self, instance): @@ -177,7 +224,7 @@ def register_type(class_): """ if class_ is None or \ class_ in native_types or \ - hasattr(class_, '_wsme_attributes'): + isusertype(class_) or iscomplex(class_): return if isinstance(class_, list):