sqlalchemy/mako

defs don't propogate sub defs

JonathanWylie opened this issue · 6 comments

When I have a chain of defs, each called so that caller is available. If an intermediate def does not recreate the subdef from it's caller, then the sub def is not accessible.

<%def name="first()">

  <%self:intermediate>
    <%def name="header()">
      <div>header</div>
    </%def>
    body
  </%self:intermediate>

</%def>

<%def name="intermediate()">

  <%self:final>
    ${caller.body()}
  </%self:final>

  <span>intermediate thing</span>

</%def>

<%def name="final()">
    ${caller.header()}
  <div>${caller.body()}</div>
</%def>

${first()}

Fails with AttributeError: Namespace 'caller' has no member 'header'

Not even reimplementing the subdef in the intermediate def to call it's caller works

<%def name="first()">

  <%self:intermediate>
    <%def name="header()">
      <div>header</div>
    </%def>
    body
  </%self:intermediate>

</%def>

<%def name="intermediate()">

  <%self:final>
    ${caller.body()}
    <%def name="header()">
      ${caller.header()}
    </%def>
  </%self:final>

  <span>intermediate thing</span>

</%def>

<%def name="final()">
  ${caller.header()}
  <div>${caller.body()}</div>
</%def>

${first()}

This fails with AttributeError: 'NoneType' object has no attribute 'header'.

It would be good if I could both override/extend a subdef. And also when calling a callable on a def namespace, it searches up the caller stack until it finds a matching callable.
I know I can do this kind of thing with inheriting templates, but it forces me to put everything in separate files because I can't define multiple templates in a single file.

hey there -

let me first start off with an update on Mako maintenance status. While we still do releases for Mako for things like supporting Python interpreters, dropping python 2.7 support which is in the works, etc., we haven't actually changed anything about the template language or runtime itself in probably ten years. this is not to say Mako is obsolete or anything, but it's used in the places that it's used these days and is probably not very popular at all for new projects where people are trying new things. hence we haven't even gotten any commentary on template / runtime specifics in almost that many years also, and there's not currently any "visionary" work being done on the language to make it something newer / fresher / etc. the template language welds very tightly to the way Python itself works so it's often not very easy to make it do new things.

in this specific case, the "def call with content" feature is probably even less popular because it's kind of quirky. However, I always liked this feature which is why we have it. I can see what you are going for, and looking in there I can observe two things. one is that your second example, which tries to call upon "caller" in the context of a nested def, seems like it should work, however within that def the "caller" is basically gone here, and to make that work seems like it involves cracking open the scoping rules in how the template is converted to Python. interestingly enough in your first example, the namespace that has the working form of "header()" that's up in "intermediate" is there, which you can get at manually like this:

<%def name="final()">
    ${caller[-2].header()}
  <div>${caller.body()}</div>
</%def>

so that does imply the CallerStack object in use here could be enhanced so that its __getattr__() walks up the stack to try to find the name, but im not sure of the implications of this because this is code i haven't looked at in about ten years. doing it inside of "intermediate" works too if we do it like this:

<%def name="intermediate()">

  <%self:final>
    ${caller.body()}
    <%def name="header()">
        ${caller[-3].header()}
    </%def>

  </%self:final>

  <span>intermediate thing</span>

</%def>

<%def name="final()">
    ${caller.header()}
  <div>${caller.body()}</div>
</%def>

so overall having CallerStack look "up" the stack when asked for an attribute may be an easier change that might make more use cases work, starting with this concept:

diff --git a/mako/runtime.py b/mako/runtime.py
index 465908e..f984c4c 100644
--- a/mako/runtime.py
+++ b/mako/runtime.py
@@ -204,7 +204,15 @@ class CallerStack(list):
         return self[-1]
 
     def __getattr__(self, key):
-        return getattr(self._get_caller(), key)
+        for idx in range(1, len(self)):
+            caller = self[-idx]
+            try:
+                return getattr(caller, key)
+            except AttributeError:
+                continue
+        else:
+            raise AttributeError("no such attribute %r" % key)
+
 
     def _push_frame(self):
         frame = self.nextcaller or None

however, the above concept would need to be written such that it does not rely upon catching an attributeerror as this is non-performant, and would also need good tests written. again we've not changed any of this code in many years.

Thanks for your detailed response @zzzeek . Let's just close this then, given these kinds of changes are not really done anymore. PyCharm recently removed support for mako templates from their pro version. So I guess mako really is legacy, even if it's not obsolete.

Thanks for your detailed response @zzzeek . Let's just close this then, given these kinds of changes are not really done anymore. PyCharm recently removed support for mako templates from their pro version. So I guess mako really is legacy, even if it's not obsolete.

yeah i am trying to understand why they did that as it seems it was causing some kind of problem, but I'm not familiar with what it was.

Thanks for your detailed response @zzzeek . Let's just close this then, given these kinds of changes are not really done anymore. PyCharm recently removed support for mako templates from their pro version. So I guess mako really is legacy, even if it's not obsolete.

yeah i am trying to understand why they did that as it seems it was causing some kind of problem, but I'm not familiar with what it was.

I'll take a look and reach out to them if need be.

I'll take a look and reach out to them if need be.

Please let me know if there is a way around it, or they provide any information. Maybe some plugin could add support back, but I'm going to have to downgrade pycharm and be stuck without python3.10 support in inspections -.-