Skip to content

Commit 48ec10d

Browse files
Allow user-defined template engine configuration via conf.py.
Also some improvements to make the template handling code better and more readable; mostly type hints.
1 parent 172acaa commit 48ec10d

13 files changed

+168
-90
lines changed

CHANGES.txt

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Features
66

77
* Trace template usage when an environment variable ``NIKOLA_TEMPLATES_TRACE``
88
is set to any non-empty value.
9+
* Give user control over the raw underlying template engine
10+
(either `mako.lookup.TemplateLookup` or `jinja2.Environment`)
11+
via an optional `conf.py` method `TEMPLATE_ENGINE_FACTORY`.
912

1013
Bugfixes
1114
--------

CONTRIBUTING.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ Here are some guidelines about how you can contribute to Nikola:
8686

8787
.. [1] Very inspired by `fabric’s <https://github.com/fabric/fabric/blob/master/CONTRIBUTING.rst>`_ — thanks!
8888
89-
.. [2] For example, logging or always making sure directories are created using ``utils.makedirs()``.
89+
.. [2] For example, logging, or always making sure directories are created using ``utils.makedirs()``.

docs/theming.rst

+33-3
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,17 @@ The following keys are currently supported:
130130

131131
The parent is so you don’t have to create a full theme each time: just
132132
create an empty theme, set the parent, and add the bits you want modified.
133-
It is strongly recommended you define a parent. If you don't, many features
134-
won’t work due to missing templates, messages, and assets until your home-grown
135-
template is complete.
133+
134+
While it is possible to create a theme without a parent, it is
135+
**strongly discouraged** and not officially supported, in the sense:
136+
We won't help with issues that are caused by a theme being parentless,
137+
and we won't guarantee that it will always work with new Nikola versions.
138+
The `base` and `base-jinja` themes provide assets, messages, and generic templates
139+
that Nikola expects to be able to use in all sites. That said, if you are making
140+
something very custom, Nikola will not prevent the creation of a theme
141+
without `base`, but you will need to manually determine which templates and
142+
messages are required in your theme. (Initially setting the ``NIKOLA_TEMPLATES_TRACE``
143+
environment variable might be of some help, see below.)
136144

137145
The following settings are recommended:
138146

@@ -484,6 +492,28 @@ at https://www.transifex.com/projects/p/nikola/
484492
If you want to create a theme that has new strings, and you want those strings to be translatable,
485493
then your theme will need a custom ``messages`` folder.
486494

495+
Configuration of the raw template engine
496+
----------------------------------------
497+
498+
For usage not covered by the above, you can define a method
499+
`TEMPLATE_ENGINE_FACTORY` in `conf.py` that constructs the raw
500+
underlying templating engine. That `raw_engine` that your method
501+
needs to return is either a `jinja2.Environment` or a
502+
`mako.loopkup.TemplateLookup` object. Your factory method is
503+
called with the same arguments as is the pertinent `__init__`.
504+
505+
E.g., to configure `jinja2` to bark and error out on missing values,
506+
instead of silently continuing with empty content, you might do this:
507+
508+
.. code:: python
509+
510+
# Somewhere in conf.py:
511+
def TEMPLATE_ENGINE_FACTORY(**args) -> jinja2.Environment:
512+
augmented_args = dict(args)
513+
augmented_args['undefined'] = jinja2.StrictUndefined
514+
return jinja2.Environment(**augmented_args)
515+
516+
487517
`LESS <http://lesscss.org/>`__ and `Sass <https://sass-lang.com/>`__
488518
--------------------------------------------------------------------
489519

nikola/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@
3131

3232
# The current Nikola version:
3333
__version__ = '8.3.1'
34-
# A flag whether logging should emmit debug information:
34+
# A flag whether logging should emit debug information:
3535
DEBUG = bool(os.getenv('NIKOLA_DEBUG'))
3636
# A flag whether special templates trace logging should be generated:
3737
TEMPLATES_TRACE = bool(os.getenv('NIKOLA_TEMPLATES_TRACE'))
3838
# When this flag is set, fewer exceptions are handled internally;
3939
# instead they are left unhandled for the run time system to deal with them,
4040
# which typically leads to the stack traces being exposed.
41+
# This is a less noisy alternative to the NIKOLA_DEBUG flag.
4142
SHOW_TRACEBACKS = bool(os.getenv('NIKOLA_SHOW_TRACEBACKS'))
4243

