Michael Merickel
2018-10-15 bda1306749c62ef4f11cfe567ed7d56c8ad94240
commit | author | age
7c0f09 1 from functools import partial
5cf9fc 2 import json
a9fed7 3 import os
b6ffe5 4 import re
a9fed7 5
0c29cf 6 from zope.interface import implementer, providedBy
677216 7 from zope.interface.registry import Components
3a2af3 8
0c29cf 9 from pyramid.interfaces import IJSONAdapter, IRendererFactory, IRendererInfo
0c1c39 10
0c29cf 11 from pyramid.compat import string_types, text_type
88c11a 12
7c0f09 13 from pyramid.csrf import get_csrf_token
3d9dd0 14 from pyramid.decorator import reify
0c1c39 15
a76e99 16 from pyramid.events import BeforeRender
b6ffe5 17
BJR 18 from pyramid.httpexceptions import HTTPBadRequest
0c1c39 19
fb9641 20 from pyramid.path import caller_package
0c1c39 21
fcb6cc 22 from pyramid.response import _get_response_factory
b60bdb 23 from pyramid.threadlocal import get_current_registry
19016b 24 from pyramid.util import hide_attrs
a9fed7 25
250c02 26 # API
0c29cf 27
250c02 28
62b479 29 def render(renderer_name, value, request=None, package=None):
00cee7 30     """ Using the renderer ``renderer_name`` (a template
TL 31     or a static renderer), render the value (or set of values) present
62b479 32     in ``value``. Return the result of the renderer's ``__call__``
250c02 33     method (usually a string or Unicode).
CM 34
00cee7 35     If the ``renderer_name`` refers to a file on disk, such as when the
TL 36     renderer is a template, it's usually best to supply the name as an
92c3e5 37     :term:`asset specification`
211c30 38     (e.g. ``packagename:path/to/template.pt``).
250c02 39
92c3e5 40     You may supply a relative asset spec as ``renderer_name``.  If
211c30 41     the ``package`` argument is supplied, a relative renderer path
92c3e5 42     will be converted to an absolute asset specification by
00cee7 43     combining the package ``package`` with the relative
TL 44     asset specification ``renderer_name``.  If ``package``
45     is ``None`` (the default), the package name of the *caller* of
46     this function will be used as the package.
62b479 47
CM 48     The ``value`` provided will be supplied as the input to the
49     renderer.  Usually, for template renderings, this should be a
50     dictionary.  For other renderers, this will need to be whatever
51     sort of value the renderer expects.
52
566a2a 53     The 'system' values supplied to the renderer will include a basic set of
CM 54     top-level system names, such as ``request``, ``context``,
55     ``renderer_name``, and ``view``.  See :ref:`renderer_system_values` for
56     the full list.  If :term:`renderer globals` have been specified, these
e74345 57     will also be used to augment the value.
250c02 58
62b479 59     Supply a ``request`` parameter in order to provide the renderer
CM 60     with the most correct 'system' values (``request`` and ``context``
61     in particular).
250c02 62
CM 63     """
62b479 64     try:
CM 65         registry = request.registry
66     except AttributeError:
67         registry = None
68     if package is None:
69         package = caller_package()
0c29cf 70     helper = RendererHelper(
MM 71         name=renderer_name, package=package, registry=registry
72     )
3803d9 73
19016b 74     with hide_attrs(request, 'response'):
dd2231 75         result = helper.render(value, None, request=request)
3803d9 76
MM 77     return result
250c02 78
0c29cf 79
MM 80 def render_to_response(
81     renderer_name, value, request=None, package=None, response=None
82 ):
00cee7 83     """ Using the renderer ``renderer_name`` (a template
TL 84     or a static renderer), render the value (or set of values) using
62b479 85     the result of the renderer's ``__call__`` method (usually a string
CM 86     or Unicode) as the response body.
250c02 87
CM 88     If the renderer name refers to a file on disk (such as when the
89     renderer is a template), it's usually best to supply the name as a
92c3e5 90     :term:`asset specification`.
250c02 91
92c3e5 92     You may supply a relative asset spec as ``renderer_name``.  If
62b479 93     the ``package`` argument is supplied, a relative renderer name
92c3e5 94     will be converted to an absolute asset specification by
00cee7 95     combining the package ``package`` with the relative
TL 96     asset specification ``renderer_name``.  If you do
62b479 97     not supply a ``package`` (or ``package`` is ``None``) the package
CM 98     name of the *caller* of this function will be used as the package.
99
100     The ``value`` provided will be supplied as the input to the
101     renderer.  Usually, for template renderings, this should be a
102     dictionary.  For other renderers, this will need to be whatever
103     sort of value the renderer expects.
104
566a2a 105     The 'system' values supplied to the renderer will include a basic set of
CM 106     top-level system names, such as ``request``, ``context``,
107     ``renderer_name``, and ``view``.  See :ref:`renderer_system_values` for
108     the full list.  If :term:`renderer globals` have been specified, these
0efeb4 109     will also be used to argument the value.
250c02 110
62b479 111     Supply a ``request`` parameter in order to provide the renderer
CM 112     with the most correct 'system' values (``request`` and ``context``
d23e69 113     in particular). Keep in mind that any changes made to ``request.response``
MM 114     prior to calling this function will not be reflected in the resulting
72bf6b 115     response object. A new response object will be created for each call
MM 116     unless one is passed as the ``response`` argument.
250c02 117
e38216 118     .. versionchanged:: 1.6
MM 119        In previous versions, any changes made to ``request.response`` outside
120        of this function call would affect the returned response. This is no
72bf6b 121        longer the case. If you wish to send in a pre-initialized response
MM 122        then you may pass one in the ``response`` argument.
e38216 123
62b479 124     """
CM 125     try:
126         registry = request.registry
127     except AttributeError:
128         registry = None
129     if package is None:
130         package = caller_package()
0c29cf 131     helper = RendererHelper(
MM 132         name=renderer_name, package=package, registry=registry
133     )
dd2231 134
19016b 135     with hide_attrs(request, 'response'):
72bf6b 136         if response is not None:
MM 137             request.response = response
dd2231 138         result = helper.render_to_response(value, None, request=request)
MM 139
140     return result
0c29cf 141
62b479 142
1281a5 143 def get_renderer(renderer_name, package=None, registry=None):
00cee7 144     """ Return the renderer object for the renderer ``renderer_name``.
62b479 145
92c3e5 146     You may supply a relative asset spec as ``renderer_name``.  If
62b479 147     the ``package`` argument is supplied, a relative renderer name
92c3e5 148     will be converted to an absolute asset specification by
00cee7 149     combining the package ``package`` with the relative
TL 150     asset specification ``renderer_name``.  If ``package`` is ``None``
151     (the default), the package name of the *caller* of this function
152     will be used as the package.
1281a5 153
CD 154     You may directly supply an :term:`application registry` using the
155     ``registry`` argument, and it will be used to look up the renderer.
156     Otherwise, the current thread-local registry (obtained via
157     :func:`~pyramid.threadlocal.get_current_registry`) will be used.
62b479 158     """
CM 159     if package is None:
160         package = caller_package()
0c29cf 161     helper = RendererHelper(
MM 162         name=renderer_name, package=package, registry=registry
163     )
f5fa3f 164     return helper.renderer
62b479 165
0c29cf 166
250c02 167 # concrete renderer factory implementations (also API)
0c29cf 168
a9fed7 169
3d9dd0 170 def string_renderer_factory(info):
e46105 171     def _render(value, system):
e6c2d2 172         if not isinstance(value, string_types):
0e131e 173             value = str(value)
c4c9a8 174         request = system.get('request')
CM 175         if request is not None:
a7b1a9 176             response = request.response
CM 177             ct = response.content_type
178             if ct == response.default_content_type:
179                 response.content_type = 'text/plain'
19473e 180         return value
0c29cf 181
19473e 182     return _render
CM 183
0c29cf 184
677216 185 _marker = object()
0c29cf 186
677216 187
d81ea3 188 class JSON(object):
MM 189     """ Renderer that returns a JSON-encoded string.
190
191     Configure a custom JSON renderer using the
85d6f8 192     :meth:`~pyramid.config.Configurator.add_renderer` API at application
d81ea3 193     startup time:
MM 194
195     .. code-block:: python
196
197        from pyramid.config import Configurator
198
199        config = Configurator()
85d6f8 200        config.add_renderer('myjson', JSON(indent=4))
d81ea3 201
85d6f8 202     Once this renderer is registered as above, you can use
d81ea3 203     ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or
814f19 204     :meth:`~pyramid.config.Configurator.add_view`:
d81ea3 205
MM 206     .. code-block:: python
207
208        from pyramid.view import view_config
209
210        @view_config(renderer='myjson')
211        def myview(request):
212            return {'greeting':'Hello world'}
213
677216 214     Custom objects can be serialized using the renderer by either
MM 215     implementing the ``__json__`` magic method, or by registering
216     adapters with the renderer.  See
217     :ref:`json_serializing_custom_objects` for more information.
218
44327c 219     .. note::
CM 220
221         The default serializer uses ``json.JSONEncoder``. A different
222         serializer can be specified via the ``serializer`` argument.  Custom
223         serializers should accept the object, a callback ``default``, and any
224         extra ``kw`` keyword arguments passed during renderer construction.
225         This feature isn't widely used but it can be used to replace the
226         stock JSON serializer with, say, simplejson.  If all you want to
227         do, however, is serialize custom objects, you should use the method
228         explained in :ref:`json_serializing_custom_objects` instead
229         of replacing the serializer.
677216 230
40dbf4 231     .. versionadded:: 1.4
TL 232        Prior to this version, there was no public API for supplying options
233        to the underlying serializer without defining a custom renderer.
d81ea3 234     """
MM 235
677216 236     def __init__(self, serializer=json.dumps, adapters=(), **kw):
MM 237         """ Any keyword arguments will be passed to the ``serializer``
238         function."""
239         self.serializer = serializer
d81ea3 240         self.kw = kw
677216 241         self.components = Components()
MM 242         for type, adapter in adapters:
243             self.add_adapter(type, adapter)
244
245     def add_adapter(self, type_or_iface, adapter):
cfabb1 246         """ When an object of the type (or interface) ``type_or_iface`` fails
CM 247         to automatically encode using the serializer, the renderer will use
248         the adapter ``adapter`` to convert it into a JSON-serializable
249         object.  The adapter must accept two arguments: the object and the
250         currently active request.
677216 251
MM 252         .. code-block:: python
253
254            class Foo(object):
255                x = 5
256
e012aa 257            def foo_adapter(obj, request):
677216 258                return obj.x
MM 259
260            renderer = JSON(indent=4)
c3df7a 261            renderer.add_adapter(Foo, foo_adapter)
cfabb1 262
CM 263         When you've done this, the JSON renderer will be able to serialize
264         instances of the ``Foo`` class when they're encountered in your view
265         results."""
befc1b 266
0c29cf 267         self.components.registerAdapter(
MM 268             adapter, (type_or_iface,), IJSONAdapter
269         )
d81ea3 270
MM 271     def __call__(self, info):
272         """ Returns a plain JSON-encoded string with content-type
273         ``application/json``. The content-type may be overridden by
274         setting ``request.response.content_type``."""
0c29cf 275
d81ea3 276         def _render(value, system):
MM 277             request = system.get('request')
278             if request is not None:
279                 response = request.response
280                 ct = response.content_type
281                 if ct == response.default_content_type:
282                     response.content_type = 'application/json'
5851d8 283             default = self._make_default(request)
23b7a2 284             return self.serializer(value, default=default, **self.kw)
befc1b 285
d81ea3 286         return _render
5851d8 287
CM 288     def _make_default(self, request):
289         def default(obj):
290             if hasattr(obj, '__json__'):
291                 return obj.__json__(request)
292             obj_iface = providedBy(obj)
293             adapters = self.components.adapters
0c29cf 294             result = adapters.lookup(
MM 295                 (obj_iface,), IJSONAdapter, default=_marker
296             )
5851d8 297             if result is _marker:
CM 298                 raise TypeError('%r is not JSON serializable' % (obj,))
299             return result(obj, request)
0c29cf 300
5851d8 301         return default
d81ea3 302
0c29cf 303
MM 304 json_renderer_factory = JSON()  # bw compat
d81ea3 305
ed4bba 306 JSONP_VALID_CALLBACK = re.compile(r"^[$a-z_][$0-9a-z_\.\[\]]+[^.]$", re.I)
0c29cf 307
b6ffe5 308
d81ea3 309 class JSONP(JSON):
1cb30e 310     """ `JSONP <https://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper
c1f3d0 311     which implements a hybrid json/jsonp renderer.  JSONP is useful for
befc1b 312     making cross-domain AJAX requests.
c1f3d0 313
CM 314     Configure a JSONP renderer using the
315     :meth:`pyramid.config.Configurator.add_renderer` API at application
316     startup time:
317
318     .. code-block:: python
319
320        from pyramid.config import Configurator
321
322        config = Configurator()
323        config.add_renderer('jsonp', JSONP(param_name='callback'))
324
18410a 325     The class' constructor also accepts arbitrary keyword arguments.  All
CM 326     keyword arguments except ``param_name`` are passed to the ``json.dumps``
327     function as its keyword arguments.
de797c 328
CM 329     .. code-block:: python
330
331        from pyramid.config import Configurator
332
333        config = Configurator()
334        config.add_renderer('jsonp', JSONP(param_name='callback', indent=4))
befc1b 335
40dbf4 336     .. versionchanged:: 1.4
TL 337        The ability of this class to accept a ``**kw`` in its constructor.
18410a 338
CM 339     The arguments passed to this class' constructor mean the same thing as
340     the arguments passed to :class:`pyramid.renderers.JSON` (including
cfabb1 341     ``serializer`` and ``adapters``).
de797c 342
c1f3d0 343     Once this renderer is registered via
CM 344     :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use
345     ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or
346     :meth:`pyramid.config.Configurator.add_view``:
347
348     .. code-block:: python
349
350        from pyramid.view import view_config
351
352        @view_config(renderer='jsonp')
353        def myview(request):
354            return {'greeting':'Hello world'}
355
356     When a view is called that uses the JSONP renderer:
357
358     - If there is a parameter in the request's HTTP query string that matches
359       the ``param_name`` of the registered JSONP renderer (by default,
360       ``callback``), the renderer will return a JSONP response.
361
362     - If there is no callback parameter in the request's query string, the
363       renderer will return a 'plain' JSON response.
364
40dbf4 365     .. versionadded:: 1.1
c1f3d0 366
2033ee 367     .. seealso::
SP 368
369         See also :ref:`jsonp_renderer`.
c1f3d0 370     """
d81ea3 371
MM 372     def __init__(self, param_name='callback', **kw):
c1f3d0 373         self.param_name = param_name
d81ea3 374         JSON.__init__(self, **kw)
c1f3d0 375
CM 376     def __call__(self, info):
377         """ Returns JSONP-encoded string with content-type
378         ``application/javascript`` if query parameter matching
379         ``self.param_name`` is present in request.GET; otherwise returns
380         plain-JSON encoded string with content-type ``application/json``"""
0c29cf 381
c1f3d0 382         def _render(value, system):
fcb6cc 383             request = system.get('request')
5851d8 384             default = self._make_default(request)
e012aa 385             val = self.serializer(value, default=default, **self.kw)
fcb6cc 386             ct = 'application/json'
MM 387             body = val
388             if request is not None:
389                 callback = request.GET.get(self.param_name)
b6ffe5 390
fcb6cc 391                 if callback is not None:
b6ffe5 392                     if not JSONP_VALID_CALLBACK.match(callback):
0c29cf 393                         raise HTTPBadRequest(
MM 394                             'Invalid JSONP callback function name.'
395                         )
b6ffe5 396
fcb6cc 397                     ct = 'application/javascript'
23b7a2 398                     body = '/**/{0}({1});'.format(callback, val)
fcb6cc 399                 response = request.response
MM 400                 if response.content_type == response.default_content_type:
401                     response.content_type = ct
c1f3d0 402             return body
0c29cf 403
c1f3d0 404         return _render
0c29cf 405
250c02 406
3b7334 407 @implementer(IRendererInfo)
62b479 408 class RendererHelper(object):
3d9dd0 409     def __init__(self, name=None, package=None, registry=None):
CM 410         if name and '.' in name:
411             rtype = os.path.splitext(name)[1]
62b479 412         else:
1939d0 413             # important.. must be a string; cannot be None; see issue 249
CM 414             rtype = name or ''
3d9dd0 415
f5fa3f 416         if registry is None:
CM 417             registry = get_current_registry()
3d9dd0 418
CM 419         self.name = name
420         self.package = package
421         self.type = rtype
422         self.registry = registry
250c02 423
62b479 424     @reify
f5fa3f 425     def settings(self):
d99dc7 426         settings = self.registry.settings
CM 427         if settings is None:
428             settings = {}
f5fa3f 429         return settings
CM 430
431     @reify
432     def renderer(self):
433         factory = self.registry.queryUtility(IRendererFactory, name=self.type)
434         if factory is None:
0c29cf 435             raise ValueError('No such renderer factory %s' % str(self.type))
f5fa3f 436         return factory(self)
CM 437
62b479 438     def get_renderer(self):
CM 439         return self.renderer
ca9e7e 440
95c9f6 441     def render_view(self, request, response, view, context):
0c29cf 442         system = {
MM 443             'view': view,
444             'renderer_name': self.name,  # b/c
445             'renderer_info': self,
446             'context': context,
447             'request': request,
448             'req': request,
449             'get_csrf_token': partial(get_csrf_token, request),
450         }
d868ff 451         return self.render_to_response(response, system, request=request)
95c9f6 452
62b479 453     def render(self, value, system_values, request=None):
CM 454         renderer = self.renderer
455         if system_values is None:
456             system_values = {
0c29cf 457                 'view': None,
MM 458                 'renderer_name': self.name,  # b/c
459                 'renderer_info': self,
460                 'context': getattr(request, 'context', None),
461                 'request': request,
462                 'req': request,
463                 'get_csrf_token': partial(get_csrf_token, request),
464             }
62b479 465
5c52da 466         system_values = BeforeRender(system_values, value)
CM 467
62b479 468         registry = self.registry
5c52da 469         registry.notify(system_values)
62b479 470         result = renderer(value, system_values)
CM 471         return result
472
473     def render_to_response(self, value, system_values, request=None):
474         result = self.render(value, system_values, request=request)
475         return self._make_response(result, request)
476
477     def _make_response(self, result, request):
0eaa60 478         # broken out of render_to_response as a separate method for testing
CM 479         # purposes
a7b1a9 480         response = getattr(request, 'response', None)
CM 481         if response is None:
482             # request is None or request is not a pyramid.response.Response
483             registry = self.registry
32cb80 484             response_factory = _get_response_factory(registry)
JA 485             response = response_factory(request)
a7b1a9 486
a007a4 487         if result is not None:
CM 488             if isinstance(result, text_type):
23b7a2 489                 response.text = result
f0a9df 490             elif isinstance(result, bytes):
LR 491                 response.body = result
492             elif hasattr(result, '__iter__'):
493                 response.app_iter = result
a007a4 494             else:
CM 495                 response.body = result
62b479 496
CM 497         return response
a7b1a9 498
73c0ae 499     def clone(self, name=None, package=None, registry=None):
CM 500         if name is None:
501             name = self.name
502         if package is None:
503             package = self.package
504         if registry is None:
505             registry = self.registry
506         return self.__class__(name=name, package=package, registry=registry)
507
0c29cf 508
aa2fe1 509 class NullRendererHelper(RendererHelper):
CM 510     """ Special renderer helper that has render_* methods which simply return
511     the value they are fed rather than converting them to response objects;
512     useful for testing purposes and special case view configuration
513     registrations that want to use the view configuration machinery but do
514     not want actual rendering to happen ."""
0c29cf 515
bfbfd8 516     def __init__(self, name=None, package=None, registry=None):
CM 517         # we override the initializer to avoid calling get_current_registry
518         # (it will return a reference to the global registry when this
519         # thing is called at module scope; we don't want that).
520         self.name = None
521         self.package = None
522         self.type = ''
523         self.registry = None
524
525     @property
526     def settings(self):
befc1b 527         return {}
bfbfd8 528
aa2fe1 529     def render_view(self, request, value, view, context):
CM 530         return value
531
532     def render(self, value, system_values, request=None):
533         return value
befc1b 534
aa2fe1 535     def render_to_response(self, value, system_values, request=None):
CM 536         return value
537
538     def clone(self, name=None, package=None, registry=None):
539         return self
befc1b 540
0c29cf 541
aa2fe1 542 null_renderer = NullRendererHelper()