diff --git a/setup.cfg b/setup.cfg index 2a71350..71738bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ wsme.protocols = packages = wsme wsme.protocols + wsme.rest wsme.tests extra_files = diff --git a/wsme/__init__.py b/wsme/__init__.py index 902011f..35109ec 100644 --- a/wsme/__init__.py +++ b/wsme/__init__.py @@ -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' +] diff --git a/wsme/api.py b/wsme/api.py index ff63bd1..9cadd28 100644 --- a/wsme/api.py +++ b/wsme/api.py @@ -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 diff --git a/wsme/pecan.py b/wsme/pecan.py index 107842b..a9d9766 100644 --- a/wsme/pecan.py +++ b/wsme/pecan.py @@ -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) diff --git a/wsme/rest/__init__.py b/wsme/rest/__init__.py new file mode 100644 index 0000000..0968770 --- /dev/null +++ b/wsme/rest/__init__.py @@ -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 diff --git a/wsme/root.py b/wsme/root.py index c1c3ff0..f91e2cc 100644 --- a/wsme/root.py +++ b/wsme/root.py @@ -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']) diff --git a/wsme/tests/test_api.py b/wsme/tests/test_api.py index 6ed562e..75a70a2 100644 --- a/wsme/tests/test_api.py +++ b/wsme/tests/test_api.py @@ -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 "

" - - @pexpose(None, "text/xml") - def ufunc(self): - return u("

\xc3\xa9

") - - 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("

"), res.body - res = app.get('/ufunc') - assert res.unicode_body == u("

\xc3\xa9

"), res.body - - class TestController(unittest.TestCase): def test_expose(self): class MyWS(WSRoot): diff --git a/wsme/tests/test_restjson.py b/wsme/tests/test_restjson.py index 31e0c99..dcaf373 100644 --- a/wsme/tests/test_restjson.py +++ b/wsme/tests/test_restjson.py @@ -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):