4344
if sys.version_info[0] == 2:

nikola/conf.py.in

+14
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,20 @@ WARN_ABOUT_TAG_METADATA = False
13191319
# those.
13201320
# TEMPLATE_FILTERS = {}
13211321

1322+
# If you want to, you can augment or change Nikola's configuration
1323+
# of the underlying template engine used
1324+
# in any way you please, by defining this function:
1325+
# def TEMPLATE_ENGINE_FACTORY(**args):
1326+
# pass
1327+
# This should return either a jinja2.Environment or a mako.lookup.TemplateLookup
1328+
# object that have been configured with the args received plus any additional configuration wanted.
1329+
#
1330+
# E.g., to configure Jinja2 to bark on non-existing values instead of silently omitting:
1331+
# def TEMPLATE_ENGINE_FACTORY(**args) -> jinja2.Environment:
1332+
# augmented_args = dict(args)
1333+
# augmented_args['undefined'] = jinja2.StrictUndefined
1334+
# return jinja2.Environment(**augmented_args)
1335+
13221336
# Put in global_context things you want available on all your templates.
13231337
# It can be anything, data, functions, modules, etc.
13241338
GLOBAL_CONTEXT = {}

nikola/log.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,7 @@ def configure_logging(logging_mode: LoggingMode = LoggingMode.NORMAL) -> None:
105105
return
106106

107107
handler = logging.StreamHandler()
108-
handler.setFormatter(
109-
ColorfulFormatter(
110-
fmt=_LOGGING_FMT,
111-
datefmt=_LOGGING_DATEFMT,
112-
)
113-
)
108+
handler.setFormatter(ColorfulFormatter(fmt=_LOGGING_FMT, datefmt=_LOGGING_DATEFMT))
114109

