-
-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why not just use {% include ... %} ? #1
Comments
Hi @davetapley. Good question. I created this because I didn't see how to accomplish that with this:
With the statement:
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. ⬇️ 😁
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 It might be worth putting something at the top of the README to let Rails folk like me know that |
It's fairly clear to me why you would not want to use an include in this situation, but why not a macro?
Your main template can call the macro:
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 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:
To me, that seems silly, when I can use this library to get the inside a page reuse and then just You can find a concrete example of this here: |
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:
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 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 %}
{% 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 :
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. |
For what it's worth, the custom function tends to be about ~5-10% more performant compared to jinja 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:
|
Nice information and details, thanks a bunch @gnat |
@mikeckennedy this package is great. thanks! ✨ |
Glad you like it Mike!
|
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
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:
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 EDIT: Perhaps the use of |
@gnat Did >= 3.11 make either approach more performant? |
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. |
Hi, Does Also, most of my previous programming is with Is that possible with Thanks |
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 |
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. |
Can you comment on why this is better than using
{% include ... %}
?https://jinja.palletsprojects.com/en/3.0.x/templates/#include
The text was updated successfully, but these errors were encountered: