mikeckennedy/jinja_partials

Why not just use {% include ... %} ?

Closed this issue ยท 29 comments

Can you comment on why this is better than using {% include ... %}?
https://jinja.palletsprojects.com/en/3.0.x/templates/#include

Hi @davetapley. Good question. I created this because I didn't see how to accomplish that with this:

# Data provided to outer template:
user: User
videos: List[Video]
# Data needed by partial/include/subtemplete
email: str
video: Video (active one from a loop in the outer template potentially)

With the statement:

{% for v in videos %}

    <div class="video">
          {% include "video.html" ignore missing with context %}
    </div>

{% endfor %}

Where do I pass and transform the data? With jinja-paritals, it's:

{% for v in videos %}

    <div class="video">
        {{ render_partial('video.html', video=v, email=user.email) }}
    </div>

{% endfor %}

Is there a clean way to do this?

Also, macros definitely will not work for this situation, but include is closer.

Hi @davetapley Got any further thoughts on this?

@mikeckennedy everything you said makes sense. I'm fairly new to Flask/Jinja, so I have no further thoughts r.e. โฌ‡๏ธ ๐Ÿ˜

Is there a clean way to do this?


I will say that as someone coming from Rails (which I assume is where you got the name 'partials' from?) I was a bit confused at first to see that this library seemed to offer the only way to do 'partials', since I didn't even know about {% include ... %} yet.

It might be worth putting something at the top of the README to let Rails folk like me know that {% include ... %} exists (and what its limitations are)?

It's fairly clear to me why you would not want to use an include in this situation, but why not a macro?

{% macro video_image(video) %}
    <img src="https://img.youtube.com/vi/{{ video.id }}/maxresdefault.jpg"
        class="img img-responsive {{ ' '.join(classes) }}"
        alt="{{ video.title }}"
        title="{{ video.title }}">
{% endmacro %}

Your main template can call the macro:

{% from 'shared/macros/video_image.html' import video_image %}

{% for v in videos %}

    <div class="video">
        {{ video_image('video.html', video=v) }}
    </div>

{% endfor %}

I left out the email str part, but I don't think it makes a difference here, does it?

Hey @mohoyer

What you have looks great for adding the HTML into the larger template. But you also need a template that is only the macro. I'm am almost certain that calling flask.render_template('shared/macros/video_image.html', **data) will not generate the content you want.

So the problem with macros is not the reuse problem. It's that it needs to be used in two places and one of those is just the macro alone, so you would need a third template that does only this:

{% from 'shared/macros/video_image.html' import video_image %}
{{ video_image('video.html', video=v) }}

To me, that seems silly, when I can use this library to get the inside a page reuse and then just flask.render_template() on the partial page response.

You can find a concrete example of this here:

https://github.com/talkpython/htmx-python-course/blob/8e2c3ea46234832b79ee35abd917bba8226f0a2d/code/ch7_infinite_scroll/ch7_final_video_collector/views/videos.py#L53

Ah, I did not understand that you also wanted to use the macro outside of a template file. It would be possible to use macros from within the view function by loading the macro through the module attribute of a loaded template that contains the macro. In order for this to work with more complex templates like video_square (which uses an import, and therefore needs a loader), we need to do it through a proper jinja2 environment though.

This works with the macro template from above:

@blueprint.get('/macro_only')
def macro_only():
    videos = video_service.all_videos()

    environment = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
    template = environment.get_template('shared/macros/video_square.html')

    return flask.make_response(template.module.video_square(videos[0]))

It is admittedly a little more convoluted than the jinja-partials solution, although you could set up the environment outside of the request context. It might come down to a question of taste though.

I'd have to do more testing to confirm equivalent behavior as far as file-caching is concerned.

Hi @mohoyer Thanks for that example. That is interesting. It's the closest anyone has suggested as an alternative yet.

Other features you need to account for:

  • You can do this transitively: One partial can use two more for example internally. Maybe that would work with macros.
  • A page with 5 partials rendering different content, unrelated individually. Think about how complex this will make your view method registering 5 macros just to render template.

While the pure macro idea would mean no [extra] external dependencies, I find it pretty useless. For example, you could implement render_partial yourself on each view inside your flask app just as well as you can build a complex macro renderer.

But the point of this library is to make it dead simple to use jinja templates as part of a whole as well as individually as a partial response from the server.

One thing maybe you can give me insight into: Some people seem to have a knee-jerk reaction to this library (not necessarily you, just my general experience) like "Oh gag, why not just use the internals like include and macro? This is useless".

