diff options
| -rw-r--r-- | CHANGES.txt | 19 | ||||
| -rw-r--r-- | TODO.txt | 13 | ||||
| -rw-r--r-- | docs/api/configuration.rst | 2 | ||||
| -rw-r--r-- | pyramid/configuration.py | 156 | ||||
| -rw-r--r-- | pyramid/tests/test_configuration.py | 263 |
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). @@ -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' + |
