summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt19
-rw-r--r--TODO.txt13
-rw-r--r--docs/api/configuration.rst2
-rw-r--r--pyramid/configuration.py156
-rw-r--r--pyramid/tests/test_configuration.py263
5 files changed, 442 insertions, 11 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index cf40de0e6..8070a3d62 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -7,6 +7,19 @@ Features (delta from BFG 1.3.X)
- Add ``pyramid.httpexceptions`` module, which is a facade for the
``webob.exc`` module.
+- Direct built-in support for the Mako templating language.
+
+- A "squiggly" (Pylons-style) route syntax is now supported by
+ ``pyramid.configuration.Configurator.add_route`` (and thus, by the
+ ``route`` ZCML directive too). For example, the following route
+ patterns are equivalent: ``{foo}/{bar}`` and ``:foo/:bar``. This is
+ purely a syntactic affordance to make route patterns more tolerable
+ for people coming from Pylons.
+
+- A new configurator method exists: ``add_handler``. This method adds
+ a Pylons-style "view handler" (such a thing used to be called a
+ "controller" in Pylons 1.0).
+
Documentation (delta from BFG 1.3)
-----------------------------------
@@ -27,9 +40,6 @@ Backwards Incompatibilities (with BFG 1.3.X)
- The logger which used to have the name of ``repoze.bfg.debug`` now
has the name ``pyramid.debug``.
-- The deprecated API named ``pyramid.router.make_app`` (aka
- ``pyramid.configuration.make_app``) was removed.
-
- The deprecated API named ``pyramid.request.get_request`` was removed.
- The deprecated API named ``pyramid.security.Unauthorized`` was
@@ -40,3 +50,6 @@ Backwards Incompatibilities (with BFG 1.3.X)
- The deprecated API named ``pyramid.view.NotFound`` was removed.
+- The literal pattern ``{<anything>}`` is no longer permitted in route
+ patterns (due to the addition of squiggly route pattern syntax
+ support).
diff --git a/TODO.txt b/TODO.txt
index 840dfb072..713ba64a1 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -62,8 +62,6 @@
- Update App engine chapter.
-- P2
-
- Add SessionFactory interface
- Create session interface (flash() is part of it, figure out granularity)
@@ -74,17 +72,16 @@
- Handler stuff from Pylons2
-- HTTPExceptions facade
-
- Signed_signature method of the request and response from pylons2.
- Provide a webob.Response class facade for forward compat.
- CRSF token machinery
-- Converter: <configure xmlns="http://namespaces.repoze.org/bfg"> ->
- <configure xmlns="http://pyramid.pylonshq.com/">
+- Mako docs (in templating).
+
+- Paster template that has Mako.
-- Converter: <include package="repoze.bfg.includes" /> ->
- <include package="pyramid.includes" />
+- ``add_handler`` documentation.
+- ``handler`` ZCML directive.
diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst
index c0b52ed3f..f13fc48e5 100644
--- a/docs/api/configuration.rst
+++ b/docs/api/configuration.rst
@@ -42,6 +42,8 @@
.. automethod:: add_translation_dirs
+ .. automethod:: add_handler
+
.. automethod:: add_view
.. automethod:: derive_view
diff --git a/pyramid/configuration.py b/pyramid/configuration.py
index d255ff0ca..d94e5504b 100644
--- a/pyramid/configuration.py
+++ b/pyramid/configuration.py
@@ -193,6 +193,8 @@ class Configurator(object):
manager = manager # for testing injection
venusian = venusian # for testing injection
+ squiggly_route_re = re.compile(r'(/{[a-zA-Z]\w*})') # for add_route
+
def __init__(self,
registry=None,
package=None,
@@ -632,6 +634,118 @@ class Configurator(object):
self.manager.pop()
return self.registry
+ def add_handler(self, route_name, pattern, handler, action=None, **kw):
+
+ """ Add a Pylons-style view handler. This function adds a
+ route and some number of views based on a handler object
+ (usually a class).
+
+ ``route_name`` is the name of the route (to be used later in
+ URL generation).
+
+ ``pattern`` is the matching pattern,
+ e.g. ``'/blog/{action}'``. ``pattern`` may be ``None``, in
+ which case the pattern of an existing route named the same as
+ ``route_name`` is used. If ``pattern`` is ``None`` and no
+ route named ``route_name`` exists, a ``ConfigurationError`` is
+ raised.
+
+ ``handler`` is a dotted name of (or direct reference to) a
+ Python handler class,
+ e.g. ``'my.package.handlers.MyHandler'``.
+
+ If ``{action}`` or ``:action`` is in
+ the pattern, the exposed methods of the handler will be used
+ as views.
+
+ If ``action`` is passed, it will be considered the method name
+ of the handler to use as a view.
+
+ Passing both ``action`` and having an ``{action}`` in the
+ route pattern is disallowed.
+
+ Any extra keyword arguments are passed along to ``add_route``.
+
+ This method returns the result of add_route."""
+ handler = self.maybe_dotted(handler)
+
+ if pattern is not None:
+ route = self.add_route(route_name, pattern, **kw)
+ else:
+ mapper = self.get_routes_mapper()
+ route = mapper.get_route(route_name)
+ if route is None:
+ raise ConfigurationError(
+ 'The "pattern" parameter may only be "None" when a route '
+ 'with the route_name argument was previously registered. '
+ 'No such route named %r exists' % route_name)
+
+ pattern = route.pattern
+
+ path_has_action = ':action' in pattern or '{action}' in pattern
+
+ if action and path_has_action:
+ raise ConfigurationError(
+ 'action= (%r) disallowed when an action is in the route '
+ 'path %r' % (action, pattern))
+
+ if path_has_action:
+ autoexpose = getattr(handler, '__autoexpose__', r'[A-Za-z]+')
+ if autoexpose:
+ try:
+ autoexpose = re.compile(autoexpose).match
+ except (re.error, TypeError), why:
+ raise ConfigurationError(why[0])
+ for method_name, method in inspect.getmembers(
+ handler, inspect.ismethod):
+ configs = getattr(method, '__exposed__', [])
+ if autoexpose and not configs:
+ if autoexpose(method_name):
+ configs = [{}]
+ for expose_config in configs:
+ # we don't want to mutate any dict in __exposed__,
+ # so we copy each
+ view_args = expose_config.copy()
+ action = view_args.pop('name', method_name)
+ preds = list(view_args.pop('custom_predicates', []))
+ preds.append(ActionPredicate(action))
+ view_args['custom_predicates'] = preds
+ self.add_view(view=handler, attr=method_name,
+ route_name=route_name, **view_args)
+ else:
+ method_name = action
+ if method_name is None:
+ method_name = '__call__'
+
+ # Scan the controller for any other methods with this action name
+ for meth_name, method in inspect.getmembers(
+ handler, inspect.ismethod):
+ configs = getattr(method, '__exposed__', [{}])
+ for expose_config in configs:
+ # Don't re-register the same view if this method name is
+ # the action name
+ if meth_name == action:
+ continue
+ # We only reg a view if the name matches the action
+ if expose_config.get('name') != method_name:
+ continue
+ # we don't want to mutate any dict in __exposed__,
+ # so we copy each
+ view_args = expose_config.copy()
+ del view_args['name']
+ self.add_view(view=handler, attr=meth_name,
+ route_name=route_name, **view_args)
+
+ # Now register the method itself
+ method = getattr(handler, method_name, None)
+ configs = getattr(method, '__exposed__', [{}])
+ for expose_config in configs:
+ self.add_view(view=handler, attr=action, route_name=route_name,
+ **expose_config)
+
+ return route
+
+
def add_view(self, view=None, name="", for_=None, permission=_marker,
request_type=None, route_name=None, request_method=None,
request_param=None, containment=None, attr=None,
@@ -1393,6 +1507,24 @@ class Configurator(object):
pattern = path
if pattern is None:
raise ConfigurationError('"pattern" argument may not be None')
+
+ # Support the ``/:colon`` syntax supported by BFG but also
+ # support the ``/{squiggly}`` segment syntax familiar to
+ # Pylons folks by transforming it (internally) into
+ # ``/:colon`` syntax.
+
+ parts = self.squiggly_route_re.split(pattern)
+ npattern = []
+
+ for part in parts:
+ match = self.squiggly_route_re.match(part)
+ if match:
+ npattern.append('/:%s' % match.group()[2:-1])
+ else:
+ npattern.append(part)
+
+ pattern = ''.join(npattern)
+
return mapper.connect(name, pattern, factory, predicates=predicates,
pregenerator=pregenerator)
@@ -2651,3 +2783,27 @@ class DottedNameResolver(object):
'The dotted name %r cannot be imported' % (dotted,))
return dotted
+
+class ActionPredicate(object):
+ action_name = 'action'
+ def __init__(self, action):
+ self.action = action
+ try:
+ self.action_re = re.compile(action + '$')
+ except (re.error, TypeError), why:
+ raise ConfigurationError(why[0])
+
+ def __call__(self, context, request):
+ matchdict = getattr(request, 'matchdict', None)
+ if matchdict is None:
+ return False
+ action = matchdict.get(self.action_name)
+ if action is None:
+ return False
+ return bool(self.action_re.match(action))
+
+ def __hash__(self):
+ # allow this predicate's phash to be compared as equal to
+ # others that share the same action name
+ return hash(self.action)
+
diff --git a/pyramid/tests/test_configuration.py b/pyramid/tests/test_configuration.py
index 48fd778af..9ff750662 100644
--- a/pyramid/tests/test_configuration.py
+++ b/pyramid/tests/test_configuration.py
@@ -1770,6 +1770,253 @@ class ConfiguratorTests(unittest.TestCase):
request = self._makeRequest(config)
self.assertEqual(view(None, request), 'OK')
+ def test_add_handler_action_in_route_pattern(self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_handler('name', '/{action}', DummyHandler)
+ self._assertRoute(config, 'name', '/:action', 0)
+ self.assertEqual(len(views), 2)
+
+ view = views[0]
+ preds = view['custom_predicates']
+ self.assertEqual(len(preds), 1)
+ pred = preds[0]
+ request = DummyRequest()
+ self.assertEqual(pred(None, request), False)
+ request.matchdict = {'action':'action1'}
+ self.assertEqual(pred(None, request), True)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['attr'], 'action1')
+ self.assertEqual(view['view'], DummyHandler)
+
+ view = views[1]
+ preds = view['custom_predicates']
+ self.assertEqual(len(preds), 1)
+ pred = preds[0]
+ request = DummyRequest()
+ self.assertEqual(pred(None, request), False)
+ request.matchdict = {'action':'action2'}
+ self.assertEqual(pred(None, request), True)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['attr'], 'action2')
+ self.assertEqual(view['view'], DummyHandler)
+
+ def test_add_handler_with_view_overridden_autoexpose_None(self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(DummyHandler):
+ __autoexpose__ = None
+ config.add_handler('name', '/{action}', MyView)
+ self._assertRoute(config, 'name', '/:action', 0)
+ self.assertEqual(len(views), 0)
+
+ def test_add_handler_with_view_overridden_autoexpose_broken_regex1(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(DummyHandler):
+ __autoexpose__ = 1
+ self.assertRaises(ConfigurationError, config.add_handler,
+ 'name', '/{action}', MyView)
+
+ def test_add_handler_with_view_overridden_autoexpose_broken_regex2(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(DummyHandler):
+ __autoexpose__ = 'a\\'
+ self.assertRaises(ConfigurationError, config.add_handler,
+ 'name', '/{action}', MyView)
+
+ def test_add_handler_with_view_method_has_expose_config(self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(object):
+ def action(self):
+ return 'response'
+ action.__exposed__ = [{'custom_predicates':(1,)}]
+ config.add_handler('name', '/{action}', MyView)
+ self._assertRoute(config, 'name', '/:action', 0)
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ preds = view['custom_predicates']
+ self.assertEqual(len(preds), 2)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['attr'], 'action')
+ self.assertEqual(view['view'], MyView)
+
+ def test_add_handler_with_view_method_has_expose_config_with_action(self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(object):
+ def action(self):
+ return 'response'
+ action.__exposed__ = [{'name':'action3000'}]
+ config.add_handler('name', '/{action}', MyView)
+ self._assertRoute(config, 'name', '/:action', 0)
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ preds = view['custom_predicates']
+ self.assertEqual(len(preds), 1)
+ pred = preds[0]
+ request = DummyRequest()
+ self.assertEqual(pred(None, request), False)
+ request.matchdict = {'action':'action3000'}
+ self.assertEqual(pred(None, request), True)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['attr'], 'action')
+ self.assertEqual(view['view'], MyView)
+
+ def test_add_handler_with_view_method_has_expose_config_with_action_regex(
+ self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyView(object):
+ def action(self):
+ return 'response'
+ action.__exposed__ = [{'name':'^action3000$'}]
+ config.add_handler('name', '/{action}', MyView)
+ self._assertRoute(config, 'name', '/:action', 0)
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ preds = view['custom_predicates']
+ self.assertEqual(len(preds), 1)
+ pred = preds[0]
+ request = DummyRequest()
+ self.assertEqual(pred(None, request), False)
+ request.matchdict = {'action':'action3000'}
+ self.assertEqual(pred(None, request), True)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['attr'], 'action')
+ self.assertEqual(view['view'], MyView)
+
+ def test_add_handler_doesnt_mutate_expose_dict(self):
+ config = self._makeOne()
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ exposed = [{'name':'^action3000$'}]
+ class MyView(object):
+ def action(self):
+ return 'response'
+ action.__exposed__ = exposed
+ config.add_handler('name', '/{action}', MyView)
+ self.assertEqual(exposed[0], {'name':'^action3000$'}) # not mutated
+
+ def test_add_handler_with_action_and_action_in_path(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError, config.add_handler,
+ 'name', '/{action}', DummyHandler, action='abc')
+
+ def test_add_handler_with_explicit_action(self):
+ config = self._makeOne()
+ class DummyHandler(object):
+ def index(self): pass
+ index.__exposed__ = [{'a':'1'}]
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_handler('name', '/abc', DummyHandler, action='index')
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ self.assertEqual(view['a'], '1')
+ self.assertEqual(view['attr'], 'index')
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['view'], DummyHandler)
+
+ def test_add_handler_with_implicit_action(self):
+ config = self._makeOne()
+ class DummyHandler(object):
+ def __call__(self): pass
+ __call__.__exposed__ = [{'a':'1'}]
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_handler('name', '/abc', DummyHandler)
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ self.assertEqual(view['a'], '1')
+ self.assertEqual(view['attr'], None)
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['view'], DummyHandler)
+
+ def test_add_handler_with_multiple_action(self):
+ config = self._makeOne()
+ class DummyHandler(object):
+ def index(self): pass
+ def create(self): pass
+ create.__exposed__ = [{'name': 'index'}]
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_handler('name', '/abc', DummyHandler, action='index')
+ self.assertEqual(len(views), 2)
+ view = views[0]
+ self.assertEqual(view['attr'], 'create')
+ self.assertEqual(view['route_name'], 'name')
+ self.assertEqual(view['view'], DummyHandler)
+ view = views[1]
+ self.assertEqual(view['attr'], 'index')
+
+ def test_add_handler_string(self):
+ import pyramid
+ views = []
+ config = self._makeOne()
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_handler('name', '/abc', 'pyramid')
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ self.assertEqual(view['view'], pyramid)
+
+ def test_add_handler_pattern_None_no_previous_route(self):
+ from pyramid.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError, config.add_handler,
+ 'name', None, 'pylons')
+
+ def test_add_handler_pattern_None_with_previous_route(self):
+ import pyramid
+ config = self._makeOne()
+ config.add_route('name', ':def')
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ config.add_route = None # shouldn't be called
+ config.add_handler('name', None, 'pyramid')
+ self.assertEqual(len(views), 1)
+ view = views[0]
+ self.assertEqual(view['view'], pyramid)
+
+
def _assertRoute(self, config, name, path, num_predicates=0):
from pyramid.interfaces import IRoutesMapper
mapper = config.registry.getUtility(IRoutesMapper)
@@ -2031,6 +2278,11 @@ class ConfiguratorTests(unittest.TestCase):
route = config.add_route('name', 'pattern', pregenerator='123')
self.assertEqual(route.pregenerator, '123')
+ def test_add_route_squiggly_syntax(self):
+ config = self._makeOne()
+ config.add_route('name', '/abc/{def}/:ghi/jkl/{mno}{/:p')
+ self._assertRoute(config, 'name', '/abc/:def/:ghi/jkl/:mno{/:p', 0)
+
def test__override_not_yet_registered(self):
from pyramid.interfaces import IPackageOverrides
package = DummyPackage('package')
@@ -4336,3 +4588,14 @@ def dummy_view(request):
def dummyfactory(request):
""" """
+
+class DummyHandler(object):
+ def __init__(self, request):
+ self.request = request
+
+ def action1(self):
+ return 'response 1'
+
+ def action2(self):
+ return 'response 2'
+