Reorganise the decorators. expose and validate are now in wsme.rest, and ws.api.signature becomes the raw decorator to declare a function signature. Got rid of the 'pexpose' decorator, which will be replaced later by a better way.

This commit is contained in:
Christophe de Vienne 2012-10-31 10:48:01 +01:00
parent 983ba099d7
commit 4747aa82f1
8 changed files with 128 additions and 170 deletions

View File

@ -37,6 +37,7 @@ wsme.protocols =
packages = packages =
wsme wsme
wsme.protocols wsme.protocols
wsme.rest
wsme.tests wsme.tests
extra_files = extra_files =

View File

@ -1,8 +1,10 @@
from wsme.api import sig, expose, validate from wsme.api import signature
from wsme.rest import expose, validate
from wsme.root import WSRoot from wsme.root import WSRoot
from wsme.types import wsattr, wsproperty, Unset from wsme.types import wsattr, wsproperty, Unset
__all__ = [ __all__ = [
'expose', 'validate', 'sig', 'expose', 'validate', 'signature',
'WSRoot', 'WSRoot',
'wsattr', 'wsproperty', 'Unset'] 'wsattr', 'wsproperty', 'Unset'
]

View File

@ -1,31 +1,6 @@
import functools import functools
import inspect import inspect
__all__ = ['expose', 'validate']
APIPATH_MAXLEN = 20
def scan_api(controller, path=[]):
"""
Recursively iterate a controller api entries, while setting
their :attr:`FunctionDefinition.path`.
"""
for name in dir(controller):
if name.startswith('_'):
continue
a = getattr(controller, name)
if inspect.ismethod(a):
if hasattr(a, '_wsme_definition'):
yield path + [name], a._wsme_definition
elif inspect.isclass(a):
continue
else:
if len(path) > APIPATH_MAXLEN:
raise ValueError("Path is too long: " + str(path))
for i in scan_api(a, path + [name]):
yield i
def iswsmefunction(f): def iswsmefunction(f):
return hasattr(f, '_wsme_definition') return hasattr(f, '_wsme_definition')
@ -85,14 +60,6 @@ class FunctionDefinition(object):
#: If the body carry the datas of a single argument, its type #: If the body carry the datas of a single argument, its type
self.body_type = None self.body_type = None
#: True if this function is exposed by a protocol and not in
#: the api tree, which means it is not part of the api.
self.protocol_specific = False
#: Override the contenttype of the returned value.
#: Make sense only with :attr:`protocol_specific` functions.
self.contenttype = None
#: Dictionnary of protocol-specific options. #: Dictionnary of protocol-specific options.
self.extra_options = None self.extra_options = None
@ -121,93 +88,45 @@ class FunctionDefinition(object):
for arg in self.arguments: for arg in self.arguments:
arg.resolve_type(registry) arg.resolve_type(registry)
def set_options(self, body=None, **extra_options):
class expose(object):
"""
Decorator that expose a function.
:param return_type: Return type of the function
Example::
class MyController(object):
@expose(int)
def getint(self):
return 1
"""
def __init__(self, return_type=None, body=None, multiple_expose=False,
**options):
self.return_type = return_type
self.body_type = body self.body_type = body
self.multiple_expose = multiple_expose self.extra_options = extra_options
def set_arg_types(self, argspec, arg_types):
args, varargs, keywords, defaults = argspec
if args[0] == 'self':
args = args[1:]
arg_types = list(arg_types)
if self.body_type is not None:
arg_types.append(self.body_type)
for i, argname in enumerate(args):
datatype = arg_types[i]
mandatory = defaults is None or i < (len(args) - len(defaults))
default = None
if not mandatory:
default = defaults[i - (len(args) - len(defaults))]
self.arguments.append(FunctionArgument(argname, datatype,
mandatory, default))
class signature(object):
def __init__(self, *types, **options):
self.return_type = types[0] if types else None
self.arg_types = types[1:] if len(types) > 1 else None
self.wrap = options.pop('wrap', False)
self.options = options self.options = options
def __call__(self, func): def __call__(self, func):
if self.multiple_expose: argspec = getargspec(func)
if self.wrap:
func = wrapfunc(func) func = wrapfunc(func)
fd = FunctionDefinition.get(func) fd = FunctionDefinition.get(func)
if fd.extra_options is not None: if fd.extra_options is not None:
raise ValueError("This function is already exposed") raise ValueError("This function is already exposed")
fd.return_type = self.return_type fd.return_type = self.return_type
fd.body_type = self.body_type fd.set_options(**self.options)
fd.extra_options = self.options if self.arg_types:
fd.set_arg_types(argspec, self.arg_types)
return func return func
sig = signature
class sig(object):
def __init__(self, return_type, *param_types, **options):
self.expose = expose(return_type, **options)
self.validate = validate(*param_types)
def __call__(self, func):
func = self.expose(func)
func = self.validate(func)
return func
class pexpose(object):
def __init__(self, return_type=None, contenttype=None):
self.return_type = return_type
self.contenttype = contenttype
def __call__(self, func):
fd = FunctionDefinition.get(func)
fd.return_type = self.return_type
fd.protocol_specific = True
fd.contenttype = self.contenttype
return func
class validate(object):
"""
Decorator that define the arguments types of a function.
Example::
class MyController(object):
@expose(str)
@validate(datetime.date, datetime.time)
def format(self, d, t):
return d.isoformat() + ' ' + t.isoformat()
"""
def __init__(self, *param_types):
self.param_types = param_types
def __call__(self, func):
fd = FunctionDefinition.get(func)
args, varargs, keywords, defaults = getargspec(func)
if args[0] == 'self':
args = args[1:]
param_types = list(self.param_types)
if fd.body_type is not None:
param_types.append(fd.body_type)
for i, argname in enumerate(args):
datatype = param_types[i]
mandatory = defaults is None or i < (len(args) - len(defaults))
default = None
if not mandatory:
default = defaults[i - (len(args) - len(defaults))]
fd.arguments.append(FunctionArgument(argname, datatype,
mandatory, default))
return func

