Repository: Pylons/pyramid

Issue:
Caching of RendererHelper in view_config()
_incomplete_: Wanted to get the thoughts and findings that @mmerickel were going through down in the bug tracker.

As of right now `pyramid_mako` has broken the `pyramid.reload_templates` setting, digging into it a little deeper the issue can be traced back to the renderer factories return being cached in `RendererHelper`: https://github.com/Pylons/pyramid/blob/master/pyramid/renderers.py#L400

When used with `render()`, this isn't really an issue since a new `RenderHelper` is created for each call to it.

However using `view_config()` the `RendererHelper` gets cached, so after the first request the return from the renderer factory is reified and the factory is never called again, and the `RendererHelper` persists.

---

To support `reload_templates` the returned function that deals with rendering is actually doing all of the work of verifying the template on disk has changed or not, this basically moves any logic for asset resolving out of a one time thing in the factory back into something that has to be done every time the renderer is called and there is no good way to cache the information so that asset resolving doesn't have to be done each and every call to render.

---

Is there a better way to solve this, to support `reload_templates` without having to go through the trouble the renderers currently go through? Is there a way to save the result of an asset lookup, or an effective way to cache it?



Discussion:
Specifically the issue in `pyramid_mako` as seen from a PR: https://github.com/Pylons/pyramid_mako/pull/20

I think it's surprising that the invocations of `p.renderers.render()` and the renderer from a `add_view(renderer=...)` have different behavior. I think it's even more surprising that the `IRenderer` returned from the factory is cached across requests without really telling anyone. My thought would be to instead pass some `renderer_factory` to the `ViewDeriver`, such that it can keep the required package/info/registry. This `renderer_factory` then would return a new `RendererHelper` object per-request.

The current behavior of caching the `IRenderer` at the time of "first render" is also a sneaky little race condition in Pyramid right now, despite probably not technically affecting anyone. I would not expect that my `IRenderer` code needed to be thread-safe, and if my suggestions are rejected then the documentation should be updated.

> Is there a better way to solve this, to support reload_templates without having to go through the trouble the renderers currently go through?

Renderers are intentionally agnostic to whether they are serving asset specs, or files or whatever else, so I doubt much can be done by Pyramid to alleviate this other than providing some helpers that renderers could use to handle this logic.


Resolving patch:
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -349,7 +349,6 @@ def rendered_view(self, view):
 
     def _rendered_view(self, view, view_renderer):
         def rendered_view(context, request):
-            renderer = view_renderer
             result = view(context, request)
             if result.__class__ is Response: # potential common case
                 response = result
@@ -367,6 +366,8 @@ def rendered_view(context, request):
                             name=renderer_name,
                             package=self.kw.get('package'),
                             registry = registry)
+                    else:
+                        renderer = view_renderer.clone()
                     if '__view__' in attrs:
                         view_inst = attrs.pop('__view__')
                     else:
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -382,6 +382,9 @@ class IRendererInfo(Interface):
     settings = Attribute('The deployment settings dictionary related '
                          'to the current application')
 
+    def clone():
+        """ Return a shallow copy that does not share any mutable state."""
+
 class IRendererFactory(Interface):
     def __call__(info):
         """ Return an object that implements


Test patch:
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -2548,6 +2548,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst, view)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         def view(request):
             return 'OK'
         deriver = self._makeOne(renderer=moo())
@@ -2585,6 +2587,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst, 'view')
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         def view(request):
             return 'OK'
         deriver = self._makeOne(renderer=moo())
@@ -3179,6 +3183,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst.__class__, View)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View(object):
             def __init__(self, context, request):
                 pass
@@ -3203,6 +3209,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst.__class__, View)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View(object):
             def __init__(self, request):
                 pass
@@ -3227,6 +3235,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst.__class__, View)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View:
             def __init__(self, context, request):
                 pass
@@ -3251,6 +3261,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst.__class__, View)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View:
             def __init__(self, request):
                 pass
@@ -3275,6 +3287,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst, view)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View:
             def index(self, context, request):
                 return {'a':'1'}
@@ -3297,6 +3311,8 @@ def render_view(inself, req, resp, view_inst, ctx):
                 self.assertEqual(view_inst, view)
                 self.assertEqual(ctx, context)
                 return response
+            def clone(self):
+                return self
         class View:
             def index(self, request):
                 return {'a':'1'}