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