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