Opened 6 years ago

Last modified 6 years ago

#30197 closed New feature

Template variable resolution with class objects — at Initial Version

Reported by: Alex Epshteyn Owned by: nobody
Component: Template system Version: dev
Severity: Normal Keywords: template, variable, resolve
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The behavior of if callable(current): ... current = current() in django.template.base.Variable._resolve_lookup prevents using a function (or a class!) as a value in a template.

This behavior only makes sense when the previous bit in self.lookups was an instance and the current bit is a method that should be invoked on that instance. I would argue that it never makes sense in any other case (for example, https://stackoverflow.com/q/6861601/1965404).

Example

Suppose you want to write a filter that returns the name of a class:

@register.filter()
def type_name(someClass):
    return someClass.__name__

Using this filter in a template like:

<div>Foo.Bar is an instance of {{ foo.bar|type_name }}</div>

might seem valid, but it turns out that this expression will never work as intended, due to the implementation of Variable._resolve_lookup — the filter we defined in this example will pretty much always return an empty string (or whatever value you configured for your app's string_if_invalid setting).

For example, if foo.bar is <type 'basestring'>, invoking it as a callable will raise TypeError: The basestring type cannot be instantiated, and if foo.bar is some other type that is actually instantiable (e.g. <type 'str'>, or a user-defined class), our filter will receive an instance of that class (rather than the class itself), which will almost-certainly lead to an exception like AttributeError: 'str' object has no attribute '__name__'

Approaches tried in the past

The workaround introduced by #15791 allows setting an attribute named do_not_call_in_templates on a callable object to be able to use the object itself as the value (rather than the value returned from invoking it).

However, this seems like a dirty hack, and furthermore, in many cases you may not want to (or even be able to) set an attribute on that object (e.g. if this object is a class that comes from a library and you have no control of it).

Current implementation of Variable._resolve_lookup*:

def _resolve_lookup(self, context):
    """
    Perform resolution of a real variable (i.e. not a literal) against the
    given context.
    """
    current = context
    try:  # catch-all for silent variable failures
        for bit in self.lookups:
            try:  # dictionary lookup
                current = current[bit]
            # (followed by nested try/except blocks for attribute lookup and list-index lookup)
            # ...
            # *** and at last ***:
            if callable(current):
                if getattr(current, 'do_not_call_in_templates', False):
                    pass
                elif getattr(current, 'alters_data', False):
                    current = context.template.engine.string_if_invalid
                else:
                    try:  # method call (assuming no args required)
                        current = current()
                    # etc...
    # etc...
    return current

* abbreviated for clarity

Proposed solution

At the very least, I think it would make sense to add a check for inspect.isclass to the if callable(current): ... current = current() block of this method.

For example:

if callable(current) and not inspect.isclass(current):
    if getattr(current, 'do_not_call_in_templates', False):
        pass
    # etc...

or

if callable(current):
    if getattr(current, 'do_not_call_in_templates', False) or inspect.isclass(current):
        pass
    # etc...

However, ideally, it would be even better to check whether the previous bit in self.lookups was an instance and the current bit is a method supported by the class of that instance before trying to invoke current as a callable.

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top