Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flask: Template in Blueprint Inherit from Template in App?

I'm a total Flask/Jinja2 newbie, so maybe I'm overlooking something obvious, but:

Shouldn't Flask, out of the box, allow a template that exists in a blueprint's templates/ folder to extend a base template defined by my app's templates/ folder? Shouldn't this work even if the blueprint also includes a "default" base template, which I override by defining my own base template of the same name?

The answer to this other SO question makes me think that both of those things should absolutely be the case. Specifically the part of the answer that says:

If there are two templates with same name[, one] in app's templates folder and [the other in] blueprint's template folder, then template in app's templates folder will get priority.

But it's not working that way at all for me. In fact, it seems to work the opposite way, i.e., the base.html from the blueprint is being pulled in by pages defined in my app, even though my app defines its own base.html (which should "get priority" if the above answer is correct).

In my app I have:

myapp/
   templates/
       base.html
       pages/
           page_base.html
           home_page.html

where pages/home_page extends pages/page_base, which in turn extends base.

I'm also using the flask_user package from PyPI, which was installed (by pip) at /usr/local/lib/python2.7/dist-packages/flask_user/. Its templates folder is arranged as follows:

flask_user/
    templates/
        base.html
        flask_user/
            [templates that extend base.html]

This package makes its templates available to applications that use it via a Blueprint which it establishes with the following calls in the init_app function of its UserManager class (__init__.py, line 154):

    # Add flask_user/templates directory using a Blueprint                  
    blueprint = Blueprint('flask_user', 'flask_user', template_folder='templates')
    app.register_blueprint(blueprint)

My initial thinking was that by defining my own myapp/templates/base.html I'd be able to customize pages rendered from templates in flask_user/templates/flask_user/ to look like other pages in my app, because (per the referenced answer) my base.html should take precedence over flask_user's base.html.

But that's not working, and what's worse -- and much more surprising -- is that my app's pages are being given the default look of flask_user's pages.

Digging Deeper...

Looking into @Pralhad Narsinh Sonar's suggestion that there may be a problem with the ordering of the template search paths, possibly caused by non-deterministic behavior of DispatchingJinjaLoader._iter_loaders() as suggested in the fewstreet.com article he cited, I did a quick experiment to see what ordering _iter_loaders() would produce for my app:

>>> from myapp.top import app, db
>>> from myapp.startup import init_app.init_app
>>> init_app(app, db)
>>> app.jinja_env.loader
<flask.templating.DispatchingJinjaLoader object at 0x7f233e396dd0>
>>> for loader in app.jinja_env.loader._iter_loaders('pages/home_page.html') :
...   print loader, loader.searchpath
... 
<jinja2.loaders.FileSystemLoader object at 0x7f233eb08490> ['/var/www/python/myapp/templates']
<jinja2.loaders.FileSystemLoader object at 0x7f233e36ef10> ['/usr/local/lib/python2.7/dist-packages/flask_user/templates']

As expected, the iterator yields the loader for my app's templates/ folder first, before yielding the loader for flask_user/templates/. In fact, the _iter_loaders() function is quite deliberately structured to return the app's loader before returning any Blueprints' loaders. (If I'm reading the fewstreet.com article correctly, the problem it's concerned with is non-deterministic ordering among multiple Blueprints, which -- since there's only one Blueprint being used by my app -- isn't my current problem.)

This result makes it even harder for me to understand why flask_user's base.html template is being used to resolve my template's {% extends "base.html" %} statement*. Given that I have my own base.html file in myapp/templates, I see no reason whatsoever for the templating system to look at anything in flask_user/templates to render myapp/templates/pages/home_page.html.

* For testing purposes I got rid of the indirection through pages/page_base.html mentioned above.

So: Obviously something else is going wrong, but what?

I haven't yet grokked enough of the relevant code in flask/templating.py or jinja2/loaders.py to understand why and how this might be happening. This being my first foray into Flask, I would have hoped I wouldn't need to.

like image 917
Hephaestus Avatar asked Oct 19 '22 08:10

Hephaestus


1 Answers

And the answer is:

All this time I've been running (and reloading) my app with debug=True.

Which is great for automatically reloading changed Python modules.

But for changed templates? Um... not so much.

After introducing a breakpoint into my home_page.html template and using the Flask debugger to look back through a few stack frames, I discovered that Jinja2 makes use of an LRU cache to store (by name) templates it has already parsed.

Because I had hatched the idea of creating my own base.html template after having already loaded a flask_user page (login.html), which had originally inherited from flask_user/templates/base.html, there was already a template named base.html in the cache by the time I introduced myapp/templates/base.html.

So I stopped and restarted the app, and now both my home_page.html and flask_user's login.html are correctly inheriting from my base.html instead of from flask_user's base.html. I suspect that before I power-cycled the app, my own base.html had never even been read by my app's template loader.

This is a fairly significant -- and, I believe, undocumented -- gotcha for a newbie to have to figure out. I'll just leave this here, in hopes that it someday helps someone else who happens to step into this particular trap.

like image 193
Hephaestus Avatar answered Oct 21 '22 23:10

Hephaestus