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