diff options
-rw-r--r-- | MANIFEST.in | 1 | ||||
-rw-r--r-- | docs/source/hooks.rst | 4 | ||||
-rw-r--r-- | pecan/core.py | 28 | ||||
-rw-r--r-- | pecan/hooks.py | 11 | ||||
-rw-r--r-- | pecan/rest.py | 28 | ||||
-rw-r--r-- | pecan/tests/test_hooks.py | 409 | ||||
-rw-r--r-- | pecan/tests/test_rest.py | 123 | ||||
-rw-r--r-- | setup.cfg | 2 |
8 files changed, 576 insertions, 30 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index 79e6393..bcffd6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ recursive-include pecan/scaffolds/rest-api * include pecan/scaffolds/rest-api/* include pecan/middleware/resources/* include LICENSE README.rst requirements.txt +recursive-include pecan/tests * diff --git a/docs/source/hooks.rst b/docs/source/hooks.rst index 1cdb4ef..3b9d364 100644 --- a/docs/source/hooks.rst +++ b/docs/source/hooks.rst @@ -71,6 +71,10 @@ response objects, and which controller was selected by Pecan's routing:: # and used to generate the response body # assert state.controller.__func__ is RootController.index.__func__ + assert isinstance(state.arguments, inspect.Arguments) + print state.arguments.args + print state.arguments.varargs + print state.arguments.keywords assert isinstance(state.request, webob.Request) assert isinstance(state.response, webob.Response) assert isinstance(state.hooks, list) diff --git a/pecan/core.py b/pecan/core.py index 29de33a..a52dae2 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -2,6 +2,7 @@ try: from simplejson import dumps, loads except ImportError: # pragma: no cover from json import dumps, loads # noqa +from inspect import Arguments from itertools import chain, tee from mimetypes import guess_type, add_type from os.path import splitext @@ -31,12 +32,14 @@ logger = logging.getLogger(__name__) class RoutingState(object): - def __init__(self, request, response, app, hooks=[], controller=None): + def __init__(self, request, response, app, hooks=[], controller=None, + arguments=None): self.request = request self.response = response self.app = app self.hooks = hooks self.controller = controller + self.arguments = arguments class Request(WebObRequest): @@ -326,6 +329,7 @@ class PecanBase(object): passed the argument specification for the controller. ''' args = [] + varargs = [] kwargs = dict() valid_args = argspec.args[1:] # pop off `self` pecan_state = state.request.pecan @@ -354,7 +358,7 @@ class PecanBase(object): if [i for i in remainder if i]: if not argspec[1]: abort(404) - args.extend(remainder) + varargs.extend(remainder) # get the default positional arguments if argspec[3]: @@ -377,7 +381,7 @@ class PecanBase(object): if name not in argspec[0]: kwargs[encode_if_needed(name)] = value - return args, kwargs + return args, varargs, kwargs def render(self, template, namespace): renderer = self.renderers.get( @@ -492,9 +496,6 @@ class PecanBase(object): ) raise exc.HTTPNotFound - # handle "before" hooks - self.handle_hooks(self.determine_hooks(controller), 'before', state) - # fetch any parameters if req.method == 'GET': params = dict(req.GET) @@ -502,15 +503,19 @@ class PecanBase(object): params = dict(req.params) # fetch the arguments for the controller - args, kwargs = self.get_args( + args, varargs, kwargs = self.get_args( state, params, remainder, cfg['argspec'], im_self ) + state.arguments = Arguments(args, varargs, kwargs) + + # handle "before" hooks + self.handle_hooks(self.determine_hooks(controller), 'before', state) - return controller, args, kwargs + return controller, args+varargs, kwargs def invoke_controller(self, controller, args, kwargs, state): ''' @@ -697,11 +702,11 @@ class ExplicitPecan(PecanBase): except IndexError: raise signature_error - args, kwargs = super(ExplicitPecan, self).get_args( + args, varargs, kwargs = super(ExplicitPecan, self).get_args( state, all_params, remainder, argspec, im_self ) args = [state.request, state.response] + args - return args, kwargs + return args, varargs, kwargs class Pecan(PecanBase): @@ -753,12 +758,14 @@ class Pecan(PecanBase): state.hooks = [] state.app = self state.controller = None + state.arguments = None return super(Pecan, self).__call__(environ, start_response) finally: del state.hooks del state.request del state.response del state.controller + del state.arguments del state.app def init_context_local(self, local_factory): @@ -772,6 +779,7 @@ class Pecan(PecanBase): state.response = _state.response controller, args, kw = super(Pecan, self).find_controller(_state) state.controller = controller + state.arguments = _state.arguments return controller, args, kw def handle_hooks(self, hooks, *args, **kw): diff --git a/pecan/hooks.py b/pecan/hooks.py index 57392d7..f1f7073 100644 --- a/pecan/hooks.py +++ b/pecan/hooks.py @@ -1,3 +1,4 @@ +import types import sys from inspect import getmembers @@ -27,7 +28,15 @@ def walk_controller(root_class, controller, hooks): for hook in hooks: value._pecan.setdefault('hooks', set()).add(hook) elif hasattr(value, '__class__'): - if name.startswith('__') and name.endswith('__'): + # Skip non-exposed methods that are defined in parent classes; + # they're internal implementation details of that class, and + # not actual routable controllers, so we shouldn't bother + # assigning hooks to them. + if ( + isinstance(value, types.MethodType) and + any(filter(lambda c: value.__func__ in c.__dict__.values(), + value.im_class.mro()[1:])) + ): continue walk_controller(root_class, value, hooks) diff --git a/pecan/rest.py b/pecan/rest.py index e78d287..a6feae2 100644 --- a/pecan/rest.py +++ b/pecan/rest.py @@ -43,6 +43,19 @@ class RestController(object): return argspec.args[3:] return argspec.args[1:] + def _handle_bad_rest_arguments(self, controller, remainder, request): + """ + Ensure that the argspec for a discovered controller actually matched + the positional arguments in the request path. If not, raise + a webob.exc.HTTPBadRequest. + """ + argspec = self._get_args_for_controller(controller) + fixed_args = len(argspec) - len( + request.pecan.get('routing_args', []) + ) + if len(remainder) < fixed_args: + abort(400) + @expose() def _route(self, args, request=None): ''' @@ -89,10 +102,10 @@ class RestController(object): _lookup_result = self._handle_lookup(args, request) if _lookup_result: return _lookup_result - except exc.HTTPNotFound: + except (exc.HTTPClientError, exc.HTTPNotFound): # - # If the matching handler results in a 404, attempt to handle - # a _lookup method (if it exists) + # If the matching handler results in a 400 or 404, attempt to + # handle a _lookup method (if it exists) # _lookup_result = self._handle_lookup(args, request) if _lookup_result: @@ -201,14 +214,10 @@ class RestController(object): # route to a get_all or get if no additional parts are available if not remainder or remainder == ['']: + remainder = list(six.moves.filter(bool, remainder)) controller = self._find_controller('get_all', 'get') if controller: - argspec = self._get_args_for_controller(controller) - fixed_args = len(argspec) - len( - request.pecan.get('routing_args', []) - ) - if len(remainder) < fixed_args: - abort(404) + self._handle_bad_rest_arguments(controller, remainder, request) return controller, [] abort(404) @@ -232,6 +241,7 @@ class RestController(object): # finally, check for the regular get_one/get requests controller = self._find_controller('get_one', 'get') if controller: + self._handle_bad_rest_arguments(controller, remainder, request) return controller, remainder abort(404) diff --git a/pecan/tests/test_hooks.py b/pecan/tests/test_hooks.py index 8f1ca39..d3fe05b 100644 --- a/pecan/tests/test_hooks.py +++ b/pecan/tests/test_hooks.py @@ -1,11 +1,13 @@ +import inspect +import operator + from webtest import TestApp +from six import PY3 from six import b as b_ from six import u as u_ from six.moves import cStringIO as StringIO -from webob import Response - -from pecan import make_app, expose, redirect, abort +from pecan import make_app, expose, redirect, abort, rest, Request, Response from pecan.hooks import ( PecanHook, TransactionHook, HookController, RequestViewerHook ) @@ -13,6 +15,9 @@ from pecan.configuration import Config from pecan.decorators import transactional, after_commit, after_rollback from pecan.tests import PecanTestCase +# The `inspect.Arguments` namedtuple is different between PY2/3 +kwargs = operator.attrgetter('varkw' if PY3 else 'keywords') + class TestHooks(PecanTestCase): @@ -412,6 +417,364 @@ class TestHooks(PecanTestCase): assert run_hook[3] == 'last - before hook', run_hook[3] +class TestStateAccess(PecanTestCase): + + def setUp(self): + super(TestStateAccess, self).setUp() + self.args = None + + class RootController(object): + @expose() + def index(self): + return 'Hello, World!' + + @expose() + def greet(self, name): + return 'Hello, %s!' % name + + @expose() + def greetmore(self, *args): + return 'Hello, %s!' % args[0] + + @expose() + def kwargs(self, **kw): + return 'Hello, %s!' % kw['name'] + + @expose() + def mixed(self, first, second, *args): + return 'Mixed' + + class SimpleHook(PecanHook): + def before(inself, state): + self.args = (state.controller, state.arguments) + + self.root = RootController() + self.app = TestApp(make_app(self.root, hooks=[SimpleHook()])) + + def test_no_args(self): + self.app.get('/') + assert self.args[0] == self.root.index + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_single_arg(self): + self.app.get('/greet/joe') + assert self.args[0] == self.root.greet + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['joe'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_single_vararg(self): + self.app.get('/greetmore/joe') + assert self.args[0] == self.root.greetmore + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == ['joe'] + assert kwargs(self.args[1]) == {} + + def test_single_kw(self): + self.app.get('/kwargs/?name=joe') + assert self.args[0] == self.root.kwargs + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'name': 'joe'} + + def test_single_kw_post(self): + self.app.post('/kwargs/', params={'name': 'joe'}) + assert self.args[0] == self.root.kwargs + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'name': 'joe'} + + def test_mixed_args(self): + self.app.get('/mixed/foo/bar/spam/eggs') + assert self.args[0] == self.root.mixed + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['foo', 'bar'] + assert self.args[1].varargs == ['spam', 'eggs'] + + +class TestStateAccessWithoutThreadLocals(PecanTestCase): + + def setUp(self): + super(TestStateAccessWithoutThreadLocals, self).setUp() + self.args = None + + class RootController(object): + @expose() + def index(self, req, resp): + return 'Hello, World!' + + @expose() + def greet(self, req, resp, name): + return 'Hello, %s!' % name + + @expose() + def greetmore(self, req, resp, *args): + return 'Hello, %s!' % args[0] + + @expose() + def kwargs(self, req, resp, **kw): + return 'Hello, %s!' % kw['name'] + + @expose() + def mixed(self, req, resp, first, second, *args): + return 'Mixed' + + class SimpleHook(PecanHook): + def before(inself, state): + self.args = (state.controller, state.arguments) + + self.root = RootController() + self.app = TestApp(make_app( + self.root, + hooks=[SimpleHook()], + use_context_locals=False + )) + + def test_no_args(self): + self.app.get('/') + assert self.args[0] == self.root.index + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 2 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_single_arg(self): + self.app.get('/greet/joe') + assert self.args[0] == self.root.greet + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 3 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].args[2] == 'joe' + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_single_vararg(self): + self.app.get('/greetmore/joe') + assert self.args[0] == self.root.greetmore + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 2 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].varargs == ['joe'] + assert kwargs(self.args[1]) == {} + + def test_single_kw(self): + self.app.get('/kwargs/?name=joe') + assert self.args[0] == self.root.kwargs + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 2 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'name': 'joe'} + + def test_single_kw_post(self): + self.app.post('/kwargs/', params={'name': 'joe'}) + assert self.args[0] == self.root.kwargs + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 2 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'name': 'joe'} + + def test_mixed_args(self): + self.app.get('/mixed/foo/bar/spam/eggs') + assert self.args[0] == self.root.mixed + assert isinstance(self.args[1], inspect.Arguments) + assert len(self.args[1].args) == 4 + assert isinstance(self.args[1].args[0], Request) + assert isinstance(self.args[1].args[1], Response) + assert self.args[1].args[2:] == ['foo', 'bar'] + assert self.args[1].varargs == ['spam', 'eggs'] + + +class TestRestControllerStateAccess(PecanTestCase): + + def setUp(self): + super(TestRestControllerStateAccess, self).setUp() + self.args = None + + class RootController(rest.RestController): + + @expose() + def _default(self, _id, *args, **kw): + return 'Default' + + @expose() + def get_all(self, **kw): + return 'All' + + @expose() + def get_one(self, _id, *args, **kw): + return 'One' + + @expose() + def post(self, *args, **kw): + return 'POST' + + @expose() + def put(self, _id, *args, **kw): + return 'PUT' + + @expose() + def delete(self, _id, *args, **kw): + return 'DELETE' + + class SimpleHook(PecanHook): + def before(inself, state): + self.args = (state.controller, state.arguments) + + self.root = RootController() + self.app = TestApp(make_app(self.root, hooks=[SimpleHook()])) + + def test_get_all(self): + self.app.get('/') + assert self.args[0] == self.root.get_all + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_get_all_with_kwargs(self): + self.app.get('/?foo=bar') + assert self.args[0] == self.root.get_all + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'foo': 'bar'} + + def test_get_one(self): + self.app.get('/1') + assert self.args[0] == self.root.get_one + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_get_one_with_varargs(self): + self.app.get('/1/2/3') + assert self.args[0] == self.root.get_one + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == ['2', '3'] + assert kwargs(self.args[1]) == {} + + def test_get_one_with_kwargs(self): + self.app.get('/1?foo=bar') + assert self.args[0] == self.root.get_one + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'foo': 'bar'} + + def test_post(self): + self.app.post('/') + assert self.args[0] == self.root.post + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_post_with_varargs(self): + self.app.post('/foo/bar') + assert self.args[0] == self.root.post + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == ['foo', 'bar'] + assert kwargs(self.args[1]) == {} + + def test_post_with_kwargs(self): + self.app.post('/', params={'foo': 'bar'}) + assert self.args[0] == self.root.post + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == [] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'foo': 'bar'} + + def test_put(self): + self.app.put('/1') + assert self.args[0] == self.root.put + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_put_with_method_argument(self): + self.app.post('/1?_method=put') + assert self.args[0] == self.root.put + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'_method': 'put'} + + def test_put_with_varargs(self): + self.app.put('/1/2/3') + assert self.args[0] == self.root.put + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == ['2', '3'] + assert kwargs(self.args[1]) == {} + + def test_put_with_kwargs(self): + self.app.put('/1?foo=bar') + assert self.args[0] == self.root.put + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'foo': 'bar'} + + def test_delete(self): + self.app.delete('/1') + assert self.args[0] == self.root.delete + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {} + + def test_delete_with_method_argument(self): + self.app.post('/1?_method=delete') + assert self.args[0] == self.root.delete + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'_method': 'delete'} + + def test_delete_with_varargs(self): + self.app.delete('/1/2/3') + assert self.args[0] == self.root.delete + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == ['2', '3'] + assert kwargs(self.args[1]) == {} + + def test_delete_with_kwargs(self): + self.app.delete('/1?foo=bar') + assert self.args[0] == self.root.delete + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'foo': 'bar'} + + def test_post_with_invalid_method_kwarg(self): + self.app.post('/1?_method=invalid') + assert self.args[0] == self.root._default + assert isinstance(self.args[1], inspect.Arguments) + assert self.args[1].args == ['1'] + assert self.args[1].varargs == [] + assert kwargs(self.args[1]) == {'_method': 'invalid'} + + class TestTransactionHook(PecanTestCase): def test_transaction_hook(self): run_hook = [] @@ -1293,3 +1656,43 @@ class TestRequestViewerHook(PecanTestCase): viewer = RequestViewerHook(conf) assert viewer.items == ['url'] + + +class TestRestControllerWithHooks(PecanTestCase): + + def test_restcontroller_with_hooks(self): + + class SomeHook(PecanHook): + + def before(self, state): + state.response.headers['X-Testing'] = 'XYZ' + + class BaseController(rest.RestController): + + @expose() + def delete(self, _id): + return 'Deleting %s' % _id + + class RootController(BaseController, HookController): + + __hooks__ = [SomeHook()] + + @expose() + def get_all(self): + return 'Hello, World!' + + app = TestApp( + make_app( + RootController() + ) + ) + + response = app.get('/') + assert response.status_int == 200 + assert response.body == b_('Hello, World!') + assert response.headers['X-Testing'] == 'XYZ' + + response = app.delete('/100/') + assert response.status_int == 200 + assert response.body == b_('Deleting 100') + assert response.headers['X-Testing'] == 'XYZ' diff --git a/pecan/tests/test_rest.py b/pecan/tests/test_rest.py index 7e1e8b6..e63c5e7 100644 --- a/pecan/tests/test_rest.py +++ b/pecan/tests/test_rest.py @@ -7,7 +7,7 @@ except: from six import b as b_ -from pecan import abort, expose, make_app, response +from pecan import abort, expose, make_app, response, redirect from pecan.rest import RestController from pecan.tests import PecanTestCase @@ -681,6 +681,117 @@ class TestRestController(PecanTestCase): assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 1 + def test_nested_get_all(self): + + class BarsController(RestController): + + @expose() + def get_one(self, foo_id, id): + return '4' + + @expose() + def get_all(self, foo_id): + return '3' + + class FoosController(RestController): + + bars = BarsController() + + @expose() + def get_one(self, id): + return '2' + + @expose() + def get_all(self): + return '1' + + class RootController(object): + foos = FoosController() + + # create the app + app = TestApp(make_app(RootController())) + + r = app.get('/foos/') + assert r.status_int == 200 + assert r.body == b_('1') + + r = app.get('/foos/1/') + assert r.status_int == 200 + assert r.body == b_('2') + + r = app.get('/foos/1/bars/') + assert r.status_int == 200 + assert r.body == b_('3') + + r = app.get('/foos/1/bars/2/') + assert r.status_int == 200 + assert r.body == b_('4') + + r = app.get('/foos/bars/', status=400) + assert r.status_int == 400 + + r = app.get('/foos/bars/1', status=400) + assert r.status_int == 400 + + def test_nested_get_all_with_lookup(self): + + class BarsController(RestController): + + @expose() + def get_one(self, foo_id, id): + return '4' + + @expose() + def get_all(self, foo_id): + return '3' + + @expose('json') + def _lookup(self, id, *remainder): + redirect('/lookup-hit/') + + class FoosController(RestController): + + bars = BarsController() + + @expose() + def get_one(self, id): + return '2' + + @expose() + def get_all(self): + return '1' + + class RootController(object): + foos = FoosController() + + # create the app + app = TestApp(make_app(RootController())) + + r = app.get('/foos/') + assert r.status_int == 200 + assert r.body == b_('1') + + r = app.get('/foos/1/') + assert r.status_int == 200 + assert r.body == b_('2') + + r = app.get('/foos/1/bars/') + assert r.status_int == 200 + assert r.body == b_('3') + + r = app.get('/foos/1/bars/2/') + assert r.status_int == 200 + assert r.body == b_('4') + + r = app.get('/foos/bars/', status=400) + assert r.status_int == 400 + + r = app.get('/foos/bars/', status=400) + + r = app.get('/foos/bars/1') + assert r.status_int == 302 + assert r.headers['Location'].endswith('/lookup-hit/') + def test_bad_rest(self): class ThingsController(RestController): @@ -773,16 +884,16 @@ class TestRestController(PecanTestCase): # test get_all r = app.get('/foos') - assert r.status_int == 200 - assert r.body == b_(dumps(dict(items=FoosController.data))) + self.assertEqual(r.status_int, 200) + self.assertEqual(r.body, b_(dumps(dict(items=FoosController.data)))) # test nested get_all r = app.get('/foos/1/bars') - assert r.status_int == 200 - assert r.body == b_(dumps(dict(items=BarsController.data[1]))) + self.assertEqual(r.status_int, 200) + self.assertEqual(r.body, b_(dumps(dict(items=BarsController.data[1])))) r = app.get('/foos/bars', expect_errors=True) - assert r.status_int == 404 + self.assertEqual(r.status_int, 400) def test_custom_with_trailing_slash(self): @@ -6,4 +6,4 @@ cover-package=pecan cover-erase=1 [pytest] -norecursedirs = +package+ +norecursedirs = +package+ config_fixtures docs .git *.egg .tox |