Chris McDonough
2011-07-18 a7c6f13c3499d8453e2248104032e843780bfffd
commit | author | age
c81aad 1 """ Utility functions for dealing with URLs in pyramid """
7ae0c2 2
750ce4 3 import os
7ae0c2 4
206188 5 from zope.deprecation import deprecated
CM 6
afc4bb 7 from repoze.lru import lru_cache
CM 8
b60bdb 9 from pyramid.interfaces import IContextURL
CM 10 from pyramid.interfaces import IRoutesMapper
11 from pyramid.interfaces import IStaticURLInfo
a97a70 12
b60bdb 13 from pyramid.encode import urlencode
CM 14 from pyramid.path import caller_package
15 from pyramid.threadlocal import get_current_registry
16 from pyramid.traversal import TraversalContextURL
17 from pyramid.traversal import quote_path_segment
7ae0c2 18
05c023 19 def route_url(route_name, request, *elements, **kw):
fd5ae9 20     """Generates a fully qualified URL for a named :app:`Pyramid`
8b1f6e 21     :term:`route configuration`.
820bd9 22
ffca4e 23     .. note:: Calling :meth:`pyramid.Request.route_url` can be used to
36c159 24               achieve the same result as :func:`pyramid.url.route_url`.
CM 25
60996a 26     Use the route's ``name`` as the first positional argument.  Use a
CM 27     request object as the second positional argument.  Additional
9a9af8 28     positional arguments are appended to the URL as path segments
CM 29     after it is generated.
820bd9 30
05c023 31     Use keyword arguments to supply values which match any dynamic
c6895b 32     path elements in the route definition.  Raises a :exc:`KeyError`
05c023 33     exception if the URL cannot be generated for any reason (not
CM 34     enough arguments, for example).
dc405b 35
5a11c0 36     For example, if you've defined a route named "foobar" with the path
c82eb9 37     ``{foo}/{bar}/*traverse``::
dc405b 38
05c023 39         route_url('foobar', request, foo='1')          => <KeyError exception>
CM 40         route_url('foobar', request, foo='1', bar='2') => <KeyError exception>
41         route_url('foobar', request, foo='1', bar='2',
820bd9 42                   traverse=('a','b'))                  => http://e.com/1/2/a/b
a8447a 43         route_url('foobar', request, foo='1', bar='2',
820bd9 44                   traverse='/a/b')                     => http://e.com/1/2/a/b
a8447a 45
CM 46     Values replacing ``:segment`` arguments can be passed as strings
47     or Unicode objects.  They will be encoded to UTF-8 and URL-quoted
48     before being placed into the generated URL.
49
50     Values replacing ``*remainder`` arguments can be passed as strings
51     *or* tuples of Unicode/string values.  If a tuple is passed as a
52     ``*remainder`` replacement value, its values are URL-quoted and
53     encoded to UTF-8.  The resulting strings are joined with slashes
54     and rendered into the URL.  If a string is passed as a
55     ``*remainder`` replacement value, it is tacked on to the URL
56     untouched.
dc405b 57
ae4039 58     If a keyword argument ``_query`` is present, it will be used to
05c023 59     compose a query string that will be tacked on to the end of the
c6895b 60     URL.  The value of ``_query`` must be a sequence of two-tuples
CM 61     *or* a data structure with an ``.items()`` method that returns a
05c023 62     sequence of two-tuples (presumably a dictionary).  This data
CM 63     structure will be turned into a query string per the documentation
c81aad 64     of :func:`pyramid.encode.urlencode` function.  After the query
c6895b 65     data is turned into a query string, a leading ``?`` is prepended,
c5f24b 66     and the resulting string is appended to the generated URL.
dc405b 67
05c023 68     .. note:: Python data structures that are passed as ``_query``
CM 69               which are sequences or dictionaries are turned into a
70               string under the same rules as when run through
c6895b 71               :func:`urllib.urlencode` with the ``doseq`` argument
CM 72               equal to ``True``.  This means that sequences can be
73               passed as values, and a k=v pair will be placed into the
74               query string for each value.
05c023 75
CM 76     If a keyword argument ``_anchor`` is present, its string
77     representation will be used as a named anchor in the generated URL
fb6a5c 78     (e.g. if ``_anchor`` is passed as ``foo`` and the route URL is
CM 79     ``http://example.com/route/url``, the resulting generated URL will
80     be ``http://example.com/route/url#foo``).
05c023 81
CM 82     .. note:: If ``_anchor`` is passed as a string, it should be UTF-8
7a65f9 83               encoded. If ``_anchor`` is passed as a Unicode object, it
05c023 84               will be converted to UTF-8 before being appended to the
CM 85               URL.  The anchor value is not quoted in any way before
86               being appended to the generated URL.
87
7a65f9 88     If both ``_anchor`` and ``_query`` are specified, the anchor
CM 89     element will always follow the query element,
05c023 90     e.g. ``http://example.com?foo=1#bar``.
CM 91
b29429 92     If a keyword ``_app_url`` is present, it will be used as the
CM 93     protocol/hostname/port/leading path prefix of the generated URL.
94     For example, using an ``_app_url`` of
95     ``http://example.com:8080/foo`` would cause the URL
96     ``http://example.com:8080/foo/fleeb/flub`` to be returned from
97     this function if the expansion of the route pattern associated
98     with the ``route_name`` expanded to ``/fleeb/flub``.  If
99     ``_app_url`` is not specified, the result of
100     ``request.application_url`` will be used as the prefix (the
101     default).
6bc662 102
c6895b 103     This function raises a :exc:`KeyError` if the URL cannot be
CM 104     generated due to missing replacement names.  Extra replacement
105     names are ignored.
70f1cd 106
CM 107     If the route object which matches the ``route_name`` argument has
108     a :term:`pregenerator`, the ``*elements`` and ``**kw`` arguments
109     arguments passed to this function might be augmented or changed.
36c159 110
dc405b 111     """
6fec21 112     try:
CM 113         reg = request.registry
114     except AttributeError:
115         reg = get_current_registry() # b/c
116     mapper = reg.getUtility(IRoutesMapper)
70f1cd 117     route = mapper.get_route(route_name)
CM 118
119     if route is None:
120         raise KeyError('No such route named %s' % route_name)
121
66051f 122     if route.pregenerator is not None:
70f1cd 123         elements, kw = route.pregenerator(request, elements, kw)
05c023 124
CM 125     anchor = ''
126     qs = ''
b29429 127     app_url = None
05c023 128
CM 129     if '_query' in kw:
f0b74a 130         qs = '?' + urlencode(kw.pop('_query'), doseq=True)
05c023 131
CM 132     if '_anchor' in kw:
f0b74a 133         anchor = kw.pop('_anchor')
05c023 134         if isinstance(anchor, unicode):
CM 135             anchor = anchor.encode('utf-8')
136         anchor = '#' + anchor
137
b29429 138     if '_app_url' in kw:
CM 139         app_url = kw.pop('_app_url')
140
70f1cd 141     path = route.generate(kw) # raises KeyError if generate fails
f0b74a 142
05c023 143     if elements:
afc4bb 144         suffix = _join_elements(elements)
05c023 145         if not path.endswith('/'):
CM 146             suffix = '/' + suffix
147     else:
148         suffix = ''
149
b29429 150     if app_url is None:
CM 151         # we only defer lookup of application_url until here because
152         # it's somewhat expensive; we won't need to do it if we've
153         # been passed _app_url
154         app_url = request.application_url
155
156     return app_url + path + suffix + qs + anchor
dc405b 157
2c9d14 158 def route_path(route_name, request, *elements, **kw):
CM 159     """Generates a path (aka a 'relative URL', a URL minus the host, scheme,
160     and port) for a named :app:`Pyramid` :term:`route configuration`.
161
ffca4e 162     .. note:: Calling :meth:`pyramid.Request.route_path` can be used to
2c9d14 163               achieve the same result as :func:`pyramid.url.route_path`.
CM 164
165     This function accepts the same argument as :func:`pyramid.url.route_url`
166     and performs the same duty.  It just omits the host, port, and scheme
0a0edf 167     information in the return value; only the script_name, path,
CM 168     query parameters, and anchor data are present in the returned string.
2c9d14 169
CM 170     For example, if you've defined a route named 'foobar' with the path
df3f64 171     ``/{foo}/{bar}``, this call to ``route_path``::
2c9d14 172
CM 173         route_path('foobar', request, foo='1', bar='2')
174
175     Will return the string ``/1/2``.
176
177     .. note:: Calling ``route_path('route', request)`` is the same as calling
0a0edf 178        ``route_url('route', request, _app_url=request.script_name)``.
CM 179        ``route_path`` is, in fact, implemented in terms of ``route_url``
180        in just this way. As a result, any ``_app_url`` passed within the
181        ``**kw`` values to ``route_path`` will be ignored.
2c9d14 182     """
0a0edf 183     kw['_app_url'] = request.script_name
647815 184     return route_url(route_name, request, *elements, **kw)
2c9d14 185
fb6a5c 186 def resource_url(resource, request, *elements, **kw):
7ae0c2 187     """
fb6a5c 188     Generate a string representing the absolute URL of the :term:`resource`
8b1f6e 189     object based on the ``wsgi.url_scheme``, ``HTTP_HOST`` or
CM 190     ``SERVER_NAME`` in the ``request``, plus any ``SCRIPT_NAME``.  The
191     overall result of this function is always a UTF-8 encoded string
c6895b 192     (never Unicode).
36c159 193
ffca4e 194     .. note:: Calling :meth:`pyramid.Request.resource_url` can be used to
fb6a5c 195               achieve the same result as :func:`pyramid.url.resource_url`.
7ae0c2 196
c8d6ab 197     Examples::
CM 198
fb6a5c 199         resource_url(context, request) =>
c8d6ab 200
CM 201                                    http://example.com/
202
fb6a5c 203         resource_url(context, request, 'a.html') =>
c8d6ab 204
CM 205                                    http://example.com/a.html
206
fb6a5c 207         resource_url(context, request, 'a.html', query={'q':'1'}) =>
c8d6ab 208
CM 209                                    http://example.com/a.html?q=1
210
fb6a5c 211         resource_url(context, request, 'a.html', anchor='abc') =>
c8d6ab 212
8b1f6e 213                                    http://example.com/a.html#abc
c8d6ab 214
7ae0c2 215     Any positional arguments passed in as ``elements`` must be strings
fcbd7b 216     Unicode objects, or integer objects.  These will be joined by slashes and
CM 217     appended to the generated resource URL.  Each of the elements passed in
218     is URL-quoted before being appended; if any element is Unicode, it will
219     converted to a UTF-8 bytestring before being URL-quoted. If any element
220     is an integer, it will be converted to its string representation before
221     being URL-quoted.
7ae0c2 222
fb6a5c 223     .. warning:: if no ``elements`` arguments are specified, the resource
7ae0c2 224                  URL will end with a trailing slash.  If any
CM 225                  ``elements`` are used, the generated URL will *not*
226                  end in trailing a slash.
227
ae4039 228     If a keyword argument ``query`` is present, it will be used to
7ae0c2 229     compose a query string that will be tacked on to the end of the
CM 230     URL.  The value of ``query`` must be a sequence of two-tuples *or*
231     a data structure with an ``.items()`` method that returns a
232     sequence of two-tuples (presumably a dictionary).  This data
233     structure will be turned into a query string per the documentation
169711 234     of ``pyramid.url.urlencode`` function.  After the query data is
7ae0c2 235     turned into a query string, a leading ``?`` is prepended, and the
c5f24b 236     resulting string is appended to the generated URL.
7ae0c2 237
CM 238     .. note:: Python data structures that are passed as ``query``
e62e47 239               which are sequences or dictionaries are turned into a
7ae0c2 240               string under the same rules as when run through
c6895b 241               :func:`urllib.urlencode` with the ``doseq`` argument
CM 242               equal to ``True``.  This means that sequences can be
243               passed as values, and a k=v pair will be placed into the
244               query string for each value.
e693ce 245
MN 246     If a keyword argument ``anchor`` is present, its string
247     representation will be used as a named anchor in the generated URL
fb6a5c 248     (e.g. if ``anchor`` is passed as ``foo`` and the resource URL is
CM 249     ``http://example.com/resource/url``, the resulting generated URL will
250     be ``http://example.com/resource/url#foo``).
e693ce 251
MN 252     .. note:: If ``anchor`` is passed as a string, it should be UTF-8
253               encoded. If ``anchor`` is passed as a Unicode object, it
254               will be converted to UTF-8 before being appended to the
255               URL.  The anchor value is not quoted in any way before
256               being appended to the generated URL.
257
258     If both ``anchor`` and ``query`` are specified, the anchor element
259     will always follow the query element,
260     e.g. ``http://example.com?foo=1#bar``.
8b1f6e 261
bac5b3 262     If the ``resource`` passed in has a ``__resource_url__`` method, it will
c4d401 263     be used to generate the URL (scheme, host, port, path) that for the base
CM 264     resource which is operated upon by this function.  See also
bac5b3 265     :ref:`overriding_resource_url_generation`.
CM 266
fb6a5c 267     .. note:: If the :term:`resource` used is the result of a
8b1f6e 268              :term:`traversal`, it must be :term:`location`-aware.
fb6a5c 269              The resource can also be the context of a :term:`URL
8b1f6e 270              dispatch`; contexts found this way do not need to be
CM 271              location-aware.
272
273     .. note:: If a 'virtual root path' is present in the request
274               environment (the value of the WSGI environ key
fb6a5c 275               ``HTTP_X_VHM_ROOT``), and the resource was obtained via
8b1f6e 276               :term:`traversal`, the URL path will not include the
CM 277               virtual root prefix (it will be stripped off the
278               left hand side of the generated URL).
92c3e5 279
206188 280     .. note:: For backwards compatibility purposes, this function can also be
CM 281        imported as ``model_url``, although doing so will emit a deprecation
282        warning.
003908 283     """
6fec21 284     try:
CM 285         reg = request.registry
286     except AttributeError:
287         reg = get_current_registry() # b/c
003908 288     
fb6a5c 289     context_url = reg.queryMultiAdapter((resource, request), IContextURL)
da442d 290     if context_url is None:
fb6a5c 291         context_url = TraversalContextURL(resource, request)
CM 292     resource_url = context_url()
925957 293
e693ce 294     qs = ''
MN 295     anchor = ''
296
7ae0c2 297     if 'query' in kw:
CM 298         qs = '?' + urlencode(kw['query'], doseq=True)
e693ce 299
MN 300     if 'anchor' in kw:
301         anchor = kw['anchor']
302         if isinstance(anchor, unicode):
303             anchor = anchor.encode('utf-8')
304         anchor = '#' + anchor
925957 305
CM 306     if elements:
afc4bb 307         suffix = _join_elements(elements)
925957 308     else:
CM 309         suffix = ''
310
fb6a5c 311     return resource_url + suffix + qs + anchor
CM 312
92c3e5 313 model_url = resource_url # b/w compat (forever)
7ae0c2 314
206188 315 deprecated(
CM 316     'model_url',
317     'pyramid.url.model_url is deprecated as of Pyramid 1.0.  Use'
318     '``pyramid.url.resource_url`` instead (API-compat, simple '
319     'rename).')
320
750ce4 321 def static_url(path, request, **kw):
CM 322     """
3e2f12 323     Generates a fully qualified URL for a static :term:`asset`.
CM 324     The asset must live within a location defined via the
aff443 325     :meth:`pyramid.config.Configurator.add_static_view`
c1eb0c 326     :term:`configuration declaration` (see :ref:`static_assets_section`).
750ce4 327
ffca4e 328     .. note:: Calling :meth:`pyramid.Request.static_url` can be used to
36c159 329               achieve the same result as :func:`pyramid.url.static_url`.
CM 330
06173d 331     Example::
CM 332
333         static_url('mypackage:static/foo.css', request) =>
334
335                                 http://example.com/static/foo.css
336
337
750ce4 338     The ``path`` argument points at a file or directory on disk which
CM 339     a URL should be generated for.  The ``path`` may be either a
3e2f12 340     relative path (e.g. ``static/foo.css``) or a :term:`asset
750ce4 341     specification` (e.g. ``mypackage:static/foo.css``).  A ``path``
c6895b 342     may not be an absolute filesystem path (a :exc:`ValueError` will
CM 343     be raised if this function is supplied with an absolute path).
750ce4 344
8b1f6e 345     The ``request`` argument should be a :term:`request` object.
750ce4 346
CM 347     The purpose of the ``**kw`` argument is the same as the purpose of
c81aad 348     the :func:`pyramid.url.route_url` ``**kw`` argument.  See the
c6895b 349     documentation for that function to understand the arguments which
CM 350     you can provide to it.  However, typically, you don't need to pass
3e2f12 351     anything as ``*kw`` when generating a static asset URL.
750ce4 352
c6895b 353     This function raises a :exc:`ValueError` if a static view
CM 354     definition cannot be found which matches the path specification.
750ce4 355
CM 356     """
357     if os.path.isabs(path):
358         raise ValueError('Absolute paths cannot be used to generate static '
3e2f12 359                          'urls (use a package-relative path or an asset '
750ce4 360                          'specification).')
CM 361     if not ':' in path:
362         # if it's not a package:relative/name and it's not an
363         # /absolute/path it's a relative/path; this means its relative
364         # to the package in which the caller's module is defined.
13c923 365         package = caller_package()
750ce4 366         path = '%s:%s' % (package.__name__, path)
6fec21 367
CM 368     try:
369         reg = request.registry
370     except AttributeError:
371         reg = get_current_registry() # b/c
750ce4 372     
b29429 373     info = reg.queryUtility(IStaticURLInfo)
CM 374     if info is None:
375         raise ValueError('No static URL definition matching %s' % path)
376         
377     return info.generate(path, request, **kw)
7ae0c2 378
5653d1 379 def current_route_url(request, *elements, **kw):
b23e6e 380     """Generates a fully qualified URL for a named :app:`Pyramid`
5653d1 381     :term:`route configuration` based on the 'current route'.
b23e6e 382     
5653d1 383     This function supplements :func:`pyramid.url.route_url`. It presents an
CM 384     easy way to generate a URL for the 'current route' (defined as the route
385     which matched when the request was generated).
386
387     The arguments to this function have the same meaning as those with the
388     same names passed to :func:`pyramid.url.route_url`.  It also understands
389     an extra argument which ``route_url`` does not named ``_route_name``.
390
391     The route name used to generate a URL is taken from either the
392     ``_route_name`` keyword argument or the name of the route which is
393     currently associated with the request if ``_route_name`` was not passed.
394     Keys and values from the current request :term:`matchdict` are combined
395     with the ``kw`` arguments to form a set of defaults named ``newkw``.
396     Then ``route_url(route_name, request, *elements, **newkw)`` is called,
397     returning a URL.
398
399     Examples follow.
400
401     If the 'current route' has the route pattern ``/foo/{page}`` and the
402     current url path is ``/foo/1`` , the matchdict will be ``{'page':'1'}``.
403     The result of ``current_route_url(request)`` in this situation will be
404     ``/foo/1``.
405
406     If the 'current route' has the route pattern ``/foo/{page}`` and the
bad27c 407     current url path is ``/foo/1``, the matchdict will be
5653d1 408     ``{'page':'1'}``.  The result of ``current_route_url(request, page='2')``
CM 409     in this situation will be ``/foo/2``.
b23e6e 410         
5653d1 411     Usage of the ``_route_name`` keyword argument: if our routing table
CM 412     defines routes ``/foo/{action}`` named 'foo' and ``/foo/{action}/{page}``
413     named ``fooaction``, and the current url pattern is ``/foo/view`` (which
414     has matched the ``/foo/{action}`` route), we may want to use the
415     matchdict args to generate a URL to the ``fooaction`` route.  In this
e6ba3c 416     scenario, ``current_route_url(request, _route_name='fooaction', page='5')``
5653d1 417     Will return string like: ``/foo/view/5``.
b23e6e 418     """
5653d1 419
CM 420     if '_route_name' in kw:
421         route_name = kw.pop('_route_name')
b23e6e 422     else:
5653d1 423         route = getattr(request, 'matched_route', None)
CM 424         route_name = getattr(route, 'name', None)
425         if route_name is None:
426             raise ValueError('Current request matches no route')
427
428     newkw = {}
429     newkw.update(request.matchdict)
430     newkw.update(kw)
431     return route_url(route_name, request, *elements, **newkw)
b23e6e 432
afc4bb 433 @lru_cache(1000)
CM 434 def _join_elements(elements):
8cf91a 435     return '/'.join([quote_path_segment(s, safe=':@&+$,') for s in elements])