View File

@ -51,7 +51,7 @@ def wsexpose(*args, **kwargs):
content_type='application/xml', content_type='application/xml',
generic=False generic=False
) )
sig = wsme.sig(*args, **kwargs) sig = wsme.signature(*args, **kwargs)
def decorate(f): def decorate(f):
sig(f) sig(f)

77
wsme/rest/__init__.py Normal file
View File

@ -0,0 +1,77 @@
import inspect
import wsme.api
APIPATH_MAXLEN = 20
class expose(object):
def __init__(self, *args, **kwargs):
self.signature = wsme.api.signature(*args, **kwargs)
def __call__(self, func):
return self.signature(func)
@classmethod
def with_method(self, method, *args, **kwargs):
kwargs['method'] = method
return expose(*args, **kwargs)
@classmethod
def get(cls, *args, **kwargs):
return expose.with_method('GET', *args, **kwargs)
@classmethod
def post(cls, *args, **kwargs):
return expose.with_method('POST', *args, **kwargs)
@classmethod
def put(cls, *args, **kwargs):
return expose.with_method('PUT', *args, **kwargs)
@classmethod
def delete(cls, *args, **kwargs):
return expose.with_method('DELETE', *args, **kwargs)
class validate(object):
"""
Decorator that define the arguments types of a function.
Example::
class MyController(object):
@expose(str)
@validate(datetime.date, datetime.time)
def format(self, d, t):
return d.isoformat() + ' ' + t.isoformat()
"""
def __init__(self, *param_types):
self.param_types = param_types
def __call__(self, func):
argspec = wsme.api.getargspec(func)
fd = wsme.api.FunctionDefinition.get(func)
fd.set_arg_types(argspec, self.param_types)
return func
def scan_api(controller, path=[]):
"""
Recursively iterate a controller api entries, while setting
their :attr:`FunctionDefinition.path`.
"""
for name in dir(controller):
if name.startswith('_'):
continue
a = getattr(controller, name)
if inspect.ismethod(a):
if wsme.api.iswsmefunction(a):
yield path + [name], a._wsme_definition
elif inspect.isclass(a):
continue
else:
if len(path) > APIPATH_MAXLEN:
raise ValueError("Path is too long: " + str(path))
for i in scan_api(a, path + [name]):
yield i

View File

