diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-01-27 21:57:11 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-01-27 21:57:11 +0000 |
| commit | e62e479e338e428f6cfd3b07790545982b7cb94f (patch) | |
| tree | c9784577f791d4a8ea5b80a9fce211ce86009712 | |
| parent | 2301cf61977102b85279ea7c04797f76012202e5 (diff) | |
| download | pyramid-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.txt | 33 | ||||
| -rw-r--r-- | docs/api/traversal.rst | 2 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/narr/vhosting.rst | 69 | ||||
| -rw-r--r-- | repoze/bfg/includes/configure.zcml | 18 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 14 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_integration.py | 2 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_traversal.py | 231 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_url.py | 158 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_urldispatch.py | 126 | ||||
| -rw-r--r-- | repoze/bfg/traversal.py | 114 | ||||
| -rw-r--r-- | repoze/bfg/url.py | 55 | ||||
| -rw-r--r-- | repoze/bfg/urldispatch.py | 61 |
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]) |
