fix to make the mechanism pulling docstrings from validators in work #44

Closed
wants to merge 5 commits into
from
View
@@ -17,3 +17,4 @@ test
nosetests.xml
man
.channel
+*.komodoproject
View
@@ -4,22 +4,19 @@
import json
import logging
-from pyramid.events import BeforeRender, NewRequest
+from pyramid.events import NewRequest
from pyramid.httpexceptions import HTTPNotFound, HTTPMethodNotAllowed
from pyramid.exceptions import PredicateMismatch
from cornice import util
from cornice.errors import Errors
from cornice.service import Service # NOQA
+from cornice.interfaces import IService
logger = logging.getLogger('cornice')
-def add_renderer_globals(event):
- event['util'] = util
-
-
def wrap_request(event):
"""Adds a "validated" dict, a custom "errors" object and an "info" dict to
the request object if they don't already exists
@@ -42,56 +39,55 @@ def add_apidoc(config, pattern, func, service, **kwargs):
info['func'] = func
+def get_service(request):
+ if getattr(request, 'matched_route'):
+ return request.registry.queryUtility(IService, name=request.matched_route.pattern)
+
+
def tween_factory(handler, registry):
"""Wraps the default WSGI workflow to provide cornice utilities"""
def cornice_tween(request):
response = handler(request)
- if request.matched_route is not None:
- # do some sanity checking on the response using filters
- pattern = request.matched_route.pattern
- service = request.registry['cornice_services'].get(pattern)
- if service is not None:
- if request.method not in service.defined_methods:
- response = HTTPMethodNotAllowed()
- response.allow = service.defined_methods
- else:
- # get the filters for this call
- kwargs = service.definitions[request.method]
- for _filter in kwargs.get('filters', []):
- response = _filter(response)
+ service = get_service(request)
+ if service is not None:
+ if request.method not in service.defined_methods:
+ response = HTTPMethodNotAllowed()
+ response.allow = service.defined_methods
+ else:
+ # get the filters for this call
+ kwargs = service.definitions[request.method]
+ for _filter in kwargs.get('filters', []):
+ response = _filter(response)
return response
return cornice_tween
def _notfound(request):
- match = request.matchdict
- if match is not None:
- pattern = request.matched_route.pattern
- service = request.registry['cornice_services'].get(pattern)
- if (service is not None
- and isinstance(request.exception, PredicateMismatch)
- and request.method in service.defined_methods):
- # maybe was it the accept predicate that was not matched
- # in this case, returns a HTTP 406 NOT ACCEPTABLE with the
- # list of available choices
- api_kwargs = service.definitions[request.method]
- if 'accept' in api_kwargs:
- accept = api_kwargs.get('accept')
- acceptable = [a for a in util.to_list(accept) if
- not callable(a)]
-
- if 'acceptable' in request.info:
- for content_type in request.info['acceptable']:
- if content_type not in acceptable:
- acceptable.append(content_type)
-
- if not request.accept.best_match(acceptable):
- # if not, return the list of accepted headers
- resp = request.response
- resp.status = 406
- resp.content_type = "application/json"
- resp.body = json.dumps(acceptable)
- return resp
+ service = get_service(request)
+ if (service is not None
+ and isinstance(request.exception, PredicateMismatch)
+ and request.method in service.defined_methods):
+ # maybe was it the accept predicate that was not matched
+ # in this case, returns a HTTP 406 NOT ACCEPTABLE with the
+ # list of available choices
+ api_kwargs = service.definitions[request.method]
+ if 'accept' in api_kwargs:
+ accept = api_kwargs.get('accept')
+ acceptable = [a for a in util.to_list(accept) if
+ not callable(a)]
+
+ if 'acceptable' in request.info:
+ for content_type in request.info['acceptable']:
+ if content_type not in acceptable:
+ acceptable.append(content_type)
+
+ if not request.accept.best_match(acceptable):
+ # if not, return the list of accepted headers
+ resp = request.response
+ resp.status = 406
+ resp.content_type = "application/json"
+ resp.body = json.dumps(acceptable)
+ return resp
# 404
return request.exception
@@ -101,7 +97,6 @@ def includeme(config):
"""
config.add_directive('add_apidoc', add_apidoc)
config.add_view(_notfound, context=HTTPNotFound)
- config.add_subscriber(add_renderer_globals, BeforeRender)
config.add_subscriber(wrap_request, NewRequest)
config.add_tween('cornice.tween_factory')
config.add_renderer('simplejson', util.json_renderer)
View
@@ -0,0 +1,6 @@
+from zope.interface import Interface
+
+
+class IService(Interface):
+ """Cornice service interface
+ """
View
@@ -5,12 +5,14 @@
import functools
import venusian
+from zope.interface import implements
+from cornice.interfaces import IService
from cornice.util import to_list, json_error, match_accept_header
from cornice.validators import (
- DEFAULT_VALIDATORS,
- DEFAULT_FILTERS,
- validate_colander_schema
+ DEFAULT_VALIDATORS,
+ DEFAULT_FILTERS,
+ validate_colander_schema
)
from cornice.schemas import CorniceSchema
@@ -24,10 +26,11 @@ def call_service(func, api_kwargs, context, request):
# apply validators
for validator in api_kwargs.get('validators', []):
validator(request)
- if len(request.errors) > 0:
- return json_error(request.errors)
+
+ if len(request.errors) > 0:
+ return json_error(request.errors)
- return func(request)
+ return dict(result=func(request), status='ok')
class Service(object):
@@ -68,6 +71,7 @@ class Service(object):
http://readthedocs.org/docs/pyramid/en/1.0-branch/glossary.html#term-acl
for more information about ACLs.
"""
+ implements(IService)
def __init__(self, **kw):
self.defined_methods = []
@@ -91,14 +95,13 @@ def __repr__(self):
self.route_name)
def _define(self, config, method):
- # setup the services hash if it isn't already
- services = config.registry.setdefault('cornice_services', {})
if self.index == -1:
+ services = list(config.registry.getUtilitiesFor(IService))
self.index = len(services)
# define the route if it isn't already
- if self.route_pattern not in services:
- services[self.route_pattern] = self
+ if not config.registry.queryUtility(IService, name=self.route_pattern):
+ config.registry.registerUtility(self, IService, name=self.route_pattern)
route_kw = {}
if self.factory is not None:
route_kw["factory"] = self.factory
View
@@ -42,7 +42,10 @@ def trim(docstring):
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
- return '\n'.join(trimmed)
+ res = '\n'.join(trimmed)
+ if not isinstance(res, unicode):
+ res = res.decode('utf8')
+ return res
from sphinx.locale import l_
from sphinx.util.docfields import Field, GroupedField, TypedField
@@ -200,7 +203,7 @@ def run(self):
# we want to list all of them
services_id = "services-%d" % env.new_serialno('services')
services_node = nodes.section(ids=[services_id])
- services_node += nodes.title(text='Services')
+ services_node += nodes.title(text=pkg)
services_ = [(service.index, path, service, methods) \
for (path, service), methods in services.items()]
View
@@ -1,23 +0,0 @@
-div.resource {
- padding: 10px;
- margin: 5px;
- background-color: #D0D0D0;
- -moz-border-radius: 15px;
- border-radius: 15px;
- width: 90%
-}
-
-div.resource-title {
- margin-bottom: 5px;
- font-size: 120%;
-}
-
-div.resource-renderer {
- float: right;
- background-color: #FF9933;
- font-weight: bold;
- -moz-border-radius: 10px;
- border-radius: 10px;
- color: white;
- padding: 6px;
-}
View
Binary file not shown.
View
Deleted file not rendered
View
Deleted file not rendered
View
@@ -1,8 +0,0 @@
-* html img,
-* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none",
-this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')",
-this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''),
-this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')",
-this.runtimeStyle.backgroundImage = "none")),this.pngSet=true)
-);}
-#wrap{display:table;height:100%}
View
Deleted file not rendered
View
@@ -1,65 +0,0 @@
-html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */
-vertical-align:baseline;background:transparent;}
-body{line-height:1;}
-ol,ul{list-style:none;}
-blockquote,q{quotes:none;}
-blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;}
-:focus{outline:0;}
-ins{text-decoration:none;}
-del{text-decoration:line-through;}
-table{border-collapse:collapse;border-spacing:0;}
-sub{vertical-align:sub;font-size:smaller;line-height:normal;}
-sup{vertical-align:super;font-size:smaller;line-height:normal;}
-ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;}
-ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;}
-li{display:list-item;}
-ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;}
-ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;}
-ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;}
-.hidden{display:none;}
-p{line-height:1.5em;}
-h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;}
-h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;}
-h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;}
-h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;}
-html,body{width:100%;height:100%;}
-body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "NobileRegular","Lucida Grande",Lucida,Verdana,sans-serif;}
-a{color:#1b61d6;text-decoration:none;}
-a:hover{color:#e88f00;text-decoration:underline;}
-body h1,
-body h2,
-body h3,
-body h4,
-body h5,
-body h6{font-family:"NeutonRegular","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;}
-#wrap{min-height:100%;}
-#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;}
-#header{background:#000000;top:0;font-size:14px;}
-#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;}
-.header,.footer{width:750px;margin-right:auto;margin-left:auto;}
-.wrapper{width:100%}
-#top,#top-small,#bottom{width:100%;}
-#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;}
-#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;}
-#bottom{color:#222;background-color:#ffffff;}
-.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;}
-.top{padding-top:40px;}
-.top-small{padding-top:10px;}
-#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;}
-.app-welcome{margin-top:25px;}
-.app-name{color:#000000;font-weight:bold;}
-.bottom{padding-top:50px;}
-#left{width:350px;float:left;padding-right:25px;}
-#right{width:350px;float:right;padding-left:25px;}
-.align-left{text-align:left;}
-.align-right{text-align:right;}
-.align-center{text-align:center;}
-ul.links{margin:0;padding:0;}
-ul.links li{list-style-type:none;font-size:14px;}
-form{border-style:none;}
-fieldset{border-style:none;}
-input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;}
-input[type=text],input[type=password]{width:205px;}
-input[type=submit]{background-color:#ddd;font-weight:bold;}
-/*Opera Fix*/
-body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;}
Deleted file not rendered
View
Deleted file not rendered
Deleted file not rendered
@@ -53,14 +53,14 @@ def test_basic_resource(self):
self.assertEquals(
self.app.get("/users").json,
- {'users': [1, 2]})
+ {'status': 'ok', 'result': {'users': [1, 2]}})
self.assertEquals(
self.app.get("/users/1").json,
- {'name': 'gawel'})
+ {'status': 'ok', 'result': {'name': 'gawel'}})
resp = self.app.get("/users/1?callback=test")
self.assertEquals(resp.body,
- 'test({"name": "gawel"})', resp.body)
+ 'test({"status": "ok", "result": {"name": "gawel"}})', resp.body)
def test_accept_headers(self):
# the accept headers should work even in case they're specified in a
@@ -69,4 +69,4 @@ def test_accept_headers(self):
self.app.post("/users",
headers={'Accept': 'text/json'},
params=json.dumps({'test': 'yeah'})).json,
- {'test': 'yeah'})
+ {'status': 'ok', 'result': {'test': 'yeah'}})
@@ -47,11 +47,11 @@ def test_basic_service_operation(self):
self.app.get("/unknown", status=404)
self.assertEquals(
self.app.get("/service1").json,
- {'test': "succeeded"})
+ {'status': 'ok', 'result': {'test': "succeeded"}})
self.assertEquals(
self.app.post("/service1", params="BODY").json,
- {'body': 'BODY'})
+ {'status': 'ok', 'result': {'body': 'BODY'}})
def test_loading_into_multiple_configurators(self):
# When initializing a second configurator, it shouldn't interfere
@@ -63,17 +63,17 @@ def test_loading_into_multiple_configurators(self):
# Calling the new configurator works as expected.
app = TestApp(CatchErrors(config2.make_wsgi_app()))
self.assertEqual(app.get("/service1").json,
- {'test': 'succeeded'})
+ {'status': 'ok', 'result': {'test': 'succeeded'}})
# Calling the old configurator works as expected.
self.assertEqual(self.app.get("/service1").json,
- {'test': 'succeeded'})
+ {'status': 'ok', 'result': {'test': 'succeeded'}})
def test_stacking_api_decorators(self):
# Stacking multiple @api calls on a single function should
# register it multiple times, just like @view_config does.
resp = self.app.get("/service2", headers={'Accept': 'text/html'})
- self.assertEquals(resp.json, {'test': 'succeeded'})
+ self.assertEquals(resp.json, {'status': 'ok', 'result': {'test': 'succeeded'}})
resp = self.app.post("/service2", headers={'Accept': 'audio/ogg'})
- self.assertEquals(resp.json, {'test': 'succeeded'})
+ self.assertEquals(resp.json, {'status': 'ok', 'result': {'test': 'succeeded'}})
@@ -122,9 +122,9 @@ def test_schema_validation(self):
resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
status=200)
- self.assertEquals(resp.json, {"test": "succeeded"})
+ self.assertEquals(resp.json, {'status': 'ok', 'result': {"test": "succeeded"}})
def test_schema_validation2(self):
resp = self.app.get('/foobar?yeah=test', status=200)
- self.assertEquals(resp.json, {"test": "succeeded"})
+ self.assertEquals(resp.json, {'status': 'ok', 'result': {"test": "succeeded"}})
Oops, something went wrong.