From 10496f77cce1b04f97ca5c3806f899a371fb9e52 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 11:37:05 -0500 Subject: [PATCH 01/12] Moving all Pecan request properties into a Pecan config object to keep WebOb's Request object cleaner --- pecan/core.py | 68 +++++++++++++++++++--------------------- pecan/rest.py | 7 ++--- tests/test_base.py | 2 +- tests/test_hooks.py | 4 +-- tests/test_validation.py | 68 ++++++++++++++++++++-------------------- 5 files changed, 72 insertions(+), 77 deletions(-) diff --git a/pecan/core.py b/pecan/core.py index d892706..47ba627 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -24,8 +24,6 @@ def proxy(key): class ObjectProxy(object): def __getattr__(self, attr): obj = getattr(state, key) - if attr == 'validation_errors': - return getattr(obj, attr, {}) return getattr(obj, attr) def __setattr__(self, attr, value): obj = getattr(state, key) @@ -41,9 +39,9 @@ response = proxy('response') def override_template(template, content_type=None): - request.override_template = template + request.pecan['override_template'] = template if content_type: - request.override_content_type = content_type + request.pecan['override_content_type'] = content_type def abort(status_code=None, detail='', headers=None, comment=None): raise exc.status_map[status_code](detail=detail, headers=headers, comment=comment) @@ -60,11 +58,9 @@ def redirect(location, internal=False, code=None, headers={}): def error_for(field): - if not request.validation_errors: - return '' - return request.validation_errors.get(field, '') + return request.pecan['validation_errors'].get(field, '') + - def static(name, value): if 'pecan.params' not in request.environ: request.environ['pecan.params'] = dict(request.str_params) @@ -95,10 +91,10 @@ class ValidationException(ForwardRequestException): location = cfg['error_handler'] if callable(location): location = location() - merge_dicts(request.validation_errors, errors) + merge_dicts(request.pecan['validation_errors'], errors) if 'pecan.params' not in request.environ: request.environ['pecan.params'] = dict(request.str_params) - request.environ['pecan.validation_errors'] = request.validation_errors + request.environ['pecan.validation_errors'] = request.pecan['validation_errors'] if cfg.get('htmlfill') is not None: request.environ['pecan.htmlfill'] = cfg['htmlfill'] request.environ['REQUEST_METHOD'] = 'GET' @@ -138,7 +134,11 @@ class Pecan(object): except NonCanonicalPath, e: if self.force_canonical and not _cfg(e.controller).get('accept_noncanonical', False): if request.method == 'POST': - raise RuntimeError, "You have POSTed to a URL '%s' which requires a slash. Most browsers will not maintain POST data when redirected. Please update your code to POST to '%s/' or set force_canonical to False" % (request.routing_path, request.routing_path) + raise RuntimeError, "You have POSTed to a URL '%s' which '\ + 'requires a slash. Most browsers will not maintain '\ + 'POST data when redirected. Please update your code '\ + 'to POST to '%s/' or set force_canonical to False" % \ + (request.pecan['routing_path'], request.pecan['routing_path']) raise exc.HTTPFound(add_slash=True) return e.controller, e.remainder @@ -171,9 +171,9 @@ class Pecan(object): args.append(im_self) # grab the routing args from nested REST controllers - if hasattr(request, 'routing_args'): - remainder = request.routing_args + list(remainder) - delattr(request, 'routing_args') + if 'routing_args' in request.pecan: + remainder = request.pecan['routing_args'] + list(remainder) + del request.pecan['routing_args'] # handle positional arguments if valid_args and remainder: @@ -212,7 +212,6 @@ class Pecan(object): def validate(self, schema, params, json=False, error_handler=None, htmlfill=None, variable_decode=None): - request.validation_errors = {} try: to_validate = params if json: @@ -225,7 +224,7 @@ class Pecan(object): if variable_decode is not None: kwargs['encode_variables'] = True kwargs.update(variable_decode) - request.validation_errors = e.unpack_errors(**kwargs) + request.pecan['validation_errors'] = e.unpack_errors(**kwargs) if error_handler is not None: raise ValidationException() if json: @@ -238,20 +237,20 @@ class Pecan(object): state.hooks = self.determine_hooks() # store the routing path to allow hooks to modify it - request.routing_path = request.path + request.pecan['routing_path'] = request.path # handle "on_route" hooks self.handle_hooks('on_route', state) # lookup the controller, respecting content-type as requested # by the file extension on the URI - path = request.routing_path + path = request.pecan['routing_path'] - if state.content_type is None and '.' in path.split('/')[-1]: + if not request.pecan['content_type'] and '.' in path.split('/')[-1]: path, format = os.path.splitext(path) # store the extension for retrieval by controllers - request.extension = format - state.content_type = self.get_content_type(format) + request.pecan['extension'] = format + request.pecan['content_type'] = self.get_content_type(format) controller, remainder = self.route(self.root, path) cfg = _cfg(controller) @@ -270,8 +269,8 @@ class Pecan(object): state.controller = controller # if unsure ask the controller for the default content type - if state.content_type is None: - state.content_type = cfg.get('content_type', 'text/html') + if not request.pecan['content_type']: + request.pecan['content_type'] = cfg.get('content_type', 'text/html') # get a sorted list of hooks, by priority state.hooks = self.determine_hooks(controller) @@ -291,7 +290,7 @@ class Pecan(object): variable_decode=cfg.get('variable_decode') ) elif 'pecan.validation_errors' in request.environ: - request.validation_errors = request.environ.pop('pecan.validation_errors') + request.pecan['validation_errors'] = request.environ.pop('pecan.validation_errors') # fetch the arguments for the controller args, kwargs = self.get_args( @@ -312,16 +311,16 @@ class Pecan(object): raw_namespace = result # pull the template out based upon content type and handle overrides - template = cfg.get('content_types', {}).get(state.content_type) + template = cfg.get('content_types', {}).get(request.pecan['content_type']) # check if for controller override of template - template = getattr(request, 'override_template', template) - state.content_type = getattr(request, 'override_content_type', state.content_type) + template = request.pecan.get('override_template', template) + request.pecan['content_type'] = request.pecan.get('override_content_type', request.pecan['content_type']) # if there is a template, render it if template: if template == 'json': - state.content_type = self.get_content_type('.json') + request.pecan['content_type'] = self.get_content_type('.json') result = render(template, result) # pass the response through htmlfill (items are popped out of the @@ -331,8 +330,8 @@ class Pecan(object): _htmlfill = request.environ.pop('pecan.htmlfill') if 'pecan.params' in request.environ: params = request.environ.pop('pecan.params') - if request.validation_errors and _htmlfill is not None and state.content_type == 'text/html': - errors = getattr(request, 'validation_errors', {}) + if request.pecan['validation_errors'] and _htmlfill is not None and request.pecan['content_type'] == 'text/html': + errors = request.pecan['validation_errors'] result = htmlfill.render(result, defaults=params, errors=errors, **_htmlfill) # If we are in a test request put the namespace where it can be @@ -350,21 +349,21 @@ class Pecan(object): response.body = result # set the content type - if state.content_type: - response.content_type = state.content_type + if request.pecan['content_type']: + response.content_type = request.pecan['content_type'] def __call__(self, environ, start_response): # create the request and response object state.request = Request(environ) - state.content_type = None state.response = Response() state.hooks = [] state.app = self # handle the request try: - # add context to the request + # add context and environment to the request state.request.context = {} + state.request.pecan = dict(content_type=None, validation_errors={}) self.handle_request() except Exception, e: @@ -387,7 +386,6 @@ class Pecan(object): return state.response(environ, start_response) finally: # clean up state - del state.content_type del state.hooks del state.request del state.response diff --git a/pecan/rest.py b/pecan/rest.py index 88eed0a..ef87bfb 100644 --- a/pecan/rest.py +++ b/pecan/rest.py @@ -52,7 +52,7 @@ class RestController(object): # get the args to figure out how much to chop off args = getargspec(getattr(self, method)) - fixed_args = len(args[0][1:]) - len(getattr(request, 'routing_args', [])) + fixed_args = len(args[0][1:]) - len(request.pecan.get('routing_args', [])) var_args = args[1] # attempt to locate a sub-controller @@ -161,7 +161,4 @@ class RestController(object): _handle_put = _handle_post def _set_routing_args(self, args): - if hasattr(request, 'routing_args'): - request.routing_args.extend(args) - else: - setattr(request, 'routing_args', args) + request.pecan.setdefault('routing_args', []).extend(args) diff --git a/tests/test_base.py b/tests/test_base.py index 183b20d..6a8fac2 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -514,7 +514,7 @@ class TestBase(TestCase): @expose() def _default(self, *args): from pecan.core import request - return request.extension + return request.pecan['extension'] app = TestApp(Pecan(RootController())) r = app.get('/index.html') diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 2261983..fb1cb5f 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -696,7 +696,7 @@ class TestHooks(object): password_confirm, age): run_hook.append('inside') - return str(len(request.validation_errors) > 0) + return str(len(request.pecan['validation_errors']) > 0) @expose(schema=RegistrationSchema(), error_handler='/errors') def with_handler(self, first_name, @@ -707,7 +707,7 @@ class TestHooks(object): password_confirm, age): run_hook.append('inside') - return str(len(request.validation_errors) > 0) + return str(len(request.pecan['validation_errors']) > 0) # test that the hooks get properly run with no validation errors app = TestApp(make_app(RootController(), hooks=[SimpleHook()])) diff --git a/tests/test_validation.py b/tests/test_validation.py index 9651450..cafbf47 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -95,7 +95,7 @@ class TestValidation(object): @expose() def errors(self, *args, **kwargs): - assert len(request.validation_errors) > 0 + assert len(request.pecan['validation_errors']) > 0 return 'There was an error!' @expose(schema=RegistrationSchema()) @@ -106,7 +106,7 @@ class TestValidation(object): password, password_confirm, age): - assert len(request.validation_errors) > 0 + assert len(request.pecan['validation_errors']) > 0 return 'Success!' @expose(schema=RegistrationSchema(), error_handler='/errors') @@ -117,17 +117,17 @@ class TestValidation(object): password, password_confirm, age): - assert len(request.validation_errors) > 0 + assert len(request.pecan['validation_errors']) > 0 return 'Success!' @expose(json_schema=RegistrationSchema()) def json(self, data): - assert len(request.validation_errors) > 0 + assert len(request.pecan['validation_errors']) > 0 return 'Success!' @expose(json_schema=RegistrationSchema(), error_handler='/errors') def json_with_handler(self, data): - assert len(request.validation_errors) > 0 + assert len(request.pecan['validation_errors']) > 0 return 'Success!' @@ -193,13 +193,13 @@ class TestValidation(object): @expose() def errors(self, *args, **kwargs): - return 'Error with %s!' % ', '.join(request.validation_errors.keys()) + return 'Error with %s!' % ', '.join(request.pecan['validation_errors'].keys()) @expose(schema=ColorSchema(), variable_decode=True) def index(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -207,16 +207,16 @@ class TestValidation(object): error_handler='/errors', variable_decode=True) def with_handler(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @expose(json_schema=ColorSchema(), variable_decode=True) def json(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -224,16 +224,16 @@ class TestValidation(object): error_handler='/errors', variable_decode=True) def json_with_handler(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @expose(schema=ColorSchema(), variable_decode=dict()) def custom(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -241,16 +241,16 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict()) def custom_with_handler(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @expose(json_schema=ColorSchema(), variable_decode=dict()) def custom_json(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -258,16 +258,16 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict()) def custom_json_with_handler(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @expose(schema=ColorSchema(), variable_decode=dict(dict_char='-', list_char='.')) def alternate(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -275,16 +275,16 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict(dict_char='-', list_char='.')) def alternate_with_handler(self, **kwargs): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @expose(json_schema=ColorSchema(), variable_decode=dict(dict_char='-', list_char='.')) def alternate_json(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -292,8 +292,8 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict(dict_char='-', list_char='.')) def alternate_json_with_handler(self, data): - if request.validation_errors: - return ', '.join(request.validation_errors.keys()) + if request.pecan['validation_errors']: + return ', '.join(request.pecan['validation_errors'].keys()) else: return 'Success!' @@ -507,7 +507,7 @@ class TestValidation(object): schema=ColorSchema(), variable_decode=True) def index(self, **kwargs): - if request.validation_errors: + if request.pecan['validation_errors']: return dict() else: return dict(data=kwargs) @@ -542,8 +542,8 @@ class TestValidation(object): schema=NameSchema(), htmlfill=dict(auto_insert_errors=True)) def json(self, **kwargs): - if request.validation_errors: - return dict(error_with=request.validation_errors.keys()) + if request.pecan['validation_errors']: + return dict(error_with=request.pecan['validation_errors'].keys()) else: return kwargs @@ -639,7 +639,7 @@ class TestValidation(object): @expose(template='mako:form_login.html', schema=LoginSchema()) def index(self, **kwargs): - if request.validation_errors: + if request.pecan['validation_errors']: return dict() else: return dict(data=kwargs) From a01bf598454bb0268c6e2652b8df6faf9025d639 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 13:17:24 -0500 Subject: [PATCH 02/12] Fixing handling of requests for unsupported content types --- pecan/core.py | 3 +++ tests/test_base.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pecan/core.py b/pecan/core.py index 47ba627..45afabd 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -271,6 +271,9 @@ class Pecan(object): # if unsure ask the controller for the default content type if not request.pecan['content_type']: request.pecan['content_type'] = cfg.get('content_type', 'text/html') + elif cfg.get('content_type') is not None and \ + request.pecan['content_type'] not in cfg.get('content_types', {}): + raise exc.HTTPNotFound # get a sorted list of hooks, by priority state.hooks = self.determine_hooks(controller) diff --git a/tests/test_base.py b/tests/test_base.py index 6a8fac2..1c4edad 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -544,7 +544,24 @@ class TestBase(TestCase): app = make_app(RootController(), wrap_app=wrap, debug=True) assert len(wrapped_apps) == 1 + + def test_bad_content_type(self): + class RootController(object): + @expose() + def index(self): + return '/' + + app = TestApp(Pecan(RootController())) + r = app.get('/') + assert r.status_int == 200 + assert r.body == '/' + + r = app.get('/index.html', expect_errors=True) + assert r.status_int == 200 + assert r.body == '/' + r = app.get('/index.txt', expect_errors=True) + assert r.status_int == 404 class TestEngines(object): From b256246a2ba4a9151c1db57e42f895cb9eec2b66 Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Sun, 6 Mar 2011 13:26:26 -0500 Subject: [PATCH 03/12] use mimetypes to determine the type --- pecan/core.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pecan/core.py b/pecan/core.py index 45afabd..9001e76 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -5,6 +5,8 @@ from util import _cfg from webob import Request, Response, exc from threading import local from itertools import chain +from mimetypes import guess_type + from formencode import htmlfill, Invalid, variabledecode from formencode.schema import merge_dicts from paste.recursive import ForwardRequestException @@ -119,12 +121,7 @@ class Pecan(object): self.force_canonical = force_canonical def get_content_type(self, format): - return { - '.html' : 'text/html', - '.xhtml' : 'text/html', - '.json' : 'application/json', - '.txt' : 'text/plain' - }.get(format) + return guess_type(format)[0] def route(self, node, path): path = path.split('/')[1:] From e39139fea879fba4f96f62b03808350a903a5b34 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 13:27:10 -0500 Subject: [PATCH 04/12] Moving render into Pecan --- pecan/core.py | 25 ++++--- tests/test_base.py | 171 ++++++++++++++++++++++++--------------------- 2 files changed, 104 insertions(+), 92 deletions(-) diff --git a/pecan/core.py b/pecan/core.py index 45afabd..2351714 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -69,16 +69,7 @@ def static(name, value): def render(template, namespace): - renderer = state.app.renderers.get(state.app.default_renderer, state.app.template_path) - if template == 'json': - renderer = state.app.renderers.get('json', state.app.template_path) - else: - namespace['error_for'] = error_for - namespace['static'] = static - if ':' in template: - renderer = state.app.renderers.get(template.split(':')[0], state.app.template_path) - template = template.split(':')[1] - return renderer.render(template, namespace) + return state.app.render(template, namespace) class ValidationException(ForwardRequestException): @@ -210,6 +201,18 @@ class Pecan(object): return args, kwargs + def render(self, template, namespace): + renderer = self.renderers.get(self.default_renderer, self.template_path) + if template == 'json': + renderer = self.renderers.get('json', self.template_path) + else: + namespace['error_for'] = error_for + namespace['static'] = static + if ':' in template: + renderer = self.renderers.get(template.split(':')[0], self.template_path) + template = template.split(':')[1] + return renderer.render(template, namespace) + def validate(self, schema, params, json=False, error_handler=None, htmlfill=None, variable_decode=None): try: @@ -324,7 +327,7 @@ class Pecan(object): if template: if template == 'json': request.pecan['content_type'] = self.get_content_type('.json') - result = render(template, result) + result = self.render(template, result) # pass the response through htmlfill (items are popped out of the # environment even if htmlfill won't run for proper cleanup) diff --git a/tests/test_base.py b/tests/test_base.py index 1c4edad..ecb483e 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,7 +3,7 @@ from paste.recursive import ForwardRequestException from unittest import TestCase from webtest import TestApp -from pecan import Pecan, expose, request, response, redirect, abort, make_app, override_template +from pecan import Pecan, expose, request, response, redirect, abort, make_app, override_template, render from pecan.templating import _builtin_renderers as builtin_renderers, error_formatters from pecan.decorators import accept_noncanonical @@ -563,6 +563,85 @@ class TestBase(TestCase): r = app.get('/index.txt', expect_errors=True) assert r.status_int == 404 + def test_canonical_index(self): + class ArgSubController(object): + @expose() + def index(self, arg): + return arg + class AcceptController(object): + @accept_noncanonical + @expose() + def index(self): + return 'accept' + class SubController(object): + @expose() + def index(self): + return 'subindex' + class RootController(object): + @expose() + def index(self): + return 'index' + + sub = SubController() + arg = ArgSubController() + accept = AcceptController() + + app = TestApp(Pecan(RootController())) + + r = app.get('/') + assert r.status_int == 200 + assert 'index' in r.body + + r = app.get('/index') + assert r.status_int == 200 + assert 'index' in r.body + + # for broken clients + r = app.get('', status=302) + assert r.status_int == 302 + + r = app.get('/sub/') + assert r.status_int == 200 + assert 'subindex' in r.body + + r = app.get('/sub', status=302) + assert r.status_int == 302 + + try: + r = app.post('/sub', dict(foo=1)) + raise Exception, "Post should fail" + except Exception, e: + assert isinstance(e, RuntimeError) + + r = app.get('/arg/index/foo') + assert r.status_int == 200 + assert r.body == 'foo' + + r = app.get('/accept/') + assert r.status_int == 200 + assert 'accept' == r.body + + r = app.get('/accept') + assert r.status_int == 200 + assert 'accept' == r.body + + app = TestApp(Pecan(RootController(), force_canonical=False)) + r = app.get('/') + assert r.status_int == 200 + assert 'index' in r.body + + r = app.get('/sub') + assert r.status_int == 200 + assert 'subindex' in r.body + + r = app.post('/sub', dict(foo=1)) + assert r.status_int == 200 + assert 'subindex' in r.body + + r = app.get('/sub/') + assert r.status_int == 200 + assert 'subindex' in r.body + class TestEngines(object): template_path = os.path.join(os.path.dirname(__file__), 'templates') @@ -669,88 +748,18 @@ class TestEngines(object): assert 'Override' in r.body assert r.content_type == 'text/plain' - def test_canonical_index(self): - class ArgSubController(object): - @expose() - def index(self, arg): - return arg - class AcceptController(object): - @accept_noncanonical - @expose() - def index(self): - return 'accept' - class SubController(object): - @expose() - def index(self): - return 'subindex' + def test_render(self): + + #if 'mako' not in builtin_renderers: + # return + class RootController(object): @expose() - def index(self): - return 'index' - - sub = SubController() - arg = ArgSubController() - accept = AcceptController() - - app = TestApp(Pecan(RootController())) - - r = app.get('/') - assert r.status_int == 200 - assert 'index' in r.body - - r = app.get('/index') - assert r.status_int == 200 - assert 'index' in r.body + def index(self, name='Jonathan'): + return render('mako.html', dict(name=name)) + return dict(name=name) - # for broken clients - r = app.get('', status=302) - assert r.status_int == 302 - - r = app.get('/sub/') - assert r.status_int == 200 - assert 'subindex' in r.body - - r = app.get('/sub', status=302) - assert r.status_int == 302 - - try: - r = app.post('/sub', dict(foo=1)) - raise Exception, "Post should fail" - except Exception, e: - assert isinstance(e, RuntimeError) - - r = app.get('/arg/index/foo') - assert r.status_int == 200 - assert r.body == 'foo' - - r = app.get('/accept/') - assert r.status_int == 200 - assert 'accept' == r.body - - r = app.get('/accept') - assert r.status_int == 200 - assert 'accept' == r.body - - app = TestApp(Pecan(RootController(), force_canonical=False)) + app = TestApp(Pecan(RootController(), template_path=self.template_path)) r = app.get('/') assert r.status_int == 200 - assert 'index' in r.body - - r = app.get('/sub') - assert r.status_int == 200 - assert 'subindex' in r.body - - r = app.post('/sub', dict(foo=1)) - assert r.status_int == 200 - assert 'subindex' in r.body - - r = app.get('/sub/') - assert r.status_int == 200 - assert 'subindex' in r.body - - - - - - - + assert "

