2. This package has two kinds of global state. The first one are global attributes defined at module level of template package. These are base.builtins, base.libraries, loader.template_source_loaders and base.templatetags_modules. The second one are dependencies on settings and some other module-level attributes in template package like base.invalid_var_format_string (this is cache; it's a flag which means "is there '%s' in settings.TEMPLATE_STRING_IF_INVALID?") or context._standard_context_processors, which is cache set in context.get_standard_processors function which transform data from settings in some way. I decided to focus on the first kind of global state at first.
5. The one issue is that the engine is needed everywhere. For example, consider creating base.Template instance. At the time of creation the template source is being compiled. The template may load a library, so the compilation cannot be done without list of all libraries defined in the engine. To sum up, we cannot just type Template(source), because this depends on template engine. I solved this issue by passing engine instance to Template constructor. In order not to break everything, I renamed Template class to _Template and created function called Template that does "return _Template(default_engine, **kwargs)".
9. Now, there is another issue. Changing API. It's obvious that changing base.Parser signature is OK since it's an internal structure. It's also easy to notice that base.Template is used everywhere so we cannot change API of Template. But there are many places where it's unclear if a method or class is public (it means that we promise we won't change its signature) or internal. For example, I don't know how much we can change loaders. I don't know if Django developers types "isinstance(obj, Template)" -- it won't work know. I don't know if I can change signature of SsiNode as I did. By the way, I think that we should make clear distinction between these two methods by using underscore convention.
11. A small issue is test regressiontests.views.tests.debug.DebugViewTests.test_template_loader_postmortem. The problem is in method django.views.debug.ExceptionReporter.get_traceback_data. It uses template_source_loaders global state which was removed. We can just use default_engine. I wonder when a Django developer uses a custom template engine instead of the global default one then they will get faulty error message. Is it important or negligible?
14. Is this plan good? What issues do you see? Could you look at my work and review it (I wonder especially if I'm using a python feature not supported by python version supported by Django?)
Hello everybody.
1. I'm working on ticket #17093 [1]. You can see progress of work at github [2]. It's about rewritting django.template so it won't use global state. I wrote this post to tell you what I did so far, what issues I deal with and to discuss them. I hope that we will gain valuable experience in splitting Django into smaller parts.
3. The goal is to write TemplateEngine class to encapsulate global state. For backward-compatibility, there will be one global instance of TemplateEngine and all functions like loader.get_template should delegate to the instance. Tests need to be rewritten.
4. What I did so far? I created base.TemplateEngine class and base.default_engine global instance. Some functions (mostly from loader module: get_template, find_template, select_templates and from base module: add_to_builtins, get_library, compile_string) delegates to appropriate methods of default_engine. I also partially rewrote tests (only tests/regressiontests/templates/tests.py) so they don't use default_engine directly.
5. The one issue is that the engine is needed everywhere. For example, consider creating base.Template instance. At the time of creation the template source is being compiled. The template may load a library, so the compilation cannot be done without list of all libraries defined in the engine. To sum up, we cannot just type Template(source), because this depends on template engine. I solved this issue by passing engine instance to Template constructor. In order not to break everything, I renamed Template class to _Template and created function called Template that does "return _Template(default_engine, **kwargs)".
10. As I said I'm trying to rewrite tests from tests.py file so they won't use the default engine. There left some sources of dependency on default engine. One of them are include tags in tests which load templates at the time of importing module [3] (or the source of problems is in base.Library.inclusion_tag?). Another one are template.response module [4] and all other test files (i. e. callables.py, custom.py). I didn't dig into these issues deeply yet since I wanted to publish my results and take some feedback.
11. A small issue is test regressiontests.views.tests.debug.DebugViewTests.test_template_loader_postmortem. The problem is in method django.views.debug.ExceptionReporter.get_traceback_data. It uses template_source_loaders global state which was removed. We can just use default_engine. I wonder when a Django developer uses a custom template engine instead of the global default one then they will get faulty error message. Is it important or negligible?
12. As I said there is another source of global-state -- django settings. I think we can deal with that by passing all necessary settings (like TEMPLATE_DEBUG, INSTALLED_APPS etc.) to TemplateEngine constructor. It means that we won't be able to create the default engine at the module-level since settings are not configured at the time of importing template package. We will have to make instancing default engine lazy. It means that there should be get_default_engine() function which creates the default engine at the very first call. I wonder if we will fall into circular imports hell or other problems...
I don't want to discourage you from working on the project if you find
it interesting and informative, and it may well be that the results are
worth the effort and end up being merged into Django; but it is also
possible that we look at the patch and say "sorry, that's just too much
disruption for the benefits" - so you should be prepared for that
possibility.
I really don't have a good sense of where the other core
devs stand on how much this type of deep refactoring is worth.
> 14. Is this plan good? What issues do you see? Could you look at my work
> and review it (I wonder especially if I'm using a python feature not
> supported by python version supported by Django?).
Best way to notice this is to fix up the test suite into a running
state, and run it with the supported Python versions (minimum is 2.6
currently, the more likely area for problems is Python 3, since Django
master now supports Python 3).
> 7. Then there is the issue of loaders. At the first, I thought that
> passing the engine will be not necessary if I change behaviour of
> loader.BaseLoader.load_template method so it will always return source
> of template instead of Template instance. So I did it for BaseLoader.
> Few days ago I did the same for loaders.cached.Loader, but I realised
> that this change means no caching at all -- the loader cached compiled
> templates. Now it caches nothing. The current architecture let us to
> cache template sources but not compiled templates.
>
> 8. There are two solutions. First, we can just pass the engine to every
> loader. Second, we can remove cache-loader and implement caching inside
> TemplateEngine. Both solutions are ugly. Do you see any better one?
I think loaders will need to have access to the template engine that is
using them (though for backwards-compatibility they may need to default
to default_engine if not given another engine).
> 3. The goal is to write TemplateEngine class to encapsulate global state. For backward-compatibility, there will be one global instance of TemplateEngine and all functions like loader.get_template should delegate to the instance. Tests need to be rewritten.
A global instance is probably unavoidable, but by itself doesn't result in less global state - it just wraps it all in one global object. Functions that need Engine state should be passed an engine instance explicitly wherever possible - not importing default_engine at module level.
The shim point for all of these IMO should be your get_default_engine function - and although the following suggestion requires some more thought/discussion - the default_engine instance might as well live on the settings object, where the setting itself could be the default engine class. Killing the global state doesn't mean killing the state - but it is a process of gathering it from all the scattered places, and consolidating it into piles, then gather those piles into one (or very few) pile and creating that only in the entry point(s) so that it is in fact local state. If there is a candidate for the one pile, it is settings. But I'm waxing now ;-)
> 2. This package has two kinds of global state. The first one are global attributes defined at module level of template package. These are base.builtins, base.libraries, loader.template_source_loaders and base.templatetags_modules. The second one are dependencies on settings and some other module-level attributes in template package like base.invalid_var_format_string (this is cache; it's a flag which means "is there '%s' in settings.TEMPLATE_STRING_IF_INVALID?") or context._standard_context_processors, which is cache set in context.get_standard_processors function which transform data from settings in some way. I decided to focus on the first kind of global state at first.
You've listed every cache that depends on a setting. It would be interesting to add a listener for the setting_changed signal to reset these caches when appropriate. This is only used in tests; one isn't supposed to change settings at runtime.
If I understand correctly, once your work is complete, you could listen to changes to a bunch of settings, and just re-instanciate the default template engine when any of them is changed.
> 10. As I said I'm trying to rewrite tests from tests.py file so they
> won't use the default engine. There left some sources of dependency on
> default engine. One of them are include tags in tests which load
> templates at the time of importing module [3] (or the source of problems
> is in base.Library.inclusion_tag?). Another one are template.response
> module [4] and all other test files (i. e. callables.py, custom.py). I
> didn't dig into these issues deeply yet since I wanted to publish my
> results and take some feedback.
For the first case, I would say those tests should simply be changed to
not load templates when the module is imported.
As for TemplateResponse, it would need to use default_engine, though I
suppose it could sprout an additional instantiation parameter to accept
an alternative engine.
> 5. The one issue is that the engine is needed everywhere. For example, consider creating base.Template instance. At the time of creation the template source is being compiled. The template may load a library, so the compilation cannot be done without list of all libraries defined in the engine. To sum up, we cannot just type Template(source), because this depends on template engine. I solved this issue by passing engine instance to Template constructor. In order not to break everything, I renamed Template class to _Template and created function called Template that does "return _Template(default_engine, **kwargs)".
Maybe a stupid question — couldn't you add an optional 'default_engine' keyword argument to the Template class, and if it isn't provided, use the default template engine?
> 13. At the end I would like to split the template package into new
> django-independent package (called i. e. 'template_engine') and
> django-specific package (named 'templates') which defines default_engine
> and all django-specific tags and filters. By django-independent I mean
> not depending on settings neigher other django modules excluding utils.
> I won't split tests in the same way - I think there is no need for that.
This makes sense in terms of code hygiene, though if you're thinking of
the "template_engine" as something that is likely to see significant use
outside of Django, my feeling is that the existence of Jinja2 makes this
unlikely.
Note: by "settings", I don't necessarily mean settings.py, just template settings, even of that means instantiating a template system on the fly.