EmilStenstrom / django-components

Create simple reusable template components in Django.
MIT License
973 stars 62 forks source link

"context" is empty in template method #26

Closed danjac closed 3 years ago

danjac commented 3 years ago

So I have this component:

class Svg(component.Component):
    def context(self, name: str, css_class: str = "", title: str = "", **attrs) -> Dict:
        return {"name": name, "css_class": css_class, "title": title, **attrs}

    def template(self, context: Dict) -> str:
        print(context)
        return f"svg/_{context['name']}.svg"

The template name is assigned dynamically depending on the name of the SVG:

{% component "svg" name="download" %}

However the template() method raises a KeyError, as the key "name" is not in the context: it's empty.

What is the order of execution here? Is template() called before context() ? If so, how do I access variables to be able to set the template name dynamically?

danjac commented 3 years ago

Looks like there's some optimization happening here in component.py:

    component_template = get_template(self.template({}))

So basically the "context" argument is pointless as it's always going to be {}, and is misleading as it gives the impression that the template context is available at rendering time.

If that's the intention, it kind of makes having "template" a method pointless as well: why not just make it a class attribute?

I'd probably want to make "template" an attribute instead, with a get_template() method that allows returning a template at render time.

EmilStenstrom commented 3 years ago

@rbeard0330 Is this related to your latest optimization?

EmilStenstrom commented 3 years ago

@danjac This is a bug. The component you've written should work as you intended it to. It's actually a good example that we should as as a simple acceptance test. I have a lot on my plate right now, but give me a couple of days and I'll have a look.

rbeard0330 commented 3 years ago

@rbeard0330 Is this related to your latest optimization?

Yes, definitely. I think I mentioned in the PR that the change was breaking for this kind of use--fundamentally, you can't compile the component when the rest of the template is compiled if the identity of the component template changes each time the template is rendered, and you'll need to compile it fresh every render. A couple thoughts:

danjac commented 3 years ago

Native "include" probably covers the use-case best here I think, and I wouldn't want to add complexity or performance regressions for this use case.

It would probably be a good idea to remove the "context" arg for template if it doesn't really do anything, or at least make it optional for backward compatibility.

EmilStenstrom commented 3 years ago

I wonder if the best way could be to look for a template method, and if that exists skip the pre-compilation? Shouldn't that give us the best of both worlds?

I do think that the code as danjac assumed should work, actually should.

danjac commented 3 years ago

@EmilStenstrom sure, but the documentation should be clear about the performance hit (if it's significant).

EmilStenstrom commented 3 years ago

If we should talk about a performance hit or not I think depends on what we compare to. If we're still faster than include, then a better way to describe it is "added optimization" if using a static template. One way to measure this could be to a benchmark comparing this to an included template with a dynamic template, just to see what we're comparing it to. This is also what #14 asks for. When @rbeard0330 added this (great) optimization, there was a 50% improvement in rendering speed in that particular benchmark.

rbeard0330 commented 3 years ago

I wonder if the best way could be to look for a template method, and if that exists skip the pre-compilation? Shouldn't that give us the best of both worlds?

I do think that the code as danjac assumed should work, actually should.

This would require adding a different interface for specifying a static template (and I agree that a template_name variable would be the way to do it), and then people would need to rewrite their components to use it. We should bench it to be sure, but I think it will also perform worse than a dynamic include. The reason is that Django caches templates, so it should only have to compile each include once, and then it can reuse the compiled template next time it's needed.

Perhaps we could imitate Django by compiling template at render-time, and then cache compiled templates based on what the template method returns? If your template method is just returning a static string, then you still only compile it once, and even if it's dynamic template, we would still only render once per different template name.

EmilStenstrom commented 3 years ago

@rbeard0330 Caching sounds ideal now that you mention it. I guess we could even "prime" the cache with an empty context, to get speedup even at the first request for static templates, at the expense of some wasted caching in case the template actually is dynamic.

rbeard0330 commented 3 years ago

I worked on this a bit this evening. It's pretty easy to do with lru_cache, but that's not supported in Python 2.

danjac commented 3 years ago

Why is there a need to support Python 2 in 2021, especially a new library?

EmilStenstrom commented 3 years ago

In 2015, when I started this projekt, Python 2 was still supported by Django. If it makes things tricky now, we can definitely drop it. Let's do it in a separate PR though.

danjac commented 3 years ago

Didn't realize the project was 5 years old :-)

Might be worth first dropping Python 2 support first so you can use lru_cache and other Python 3 improvements. Django itself no longer supports Python 2.

EmilStenstrom commented 3 years ago

I just dropped support for all Python and Django versions not officially supported my Django itself. This means we're now Python 3.6+ and Django 2.2+. Hope this makes things easier to hack on going forward :)

EmilStenstrom commented 3 years ago

@danjac I just released version 0.9 of the django-reusable-components package. Please try your code again and let us know how it works!

EmilStenstrom commented 3 years ago

I just added a test for the dynamic template case, to make sure we don't accidentally break this case again.