From a06989e1a50b058896d3fcaeed44246902c637ce Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Fri, 28 Oct 2011 11:59:30 +0200 Subject: [PATCH] Split the controller module into api, protocols and root. --HG-- rename : wsme/rest.py => wsme/protocols/rest.py rename : wsme/tests/test_controller.py => wsme/tests/test_api.py --- wsme/__init__.py | 5 +- wsme/api.py | 202 +++++++ wsme/controller.py | 497 ------------------ wsme/protocols/__init__.py | 37 ++ wsme/{ => protocols}/rest.py | 2 +- wsme/protocols/restjson.py | 2 +- wsme/protocols/restxml.py | 2 +- wsme/root.py | 273 ++++++++++ .../tests/{test_controller.py => test_api.py} | 57 +- wsme/tests/test_protocols.py | 60 +++ 10 files changed, 581 insertions(+), 556 deletions(-) create mode 100644 wsme/api.py rename wsme/{ => protocols}/rest.py (97%) create mode 100644 wsme/root.py rename wsme/tests/{test_controller.py => test_api.py} (79%) create mode 100644 wsme/tests/test_protocols.py diff --git a/wsme/__init__.py b/wsme/__init__.py index ee4a877..b6183cc 100644 --- a/wsme/__init__.py +++ b/wsme/__init__.py @@ -1,2 +1,3 @@ -from controller import expose, validate, WSRoot -from types import wsattr, wsproperty +from wsme.api import expose, validate +from wsme.root import WSRoot +from wsme.types import wsattr, wsproperty diff --git a/wsme/api.py b/wsme/api.py new file mode 100644 index 0000000..44553d0 --- /dev/null +++ b/wsme/api.py @@ -0,0 +1,202 @@ +import inspect + +import pkg_resources + +from wsme.types import register_type + +__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 + + +class FunctionArgument(object): + """ + An argument definition of an api entry + """ + def __init__(self, name, datatype, mandatory, default): + #: argument name + self.name = name + + #: Data type + self.datatype = datatype + + #: True if the argument is mandatory + self.mandatory = mandatory + + #: Default value if argument is omitted + self.default = default + + +def funcproxy(func): + """ + A very simple wrapper for exposed function. + + It will carry the FunctionDefinition in place of the + decorared function so that a same function can be exposed + several times (for example a parent function can be exposed + in different ways in the children classes). + + The returned function also carry a ``_original_func`` attribute + so that it can be inspected if needed. + """ + def newfunc(*args, **kw): + return func(*args, **kw) + newfunc._is_wsme_funcproxy = True + newfunc._original_func = func + return newfunc + + +def isfuncproxy(func): + """ + Returns True if ``func`` is already a function proxy. + """ + return getattr(func, '_is_wsme_funcproxy', False) + + +class FunctionDefinition(object): + """ + An api entry definition + """ + def __init__(self, func): + #: Function name + self.name = func.__name__ + + #: Function documentation + self.doc = func.__doc__ + + #: Return type + self.return_type = None + + #: The function arguments (list of :class:`FunctionArgument`) + self.arguments = [] + + #: 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 = {} + + @classmethod + def get(cls, func): + """ + Returns the :class:`FunctionDefinition` of a method. + """ + if not isfuncproxy(func): + fd = FunctionDefinition(func) + func = funcproxy(func) + func._wsme_definition = fd + + return func, func._wsme_definition + + def get_arg(self, name): + """ + Returns a :class:`FunctionArgument` from its name + """ + for arg in self.arguments: + if arg.name == name: + return arg + return None + + +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, **options): + self.return_type = return_type + self.options = options + register_type(return_type) + + def __call__(self, func): + func, fd = FunctionDefinition.get(func) + if fd.return_type is not None: + raise ValueError("This function is already exposed") + fd.return_type = self.return_type + fd.extra_options = self.options + return func + + +class pexpose(object): + def __init__(self, return_type=None, contenttype=None): + self.return_type = return_type + self.contenttype = contenttype + register_type(return_type) + + def __call__(self, func): + 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): + func, fd = FunctionDefinition.get(func) + args, varargs, keywords, defaults = inspect.getargspec( + func._original_func) + if args[0] == 'self': + args = args[1:] + for i, argname in enumerate(args): + datatype = self.param_types[i] + register_type(datatype) + 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 + + + diff --git a/wsme/controller.py b/wsme/controller.py index b35671d..e69de29 100644 --- a/wsme/controller.py +++ b/wsme/controller.py @@ -1,497 +0,0 @@ -import inspect -import traceback -import weakref -import logging -import webob -import sys - -import pkg_resources - -from wsme import exc -from wsme.types import register_type - -__all__ = ['expose', 'validate'] - -log = logging.getLogger(__name__) - -registered_protocols = {} - -APIPATH_MAXLEN = 20 - - -html_body = """ - - - - - -%(content)s - - -""" - - -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 - - -class FunctionArgument(object): - """ - An argument definition of an api entry - """ - def __init__(self, name, datatype, mandatory, default): - #: argument name - self.name = name - - #: Data type - self.datatype = datatype - - #: True if the argument is mandatory - self.mandatory = mandatory - - #: Default value if argument is omitted - self.default = default - - -def funcproxy(func): - """ - A very simple wrapper for exposed function. - - It will carry the FunctionDefinition in place of the - decorared function so that a same function can be exposed - several times (for example a parent function can be exposed - in different ways in the children classes). - - The returned function also carry a ``_original_func`` attribute - so that it can be inspected if needed. - """ - def newfunc(*args, **kw): - return func(*args, **kw) - newfunc._is_wsme_funcproxy = True - newfunc._original_func = func - return newfunc - - -def isfuncproxy(func): - """ - Returns True if ``func`` is already a function proxy. - """ - return getattr(func, '_is_wsme_funcproxy', False) - - -class FunctionDefinition(object): - """ - An api entry definition - """ - def __init__(self, func): - #: Function name - self.name = func.__name__ - - #: Function documentation - self.doc = func.__doc__ - - #: Return type - self.return_type = None - - #: The function arguments (list of :class:`FunctionArgument`) - self.arguments = [] - - #: 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 = {} - - @classmethod - def get(cls, func): - """ - Returns the :class:`FunctionDefinition` of a method. - """ - if not isfuncproxy(func): - fd = FunctionDefinition(func) - func = funcproxy(func) - func._wsme_definition = fd - - return func, func._wsme_definition - - def get_arg(self, name): - """ - Returns a :class:`FunctionArgument` from its name - """ - for arg in self.arguments: - if arg.name == name: - return arg - return None - - -def register_protocol(protocol): - registered_protocols[protocol.name] = protocol - - -def getprotocol(name, **options): - protocol_class = registered_protocols.get(name) - if protocol_class is None: - for entry_point in pkg_resources.iter_entry_points( - 'wsme.protocols', name): - if entry_point.name == name: - protocol_class = entry_point.load() - if protocol_class is None: - raise ValueError("Cannot find protocol '%s'" % name) - registered_protocols[name] = protocol_class - return protocol_class(**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, **options): - self.return_type = return_type - self.options = options - register_type(return_type) - - def __call__(self, func): - func, fd = FunctionDefinition.get(func) - if fd.return_type is not None: - raise ValueError("This function is already exposed") - fd.return_type = self.return_type - fd.extra_options = self.options - return func - - -class pexpose(object): - def __init__(self, return_type=None, contenttype=None): - self.return_type = return_type - self.contenttype = contenttype - register_type(return_type) - - def __call__(self, func): - 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): - func, fd = FunctionDefinition.get(func) - args, varargs, keywords, defaults = inspect.getargspec( - func._original_func) - if args[0] == 'self': - args = args[1:] - for i, argname in enumerate(args): - datatype = self.param_types[i] - register_type(datatype) - 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 - - -class CallContext(object): - def __init__(self, request): - self.request = weakref.proxy(request) - self.path = None - - self.func = None - self.funcdef = None - - -class WSRoot(object): - """ - Root controller for webservices. - - :param protocols: A list of protocols to enable (see :meth:`addprotocol`) - :param webpath: The web path where the webservice is published. - """ - def __init__(self, protocols=[], webpath=''): - self._debug = True - self._webpath = webpath - self.protocols = [] - for protocol in protocols: - self.addprotocol(protocol) - - self._api = None - - def addprotocol(self, protocol, **options): - """ - Enable a new protocol on the controller. - - :param protocol: A registered protocol name or an instance - of a protocol. - """ - if isinstance(protocol, str): - protocol = getprotocol(protocol, **options) - self.protocols.append(protocol) - protocol.root = weakref.proxy(self) - - def getapi(self): - """ - Returns the api description. - - :rtype: list of (path, :class:`FunctionDefinition`) - """ - if self._api is None: - self._api = [i for i in scan_api(self)] - return self._api - - def _get_protocol(self, name): - for protocol in self.protocols: - if protocol.name == name: - return protocol - - def _select_protocol(self, request): - log.debug("Selecting a protocol for the following request :\n" - "headers: %s\nbody: %s", request.headers, - len(request.body) > 512 - and request.body[:512] - or request.body) - protocol = None - if 'wsmeproto' in request.params: - return self._get_protocol(request.params['wsmeproto']) - else: - - for p in self.protocols: - if p.accept(request): - protocol = p - break - return protocol - - def _do_call(self, protocol, context): - request = context.request - request.calls.append(context) - try: - if context.path is None: - context.path = protocol.extract_path(context) - - if context.path is None: - raise exc.ClientSideError( - u'The %s protocol was unable to extract a function ' - u'path from the request' % protocol.name) - - context.func, context.funcdef = self._lookup_function(context.path) - kw = protocol.read_arguments(context) - - for arg in context.funcdef.arguments: - if arg.mandatory and arg.name not in kw: - raise exc.MissingArgument(arg.name) - - result = context.func(**kw) - - 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) - - except Exception, e: - infos = self._format_exception(sys.exc_info()) - if isinstance(e, exc.ClientSideError): - request.client_errorcount += 1 - else: - request.server_errorcount += 1 - return protocol.encode_error(context, infos) - - def _handle_request(self, request): - def default_prepare_response_body(request, results): - return '\n'.join(results) - - res = webob.Response() - res_content_type = None - try: - protocol = self._select_protocol(request) - if protocol is None: - msg = ("None of the following protocols can handle this " - "request : %s" % ','.join( - (p.name for p in self.protocols))) - res.status = 500 - res.content_type = 'text/plain' - res.body = msg - log.error(msg) - return res - - context = None - - request.calls = [] - request.client_errorcount = 0 - request.server_errorcount = 0 - - if hasattr(protocol, 'prepare_response_body'): - prepare_response_body = protocol.prepare_response_body - else: - prepare_response_body = default_prepare_response_body - - body = prepare_response_body(request, ( - self._do_call(protocol, context) - for context in protocol.iter_calls(request))) - if isinstance(body, unicode): - res.unicode_body = body - else: - res.body = body - - if len(request.calls) == 1: - if hasattr(protocol, 'get_response_status'): - res.status = protocol.get_response_status(request) - else: - if request.client_errorcount: - res.status = 400 - elif request.server_errorcount: - 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) - except Exception, e: - infos = self._format_exception(sys.exc_info()) - request.server_errorcount += 1 - res.body = protocol.encode_error(context, infos) - res.status = 500 - - if res_content_type is None: - # Attempt to correctly guess what content-type we should return. - last_q = 0 - ctypes = [ct for ct in protocol.content_types if ct] - if ctypes: - res_content_type = request.accept.best_match(ctypes) - - # If not we will attempt to convert the body to an accepted - # output format. - if res_content_type is None: - if "text/html" in request.accept: - res.body = self._html_format(res.body, protocol.content_types) - res_content_type = "text/html" - - # TODO should we consider the encoding asked by - # the web browser ? - res.headers['Content-Type'] = "%s; charset=UTF-8" % res_content_type - - return res - - def _lookup_function(self, path): - a = self - - isprotocol_specific = path[0] == '_protocol' - - if isprotocol_specific: - a = self._get_protocol(path[1]) - path = path[2:] - - for name in path: - a = getattr(a, name, None) - if a is None: - break - - if not hasattr(a, '_wsme_definition'): - raise exc.UnknownFunction('/'.join(path)) - - definition = a._wsme_definition - - return a, definition - - def _format_exception(self, excinfo): - """Extract informations that can be sent to the client.""" - error = excinfo[1] - if isinstance(error, exc.ClientSideError): - r = dict(faultcode="Client", - faultstring=error.faultstring) - log.warning("Client-side error: %s" % r['faultstring']) - r['debuginfo'] = None - return r - else: - faultstring = str(error) - debuginfo = "\n".join(traceback.format_exception(*excinfo)) - - log.error('Server-side error: "%s". Detail: \n%s' % ( - faultstring, debuginfo)) - - r = dict(faultcode="Server", faultstring=faultstring) - if self._debug: - r['debuginfo'] = debuginfo - else: - r['debuginfo'] = None - return r - - def _html_format(self, content, content_types): - try: - from pygments import highlight - from pygments.lexers import get_lexer_for_mimetype - from pygments.formatters import HtmlFormatter - - lexer = None - for ct in content_types: - try: - lexer = get_lexer_for_mimetype(ct) - break - except: - pass - - if lexer is None: - raise ValueError("No lexer found") - formatter = HtmlFormatter() - return html_body % dict( - css=formatter.get_style_defs(), - content=highlight(content, lexer, formatter).encode('utf8')) - except Exception, e: - log.warning( - "Could not pygment the content because of the following " - "error :\n%s" % e) - return html_body % dict( - css='', - content='
%s
' % - content.replace('>', '>').replace('<', '<')) diff --git a/wsme/protocols/__init__.py b/wsme/protocols/__init__.py index e69de29..ed9d813 100644 --- a/wsme/protocols/__init__.py +++ b/wsme/protocols/__init__.py @@ -0,0 +1,37 @@ +import weakref + +import pkg_resources + +__all__ = [ + 'CallContext', + + 'register_protocol', 'getprotocol', +] + +registered_protocols = {} + + +class CallContext(object): + def __init__(self, request): + self.request = weakref.proxy(request) + self.path = None + + self.func = None + self.funcdef = None + + +def register_protocol(protocol): + registered_protocols[protocol.name] = protocol + + +def getprotocol(name, **options): + protocol_class = registered_protocols.get(name) + if protocol_class is None: + for entry_point in pkg_resources.iter_entry_points( + 'wsme.protocols', name): + if entry_point.name == name: + protocol_class = entry_point.load() + if protocol_class is None: + raise ValueError("Cannot find protocol '%s'" % name) + registered_protocols[name] = protocol_class + return protocol_class(**options) diff --git a/wsme/rest.py b/wsme/protocols/rest.py similarity index 97% rename from wsme/rest.py rename to wsme/protocols/rest.py index 79464a7..0cca229 100644 --- a/wsme/rest.py +++ b/wsme/protocols/rest.py @@ -1,7 +1,7 @@ import logging from wsme.exc import UnknownFunction, MissingArgument, UnknownArgument -from wsme.controller import CallContext +from wsme.protocols import CallContext log = logging.getLogger(__name__) diff --git a/wsme/protocols/restjson.py b/wsme/protocols/restjson.py index 447f58a..b9babff 100644 --- a/wsme/protocols/restjson.py +++ b/wsme/protocols/restjson.py @@ -7,7 +7,7 @@ import decimal from simplegeneric import generic -from wsme.rest import RestProtocol +from wsme.protocols.rest import RestProtocol import wsme.types try: diff --git a/wsme/protocols/restxml.py b/wsme/protocols/restxml.py index a05e889..97af455 100644 --- a/wsme/protocols/restxml.py +++ b/wsme/protocols/restxml.py @@ -8,7 +8,7 @@ except ImportError: from simplegeneric import generic -from wsme.rest import RestProtocol +from wsme.protocols.rest import RestProtocol from wsme.exc import * import wsme.types diff --git a/wsme/root.py b/wsme/root.py new file mode 100644 index 0000000..a01d6b9 --- /dev/null +++ b/wsme/root.py @@ -0,0 +1,273 @@ +import logging +import sys +import traceback +import weakref + +import webob + +from wsme import exc +from wsme.protocols import getprotocol +from wsme.api import scan_api + +log = logging.getLogger(__name__) + +html_body = """ + + + + + +%(content)s + + +""" + + +class WSRoot(object): + """ + Root controller for webservices. + + :param protocols: A list of protocols to enable (see :meth:`addprotocol`) + :param webpath: The web path where the webservice is published. + """ + def __init__(self, protocols=[], webpath=''): + self._debug = True + self._webpath = webpath + self.protocols = [] + for protocol in protocols: + self.addprotocol(protocol) + + self._api = None + + def addprotocol(self, protocol, **options): + """ + Enable a new protocol on the controller. + + :param protocol: A registered protocol name or an instance + of a protocol. + """ + if isinstance(protocol, str): + protocol = getprotocol(protocol, **options) + self.protocols.append(protocol) + protocol.root = weakref.proxy(self) + + def getapi(self): + """ + Returns the api description. + + :rtype: list of (path, :class:`FunctionDefinition`) + """ + if self._api is None: + self._api = [i for i in scan_api(self)] + return self._api + + def _get_protocol(self, name): + for protocol in self.protocols: + if protocol.name == name: + return protocol + + def _select_protocol(self, request): + log.debug("Selecting a protocol for the following request :\n" + "headers: %s\nbody: %s", request.headers, + len(request.body) > 512 + and request.body[:512] + or request.body) + protocol = None + if 'wsmeproto' in request.params: + return self._get_protocol(request.params['wsmeproto']) + else: + + for p in self.protocols: + if p.accept(request): + protocol = p + break + return protocol + + def _do_call(self, protocol, context): + request = context.request + request.calls.append(context) + try: + if context.path is None: + context.path = protocol.extract_path(context) + + if context.path is None: + raise exc.ClientSideError( + u'The %s protocol was unable to extract a function ' + u'path from the request' % protocol.name) + + context.func, context.funcdef = self._lookup_function(context.path) + kw = protocol.read_arguments(context) + + for arg in context.funcdef.arguments: + if arg.mandatory and arg.name not in kw: + raise exc.MissingArgument(arg.name) + + result = context.func(**kw) + + 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) + + except Exception, e: + infos = self._format_exception(sys.exc_info()) + if isinstance(e, exc.ClientSideError): + request.client_errorcount += 1 + else: + request.server_errorcount += 1 + return protocol.encode_error(context, infos) + + def _handle_request(self, request): + def default_prepare_response_body(request, results): + return '\n'.join(results) + + res = webob.Response() + res_content_type = None + try: + protocol = self._select_protocol(request) + if protocol is None: + msg = ("None of the following protocols can handle this " + "request : %s" % ','.join( + (p.name for p in self.protocols))) + res.status = 500 + res.content_type = 'text/plain' + res.body = msg + log.error(msg) + return res + + context = None + + request.calls = [] + request.client_errorcount = 0 + request.server_errorcount = 0 + + if hasattr(protocol, 'prepare_response_body'): + prepare_response_body = protocol.prepare_response_body + else: + prepare_response_body = default_prepare_response_body + + body = prepare_response_body(request, ( + self._do_call(protocol, context) + for context in protocol.iter_calls(request))) + if isinstance(body, unicode): + res.unicode_body = body + else: + res.body = body + + if len(request.calls) == 1: + if hasattr(protocol, 'get_response_status'): + res.status = protocol.get_response_status(request) + else: + if request.client_errorcount: + res.status = 400 + elif request.server_errorcount: + 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) + except Exception, e: + infos = self._format_exception(sys.exc_info()) + request.server_errorcount += 1 + res.body = protocol.encode_error(context, infos) + res.status = 500 + + if res_content_type is None: + # Attempt to correctly guess what content-type we should return. + last_q = 0 + ctypes = [ct for ct in protocol.content_types if ct] + if ctypes: + res_content_type = request.accept.best_match(ctypes) + + # If not we will attempt to convert the body to an accepted + # output format. + if res_content_type is None: + if "text/html" in request.accept: + res.body = self._html_format(res.body, protocol.content_types) + res_content_type = "text/html" + + # TODO should we consider the encoding asked by + # the web browser ? + res.headers['Content-Type'] = "%s; charset=UTF-8" % res_content_type + + return res + + def _lookup_function(self, path): + a = self + + isprotocol_specific = path[0] == '_protocol' + + if isprotocol_specific: + a = self._get_protocol(path[1]) + path = path[2:] + + for name in path: + a = getattr(a, name, None) + if a is None: + break + + if not hasattr(a, '_wsme_definition'): + raise exc.UnknownFunction('/'.join(path)) + + definition = a._wsme_definition + + return a, definition + + def _format_exception(self, excinfo): + """Extract informations that can be sent to the client.""" + error = excinfo[1] + if isinstance(error, exc.ClientSideError): + r = dict(faultcode="Client", + faultstring=error.faultstring) + log.warning("Client-side error: %s" % r['faultstring']) + r['debuginfo'] = None + return r + else: + faultstring = str(error) + debuginfo = "\n".join(traceback.format_exception(*excinfo)) + + log.error('Server-side error: "%s". Detail: \n%s' % ( + faultstring, debuginfo)) + + r = dict(faultcode="Server", faultstring=faultstring) + if self._debug: + r['debuginfo'] = debuginfo + else: + r['debuginfo'] = None + return r + + def _html_format(self, content, content_types): + try: + from pygments import highlight + from pygments.lexers import get_lexer_for_mimetype + from pygments.formatters import HtmlFormatter + + lexer = None + for ct in content_types: + try: + lexer = get_lexer_for_mimetype(ct) + break + except: + pass + + if lexer is None: + raise ValueError("No lexer found") + formatter = HtmlFormatter() + return html_body % dict( + css=formatter.get_style_defs(), + content=highlight(content, lexer, formatter).encode('utf8')) + except Exception, e: + log.warning( + "Could not pygment the content because of the following " + "error :\n%s" % e) + return html_body % dict( + css='', + content='
%s
' % + content.replace('>', '>').replace('<', '<')) diff --git a/wsme/tests/test_controller.py b/wsme/tests/test_api.py similarity index 79% rename from wsme/tests/test_controller.py rename to wsme/tests/test_api.py index 9880f4b..2e09aa7 100644 --- a/wsme/tests/test_controller.py +++ b/wsme/tests/test_api.py @@ -6,47 +6,12 @@ from webob.dec import wsgify import webtest from wsme import * -from wsme.controller import getprotocol, scan_api, pexpose -from wsme.controller import FunctionArgument, FunctionDefinition, CallContext +from wsme.api import scan_api, pexpose +from wsme.api import FunctionArgument, FunctionDefinition from wsme.types import iscomplex import wsme.wsgi - -class DummyProtocol(object): - name = 'dummy' - content_types = ['', None] - - def __init__(self): - self.hits = 0 - - def accept(self, req): - return True - - def iter_calls(self, req): - yield CallContext(req) - - def extract_path(self, context): - return ['touch'] - - def read_arguments(self, context): - self.lastreq = context.request.__init__.__self__ - self.hits += 1 - return {} - - def encode_result(self, context, result): - return str(result) - - def encode_error(self, context, infos): - return str(infos) - - -def test_getprotocol(): - try: - getprotocol('invalid') - assert False, "ValueError was not raised" - except ValueError, e: - pass - +from wsme.tests.test_protocols import DummyProtocol def test_pexpose(): class Proto(DummyProtocol): @@ -124,22 +89,6 @@ class TestController(unittest.TestCase): assert iscomplex(ComplexType) - def test_register_protocol(self): - import wsme.controller - wsme.controller.register_protocol(DummyProtocol) - assert wsme.controller.registered_protocols['dummy'] == DummyProtocol - - r = WSRoot() - assert len(r.protocols) == 0 - - r.addprotocol('dummy') - assert len(r.protocols) == 1 - assert r.protocols[0].__class__ == DummyProtocol - - r = WSRoot(['dummy']) - assert len(r.protocols) == 1 - assert r.protocols[0].__class__ == DummyProtocol - def test_scan_api(self): class NS(object): @expose(int) diff --git a/wsme/tests/test_protocols.py b/wsme/tests/test_protocols.py new file mode 100644 index 0000000..dadd8ee --- /dev/null +++ b/wsme/tests/test_protocols.py @@ -0,0 +1,60 @@ +# encoding=utf8 + +import unittest + +from wsme import WSRoot +from wsme.protocols import getprotocol, CallContext +import wsme.protocols + +class DummyProtocol(object): + name = 'dummy' + content_types = ['', None] + + def __init__(self): + self.hits = 0 + + def accept(self, req): + return True + + def iter_calls(self, req): + yield CallContext(req) + + def extract_path(self, context): + return ['touch'] + + def read_arguments(self, context): + self.lastreq = context.request.__init__.__self__ + self.hits += 1 + return {} + + def encode_result(self, context, result): + return str(result) + + def encode_error(self, context, infos): + return str(infos) + + +def test_getprotocol(): + try: + getprotocol('invalid') + assert False, "ValueError was not raised" + except ValueError, e: + pass + +class TestProtocols(unittest.TestCase): + def test_register_protocol(self): + wsme.protocols.register_protocol(DummyProtocol) + assert wsme.protocols.registered_protocols['dummy'] == DummyProtocol + + r = WSRoot() + assert len(r.protocols) == 0 + + r.addprotocol('dummy') + assert len(r.protocols) == 1 + assert r.protocols[0].__class__ == DummyProtocol + + r = WSRoot(['dummy']) + assert len(r.protocols) == 1 + assert r.protocols[0].__class__ == DummyProtocol + +