Hello community, here is the log from the commit of package python-pecan for openSUSE:Factory checked in at 2015-02-25 02:17:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pecan (Old) and /work/SRC/openSUSE:Factory/.python-pecan.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-pecan" Changes: -------- --- /work/SRC/openSUSE:Factory/python-pecan/python-pecan.changes 2014-11-11 10:15:59.000000000 +0100 +++ /work/SRC/openSUSE:Factory/.python-pecan.new/python-pecan.changes 2015-02-25 02:18:26.000000000 +0100 @@ -1,0 +2,40 @@ +Tue Feb 24 12:49:00 UTC 2015 - tbechtold@suse.com + +- update to 0.8.3 + *Properly* detect Python < 3.3. + * Fix py32 support; importlib.machinery.SourceFileLoader doesn't exist until + py33 + * Version increment. + * Change pecan to more gracefully handle a few odd request encoding edge + cases. + * Document pecan.request.context + * Improve ImportError verbosity for configuration files. + * core: optimize renderer computing + * middleware.static: remove useless stored variable + * Version increment. + * Don't clone pecan-dependent projects from cgit. + * Allows multiple parameters to be converted to list + * tox: pass posargs to test command + * Corrects ouptut -> output in docs/source/hooks.rst + * Add documentation for generic REST controllers. + * Version increment. + * Improve detection of infinite recursion for PecanHook and pypy. + * Fix broken wsme-stable tests and remove some deprecated pip flags. + * Fix typo in description + * Only define remainder when not empty + * Fixes expose of staticmethod + * Version increment. + * Resolve a bug that mixes up argument order for generic functions. + * Fix a routing bug for generic subcontrollers. + * Remove `assert` for flow control; it can be optimized away with `python -O`. + * For HTTP POSTs, map JSON request bodies to controller keyword arguments. + * Improve argspec detection and leniency for wrapped controllers. + * When path arguments are incorrect for RestController, return HTTP 404, not + 400. + * When detecting non-content for HTTP 204, properly catch UnicodeDecodeError. + * Fix a bug in generic function handling when context locals are disabled. + * Stop using distribute for Python 3 tests. + * Revert "Import run_cross_tests.sh from oslo-incubator" + * Add docs environment to tox.ini + +------------------------------------------------------------------- Old: ---- pecan-0.7.0.tar.gz New: ---- pecan-0.8.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pecan.spec ++++++ --- /var/tmp/diff_new_pack.oJdSjz/_old 2015-02-25 02:18:27.000000000 +0100 +++ /var/tmp/diff_new_pack.oJdSjz/_new 2015-02-25 02:18:27.000000000 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pecan # -# Copyright (c) 2014 SUSE LINUX Products GmbH, Nuernberg, Germany. +# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: python-pecan -Version: 0.7.0 +Version: 0.8.3 Release: 0 Summary: A WSGI object-dispatching web framework, designed to be lean and fast License: BSD-3-Clause @@ -44,7 +44,7 @@ Requires: python-Mako >= 0.4.0 Requires: python-WebOb >= 1.2dev Requires: python-WebTest >= 1.3.1 -Requires: python-logutils +Requires: python-logutils >= 0.3 Requires: python-singledispatch Requires: python-six Suggests: python-Jinja2 ++++++ pecan-0.7.0.tar.gz -> pecan-0.8.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/PKG-INFO new/pecan-0.8.3/PKG-INFO --- old/pecan-0.7.0/PKG-INFO 2014-08-29 14:51:16.000000000 +0200 +++ new/pecan-0.8.3/PKG-INFO 2015-01-12 23:57:54.000000000 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 1.1 Name: pecan -Version: 0.7.0 -Summary: A WSGI object-dispatching web framework, designed to be lean and fast, with few dependancies. +Version: 0.8.3 +Summary: A WSGI object-dispatching web framework, designed to be lean and fast, with few dependencies. Home-page: http://github.com/stackforge/pecan Author: Jonathan LaCour Author-email: info@pecanpy.org diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/commands/base.py new/pecan-0.8.3/pecan/commands/base.py --- old/pecan-0.7.0/pecan/commands/base.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/commands/base.py 2015-01-12 23:57:45.000000000 +0100 @@ -46,7 +46,7 @@ continue try: cmd = ep.load() - assert hasattr(cmd, 'run') + cmd.run # ensure existance; catch AttributeError otherwise except Exception as e: # pragma: nocover warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning) continue diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/commands/create.py new/pecan-0.8.3/pecan/commands/create.py --- old/pecan-0.7.0/pecan/commands/create.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/commands/create.py 2015-01-12 23:57:45.000000000 +0100 @@ -22,7 +22,7 @@ log.debug('%s loading scaffold %s', self.__class__.__name__, ep) try: cmd = ep.load() - assert hasattr(cmd, 'copy_to') + cmd.copy_to # ensure existance; catch AttributeError otherwise except Exception as e: # pragma: nocover warn( "Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/configuration.py new/pecan-0.8.3/pecan/configuration.py --- old/pecan-0.7.0/pecan/configuration.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/configuration.py 2015-01-12 23:57:46.000000000 +0100 @@ -1,9 +1,15 @@ import re import inspect import os +import sys import six +if sys.version_info >= (3, 3): + from importlib.machinery import SourceFileLoader +else: + import imp + IDENTIFIER = re.compile(r'[a-z_](\w)*$', re.IGNORECASE) @@ -152,8 +158,24 @@ if not os.path.isfile(abspath): raise RuntimeError('`%s` is not a file.' % abspath) + # First, make sure the code will actually compile (and has no SyntaxErrors) with open(abspath, 'rb') as f: - exec(compile(f.read(), abspath, 'exec'), globals(), conf_dict) + compiled = compile(f.read(), abspath, 'exec') + + # Next, attempt to actually import the file as a module. + # This provides more verbose import-related error reporting than exec() + absname, _ = os.path.splitext(abspath) + basepath, module_name = absname.rsplit(os.sep, 1) + if sys.version_info >= (3, 3): + SourceFileLoader(module_name, abspath).load_module(module_name) + else: + imp.load_module( + module_name, + *imp.find_module(module_name, [basepath]) + ) + + # If we were able to import as a module, actually exec the compiled code + exec(compiled, globals(), conf_dict) conf_dict['__file__'] = abspath return conf_from_dict(conf_dict) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/core.py new/pecan-0.8.3/pecan/core.py --- old/pecan-0.7.0/pecan/core.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/core.py 2015-01-12 23:57:46.000000000 +0100 @@ -11,15 +11,20 @@ import types import six +if six.PY3: + from .compat import is_bound_method as ismethod +else: + from inspect import ismethod from webob import (Request as WebObRequest, Response as WebObResponse, exc, acceptparse) +from webob.multidict import NestedMultiDict from .compat import urlparse, unquote_plus, izip from .secure import handle_security from .templating import RendererFactory from .routing import lookup_controller, NonCanonicalPath -from .util import _cfg, encode_if_needed +from .util import _cfg, encode_if_needed, getargspec from .middleware.recursive import ForwardRequestException @@ -43,7 +48,13 @@ class Request(WebObRequest): - pass + + def __getattribute__(self, name): + try: + return WebObRequest.__getattribute__(self, name) + except UnicodeDecodeError as e: + logger.exception(e) + abort(400) class Response(WebObResponse): @@ -254,7 +265,8 @@ module = __import__(name, fromlist=fromlist) kallable = getattr(module, parts[-1]) msg = "%s does not represent a callable class or function." - assert hasattr(kallable, '__call__'), msg % item + if not six.callable(kallable): + raise TypeError(msg % item) return kallable() raise ImportError('No item named %s' % item) @@ -338,7 +350,7 @@ return unquote_plus(x) if isinstance(x, six.string_types) \ else x - remainder = [_decode(x) for x in remainder] + remainder = [_decode(x) for x in remainder if x] if im_self is not None: args.append(im_self) @@ -384,18 +396,19 @@ return args, varargs, 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) - if ':' in template: + elif ':' in template: + renderer_name, template = template.split(':', 1) renderer = self.renderers.get( - template.split(':')[0], + renderer_name, + self.template_path + ) + else: + renderer = self.renderers.get( + self.default_renderer, self.template_path ) - template = template.split(':')[1] return renderer.render(template, namespace) def find_controller(self, state): @@ -498,14 +511,22 @@ # fetch any parameters if req.method == 'GET': - params = dict(req.GET) + params = req.GET + elif req.content_type in ('application/json', + 'application/javascript'): + try: + if not isinstance(req.json, dict): + raise TypeError('%s is not a dict' % req.json) + params = NestedMultiDict(req.GET, req.json) + except (TypeError, ValueError): + params = req.params else: - params = dict(req.params) + params = req.params # fetch the arguments for the controller args, varargs, kwargs = self.get_args( state, - params, + params.mixed(), remainder, cfg['argspec'], im_self @@ -527,6 +548,15 @@ resp = state.response pecan_state = req.pecan + # If a keyword is supplied via HTTP GET or POST arguments, but the + # function signature does not allow it, just drop it (rather than + # generating a TypeError). + argspec = getargspec(controller) + keys = kwargs.keys() + for key in keys: + if key not in argspec.args and not argspec.keywords: + kwargs.pop(key) + # get the result from the controller result = controller(*args, **kwargs) @@ -594,9 +624,15 @@ else: text = None if state.response.charset: - # `response.text` cannot be accessed without a charset - # (because we don't know which encoding to use) - text = state.response.text + # `response.text` cannot be accessed without a valid + # charset (because we don't know which encoding to use) + try: + text = state.response.text + except UnicodeDecodeError: + # If a valid charset is not specified, don't bother + # trying to guess it (because there's obviously + # content, so we know this shouldn't be a 204) + pass if not any((state.response.body, text)): state.response.status = 204 @@ -684,15 +720,23 @@ # When comparing the argspec of the method to GET/POST params, # ignore the implicit (req, resp) at the beginning of the function # signature - signature_error = TypeError( - 'When `use_context_locals` is `False`, pecan passes an explicit ' - 'reference to the request and response as the first two arguments ' - 'to the controller.\nChange the `%s.%s.%s` signature to accept ' - 'exactly 2 initial arguments (req, resp)' % ( + if hasattr(state.controller, '__self__'): + _repr = '.'.join(( state.controller.__self__.__class__.__module__, state.controller.__self__.__class__.__name__, state.controller.__name__ - ) + )) + else: + _repr = '.'.join(( + state.controller.__module__, + state.controller.__name__ + )) + + signature_error = TypeError( + 'When `use_context_locals` is `False`, pecan passes an explicit ' + 'reference to the request and response as the first two arguments ' + 'to the controller.\nChange the `%s` signature to accept exactly ' + '2 initial arguments (req, resp)' % _repr ) try: positional = argspec.args[:] @@ -705,7 +749,13 @@ args, varargs, kwargs = super(ExplicitPecan, self).get_args( state, all_params, remainder, argspec, im_self ) - args = [state.request, state.response] + args + + if ismethod(state.controller): + args = [state.request, state.response] + args + else: + # generic controllers have an explicit self *first* + # (because they're decorated functions, not instance methods) + args[1:1] = [state.request, state.response] return args, varargs, kwargs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/decorators.py new/pecan-0.8.3/pecan/decorators.py --- old/pecan-0.7.0/pecan/decorators.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/decorators.py 2015-01-12 23:57:45.000000000 +0100 @@ -1,8 +1,8 @@ -from inspect import getargspec, getmembers, isclass, ismethod, isfunction +from inspect import getmembers, isclass, ismethod, isfunction import six -from .util import _cfg +from .util import _cfg, getargspec __all__ = [ 'expose', 'transactional', 'accept_noncanonical', 'after_commit', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/hooks.py new/pecan-0.8.3/pecan/hooks.py --- old/pecan-0.7.0/pecan/hooks.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/hooks.py 2015-01-12 23:57:45.000000000 +0100 @@ -2,6 +2,7 @@ import sys from inspect import getmembers +import six from webob.exc import HTTPFound from .util import iscontroller, _cfg @@ -12,8 +13,20 @@ ] -def walk_controller(root_class, controller, hooks): - if not isinstance(controller, (int, dict)): +def walk_controller(root_class, controller, hooks, seen=None): + seen = seen or set() + if type(controller) not in vars(six.moves.builtins).values(): + # Avoid recursion loops + try: + if controller in seen: + return + seen.add(controller) + except TypeError: + # If we discover an unhashable item (like a list), it's not + # something that we want to traverse because it's not the sort of + # thing we would add a hook to + return + for hook in getattr(controller, '__hooks__', []): # Append hooks from controller class definition hooks.add(hook) @@ -38,7 +51,7 @@ value.im_class.mro()[1:])) ): continue - walk_controller(root_class, value, hooks) + walk_controller(root_class, value, hooks, seen) class HookControllerMeta(type): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/middleware/debug.py new/pecan-0.8.3/pecan/middleware/debug.py --- old/pecan-0.7.0/pecan/middleware/debug.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/middleware/debug.py 2015-01-12 23:57:45.000000000 +0100 @@ -269,9 +269,11 @@ self.debugger = debugger def __call__(self, environ, start_response): - assert not environ['wsgi.multiprocess'], ( - "The DebugMiddleware middleware is not usable in a " - "multi-process environment") + if environ['wsgi.multiprocess']: + raise RuntimeError( + "The DebugMiddleware middleware is not usable in a " + "multi-process environment" + ) if environ.get('paste.testing'): return self.app(environ, start_response) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/middleware/static.py new/pecan-0.8.3/pecan/middleware/static.py --- old/pecan-0.7.0/pecan/middleware/static.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/middleware/static.py 2015-01-12 23:57:45.000000000 +0100 @@ -115,7 +115,6 @@ def __init__(self, app, directory, fallback_mimetype='text/plain'): self.app = app - self.directory = directory self.loader = self.get_directory_loader(directory) self.fallback_mimetype = fallback_mimetype diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/rest.py new/pecan-0.8.3/pecan/rest.py --- old/pecan-0.7.0/pecan/rest.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/rest.py 2015-01-12 23:57:45.000000000 +0100 @@ -1,4 +1,4 @@ -from inspect import getargspec, ismethod +from inspect import ismethod import warnings from webob import exc @@ -7,7 +7,7 @@ from .core import abort from .decorators import expose from .routing import lookup_controller, handle_lookup_traversal -from .util import iscontroller +from .util import iscontroller, getargspec class RestController(object): @@ -54,7 +54,10 @@ request.pecan.get('routing_args', []) ) if len(remainder) < fixed_args: - abort(400) + # For controllers that are missing intermediate IDs + # (e.g., /authors/books vs /authors/1/books), return a 404 for an + # invalid path. + abort(404) @expose() def _route(self, args, request=None): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/routing.py new/pecan-0.8.3/pecan/routing.py --- old/pecan-0.7.0/pecan/routing.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/routing.py 2015-01-12 23:57:45.000000000 +0100 @@ -1,10 +1,9 @@ import warnings -from inspect import getargspec from webob import exc from .secure import handle_security, cross_boundary -from .util import iscontroller +from .util import iscontroller, getargspec, _cfg __all__ = ['lookup_controller', 'find_object'] @@ -149,6 +148,17 @@ if not remainder: raise PecanNotFound + + prev_remainder = remainder prev_obj = obj remainder = rest obj = getattr(obj, next_obj, None) + + # Last-ditch effort: if there's not a matching subcontroller, no + # `_default`, no `_lookup`, and no `_route`, look to see if there's + # an `index` that has a generic method defined for the current request + # method. + if not obj and not notfound_handlers and hasattr(prev_obj, 'index'): + if request.method in _cfg(prev_obj.index).get('generic_handlers', + {}): + return prev_obj.index, prev_remainder diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/middleware/test_debug.py new/pecan-0.8.3/pecan/tests/middleware/test_debug.py --- old/pecan-0.7.0/pecan/tests/middleware/test_debug.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/middleware/test_debug.py 2015-01-12 23:57:45.000000000 +0100 @@ -58,7 +58,7 @@ app = TestApp(MultiProcessApp(DebugMiddleware(conditional_error_app))) self.assertRaises( - AssertionError, + RuntimeError, app.get, '/' ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_base.py new/pecan-0.8.3/pecan/tests/test_base.py --- old/pecan-0.7.0/pecan/tests/test_base.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_base.py 2015-01-12 23:57:46.000000000 +0100 @@ -12,6 +12,7 @@ from webtest import TestApp import six from six import b as b_ +from six import u as u_ from six.moves import cStringIO as StringIO from pecan import ( @@ -68,6 +69,10 @@ def explicit_json_body(self): response.json_body = {'foo': 'bar'} + @expose() + def non_unicode(self): + return chr(0xc0) + return TestApp(Pecan(RootController())) def test_empty_index(self): @@ -77,6 +82,10 @@ self.assertEqual(r.headers['Content-Length'], '0') self.assertEqual(len(r.body), 0) + def test_index_with_non_unicode(self): + r = self.app_.get('/non_unicode/') + self.assertEqual(r.status_int, 200) + def test_explicit_body(self): r = self.app_.get('/explicit_body/') self.assertEqual(r.status_int, 200) @@ -137,6 +146,23 @@ assert len(r.body) == 0 +class TestInvalidURLEncoding(PecanTestCase): + + @property + def app_(self): + class RootController(object): + + @expose() + def _route(self, args, request): + assert request.path + + return TestApp(Pecan(RootController())) + + def test_rest_with_non_utf_8_body(self): + r = self.app_.get('/%aa/', expect_errors=True) + assert r.status_int == 400 + + class TestIndexRouting(PecanTestCase): @property @@ -432,6 +458,16 @@ assert r.status_int == 200 assert r.body == b_('index: 4') + def test_explicit_json_kwargs(self): + r = self.app_.post_json('/', {'id': '4'}) + assert r.status_int == 200 + assert r.body == b_('index: 4') + + def test_path_with_explicit_json_kwargs(self): + r = self.app_.post_json('/4', {'id': 'four'}) + assert r.status_int == 200 + assert r.body == b_('index: 4') + def test_multiple_kwargs(self): r = self.app_.get('/?id=5&dummy=dummy') assert r.status_int == 200 @@ -442,6 +478,11 @@ assert r.status_int == 200 assert r.body == b_('index: 6') + def test_json_kwargs_from_root(self): + r = self.app_.post_json('/', {'id': '6', 'dummy': 'dummy'}) + assert r.status_int == 200 + assert r.body == b_('index: 6') + # multiple args def test_multiple_positional_arguments(self): @@ -469,6 +510,11 @@ assert r.status_int == 200 assert r.body == b_('multiple: five, six') + def test_positional_args_with_json_kwargs(self): + r = self.app_.post_json('/multiple', {'one': 'five', 'two': 'six'}) + assert r.status_int == 200 + assert r.body == b_('multiple: five, six') + def test_positional_args_with_url_encoded_dictionary_kwargs(self): r = self.app_.post('/multiple', {'one': 'Five%20', 'two': 'Six%20%21'}) assert r.status_int == 200 @@ -519,6 +565,11 @@ assert r.status_int == 200 assert r.body == b_('optional: 4') + def test_optional_arg_with_json_kwargs(self): + r = self.app_.post_json('/optional', {'id': '4'}) + assert r.status_int == 200 + assert r.body == b_('optional: 4') + def test_optional_arg_with_url_encoded_kwargs(self): r = self.app_.post('/optional', {'id': 'Some%20Number'}) assert r.status_int == 200 @@ -529,6 +580,11 @@ assert r.status_int == 200 assert r.body == b_('optional: 5') + def test_multiple_positional_arguments_with_json_kwargs(self): + r = self.app_.post_json('/optional/5', {'id': 'five'}) + assert r.status_int == 200 + assert r.body == b_('optional: 5') + def test_multiple_positional_url_encoded_arguments_with_kwargs(self): r = self.app_.post('/optional/Some%20Number', {'id': 'five'}) assert r.status_int == 200 @@ -549,6 +605,11 @@ assert r.status_int == 200 assert r.body == b_('optional: 7') + def test_optional_arg_with_multiple_json_kwargs(self): + r = self.app_.post_json('/optional', {'id': '7', 'dummy': 'dummy'}) + assert r.status_int == 200 + assert r.body == b_('optional: 7') + def test_optional_arg_with_multiple_url_encoded_dictionary_kwargs(self): r = self.app_.post('/optional', { 'id': 'Some%20Number', @@ -613,6 +674,11 @@ assert r.status_int == 200 assert r.body == b_('multiple_optional: 1, None, None') + def test_multiple_optional_positional_args_with_json_kwargs(self): + r = self.app_.post_json('/multiple_optional', {'one': '1'}) + assert r.status_int == 200 + assert r.body == b_('multiple_optional: 1, None, None') + def test_multiple_optional_positional_args_with_encoded_dict_kwargs(self): r = self.app_.post('/multiple_optional', {'one': 'One%21'}) assert r.status_int == 200 @@ -623,6 +689,11 @@ assert r.status_int == 200 assert r.body == b_('multiple_optional: 1, None, None') + def test_multiple_optional_positional_args_and_json_kwargs(self): + r = self.app_.post_json('/multiple_optional/1', {'one': 'one'}) + assert r.status_int == 200 + assert r.body == b_('multiple_optional: 1, None, None') + def test_multiple_optional_encoded_positional_args_and_dict_kwargs(self): r = self.app_.post('/multiple_optional/One%21', {'one': 'one'}) assert r.status_int == 200 @@ -648,6 +719,14 @@ assert r.status_int == 200 assert r.body == b_('multiple_optional: 1, 2, 3') + def test_multiple_optional_args_with_multiple_json_kwargs(self): + r = self.app_.post_json( + '/multiple_optional', + {'one': '1', 'two': '2', 'three': '3', 'four': '4'} + ) + assert r.status_int == 200 + assert r.body == b_('multiple_optional: 1, 2, 3') + def test_multiple_optional_args_with_multiple_encoded_dict_kwargs(self): r = self.app_.post( '/multiple_optional', @@ -701,6 +780,14 @@ assert r.status_int == 200 assert r.body == b_('variable_args: ') + def test_variable_args_with_json_kwargs(self): + r = self.app_.post_json( + '/variable_args', + {'id': '3', 'dummy': 'dummy'} + ) + assert r.status_int == 200 + assert r.body == b_('variable_args: ') + def test_variable_kwargs(self): r = self.app_.get('/variable_kwargs') assert r.status_int == 200 @@ -727,6 +814,14 @@ assert r.status_int == 200 assert r.body == b_('variable_kwargs: dummy=dummy, id=3') + def test_multiple_variable_kwargs_with_json_kwargs(self): + r = self.app_.post_json( + '/variable_kwargs', + {'id': '3', 'dummy': 'dummy'} + ) + assert r.status_int == 200 + assert r.body == b_('variable_kwargs: dummy=dummy, id=3') + def test_multiple_variable_kwargs_with_encoded_dict_kwargs(self): r = self.app_.post( '/variable_kwargs', @@ -771,6 +866,14 @@ assert r.status_int == 200 assert r.body == b_('variable_all: 6, day=12, month=1') + def test_variable_post_with_json_kwargs(self): + r = self.app_.post_json( + '/variable_all/6', + {'month': '1', 'day': '12'} + ) + assert r.status_int == 200 + assert r.body == b_('variable_all: 6, day=12, month=1') + def test_variable_post_mixed(self): r = self.app_.post( '/variable_all/7', @@ -779,6 +882,41 @@ assert r.status_int == 200 assert r.body == b_('variable_all: 7, day=12, id=seven, month=1') + def test_variable_post_mixed_with_json(self): + r = self.app_.post_json( + '/variable_all/7', + {'id': 'seven', 'month': '1', 'day': '12'} + ) + assert r.status_int == 200 + assert r.body == b_('variable_all: 7, day=12, id=seven, month=1') + + def test_duplicate_query_parameters_GET(self): + r = self.app_.get('/variable_kwargs?list=1&list=2') + l = [u_('1'), u_('2')] + assert r.status_int == 200 + assert r.body == b_('variable_kwargs: list=%s' % l) + + def test_duplicate_query_parameters_POST(self): + r = self.app_.post('/variable_kwargs', + {'list': ['1', '2']}) + l = [u_('1'), u_('2')] + assert r.status_int == 200 + assert r.body == b_('variable_kwargs: list=%s' % l) + + def test_duplicate_query_parameters_POST_mixed(self): + r = self.app_.post('/variable_kwargs?list=1&list=2', + {'list': ['3', '4']}) + l = [u_('1'), u_('2'), u_('3'), u_('4')] + assert r.status_int == 200 + assert r.body == b_('variable_kwargs: list=%s' % l) + + def test_duplicate_query_parameters_POST_mixed_json(self): + r = self.app_.post('/variable_kwargs?list=1&list=2', + {'list': 3}) + l = [u_('1'), u_('2'), u_('3')] + assert r.status_int == 200 + assert r.body == b_('variable_kwargs: list=%s' % l) + def test_no_remainder(self): try: r = self.app_.get('/eater') @@ -835,11 +973,29 @@ assert r.status_int == 200 assert r.body == b_('eater: 9, None, day=12, month=1') + def test_post_empty_remainder_with_json_kwargs(self): + r = self.app_.post_json('/eater/9/', {'month': '1', 'day': '12'}) + assert r.status_int == 200 + assert r.body == b_('eater: 9, None, day=12, month=1') + + def test_post_remainder_with_json_kwargs(self): + r = self.app_.post_json('/eater/9', {'month': '1', 'day': '12'}) + assert r.status_int == 200 + assert r.body == b_('eater: 9, None, day=12, month=1') + def test_post_many_remainders_with_many_kwargs(self): r = self.app_.post( '/eater/10', {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'} ) + assert r.status_int == 200 + assert r.body == b_('eater: 10, dummy, day=12, month=1') + + def test_post_many_remainders_with_many_json_kwargs(self): + r = self.app_.post_json( + '/eater/10', + {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'} + ) assert r.status_int == 200 assert r.body == b_('eater: 10, dummy, day=12, month=1') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_conf.py new/pecan-0.8.3/pecan/tests/test_conf.py --- old/pecan-0.7.0/pecan/tests/test_conf.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_conf.py 2015-01-12 23:57:45.000000000 +0100 @@ -144,6 +144,22 @@ f.name ) + def test_config_with_non_package_relative_import(self): + from pecan import configuration + with tempfile.NamedTemporaryFile('wb', suffix='.py') as f: + f.write(b_('\n'.join(['from . import variables']))) + f.flush() + configuration.Config({}) + + try: + configuration.conf_from_file(f.name) + except (ValueError, SystemError) as e: + assert 'relative import' in str(e) + else: + raise AssertionError( + "A relative import-related error should have been raised" + ) + def test_config_with_bad_import(self): from pecan import configuration path = ('bad', 'importerror.py') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_generic.py new/pecan-0.8.3/pecan/tests/test_generic.py --- old/pecan-0.7.0/pecan/tests/test_generic.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_generic.py 2015-01-12 23:57:45.000000000 +0100 @@ -60,3 +60,29 @@ r = app.delete('/', expect_errors=True) assert r.status_int == 405 assert r.headers['Allow'] == 'GET, PATCH, POST' + + def test_nested_generic(self): + + class SubSubController(object): + @expose(generic=True) + def index(self): + return 'GET' + + @index.when(method='DELETE', template='json') + def do_delete(self, name, *args): + return dict(result=name, args=', '.join(args)) + + class SubController(object): + sub = SubSubController() + + class RootController(object): + sub = SubController() + + app = TestApp(Pecan(RootController())) + r = app.get('/sub/sub/') + assert r.status_int == 200 + assert r.body == b_('GET') + + r = app.delete('/sub/sub/joe/is/cool') + assert r.status_int == 200 + assert r.body == b_(dumps(dict(result='joe', args='is, cool'))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_hooks.py new/pecan-0.8.3/pecan/tests/test_hooks.py --- old/pecan-0.7.0/pecan/tests/test_hooks.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_hooks.py 2015-01-12 23:57:45.000000000 +0100 @@ -1681,6 +1681,19 @@ def get_all(self): return 'Hello, World!' + @staticmethod + def static(cls): + return 'static' + + @property + def foo(self): + return 'bar' + + def testing123(self): + return 'bar' + + unhashable = [1, 'two', 3] + app = TestApp( make_app( RootController() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_no_thread_locals.py new/pecan-0.8.3/pecan/tests/test_no_thread_locals.py --- old/pecan-0.7.0/pecan/tests/test_no_thread_locals.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_no_thread_locals.py 2015-01-12 23:57:45.000000000 +0100 @@ -1,4 +1,5 @@ -from json import dumps +import time +from json import dumps, loads import warnings from webtest import TestApp @@ -7,7 +8,7 @@ import webob import mock -from pecan import Pecan, expose, abort +from pecan import Pecan, expose, abort, Request, Response from pecan.rest import RestController from pecan.hooks import PecanHook, HookController from pecan.tests import PecanTestCase @@ -24,6 +25,21 @@ assert isinstance(resp, webob.Response) return 'Hello, World!' + @expose() + def warning(self): + return ("This should be unroutable because (req, resp) are not" + " arguments. It should raise a TypeError.") + + @expose(generic=True) + def generic(self): + return ("This should be unroutable because (req, resp) are not" + " arguments. It should raise a TypeError.") + + @generic.when(method='PUT') + def generic_put(self, _id): + return ("This should be unroutable because (req, resp) are not" + " arguments. It should raise a TypeError.") + return RootController def test_locals_are_not_used(self): @@ -36,6 +52,36 @@ self.assertRaises(AssertionError, Pecan, self.root) + def test_threadlocal_argument_warning(self): + with mock.patch('threading.local', side_effect=AssertionError()): + + app = TestApp(Pecan(self.root(), use_context_locals=False)) + self.assertRaises( + TypeError, + app.get, + '/warning/' + ) + + def test_threadlocal_argument_warning_on_generic(self): + with mock.patch('threading.local', side_effect=AssertionError()): + + app = TestApp(Pecan(self.root(), use_context_locals=False)) + self.assertRaises( + TypeError, + app.get, + '/generic/' + ) + + def test_threadlocal_argument_warning_on_generic_delegate(self): + with mock.patch('threading.local', side_effect=AssertionError()): + + app = TestApp(Pecan(self.root(), use_context_locals=False)) + self.assertRaises( + TypeError, + app.put, + '/generic/' + ) + class TestIndexRouting(PecanTestCase): @@ -1310,3 +1356,85 @@ assert run_hook[3] == 'inside_sub' assert run_hook[4] == 'after1' assert run_hook[5] == 'after2' + + +class TestGeneric(PecanTestCase): + + @property + def root(self): + class RootController(object): + + def __init__(self, unique): + self.unique = unique + + @expose(generic=True, template='json') + def index(self, req, resp): + assert self.__class__.__name__ == 'RootController' + assert isinstance(req, Request) + assert isinstance(resp, Response) + assert self.unique == req.headers.get('X-Unique') + return {'hello': 'world'} + + @index.when(method='POST', template='json') + def index_post(self, req, resp): + assert self.__class__.__name__ == 'RootController' + assert isinstance(req, Request) + assert isinstance(resp, Response) + assert self.unique == req.headers.get('X-Unique') + return req.json + + @expose(template='json') + def echo(self, req, resp): + assert self.__class__.__name__ == 'RootController' + assert isinstance(req, Request) + assert isinstance(resp, Response) + assert self.unique == req.headers.get('X-Unique') + return req.json + + @expose(template='json') + def extra(self, req, resp, first, second): + assert self.__class__.__name__ == 'RootController' + assert isinstance(req, Request) + assert isinstance(resp, Response) + assert self.unique == req.headers.get('X-Unique') + return {'first': first, 'second': second} + + return RootController + + def test_generics_with_im_self_default(self): + uniq = str(time.time()) + with mock.patch('threading.local', side_effect=AssertionError()): + app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) + r = app.get('/', headers={'X-Unique': uniq}) + assert r.status_int == 200 + json_resp = loads(r.body.decode()) + assert json_resp['hello'] == 'world' + + def test_generics_with_im_self_with_method(self): + uniq = str(time.time()) + with mock.patch('threading.local', side_effect=AssertionError()): + app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) + r = app.post_json('/', {'foo': 'bar'}, headers={'X-Unique': uniq}) + assert r.status_int == 200 + json_resp = loads(r.body.decode()) + assert json_resp['foo'] == 'bar' + + def test_generics_with_im_self_with_path(self): + uniq = str(time.time()) + with mock.patch('threading.local', side_effect=AssertionError()): + app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) + r = app.post_json('/echo/', {'foo': 'bar'}, + headers={'X-Unique': uniq}) + assert r.status_int == 200 + json_resp = loads(r.body.decode()) + assert json_resp['foo'] == 'bar' + + def test_generics_with_im_self_with_extra_args(self): + uniq = str(time.time()) + with mock.patch('threading.local', side_effect=AssertionError()): + app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) + r = app.get('/extra/123/456', headers={'X-Unique': uniq}) + assert r.status_int == 200 + json_resp = loads(r.body.decode()) + assert json_resp['first'] == '123' + assert json_resp['second'] == '456' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_rest.py new/pecan-0.8.3/pecan/tests/test_rest.py --- old/pecan-0.7.0/pecan/tests/test_rest.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/tests/test_rest.py 2015-01-12 23:57:46.000000000 +0100 @@ -1,11 +1,12 @@ -from webtest import TestApp +import struct import warnings try: from simplejson import dumps, loads except: from json import dumps, loads # noqa -from six import b as b_ +from six import b as b_, PY3 +from webtest import TestApp from pecan import abort, expose, make_app, response, redirect from pecan.rest import RestController @@ -727,11 +728,11 @@ 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=404) + assert r.status_int == 404 - r = app.get('/foos/bars/1', status=400) - assert r.status_int == 400 + r = app.get('/foos/bars/1', status=404) + assert r.status_int == 404 def test_nested_get_all_with_lookup(self): @@ -783,10 +784,9 @@ 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/') + assert r.status_int == 302 + assert r.headers['Location'].endswith('/lookup-hit/') r = app.get('/foos/bars/1') assert r.status_int == 302 @@ -893,7 +893,7 @@ self.assertEqual(r.body, b_(dumps(dict(items=BarsController.data[1])))) r = app.get('/foos/bars', expect_errors=True) - self.assertEqual(r.status_int, 400) + self.assertEqual(r.status_int, 404) def test_custom_with_trailing_slash(self): @@ -1360,6 +1360,27 @@ assert r.status_int == 200 assert r.body == b_("DEFAULT missing") + def test_rest_with_non_utf_8_body(self): + if PY3: + # webob+PY3 doesn't suffer from this bug; the POST parsing in PY3 + # seems to more gracefully detect the bytestring + return + + class FooController(RestController): + + @expose() + def post(self): + return "POST" + + class RootController(RestController): + foo = FooController() + + app = TestApp(make_app(RootController())) + + data = struct.pack('255h', *range(0, 255)) + r = app.post('/foo/', data, expect_errors=True) + assert r.status_int == 400 + def test_dynamic_rest_lookup(self): class BarController(RestController): @expose() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/tests/test_util.py new/pecan-0.8.3/pecan/tests/test_util.py --- old/pecan-0.7.0/pecan/tests/test_util.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pecan-0.8.3/pecan/tests/test_util.py 2015-01-12 23:57:45.000000000 +0100 @@ -0,0 +1,95 @@ +import functools +import inspect +import unittest + +from pecan import expose +from pecan import util + + +class TestArgSpec(unittest.TestCase): + + @property + def controller(self): + + class RootController(object): + + @expose() + def index(self, a, b, c=1, *args, **kwargs): + return 'Hello, World!' + + @staticmethod + @expose() + def static_index(a, b, c=1, *args, **kwargs): + return 'Hello, World!' + + return RootController() + + def test_no_decorator(self): + expected = inspect.getargspec(self.controller.index.__func__) + actual = util.getargspec(self.controller.index.__func__) + assert expected == actual + + expected = inspect.getargspec(self.controller.static_index) + actual = util.getargspec(self.controller.static_index) + assert expected == actual + + def test_simple_decorator(self): + def dec(f): + return f + + expected = inspect.getargspec(self.controller.index.__func__) + actual = util.getargspec(dec(self.controller.index.__func__)) + assert expected == actual + + expected = inspect.getargspec(self.controller.static_index) + actual = util.getargspec(dec(self.controller.static_index)) + assert expected == actual + + def test_simple_wrapper(self): + def dec(f): + @functools.wraps(f) + def wrapped(*a, **kw): + return f(*a, **kw) + return wrapped + + expected = inspect.getargspec(self.controller.index.__func__) + actual = util.getargspec(dec(self.controller.index.__func__)) + assert expected == actual + + expected = inspect.getargspec(self.controller.static_index) + actual = util.getargspec(dec(self.controller.static_index)) + assert expected == actual + + def test_multiple_decorators(self): + def dec(f): + @functools.wraps(f) + def wrapped(*a, **kw): + return f(*a, **kw) + return wrapped + + expected = inspect.getargspec(self.controller.index.__func__) + actual = util.getargspec(dec(dec(dec(self.controller.index.__func__)))) + assert expected == actual + + expected = inspect.getargspec(self.controller.static_index) + actual = util.getargspec(dec(dec(dec( + self.controller.static_index)))) + assert expected == actual + + def test_decorator_with_args(self): + def dec(flag): + def inner(f): + @functools.wraps(f) + def wrapped(*a, **kw): + return f(*a, **kw) + return wrapped + return inner + + expected = inspect.getargspec(self.controller.index.__func__) + actual = util.getargspec(dec(True)(self.controller.index.__func__)) + assert expected == actual + + expected = inspect.getargspec(self.controller.static_index) + actual = util.getargspec(dec(True)( + self.controller.static_index)) + assert expected == actual diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan/util.py new/pecan-0.8.3/pecan/util.py --- old/pecan-0.7.0/pecan/util.py 2014-08-29 14:51:05.000000000 +0200 +++ new/pecan-0.8.3/pecan/util.py 2015-01-12 23:57:45.000000000 +0100 @@ -1,10 +1,44 @@ +import inspect import sys +import six + def iscontroller(obj): return getattr(obj, 'exposed', False) +def getargspec(method): + """ + Drill through layers of decorators attempting to locate the actual argspec + for a method. + """ + + argspec = inspect.getargspec(method) + args = argspec[0] + if args and args[0] == 'self': + return argspec + if hasattr(method, '__func__'): + method = method.__func__ + + func_closure = six.get_function_closure(method) + + # NOTE(sileht): if the closure is None we cannot look deeper, + # so return actual argspec, this occurs when the method + # is static for example. + if func_closure is None: + return argspec + + closure = next( + ( + c for c in func_closure if six.callable(c.cell_contents) + ), + None + ) + method = closure.cell_contents + return getargspec(method) + + def _cfg(f): if not hasattr(f, '_pecan'): f._pecan = {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan.egg-info/PKG-INFO new/pecan-0.8.3/pecan.egg-info/PKG-INFO --- old/pecan-0.7.0/pecan.egg-info/PKG-INFO 2014-08-29 14:51:16.000000000 +0200 +++ new/pecan-0.8.3/pecan.egg-info/PKG-INFO 2015-01-12 23:57:54.000000000 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 1.1 Name: pecan -Version: 0.7.0 -Summary: A WSGI object-dispatching web framework, designed to be lean and fast, with few dependancies. +Version: 0.8.3 +Summary: A WSGI object-dispatching web framework, designed to be lean and fast, with few dependencies. Home-page: http://github.com/stackforge/pecan Author: Jonathan LaCour Author-email: info@pecanpy.org diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan.egg-info/SOURCES.txt new/pecan-0.8.3/pecan.egg-info/SOURCES.txt --- old/pecan-0.7.0/pecan.egg-info/SOURCES.txt 2014-08-29 14:51:16.000000000 +0200 +++ new/pecan-0.8.3/pecan.egg-info/SOURCES.txt 2015-01-12 23:57:54.000000000 +0100 @@ -91,6 +91,7 @@ pecan/tests/test_scaffolds.py pecan/tests/test_secure.py pecan/tests/test_templating.py +pecan/tests/test_util.py pecan/tests/config_fixtures/config.py pecan/tests/config_fixtures/empty.py pecan/tests/config_fixtures/foobar.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/pecan.egg-info/requires.txt new/pecan-0.8.3/pecan.egg-info/requires.txt --- old/pecan-0.7.0/pecan.egg-info/requires.txt 2014-08-29 14:51:16.000000000 +0200 +++ new/pecan-0.8.3/pecan.egg-info/requires.txt 2015-01-12 23:57:54.000000000 +0100 @@ -3,4 +3,4 @@ WebTest>=1.3.1 six logutils>=0.3 -singledispatch \ No newline at end of file +singledispatch diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/setup.cfg new/pecan-0.8.3/setup.cfg --- old/pecan-0.7.0/setup.cfg 2014-08-29 14:51:16.000000000 +0200 +++ new/pecan-0.8.3/setup.cfg 2015-01-12 23:57:54.000000000 +0100 @@ -9,7 +9,7 @@ norecursedirs = +package+ config_fixtures docs .git *.egg .tox [egg_info] -tag_svn_revision = 0 tag_date = 0 +tag_svn_revision = 0 tag_build = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pecan-0.7.0/setup.py new/pecan-0.8.3/setup.py --- old/pecan-0.7.0/setup.py 2014-08-29 14:51:06.000000000 +0200 +++ new/pecan-0.8.3/setup.py 2015-01-12 23:57:46.000000000 +0100 @@ -3,7 +3,7 @@ from setuptools import setup, find_packages -version = '0.7.0' +version = '0.8.3' # # determine requirements @@ -70,7 +70,7 @@ name='pecan', version=version, description="A WSGI object-dispatching web framework, designed to be " - "lean and fast, with few dependancies.", + "lean and fast, with few dependencies.", long_description=None, classifiers=[ 'Development Status :: 5 - Production/Stable', -- To unsubscribe, e-mail: opensuse-commit+unsubscribe@opensuse.org For additional commands, e-mail: opensuse-commit+help@opensuse.org