Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -17,3 +17,4 @@ test
nosetests.xml
man
.channel
*.komodoproject
89 changes: 42 additions & 47 deletions cornice/__init__.py
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
6 changes: 6 additions & 0 deletions cornice/interfaces.py
@@ -0,0 +1,6 @@
from zope.interface import Interface


class IService(Interface):
"""Cornice service interface
"""
23 changes: 13 additions & 10 deletions cornice/service.py
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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 = []
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions cornice/sphinxext.py
Expand Up @@ -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
Expand Down Expand Up @@ -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()]
Expand Down
23 changes: 0 additions & 23 deletions cornice/static/cornice.css

This file was deleted.

Binary file removed cornice/static/favicon.ico
Binary file not shown.
Binary file removed cornice/static/footerbg.png
Binary file not shown.
Binary file removed cornice/static/headerbg.png
Binary file not shown.
8 changes: 0 additions & 8 deletions cornice/static/ie6.css

This file was deleted.

Binary file removed cornice/static/middlebg.png
Binary file not shown.
65 changes: 0 additions & 65 deletions cornice/static/pylons.css

This file was deleted.

Binary file removed cornice/static/pyramid-small.png
Binary file not shown.
Binary file removed cornice/static/pyramid.png
Binary file not shown.
Binary file removed cornice/static/transparent.gif
Binary file not shown.
8 changes: 4 additions & 4 deletions cornice/tests/test_resource.py
Expand Up @@ -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
Expand All @@ -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'}})
12 changes: 6 additions & 6 deletions cornice/tests/test_service_definition.py
Expand Up @@ -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
Expand All @@ -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'}})
4 changes: 2 additions & 2 deletions cornice/tests/test_service_description.py
Expand Up @@ -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"}})