From 62267e01d6eeaf8de871487898ad1ce02878c29a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 18 Jan 2009 23:38:47 +0000 Subject: Merge "routesmapper branch" to trunk. --- CHANGES.txt | 40 ++++ docs/api/view.rst | 3 +- docs/narr/urldispatch.rst | 242 ++++++++++-------------- repoze/bfg/includes/configure.zcml | 2 +- repoze/bfg/includes/meta.zcml | 15 ++ repoze/bfg/interfaces.py | 7 + repoze/bfg/registry.py | 5 +- repoze/bfg/router.py | 27 ++- repoze/bfg/tests/fixtureapp/views.py | 2 +- repoze/bfg/tests/routesapp/__init__.py | 1 + repoze/bfg/tests/routesapp/configure.zcml | 16 ++ repoze/bfg/tests/routesapp/models.py | 5 + repoze/bfg/tests/routesapp/templates/fixture.pt | 6 + repoze/bfg/tests/routesapp/views.py | 8 + repoze/bfg/tests/test_registry.py | 15 +- repoze/bfg/tests/test_router.py | 87 +++++++++ repoze/bfg/tests/test_traversal.py | 55 ++++++ repoze/bfg/tests/test_urldispatch.py | 167 +++++++++++++--- repoze/bfg/tests/test_zcml.py | 209 ++++++++++++++++++++ repoze/bfg/traversal.py | 144 +++++++------- repoze/bfg/urldispatch.py | 135 +++++++++++-- repoze/bfg/zcml.py | 147 +++++++++++++- 22 files changed, 1060 insertions(+), 278 deletions(-) create mode 100644 repoze/bfg/tests/routesapp/__init__.py create mode 100644 repoze/bfg/tests/routesapp/configure.zcml create mode 100644 repoze/bfg/tests/routesapp/models.py create mode 100644 repoze/bfg/tests/routesapp/templates/fixture.pt create mode 100644 repoze/bfg/tests/routesapp/views.py diff --git a/CHANGES.txt b/CHANGES.txt index 5d29739b8..dd6a68497 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,26 @@ Bug Fixes Features -------- +- URL-dispatch has been overhauled: it is no longer necessary to + manually create a RoutesMapper in your application's entry point + callable in order to use URL-dispatch (aka `Routes + `_). A new ``route`` directive has been + added to the available list of ZCML directives. Each ``route`` + directive inserted into your application's ``configure.zcml`` + establishes a Routes mapper connection. If any ``route`` + declarations are made via ZCML within a particular application, the + ``get_root`` callable passed in to ``repoze.bfg.router.make_app`` + will automatically be wrapped in the equivalent of a RoutesMapper. + Additionally, the new ``route`` directive allows the specification + of a ``context_interfaces`` attribute for a route, this will be used + to tag the manufactured routes context with specific interfaces when + a route specifying a ``context_interfaces`` attribute is matched. + +- A new interface ``repoze.bfg.interfaces.IContextNotFound`` was + added. This interface is attached to a "dummy" context generated + when Routes cannot find a match and there is no "fallback" get_root + callable that uses traversal. + - The ``bfg_starter`` and ``bfg_zodb`` "paster create" templates now contain images and CSS which are displayed when the default page is displayed after initial project generation. @@ -39,6 +59,15 @@ Features allow both to be overridden via a ZCML utility hook. See the "Using ZCML Hooks" chapter of the documentation for more information. +Deprecations +------------ + +- The class ``repoze.bfg.urldispatch.RoutesContext`` has been renamed + to ``repoze.bfg.urldispatch.DefaultRoutesContext``. The class + should be imported by the new name as necessary (although in reality + it probably shouldn't be imported from anywhere except internally + within BFG, as it's not part of the API). + Implementation Changes ---------------------- @@ -50,9 +79,20 @@ Implementation Changes ``webob.Request.get_response`` to do its work rather than relying on homegrown WSGI code. +- The ``repoze.bfg.urldispatch.RoutesModelTraverser`` class has been + moved to ``repoze.bfg.traversal.RoutesModelTraverser``. + +- The ``repoze.bfg.registry.makeRegistry`` function was renamed to + ``repoze.bfg.registry.populateRegistry`` and now accepts a + ``registry`` argument (which should be an instance of + ``zope.component.registry.Components``). + Documentation Additions ----------------------- +- Updated narrative urldispatch chapter with changes required by + ```` ZCML directive. + - Add a section on "Using BFG Security With URL Dispatch" into the urldispatch chapter of the documentation. diff --git a/docs/api/view.rst b/docs/api/view.rst index 5afd1118a..3b34b7e22 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -15,7 +15,8 @@ .. autofunction:: view_execution_permitted - .. autofunction:: bfg_view + .. autoclass:: bfg_view + :members: .. autoclass:: static :members: diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 7df7ce8ed..e6e5287c1 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -14,17 +14,10 @@ is a mechanism which allows you to declaratively map URLs to code. to is defined by a *controller* and an *action*. However, neither concept (controller nor action) exists within :mod:`repoze.bfg`. Instead, when you map a URL pattern to - code in bfg, you will map the URL patterm to a :term:`view`. - As a result, there is a bit of mental gynmastics you'll need - to do when dealing with Routes URL dispatch in bfg. In - general, whenever you see the name *controller* in the - context of :term:`Routes`, you should map that mentally to - the bfg term :term:`view`. *Actions* do not exist in - :mod:`repoze.bfg`: in other frameworks these may refer to - methods of a *controller class*, but since views in - :mod:`repoze.bfg` are simple callables (usually functions) - as opposed to classes, there is no direct concept mapping of - an action. + code in bfg, you will map the URL patterm to a + :term:`context` and a :term:`view name`. Once the context + is found, "normal" :mod:`repoze.bfg` :term:`view` lookup + will be done using the context and the view name. It often makes a lot of sense to use :term:`URL dispatch` instead of :term:`traversal` in an application that has no natural hierarchy. @@ -42,96 +35,90 @@ natural :term:`traversal`, allowing a :term:`Routes` "mapper" object to have the "first crack" at resolving a given URL, allowing the framework to fall back to traversal as necessary. -To this end, the :mod:`repoze.bfg` framework defines a module named -:mod:`repoze.bfg.urldispatch`. This module contains a class named -:class:`RoutesMapper`. An Instance of this class is willing to act as -a :mod:`repoze.bfg` ``get_root`` callable, and is willing to be -configured with *route mappings* as necessary via its ``.connect`` -method. - -The :class:`RoutesMapper` is essentially willing to act as the "root -callable". When it acts as such a callable, it is willing to check -the requested URL against a *routes map*, and subsequently look up and -call a :mod:`repoze.bfg` view with the information it finds within a +To this end, the :mod:`repoze.bfg` framework allows you to inject +``route` ZCML directives into your application's ``configure.zcml`` +file. These directives have much the same job as imperatively using +the ``.connect`` method of a routes Mapper object, with some +BFG-specific behavior. + +When any ``route`` ZCML directive is used, BFG wraps the "default" +"root factory" (aka ``get_root``) in a special ``RoutesRootFactory`` +instance. This then acts as the root factory (a callable). When it +acts as such a callable, it is willing to check the requested URL +against a *routes map* to find the :term:`context` and the +:term:`view name`. Subsequently, BFG will look up and call a +:mod:`repoze.bfg` view with the information it finds within a particular route, if any configured route matches the currently -requested URL. A ``get_root`` callable is a callable passed to the -:mod:`repoze.bfg` framework by an application, allowing -:mod:`repoze.bfg` to fail over to another "root" object in case the -routes mapper can't find a match for a particular URL. If no URL -matches, the :class:`RoutesMapper` will fall back to calling the -fallback ``get_root`` callable that is passed in to it at construction -time, which allows your application to fall back to a different "root" -(perhaps one based on traversal). By configuring a -:class:`RoutesMapper` appropriately, you can mix and match URL -dispatch and traversal in this way. +requested URL. If no route matches the configured routes, +:mod:`repoze.bfg` will fail over to calling the ``get_root`` callable +passed to the application in it's ``make_app`` function. By +configuring your ZCML ``route`` statements appropriately, you can mix +and match URL dispatch and traversal in this way. .. note:: See :ref:`modelspy_project_section` for an example of a - simple ``get_root`` callable that uses traversal. + simple ``get_root`` callable that will use traversal. -Configuring a :class:`RoutesMapper` with individual routes is -performed by creating an instance of a :class:`RoutesMapper`, and -calling its ``.connect`` method with the same arguments you'd use if -you were creating a route mapping using a "raw" :term:`Routes` -``Mapper`` object. See `Setting up routes +Each ZCML ``route``statement equals a call to the term:`Routes` +``Mapper`` object's ``connect`` method. See `Setting up routes `_ for -examples of using a Routes ``Mapper`` object. When you are finished -configuring it, you can pass it as a ``get_root`` callable to +examples of using a Routes ``Mapper`` object outside of :mod:`repoze.bfg`. -When you configure a :class:`RoutesMapper` with a route via -``.connect``, you'll pass in the name of a ``controller`` as a keyword -argument. This will be a string. The string should match the -**name** of a :mod:`repoze.bfg` :term:`view` callable that is -registered for the type ``repoze.bfg.interfaces.IRoutesContext`` (via -a ZCML directive, see :ref:`views_chapter` for more information about -registering bfg views). When a URL is matched, this view will be -called with a :term:`context` manufactured "on the fly" by the -:class:`RoutesMapper`. The context object will have attributes which -match all of the :term:`Routes` matching arguments returned by the -mapper. - Example 1 --------- -Below is an example of configuring a :class:`RoutesMapper` for usage -as a ``get_root`` callback. +Below is an example of some route statements you might add to your +``configure.zcml``: -.. code-block:: python +.. code-block:: xml :linenos: - from repoze.bfg.urldispatch import RoutesMapper + - def fallback_get_root(environ): - return {} + - root = RoutesMapper(fallback_get_root) - root.connect('ideas/:idea', controller='ideas') - root.connect('users/:user', controller='users') - root.connect('tags/:tag', controller='tags') + -The above configuration will allow the mapper to service URLs in the forms:: +The above configuration will allow :mod:`repoze.bfg` to service URLs +in these forms: + +.. code-block:: bash + :linenos: /ideas/ /users/ /tags/ -If this mapper is used as a ``get_root`` callback, when a URL matches -the pattern ``/ideas/``, the view registered with the name -'ideas' for the interface ``repoze.bfg.interfaces.IRoutesContext`` -will be called. An error will be raised if no view can be found with -that interfaces type or name. +When a URL matches the pattern ``/ideas/``, the view +registered with the name 'ideas' for the interface +``repoze.bfg.interfaces.IRoutesContext`` will be called. An error +will be raised if no view can be found with that interfaces type or +name. The context object passed to a view found as the result of URL dispatch will be an instance of the ``repoze.bfg.urldispatch.RoutesContext`` object. You can override -this behavior by passing in a ``context_factory`` argument to the -mapper's connect method for a particular route. The -``context_factory`` should be a callable that accepts arbitrary -keyword arguments and returns an instance of a class that will be the -context used by the view. - -If no route matches in the above configuration, the routes mapper will -call the "fallback" ``get_root`` callable provided to it above. +this behavior by passing in a ``context_factory`` argument to the ZCML +directive for a particular route. The ``context_factory`` should be a +callable that accepts arbitrary keyword arguments and returns an +instance of a class that will be the context used by the view. + +The context object will be decorated by default with the +``repoze.bfg.interfaces.IRoutesContext`` interface. To decorate a +context found via a route with other interfaces, you can use a +``context_interfaces`` attribute on the ZCML statement. It should be +a space-separated list of dotted Python names that point at interfaces. + +If no route matches in the above configuration, :mod:`repoze.bfg` will +call the "fallback" ``get_root`` callable provided to it during +``make_app`. If the "fallback" ``get_root`` is None, a ``NotFound`` +error will be raised when no route matches. Example 2 --------- @@ -144,35 +131,31 @@ function is as follows: :linenos: + + All context objects found via Routes URL dispatch will provide the -``IRoutesContext`` interface (attached dynamically). You might then -configure the ``RoutesMapper`` like so: +``IRoutesContext`` interface (attached dynamically). The above +``route`` statement will also cause contexts generated by the route to +have the ``.interfaces.ISomeContext`` interface as well. The +``.models`` modulemight look like so: .. code-block:: python :linenos: - from repoze.bfg.router import make_app - from repoze.bfg.urldispatch import RoutesMapper - - def fallback_get_root(environ): - return {} # the graph traversal root is empty in this example - class Article(object): def __init__(self, **kw): self.__dict__.update(kw) - get_root = RoutesMapper(fallback_get_root) - get_root.connect('archives/:article', controller='articles', - context_factory=Article) - - import myapp - app = make_app(get_root, myapp) - The effect of this configuration: when this :mod:`repoze.bfg` application runs, if any URL matches the pattern ``archives/:article``, the ``.views.articles_view`` view will be @@ -195,65 +178,33 @@ called. This framework operates in terms of ACLs (Access Control Lists, see :ref:`security_chapter` for more information about the :mod:`repoze.bfg` security subsystem). A common thing to want to do is to attach an ``__acl__`` to the context object dynamically for -declarative security purposes. A Routes 'trick' can allow for this. - -Routes makes it possible to pass a ``conditions`` argument to the -``connect`` method of a mapper. The value of ``conditions`` is a -dictionary. If you pass a ``conditions`` dictionary to this -``connect`` method with a ``function`` key that has a value which is a -Python function, this function can be used to update the ``__acl__`` -of the model object. - -When Routes tries to resolve a particular route via a match, the route -object itself will pass the environment and the "match_dict" to the -``conditions`` function. Typically, a ``conditions`` function decides -whether or not the route match should "succeed". But we'll use it -differently: we'll use it to update the "match dict". The match dict -is what is eventually returned by Routes to :mod:`repoze.bfg`. If the -function that is used as the ``conditions`` function adds an -``__acl__`` key/value pair to the match dict and subsequently always -returns ``True`` (indicating that the "condition" passed), the -resulting ``__acl__`` key and its value will appear in the match -dictionary. Since all values returned in the match dictionary are -eventually set on your context object, :mod:`repoze.bfg` will set an -``__acl__`` attribute on the context object returned to -:mod:`repoze.bfg` matching the value you've put into the match -dictionary under ``__acl__``, just in time for the :mod:`repoze.bfg` -security machinery to find it. :mod:`repoze.bfg` security will allow -or deny further processing of the request based on the ACL. - -Here's an example: +declarative security purposes. You can use the ``context_factory`` +argument that points at a context factory which attaches a custom +``__acl__`` to an object at its creation time. -.. code-block:: python +Such a ``context_factory`` might look like so: - from repoze.bfg.router import make_app - from repoze.bfg.security import Allow - from repoze.bfg.urldispatch import RoutesMapper - - def fallback_get_root(environ): - return {} # the graph traversal root is empty in this example +.. code-block:: python + :linenos: class Article(object): def __init__(self, **kw): self.__dict__.update(kw) - def add_acl(environ, match_dict): - if match_dict.get('article') == 'article1': - routes_dict['__acl__'] = [ (Allow, 'editor', 'view') ] - - get_root = RoutesMapper(fallback_get_root) - get_root.connect('archives/:article', controller='articles', - context_factory=Article, conditions={'function':add_acl}) - - import myapp - app = make_app(get_root, myapp) - -If the route ``archives/:article`` is matched, :mod:`repoze.bfg` will -generate an ``Article`` :term:`context` with an ACL on it that allows -the ``editor`` principal the ``view`` permission. Obviously you can -do more generic things that inspect the routes match dict to see if -the ``article`` argument matches a particular string; our sample -``add_acl`` function is not very ambitious. + def article_context_factory(**kw): + model = Article(**kw) + article = kw.get('article', None) + if article == '1': + model.__acl__ = [ (Allow, 'editor', 'view') ] + return model + +If the route ``archives/:article`` is matched, and the article number +is ``1``, :mod:`repoze.bfg` will generate an ``Article`` +:term:`context` with an ACL on it that allows the ``editor`` principal +the ``view`` permission. Obviously you can do more generic things +that inspect the routes match dict to see if the ``article`` argument +matches a particular string; our sample ``article_context_factory`` +function is not very ambitious. .. note:: See :ref:`security_chapter` for more information about :mod:`repoze.bfg` security and ACLs. @@ -269,6 +220,3 @@ Further Documentation and Examples URL-dispatch related API documentation is available in :ref:`urldispatch_module` . -The `repoze.shootout `_ -application uses URL dispatch to serve its "ideas", "users" and "tags" -pages. diff --git a/repoze/bfg/includes/configure.zcml b/repoze/bfg/includes/configure.zcml index 6e830e96f..07f8ab3fa 100644 --- a/repoze/bfg/includes/configure.zcml +++ b/repoze/bfg/includes/configure.zcml @@ -11,7 +11,7 @@ /> diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml index 89d45e0e6..83fb48e87 100644 --- a/repoze/bfg/includes/meta.zcml +++ b/repoze/bfg/includes/meta.zcml @@ -18,4 +18,19 @@ + + + + diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index 4f78ae023..567fb6d33 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -186,4 +186,11 @@ HTTP_METHOD_INTERFACES = { 'DELETE':IDELETERequest, 'HEAD':IHEADRequest, } + +class IRoutesMapper(Interface): + """ Interface representing a Routes ``Mapper`` object """ + +class IContextNotFound(Interface): + """ Interface implemented by contexts generated by code which + cannot find a context during root finding or traversal """ diff --git a/repoze/bfg/registry.py b/repoze/bfg/registry.py index 8e4c9b5ec..c62a92a7d 100644 --- a/repoze/bfg/registry.py +++ b/repoze/bfg/registry.py @@ -5,7 +5,6 @@ import zope.component from zope.component import getGlobalSiteManager from zope.component.interfaces import ComponentLookupError from zope.component.interfaces import IComponentLookup -from zope.component.registry import Components from zope.component import getSiteManager as original_getSiteManager from zope.deferredimport import deprecated @@ -54,7 +53,7 @@ def setRegistryManager(manager): # for unit tests registry_manager = manager return old_registry_manager -def makeRegistry(filename, package, lock=threading.Lock()): +def populateRegistry(registry, filename, package, lock=threading.Lock()): """ We push our ZCML-defined configuration into an app-local component registry in order to allow more than one bfg app to live @@ -71,13 +70,11 @@ def makeRegistry(filename, package, lock=threading.Lock()): registry.""" lock.acquire() - registry = Components(package.__name__) registry_manager.push(registry) try: original_getSiteManager.sethook(getSiteManager) zope.component.getGlobalSiteManager = registry_manager.get zcml_configure(filename, package=package) - return registry finally: zope.component.getGlobalSiteManager = getGlobalSiteManager lock.release() diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index 817d5422a..849b40d7e 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -5,6 +5,7 @@ from zope.component import getAdapter from zope.component import getUtility from zope.component import queryUtility from zope.component.event import dispatch +from zope.component.registry import Components from zope.interface import alsoProvides from zope.interface import implements @@ -21,6 +22,7 @@ from repoze.bfg.interfaces import ILogger from repoze.bfg.interfaces import ITraverserFactory from repoze.bfg.interfaces import IRequest from repoze.bfg.interfaces import IRequestFactory +from repoze.bfg.interfaces import IRoutesMapper from repoze.bfg.interfaces import HTTP_METHOD_INTERFACES from repoze.bfg.interfaces import IRouter @@ -30,9 +32,11 @@ from repoze.bfg.interfaces import ISettings from repoze.bfg.log import make_stream_logger from repoze.bfg.registry import registry_manager -from repoze.bfg.registry import makeRegistry +from repoze.bfg.registry import populateRegistry from repoze.bfg.settings import Settings +from repoze.bfg.urldispatch import RoutesRootFactory + from repoze.bfg.view import render_view_to_response from repoze.bfg.view import view_execution_permitted @@ -140,13 +144,28 @@ def make_app(root_factory, package=None, filename='configure.zcml', e.g. ``{'reload_templates':True}``""" if options is None: options = {} - - registry = makeRegistry(filename, package) - registry.registerUtility(root_factory, IRootFactory) + regname = filename + if package: + regname = package.__name__ + registry = Components(regname) debug_logger = make_stream_logger('repoze.bfg.debug', sys.stderr) registry.registerUtility(debug_logger, ILogger, 'repoze.bfg.debug') settings = Settings(options) registry.registerUtility(settings, ISettings) + mapper = RoutesRootFactory(root_factory) + registry.registerUtility(mapper, IRoutesMapper) + populateRegistry(registry, filename, package) + if mapper.has_routes(): + # if the user had any statements in his configuration, + # use the RoutesRootFactory as the root factory + root_factory = mapper + else: + # otherwise, use only the supplied root_factory (unless it's None) + if root_factory is None: + raise ValueError( + 'root_factory (aka get_root) was None and no routes connected') + + registry.registerUtility(root_factory, IRootFactory) app = Router(registry) try: diff --git a/repoze/bfg/tests/fixtureapp/views.py b/repoze/bfg/tests/fixtureapp/views.py index ccf0e4811..f805b88c9 100644 --- a/repoze/bfg/tests/fixtureapp/views.py +++ b/repoze/bfg/tests/fixtureapp/views.py @@ -1,7 +1,7 @@ from zope.interface import Interface def fixture_view(context, request): - return None + """ """ class IDummy(Interface): pass diff --git a/repoze/bfg/tests/routesapp/__init__.py b/repoze/bfg/tests/routesapp/__init__.py new file mode 100644 index 000000000..546616b2c --- /dev/null +++ b/repoze/bfg/tests/routesapp/__init__.py @@ -0,0 +1 @@ +# fixture application diff --git a/repoze/bfg/tests/routesapp/configure.zcml b/repoze/bfg/tests/routesapp/configure.zcml new file mode 100644 index 000000000..388fc2330 --- /dev/null +++ b/repoze/bfg/tests/routesapp/configure.zcml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/repoze/bfg/tests/routesapp/models.py b/repoze/bfg/tests/routesapp/models.py new file mode 100644 index 000000000..a57b06308 --- /dev/null +++ b/repoze/bfg/tests/routesapp/models.py @@ -0,0 +1,5 @@ +from zope.interface import Interface + +class IFixture(Interface): + pass + diff --git a/repoze/bfg/tests/routesapp/templates/fixture.pt b/repoze/bfg/tests/routesapp/templates/fixture.pt new file mode 100644 index 000000000..06dd4e2b1 --- /dev/null +++ b/repoze/bfg/tests/routesapp/templates/fixture.pt @@ -0,0 +1,6 @@ + + + + + diff --git a/repoze/bfg/tests/routesapp/views.py b/repoze/bfg/tests/routesapp/views.py new file mode 100644 index 000000000..f805b88c9 --- /dev/null +++ b/repoze/bfg/tests/routesapp/views.py @@ -0,0 +1,8 @@ +from zope.interface import Interface + +def fixture_view(context, request): + """ """ + +class IDummy(Interface): + pass + diff --git a/repoze/bfg/tests/test_registry.py b/repoze/bfg/tests/test_registry.py index 5a8bc15c6..edd45b458 100644 --- a/repoze/bfg/tests/test_registry.py +++ b/repoze/bfg/tests/test_registry.py @@ -2,7 +2,7 @@ import unittest from zope.testing.cleanup import cleanUp -class TestMakeRegistry(unittest.TestCase): +class TestPopulateRegistry(unittest.TestCase): def setUp(self): cleanUp() @@ -10,8 +10,8 @@ class TestMakeRegistry(unittest.TestCase): cleanUp() def _callFUT(self, *arg, **kw): - from repoze.bfg.registry import makeRegistry - return makeRegistry(*arg, **kw) + from repoze.bfg.registry import populateRegistry + return populateRegistry(*arg, **kw) def test_it(self): from repoze.bfg.tests import fixtureapp @@ -20,11 +20,12 @@ class TestMakeRegistry(unittest.TestCase): import repoze.bfg.registry try: old = repoze.bfg.registry.setRegistryManager(dummyregmgr) - registry = self._callFUT('configure.zcml', - fixtureapp, - lock=dummylock) from zope.component.registry import Components - self.failUnless(isinstance(registry, Components)) + registry = Components('hello') + self._callFUT(registry, + 'configure.zcml', + fixtureapp, + lock=dummylock) self.assertEqual(dummylock.acquired, True) self.assertEqual(dummylock.released, True) self.assertEqual(dummyregmgr.registry, registry) diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index a4d0cb369..2a8539d4c 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -123,6 +123,24 @@ class RouterTests(unittest.TestCase): self.failIf('debug_notfound' in result[0]) self.assertEqual(len(logger.messages), 0) + def test_call_root_is_icontextnotfound(self): + from zope.interface import implements + from repoze.bfg.interfaces import IContextNotFound + class NotFound(object): + implements(IContextNotFound) + context = NotFound() + traversalfactory = make_traversal_factory(context, '', []) + self._registerTraverserFactory(traversalfactory, '', None) + environ = self._makeEnviron() + start_response = DummyStartResponse() + rootfactory = make_rootfactory(NotFound()) + self._registerRootFactory(rootfactory) + router = self._makeOne(None) + result = router(environ, start_response) + status = start_response.status + self.assertEqual(status, '404 Not Found') + self.failUnless('http://localhost:8080' in result[0], result) + def test_call_no_view_registered_debug_notfound_false(self): rootfactory = make_rootfactory(None) environ = self._makeEnviron() @@ -587,6 +605,75 @@ class MakeAppTests(unittest.TestCase): finally: repoze.bfg.router.registry_manager = old_registry_manager + def test_routes_in_config_with_rootpolicy(self): + options= {'reload_templates':True, + 'debug_authorization':True} + import repoze.bfg.router + old_registry_manager = repoze.bfg.router.registry_manager + dummy_registry_manager = DummyRegistryManager() + repoze.bfg.router.registry_manager = dummy_registry_manager + from repoze.bfg.urldispatch import RoutesRootFactory + try: + from repoze.bfg.tests import routesapp + rootpolicy = make_rootfactory(None) + app = self._callFUT(rootpolicy, routesapp, options=options) + from repoze.bfg.interfaces import ISettings + from repoze.bfg.interfaces import ILogger + from repoze.bfg.interfaces import IRootFactory + settings = app.registry.getUtility(ISettings) + logger = app.registry.getUtility(ILogger, name='repoze.bfg.debug') + rootfactory = app.registry.getUtility(IRootFactory) + self.assertEqual(logger.name, 'repoze.bfg.debug') + self.assertEqual(settings.reload_templates, True) + self.assertEqual(settings.debug_authorization, True) + self.failUnless(isinstance(rootfactory, RoutesRootFactory)) + self.assertEqual(rootfactory.get_root, rootpolicy) + self.assertEqual(dummy_registry_manager.pushed, True) + self.assertEqual(dummy_registry_manager.popped, True) + finally: + repoze.bfg.router.registry_manager = old_registry_manager + + def test_routes_in_config_no_rootpolicy(self): + options= {'reload_templates':True, + 'debug_authorization':True} + import repoze.bfg.router + old_registry_manager = repoze.bfg.router.registry_manager + dummy_registry_manager = DummyRegistryManager() + repoze.bfg.router.registry_manager = dummy_registry_manager + from repoze.bfg.urldispatch import RoutesRootFactory + try: + from repoze.bfg.tests import routesapp + app = self._callFUT(None, routesapp, options=options) + from repoze.bfg.interfaces import ISettings + from repoze.bfg.interfaces import ILogger + from repoze.bfg.interfaces import IRootFactory + settings = app.registry.getUtility(ISettings) + logger = app.registry.getUtility(ILogger, name='repoze.bfg.debug') + rootfactory = app.registry.getUtility(IRootFactory) + self.assertEqual(logger.name, 'repoze.bfg.debug') + self.assertEqual(settings.reload_templates, True) + self.assertEqual(settings.debug_authorization, True) + self.failUnless(isinstance(rootfactory, RoutesRootFactory)) + self.assertEqual(rootfactory.get_root, None) + self.assertEqual(dummy_registry_manager.pushed, True) + self.assertEqual(dummy_registry_manager.popped, True) + finally: + repoze.bfg.router.registry_manager = old_registry_manager + + def test_no_routes_in_config_no_rootpolicy(self): + options= {'reload_templates':True, + 'debug_authorization':True} + import repoze.bfg.router + old_registry_manager = repoze.bfg.router.registry_manager + dummy_registry_manager = DummyRegistryManager() + repoze.bfg.router.registry_manager = dummy_registry_manager + try: + from repoze.bfg.tests import fixtureapp + self.assertRaises(ValueError, self._callFUT, None, fixtureapp, + options=options) + finally: + repoze.bfg.router.registry_manager = old_registry_manager + class DummyRegistryManager: def push(self, registry): self.pushed = True diff --git a/repoze/bfg/tests/test_traversal.py b/repoze/bfg/tests/test_traversal.py index e83717843..873291bb8 100644 --- a/repoze/bfg/tests/test_traversal.py +++ b/repoze/bfg/tests/test_traversal.py @@ -198,6 +198,61 @@ class ModelGraphTraverserTests(unittest.TestCase): environ = self._getEnviron(PATH_INFO='/%s' % segment) ctx, name, subpath = policy(environ) # test is: this doesn't fail +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(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(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(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(self): + model = DummyContext() + traverser = self._makeOne(model) + result = traverser({}) + self.assertEqual(result[0], model) + self.assertEqual(result[1], '') + self.assertEqual(result[2], []) + class FindInterfaceTests(unittest.TestCase): def _callFUT(self, context, iface): from repoze.bfg.traversal import find_interface diff --git a/repoze/bfg/tests/test_urldispatch.py b/repoze/bfg/tests/test_urldispatch.py index 806af28df..82d4af991 100644 --- a/repoze/bfg/tests/test_urldispatch.py +++ b/repoze/bfg/tests/test_urldispatch.py @@ -1,6 +1,14 @@ import unittest class RoutesMapperTests(unittest.TestCase): + def setUp(self): + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + from zope.deprecation import __show__ + __show__.on() + def _getEnviron(self, **kw): environ = {'SERVER_NAME':'localhost', 'wsgi.url_scheme':'http'} @@ -72,36 +80,141 @@ class RoutesMapperTests(unittest.TestCase): result = url_for(controller='foo', action='action2', article='article2') self.assertEqual(result, '/archives/action2/article2') -class TestRoutesModelTraverser(unittest.TestCase): +class RoutesRootFactoryTests(unittest.TestCase): + def _getEnviron(self, **kw): + environ = {'SERVER_NAME':'localhost', + 'wsgi.url_scheme':'http'} + environ.update(kw) + return environ + def _getTargetClass(self): - from repoze.bfg.urldispatch import RoutesModelTraverser - return RoutesModelTraverser + from repoze.bfg.urldispatch import RoutesRootFactory + return RoutesRootFactory - def _makeOne(self, model): + def _makeOne(self, get_root): 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(self): - model = DummyModel() - traverser = self._makeOne(model) - result = traverser({}) - self.assertEqual(result[0], model) - self.assertEqual(result[1], 'controller') - self.assertEqual(result[2], '') - -class DummyModel: - controller = 'controller' - + return klass(get_root) + + def test_no_route_matches(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + environ = self._getEnviron(PATH_INFO='/') + result = mapper(environ) + self.assertEqual(result, marker) + self.assertEqual(mapper.environ, environ) + + def test_route_matches(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + mapper.connect('archives/:action/:article', view_name='foo') + environ = self._getEnviron(PATH_INFO='/archives/action1/article1') + result = mapper(environ) + from repoze.bfg.interfaces import IRoutesContext + self.failUnless(IRoutesContext.providedBy(result)) + self.assertEqual(result.view_name, 'foo') + self.assertEqual(result.action, 'action1') + self.assertEqual(result.article, 'article1') + + def test_unicode_in_route_default(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + class DummyRoute: + routepath = ':id' + context_factory = None + context_interfaces = () + la = unicode('\xc3\xb1a', 'utf-8') + mapper.routematch = lambda *arg: ({la:'id'}, DummyRoute) + mapper.connect(':la') + environ = self._getEnviron(PATH_INFO='/foo') + result = mapper(environ) + from repoze.bfg.interfaces import IRoutesContext + self.failUnless(IRoutesContext.providedBy(result)) + self.assertEqual(getattr(result, la.encode('utf-8')), 'id') + + def test_no_fallback_get_root(self): + marker = () + mapper = self._makeOne(None) + mapper.connect('wont/:be/:found', view_name='foo') + environ = self._getEnviron(PATH_INFO='/archives/action1/article1') + result = mapper(environ) + from repoze.bfg.urldispatch import RoutesContextNotFound + self.failUnless(isinstance(result, RoutesContextNotFound)) + + def test_custom_context_factory(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + from zope.interface import implements, Interface + class IDummy(Interface): + pass + class Dummy(object): + implements(IDummy) + def __init__(self, **kw): + self.__dict__.update(kw) + mapper.connect('archives/:action/:article', view_name='foo', + context_factory=Dummy) + environ = self._getEnviron(PATH_INFO='/archives/action1/article1') + result = mapper(environ) + self.assertEqual(result.view_name, 'foo') + self.assertEqual(result.action, 'action1') + self.assertEqual(result.article, 'article1') + from repoze.bfg.interfaces import IRoutesContext + self.failUnless(IRoutesContext.providedBy(result)) + self.failUnless(isinstance(result, Dummy)) + self.failUnless(IDummy.providedBy(result)) + self.failIf(hasattr(result, 'context_factory')) + + def test_custom_context_interfaces(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + from zope.interface import Interface + class IDummy(Interface): + pass + mapper.connect('archives/:action/:article', view_name='foo', + context_interfaces = [IDummy]) + environ = self._getEnviron(PATH_INFO='/archives/action1/article1') + result = mapper(environ) + self.assertEqual(result.view_name, 'foo') + self.assertEqual(result.action, 'action1') + self.assertEqual(result.article, 'article1') + from repoze.bfg.interfaces import IRoutesContext + self.failUnless(IRoutesContext.providedBy(result)) + self.failUnless(IDummy.providedBy(result)) + self.failIf(hasattr(result, 'context_interfaces')) + + def test_has_routes(self): + mapper = self._makeOne(None) + self.assertEqual(mapper.has_routes(), False) + mapper.connect('archives/:action/:article', view_name='foo') + self.assertEqual(mapper.has_routes(), True) + + def test_url_for(self): + marker = () + get_root = make_get_root(marker) + mapper = self._makeOne(get_root) + mapper.connect('archives/:action/:article', view_name='foo') + environ = self._getEnviron(PATH_INFO='/archives/action1/article1') + result = mapper(environ) + from routes import url_for + result = url_for(view_name='foo', action='action2', article='article2') + self.assertEqual(result, '/archives/action2/article2') + +class TestRoutesContextNotFound(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.urldispatch import RoutesContextNotFound + return RoutesContextNotFound + + def _makeOne(self, msg): + return self._getTargetClass()(msg) + + def test_it(self): + inst = self._makeOne('hi') + self.assertEqual(inst.msg, 'hi') + def make_get_root(result): def dummy_get_root(environ): return result diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index a59f88ae1..26de9481a 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -227,7 +227,184 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(regadapt['args'][3], IView) self.assertEqual(regadapt['args'][4], '') self.assertEqual(regadapt['args'][5], None) + +class TestRouteRequirementFunction(unittest.TestCase): + def _callFUT(self, context, attr, expr): + from repoze.bfg.zcml import route_requirement + return route_requirement(context, attr, expr) + + def test_it(self): + context = DummyContext() + context.context = DummyContext() + context.context.requirements = {} + self._callFUT(context, 'a', 'b') + self.assertEqual(context.context.requirements['a'], 'b') + self.assertRaises(ValueError, self._callFUT, context, 'a', 'b') + +class TestConnectRouteFunction(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, directive): + from repoze.bfg.zcml import connect_route + return connect_route(directive) + + def _registerRoutesMapper(self): + from zope.component import getGlobalSiteManager + gsm = getGlobalSiteManager() + mapper = DummyMapper() + from repoze.bfg.interfaces import IRoutesMapper + gsm.registerUtility(mapper, IRoutesMapper) + return mapper + + def test_no_mapper(self): + directive = DummyRouteDirective() + self._callFUT(directive) # doesn't blow up when no routes mapper reg'd + + def test_defaults(self): + mapper = self._registerRoutesMapper() + directive = DummyRouteDirective() + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + self.assertEqual(mapper.connections[0][1], {'requirements': {}}) + + def test_name_and_path(self): + mapper = self._registerRoutesMapper() + directive = DummyRouteDirective(name='abc') + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('abc', 'a/b/c',)) + self.assertEqual(mapper.connections[0][1], {'requirements': {}}) + + def test_all_directives(self): + mapper = self._registerRoutesMapper() + def foo(): + """ """ + directive = DummyRouteDirective( + minimize=True, explicit=True, encoding='utf-8', static=True, + filter=foo, absolute=True, member_name='m', collection_name='c', + parent_member_name='p', parent_collection_name='c', + condition_method='GET', condition_subdomain=True, + condition_function=foo, subdomains=['a'], + context_factory=foo, context_interfaces=[IDummy]) + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + pr = {'member_name':'p', 'collection_name':'c'} + c = {'method':'GET', 'sub_domain':['a'], 'function':foo} + self.assertEqual(mapper.connections[0][1], + {'requirements': {}, + '_minimize':True, + '_explicit':True, + '_encoding':'utf-8', + '_static':True, + '_filter':foo, + '_absolute':True, + '_member_name':'m', + '_collection_name':'c', + '_parent_resource':pr, + 'conditions':c, + 'context_factory':foo, + 'context_interfaces':[IDummy], + }) + + def test_condition_subdomain_true(self): + mapper = self._registerRoutesMapper() + directive = DummyRouteDirective(static=True, explicit=True, + condition_subdomain=True) + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + self.assertEqual(mapper.connections[0][1], + {'requirements': {}, + '_static':True, + '_explicit':True, + 'conditions':{'sub_domain':True} + }) + + def test_condition_function(self): + mapper = self._registerRoutesMapper() + def foo(e, r): + """ """ + directive = DummyRouteDirective(static=True, explicit=True, + condition_function=foo) + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + self.assertEqual(mapper.connections[0][1], + {'requirements': {}, + '_static':True, + '_explicit':True, + 'conditions':{'function':foo} + }) + + def test_condition_method(self): + mapper = self._registerRoutesMapper() + directive = DummyRouteDirective(static=True, explicit=True, + condition_method='GET') + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + self.assertEqual(mapper.connections[0][1], + {'requirements': {}, + '_static':True, + '_explicit':True, + 'conditions':{'method':'GET'} + }) + + def test_subdomains(self): + mapper = self._registerRoutesMapper() + directive = DummyRouteDirective(static=True, explicit=True, + subdomains=['a', 'b']) + self._callFUT(directive) + self.assertEqual(len(mapper.connections), 1) + self.assertEqual(mapper.connections[0][0], ('a/b/c',)) + self.assertEqual(mapper.connections[0][1], + {'requirements': {}, + '_static':True, + '_explicit':True, + 'conditions':{'sub_domain':['a', 'b']} + }) + +class TestRouteGroupingContextDecorator(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _getTargetClass(self): + from repoze.bfg.zcml import Route + return Route + + def _makeOne(self, context, path, **kw): + return self._getTargetClass()(context, path, **kw) + + def test_defaults(self): + context = DummyContext() + route = self._makeOne(context, 'abc') + self.assertEqual(route.requirements, {}) + self.assertEqual(route.parent_member_name, None) + self.assertEqual(route.parent_collection_name, None) + + def test_parent_collection_name_missing(self): + context = DummyContext() + self.assertRaises(ValueError, self._makeOne, context, 'abc', + parent_member_name='a') + + def test_parent_collection_name_present(self): + context = DummyContext() + route = self._makeOne(context, 'abc', + parent_member_name='a', + parent_collection_name='p') + self.assertEqual(route.parent_member_name, 'a') + self.assertEqual(route.parent_collection_name, 'p') + class TestZCMLPickling(unittest.TestCase): i = 0 @@ -565,6 +742,38 @@ class DummyContext: class Dummy: pass +class DummyRouteDirective: + encoding = None + static = False + minimize = False + explicit = False + static = False + filter = None + absolute = False + member_name = False + collection_name = None + parent_member_name = None + parent_collection_name = None + condition_method = None + condition_subdomain = None + condition_function = None + subdomains = None + path = 'a/b/c' + name = None + context_factory = None + context_interfaces = () + def __init__(self, **kw): + if not 'requirements' in kw: + kw['requirements'] = {} + self.__dict__.update(kw) + +class DummyMapper: + def __init__(self): + self.connections = [] + + def connect(self, *arg, **kw): + self.connections.append((arg, kw)) + from zope.interface import Interface class IDummy(Interface): pass diff --git a/repoze/bfg/traversal.py b/repoze/bfg/traversal.py index 5960663c8..200deb8b7 100644 --- a/repoze/bfg/traversal.py +++ b/repoze/bfg/traversal.py @@ -19,71 +19,6 @@ deprecated( model_url = "repoze.bfg.url:model_url", ) -def split_path(path): - while path.startswith('/'): - path = path[1:] - while path.endswith('/'): - path = path[:-1] - clean = [] - for segment in path.split('/'): - segment = urllib.unquote(segment) # deal with spaces in path segment - if not segment or segment=='.': - continue - elif segment == '..': - del clean[-1] - else: - clean.append(segment) - return clean - -def step(ob, name, default, as_unicode=True): - if as_unicode: - try: - name = name.decode('utf-8') - except UnicodeDecodeError: - raise TypeError('Could not decode path segment "%s" using the ' - 'UTF-8 decoding scheme' % name) - if name.startswith('@@'): - return name[2:], default - if not hasattr(ob, '__getitem__'): - return name, default - try: - return name, ob[name] - except KeyError: - return name, default - -_marker = [] - -class ModelGraphTraverser(object): - classProvides(ITraverserFactory) - implements(ITraverser) - def __init__(self, root): - self.root = root - self.locatable = ILocation.providedBy(root) - self.unicode_path_segments = True - settings = queryUtility(ISettings) - if settings is not None: - self.unicode_path_segments = settings.unicode_path_segments - - def __call__(self, environ): - unicode_path_segments = self.unicode_path_segments - path = environ.get('PATH_INFO', '/') - path = split_path(path) - ob = self.root - - name = '' - - while path: - segment = path.pop(0) - segment, next = step(ob, segment, _marker, unicode_path_segments) - if next is _marker: - name = segment - break - if (self.locatable) and (not ILocation.providedBy(next)): - next = LocationProxy(next, ob, segment) - ob = next - - return ob, name, path - def find_root(model): """ Find the root node in the graph to which ``model`` belongs. Note that ``model`` should be :term:`location`-aware. @@ -164,3 +99,82 @@ def model_path(model, *elements): path = '/'.join([path, suffix]) return path + +def split_path(path): + while path.startswith('/'): + path = path[1:] + while path.endswith('/'): + path = path[:-1] + clean = [] + for segment in path.split('/'): + segment = urllib.unquote(segment) # deal with spaces in path segment + if not segment or segment=='.': + continue + elif segment == '..': + del clean[-1] + else: + clean.append(segment) + return clean + +def step(ob, name, default, as_unicode=True): + if as_unicode: + try: + name = name.decode('utf-8') + except UnicodeDecodeError: + raise TypeError('Could not decode path segment "%s" using the ' + 'UTF-8 decoding scheme' % name) + if name.startswith('@@'): + return name[2:], default + if not hasattr(ob, '__getitem__'): + return name, default + try: + return name, ob[name] + except KeyError: + return name, default + +_marker = [] + +class ModelGraphTraverser(object): + classProvides(ITraverserFactory) + implements(ITraverser) + def __init__(self, root): + self.root = root + self.locatable = ILocation.providedBy(root) + self.unicode_path_segments = True + settings = queryUtility(ISettings) + if settings is not None: + self.unicode_path_segments = settings.unicode_path_segments + + def __call__(self, environ): + unicode_path_segments = self.unicode_path_segments + path = environ.get('PATH_INFO', '/') + path = split_path(path) + ob = self.root + + name = '' + + while path: + segment = path.pop(0) + segment, next = step(ob, segment, _marker, unicode_path_segments) + if next is _marker: + name = segment + break + if (self.locatable) and (not ILocation.providedBy(next)): + next = LocationProxy(next, ob, segment) + ob = next + + return ob, name, path + +class RoutesModelTraverser(object): + classProvides(ITraverserFactory) + implements(ITraverser) + def __init__(self, context): + self.context = context + + def __call__(self, environ): + view_name = getattr(self.context, 'controller', None) # b/w compat<0.6.3 + if view_name is None: + view_name = getattr(self.context, 'view_name', '') # 0.6.3+ + subpath = getattr(self.context, 'subpath', '') # 0.6.3+ + subpath = filter(None, subpath.split('/')) + return self.context, view_name, subpath diff --git a/repoze/bfg/urldispatch.py b/repoze/bfg/urldispatch.py index 5a8c7664f..1881ebb07 100644 --- a/repoze/bfg/urldispatch.py +++ b/repoze/bfg/urldispatch.py @@ -1,17 +1,30 @@ from zope.interface import implements -from zope.interface import classProvides from zope.interface import alsoProvides from routes import Mapper from routes import request_config from repoze.bfg.interfaces import IRoutesContext -from repoze.bfg.interfaces import ITraverserFactory -from repoze.bfg.interfaces import ITraverser +from repoze.bfg.interfaces import IContextNotFound + +from zope.deferredimport import deprecated +from zope.deprecation import deprecated as deprecated2 _marker = () -class RoutesContext(object): +deprecated( + "('from repoze.bfg.urldispatch import RoutesContext' is now " + "deprecated; instead use 'from repoze.bfg.urldispatch import " + "DefaultRoutesContext')", + RoutesContext = "repoze.bfg.urldispatch:DefaultRoutesContext", + ) + +deprecated2('RoutesMapper', + 'Usage of the ``RoutesMapper`` class is deprecated. As of ' + 'repoze.bfg 0.6.3, you should use the ```` ZCML ' + 'directive instead of manually creating a RoutesMapper.') + +class DefaultRoutesContext(object): implements(IRoutesContext) def __init__(self, **kw): self.__dict__.update(kw) @@ -28,7 +41,13 @@ class RoutesMapper(object): *name* matches the Routes 'controller' name for the match. It will be passed a context object that has attributes that match the Routes match arguments dictionary keys. If no Routes route - matches the current request, the 'fallback' get_root is called.""" + matches the current request, the 'fallback' get_root is called. + + .. warning:: This class is deprecated. As of :mod:`repoze.bfg` + 0.6.3, you should use the ```` ZCML directive instead + of manually creating a RoutesMapper. See :ref:`urldispatch_chapter` + for more information. + """ def __init__(self, get_root): self.get_root = get_root self.mapper = Mapper(controller_scan=None, directory=None, @@ -46,7 +65,7 @@ class RoutesMapper(object): if args: context_factory = args.get('context_factory', _marker) if context_factory is _marker: - context_factory = RoutesContext + context_factory = DefaultRoutesContext else: args = args.copy() del args['context_factory'] @@ -74,24 +93,100 @@ class RoutesMapper(object): arguments supplied by the Routes mapper's ``match`` method for this route, and should return an instance of a class. If ``context_factory`` is not supplied in this way for a route, a - default context factory (the ``RoutesContext`` class) will be - used. The interface ``repoze.bfg.interfaces.IRoutesContext`` - will always be tacked on to the context instance in addition - to whatever interfaces the context instance already supplies. + default context factory (the ``DefaultRoutesContext`` class) + will be used. The interface + ``repoze.bfg.interfaces.IRoutesContext`` will always be tacked + on to the context instance in addition to whatever interfaces + the context instance already supplies. """ self.mapper.connect(*arg, **kw) -class RoutesModelTraverser(object): - classProvides(ITraverserFactory) - implements(ITraverser) - def __init__(self, context): - self.context = context +class RoutesContextNotFound(object): + implements(IContextNotFound) + def __init__(self, msg): + self.msg = msg + +class RoutesRootFactory(Mapper): + """ The ``RoutesRootFactory`` is a wrapper for the ``get_root`` + callable passed in to the repoze.bfg ``Router`` at initialization + time. When it is instantiated, it wraps the get_root of an + application in such a way that the `Routes + `_ engine has the 'first + crack' at resolving the current request URL to a repoze.bfg view. + Any view that claims it is 'for' the interface + ``repoze.bfg.interfaces.IRoutesContext`` will be called if its + *name* matches the Routes ``view_name`` name for the match and any + of the interfaces named in ``context_interfaces``. It will be + passed a context object that has attributes that match the Routes + match arguments dictionary keys. If no Routes route matches the + current request, the 'fallback' get_root is called.""" + def __init__(self, get_root=None, **kw): + self.get_root = get_root + kw['controller_scan'] = None + kw['always_scan'] = False + kw['directory'] = None + kw['explicit'] = True + Mapper.__init__(self, **kw) + self._regs_created = False + + def has_routes(self): + return bool(self.matchlist) + + def connect(self, *arg, **kw): + # we need to deal with our custom attributes specially :-( + context_factory = None + context_interfaces = () + if 'context_interfaces' in kw: + context_interfaces = kw.pop('context_interfaces') + if 'context_factory' in kw: + context_factory = kw.pop('context_factory') + result = Mapper.connect(self, *arg, **kw) + self.matchlist[-1].context_factory = context_factory + self.matchlist[-1].context_interfaces = context_interfaces + return result def __call__(self, environ): - return self.context, self.context.controller, '' - - - - + if not self._regs_created: + self.create_regs([]) + self._regs_created = True + path = environ.get('PATH_INFO', '/') + self.environ = environ # sets the thread local + match = self.routematch(path) + if match: + args, route = match + else: + args = None + if args: + args = args.copy() + routepath = route.routepath + context_factory = route.context_factory + if not context_factory: + context_factory = DefaultRoutesContext + config = request_config() + config.mapper = self + config.mapper_dict = args + config.host = environ.get('HTTP_HOST', environ['SERVER_NAME']) + config.protocol = environ['wsgi.url_scheme'] + config.redirect = None + kw = {} + for k, v in args.items(): + # Routes "helpfully" converts default parameter names + # into Unicode; these can't be used as attr names + if k.__class__ is unicode: + k = k.encode('utf-8') + kw[k] = v + context = context_factory(**kw) + context_interfaces = route.context_interfaces + for iface in context_interfaces: + alsoProvides(context, iface) + alsoProvides(context, IRoutesContext) + return context + + if self.get_root is None: + # no fallback get_root + return RoutesContextNotFound( + 'Routes context cannot be found and no fallback "get_root"') + # fall back to original get_root + return self.get_root(environ) diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index 42e979bcb..72839495d 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -6,18 +6,24 @@ import types from zope.configuration import xmlconfig -from zope.component import getSiteManager from zope.component import adaptedBy +from zope.component import getSiteManager +from zope.component import queryUtility + import zope.configuration.config from zope.configuration.exceptions import ConfigurationError from zope.configuration.fields import GlobalObject +from zope.configuration.fields import Tokens from zope.interface import Interface +from zope.interface import implements +from zope.schema import Bool from zope.schema import TextLine from repoze.bfg.interfaces import IRequest +from repoze.bfg.interfaces import IRoutesMapper from repoze.bfg.interfaces import IViewPermission from repoze.bfg.interfaces import IView from repoze.bfg.path import package_path @@ -250,3 +256,142 @@ class BFGViewFunctionGrokker(martian.InstanceGrokker): cacheable=Uncacheable) return True return False + +class IRouteRequirementDirective(Interface): + """ The interface for the ``requirement`` route subdirective """ + attr = TextLine(title=u'attr', required=True) + expr = TextLine(title=u'expression', required=True) + +def route_requirement(context, attr, expr): + route = context.context + if attr in route.requirements: + raise ValueError('Duplicate requirement', attr) + route.requirements[attr] = expr + +class IRouteDirective(Interface): + """ The interface for the ``route`` ZCML directive + """ + path = TextLine(title=u'path', required=True) + name = TextLine(title=u'name', required=False) + context_factory = GlobalObject(title=u'context_factory', required=False) + context_interfaces = Tokens(title=u'context_interfaces', required=False, + value_type=GlobalObject()) + minimize = Bool(title=u'minimize', required=False) + encoding = TextLine(title=u'path', required=False) + static = Bool(title=u'static', required=False) + filter = GlobalObject(title=u'filter', required=False) + absolute = Bool(title=u'absolute', required=False) + member_name = TextLine(title=u'member_name', required=False) + collection_name = TextLine(title=u'collection_name', required=False) + condition_method = TextLine(title=u'condition_method', required=False) + condition_subdomain = TextLine(title=u'condition_subdomain', required=False) + condition_function = GlobalObject(title=u'condition_function', + required=False) + parent_member_name = TextLine(title=u'parent member_name', required=False) + parent_collection_name = TextLine(title=u'parent collection_name', + required=False) + explicit = Bool(title=u'explicit', required=False) + subdomains = Tokens(title=u'subdomains', required=False, + value_type=TextLine()) + +def connect_route(directive): + mapper = queryUtility(IRoutesMapper) + if mapper is None: + return + args = [] + if directive.name: + args.append(directive.name) + args.append(directive.path) + kw = dict(requirements=directive.requirements) + if directive.minimize: + kw['_minimize'] = True + if directive.explicit: + kw['_explicit'] = True + if directive.encoding: + kw['_encoding'] = directive.encoding + if directive.static: + kw['_static'] = True + if directive.filter: + kw['_filter'] = directive.filter + if directive.absolute: + kw['_absolute'] = True + if directive.member_name: + kw['_member_name'] = directive.member_name + if directive.collection_name: + kw['_collection_name'] = directive.collection_name + if directive.parent_member_name and directive.parent_collection_name: + kw['_parent_resource'] = { + 'member_name':directive.parent_member_name, + 'collection_name':directive.parent_collection_name, + } + conditions = {} + if directive.condition_method: + conditions['method'] = directive.condition_method + if directive.condition_subdomain: + conditions['sub_domain'] = directive.condition_subdomain + if directive.condition_function: + conditions['function'] = directive.condition_function + if directive.subdomains: + conditions['sub_domain'] = directive.subdomains + if conditions: + kw['conditions'] = conditions + + if directive.context_factory: + kw['context_factory'] = directive.context_factory + if directive.context_interfaces: + kw['context_interfaces'] = directive.context_interfaces + + return mapper.connect(*args, **kw) + +class Route(zope.configuration.config.GroupingContextDecorator): + """ Handle ``route`` ZCML directives + """ + + implements(zope.configuration.config.IConfigurationContext,IRouteDirective) + + def __init__(self, context, path, name=None, context_factory=None, + context_interfaces=(), minimize=True, encoding=None, + static=False, filter=None, absolute=False, + member_name=None, collection_name=None, condition_method=None, + condition_subdomain=None, condition_function=None, + parent_member_name=None, parent_collection_name=None, + subdomains=None, explicit=False): + self.context = context + self.path = path + self.name = name + self.context_factory = context_factory + self.context_interfaces = context_interfaces + self.minimize = minimize + self.encoding = encoding + self.static = static + self.filter = filter + self.absolute = absolute + self.member_name = member_name + self.collection_name = collection_name + self.condition_method= condition_method + self.condition_subdomain = condition_subdomain + self.condition_function = condition_function + self.explicit = explicit + self.subdomains = subdomains + if parent_member_name is not None: + if parent_collection_name is not None: + self.parent_member_name = parent_member_name + self.parent_collection_name = parent_collection_name + else: + raise ValueError( + 'parent_member_name and parent_collection_name must be ' + 'specified together') + else: + self.parent_member_name = None + self.parent_collection_name = None + # added by subdirectives + self.requirements = {} + + def after(self): + self.context.action( + discriminator = ('route', self.path, repr(self.requirements), + self.condition_method, self.condition_subdomain, + self.condition_function, self.subdomains), + callable = connect_route, + args = (self,), + ) -- cgit v1.2.3