Michael Merickel
2018-10-10 4a9f4f43684c3a754f43935b97013057340c305d
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``
MM 231           HTTP request header.  This value may be a specific media type such
4a9f4f 232           as ``text/html``, or a list of the same. If the media type is
MM 233           acceptable by the ``Accept`` header of the request, or if the
234           ``Accept`` header isn't set at all in the request, this predicate
235           will match. If this does not match the ``Accept`` header of the
236           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
MM 242           .. versionchanged:: 1.10
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(
296                 ('The "custom_predicates" argument to Configurator.add_route '
297                  'is deprecated as of Pyramid 1.5.  Use '
298                  '"config.add_route_predicate" and use the registered '
299                  'route predicate as a predicate argument to add_route '
300                  'instead. See "Adding A Third Party View, Route, or '
301                  'Subscriber Predicate" in the "Hooks" chapter of the '
302                  'documentation for more information.'),
c151ad 303                 DeprecationWarning,
b01f1d 304                 stacklevel=3
CM 305                 )
121f45 306
MM 307         if accept is not None:
c3c83e 308             if not is_nonstr_iter(accept):
4a9f4f 309                 if '*' in accept:
MM 310                     warnings.warn(
311                         ('Passing a media range to the "accept" argument of '
312                          'Configurator.add_route is deprecated as of Pyramid '
313                          '1.10. Use a list of explicit media types.'),
314                         DeprecationWarning,
315                         stacklevel=3,
316                         )
317                 # XXX switch this to verify=True when range support is dropped
318                 accept = [normalize_accept_offer(accept, verify=False)]
30f79d 319
4a9f4f 320             else:
MM 321                 accept = [
322                     normalize_accept_offer(accept_option)
323                     for accept_option in accept
324                 ]
121f45 325
5bf23f 326         # these are route predicates; if they do not match, the next route
CM 327         # in the routelist will be tried
d8e504 328         if request_method is not None:
CM 329             request_method = as_sorted_tuple(request_method)
330
5bf23f 331         factory = self.maybe_dotted(factory)
CM 332         if pattern is None:
333             pattern = path
334         if pattern is None:
335             raise ConfigurationError('"pattern" argument may not be None')
336
d07d16 337         # check for an external route; an external route is one which is
CM 338         # is a full url (e.g. 'http://example.com/{id}')
84367e 339         parsed = urlparse.urlparse(pattern)
582c2e 340         external_url = pattern
JA 341
84367e 342         if parsed.hostname:
MM 343             pattern = parsed.path
344
345             original_pregenerator = pregenerator
346             def external_url_pregenerator(request, elements, kw):
d07d16 347                 if '_app_url' in kw:
CM 348                     raise ValueError(
349                         'You cannot generate a path to an external route '
350                         'pattern via request.route_path nor pass an _app_url '
351                         'to request.route_url when generating a URL for an '
352                         'external route pattern (pattern was "%s") ' %
353                         (pattern,)
354                         )
355                 if '_scheme' in kw:
356                     scheme = kw['_scheme']
357                 elif parsed.scheme:
358                     scheme = parsed.scheme
359                 else:
360                     scheme = request.scheme
361                 kw['_app_url'] = '{0}://{1}'.format(scheme, parsed.netloc)
84367e 362
MM 363                 if original_pregenerator:
364                     elements, kw = original_pregenerator(
365                         request, elements, kw)
366                 return elements, kw
367
368             pregenerator = external_url_pregenerator
369             static = True
370
371         elif self.route_prefix:
5bf23f 372             pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/')
33b638 373
eb2fee 374         mapper = self.get_routes_mapper()
5bf23f 375
522405 376         introspectables = []
CM 377
378         intr = self.introspectable('routes',
379                                    name,
87f8d2 380                                    '%s (pattern: %r)' % (name, pattern),
CM 381                                    'route')
3b5ccb 382         intr['name'] = name
CM 383         intr['pattern'] = pattern
384         intr['factory'] = factory
87f8d2 385         intr['xhr'] = xhr
d8e504 386         intr['request_methods'] = request_method
87f8d2 387         intr['path_info'] = path_info
CM 388         intr['request_param'] = request_param
389         intr['header'] = header
390         intr['accept'] = accept
391         intr['traverse'] = traverse
392         intr['custom_predicates'] = custom_predicates
3b5ccb 393         intr['pregenerator'] = pregenerator
CM 394         intr['static'] = static
395         intr['use_global_views'] = use_global_views
582c2e 396
JA 397         if static is True:
398             intr['external_url'] = external_url
399
522405 400         introspectables.append(intr)
CM 401
402         if factory:
403             factory_intr = self.introspectable('root factories',
404                                                name,
405                                                self.object_description(factory),
406                                                'root factory')
407             factory_intr['factory'] = factory
408             factory_intr['route_name'] = name
409             factory_intr.relate('routes', name)
410             introspectables.append(factory_intr)
3b5ccb 411
de79bc 412         def register_route_request_iface():
b9f2f5 413             request_iface = self.registry.queryUtility(IRouteRequest, name=name)
MM 414             if request_iface is None:
415                 if use_global_views:
416                     bases = (IRequest,)
417                 else:
418                     bases = ()
419                 request_iface = route_request_iface(name, bases)
420                 self.registry.registerUtility(
421                     request_iface, IRouteRequest, name=name)
422
de79bc 423         def register_connect():
8ec8e2 424             pvals = predicates.copy()
9c8ec5 425             pvals.update(
CM 426                 dict(
427                     xhr=xhr,
428                     request_method=request_method,
429                     path_info=path_info,
430                     request_param=request_param,
431                     header=header,
432                     accept=accept,
433                     traverse=traverse,
434                     custom=predvalseq(custom_predicates),
435                     )
436                 )
437
405213 438             predlist = self.get_predlist('route')
9c8ec5 439             _, preds, _ = predlist.make(self, **pvals)
3b5ccb 440             route = mapper.connect(
9c8ec5 441                 name, pattern, factory, predicates=preds,
3b5ccb 442                 pregenerator=pregenerator, static=static
CM 443                 )
444             intr['object'] = route
445             return route
eb2fee 446
de79bc 447         # We have to connect routes in the order they were provided;
CM 448         # we can't use a phase to do that, because when the actions are
449         # sorted, actions in the same phase lose relative ordering
19a575 450         self.action(('route-connect', name), register_connect)
de79bc 451
CM 452         # But IRouteRequest interfaces must be registered before we begin to
453         # process view registrations (in phase 3)
454         self.action(('route', name), register_route_request_iface,
522405 455                     order=PHASE2_CONFIG, introspectables=introspectables)
381de3 456
9c8ec5 457     @action_method
CM 458     def add_route_predicate(self, name, factory, weighs_more_than=None,
cfd8cc 459                             weighs_less_than=None):
9c8ec5 460         """ Adds a route predicate factory.  The view predicate can later be
CM 461         named as a keyword argument to
462         :meth:`pyramid.config.Configurator.add_route`.
463
464         ``name`` should be the name of the predicate.  It must be a valid
465         Python identifier (it will be used as a keyword argument to
cfd8cc 466         ``add_route``).
9c8ec5 467
d71aca 468         ``factory`` should be a :term:`predicate factory` or :term:`dotted
BJR 469         Python name` which refers to a predicate factory.
5664c4 470
95f766 471         See :ref:`view_and_route_predicates` for more information.
5664c4 472
0b23b3 473         .. versionadded:: 1.4
9c8ec5 474         """
95f766 475         self._add_predicate(
CM 476             'route',
477             name,
478             factory,
479             weighs_more_than=weighs_more_than,
480             weighs_less_than=weighs_less_than
481             )
9c8ec5 482
CM 483     def add_default_route_predicates(self):
c7974f 484         p = pyramid.predicates
9c8ec5 485         for (name, factory) in (
8ec8e2 486             ('xhr', p.XHRPredicate),
CM 487             ('request_method', p.RequestMethodPredicate),
488             ('path_info', p.PathInfoPredicate),
489             ('request_param', p.RequestParamPredicate),
490             ('header', p.HeaderPredicate),
491             ('accept', p.AcceptPredicate),
c7337b 492             ('effective_principals', p.EffectivePrincipalsPredicate),
8ec8e2 493             ('custom', p.CustomPredicate),
CM 494             ('traverse', p.TraversePredicate),
9c8ec5 495             ):
CM 496             self.add_route_predicate(name, factory)
503bbb 497
5bf23f 498     def get_routes_mapper(self):
CM 499         """ Return the :term:`routes mapper` object associated with
500         this configurator's :term:`registry`."""
501         mapper = self.registry.queryUtility(IRoutesMapper)
502         if mapper is None:
503             mapper = RoutesMapper()
504             self.registry.registerUtility(mapper, IRoutesMapper)
505         return mapper
506
efd61e 507     @contextlib.contextmanager
HS 508     def route_prefix_context(self, route_prefix):
509         """ Return this configurator with the
510         :attr:`pyramid.config.Configurator.route_prefix` attribute mutated to
511         include the new ``route_prefix``.
512
513         When the context exits, the ``route_prefix`` is reset to the original.
514
515         Example Usage:
516
517         >>> config = Configurator()
518         >>> with config.route_prefix_context('foo'):
519         ...     config.add_route('bar', '/bar')
520
521         Arguments
522
523         route_prefix
524
525           A string suitable to be used as a route prefix, or ``None``.
526
f6aee3 527         .. versionadded:: 1.10
efd61e 528         """
HS 529
530         original_route_prefix = self.route_prefix
531
532         if route_prefix is None:
533             route_prefix = ''
534
535         old_route_prefix = self.route_prefix
536         if old_route_prefix is None:
537             old_route_prefix = ''
538
539         route_prefix = '{}/{}'.format(
540             old_route_prefix.rstrip('/'),
541             route_prefix.lstrip('/'),
542         )
543
544         route_prefix = route_prefix.strip('/')
545
546         if not route_prefix:
547             route_prefix = None
548
549         self.begin()
550         try:
551             self.route_prefix = route_prefix
552             yield
553
554         finally:
555             self.route_prefix = original_route_prefix
556             self.end()