Hello, Jonathan!

" in r.body From 5714dc58cc905142e0bbf7add8b6f972f128a742 Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Sun, 6 Mar 2011 13:56:16 -0500 Subject: [PATCH 05/12] updated ext test to use None content_type --- tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index ecb483e..996daa7 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -511,7 +511,7 @@ class TestBase(TestCase): Test extension splits """ class RootController(object): - @expose() + @expose(content_type=None) def _default(self, *args): from pecan.core import request return request.pecan['extension'] From db5f828b6ab95d9e86097c120b4661b361e65bae Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Sun, 6 Mar 2011 13:56:36 -0500 Subject: [PATCH 06/12] update to guess_type on full path --- pecan/core.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pecan/core.py b/pecan/core.py index f2e6923..fc0ed71 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -111,9 +111,6 @@ class Pecan(object): self.template_path = template_path self.force_canonical = force_canonical - def get_content_type(self, format): - return guess_type(format)[0] - def route(self, node, path): path = path.split('/')[1:] try: @@ -247,10 +244,10 @@ class Pecan(object): path = request.pecan['routing_path'] if not request.pecan['content_type'] and '.' in path.split('/')[-1]: - path, format = os.path.splitext(path) + request.pecan['content_type'] = guess_type(path)[0] # store the extension for retrieval by controllers + path, format = os.path.splitext(path) request.pecan['extension'] = format - request.pecan['content_type'] = self.get_content_type(format) controller, remainder = self.route(self.root, path) cfg = _cfg(controller) @@ -323,7 +320,7 @@ class Pecan(object): # if there is a template, render it if template: if template == 'json': - request.pecan['content_type'] = self.get_content_type('.json') + request.pecan['content_type'] = 'application/json' result = self.render(template, result) # pass the response through htmlfill (items are popped out of the From 176ba88d1710554ee37bfd81016a4f516bf2a05c Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 14:07:50 -0500 Subject: [PATCH 07/12] Fixing base project template for routing/validation changes --- pecan/templates/project/+package+/controllers/root.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pecan/templates/project/+package+/controllers/root.py b/pecan/templates/project/+package+/controllers/root.py index 35f74ed..2990f59 100644 --- a/pecan/templates/project/+package+/controllers/root.py +++ b/pecan/templates/project/+package+/controllers/root.py @@ -11,9 +11,9 @@ class SampleForm(Schema): class RootController(object): @expose('index.html') def index(self, name='', age=''): - return dict(errors=request.validation_errors, name=name, age=age) + return dict(errors=request.pecan['validation_errors'], name=name, age=age) - @expose('success.html', schema=SampleForm(), error_handler='index') + @expose('success.html', schema=SampleForm(), error_handler='/index') def handle_form(self, name, age): return dict(name=name, age=age) From e08ab7e031e48835600276e30092965751f16810 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 14:10:35 -0500 Subject: [PATCH 08/12] Adding test for bad internal redirects --- tests/test_base.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index ecb483e..c410b1f 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -438,6 +438,10 @@ class TestBase(TestCase): def internal(self): redirect('/testing', internal=True) + @expose() + def bad_internal(self): + redirect('/testing', internal=True, code=301) + @expose() def permanent(self): redirect('/testing', code=301) @@ -446,21 +450,25 @@ class TestBase(TestCase): def testing(self): return 'it worked!' - app = TestApp(Pecan(RootController())) + app = TestApp(make_app(RootController(), debug=True)) r = app.get('/') assert r.status_int == 302 r = r.follow() assert r.status_int == 200 assert r.body == 'it worked!' - self.assertRaises(ForwardRequestException, app.get, '/internal') + r = app.get('/internal') + assert r.status_int == 200 + assert r.body == 'it worked!' + + self.assertRaises(ValueError, app.get, '/bad_internal') r = app.get('/permanent') assert r.status_int == 301 r = r.follow() assert r.status_int == 200 assert r.body == 'it worked!' - + def test_streaming_response(self): import StringIO class RootController(object): From 37f6e8bbbb5cb955caf92077422eca26d5f1f30c Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Sun, 6 Mar 2011 14:10:57 -0500 Subject: [PATCH 09/12] Adding test for deleting proxy attributes --- tests/test_base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index c410b1f..1de0e03 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -649,6 +649,21 @@ class TestBase(TestCase): r = app.get('/sub/') assert r.status_int == 200 assert 'subindex' in r.body + + def test_proxy(self): + class RootController(object): + @expose() + def index(self): + request.testing = True + assert request.testing == True + del request.testing + assert hasattr(request, 'testing') == False + return '/' + + app = TestApp(make_app(RootController(), debug=True)) + r = app.get('/') + assert r.status_int == 200 + class TestEngines(object): From 0cabf7386642459134cbe7b87cbeb0685944cd12 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Mon, 7 Mar 2011 12:31:54 -0500 Subject: [PATCH 10/12] Adding a test for bad config keys --- tests/test_conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_conf.py b/tests/test_conf.py index 1875218..32c4a8b 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -116,3 +116,8 @@ class TestConf(TestCase): def test_config_set_from_module_with_extension(self): configuration.set_config('config.py') self.assertEqual(_runtime_conf.server.host, '1.1.1.1') + + def test_config_bad_key(self): + conf = configuration.Config({'a': 1}) + assert conf.a == 1 + self.assertRaises(AttributeError, getattr, conf, 'b') From 7c95229eb8996cfb37edd0271edeb4db5c3efdb6 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Mon, 7 Mar 2011 12:32:43 -0500 Subject: [PATCH 11/12] Fixing JSON encoder for ResultProxy/RowProxy and adding test coverage. Now at 100%. --- pecan/jsonify.py | 7 ++- tests/test_jsonify.py | 113 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/pecan/jsonify.py b/pecan/jsonify.py index 3b6c150..90f7f7a 100644 --- a/pecan/jsonify.py +++ b/pecan/jsonify.py @@ -49,9 +49,12 @@ class GenericJSON(JSONEncoder): props[key] = getattr(obj, key) return props elif isinstance(obj, ResultProxy): - return dict(rows=list(obj), count=obj.rowcount) + props = dict(rows=list(obj), count=obj.rowcount) + if props['count'] < 0: + props['count'] = len(props['rows']) + return props elif isinstance(obj, RowProxy): - return dict(rows=dict(obj), count=1) + return dict(obj) elif isinstance(obj, (MultiDict, UnicodeMultiDict)): return obj.mixed() else: diff --git a/tests/test_jsonify.py b/tests/test_jsonify.py index 8ede881..54935fa 100644 --- a/tests/test_jsonify.py +++ b/tests/test_jsonify.py @@ -1,15 +1,20 @@ -from datetime import datetime, date -from decimal import Decimal +from datetime import datetime, date +from decimal import Decimal try: - from simplejson import loads + from simplejson import loads except: - from json import loads -from unittest import TestCase + from json import loads +try: + from sqlalchemy import orm, schema, types + from sqlalchemy.engine import create_engine +except ImportError: + create_engine = None +from unittest import TestCase -from pecan.jsonify import jsonify, encode -from pecan import Pecan, expose, request -from webtest import TestApp -from webob.multidict import MultiDict, UnicodeMultiDict +from pecan.jsonify import jsonify, encode, ResultProxy, RowProxy +from pecan import Pecan, expose, request +from webtest import TestApp +from webob.multidict import MultiDict, UnicodeMultiDict def make_person(): class Person(object): @@ -106,3 +111,93 @@ class TestJsonifyGenericEncoder(TestCase): class Foo(object): pass self.assertRaises(TypeError, encode, Foo()) + +class TestJsonifySQLAlchemyGenericEncoder(TestCase): + + def setUp(self): + if not create_engine: + self.create_fake_proxies() + else: + self.create_sa_proxies() + + def create_fake_proxies(self): + + # create a fake SA object + class FakeSAObject(object): + def __init__(self): + self._sa_class_manager = object() + self._sa_instance_state = 'awesome' + self.id = 1 + self.first_name = 'Jonathan' + self.last_name = 'LaCour' + + # create a fake result proxy + class FakeResultProxy(ResultProxy): + def __init__(self): + self.rowcount = -1 + self.rows = [] + def __iter__(self): + return iter(self.rows) + def append(self, row): + self.rows.append(row) + + # create a fake row proxy + class FakeRowProxy(RowProxy): + def __init__(self, arg=None): + self.row = dict(arg) + def __getitem__(self, key): + return self.row.__getitem__(key) + def keys(self): + return self.row.keys() + + # get the SA objects + self.sa_object = FakeSAObject() + self.result_proxy = FakeResultProxy() + self.result_proxy.append(FakeRowProxy([('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour')])) + self.result_proxy.append(FakeRowProxy([('id', 2), ('first_name', 'Yoann'), ('last_name', 'Roman')])) + self.row_proxy = FakeRowProxy([('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour')]) + + def create_sa_proxies(self): + + # create the table and mapper + metadata = schema.MetaData() + user_table = schema.Table('user', metadata, + schema.Column('id', types.Integer, primary_key=True), + schema.Column('first_name', types.Unicode(25)), + schema.Column('last_name', types.Unicode(25))) + class User(object): + pass + orm.mapper(User, user_table) + + # create the session + engine = create_engine('sqlite:///:memory:') + metadata.bind = engine + metadata.create_all() + session = orm.sessionmaker(bind=engine)() + + # add some dummy data + user_table.insert().execute([ + {'first_name': u'Jonathan', 'last_name': u'LaCour'}, + {'first_name': u'Yoann', 'last_name': u'Roman'} + ]) + + # get the SA objects + self.sa_object = session.query(User).first() + select = user_table.select() + self.result_proxy = select.execute() + self.row_proxy = select.execute().fetchone() + + def test_sa_object(self): + result = encode(self.sa_object) + assert loads(result) == {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'} + + def test_result_proxy(self): + result = encode(self.result_proxy) + assert loads(result) == {'count': 2, 'rows': [ + {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'}, + {'id': 2, 'first_name': 'Yoann', 'last_name': 'Roman'} + ]} + + def test_row_proxy(self): + result = encode(self.row_proxy) + assert loads(result) == {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'} From 8232b0f412b096c9cdae536449bc4c223e37a5f8 Mon Sep 17 00:00:00 2001 From: Yoann Roman Date: Mon, 7 Mar 2011 12:33:44 -0500 Subject: [PATCH 12/12] Adding ignore for HTML coverage reports --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ba6eefe..bde7c10 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +htmlcov