Michael Merickel
2018-10-19 d579f2104de139e0b0fc5d6c81aabb2f826e5e54
commit | author | age
efd61e 1 import contextlib
b01f1d 2 import warnings
CM 3
84367e 4 from pyramid.compat import urlparse
ee117e 5 from pyramid.interfaces import (
CM 6     IRequest,
7     IRouteRequest,
8     IRoutesMapper,
9     PHASE2_CONFIG,
0c29cf 10 )
5bf23f 11
CM 12 from pyramid.exceptions import ConfigurationError
d579f2 13 import pyramid.predicates
5bf23f 14 from pyramid.request import route_request_iface
CM 15 from pyramid.urldispatch import RoutesMapper
16
0c29cf 17 from pyramid.util import as_sorted_tuple, is_nonstr_iter
9c8ec5 18
d579f2 19 from pyramid.config.actions import action_method
MM 20 from pyramid.config.predicates import normalize_accept_offer, predvalseq
52fde9 21
0c29cf 22
5bf23f 23 class RoutesConfiguratorMixin(object):
CM 24     @action_method
0c29cf 25     def add_route(
MM 26         self,
27         name,
28         pattern=None,
29         factory=None,
30         for_=None,
31         header=None,
32         xhr=None,
33         accept=None,
34         path_info=None,
35         request_method=None,
36         request_param=None,
37         traverse=None,
38         custom_predicates=(),
39         use_global_views=False,
40         path=None,
41         pregenerator=None,
42         static=False,
43         **predicates
44     ):
5bf23f 45         """ Add a :term:`route configuration` to the current
CM 46         configuration state, as well as possibly a :term:`view
47         configuration` to be used to specify a :term:`view callable`
48         that will be invoked when this route matches.  The arguments
49         to this method are divided into *predicate*, *non-predicate*,
50         and *view-related* types.  :term:`Route predicate` arguments
51         narrow the circumstances in which a route will be match a
52         request; non-predicate arguments are informational.
53
54         Non-Predicate Arguments
55
56         name
57
58           The name of the route, e.g. ``myroute``.  This attribute is
59           required.  It must be unique among all defined routes in a given
60           application.
61
62         factory
63
64           A Python object (often a function or a class) or a :term:`dotted
65           Python name` which refers to the same object that will generate a
66           :app:`Pyramid` root resource object when this route matches. For
67           example, ``mypackage.resources.MyFactory``.  If this argument is
0507ac 68           not specified, a default root factory will be used.  See
CM 69           :ref:`the_resource_tree` for more information about root factories.
5bf23f 70
CM 71         traverse
72
73           If you would like to cause the :term:`context` to be
74           something other than the :term:`root` object when this route
75           matches, you can spell a traversal pattern as the
76           ``traverse`` argument.  This traversal pattern will be used
77           as the traversal path: traversal will begin at the root
78           object implied by this route (either the global root, or the
79           object returned by the ``factory`` associated with this
80           route).
81
82           The syntax of the ``traverse`` argument is the same as it is
83           for ``pattern``. For example, if the ``pattern`` provided to
84           ``add_route`` is ``articles/{article}/edit``, and the
85           ``traverse`` argument provided to ``add_route`` is
86           ``/{article}``, when a request comes in that causes the route
87           to match in such a way that the ``article`` match value is
f1f49b 88           ``'1'`` (when the request URI is ``/articles/1/edit``), the
5bf23f 89           traversal path will be generated as ``/1``.  This means that
CM 90           the root object's ``__getitem__`` will be called with the
f1f49b 91           name ``'1'`` during the traversal phase.  If the ``'1'`` object
5bf23f 92           exists, it will become the :term:`context` of the request.
CM 93           :ref:`traversal_chapter` has more information about
94           traversal.
95
96           If the traversal path contains segment marker names which
97           are not present in the ``pattern`` argument, a runtime error
98           will occur.  The ``traverse`` pattern should not contain
99           segment markers that do not exist in the ``pattern``
100           argument.
101
102           A similar combining of routing and traversal is available
103           when a route is matched which contains a ``*traverse``
104           remainder marker in its pattern (see
105           :ref:`using_traverse_in_a_route_pattern`).  The ``traverse``
106           argument to add_route allows you to associate route patterns
043ccd 107           with an arbitrary traversal path without using a
5bf23f 108           ``*traverse`` remainder marker; instead you can use other
CM 109           match information.
110
111           Note that the ``traverse`` argument to ``add_route`` is
112           ignored when attached to a route that has a ``*traverse``
113           remainder marker in its pattern.
114
115         pregenerator
116
117            This option should be a callable object that implements the
118            :class:`pyramid.interfaces.IRoutePregenerator` interface.  A
119            :term:`pregenerator` is a callable called by the
120            :meth:`pyramid.request.Request.route_url` function to augment or
121            replace the arguments it is passed when generating a URL for the
122            route.  This is a feature not often used directly by applications,
123            it is meant to be hooked by frameworks that use :app:`Pyramid` as
124            a base.
125
126         use_global_views
127
128           When a request matches this route, and view lookup cannot
129           find a view which has a ``route_name`` predicate argument
130           that matches the route, try to fall back to using a view
131           that otherwise matches the context, request, and view name
132           (but which does not match the route_name predicate).
133
134         static
135
136           If ``static`` is ``True``, this route will never match an incoming
137           request; it will only be useful for URL generation.  By default,
138           ``static`` is ``False``.  See :ref:`static_route_narr`.
139
0b23b3 140           .. versionadded:: 1.1
5bf23f 141
CM 142         Predicate Arguments
143
144         pattern
145
146           The pattern of the route e.g. ``ideas/{idea}``.  This
147           argument is required.  See :ref:`route_pattern_syntax`
148           for information about the syntax of route patterns.  If the
149           pattern doesn't match the current URL, route matching
150           continues.
151
012b97 152           .. note::
M 153
154              For backwards compatibility purposes (as of :app:`Pyramid` 1.0), a
155              ``path`` keyword argument passed to this function will be used to
156              represent the pattern value if the ``pattern`` argument is
a54bc1 157              ``None``.  If both ``path`` and ``pattern`` are passed,
MM 158              ``pattern`` wins.
012b97 159
5bf23f 160         xhr
CM 161
162           This value should be either ``True`` or ``False``.  If this
163           value is specified and is ``True``, the :term:`request` must
164           possess an ``HTTP_X_REQUESTED_WITH`` (aka
165           ``X-Requested-With``) header for this route to match.  This
166           is useful for detecting AJAX requests issued from jQuery,
167           Prototype and other Javascript libraries.  If this predicate
168           returns ``False``, route matching continues.
169
170         request_method
171
49f082 172           A string representing an HTTP method name, e.g. ``GET``, ``POST``,
CM 173           ``HEAD``, ``DELETE``, ``PUT`` or a tuple of elements containing
174           HTTP method names.  If this argument is not specified, this route
175           will match if the request has *any* request method.  If this
176           predicate returns ``False``, route matching continues.
177
0b23b3 178           .. versionchanged:: 1.2
TL 179              The ability to pass a tuple of items as ``request_method``.
180              Previous versions allowed only a string.
5bf23f 181
CM 182         path_info
183
184           This value represents a regular expression pattern that will
185           be tested against the ``PATH_INFO`` WSGI environment
186           variable.  If the regex matches, this predicate will return
187           ``True``.  If this predicate returns ``False``, route
188           matching continues.
189
190         request_param
191
939165 192           This value can be any string or an iterable of strings.  A view
BG 193           declaration with this argument ensures that the associated route will
194           only match when the request has a key in the ``request.params``
5bf23f 195           dictionary (an HTTP ``GET`` or ``POST`` variable) that has a
CM 196           name which matches the supplied value.  If the value
197           supplied as the argument has a ``=`` sign in it,
198           e.g. ``request_param="foo=123"``, then the key
199           (``foo``) must both exist in the ``request.params`` dictionary, and
200           the value must match the right hand side of the expression (``123``)
201           for the route to "match" the current request.  If this predicate
202           returns ``False``, route matching continues.
203
204         header
205
206           This argument represents an HTTP header name or a header
207           name/value pair.  If the argument contains a ``:`` (colon),
208           it will be considered a name/value pair
209           (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``).  If
210           the value contains a colon, the value portion should be a
211           regular expression.  If the value does not contain a colon,
212           the entire value will be considered to be the header name
213           (e.g. ``If-Modified-Since``).  If the value evaluates to a
214           header name only without a value, the header specified by
215           the name must be present in the request for this predicate
216           to be true.  If the value evaluates to a header name/value
217           pair, the header specified by the name must be present in
218           the request *and* the regular expression specified as the
219           value must match the header value.  Whether or not the value
220           represents a header name or a header name/value pair, the
221           case of the header name is not significant.  If this
222           predicate returns ``False``, route matching continues.
223
121f45 224         accept
MM 225
30f79d 226           A :term:`media type` that will be matched against the ``Accept``
4cc3a6 227           HTTP request header.  If this value is specified, it may be a
MM 228           specific media type such as ``text/html``, or a list of the same.
229           If the media type is acceptable by the ``Accept`` header of the
230           request, or if the ``Accept`` header isn't set at all in the request,
231           this predicate will match. If this does not match the ``Accept``
232           header of the request, route matching continues.
121f45 233
MM 234           If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is
235           not taken into consideration when deciding whether or not to select
236           the route.
4a9f4f 237
4cc3a6 238           Unlike the ``accept`` argument to
MM 239           :meth:`pyramid.config.Configurator.add_view`, this value is
240           strictly a predicate and supports :func:`pyramid.config.not_`.
241
4a9f4f 242           .. versionchanged:: 1.10
MM 243
244               Specifying a media range is deprecated due to changes in WebOb
245               and ambiguities that occur when trying to match ranges against
246               ranges in the ``Accept`` header. Support will be removed in
247               :app:`Pyramid` 2.0. Use a list of specific media types to match
248               more than one type.
121f45 249
c7337b 250         effective_principals
CM 251
252           If specified, this value should be a :term:`principal` identifier or
253           a sequence of principal identifiers.  If the
0184b5 254           :attr:`pyramid.request.Request.effective_principals` property
CM 255           indicates that every principal named in the argument list is present
256           in the current request, this predicate will return True; otherwise it
257           will return False.  For example:
c7337b 258           ``effective_principals=pyramid.security.Authenticated`` or
CM 259           ``effective_principals=('fred', 'group:admins')``.
260
261           .. versionadded:: 1.4a4
262
5bf23f 263         custom_predicates
CM 264
e96f1b 265           .. deprecated:: 1.5
2033ee 266               This value should be a sequence of references to custom
SP 267               predicate callables.  Use custom predicates when no set of
268               predefined predicates does what you need.  Custom predicates
269               can be combined with predefined predicates as necessary.
270               Each custom predicate callable should accept two arguments:
271               ``info`` and ``request`` and should return either ``True``
272               or ``False`` after doing arbitrary evaluation of the info
273               and/or the request.  If all custom and non-custom predicate
274               callables return ``True`` the associated route will be
275               considered viable for a given request.  If any predicate
276               callable returns ``False``, route matching continues.  Note
277               that the value ``info`` passed to a custom route predicate
278               is a dictionary containing matching information; see
279               :ref:`custom_route_predicates` for more information about
280               ``info``.
5bf23f 281
8ec8e2 282         predicates
9c8ec5 283
5664c4 284           Pass a key/value pair here to use a third-party predicate
CM 285           registered via
cfd8cc 286           :meth:`pyramid.config.Configurator.add_route_predicate`.  More than
5664c4 287           one key/value pair can be used at the same time.  See
95f766 288           :ref:`view_and_route_predicates` for more information about
0b23b3 289           third-party predicates.
TL 290
291           .. versionadded:: 1.4
292
5bf23f 293         """
b01f1d 294         if custom_predicates:
CM 295             warnings.warn(
0c29cf 296                 (
a54bc1 297                     'The "custom_predicates" argument to '
MM 298                     'Configurator.add_route is deprecated as of Pyramid 1.5. '
299                     'Use "config.add_route_predicate" and use the registered '
0c29cf 300                     'route predicate as a predicate argument to add_route '
MM 301                     'instead. See "Adding A Third Party View, Route, or '
302                     'Subscriber Predicate" in the "Hooks" chapter of the '
303                     'documentation for more information.'
304                 ),
c151ad 305                 DeprecationWarning,
0c29cf 306                 stacklevel=3,
MM 307             )
121f45 308
MM 309         if accept is not None:
c3c83e 310             if not is_nonstr_iter(accept):
4a9f4f 311                 if '*' in accept:
MM 312                     warnings.warn(
0c29cf 313                         (
a54bc1 314                             'Passing a media range to the "accept" argument '
MM 315                             'of Configurator.add_route is deprecated as of '
316                             'Pyramid 1.10. Use a list of explicit media types.'
0c29cf 317                         ),
4a9f4f 318                         DeprecationWarning,
MM 319                         stacklevel=3,
0c29cf 320                     )
19eef8 321                 # XXX switch this to False when range support is dropped
MM 322                 accept = [normalize_accept_offer(accept, allow_range=True)]
30f79d 323
4a9f4f 324             else:
MM 325                 accept = [
326                     normalize_accept_offer(accept_option)
327                     for accept_option in accept
328                 ]
121f45 329
5bf23f 330         # these are route predicates; if they do not match, the next route
CM 331         # in the routelist will be tried
d8e504 332         if request_method is not None:
CM 333             request_method = as_sorted_tuple(request_method)
334
5bf23f 335         factory = self.maybe_dotted(factory)
CM 336         if pattern is None:
337             pattern = path
338         if pattern is None:
339             raise ConfigurationError('"pattern" argument may not be None')
340
d07d16 341         # check for an external route; an external route is one which is
CM 342         # is a full url (e.g. 'http://example.com/{id}')
84367e 343         parsed = urlparse.urlparse(pattern)
582c2e 344         external_url = pattern
JA 345
84367e 346         if parsed.hostname:
MM 347             pattern = parsed.path
348
349             original_pregenerator = pregenerator
0c29cf 350
84367e 351             def external_url_pregenerator(request, elements, kw):
d07d16 352                 if '_app_url' in kw:
CM 353                     raise ValueError(
354                         'You cannot generate a path to an external route '
355                         'pattern via request.route_path nor pass an _app_url '
356                         'to request.route_url when generating a URL for an '
0c29cf 357                         'external route pattern (pattern was "%s") '
MM 358                         % (pattern,)
359                     )
d07d16 360                 if '_scheme' in kw:
CM 361                     scheme = kw['_scheme']
362                 elif parsed.scheme:
363                     scheme = parsed.scheme
364                 else:
365                     scheme = request.scheme
366                 kw['_app_url'] = '{0}://{1}'.format(scheme, parsed.netloc)
84367e 367
MM 368                 if original_pregenerator:
0c29cf 369                     elements, kw = original_pregenerator(request, elements, kw)
84367e 370                 return elements, kw
MM 371
372             pregenerator = external_url_pregenerator
373             static = True
374
375         elif self.route_prefix:
5bf23f 376             pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/')
33b638 377
eb2fee 378         mapper = self.get_routes_mapper()
5bf23f 379
522405 380         introspectables = []
CM 381
0c29cf 382         intr = self.introspectable(
MM 383             'routes', name, '%s (pattern: %r)' % (name, pattern), 'route'
384         )
3b5ccb 385         intr['name'] = name
CM 386         intr['pattern'] = pattern
387         intr['factory'] = factory
87f8d2 388         intr['xhr'] = xhr
d8e504 389         intr['request_methods'] = request_method
87f8d2 390         intr['path_info'] = path_info
CM 391         intr['request_param'] = request_param
392         intr['header'] = header
393         intr['accept'] = accept
394         intr['traverse'] = traverse
395         intr['custom_predicates'] = custom_predicates
3b5ccb 396         intr['pregenerator'] = pregenerator
CM 397         intr['static'] = static
398         intr['use_global_views'] = use_global_views
582c2e 399
JA 400         if static is True:
401             intr['external_url'] = external_url
402
522405 403         introspectables.append(intr)
CM 404
405         if factory:
0c29cf 406             factory_intr = self.introspectable(
MM 407                 'root factories',
408                 name,
409                 self.object_description(factory),
410                 'root factory',
411             )
522405 412             factory_intr['factory'] = factory
CM 413             factory_intr['route_name'] = name
414             factory_intr.relate('routes', name)
415             introspectables.append(factory_intr)
3b5ccb 416
de79bc 417         def register_route_request_iface():
0c29cf 418             request_iface = self.registry.queryUtility(
MM 419                 IRouteRequest, name=name
420             )
b9f2f5 421             if request_iface is None:
MM 422                 if use_global_views:
423                     bases = (IRequest,)
424                 else:
425                     bases = ()
426                 request_iface = route_request_iface(name, bases)
427                 self.registry.registerUtility(
0c29cf 428                     request_iface, IRouteRequest, name=name
MM 429                 )
b9f2f5 430
de79bc 431         def register_connect():
8ec8e2 432             pvals = predicates.copy()
9c8ec5 433             pvals.update(
CM 434                 dict(
435                     xhr=xhr,
436                     request_method=request_method,
437                     path_info=path_info,
438                     request_param=request_param,
439                     header=header,
440                     accept=accept,
441                     traverse=traverse,
442                     custom=predvalseq(custom_predicates),
443                 )
0c29cf 444             )
9c8ec5 445
405213 446             predlist = self.get_predlist('route')
9c8ec5 447             _, preds, _ = predlist.make(self, **pvals)
3b5ccb 448             route = mapper.connect(
0c29cf 449                 name,
MM 450                 pattern,
451                 factory,
452                 predicates=preds,
453                 pregenerator=pregenerator,
454                 static=static,
455             )
3b5ccb 456             intr['object'] = route
CM 457             return route
eb2fee 458
de79bc 459         # We have to connect routes in the order they were provided;
CM 460         # we can't use a phase to do that, because when the actions are
461         # sorted, actions in the same phase lose relative ordering
19a575 462         self.action(('route-connect', name), register_connect)
de79bc 463
CM 464         # But IRouteRequest interfaces must be registered before we begin to
465         # process view registrations (in phase 3)
0c29cf 466         self.action(
MM 467             ('route', name),
468             register_route_request_iface,
469             order=PHASE2_CONFIG,
470             introspectables=introspectables,
471         )
381de3 472
9c8ec5 473     @action_method
0c29cf 474     def add_route_predicate(
MM 475         self, name, factory, weighs_more_than=None, weighs_less_than=None
476     ):
9c8ec5 477         """ Adds a route predicate factory.  The view predicate can later be
CM 478         named as a keyword argument to
479         :meth:`pyramid.config.Configurator.add_route`.
480
481         ``name`` should be the name of the predicate.  It must be a valid
482         Python identifier (it will be used as a keyword argument to
cfd8cc 483         ``add_route``).
9c8ec5 484
d71aca 485         ``factory`` should be a :term:`predicate factory` or :term:`dotted
BJR 486         Python name` which refers to a predicate factory.
5664c4 487
95f766 488         See :ref:`view_and_route_predicates` for more information.
5664c4 489
0b23b3 490         .. versionadded:: 1.4
9c8ec5 491         """
95f766 492         self._add_predicate(
CM 493             'route',
494             name,
495             factory,
496             weighs_more_than=weighs_more_than,
0c29cf 497             weighs_less_than=weighs_less_than,
MM 498         )
9c8ec5 499
CM 500     def add_default_route_predicates(self):
c7974f 501         p = pyramid.predicates
9c8ec5 502         for (name, factory) in (
8ec8e2 503             ('xhr', p.XHRPredicate),
CM 504             ('request_method', p.RequestMethodPredicate),
505             ('path_info', p.PathInfoPredicate),
506             ('request_param', p.RequestParamPredicate),
507             ('header', p.HeaderPredicate),
508             ('accept', p.AcceptPredicate),
c7337b 509             ('effective_principals', p.EffectivePrincipalsPredicate),
8ec8e2 510             ('custom', p.CustomPredicate),
CM 511             ('traverse', p.TraversePredicate),
0c29cf 512         ):
9c8ec5 513             self.add_route_predicate(name, factory)
503bbb 514
5bf23f 515     def get_routes_mapper(self):
CM 516         """ Return the :term:`routes mapper` object associated with
517         this configurator's :term:`registry`."""
518         mapper = self.registry.queryUtility(IRoutesMapper)
519         if mapper is None:
520             mapper = RoutesMapper()
521             self.registry.registerUtility(mapper, IRoutesMapper)
522         return mapper
523
efd61e 524     @contextlib.contextmanager
HS 525     def route_prefix_context(self, route_prefix):
526         """ Return this configurator with the
527         :attr:`pyramid.config.Configurator.route_prefix` attribute mutated to
528         include the new ``route_prefix``.
529
530         When the context exits, the ``route_prefix`` is reset to the original.
531
66a767 532         ``route_prefix`` is a string suitable to be used as a route prefix,
MM 533         or ``None``.
534
efd61e 535         Example Usage:
HS 536
e14661 537         .. code-block:: python
efd61e 538
e14661 539             config = Configurator()
MM 540             with config.route_prefix_context('foo'):
541                 config.add_route('bar', '/bar')
efd61e 542
f6aee3 543         .. versionadded:: 1.10
efd61e 544
e14661 545         """
efd61e 546         original_route_prefix = self.route_prefix
HS 547
548         if route_prefix is None:
549             route_prefix = ''
550
551         old_route_prefix = self.route_prefix
552         if old_route_prefix is None:
553             old_route_prefix = ''
554
555         route_prefix = '{}/{}'.format(
0c29cf 556             old_route_prefix.rstrip('/'), route_prefix.lstrip('/')
efd61e 557         )
HS 558
559         route_prefix = route_prefix.strip('/')
560
561         if not route_prefix:
562             route_prefix = None
563
564         self.begin()
565         try:
566             self.route_prefix = route_prefix
567             yield
568
569         finally:
570             self.route_prefix = original_route_prefix
571             self.end()