115110
handlers = [handler]
116111
if logging_mode == LoggingMode.STRICT:
@@ -152,7 +147,7 @@ def init_template_trace_logging(filename: str) -> None:
152147
153148
As there is lots of other stuff happening on the normal output stream,
154149
this info is also written to a log file.
155-
"""
150+
"""
156151
TEMPLATES_LOGGER.level = logging.DEBUG
157152
formatter = logging.Formatter(
158153
fmt=_LOGGING_FMT,

nikola/nikola.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import pathlib
3737
import sys
3838
import typing
39+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
3940
import mimetypes
4041
from collections import defaultdict
4142
from copy import copy
@@ -373,7 +374,7 @@ class Nikola(object):
373374
plugin_manager: PluginManager
374375
_template_system: TemplateSystem
375376

376-
def __init__(self, **config):
377+
def __init__(self, **config) -> None:
377378
"""Initialize proper environment for running tasks."""
378379
# Register our own path handlers
379380
self.path_handlers = {
@@ -395,7 +396,7 @@ def __init__(self, **config):
395396
self.timeline = []
396397
self.pages = []
397398
self._scanned = False
398-
self._template_system: typing.Optional[TemplateSystem] = None
399+
self._template_system: Optional[TemplateSystem] = None
399400
self._THEMES = None
400401
self._MESSAGES = None
401402
self.filters = {}
@@ -996,13 +997,13 @@ def __init__(self, **config):
996997
# WebP files have no official MIME type yet, but we need to recognize them (Issue #3671)
997998
mimetypes.add_type('image/webp', '.webp')
998999

999-
def _filter_duplicate_plugins(self, plugin_list: typing.Iterable[PluginCandidate]):
1000+
def _filter_duplicate_plugins(self, plugin_list: Iterable[PluginCandidate]):
10001001
"""Find repeated plugins and discard the less local copy."""
10011002
def plugin_position_in_places(plugin: PluginInfo):
10021003
# plugin here is a tuple:
10031004
# (path to the .plugin file, path to plugin module w/o .py, plugin metadata)
1005+
place: pathlib.Path
10041006
for i, place in enumerate(self._plugin_places):
1005-
place: pathlib.Path
10061007
try:
10071008
# Path.is_relative_to backport
10081009
plugin.source_dir.relative_to(place)
@@ -1025,7 +1026,7 @@ def plugin_position_in_places(plugin: PluginInfo):
10251026
result.append(plugins[-1])
10261027
return result
10271028

1028-
def init_plugins(self, commands_only=False, load_all=False):
1029+
def init_plugins(self, commands_only=False, load_all=False) -> None:
10291030
"""Load plugins as needed."""
10301031
extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS']
10311032
self._loading_commands_only = commands_only
@@ -1086,9 +1087,9 @@ def init_plugins(self, commands_only=False, load_all=False):
10861087
# Search for compiler plugins which we disabled but shouldn't have
10871088
self._activate_plugins_of_category("PostScanner")
10881089
if not load_all:
1089-
file_extensions = set()
1090+
file_extensions: Set[str] = set()
1091+
post_scanner: PostScanner
10901092
for post_scanner in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('PostScanner')]:
1091-
post_scanner: PostScanner
10921093
exts = post_scanner.supported_extensions()
10931094
if exts is not None:
10941095
file_extensions.update(exts)
@@ -1126,8 +1127,8 @@ def init_plugins(self, commands_only=False, load_all=False):
11261127

11271128
self._activate_plugins_of_category("Taxonomy")
11281129
self.taxonomy_plugins = {}
1130+
taxonomy: Taxonomy
11291131
for taxonomy in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('Taxonomy')]:
1130-
taxonomy: Taxonomy
11311132
if not taxonomy.is_enabled():
11321133
continue
11331134
if taxonomy.classification_name in self.taxonomy_plugins:
@@ -1322,7 +1323,7 @@ def _activate_plugin(self, plugin_info: PluginInfo) -> None:
13221323
if candidate.exists() and candidate.is_dir():
13231324
self.template_system.inject_directory(str(candidate))
13241325

1325-
def _activate_plugins_of_category(self, category) -> typing.List[PluginInfo]:
1326+
def _activate_plugins_of_category(self, category) -> List[PluginInfo]:
13261327
"""Activate all the plugins of a given category and return them."""
13271328
# this code duplicated in tests/base.py
13281329
plugins = []
@@ -1397,6 +1398,11 @@ def _get_template_system(self):
13971398
"plugin\n".format(template_sys_name))
13981399
sys.exit(1)
13991400
self._template_system = typing.cast(TemplateSystem, pi.plugin_object)
1401+
1402+
engine_factory: Optional[Callable[..., Any]] = self.config.get("TEMPLATE_ENGINE_FACTORY")
1403+
if engine_factory is not None:
1404+
self._template_system.user_engine_factory(engine_factory)
1405+
14001406
lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name), "templates")
14011407
for name in self.THEMES]
14021408
self._template_system.set_directories(lookup_dirs,
@@ -1444,7 +1450,7 @@ def get_compiler(self, source_name):
14441450

14451451
return compiler
14461452

1447-
def render_template(self, template_name, output_name, context, url_type=None, is_fragment=False):
1453+
def render_template(self, template_name: str, output_name: str, context, url_type=None, is_fragment=False):
14481454
"""Render a template with the global context.
14491455
14501456
If ``output_name`` is None, will return a string and all URL
@@ -1463,7 +1469,7 @@ def render_template(self, template_name, output_name, context, url_type=None, is
14631469
utils.TEMPLATES_LOGGER.debug("For %s, template %s builds %s", context["post"].source_path, template_name, output_name)
14641470
else:
14651471
utils.TEMPLATES_LOGGER.debug("Template %s builds %s", template_name, output_name)
1466-
local_context: typing.Dict[str, typing.Any] = {}
1472+
local_context: Dict[str, Any] = {}
14671473
local_context["template_name"] = template_name
14681474
local_context.update(self.GLOBAL_CONTEXT)
14691475
local_context.update(context)

0 commit comments

Comments
 (0)