From d1209e077a1607440677a363651bda4393d72d82 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 16 Apr 2009 20:31:40 +0000 Subject: - The interface for ``repoze.bfg.interfaces.ITraverser`` and the built-in implementations that implement the interface (``repoze.bfg.traversal.ModelGraphTraverser``, and ``repoze.bfg.urldispatch.RoutesModelTraverser``) now expect the ``__call__`` method of an ITraverser to return 3 additional arguments: ``traversed``, ``virtual_root``, and ``virtual_root_path`` (the old contract was that the ``__call__`` method of an ITraverser returned; three arguments, the contract new is that it returns six). ``traversed`` will be a sequence of Unicode names that were traversed (including the virtual root path, if any) or ``None`` if no traversal was performed, ``virtual_root`` will be a model object representing the virtual root (or the physical root if traversal was not performed), and ``virtual_root_path`` will be a sequence representing the virtual root path (a sequence of Unicode names) or ``None`` if traversal was not performed. Six arguments are now returned from BFG ITraversers. They are returned in this order: ``context``, ``view_name``, ``subpath``, ``traversed``, ``virtual_root``, and ``virtual_root_path``. Places in the BFG code which called an ITraverser continue to accept a 3-argument return value, although BFG will generate and log a warning when one is encountered. - The request object now has the following attributes: ``traversed`` (the sequence of names traversed or ``None`` if traversal was not performed), ``virtual_root`` (the model object representing the virtual root, including the virtual root path if any), and ``virtual_root_path`` (the seuquence of names representing the virtual root path or ``None`` if traversal was not performed). - A new decorator named ``wsgiapp2`` was added to the ``repoze.bfg.wsgi`` module. This decorator performs the same function as ``repoze.bfg.wsgi.wsgiapp`` except it fixes up the ``SCRIPT_NAME``, and ``PATH_INFO`` environment values before invoking the WSGI subapplication. - The ``repoze.bfg.testing.DummyRequest`` object now has default attributes for ``traversed``, ``virtual_root``, and ``virtual_root_path``. - The RoutesModelTraverser now behaves more like the Routes "RoutesMiddleware" object when an element in the match dict is named ``path_info`` (usually when there's a pattern like ``http://foo/*path_info``). When this is the case, the ``PATH_INFO`` environment variable is set to the value in the match dict, and the ``SCRIPT_NAME`` is appended to with the prefix of the original ``PATH_INFO`` not including the value of the new variable. - The notfound debug now shows the traversed path, the virtual root, and the virtual root path too. --- CHANGES.txt | 59 ++++++++++++++++++++++ docs/api/wsgi.rst | 1 + docs/glossary.rst | 5 ++ docs/narr/traversal.rst | 16 ++++++ repoze/bfg/interfaces.py | 16 +++++- repoze/bfg/router.py | 31 ++++++++++-- repoze/bfg/testing.py | 3 ++ repoze/bfg/tests/test_integration.py | 1 + repoze/bfg/tests/test_router.py | 36 ++++++++++++- repoze/bfg/tests/test_traversal.py | 49 ++++++++++++++---- repoze/bfg/tests/test_urldispatch.py | 34 +++++++++++++ repoze/bfg/tests/test_wsgi.py | 97 ++++++++++++++++++++++++++++++++++++ repoze/bfg/traversal.py | 48 ++++++++++++++---- repoze/bfg/urldispatch.py | 28 ++++++++--- repoze/bfg/wsgi.py | 72 +++++++++++++++++++++++++- 15 files changed, 462 insertions(+), 34 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 463daf95c..3a6006c07 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,62 @@ +Next release +============ + +Features +-------- + +- The interface for ``repoze.bfg.interfaces.ITraverser`` and the + built-in implementations that implement the interface + (``repoze.bfg.traversal.ModelGraphTraverser``, and + ``repoze.bfg.urldispatch.RoutesModelTraverser``) now expect the + ``__call__`` method of an ITraverser to return 3 additional + arguments: ``traversed``, ``virtual_root``, and + ``virtual_root_path`` (the old contract was that the ``__call__`` + method of an ITraverser returned; three arguments, the contract new + is that it returns six). ``traversed`` will be a sequence of + Unicode names that were traversed (including the virtual root path, + if any) or ``None`` if no traversal was performed, ``virtual_root`` + will be a model object representing the virtual root (or the + physical root if traversal was not performed), and + ``virtual_root_path`` will be a sequence representing the virtual + root path (a sequence of Unicode names) or ``None`` if traversal was + not performed. + + Six arguments are now returned from BFG ITraversers. They are + returned in this order: ``context``, ``view_name``, ``subpath``, + ``traversed``, ``virtual_root``, and ``virtual_root_path``. + + Places in the BFG code which called an ITraverser continue to accept + a 3-argument return value, although BFG will generate and log a + warning when one is encountered. + +- The request object now has the following attributes: ``traversed`` + (the sequence of names traversed or ``None`` if traversal was not + performed), ``virtual_root`` (the model object representing the + virtual root, including the virtual root path if any), and + ``virtual_root_path`` (the seuquence of names representing the + virtual root path or ``None`` if traversal was not performed). + +- A new decorator named ``wsgiapp2`` was added to the + ``repoze.bfg.wsgi`` module. This decorator performs the same + function as ``repoze.bfg.wsgi.wsgiapp`` except it fixes up the + ``SCRIPT_NAME``, and ``PATH_INFO`` environment values before + invoking the WSGI subapplication. + +- The ``repoze.bfg.testing.DummyRequest`` object now has default + attributes for ``traversed``, ``virtual_root``, and + ``virtual_root_path``. + +- The RoutesModelTraverser now behaves more like the Routes + "RoutesMiddleware" object when an element in the match dict is named + ``path_info`` (usually when there's a pattern like + ``http://foo/*path_info``). When this is the case, the + ``PATH_INFO`` environment variable is set to the value in the match + dict, and the ``SCRIPT_NAME`` is appended to with the prefix of the + original ``PATH_INFO`` not including the value of the new variable. + +- The notfound debug now shows the traversed path, the virtual root, + and the virtual root path too. + 0.7.0 (2009-04-11) ================== diff --git a/docs/api/wsgi.rst b/docs/api/wsgi.rst index 15bde75ed..6b5423c55 100644 --- a/docs/api/wsgi.rst +++ b/docs/api/wsgi.rst @@ -7,3 +7,4 @@ .. autofunction:: wsgiapp + .. autofunction:: wsgiapp2 diff --git a/docs/glossary.rst b/docs/glossary.rst index a0484bc65..5427a7602 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -339,3 +339,8 @@ Glossary alternative mechanisms for common :mod:`repoze.bfg` application configuration tasks. The functionality of this package has been merged into the :mod:`repoze.bfg` core as of version 0.6.3. + Virtual root + A model object representing the "virtual" root of a request; this + is typically the physical root object (the object returned by the + application root factory) unless :ref:`vhosting_chapter` is in + use. diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 9ff7329c5..3977f46d0 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -282,6 +282,22 @@ The :term:`context` will always be available to a view as the ``context`` attribute of the :term:`request` object. It will be the context object implied by the current request. +The "traversal path" will always be available to a view as the +``traversed`` attribute of the :term:`request` object. It will be a +sequence representing the ordered set of names that were used to +traverse to the context, not including the view name or subpath. If +there is a virtual root associated with request, the virtual root path +is included within the traversal path. + +The :term:`virtual root` will always be available to a view as the +``virtual_root`` attribute of the :term:`request` object. It will be +the virtual root object implied by the current request. + +The :term:`virtual root path` will always be available to a view as +the ``virtual_root_path`` attribute of the :term:`request` object. It +will be a sequence representing the ordered set of names that were +used to traverse to the virtual root obejct. + Unicode and Traversal --------------------- diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index 80a6bda26..cb0220a2e 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -77,8 +77,20 @@ class IRootFactory(Interface): class ITraverser(Interface): def __call__(environ): - """ Return a tuple in the form (context, name, subpath), typically - the result of an object graph traversal """ + """ Return a tuple in the form ``(context, view_name, subpath, + traversed, virtual_root, virtual_root_path)`` , typically the + result of an object graph traversal. ``context`` will be a + model object, ``view_name`` will be the view name used (a + Unicode name), ``subpath`` will be a sequence of Unicode names + that followed the view name but were not traversed, + ``traversed`` will be a sequence of Unicode names that were + traversed (including the virtual root path, if any) or + ``None`` if no traversal was performed, ``virtual_root`` will + be a model object representing the virtual root (or the + physical root if traversal was not performed), and + ``virtual_root_path`` will be a sequence representing the + virtual root path (a sequence of Unicode names) or None if + traversal was not performed.""" class ITraverserFactory(Interface): def __call__(context): diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index bc4a8cf7b..5278da892 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -72,6 +72,7 @@ class Router(object): self.logger = registry.queryUtility(ILogger, 'repoze.bfg.debug') self.root_factory = registry.getUtility(IRootFactory) self.root_policy = self.root_factory # b/w compat + self.traverser_warned = {} def __call__(self, environ, start_response): """ @@ -99,7 +100,24 @@ class Router(object): registry.has_listeners and registry.notify(NewRequest(request)) root = self.root_factory(environ) traverser = registry.getAdapter(root, ITraverserFactory) - context, view_name, subpath = traverser(environ) + vals = traverser(environ) + + try: + context, view_name, subpath, traversed, vroot, vroot_path = vals + except ValueError: + if not (traverser.__class__ in self.traverser_warned): + self.logger and self.logger.warn( + '%s is an pre-0.7.1-style ITraverser returning only ' + '3 arguments; please update it to the new ' + '6-argument-returning interface for improved ' + 'functionality. See the repoze.bfg.interfaces module ' + 'for the new ITraverser interface ' + 'definition' % traverser) + self.traverser_warned[traverser.__class__] = True + context, view_name, subpath = vals + traversed = [] + vroot = root + vroot_path = [] if isinstance(request, WebObRequest): # webob.Request's __setattr__ (as of 0.9.5 and lower) @@ -111,11 +129,17 @@ class Router(object): attrs['context'] = context attrs['view_name'] = view_name attrs['subpath'] = subpath + attrs['traversed'] = traversed + attrs['virtual_root'] = vroot + attrs['virtual_root_path'] = vroot_path else: request.root = root request.context = context request.view_name = view_name request.subpath = subpath + request.traversed = traversed + request.virtual_root = vroot + request.virtual_root_path = vroot_path security_policy = self.security_policy @@ -156,9 +180,10 @@ class Router(object): if self.debug_notfound: msg = ( 'debug_notfound of url %s; path_info: %r, context: %r, ' - 'view_name: %r, subpath: %r' % ( + 'view_name: %r, subpath: %r, traversed: %r, ' + 'vroot: %r, vroot_path: %r' % ( request.url, request.path_info, context, view_name, - subpath) + subpath, traversed, vroot, vroot_path) ) logger and logger.debug(msg) else: diff --git a/repoze/bfg/testing.py b/repoze/bfg/testing.py index 1ad8ddf17..924f2602a 100644 --- a/repoze/bfg/testing.py +++ b/repoze/bfg/testing.py @@ -364,6 +364,9 @@ class DummyRequest: self.body = '' self.view_name = '' self.subpath = [] + self.traversed = None + self.virtual_root = None + self.virtual_root_path = None self.context = None self.marshalled = params # repoze.monty self.__dict__.update(kw) diff --git a/repoze/bfg/tests/test_integration.py b/repoze/bfg/tests/test_integration.py index b6e5e1c0c..1159ca380 100644 --- a/repoze/bfg/tests/test_integration.py +++ b/repoze/bfg/tests/test_integration.py @@ -160,6 +160,7 @@ class DummyContext: class DummyRequest: subpath = ('__init__.py',) + traversed = None environ = {'REQUEST_METHOD':'GET', 'wsgi.version':(1,0)} def get_response(self, application): return application(None, None) diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index 29849e396..dd249a8fe 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -21,6 +21,7 @@ class RouterTests(unittest.TestCase): self.messages = [] def info(self, msg): self.messages.append(msg) + warn = info debug = info logger = Logger() self.registry.registerUtility(logger, ILogger, name='repoze.bfg.debug') @@ -93,6 +94,28 @@ class RouterTests(unittest.TestCase): router = self._makeOne() self.assertEqual(router.root_policy, rootfactory) + def test_3arg_policy(self): + rootfactory = make_rootfactory(None) + environ = self._makeEnviron() + context = DummyContext() + traversalfactory = make_3arg_traversal_factory(context, '', []) + self._registerTraverserFactory(traversalfactory, '', None) + logger = self._registerLogger() + self._registerRootFactory(rootfactory) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.failUnless(traversalfactory in router.traverser_warned) + headers = start_response.headers + self.assertEqual(len(headers), 2) + status = start_response.status + self.assertEqual(status, '404 Not Found') + self.failUnless('http://localhost:8080' in result[0], result) + self.failIf('debug_notfound' in result[0]) + self.assertEqual(len(logger.messages), 1) + message = logger.messages[0] + self.failUnless('is an pre-0.7.1-style ITraverser ' in message) + def test_call_no_view_registered_no_isettings(self): rootfactory = make_rootfactory(None) environ = self._makeEnviron() @@ -717,7 +740,17 @@ def make_view(response): return response return view -def make_traversal_factory(context, name, subpath): +def make_traversal_factory(context, name, subpath, vroot=None, + vroot_path=(), traversed=()): + class DummyTraversalFactory: + def __init__(self, root): + self.root = root + + def __call__(self, path): + return context, name, subpath, traversed, vroot, vroot_path + return DummyTraversalFactory + +def make_3arg_traversal_factory(context, name, subpath): class DummyTraversalFactory: def __init__(self, root): self.root = root @@ -726,6 +759,7 @@ def make_traversal_factory(context, name, subpath): return context, name, subpath return DummyTraversalFactory + def make_permission_factory(result): class DummyPermissionFactory: def __init__(self, context, request): diff --git a/repoze/bfg/tests/test_traversal.py b/repoze/bfg/tests/test_traversal.py index 9cdf04910..18e9a9f18 100644 --- a/repoze/bfg/tests/test_traversal.py +++ b/repoze/bfg/tests/test_traversal.py @@ -83,70 +83,95 @@ class ModelGraphTraverserTests(unittest.TestCase): def test_call_with_no_pathinfo(self): policy = self._makeOne(None) environ = self._getEnviron() - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, None) self.assertEqual(name, '') self.assertEqual(subpath, []) + self.assertEqual(traversed, []) + self.assertEqual(vroot, policy.root) + self.assertEqual(vroot_path, []) def test_call_pathel_with_no_getitem(self): policy = self._makeOne(None) environ = self._getEnviron(PATH_INFO='/foo/bar') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, None) self.assertEqual(name, 'foo') self.assertEqual(subpath, ['bar']) + self.assertEqual(traversed, []) + self.assertEqual(vroot, policy.root) + self.assertEqual(vroot_path, []) def test_call_withconn_getitem_emptypath_nosubpath(self): root = DummyContext() policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, root) self.assertEqual(name, '') self.assertEqual(subpath, []) + self.assertEqual(traversed, []) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_call_withconn_getitem_withpath_nosubpath(self): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='/foo/bar') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, foo) self.assertEqual(name, 'bar') self.assertEqual(subpath, []) + self.assertEqual(traversed, [u'foo']) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_call_withconn_getitem_withpath_withsubpath(self): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='/foo/bar/baz/buz') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, foo) self.assertEqual(name, 'bar') self.assertEqual(subpath, ['baz', 'buz']) + self.assertEqual(traversed, [u'foo']) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_call_with_explicit_viewname(self): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='/@@foo') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, root) self.assertEqual(name, 'foo') self.assertEqual(subpath, []) + self.assertEqual(traversed, []) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_call_with_vh_root(self): environ = self._getEnviron(PATH_INFO='/baz', HTTP_X_VHM_ROOT='/foo/bar') baz = DummyContext() + baz.name = 'baz' bar = DummyContext(baz) + bar.name = 'bar' foo = DummyContext(bar) + foo.name = 'foo' root = DummyContext(foo) + root.name = 'root' policy = self._makeOne(root) - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(ctx, baz) self.assertEqual(name, '') self.assertEqual(subpath, []) + self.assertEqual(traversed, [u'foo', u'bar', u'baz']) + self.assertEqual(vroot, bar) + self.assertEqual(vroot_path, [u'foo', u'bar']) def test_call_with_ILocation_root_proxies(self): baz = DummyContext() @@ -161,7 +186,7 @@ class ModelGraphTraverserTests(unittest.TestCase): root.__parent__ = None policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='/foo/bar/baz') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(name, '') self.assertEqual(subpath, []) self.assertEqual(ctx, baz) @@ -177,6 +202,9 @@ class ModelGraphTraverserTests(unittest.TestCase): self.failIf(isProxy(ctx.__parent__.__parent__.__parent__)) self.assertEqual(ctx.__parent__.__parent__.__parent__.__name__, None) self.assertEqual(ctx.__parent__.__parent__.__parent__.__parent__, None) + self.assertEqual(traversed, [u'foo', u'bar', u'baz']) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_call_with_ILocation_root_proxies_til_next_ILocation(self): # This is a test of an insane setup; it tests the case where @@ -200,7 +228,7 @@ class ModelGraphTraverserTests(unittest.TestCase): root.__parent__ = None policy = self._makeOne(root) environ = self._getEnviron(PATH_INFO='/foo/bar/baz') - ctx, name, subpath = policy(environ) + ctx, name, subpath, traversed, vroot, vroot_path = policy(environ) self.assertEqual(name, '') self.assertEqual(subpath, []) self.assertEqual(ctx, baz) @@ -208,6 +236,9 @@ class ModelGraphTraverserTests(unittest.TestCase): self.assertEqual(ctx.__name__, 'baz') self.assertEqual(ctx.__parent__, bar) self.failIf(isProxy(ctx.__parent__)) + self.assertEqual(traversed, [u'foo', u'bar', u'baz']) + self.assertEqual(vroot, root) + self.assertEqual(vroot_path, []) def test_non_utf8_path_segment_unicode_path_segments_fails(self): foo = DummyContext() diff --git a/repoze/bfg/tests/test_urldispatch.py b/repoze/bfg/tests/test_urldispatch.py index 0fc828161..fec394d56 100644 --- a/repoze/bfg/tests/test_urldispatch.py +++ b/repoze/bfg/tests/test_urldispatch.py @@ -248,6 +248,9 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], 'controller') self.assertEqual(result[2], []) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) def test_call_with_only_view_name_bwcompat(self): model = DummyContext() @@ -257,6 +260,9 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], 'view_name') self.assertEqual(result[2], []) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) def test_call_with_subpath_bwcompat(self): model = DummyContext() @@ -267,6 +273,9 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], 'view_name') self.assertEqual(result[2], ['a', 'b', 'c']) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) def test_call_with_no_view_name_or_controller_bwcompat(self): model = DummyContext() @@ -275,6 +284,9 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], '') self.assertEqual(result[2], []) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) def test_call_with_only_view_name(self): model = DummyContext() @@ -285,6 +297,9 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], 'view_name') self.assertEqual(result[2], []) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) def test_call_with_view_name_and_subpath(self): model = DummyContext() @@ -295,6 +310,25 @@ class RoutesModelTraverserTests(unittest.TestCase): self.assertEqual(result[0], model) self.assertEqual(result[1], 'view_name') self.assertEqual(result[2], ['a', 'b','c']) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) + + def test_with_path_info(self): + model = DummyContext() + traverser = self._makeOne(model) + routing_args = ((), {'view_name':'view_name', 'path_info':'foo/bar'}) + environ = {'wsgiorg.routing_args': routing_args, + 'PATH_INFO':'/a/b/foo/bar', 'SCRIPT_NAME':''} + result = traverser(environ) + self.assertEqual(result[0], model) + self.assertEqual(result[1], 'view_name') + self.assertEqual(result[2], []) + self.assertEqual(result[3], None) + self.assertEqual(result[4], model) + self.assertEqual(result[5], None) + self.assertEqual(environ['PATH_INFO'], '/foo/bar') + self.assertEqual(environ['SCRIPT_NAME'], '/a/b') class RoutesContextURLTests(unittest.TestCase): def _getTargetClass(self): diff --git a/repoze/bfg/tests/test_wsgi.py b/repoze/bfg/tests/test_wsgi.py index 1fd2393b8..b9568eb82 100644 --- a/repoze/bfg/tests/test_wsgi.py +++ b/repoze/bfg/tests/test_wsgi.py @@ -12,6 +12,103 @@ class WSGIAppTests(unittest.TestCase): response = decorator(context, request) self.assertEqual(response, dummyapp) +class WSGIApp2Tests(unittest.TestCase): + def _callFUT(self, app): + from repoze.bfg.wsgi import wsgiapp2 + return wsgiapp2(app) + + def test_decorator_traversed_is_None(self): + context = DummyContext() + request = DummyRequest() + request.traversed = None + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + + def test_decorator_traversed_not_None_with_subpath_and_view_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = ['a', 'b'] + request.virtual_root_path = ['a'] + request.subpath = ['subpath'] + request.view_name = 'view_name' + request.environ = {'SCRIPT_NAME':'/foo'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/subpath') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b/view_name') + + def test_decorator_traversed_not_None_with_subpath_no_view_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = ['a', 'b'] + request.virtual_root_path = ['a'] + request.subpath = ['subpath'] + request.view_name = '' + request.environ = {'SCRIPT_NAME':'/foo'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/subpath') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b') + + def test_decorator_traversed_not_None_no_subpath_with_view_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = ['a', 'b'] + request.virtual_root_path = ['a'] + request.subpath = [] + request.view_name = 'view_name' + request.environ = {'SCRIPT_NAME':'/foo'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b/view_name') + + def test_decorator_traversed_empty_with_view_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = [] + request.virtual_root_path = [] + request.subpath = [] + request.view_name = 'view_name' + request.environ = {'SCRIPT_NAME':'/foo'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/view_name') + + def test_decorator_traversed_empty_no_view_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = [] + request.virtual_root_path = [] + request.subpath = [] + request.view_name = '' + request.environ = {'SCRIPT_NAME':'/foo'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo') + + def test_decorator_traversed_empty_no_view_name_no_script_name(self): + context = DummyContext() + request = DummyRequest() + request.traversed = [] + request.virtual_root_path = [] + request.subpath = [] + request.view_name = '' + request.environ = {'SCRIPT_NAME':''} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + class TestNotFound(unittest.TestCase): def _getTargetClass(self): from repoze.bfg.wsgi import NotFound diff --git a/repoze/bfg/traversal.py b/repoze/bfg/traversal.py index 5935d37d7..60d388809 100644 --- a/repoze/bfg/traversal.py +++ b/repoze/bfg/traversal.py @@ -2,6 +2,7 @@ import re import urllib from zope.component import getMultiAdapter +from zope.component import queryUtility from zope.deferredimport import deprecated @@ -18,6 +19,7 @@ from repoze.bfg.interfaces import ILocation from repoze.bfg.interfaces import ITraverser from repoze.bfg.interfaces import ITraverserFactory from repoze.bfg.interfaces import VH_ROOT_KEY +from repoze.bfg.interfaces import ILogger deprecated( "('from repoze.bfg.traversal import model_url' is now " @@ -111,7 +113,21 @@ def find_model(model, path): if path and path[0] == '/': model = find_root(model) - ob, name, _ = ITraverserFactory(model)({'PATH_INFO':path}) + traverser = ITraverserFactory(model) + val = traverser({'PATH_INFO':path}) + try: + ob, name, _, _, _, _ = val + except ValueError: + # b/w compat for three-value-returning ITraverser (prior to 0.7.1) + logger = queryUtility(ILogger, 'repoze.bfg.debug') + logger and logger.warn( + '%s is an pre-0.7.1-style ITraverser returning only ' + '3 arguments; please update it to the new ' + '6-argument-returning interface for improved ' + 'functionality. See the repoze.bfg.interfaces module ' + 'for the new ITraverser interface ' + 'definition' % traverser) + ob, name, _ = val if name: raise KeyError('%r has no subelement %s' % (ob, name)) return ob @@ -375,35 +391,45 @@ class ModelGraphTraverser(object): except KeyError: path = '/' try: - vroot = environ[VH_ROOT_KEY] - path = vroot + path + vroot_path_string = environ[VH_ROOT_KEY] except KeyError: - pass - - path = traversal_path(path) + vroot_path = [] + vroot_idx = 0 + else: + vroot_path = list(traversal_path(vroot_path_string)) + vroot_idx = len(vroot_path) + path = vroot_path_string + path - ob = self.root + path = list(traversal_path(path)) + + traversed = [] + + ob = vroot = self.root name = '' locatable = ILocation.providedBy(ob) i = 1 + for segment in path: if segment[:2] =='@@': - return ob, segment[2:], list(path[i:]) + return ob, segment[2:], path[i:], traversed, vroot, vroot_path try: getitem = ob.__getitem__ except AttributeError: - return ob, segment, list(path[i:]) + return ob, segment, path[i:], traversed, vroot, vroot_path try: next = getitem(segment) except KeyError: - return ob, segment, list(path[i:]) + return ob, segment, path[i:], traversed, vroot, vroot_path if locatable and (not ILocation.providedBy(next)): next = LocationProxy(next, ob, segment) + if vroot_idx == i-1: + vroot = ob + traversed.append(segment) ob = next i += 1 - return ob, '', [] + return ob, '', [], traversed, vroot, vroot_path class TraversalContextURL(object): """ The IContextURL adapter used to generate URLs for a context diff --git a/repoze/bfg/urldispatch.py b/repoze/bfg/urldispatch.py index 03b73d6c6..96650f9a5 100644 --- a/repoze/bfg/urldispatch.py +++ b/repoze/bfg/urldispatch.py @@ -1,3 +1,5 @@ +import re + from zope.interface import implements from zope.interface import alsoProvides @@ -210,26 +212,40 @@ class RoutesModelTraverser(object): # compatibility try: # 0.6.5 + - routing_args = environ['wsgiorg.routing_args'][1] + match = environ['wsgiorg.routing_args'][1] except KeyError: # <= 0.6.4 - routing_args = self.context.__dict__ + match = self.context.__dict__ try: - view_name = routing_args['view_name'] + view_name = match['view_name'] except KeyError: # b/w compat < 0.6.3 try: - view_name = routing_args['controller'] + view_name = match['controller'] except KeyError: view_name = '' try: - subpath = routing_args['subpath'] + subpath = match['subpath'] subpath = filter(None, subpath.split('/')) except KeyError: # b/w compat < 0.6.5 subpath = [] - return self.context, view_name, subpath + if 'path_info' in match: + # this is stolen from routes.middleware; if the route map + # has a *path_info capture, use it to influence the path + # info and script_name of the generated environment + oldpath = environ['PATH_INFO'] + newpath = match['path_info'] or '' + environ['PATH_INFO'] = newpath + if not environ['PATH_INFO'].startswith('/'): + environ['PATH_INFO'] = '/' + environ['PATH_INFO'] + pattern = r'^(.*?)/' + re.escape(newpath) + '$' + environ['SCRIPT_NAME'] += re.sub(pattern, r'\1', oldpath) + if environ['SCRIPT_NAME'].endswith('/'): + environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'][:-1] + + return self.context, view_name, subpath, None, self.context, None class RoutesContextURL(object): """ The IContextURL adapter used to generate URLs for a context diff --git a/repoze/bfg/wsgi.py b/repoze/bfg/wsgi.py index 667de4412..9e524ce6f 100644 --- a/repoze/bfg/wsgi.py +++ b/repoze/bfg/wsgi.py @@ -6,8 +6,47 @@ except ImportError: # < 2.5 from repoze.bfg.functional import wraps +from repoze.bfg.traversal import quote_path_segment + def wsgiapp(wrapped): - """ Decorator to turn a WSGI application into a repoze.bfg view callable. + """ Decorator to turn a WSGI application into a repoze.bfg view + callable. This decorator differs from the `wsgiapp2`` decorator + inasmuch as fixups of ``PATH_INFO`` and ``SCRIPT_NAME`` within the + WSGI environment *are not* performed before the application is + invoked. + + E.g.:: + + @wsgiapp + def hello_world(environ, start_response): + body = 'Hello world' + start_response('200 OK', [ ('Content-Type', 'text/plain'), + ('Content-Length', len(body)) ] ) + return [body] + + Allows the following view declaration to be made:: + + + + The wsgiapp decorator will convert the result of the WSGI + application to a Response and return it to repoze.bfg as if the + WSGI app were a repoze.bfg view. + + """ + def decorator(context, request): + return request.get_response(wrapped) + return wraps(wrapped)(decorator) # pickleability + +def wsgiapp2(wrapped): + """ Decorator to turn a WSGI application into a repoze.bfg view + callable. This decorator differs from the `wsgiapp`` decorator + inasmuch as fixups of ``PATH_INFO`` and ``SCRIPT_NAME`` within the + WSGI environment *are* performed before the application is + invoked. E.g.:: @@ -28,9 +67,38 @@ def wsgiapp(wrapped): The wsgiapp decorator will convert the result of the WSGI application to a Response and return it to repoze.bfg as if the - WSGI app were a repoze.bfg view. + WSGI app were a repoze.bfg view. The ``SCRIPT_NAME`` and + ``PATH_INFO`` values present in the WSGI environment are fixed up + before the application is invoked. """ def decorator(context, request): + traversed = request.traversed + if traversed is not None: + # We need to fix up PATH_INFO and SCRIPT_NAME to give the + # subapplication the right information, sans the info it + # took to traverse here. If ``traversed`` is None here, + # it means that no traversal was done. For example, it + # will be None in the case that the context is one + # obtained via a Routes match (Routes 'traversal' doesn't + # actually traverse). If this view is invoked on a Routes + # context, this fixup is not invoked. Instead, the route + # used to reach it should use *path_info in the actual + # route pattern to get a similar fix-up done. + vroot_path = request.virtual_root_path or [] + view_name = request.view_name + subpath = request.subpath or [] + script_list = traversed[len(vroot_path):] + script_list = [ quote_path_segment(name) for name in script_list ] + if view_name: + script_list.append(quote_path_segment(view_name)) + script_name = '/' + '/'.join(script_list) + path_list = [ quote_path_segment(name) for name in subpath ] + path_info = '/' + '/'.join(path_list) + request.environ['PATH_INFO'] = path_info + script_name = request.environ['SCRIPT_NAME'] + script_name + if script_name.endswith('/'): + script_name = script_name[:-1] + request.environ['SCRIPT_NAME'] = script_name return request.get_response(wrapped) return wraps(wrapped)(decorator) # pickleability -- cgit v1.2.3