summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-01-18 23:38:47 +0000
committerChris McDonough <chrism@agendaless.com>2009-01-18 23:38:47 +0000
commit62267e01d6eeaf8de871487898ad1ce02878c29a (patch)
tree234346245e58993c27e139906ee7a19a7b67a8e4
parent58afb33d70dece410b998a9c064eba42e3f843d7 (diff)
downloadpyramid-62267e01d6eeaf8de871487898ad1ce02878c29a.tar.gz
pyramid-62267e01d6eeaf8de871487898ad1ce02878c29a.tar.bz2
pyramid-62267e01d6eeaf8de871487898ad1ce02878c29a.zip
Merge "routesmapper branch" to trunk.
-rw-r--r--CHANGES.txt40
-rw-r--r--docs/api/view.rst3
-rw-r--r--docs/narr/urldispatch.rst242
-rw-r--r--repoze/bfg/includes/configure.zcml2
-rw-r--r--repoze/bfg/includes/meta.zcml15
-rw-r--r--repoze/bfg/interfaces.py7
-rw-r--r--repoze/bfg/registry.py5
-rw-r--r--repoze/bfg/router.py27
-rw-r--r--repoze/bfg/tests/fixtureapp/views.py2
-rw-r--r--repoze/bfg/tests/routesapp/__init__.py1
-rw-r--r--repoze/bfg/tests/routesapp/configure.zcml16
-rw-r--r--repoze/bfg/tests/routesapp/models.py5
-rw-r--r--repoze/bfg/tests/routesapp/templates/fixture.pt6
-rw-r--r--repoze/bfg/tests/routesapp/views.py8
-rw-r--r--repoze/bfg/tests/test_registry.py15
-rw-r--r--repoze/bfg/tests/test_router.py87
-rw-r--r--repoze/bfg/tests/test_traversal.py55
-rw-r--r--repoze/bfg/tests/test_urldispatch.py167
-rw-r--r--repoze/bfg/tests/test_zcml.py209
-rw-r--r--repoze/bfg/traversal.py144
-rw-r--r--repoze/bfg/urldispatch.py135
-rw-r--r--repoze/bfg/zcml.py147
22 files changed, 1060 insertions, 278 deletions
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
+ <http://routes.groovie.org>`_). 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
+ ``<route..>`` 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
<http://routes.groovie.org/manual.html#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
+ <route
+ path="ideas/:idea"
+ view_name="ideas"/>
- def fallback_get_root(environ):
- return {}
+ <route
+ path="users/:user"
+ view_name="users"/>
- root = RoutesMapper(fallback_get_root)
- root.connect('ideas/:idea', controller='ideas')
- root.connect('users/:user', controller='users')
- root.connect('tags/:tag', controller='tags')
+ <route
+ path="tags/:tag"
+ view_name="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/<ideaname>
/users/<username>
/tags/<tagname>
-If this mapper is used as a ``get_root`` callback, when a URL matches
-the pattern ``/ideas/<ideaname>``, 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/<ideaname>``, 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:
<view
- for="repoze.bfg.interfaces.IRoutesContext"
+ for=".interfaces.ISomeContext"
view=".views.articles_view"
name="articles"
/>
+ <route
+ path="archives/:article"
+ view_name="articles"
+ context_factory=".models.Article"
+ context_interfaces=".interfaces.ISomeContext"
+ />
+
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 <http://svn.repoze.org/repoze.shootout/trunk/>`_
-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 @@
/>
<adapter
- factory="repoze.bfg.urldispatch.RoutesModelTraverser"
+ factory="repoze.bfg.traversal.RoutesModelTraverser"
provides="repoze.bfg.interfaces.ITraverserFactory"
for="repoze.bfg.interfaces.IRoutesContext"
/>
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 @@
</meta:directives>
+ <meta:groupingDirective
+ namespace="http://namespaces.repoze.org/bfg"
+ name="route"
+ schema="repoze.bfg.zcml.IRouteDirective"
+ handler="repoze.bfg.zcml.Route"
+ />
+
+ <meta:directive
+ name="requirement"
+ namespace="http://namespaces.repoze.org/bfg"
+ usedIn="repoze.bfg.zcml.IRouteDirective"
+ schema="repoze.bfg.zcml.IRouteRequirementDirective"
+ handler="repoze.bfg.zcml.route_requirement"
+ />
+
</configure>
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 <route/> 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 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <include package="repoze.bfg.includes" />
+
+ <route
+ path=":id/:view_name"
+ context_interfaces=".models.IFixture"/>
+
+ <view
+ name="default"
+ view=".views.fixture_view"
+ for=".models.IFixture"
+ permission="repoze.view"
+ />
+
+</configure>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head></head>
+<body>
+</body>
+</html>
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 ``<route.. >`` 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 ``<route.. >`` 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
+ <http://routes.groovie.org/index.html>`_ 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,),
+ )