Proposal for a new template tag to disable invoking callable variables

Skip to first unread message

Alex Epshteyn

unread,
Feb 23, 2019, 5:46:04 PM2/23/19
to Django developers (Contributions to Django itself)
I recently posted #30205, which proposes a new template tag to solve a template problem that has been bothering me (see #30197).

Requesting feedback from the community regarding both of these tickets.

If there is consensus, I'd be happy to take a shot at implementing it.

Thanks,

Alex

Alex Epshteyn

unread,
Feb 23, 2019, 9:33:38 PM2/23/19
to Django developers (Contributions to Django itself)
The reason for this suggestion is over the years, while writing templates, I've had to spend a lot of time trying to figure out why I was getting an unexpected value (often an empty string) substituted for a template variable when that variable clearly had a different value.  

The reason that happens, of course, is that the ​django.template.base.Variable._resolve_lookup method tries to invoke every callable value in a variable's resolution path, including the variable itself.  If that callable is a function with parameters, for example, the invocation raises an exception and you end up with an empty string (or whatever value you use for the string_if_invalid setting) as a result.  If it's a different kind of callable, like a class or a regular object that implements __call__ for whatever reason, you just end up with something other than what you wanted.

While automatically invoking callables makes sense when the current "bit" in the resolution path is a method and the previous "bit" was an instance, it is very surprising when the "callable" value is not actually a method (e.g. it's a class or a standalone function), in which case I don't think it makes sense to call it automatically.  In fact, this goes against the fundamental Python (and Django) philosophy of "Explicit is better than implicit".

There are legitimate cases where you might want to pass something like a class object to a template and to not have it mysteriously replaced by an arbitrary instance of that class (see #30197). I also think that the existing workaround of setting a do_not_call_in_templates attribute on such object is insufficient to cover all such cases (e.g. when the object is a class or function that comes from some library and you don't want to risk messing with it).

My comment on #30197 suggested replacing this implicit behavior with an explicit tag like {% call foo %} instead of {{ foo }} (this example assumes that foo is callable).  Although that would be a breaking change, I think it might be worth considering because it would be in the spirit of upholding the "Explicit is better than implicit" principle, and prevent a frequently-occurring problem for template authors (which is evidenced by the prevalence of tickets and StackOverflow questions about this topic; e.g. https://stackoverflow.com/questions/6861601/cannot-resolve-callable-context-variable)

However, in #30205, I am proposing a non-breaking change to solve this problem -- a new template tag similar to autoescape, which could be used like this:

{% callables off %}
  <div>The class name is {{ foo.bar|type_name }}</div>
{% endcallables %}

(in the above example foo is an object containing an attribute named "bar" whose value is a class, and |type_name is a user-defined filter that returns the "__name__" attribute of its argument)

Adam Johnson

unread,
Mar 1, 2019, 12:01:45 PM3/1/19
to django-d...@googlegroups.com
I've had a review of the current and historical tickets, and I think that do_not_call_in_templates is sufficient. A template tag to control such a low level feature of the templating system seems a bit niche. If you are worried about adding attributes with objects from third party libraries, you can always proxy them with a custom wrapt.ObjectProxy before passing to the template (docs: https://wrapt.readthedocs.io/en/latest/wrappers.html ), for example:

In [1]: import wrapt

In [2]: class DoNotCallInTemplatesProxy(wrapt.ObjectProxy):
   ...:     do_not_call_in_templates = True
   ...:

In [3]: class Dog:
   ...:     def __call__(self):
   ...:         return 'foobar'
   ...:
   ...: third_party_callable = Dog()
   ...:

In [4]: x = DoNotCallInTemplatesProxy(third_party_callable)

In [5]: x.do_not_call_in_templates
Out[5]: True

In [6]: third_party_callable.do_not_call_in_templates
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-1ec2cadaa18a> in <module>
----> 1 third_party_callable.do_not_call_in_templates

AttributeError: 'Dog' object has no attribute 'do_not_call_in_templates'

--
You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-develop...@googlegroups.com.
To post to this group, send email to django-d...@googlegroups.com.
Visit this group at https://groups.google.com/group/django-developers.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/962bead6-e186-416b-a18a-3e0f2ad23da9%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Adam
Reply all
Reply to author
Forward
0 new messages