summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-01-27 21:57:11 +0000
committerChris McDonough <chrism@agendaless.com>2009-01-27 21:57:11 +0000
commite62e479e338e428f6cfd3b07790545982b7cb94f (patch)
treec9784577f791d4a8ea5b80a9fce211ce86009712
parent2301cf61977102b85279ea7c04797f76012202e5 (diff)
downloadpyramid-e62e479e338e428f6cfd3b07790545982b7cb94f.tar.gz
pyramid-e62e479e338e428f6cfd3b07790545982b7cb94f.tar.bz2
pyramid-e62e479e338e428f6cfd3b07790545982b7cb94f.zip
Features
-------- - The ``repoze.bfg.url.model_url`` API now works against contexts derived from Routes URL dispatch (``Routes.util.url_for`` is called under the hood). - "Virtual root" support for traversal-based applications has been added. Virtual root support is useful when you'd like to host some model in a :mod:`repoze.bfg` model graph as an application under a URL pathname that does not include the model path itself. For more information, see the (new) "Virtual Hosting" chapter in the documentation. - A ``repoze.bfg.traversal.virtual_root`` API has been added. When called, it returns the virtual root object (or the physical root object if no virtual root has been specified). Implementation Changes ---------------------- - ``repoze.bfg.traversal.RoutesModelTraverser`` has been moved to ``repoze.bfg.urldispatch``. - ``model_url`` URL generation is now performed via an adapter lookup based on the context and the request. - ZCML which registers two adapters for the ``IContextURL`` interface has been added to the configure.zcml in ``repoze.bfg.includes``.
-rw-r--r--CHANGES.txt33
-rw-r--r--docs/api/traversal.rst2
-rw-r--r--docs/index.rst1
-rw-r--r--docs/narr/vhosting.rst69
-rw-r--r--repoze/bfg/includes/configure.zcml18
-rw-r--r--repoze/bfg/interfaces.py14
-rw-r--r--repoze/bfg/tests/test_integration.py2
-rw-r--r--repoze/bfg/tests/test_traversal.py231
-rw-r--r--repoze/bfg/tests/test_url.py158
-rw-r--r--repoze/bfg/tests/test_urldispatch.py126
-rw-r--r--repoze/bfg/traversal.py114
-rw-r--r--repoze/bfg/url.py55
-rw-r--r--repoze/bfg/urldispatch.py61
13 files changed, 622 insertions, 262 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 739786fa7..21818b846 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,36 @@
+Next Release
+============
+
+Features
+--------
+
+- The ``repoze.bfg.url.model_url`` API now works against contexts
+ derived from Routes URL dispatch (``Routes.util.url_for`` is called
+ under the hood).
+
+- "Virtual root" support for traversal-based applications has been
+ added. Virtual root support is useful when you'd like to host some
+ model in a :mod:`repoze.bfg` model graph as an application under a
+ URL pathname that does not include the model path itself. For more
+ information, see the (new) "Virtual Hosting" chapter in the
+ documentation.
+
+- A ``repoze.bfg.traversal.virtual_root`` API has been added. When
+ called, it returns the virtual root object (or the physical root
+ object if no virtual root has been specified).
+
+Implementation Changes
+----------------------
+
+- ``repoze.bfg.traversal.RoutesModelTraverser`` has been moved to
+ ``repoze.bfg.urldispatch``.
+
+- ``model_url`` URL generation is now performed via an adapter lookup
+ based on the context and the request.
+
+- ZCML which registers two adapters for the ``IContextURL`` interface
+ has been added to the configure.zcml in ``repoze.bfg.includes``.
+
0.6.6 (2009-01-26)
==================
diff --git a/docs/api/traversal.rst b/docs/api/traversal.rst
index d9b4aabfc..1fcc9a2c6 100644
--- a/docs/api/traversal.rst
+++ b/docs/api/traversal.rst
@@ -15,6 +15,8 @@
.. autofunction:: traversal_path(path)
+ .. autofunction:: virtual_root
+
.. note:: A function named ``model_url`` used to be present in this
module. It was moved to :ref:`url_module` in version 0.6.1.
diff --git a/docs/index.rst b/docs/index.rst
index 88250cd29..2fc022bc6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -35,6 +35,7 @@ Narrative documentation in chapter form explaining how to use
narr/templates
narr/models
narr/security
+ narr/vhosting
narr/events
narr/environment
narr/unittesting
diff --git a/docs/narr/vhosting.rst b/docs/narr/vhosting.rst
new file mode 100644
index 000000000..2cd1346e3
--- /dev/null
+++ b/docs/narr/vhosting.rst
@@ -0,0 +1,69 @@
+.. _vhosting_chapter:
+
+Virtual Hosting
+===============
+
+:mod:`repoze.bfg` supports a traditional form of virtual hosting
+provided by packages like Paste's `urlmap
+<http://pythonpaste.org/modules/urlmap.html>`_ middleware, where you
+can host a :mod:`repoze.bfg` application as a "subset" of some other
+site (e.g. ``http://example.com/mybfgapplication``). Nothing special
+needs to be done within a :mod:`repoze.bfg` application to make this
+work.
+
+However, :mod:`repoze.bfg` also supports "virtual roots", which can be
+used in :term:`traversal` -based (but not :term:`URL-dispatch` -based)
+applications. These are explained below.
+
+Virtual Root Support
+--------------------
+
+Virtual root support is useful when you'd like to host some model in a
+:mod:`repoze.bfg` model graph as an application under a URL pathname
+that does not include the model path itself. For example, you might
+want to serve the object at the traversal path ``/cms`` as an
+application on reachable via ``http://example.com/`` (as opposed to
+``http://example.com/cms``). To specify a virtual root, cause an
+environment variable to be inserted into the WSGI environ named
+``HTTP_X_VHM_ROOT`` with a value that is the absolute pathname to the
+model object in the traversal graph that should behave as the "root"
+model. As a result, the traversal machinery will respect this value
+during traversal (prepending it to the PATH_INFO before traversal
+starts), and the ``repoze.bfg.url.model_url`` API will generate the
+"correct" virtually-rooted URLs.
+
+An example of an Apache ``mod_proxy`` configuration that will host the
+``/cms`` subobject as ``http://www.example.com/`` using this facility
+is below:
+
+.. code-block:: xml
+
+ NameVirtualHost *:80
+
+ <VirtualHost *:80>
+ ServerName www.example.com
+ RewriteEngine On
+ RewriteRule ^/(.*) http://127.0.0.1:6543/$1 [L,P]
+ ProxyPreserveHost on
+ RequestHeader add X-Vhm-Root /cms
+ </VirtualHost>
+
+For a :mod:`repoze.bfg` application running under ``mod_wsgi``, the
+same can be achieved using ``SetEnv``:
+
+.. code-block:: xml
+
+ <Location />
+ SetEnv HTTP_X_VHM_ROOT /cms
+ </Location>
+
+Setting a virtual root has no effect when using an application based
+on :term:`URL dispatch`.
+
+Further Documentation and Examples
+----------------------------------
+
+The API documentation in :ref:`traversal_module` documents a
+``repoze.bfg.traversal.virtual_root`` API. When called, it returns
+the virtual root object (or the physical root object if no virtual
+root has been specified).
diff --git a/repoze/bfg/includes/configure.zcml b/repoze/bfg/includes/configure.zcml
index 07f8ab3fa..7a07e1775 100644
--- a/repoze/bfg/includes/configure.zcml
+++ b/repoze/bfg/includes/configure.zcml
@@ -4,6 +4,8 @@
<include package="chameleon.zpt" file="configure.zcml"/>
+ <!-- traversal adapters -->
+
<adapter
factory="repoze.bfg.traversal.ModelGraphTraverser"
provides="repoze.bfg.interfaces.ITraverserFactory"
@@ -11,11 +13,25 @@
/>
<adapter
- factory="repoze.bfg.traversal.RoutesModelTraverser"
+ factory="repoze.bfg.traversal.TraversalContextURL"
+ provides="repoze.bfg.interfaces.IContextURL"
+ for="*"
+ />
+
+ <!-- URL dispatch adapters -->
+
+ <adapter
+ factory="repoze.bfg.urldispatch.RoutesModelTraverser"
provides="repoze.bfg.interfaces.ITraverserFactory"
for="repoze.bfg.interfaces.IRoutesContext"
/>
+ <adapter
+ factory="repoze.bfg.urldispatch.RoutesContextURL"
+ provides="repoze.bfg.interfaces.IContextURL"
+ for="repoze.bfg.interfaces.IRoutesContext"
+ />
+
<include file="meta.zcml" />
</configure>
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index 7d4286ca3..80a6bda26 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -201,8 +201,14 @@ class IUnauthorizedAppFactory(Interface):
``message`` key in the WSGI environ provides information
pertaining to the reason for the unauthorized."""
-class IURLGenerator(Interface):
- """ A utility which provides virtual hosting services
+class IContextURL(Interface):
+ """ An adapter which deals with URLs related to a context.
"""
- def model_url(model, request):
- """ Return a URL that points to the model """
+ def virtual_root():
+ """ Return the virtual root related to a request and the
+ current context"""
+
+ def __call__():
+ """ Return a URL that points to the context """
+
+VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
diff --git a/repoze/bfg/tests/test_integration.py b/repoze/bfg/tests/test_integration.py
index 4c5777738..b6e5e1c0c 100644
--- a/repoze/bfg/tests/test_integration.py
+++ b/repoze/bfg/tests/test_integration.py
@@ -153,7 +153,7 @@ class TestGrokkedApp(unittest.TestCase):
actions = context.actions
import cPickle
self.assertRaises(cPickle.PicklingError, cPickle.dumps, actions, -1)
- self.assertEqual(len(actions), 5)
+ self.assertEqual(len(actions), 7)
class DummyContext:
pass
diff --git a/repoze/bfg/tests/test_traversal.py b/repoze/bfg/tests/test_traversal.py
index f21cf7d8d..48b968458 100644
--- a/repoze/bfg/tests/test_traversal.py
+++ b/repoze/bfg/tests/test_traversal.py
@@ -212,81 +212,6 @@ class ModelGraphTraverserTests(unittest.TestCase):
environ = self._getEnviron(PATH_INFO='/%s' % segment)
self.assertRaises(TypeError, policy, environ)
-class RoutesModelTraverserTests(unittest.TestCase):
- def _getTargetClass(self):
- from repoze.bfg.traversal import RoutesModelTraverser
- return RoutesModelTraverser
-
- def _makeOne(self, model):
- klass = self._getTargetClass()
- return klass(model)
-
- def test_class_conforms_to_ITraverser(self):
- from zope.interface.verify import verifyClass
- from repoze.bfg.interfaces import ITraverser
- verifyClass(ITraverser, self._getTargetClass())
-
- def test_instance_conforms_to_ITraverser(self):
- from zope.interface.verify import verifyObject
- from repoze.bfg.interfaces import ITraverser
- verifyObject(ITraverser, self._makeOne(None))
-
- def test_call_with_only_controller_bwcompat(self):
- model = DummyContext()
- model.controller = 'controller'
- traverser = self._makeOne(model)
- result = traverser({})
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], 'controller')
- self.assertEqual(result[2], [])
-
- def test_call_with_only_view_name_bwcompat(self):
- model = DummyContext()
- model.view_name = 'view_name'
- traverser = self._makeOne(model)
- result = traverser({})
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], 'view_name')
- self.assertEqual(result[2], [])
-
- def test_call_with_subpath_bwcompat(self):
- model = DummyContext()
- model.view_name = 'view_name'
- model.subpath = '/a/b/c'
- traverser = self._makeOne(model)
- result = traverser({})
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], 'view_name')
- self.assertEqual(result[2], ['a', 'b', 'c'])
-
- def test_call_with_no_view_name_or_controller_bwcompat(self):
- model = DummyContext()
- traverser = self._makeOne(model)
- result = traverser({})
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], '')
- self.assertEqual(result[2], [])
-
- def test_call_with_only_view_name(self):
- model = DummyContext()
- traverser = self._makeOne(model)
- routing_args = ((), {'view_name':'view_name'})
- environ = {'wsgiorg.routing_args': routing_args}
- result = traverser(environ)
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], 'view_name')
- self.assertEqual(result[2], [])
-
- def test_call_with_view_name_and_subpath(self):
- model = DummyContext()
- traverser = self._makeOne(model)
- routing_args = ((), {'view_name':'view_name', 'subpath':'/a/b/c'})
- environ = {'wsgiorg.routing_args': routing_args}
- result = traverser(environ)
- self.assertEqual(result[0], model)
- self.assertEqual(result[1], 'view_name')
- self.assertEqual(result[2], ['a', 'b','c'])
-
class FindInterfaceTests(unittest.TestCase):
def _callFUT(self, context, iface):
from repoze.bfg.traversal import find_interface
@@ -439,6 +364,152 @@ class ModelPathTests(unittest.TestCase):
result = self._callFUT(other)
self.assertEqual(result, '/other')
+class TraversalContextURLTests(unittest.TestCase):
+ def _makeOne(self, context, url):
+ return self._getTargetClass()(context, url)
+
+ def _getTargetClass(self):
+ from repoze.bfg.traversal import TraversalContextURL
+ return TraversalContextURL
+
+ def _registerTraverserFactory(self, traverser):
+ import zope.component
+ gsm = zope.component.getGlobalSiteManager()
+ from repoze.bfg.interfaces import ITraverserFactory
+ from zope.interface import Interface
+ gsm.registerAdapter(traverser, (Interface,), ITraverserFactory)
+
+ def test_class_conforms_to_IContextURL(self):
+ from zope.interface.verify import verifyClass
+ from repoze.bfg.interfaces import IContextURL
+ verifyClass(IContextURL, self._getTargetClass())
+
+ def test_instance_conforms_to_IContextURL(self):
+ from zope.interface.verify import verifyObject
+ from repoze.bfg.interfaces import IContextURL
+ context = DummyContext()
+ request = DummyRequest()
+ verifyObject(IContextURL, self._makeOne(context, request))
+
+ def test_call_withlineage(self):
+ baz = DummyContext()
+ bar = DummyContext(baz)
+ foo = DummyContext(bar)
+ root = DummyContext(foo)
+ root.__parent__ = None
+ root.__name__ = None
+ foo.__parent__ = root
+ foo.__name__ = 'foo '
+ bar.__parent__ = foo
+ bar.__name__ = 'bar'
+ baz.__parent__ = bar
+ baz.__name__ = 'baz'
+ request = DummyRequest()
+ context_url = self._makeOne(baz, request)
+ result = context_url()
+ self.assertEqual(result, 'http://example.com:5432/foo%20/bar/baz/')
+
+ def test_call_nolineage(self):
+ context = DummyContext()
+ context.__name__ = ''
+ context.__parent__ = None
+ request = DummyRequest()
+ context_url = self._makeOne(context, request)
+ result = context_url()
+ self.assertEqual(result, 'http://example.com:5432/')
+
+ def test_call_unicode_mixed_with_bytes_in_model_names(self):
+ root = DummyContext()
+ root.__parent__ = None
+ root.__name__ = None
+ one = DummyContext()
+ one.__parent__ = root
+ one.__name__ = unicode('La Pe\xc3\xb1a', 'utf-8')
+ two = DummyContext()
+ two.__parent__ = one
+ two.__name__ = 'La Pe\xc3\xb1a'
+ request = DummyRequest()
+ context_url = self._makeOne(two, request)
+ result = context_url()
+ self.assertEqual(result,
+ 'http://example.com:5432/La%20Pe%C3%B1a/La%20Pe%C3%B1a/')
+
+ def test_call_with_vroot_path(self):
+ from repoze.bfg.interfaces import VH_ROOT_KEY
+ root = DummyContext()
+ root.__parent__ = None
+ root.__name__ = None
+ one = DummyContext()
+ one.__parent__ = root
+ one.__name__ = 'one'
+ two = DummyContext()
+ two.__parent__ = one
+ two.__name__ = 'two'
+ request = DummyRequest({VH_ROOT_KEY:'/one'})
+ context_url = self._makeOne(two, request)
+ result = context_url()
+ self.assertEqual(result, 'http://example.com:5432/two/')
+
+ request = DummyRequest({VH_ROOT_KEY:'/one/two'})
+ context_url = self._makeOne(two, request)
+ result = context_url()
+ self.assertEqual(result, 'http://example.com:5432/')
+
+ def test_virtual_root_no_vroot_path(self):
+ root = DummyContext()
+ root.__name__ = None
+ root.__parent__ = None
+ one = DummyContext()
+ one.__name__ = 'one'
+ one.__parent__ = root
+ request = DummyRequest()
+ context_url = self._makeOne(one, request)
+ self.assertEqual(context_url.virtual_root(), root)
+
+ def test_virtual_root_no_vroot_path_with_root_on_request(self):
+ context = DummyContext()
+ context.__parent__ = None
+ request = DummyRequest()
+ request.root = DummyContext()
+ context_url = self._makeOne(context, request)
+ self.assertEqual(context_url.virtual_root(), request.root)
+
+ def test_virtual_root_with_vroot_path(self):
+ from repoze.bfg.interfaces import VH_ROOT_KEY
+ context = DummyContext()
+ context.__parent__ = None
+ traversed_to = DummyContext()
+ environ = {VH_ROOT_KEY:'/one'}
+ request = DummyRequest(environ)
+ traverser = make_traverser(traversed_to, '', [])
+ self._registerTraverserFactory(traverser)
+ context_url = self._makeOne(context, request)
+ self.assertEqual(context_url.virtual_root(), traversed_to)
+ self.assertEqual(context.environ['PATH_INFO'], '/one')
+
+class TestVirtualRoot(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, model, request):
+ from repoze.bfg.traversal import virtual_root
+ return virtual_root(model, request)
+
+ def test_it(self):
+ from zope.component import getGlobalSiteManager
+ from repoze.bfg.interfaces import IContextURL
+ from zope.interface import Interface
+ gsm = getGlobalSiteManager()
+ gsm.registerAdapter(DummyContextURL, (Interface,Interface),
+ IContextURL)
+ context = DummyContext()
+ request = DummyRequest()
+ result = self._callFUT(context, request)
+ self.assertEqual(result, '123')
+
def make_traverser(*args):
class DummyTraverser(object):
def __init__(self, context):
@@ -460,4 +531,14 @@ class DummyContext(object):
class DummyRequest:
application_url = 'http://example.com:5432' # app_url never ends with slash
+ def __init__(self, environ=None):
+ if environ is None:
+ environ = {}
+ self.environ = environ
+
+class DummyContextURL:
+ def __init__(self, context, request):
+ pass
+ def virtual_root(self):
+ return '123'
diff --git a/repoze/bfg/tests/test_url.py b/repoze/bfg/tests/test_url.py
index a1aa84890..5f8cd34f4 100644
--- a/repoze/bfg/tests/test_url.py
+++ b/repoze/bfg/tests/test_url.py
@@ -1,150 +1,76 @@
import unittest
-class DefaultURLGeneratorTests(unittest.TestCase):
- def _makeOne(self):
- return self._getTargetClass()()
-
- def _getTargetClass(self):
- from repoze.bfg.url import DefaultURLGenerator
- return DefaultURLGenerator
-
- def test_class_conforms_to_IURLGenerator(self):
- from zope.interface.verify import verifyClass
- from repoze.bfg.interfaces import IURLGenerator
- verifyClass(IURLGenerator, self._getTargetClass())
-
- def test_instance_conforms_to_IURLGenerator(self):
- from zope.interface.verify import verifyObject
- from repoze.bfg.interfaces import IURLGenerator
- context = DummyContext()
- verifyObject(IURLGenerator, self._makeOne())
-
- def test_model_url_withlineage(self):
- baz = DummyContext()
- bar = DummyContext(baz)
- foo = DummyContext(bar)
- root = DummyContext(foo)
- root.__parent__ = None
- root.__name__ = None
- foo.__parent__ = root
- foo.__name__ = 'foo '
- bar.__parent__ = foo
- bar.__name__ = 'bar'
- baz.__parent__ = bar
- baz.__name__ = 'baz'
- request = DummyRequest()
- gen = self._makeOne()
- result = gen.model_url(baz, request)
- self.assertEqual(result, 'http://example.com:5432/foo%20/bar/baz/')
-
- def test_model_url_nolineage(self):
- context = DummyContext()
- context.__name__ = ''
- context.__parent__ = None
- request = DummyRequest()
- gen = self._makeOne()
- result = gen.model_url(context, request)
- self.assertEqual(result, 'http://example.com:5432/')
+from zope.testing.cleanup import cleanUp
class ModelURLTests(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
def _callFUT(self, model, request, *elements, **kw):
+ self._registerContextURL()
from repoze.bfg.url import model_url
return model_url(model, request, *elements, **kw)
- def test_extra_args(self):
- baz = DummyContext()
- bar = DummyContext(baz)
- foo = DummyContext(bar)
- root = DummyContext(foo)
- root.__parent__ = None
- root.__name__ = None
- foo.__parent__ = root
- foo.__name__ = 'foo '
- bar.__parent__ = foo
- bar.__name__ = 'bar'
- baz.__parent__ = bar
- baz.__name__ = 'baz'
- request = DummyRequest()
- result = self._callFUT(baz, request, 'this/theotherthing', 'that')
- self.assertEqual(
- result,
- 'http://example.com:5432/foo%20/bar/baz/this/theotherthing/that')
-
- def test_root_default_app_url(self):
+ def _registerContextURL(self):
+ from repoze.bfg.interfaces import IContextURL
+ from zope.interface import Interface
+ from zope.component import getGlobalSiteManager
+ class DummyContextURL(object):
+ def __init__(self, context, request):
+ pass
+ def __call__(self):
+ return 'http://example.com/context/'
+ gsm = getGlobalSiteManager()
+ gsm.registerAdapter(DummyContextURL, (Interface, Interface),
+ IContextURL)
+
+ def test_root_default(self):
root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
request = DummyRequest()
result = self._callFUT(root, request)
- self.assertEqual(result, 'http://example.com:5432/')
-
- def test_nonroot_default_app_url(self):
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
- other = DummyContext()
- other.__parent__ = root
- other.__name__ = 'nonroot object'
- request = DummyRequest()
- result = self._callFUT(other, request)
- self.assertEqual(result, 'http://example.com:5432/nonroot%20object/')
+ self.assertEqual(result, 'http://example.com/context/')
- def test_unicode_mixed_with_bytes_in_model_names(self):
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
- one = DummyContext()
- one.__parent__ = root
- one.__name__ = unicode('La Pe\xc3\xb1a', 'utf-8')
- two = DummyContext()
- two.__parent__ = one
- two.__name__ = 'La Pe\xc3\xb1a'
+ def test_extra_args(self):
+ context = DummyContext()
request = DummyRequest()
- result = self._callFUT(two, request)
- self.assertEqual(result,
- 'http://example.com:5432/La%20Pe%C3%B1a/La%20Pe%C3%B1a/')
+ result = self._callFUT(context, request, 'this/theotherthing', 'that')
+ self.assertEqual(
+ result,
+ 'http://example.com/context/this/theotherthing/that')
def test_unicode_in_element_names(self):
uc = unicode('La Pe\xc3\xb1a', 'utf-8')
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
- one = DummyContext()
- one.__parent__ = root
- one.__name__ = uc
+ context = DummyContext()
request = DummyRequest()
- result = self._callFUT(one, request, uc)
+ result = self._callFUT(context, request, uc)
self.assertEqual(result,
- 'http://example.com:5432/La%20Pe%C3%B1a/La%20Pe%C3%B1a')
+ 'http://example.com/context/La%20Pe%C3%B1a')
def test_element_names_url_quoted(self):
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
+ context = DummyContext()
request = DummyRequest()
- result = self._callFUT(root, request, 'a b c')
- self.assertEqual(result, 'http://example.com:5432/a%20b%20c')
+ result = self._callFUT(context, request, 'a b c')
+ self.assertEqual(result, 'http://example.com/context/a%20b%20c')
def test_with_query_dict(self):
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
+ context = DummyContext()
request = DummyRequest()
uc = unicode('La Pe\xc3\xb1a', 'utf-8')
- result = self._callFUT(root, request, 'a', query={'a':uc})
+ result = self._callFUT(context, request, 'a', query={'a':uc})
self.assertEqual(result,
- 'http://example.com:5432/a?a=La+Pe%C3%B1a')
+ 'http://example.com/context/a?a=La+Pe%C3%B1a')
def test_with_query_seq(self):
- root = DummyContext()
- root.__parent__ = None
- root.__name__ = None
+ context = DummyContext()
request = DummyRequest()
uc = unicode('La Pe\xc3\xb1a', 'utf-8')
- result = self._callFUT(root, request, 'a', query=[('a', 'hi there'),
- ('b', uc)])
+ result = self._callFUT(context, request, 'a', query=[('a', 'hi there'),
+ ('b', uc)])
self.assertEqual(result,
- 'http://example.com:5432/a?a=hi+there&b=La+Pe%C3%B1a')
+ 'http://example.com/context/a?a=hi+there&b=La+Pe%C3%B1a')
class UrlEncodeTests(unittest.TestCase):
def _callFUT(self, query, doseq=False):
diff --git a/repoze/bfg/tests/test_urldispatch.py b/repoze/bfg/tests/test_urldispatch.py
index 71ac78698..0fc828161 100644
--- a/repoze/bfg/tests/test_urldispatch.py
+++ b/repoze/bfg/tests/test_urldispatch.py
@@ -221,6 +221,130 @@ def make_get_root(result):
return result
return dummy_get_root
+class RoutesModelTraverserTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.urldispatch import RoutesModelTraverser
+ return RoutesModelTraverser
-
+ def _makeOne(self, model):
+ klass = self._getTargetClass()
+ return klass(model)
+
+ def test_class_conforms_to_ITraverser(self):
+ from zope.interface.verify import verifyClass
+ from repoze.bfg.interfaces import ITraverser
+ verifyClass(ITraverser, self._getTargetClass())
+
+ def test_instance_conforms_to_ITraverser(self):
+ from zope.interface.verify import verifyObject
+ from repoze.bfg.interfaces import ITraverser
+ verifyObject(ITraverser, self._makeOne(None))
+
+ def test_call_with_only_controller_bwcompat(self):
+ model = DummyContext()
+ model.controller = 'controller'
+ traverser = self._makeOne(model)
+ result = traverser({})
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], 'controller')
+ self.assertEqual(result[2], [])
+
+ def test_call_with_only_view_name_bwcompat(self):
+ model = DummyContext()
+ model.view_name = 'view_name'
+ traverser = self._makeOne(model)
+ result = traverser({})
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], 'view_name')
+ self.assertEqual(result[2], [])
+
+ def test_call_with_subpath_bwcompat(self):
+ model = DummyContext()
+ model.view_name = 'view_name'
+ model.subpath = '/a/b/c'
+ traverser = self._makeOne(model)
+ result = traverser({})
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], 'view_name')
+ self.assertEqual(result[2], ['a', 'b', 'c'])
+
+ def test_call_with_no_view_name_or_controller_bwcompat(self):
+ model = DummyContext()
+ traverser = self._makeOne(model)
+ result = traverser({})
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], '')
+ self.assertEqual(result[2], [])
+
+ def test_call_with_only_view_name(self):
+ model = DummyContext()
+ traverser = self._makeOne(model)
+ routing_args = ((), {'view_name':'view_name'})
+ environ = {'wsgiorg.routing_args': routing_args}
+ result = traverser(environ)
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], 'view_name')
+ self.assertEqual(result[2], [])
+ def test_call_with_view_name_and_subpath(self):
+ model = DummyContext()
+ traverser = self._makeOne(model)
+ routing_args = ((), {'view_name':'view_name', 'subpath':'/a/b/c'})
+ environ = {'wsgiorg.routing_args': routing_args}
+ result = traverser(environ)
+ self.assertEqual(result[0], model)
+ self.assertEqual(result[1], 'view_name')
+ self.assertEqual(result[2], ['a', 'b','c'])
+
+class RoutesContextURLTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.urldispatch import RoutesContextURL
+ return RoutesContextURL
+
+ def _makeOne(self, context, request):
+ return self._getTargetClass()(context, request)
+
+ def test_class_conforms_to_IContextURL(self):
+ from zope.interface.verify import verifyClass
+ from repoze.bfg.interfaces import IContextURL
+ verifyClass(IContextURL, self._getTargetClass())
+
+ def test_instance_conforms_to_IContextURL(self):
+ from zope.interface.verify import verifyObject
+ from repoze.bfg.interfaces import IContextURL
+ verifyObject(IContextURL, self._makeOne(None, None))
+
+ def test_get_virtual_root(self):
+ context_url = self._makeOne(1,2)
+ self.assertEqual(context_url.virtual_root(), 1)
+
+ def test_call(self):
+ from routes import Mapper
+ mapper = Mapper(controller_scan=None, directory=None,
+ explicit=True, always_scan=False)
+ args = {'a':'1', 'b':'2', 'c':'3'}
+ mapper.connect(':a/:b/:c')
+ mapper.create_regs([])
+ environ = {'SERVER_NAME':'example.com', 'wsgi.url_scheme':'http',
+ 'SERVER_PORT':'80', 'wsgiorg.routing_args':((), args)}
+ mapper.environ = environ
+ from routes import request_config
+ config = request_config()
+ config.environ = environ
+ config.mapper = mapper
+ config.mapper_dict = args
+ config.host = 'www.example.com'
+ config.protocol = 'https'
+ config.redirect = None
+ request = DummyRequest()
+ request.environ = environ
+ context_url = self._makeOne(None, request)
+ result = context_url()
+ self.assertEqual(result, '/1/2/3')
+
+class DummyContext(object):
+ """ """
+
+class DummyRequest(object):
+ """ """
+
diff --git a/repoze/bfg/traversal.py b/repoze/bfg/traversal.py
index d6f92e118..e875219c1 100644
--- a/repoze/bfg/traversal.py
+++ b/repoze/bfg/traversal.py
@@ -1,5 +1,7 @@
import urllib
+from zope.component import getMultiAdapter
+
from zope.deferredimport import deprecated
from zope.interface import classProvides
@@ -9,10 +11,13 @@ from repoze.bfg.location import LocationProxy
from repoze.bfg.location import lineage
from repoze.bfg.lru import lru_cache
+from repoze.bfg.url import _urlsegment
+from repoze.bfg.interfaces import IContextURL
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
deprecated(
"('from repoze.bfg.traversal import model_url' is now "
@@ -20,6 +25,13 @@ deprecated(
model_url = "repoze.bfg.url:model_url",
)
+deprecated(
+ "('from repoze.bfg.traversal import RoutesModelTraverser' is now "
+ "deprecated; instead use 'from repoze.bfg.urldispatch "
+ "import RoutesModelTraverser')",
+ RoutesModelTraverser = "repoze.bfg.urldispatch:RoutesModelTraverser",
+ )
+
# ``split_path`` wasn't actually ever an API but people were using it
# anyway. I turned it into the ``traversal_path`` API in 0.6.5, and
# generate the below deprecation to give folks a heads up.
@@ -109,6 +121,34 @@ def model_path(model, *elements):
path = '/'.join([path, suffix])
return path
+def virtual_root(model, request):
+ """ Return the model object representing the 'virtual root' of the
+ current request. Using a virtual root in a traversal-based
+ :mod:`repoze.bfg` application permits rooting, for example, the
+ object at the traversal path ``/cms`` at ``http://example.com/``
+ instead of rooting it at ``http://example.com/cms/``.
+
+ If the ``model`` passed in is a context obtained via
+ :term:`traversal`, and if the ``%s`` key is in the WSGI
+ environment, the value of this key will be treated as a 'virtual
+ root path': the :mod:``repoze.bfg.traversal.find_model`` API will
+ be used to find the virtual root object using this path; if the
+ object is found, it will found will be returned. If the ``%s``
+ key is is not present in the WSGI environment, the physical
+ :term:`root` of the graph will be returned instead.
+
+ .. note:: Virtual roots are not useful in at all applications that
+ use :term:`URL dispatch`. Contexts obtained via URL
+ dispatch don't really support being virtually rooted
+ (each URL dispatch context is both its own physical and
+ virtual root). However, for symmetry, if this API is
+ called with a model which is a context obtained via URL
+ dispatch, the model passed in will be returned
+ unconditonally.
+ """ % (VH_ROOT_KEY, VH_ROOT_KEY)
+ urlgenerator = getMultiAdapter((model, request), IContextURL)
+ return urlgenerator.virtual_root()
+
@lru_cache(500)
def traversal_path(path):
""" Given a PATH_INFO string (slash-separated path elements),
@@ -217,35 +257,57 @@ class ModelGraphTraverser(object):
except KeyError:
return name, default
-class RoutesModelTraverser(object):
- classProvides(ITraverserFactory)
- implements(ITraverser)
- def __init__(self, context):
+class TraversalContextURL(object):
+ """ The IContextURL adapter used to generate URLs for a context
+ object obtained via graph traversal"""
+ implements(IContextURL)
+
+ vroot_varname = VH_ROOT_KEY
+
+ def __init__(self, context, request):
self.context = context
+ self.request = request
- def __call__(self, environ):
- # the traverser *wants* to get routing args from the environ
- # as of 0.6.5; the rest of this stuff is for backwards
- # compatibility
- try:
- # 0.6.5 +
- routing_args = environ['wsgiorg.routing_args'][1]
- except KeyError:
- # <= 0.6.4
- routing_args = self.context.__dict__
+ def virtual_root(self):
try:
- view_name = routing_args['view_name']
+ vroot_path = self.request.environ[self.vroot_varname]
except KeyError:
- # b/w compat < 0.6.3
+ # shortcut instead of using find_root; we probably already
+ # have it on the request
try:
- view_name = routing_args['controller']
- except KeyError:
- view_name = ''
- try:
- subpath = routing_args['subpath']
- subpath = filter(None, subpath.split('/'))
- except KeyError:
- # b/w compat < 0.6.5
- subpath = []
+ return self.request.root
+ except AttributeError:
+ return find_root(self.context)
+ return find_model(self.context, vroot_path)
+
+ def __call__(self):
+ """ Generate a URL based on the lineage of a model obtained
+ via traversal. If any model in the context lineage has a
+ unicode name, it will be converted to a UTF-8 string before
+ being attached to the URL. When composing the path based on
+ the model lineage, empty names in the model graph are ignored.
+ If a ``%s`` key is present in the WSGI environment, its value
+ will be treated as a 'virtual root path': the path of the URL
+ generated by this will be left-stripped of this virtual root
+ path value.
+ """ % VH_ROOT_KEY
+ rpath = []
+ for location in lineage(self.context):
+ name = location.__name__
+ if name:
+ rpath.append(_urlsegment(name))
+ if rpath:
+ path = '/' + '/'.join(reversed(rpath)) + '/'
+ else:
+ path = '/'
+ request = self.request
+ # if the path starts with the virtual root path, trim it out
+ vroot_path = request.environ.get(self.vroot_varname)
+ if vroot_path is not None:
+ if path.startswith(vroot_path):
+ path = path[len(vroot_path):]
+
+ app_url = request.application_url # never ends in a slash
+ return app_url + path
+
- return self.context, view_name, subpath
diff --git a/repoze/bfg/url.py b/repoze/bfg/url.py
index a51333353..9ea82ed7a 100644
--- a/repoze/bfg/url.py
+++ b/repoze/bfg/url.py
@@ -3,43 +3,28 @@
import re
import urllib
-from zope.component import queryUtility
-from zope.interface import implements
-
-from repoze.bfg.location import lineage
-from repoze.bfg.interfaces import IURLGenerator
-
-class DefaultURLGenerator(object):
- implements(IURLGenerator)
- def model_url(self, model, request):
- rpath = []
- for location in lineage(model):
- name = location.__name__
- if name:
- rpath.append(_urlsegment(name))
- if rpath:
- prefix = '/' + '/'.join(reversed(rpath)) + '/'
- else:
- prefix = '/'
- return request.application_url + prefix
-
-default_url_generator = DefaultURLGenerator()
+from zope.component import getMultiAdapter
+from repoze.bfg.interfaces import IContextURL
+from repoze.bfg.interfaces import VH_ROOT_KEY
def model_url(model, request, *elements, **kw):
"""
- Generate a string representing the absolute URL of the model
- object based on the ``wsgi.url_scheme``, ``HTTP_HOST`` or
+ Generate a string representing the absolute URL of the model (or
+ context) object based on the ``wsgi.url_scheme``, ``HTTP_HOST`` or
``SERVER_NAME`` in the request, plus any ``SCRIPT_NAME``. If a
+ 'virtual root path' is present in the request environment (the
+ value of the environ key ``%s``), and the ``model`` was obtained
+ via traversal, the URL path will not include the virtual root
+ prefix (it will be stripped out of the generated URL). If a
``query`` keyword argument is provided, a query string based on
its value will be composed and appended to the generated URL
string (see details below). The overall result of this function
- is always a string (never unicode). The ``model`` passed in must
- be :term:`location`-aware.
+ is always a UTF-8 encoded string (never unicode).
- .. note:: If any model in the lineage has a unicode name, it will
- be converted to UTF-8 before being attached to the URL.
- When composing the path based on the model lineage,
- empty names in the model graph are ignored.
+ .. note:: If the ``model`` used is the result of a traversal, it
+ must be :term:`location`-aware. The 'model' can also be the
+ context of a URL dispatch; contexts found this way do not need
+ to be location-aware.
Any positional arguments passed in as ``elements`` must be strings
or unicode objects. These will be joined by slashes and appended
@@ -63,19 +48,16 @@ def model_url(model, request, *elements, **kw):
the resulting string is appended to the generated URL.
.. note:: Python data structures that are passed as ``query``
- whichare sequences or dictionaries are turned into a
+ which are sequences or dictionaries are turned into a
string under the same rules as when run through
urllib.urlencode with the ``doseq`` argument equal to
``True``. This means that sequences can be passed as
values, and a k=v pair will be placed into the query
string for each value.
- """
-
- urlgenerator = queryUtility(IURLGenerator)
- if urlgenerator is None:
- urlgenerator = default_url_generator
+ """ % VH_ROOT_KEY
- model_url = urlgenerator.model_url(model, request)
+ context_url = getMultiAdapter((model, request), IContextURL)
+ model_url = context_url()
if 'query' in kw:
qs = '?' + urlencode(kw['query'], doseq=True)
@@ -87,7 +69,6 @@ def model_url(model, request, *elements, **kw):
else:
suffix = ''
- app_url = request.application_url # never ends in a slash
return model_url + suffix + qs
def urlencode(query, doseq=False):
diff --git a/repoze/bfg/urldispatch.py b/repoze/bfg/urldispatch.py
index 0c71c6e74..03b73d6c6 100644
--- a/repoze/bfg/urldispatch.py
+++ b/repoze/bfg/urldispatch.py
@@ -1,11 +1,17 @@
from zope.interface import implements
from zope.interface import alsoProvides
+from zope.interface import classProvides
+
from routes import Mapper
from routes import request_config
+from routes import url_for
-from repoze.bfg.interfaces import IRoutesContext
from repoze.bfg.interfaces import IContextNotFound
+from repoze.bfg.interfaces import IContextURL
+from repoze.bfg.interfaces import IRoutesContext
+from repoze.bfg.interfaces import ITraverser
+from repoze.bfg.interfaces import ITraverserFactory
from zope.deferredimport import deprecated
from zope.deprecation import deprecated as deprecated2
@@ -191,3 +197,56 @@ class RoutesRootFactory(Mapper):
# fall back to original get_root
return self.get_root(environ)
+
+class RoutesModelTraverser(object):
+ classProvides(ITraverserFactory)
+ implements(ITraverser)
+ def __init__(self, context):
+ self.context = context
+
+ def __call__(self, environ):
+ # the traverser *wants* to get routing args from the environ
+ # as of 0.6.5; the rest of this stuff is for backwards
+ # compatibility
+ try:
+ # 0.6.5 +
+ routing_args = environ['wsgiorg.routing_args'][1]
+ except KeyError:
+ # <= 0.6.4
+ routing_args = self.context.__dict__
+ try:
+ view_name = routing_args['view_name']
+ except KeyError:
+ # b/w compat < 0.6.3
+ try:
+ view_name = routing_args['controller']
+ except KeyError:
+ view_name = ''
+ try:
+ subpath = routing_args['subpath']
+ subpath = filter(None, subpath.split('/'))
+ except KeyError:
+ # b/w compat < 0.6.5
+ subpath = []
+
+ return self.context, view_name, subpath
+
+class RoutesContextURL(object):
+ """ The IContextURL adapter used to generate URLs for a context
+ object obtained via Routes URL dispatch. This implementation
+ juses the ``url_for`` Routes API to generate a URL based on
+ ``environ['wsgiorg.routing_args']``. Routes context objects,
+ unlike traversal-based context objects, cannot have a virtual root
+ that differs from its physical root; furthermore, the physical
+ root of a Routes context is always itself, so the ``virtual_root``
+ function returns the context of this adapter unconditionally."""
+ implements(IContextURL)
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def virtual_root(self):
+ return self.context
+
+ def __call__(self):
+ return url_for(**self.request.environ['wsgiorg.routing_args'][1])