Is it that they don't see all the use-cases and think it's already there or what? I'm genuinely asking, in case you have some insights I'm missing.

Macros certainly work transitively as well. That's actually the reason why I needed the environment and the FileSystemLoader: When I changed your example app to use macros I imported the macro version of video_image in my template file with the macro for video_square. The import in the template is what requires the loader to be specified, which requires an environment.

Regarding your point about a page with 5 macro/partials: I'd argue that with a small wrapper the solution using macros could be made just as simple to use as render_partial. Arguably, render_partial is serving the same function as such a wrapper would, just using render_template instead of calling the macro, while requiring a little less code to do so.

I cannot speak to other peoples' reasoning, but I mostly work on projects with an existing code base that already contains a fair number of macros. In my situation, rewriting all macros as partials honestly does not sound incredibly enticing and I would rather use the suggested workaround to render them into text snippets if I had to do that. Which I might soon have to, that's why I was happy to get into this :)

Hi Max, thanks for your thoughts. I figured the macros, once set in motion, would keep going recursively but wasn't sure.

As for already working on a project with lots of macros, that would totally make sense to just keep going like that. I don't think I would rewrite things either. But on a new project that also has to directly render the template, like when doing HTMX, I think this is a bit cleaner.

I closed this issue but I did also update the readme.md to link here for this discussion.

Why not just:

{% for video in videos %}

<div class="video">
      {% set email = user.email -%}
      {% include "video.html" ignore missing with context %}
</div>

{% endfor %}

Hi @nickleman

That will work I think. But it's just more clumsy. Not too bad for one variable, the example we've been discussing needs two:

{% for v in videos %}

<div class="video">
      {% set email = user.email %}
      {% set video = v %}
      {% include "video.html" ignore missing with context %}
</div>

{% endfor %}

What if there is more?

{% for v in videos %}

<div class="video">
      {% set email = user.email %}
      {% set video = v %}
      {% set cat_name = category.category %}
      {% include "video.html" ignore missing with context %}
</div>

{% endfor %}

Does that still feel better than:

{% for v in videos %}

<div class="video">
    {{ render_partial("video.html", video=v, email=user.email, cat_name=category.category) }}
</div>

{% endfor %}

To me, definitely not. But if you don't like how that looks, then go for the set multiple variables + include. It's a choice for sure, not a requirement to use this library.

Since Jinja 2.1 (actual version is 3.0) context is automatically added in included templates and contains created variables. Therefore, you should be able to do just this :

{% for video in videos %}
<div class="video">
      {% include "video.html" ignore missing %}
</div>
{% endfor %}

See https://jinja.palletsprojects.com/en/3.0.x/templates/#import-visibility

Hi @Mindiell thanks for the feedback. But "keeping the same context" is exactly what I'm trying to avoid with this library. That's not what we want. What we want is the equivalent of calling a function with variables that are local with certain names and fields and passing them to a function with its own param names.

Example:

user = ...
time = now()
record_action(user.id, time)

With this function

def record_action(entity_id, timestamp)

What you're suggesting would require every caller of the function to write code like this:

user = ...
time = now()

entity_id = user.id
timestamp = time

record_action()

Does that look like an improvement? No way. See the discussion just a couple of entries in this thread above for this in the context of jinja templates. Especially this entry.

With jinja, I believe you can:

{% for v in videos %}
  <div class="video">
    {% with video=v email=user.email %}
      {% include "video.html"%}
    {% endwith %}
  </div>
{% endfor %}

I think you can too. But to me, it's less clear that the variables are part of video.html, not to be used locally. I find the render_partial call to communicate intent more and use one fewer lines of code. But thanks for the example.

gnat commented

For what it's worth, the custom function tends to be about ~5-10% more performant compared to jinja include in my testing (with had no effect on speed), although this may change with Python 3.11

Here's the minimum implementation (Starlette / FastAPI) so people don't have to hunt for the magical lines:

def render_partial(name: str, **context) -> str:
	return templates.get_template(name).render(**context)

# Wherever you instantiate Jinja...
templates = Jinja2Templates('app/templates')
templates.env.globals['render_partial'] = render_partial

Usage example in your templates:

{{ render_partial("date_selector.html", label='Date of Birth') }}

Nice information and details, thanks a bunch @gnat

s2t2 commented

@mikeckennedy this package is great. thanks! โœจ

mkrd commented

I just found https://flask.palletsprojects.com/en/2.2.x/api/#flask.get_template_attribute

