A method that yields all paths the loader will look at for a given template
name. For example, if the filesystem loader receives "index.html" as an
argument, this yields the full path of "index.html" for each directory on the
search path. The cached loader yields tuples of (loader_class, template_paths)
for each configured loader.
On 28 déc. 2014, at 06:30, Preston Timmons <preston...@gmail.com> wrote:## 1. Add new template loader apis
I tried to solve this patch without changing the template loader apis, but Ieventually decided this was inevitable for two reasons:1. The loaders don't implement a consistent api. For instance, thefilesystem and app loaders define a get_template_source method that isused elsewhere for debug information, whereas the egg and cached loaders donot. The cached loader implements load_template but not load_template_source.
2. The loaders need to be able to skip templates that have already beenextended, but adding a new argument to load_template is difficult to do in abackwards-compatible way.
This led me to the following loader api:### Loader APILoader.get_template_sourcesA method that yields all paths the loader will look at for a given template
name. For example, if the filesystem loader receives "index.html" as an
argument, this yields the full path of "index.html" for each directory on the
search path. The cached loader yields tuples of (loader_class, template_paths)
for each configured loader.This method already exists for the filesystem and app loaders. This patchimplements it for all loaders.
Loader.get_internalReturns the contents for a template given a ``source`` as returned by``get_template_sources``.This is where a filesystem loader would read contents from the filesystem,or a database loader would read from the database. If a matching templatedoesn't exist this should raise a ``TemplateDoesNotExist`` error.
BaseLoader.get_templateReturns a ``Template`` object for a given ``template_name`` by loopingthrough results from ``get_template_sources`` and calling ``get_internal``.This returns the first matching template. If no template is found,``TemplateSyntaxError`` is raised.This method takes an optional ``skip`` argument. ``skip`` is a setcontaining template sources. This is used when extending templates toallow enable extending other templates of the same name. It's also usedto avoid recursion errors.In general, it will be enough to define ``get_template_sources`` and``get_internal`` for custom template loaders. ``get_template`` willusually not need to be overridden.
## 2. Update the extends tagThe new extends tag keeps track of which sources were tried in the localcontext. These sources are passed to the loader ``get_template`` method asthe ``skip`` argument. In doing so, the extends tag never extends the samesource file twice. This enables recursive extending, and also avoidsfilesystem recursion errors when extending is truly circular.The main caveat here is that I changed Template.origin to always be availableon the template--not just when TEMPLATE_DEBUG is true. Without this, theextends tag has no way to know the source path of the current template.I think this change is okay, but I don't know the original reasons forexcluding this in the first place. It could be simply because there was nouse-case presented outside of debug.
## 3. Debug apiDjango displays a template postmortem message when a matching template isn'tfound. This is a linear list grouped by template loader. This api is currentlysupported only by the filesystem and app directories loaders.
(…)
### The approachRather than trying to rerun template engines or loaders, this patch passes indebug information as part of ``TemplateDoesNotExist``. This is done by updatingthe multiple places where Django catches ``TemplateDoesNotExist`` exceptionsand silently throws them away. Instead of simply silencing them, this patchaccumulates which templates failed and includes them in subsequent``TemplateDoesNotExist`` exceptions that are raised.This information is kept as a list of tuples saved to a ``tried`` attribute ofthe exception.
Here's a somewhat complex example of this attribute when using multipleloaders. It extends through multiple loaders, but fails when no more matchingtemplates exist:[# Note: (loader, path, status)(filesystem.Loader, "/path/to/fs/base.html", "Found")(filesystem.Loader, "/path/to/fs/base.html", "Skipped")(app_directories.Loader, "/path/to/app/base.html", "Found")(filesystem.Loader, "/path/to/fs/base.html", "Skipped")(filesystem.Loader, "/path/to/app/base.html", "Skipped")(app_directories.Loader, "/path/to/app2/base.html", "Source does not exist")]
### Multiple engine supportI'll publish an updated pull request once Aymeric's multiple template enginesbranch lands. At that time, I think the ``tried`` attribute will change tothis:[# (loader, path, status)(engine, filesystem.Loader, "/path/to/fs/base.html", "Found")(engine, filesystem.Loader, "/path/to/fs/base.html", "Skipped")(engine, app_directories.Loader, "/path/to/app/base.html", "Found")(engine, filesystem.Loader, "/path/to/fs/base.html", "Skipped")(engine, filesystem.Loader, "/path/to/app/base.html", "Skipped")(engine, app_directories.Loader, "/path/to/app2/base.html", "Source does not exist")]
## ConclusionOverall, I'm pretty happy with this new api. I have a few concerns though,which are as follows:1. This patch deprecates the old loader apis: Loader.load_template,Loader.load_template_sources, etc. I think this is good, but there's alreadya lot of deprecations happening with the multiple engine branch.
2. The origin api needs a design decision. I made that available regardless ofthe debug setting. Also, it sounds like Aymeric may be doing some work torefactor this api.
3. The debug api is also something Aymeric is scheduled to work on. Aymeric,if you've got something better in mind I've no problem waiting for that to bein place.
Even if it isn’t consistently implemented, the loader API is documented:
https://docs.djangoproject.com/en/1.7/ref/templates/api/#using-an-alternative-template-language
To sum up:
load_template_source(template_name) —> (template_string, template_origin)
load_template(template_name) —> (template, template_origin)
The behavior is loosely defined; anything that matches the signature is fine.
template_origin is especially ill-defined.
Are you proposing to change the API outright? Or are you planning to
implement a deprecation path for existing third-party template loaders?
How would a loader that loads templates from a database implement this
method?
It seems to me that the proper way to define such a method is “yields some
opaque objects that implement the following API: …” I suspect that API
would be quite similar to the Origin API — maybe an idealized version if
the current version doesn’t suffice.
Attaching this method to the objects yielded by get_template_sources would
provide better encapsulation than attaching it to the Loader itself.
At first sight adding a “skip” optional argument to Engine.get_template,
Engine.find_template, BaseLoader.__call__ and BaseLoader.load_template
may work. Could you explain where the difficulty lies?
Your design makes sense. My main suggestion is to fit it in the current APIs.
I would try to reuse load_template instead of introducing get_template and to
hook to a revamped Origin API. The Origin API was documented in Django
1.7; changing it it less disruptive than changing the Loader API. Did you try
this, and if you did, why didn’t it work?
This API is still tied to the Django Template Language. I would prefer a generic
list of (“template identifier”, “loading mechanism”, “reason for failure”) that every
backend could implement.
Furthermore, I’m not sure what this list represents. Is it a “template call stack”
i.e. the chain of templates included up to the point where loading a template
failed? Or is it a list of ways the template backend tried to load the failing
template before giving up?
I agree with making it always available. The real question is about the API itself.
We documented the existing concrete implementations in Django 1.7 but they’re
inconsistent and their purpose is still unclear.
https://docs.djangoproject.com/en/1.7/ref/templates/api/# template-origin
I was mainly thinking about providing tracebacks for errors in templates. Jinja2
has fancy integration with Python’s traceback system. Django emulates it in its
debug view with exc.django_template_source and get_template_exception_info.
That’s independent from template loading exceptions.
Did that explain it better? If not, I can try again. :)
The debugging information is two dimensional data: for each level of
template extending, for each template loader, you have one entry —
entries are 4-tuples in your current proposal.
At this point, I think we should talk about template origins. What
is their purpose? What is the complete abstract Origin API? The
current API only includes a `name` attribute and an unspecified
`reload()` method; I don't think that's sufficient.
As a quick recap:First, this branch uses origins everywhere: for extends history, forTemplateDoesNotExist errors, etc. There's no more 4-tuple of statuses.
Second, I use these attributes on the origin:engineloaderloadname -> template namename -> full template pathstatus -> Skipped, Found, Source does not existtried -> a list of origins tried up to this onereload -> gets template contents__eq____hash__ -> added so the cache loader can use origin as a cache key
Third, loaders implement these methods:get_template_sources -> yields originsget_internal -> takes an origin and returns the template contents
Fourth, TemplateDoesNotExist takes an optional tried argument which is alsoa list of origins. This will be used in the debug views. I didn't change thedebug views in this branch yet, though.
QuestionsOrigin attributesI'd like to do some renaming:origin.loadname -> origin.template_nameI think it's hard to remember the difference between origin.name andorigin.loadname. I think "template_name" is more conventional.
origin.reload -> origin.readIf the method to read a template contents lives on the origin, I chose toprefer using the origin method rather than the loader method directly. There'snot much consequence here, but it means it's no longer a special method justfor the debug view.
tried and statusThe nice thing about putting the tried and status on origin are that it workswell with a recursive api. It even means I could get rid of the extra contextvariable used to store extends history. The sucky things are that tried andstatus are set and manipulated outside the origin constructor. The cachedloader also has to be sure to reset the origin before returning the cachedtemplate. That might be warranted, but part of me wonders if it's lousy api.
Deprecation pathIf we want to maintain the load_template api, I can change it to accept theskip argument. Within load_template I can add a hasattr check for"get_internal". If it exists, use the new code. Otherwise, use the old"load_template_sources" method.Since adding a kwarg to load_template could potentially break 3rd-partyloaders that override it, I could add an _accepts_skip_kwarg property tobase.Loader like you've done with _accepts_engine_in_init = True. Thiswould enable Engine.find_template to exclude that if necessary.If we do this, I'd like to deprecate the (template, template_origin)return value in favor of just template. Returning a tuple is redundant.Ditto for Engine.find_template.If we use get_template instead, we don't need to deprecate the return value andwe don't need _accepts_skip_kwargs. Instead we can just recommend use of thenew method until load_template is removed.Either way, load_template_sources is obsoleted.Which deprecation path do you think is preferable?
ConclusionIf the api so far makes sense, I'll update the debug view post-mortemmessages and add in the docs and deprecation warnings.
Perhaps we’ll bikeshed names at some point but let’s leave the “naming
things” problem aside for now.
“name” must be optional for loaders that don’t load templates from files.
“loadname” must be optional too for templates instantiated from strings.
Here optional means that things don’t break if the value is None.
I don’t like the code duplication between get_internal and
load_template_source. I assume your design ended up that way because
you needed get_internal and load_template_sources will be deprecated.
Is this correct?
TemplateDoesNotExist is used both internally by the DTL and in generic
APIs such as django.template.loader.get_template. If it’s designed to
accept a list of tried origins, then we need:
- a generic Origin without engine, loader, status and tried (at least)
- a DTL Origin as described above.
Does this make sense?
Yes. Maintaining compatibility with LoaderOrigin as documented in 1.7
appears to be a lost cause anyway.
I think the appropriate way for origins to return template code would be to
call back into the template loaders. Reading template contents in the job
of the loader. It should be possible to write a single origin class that works
for most loaders.
The sole purpose of the reload() method is to display tracebacks in templates.
This logic should be moved out of the debug view and into the template
backends. See #24119. As a consequence, we should leave it outside of this
refactoring and simply preserve it for DTL origins until that ticket gets fixed.
If you’re going to change both its arguments and its return type, you can change
the method's name :-) The second solution looks better to me. Hopefully we can
write a “just do this” guide to help people maintaining custom template loaders
upgrading.
Thanks for your continued work on this and sorry for my slow answers.
> Does that makes sense, or should I look for a way to incorporate that
> into LoaderOrigin?
Looking at the implementation, it feels weird to add attributes to the origin
that are only used by the loader… If origins were dumb bags of data this
would be all right but they have a reload() method that calls the loader.
Such mutual dependencies never end well :-/ That’s a broader design
problem not directly related to your refactoring, but while you’re breaking
all the things, perhaps we could fix it too.
By the way, I would like to check if we can deprecate the dirs argument of
LoaderOrigin. It may only be used by deprecated code as of Django 1.8.
In fact context.template.engine would be much better than context.engine. I’ll try
to fix that in 1.8 before it’s too late.
Yes, I do.
This is an independent follow-up to the big refactoring we’re talking about, isn’t it?
It depends. The cached loader is what I'm least happy with in my refactoring. Thisinternal cache idea I think is simpler, more performant, and easier to understand.Adding it makes the cached loader changes unnecessary.
In that case, would it be possible to do the caching improvements before the mainrefactoring? I'm trying to keep patches at a reviewable size :-)--Aymeric.