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 =
wsme
wsme.protocols
wsme.rest
wsme.tests
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.types import wsattr, wsproperty, Unset
__all__ = [
'expose', 'validate', 'sig',
'expose', 'validate', 'signature',
'WSRoot',
'wsattr', 'wsproperty', 'Unset']
'wsattr', 'wsproperty', 'Unset'
]

View File

@ -1,31 +1,6 @@
import functools
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):
return hasattr(f, '_wsme_definition')
@ -85,14 +60,6 @@ class FunctionDefinition(object):
#: If the body carry the datas of a single argument, its type
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.
self.extra_options = None
@ -121,93 +88,45 @@ class FunctionDefinition(object):
for arg in self.arguments:
arg.resolve_type(registry)
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
def set_options(self, body=None, **extra_options):
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
def __call__(self, func):
if self.multiple_expose:
argspec = getargspec(func)
if self.wrap:
func = wrapfunc(func)
fd = FunctionDefinition.get(func)
if fd.extra_options is not None:
raise ValueError("This function is already exposed")
fd.return_type = self.return_type
fd.body_type = self.body_type
fd.extra_options = self.options
fd.set_options(**self.options)
if self.arg_types:
fd.set_arg_types(argspec, self.arg_types)
return func
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
sig = signature

View File

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

View File

@ -1,13 +1,13 @@
# encoding=utf8
from six import u, b
from six import b
import sys
import unittest
import webtest
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.types import iscomplex
import wsme.types
@ -15,41 +15,6 @@ import wsme.types
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):
def test_expose(self):
class MyWS(WSRoot):

View File

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