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 - [email protected]
+
+- 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: [email protected]
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: [email protected]
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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to