@ -8,9 +8,9 @@ import six
import webob import webob
from wsme import exc from wsme.exc import ClientSideError, MissingArgument, UnknownFunction
from wsme.protocols import getprotocol from wsme.protocols import getprotocol
from wsme.api import scan_api from wsme.rest import scan_api
from wsme import spore from wsme import spore
import wsme.types import wsme.types
@ -125,7 +125,7 @@ class WSRoot(object):
:rtype: list of (path, :class:`FunctionDefinition`) :rtype: list of (path, :class:`FunctionDefinition`)
""" """
if self._api is None: if self._api is None:
self._api = [i for i in self._scan_api(self)] self._api = list(self._scan_api(self))
for path, fdef in self._api: for path, fdef in self._api:
fdef.resolve_types(self.__registry__) fdef.resolve_types(self.__registry__)
return self._api return self._api
@ -167,7 +167,7 @@ class WSRoot(object):
context.path = protocol.extract_path(context) context.path = protocol.extract_path(context)
if context.path is None: if context.path is None:
raise exc.ClientSideError(u( raise ClientSideError(u(
'The %s protocol was unable to extract a function ' 'The %s protocol was unable to extract a function '
'path from the request') % protocol.name) 'path from the request') % protocol.name)
@ -176,7 +176,7 @@ class WSRoot(object):
for arg in context.funcdef.arguments: for arg in context.funcdef.arguments:
if arg.mandatory and arg.name not in kw: if arg.mandatory and arg.name not in kw:
raise exc.MissingArgument(arg.name) raise MissingArgument(arg.name)
txn = self.begin() txn = self.begin()
try: try:
@ -186,9 +186,6 @@ class WSRoot(object):
txn.abort() txn.abort()
raise raise
if context.funcdef.protocol_specific \
and context.funcdef.return_type is None:
return result
else: else:
# TODO make sure result type == a._wsme_definition.return_type # TODO make sure result type == a._wsme_definition.return_type
return protocol.encode_result(context, result) return protocol.encode_result(context, result)
@ -196,7 +193,7 @@ class WSRoot(object):
except Exception: except Exception:
e = sys.exc_info()[1] e = sys.exc_info()[1]
infos = self._format_exception(sys.exc_info()) infos = self._format_exception(sys.exc_info())
if isinstance(e, exc.ClientSideError): if isinstance(e, ClientSideError):
request.client_errorcount += 1 request.client_errorcount += 1
else: else:
request.server_errorcount += 1 request.server_errorcount += 1
@ -263,8 +260,6 @@ class WSRoot(object):
res.status = 500 res.status = 500
else: else:
res.status = 200 res.status = 200
if request.calls[0].funcdef:
res_content_type = request.calls[0].funcdef.contenttype
else: else:
res.status = protocol.get_response_status(request) res.status = protocol.get_response_status(request)
res_content_type = protocol.get_response_contenttype(request) res_content_type = protocol.get_response_contenttype(request)
@ -308,7 +303,7 @@ class WSRoot(object):
break break
if not hasattr(a, '_wsme_definition'): if not hasattr(a, '_wsme_definition'):
raise exc.UnknownFunction('/'.join(path)) raise UnknownFunction('/'.join(path))
definition = a._wsme_definition definition = a._wsme_definition
@ -317,7 +312,7 @@ class WSRoot(object):
def _format_exception(self, excinfo): def _format_exception(self, excinfo):
"""Extract informations that can be sent to the client.""" """Extract informations that can be sent to the client."""
error = excinfo[1] error = excinfo[1]
if isinstance(error, exc.ClientSideError): if isinstance(error, ClientSideError):
r = dict(faultcode="Client", r = dict(faultcode="Client",
faultstring=error.faultstring) faultstring=error.faultstring)
log.warning("Client-side error: %s" % r['faultstring']) log.warning("Client-side error: %s" % r['faultstring'])

View File

@ -1,13 +1,13 @@
# encoding=utf8 # encoding=utf8
from six import u, b from six import b
import sys import sys
import unittest import unittest
import webtest import webtest
from wsme import WSRoot, expose, validate from wsme import WSRoot, expose, validate
from wsme.api import scan_api, pexpose from wsme.rest import scan_api
from wsme.api import FunctionArgument, FunctionDefinition from wsme.api import FunctionArgument, FunctionDefinition
from wsme.types import iscomplex from wsme.types import iscomplex
import wsme.types import wsme.types
@ -15,41 +15,6 @@ import wsme.types
from wsme.tests.test_protocols import DummyProtocol from wsme.tests.test_protocols import DummyProtocol
def test_pexpose():
class Proto(DummyProtocol):
def extract_path(self, context):
if context.request.path.endswith('ufunc'):
return ['_protocol', 'dummy', 'ufunc']
else:
return ['_protocol', 'dummy', 'func']
@pexpose(None, "text/xml")
def func(self):
return "<p></p>"
@pexpose(None, "text/xml")
def ufunc(self):
return u("<p>\xc3\xa9</p>")
fd = FunctionDefinition.get(Proto.func)
assert fd.return_type is None
assert fd.protocol_specific
assert fd.contenttype == "text/xml"
p = Proto()
r = WSRoot()
r.addprotocol(p)
app = webtest.TestApp(r.wsgiapp())
res = app.get('/func')
assert res.status_int == 200
assert res.body == b("<p></p>"), res.body
res = app.get('/ufunc')
assert res.unicode_body == u("<p>\xc3\xa9</p>"), res.body
class TestController(unittest.TestCase): class TestController(unittest.TestCase):
def test_expose(self): def test_expose(self):
class MyWS(WSRoot): class MyWS(WSRoot):

View File

@ -1,20 +1,19 @@
import base64 import base64
import datetime import datetime
import decimal import decimal
import urllib
import wsme.tests.protocol import wsme.tests.protocol
try: try:
import simplejson as json import simplejson as json
except: except:
import json import json # noqa
import wsme.protocols.restjson import wsme.protocols.restjson
from wsme.protocols.restjson import fromjson, tojson from wsme.protocols.restjson import fromjson, tojson
from wsme.utils import parse_isodatetime, parse_isotime, parse_isodate from wsme.utils import parse_isodatetime, parse_isotime, parse_isodate
from wsme.types import isusertype, register_type from wsme.types import isusertype, register_type
from wsme.api import expose, validate from wsme.rest import expose, validate
import six import six
@ -23,7 +22,7 @@ from six import b, u
if six.PY3: if six.PY3:
from urllib.parse import urlencode from urllib.parse import urlencode
else: else:
from urllib import urlencode from urllib import urlencode # noqa
def prepare_value(value, datatype): def prepare_value(value, datatype):