The name is a bit obscure, and it returns the macro as a python function, but it should do the same, with the added benefit of being able to leave the "partial" in the file it is used, and if you render it separately, then you use get_template_attribute.

One could easily write a one-liner

def render_macro(template_name, macro_name, **kwargs):
    return get_template_attribute(template_name, macro_name)(**kwargs)

Or am I totally missing something here?

Hey @mkrd, what's missing is the fact that you want to render the content that is the macro as an individual page:

If you have:

main page content
  -- macro1
  -- macro2

And need to be able to return both "main page content" and "macro1" content directly as an HTML response, you need to create a third "macro holder page" which is silly. That's why macros are the answer here.

Hey @mkrd, what's missing is the fact that you want to render the content that is the macro as an individual page:

If you have:

main page content
  -- macro1
  -- macro2

And need to be able to return both "main page content" and "macro1" content directly as an HTML response, you need to create a third "macro holder page" which is silly. That's why macros are the answer here.

Look at @mkrd 's code again. You can totally do this with get_template_attribute. I use it for this exact reason and it allows the macro to be a macro, as well rendered as an independent response. It also receives the variables just like your partials do. It's functionally the same as @gnat 's custom render function, except it maintains classic macro functionality in addition to the partial behavior. It's appears to be the most flexible and complete solution.

EDIT: Perhaps the use of template_name in the get get_template_attribute function call is misleading. That's actually the path to the file that contains the macro. The macro_name variable is the name of the macro defined in that file.

For what it's worth, the custom function tends to be about ~5-10% more performant compared to jinja include in my testing (with had no effect on speed), although this may change with Python 3.11

Here's the minimum implementation (Starlette / FastAPI) so people don't have to hunt for the magical lines:

def render_partial(name: str, **context) -> str:
	return templates.get_template(name).render(**context)

# Wherever you instantiate Jinja...
templates = Jinja2Templates('app/templates')
templates.env.globals['render_partial'] = render_partial

Usage example in your templates:

{{ render_partial("date_selector.html", label='Date of Birth') }}

@gnat Did >= 3.11 make either approach more performant?

mkrd commented

Hey! I revisited the render_macro function, and here is the complete working version:

from flask import current_app
from jinja2 import Template
from jinja2.nodes import Macro, Name, TemplateData
from markupsafe import Markup


def _flatten_macro_source_nodes(nodes: list) -> str:
	res = ""
	for node in nodes:
		if isinstance(node, TemplateData):
			res += node.data
		elif isinstance(node, Name):
			res += f"{{{{ {node.name} }}}}"
		else:
			msg = f"Unsupported node type: {type(node)}"
			raise TypeError(msg)
	return res


def render_macro(template_name: str, macro_name: str, **kwargs: dict) -> str:
	env = current_app.jinja_env

	template_source, _, _ = env.loader.get_source(env, template_name)
	macros = env.parse(template_source).find_all(Macro)

	if not (macro := next((m for m in macros if m.name == macro_name), None)):
		msg = f"Macro {macro_name} not found in template {template_name}"
		raise ValueError(msg)

	macro_source = _flatten_macro_source_nodes(macro.body[0].nodes)
	macro_args = ",".join(a.name for a in macro.args if a.ctx == "param")
	macro_definition = f"{{% macro {macro_name}({macro_args}) %}}{macro_source}{{% endmacro %}}"
	macro_call = f"{{{{ {macro_name}({', '.join(kwargs.keys())}) }}}}"

	rendered = Template(macro_definition + macro_call).render(**kwargs, g=env.globals)
	return Markup(rendered)

A bit more involved than a one liner, but it works and properly isolates the macro from the rest of the template.
With a bit more effort, one could generalize it so that it is not flask specific.

Hi,

Does render_partial work with Pyramid/Jinja? I'm assuming it would but wanted to know before I tried it.

Also, most of my previous programming is with Blazor and I'm new to the Python world. The render_partial looks like it could be synonymous with Blazor components. In Blazor everything is a component and you can pass data to and from them. You can also pass from child components back to parent components and pass events and views between child components.

Is that possible with render_partial?

Thanks
Dave

Hi @TrailBlazor Blazor is a really cool technology. The origins for me actually come from Razor and C#. They had a really great way to do that in ASP.NET so I took some inspiration from there.

They can be composed in the sense that partials can themselves call render_partial, you just have to pass the needed data.

Thanks for the response @mikeckennedy . I might try using them in our Pyramid environment.

Sounds good @TrailBlazor I'm using them in Pyramid (the chameleon version) on Talk Python & TP Training.