diff --git a/docs/settings.rst b/docs/settings.rst index 02f4359f..e2e938af 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -720,6 +720,12 @@ Setting name (followed by default value, if any) What does it do? the paths defined in this settings, they will be progressively overwritten. ``CSS_FILE = 'main.css'`` Specify the CSS file you want to load. +``THEMES = ['simple', ['!simple', 'simple']]`` Extra themes that can be inherited from, either + implicitly (just a path to the theme) or explicitly + using a prefix marker (tuple of prefix and path to + theme). They can also inherit from each other, + but only in the specified order. + See :ref:`theme_inheritance` for details. ================================================ ===================================================== diff --git a/docs/themes.rst b/docs/themes.rst index c9fc2b37..2076ad91 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -346,26 +346,48 @@ Here is a complete list of the feed variables:: TRANSLATION_FEED_ATOM TRANSLATION_FEED_RSS +.. _theme_inheritance: -Inheritance -=========== +Theme inheritance +================= -Since version 3.0, Pelican supports inheritance from the ``simple`` theme, so -you can re-use the ``simple`` theme templates in your own themes. +Since version 3.6, Pelican supports both implicit and explicit +inheritance from any theme specified in the ``THEMES`` setting. By +default the special ``simple`` theme is inherited both implicitly and +explicitly, so you can re-use its theme templates in your own themes. -If one of the mandatory files in the ``templates/`` directory of your theme is -missing, it will be replaced by the matching template from the ``simple`` theme. -So if the HTML structure of a template in the ``simple`` theme is right for you, +Implicit Inheritance +-------------------- + +If one of the mandatory files in the ``templates/`` directory of your +theme is missing, it will be replaced by a matching template from any +of the implicitly inherited themes. So if the HTML structure of a +template in the by default inherited ``simple`` theme is right for you, you don't have to write a new template from scratch. -You can also extend templates from the ``simple`` theme in your own themes by -using the ``{% extends %}`` directive as in the following example: +Explicit Inheritance +-------------------- + +You explicitly extend templates from explicitly inherited themes in +your own themes by using the ``{% extends %}`` directive as in the +following example: + +With the following ``THEMES`` setting which is the default plus an +extra explicitly inherited theme + +.. code-block:: python + + THEMES = ['simple', ['!simple', 'simple'], ['!foo', 'foo']] + +You can extend parent (inherited) or sibling (your own theme) templates .. code-block:: html+jinja {% extends "!simple/index.html" %} - {% extends "index.html" %} + {% extends "!foo/index.html" %} + + {% extends "index.html" %} Example diff --git a/pelican/__init__.py b/pelican/__init__.py index 076375ba..db92acca 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -47,6 +47,7 @@ class Pelican(object): self.path = settings['PATH'] self.theme = settings['THEME'] + self.themes = settings['THEMES'] self.output_path = settings['OUTPUT_PATH'] self.ignore_files = settings['IGNORE_FILES'] self.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY'] @@ -87,6 +88,10 @@ class Pelican(object): def _handle_deprecation(self): + if self.settings['EXTRA_TEMPLATES_PATHS']: + logger.warning('`EXTRA_TEMPLATES_PATHS` is soon to be deprecated.' + ' Use the `THEMES` setting for the same behaviour') + if self.settings.get('CLEAN_URLS', False): logger.warning('Found deprecated `CLEAN_URLS` in settings.' ' Modifying the following settings for the' @@ -154,6 +159,7 @@ class Pelican(object): settings=self.settings, path=self.path, theme=self.theme, + themes=self.themes, output_path=self.output_path, ) for cls in self.get_generator_classes() ] @@ -360,9 +366,13 @@ def main(): for static_path in settings.get("STATIC_PATHS", []): watchers[static_path] = folder_watcher(static_path, [''], pelican.ignore_files) + for theme in pelican.themes: + theme = theme[1] if isinstance(theme, tuple) else theme + watchers[theme] = folder_watcher(theme, [''], pelican.ignore_files) + try: if args.autoreload: - print(' --- AutoReload Mode: Monitoring `content`, `theme` and' + print(' --- AutoReload Mode: Monitoring `content`, `theme`, and' ' `settings` for changes. ---') def _ignore_cache(pelican_obj): @@ -372,7 +382,7 @@ def main(): while True: try: # Check source dir for changed files ending with the given - # extension in the settings. In the theme dir is no such + # extension in the settings. In the theme dir there is no such # restriction; all files are recursively checked if they # have changed, no matter what extension the filenames # have. diff --git a/pelican/generators.py b/pelican/generators.py index 5122fa6d..33d4b0d1 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -33,12 +33,13 @@ logger = logging.getLogger(__name__) class Generator(object): """Baseclass generator""" - def __init__(self, context, settings, path, theme, output_path, + def __init__(self, context, settings, path, theme, themes, output_path, readers_cache_name='', **kwargs): self.context = context self.settings = settings self.path = path self.theme = theme + self.themes = themes self.output_path = output_path for arg, value in kwargs.items(): @@ -53,18 +54,27 @@ class Generator(object): os.path.join(self.theme, 'templates'))) self._templates_path += self.settings['EXTRA_TEMPLATES_PATHS'] - theme_path = os.path.dirname(os.path.abspath(__file__)) + explicit_themes = {} + themes = [FileSystemLoader(self._templates_path)] + for theme in self.themes: + if isinstance(theme, list): # explicit inheritance + prefix, theme_path = theme + templates_path = os.path.join(theme_path, "templates") + logger.debug('Template path for prefix %s: %s', prefix, + templates_path) + explicit_themes[prefix] = FileSystemLoader(templates_path) + else: # implicit inheritance + templates_path = os.path.join(theme, "templates") + logger.debug('Implicit template path: %s', templates_path) + themes.append(FileSystemLoader(templates_path)) + + themes.append(PrefixLoader(explicit_themes)) + loader=ChoiceLoader(themes) - simple_loader = FileSystemLoader(os.path.join(theme_path, - "themes", "simple", "templates")) self.env = Environment( trim_blocks=True, lstrip_blocks=True, - loader=ChoiceLoader([ - FileSystemLoader(self._templates_path), - simple_loader, # implicit inheritance - PrefixLoader({'!simple': simple_loader}) # explicit one - ]), + loader=loader, extensions=self.settings['JINJA_EXTENSIONS'], ) @@ -717,6 +727,13 @@ class StaticGenerator(Generator): self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme, self.settings['THEME_STATIC_DIR'], self.output_path, os.curdir) + + for theme in self.themes: + theme = theme[1] if isinstance(theme, list) else theme + self._copy_paths(self.settings['THEME_STATIC_PATHS'], theme, + self.settings['THEME_STATIC_DIR'], self.output_path, + os.curdir) + # copy all Static files for sc in self.context['staticfiles']: source_path = os.path.join(self.path, sc.source_path) diff --git a/pelican/settings.py b/pelican/settings.py index 794733d7..d4a271ee 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -27,6 +27,11 @@ logger = logging.getLogger(__name__) DEFAULT_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'themes', 'notmyidea') + + +SIMPLE_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'themes', 'simple') + DEFAULT_CONFIG = { 'PATH': os.curdir, 'ARTICLE_PATHS': [''], @@ -34,6 +39,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], @@ -164,6 +170,18 @@ def read_settings(path=None, override=None): elif local_settings['PLUGIN_PATHS'] is not None: local_settings['PLUGIN_PATHS'] = [os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), pluginpath))) if not isabs(pluginpath) else pluginpath for pluginpath in local_settings['PLUGIN_PATHS']] + + if 'THEMES' in local_settings and local_settings['THEMES']: + for i, p in enumerate(local_settings['THEMES']): + explicit = isinstance(p, list) + p = p[1] if explicit else p + if not isabs(p): + absp = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), p))) + if os.path.exists(absp): + if explicit: + local_settings['THEMES'][i][1] = absp + else: + local_settings['THEMES'][i] = absp else: local_settings = copy.deepcopy(DEFAULT_CONFIG) @@ -223,6 +241,22 @@ def configure_settings(settings): raise Exception("Could not find the theme %s" % settings['THEME']) + for i, theme in enumerate(settings['THEMES']): + explicit = isinstance(theme, list) + theme = theme[1] if explicit else theme + if not os.path.isdir(theme): + theme_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'themes', theme) + if os.path.exists(theme_path): + if explicit: + settings['THEMES'][i][1] = theme_path + else: + settings['THEMES'][i] = theme_path + else: + raise Exception("Could not find the theme %s" + % theme) + # make paths selected for writing absolute if necessary settings['WRITE_SELECTED'] = [ os.path.abspath(path) for path in diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 4be1b35e..cfbd5b86 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -28,7 +28,7 @@ class TestGenerator(unittest.TestCase): self.settings = get_settings() self.settings['READERS'] = {'asc': None} self.generator = Generator(self.settings.copy(), self.settings, - CUR_DIR, self.settings['THEME'], None) + CUR_DIR, self.settings['THEME'], self.settings['THEMES'], None) def tearDown(self): locale.setlocale(locale.LC_ALL, self.old_locale) @@ -48,7 +48,8 @@ class TestGenerator(unittest.TestCase): generator = Generator(context=self.settings.copy(), settings=self.settings, path=os.path.join(CUR_DIR, 'nested_content'), - theme=self.settings['THEME'], output_path=None) + theme=self.settings['THEME'], themes=self.settings['THEMES'], + output_path=None) filepaths = generator.get_files(paths=['maindir']) found_files = {os.path.basename(f) for f in filepaths} @@ -86,7 +87,7 @@ class TestArticlesGenerator(unittest.TestCase): cls.generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) cls.generator.generate_context() cls.articles = cls.distill_articles(cls.generator.articles) @@ -106,7 +107,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_PATH'] = self.temp_cache generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) writer = MagicMock() generator.generate_feeds(writer) writer.write_feed.assert_called_with([], settings, @@ -114,7 +115,7 @@ class TestArticlesGenerator(unittest.TestCase): generator = ArticlesGenerator( context=settings, settings=get_settings(FEED_ALL_ATOM=None), - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) writer = MagicMock() generator.generate_feeds(writer) self.assertFalse(writer.write_feed.called) @@ -187,7 +188,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['filenames'] = {} generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() # test for name # categories are grouped by slug; if two categories have the same slug @@ -210,7 +211,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_PATH'] = self.temp_cache generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives.html", @@ -225,7 +226,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_PATH'] = self.temp_cache generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives/index.html", @@ -241,7 +242,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_PATH'] = self.temp_cache generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) self.assertEqual(write.call_count, 0) @@ -268,7 +269,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_PATH'] = self.temp_cache generator = ArticlesGenerator( context=settings, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -285,7 +286,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['MONTH_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/{date:%b}/index.html' generator = ArticlesGenerator( context=settings, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -303,7 +304,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['DAY_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' generator = ArticlesGenerator( context=settings, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -337,13 +338,13 @@ class TestArticlesGenerator(unittest.TestCase): generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() self.assertTrue(hasattr(generator, '_cache')) generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() generator.readers.read_file.assert_called_count == 0 @@ -356,13 +357,13 @@ class TestArticlesGenerator(unittest.TestCase): generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() self.assertTrue(hasattr(generator.readers, '_cache')) generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) readers = generator.readers.readers for reader in readers.values(): reader.read = MagicMock() @@ -380,7 +381,7 @@ class TestArticlesGenerator(unittest.TestCase): generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() self.assertTrue(hasattr(generator, '_cache_open')) @@ -389,7 +390,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['LOAD_CONTENT_CACHE'] = False generator = ArticlesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() generator.readers.read_file.assert_called_count == orig_call_count @@ -418,7 +419,7 @@ class TestPageGenerator(unittest.TestCase): generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + path=CUR_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() pages = self.distill_pages(generator.pages) hidden_pages = self.distill_pages(generator.hidden_pages) @@ -449,13 +450,13 @@ class TestPageGenerator(unittest.TestCase): generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() self.assertTrue(hasattr(generator, '_cache')) generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() generator.readers.read_file.assert_called_count == 0 @@ -468,13 +469,13 @@ class TestPageGenerator(unittest.TestCase): generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.generate_context() self.assertTrue(hasattr(generator.readers, '_cache')) generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) readers = generator.readers.readers for reader in readers.values(): reader.read = MagicMock() @@ -492,7 +493,7 @@ class TestPageGenerator(unittest.TestCase): generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() self.assertTrue(hasattr(generator, '_cache_open')) @@ -501,7 +502,7 @@ class TestPageGenerator(unittest.TestCase): settings['LOAD_CONTENT_CACHE'] = False generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], themes=settings['THEMES'], output_path=None) generator.readers.read_file = MagicMock() generator.generate_context() generator.readers.read_file.assert_called_count == orig_call_count @@ -522,7 +523,8 @@ class TestPageGenerator(unittest.TestCase): ] generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + path=CUR_DIR, theme=settings['THEME'], themes=settings['THEMES'], + output_path=None) generator.generate_context() pages = self.distill_pages(generator.pages) self.assertEqual(pages_expected_sorted_by_filename, pages) @@ -538,7 +540,8 @@ class TestPageGenerator(unittest.TestCase): settings['PAGE_ORDER_BY'] = 'title' generator = PagesGenerator( context=settings.copy(), settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + path=CUR_DIR, theme=settings['THEME'], themes=settings['THEMES'], + output_path=None) generator.generate_context() pages = self.distill_pages(generator.pages) self.assertEqual(pages_expected_sorted_by_title, pages) @@ -570,7 +573,7 @@ class TestTemplatePagesGenerator(unittest.TestCase): generator = TemplatePagesGenerator( context={'foo': 'bar'}, settings=settings, - path=self.temp_content, theme='', output_path=self.temp_output) + path=self.temp_content, theme='', themes=settings['THEMES'], output_path=self.temp_output) # create a dummy template file template_dir = os.path.join(self.temp_content, 'template') @@ -607,6 +610,7 @@ class TestStaticGenerator(unittest.TestCase): StaticGenerator(context=context, settings=settings, path=settings['PATH'], output_path=None, + themes=settings['THEMES'], theme=settings['THEME']).generate_context() staticnames = [os.path.basename(c.source_path) @@ -631,6 +635,7 @@ class TestStaticGenerator(unittest.TestCase): for generator_class in (PagesGenerator, StaticGenerator): generator_class(context=context, settings=settings, path=settings['PATH'], output_path=None, + themes=settings['THEMES'], theme=settings['THEME']).generate_context() staticnames = [os.path.basename(c.source_path) @@ -648,6 +653,7 @@ class TestStaticGenerator(unittest.TestCase): for generator_class in (PagesGenerator, StaticGenerator): generator_class(context=context, settings=settings, path=settings['PATH'], output_path=None, + themes=settings['THEMES'], theme=settings['THEME']).generate_context() staticnames = [os.path.basename(c.source_path) diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 260eff05..7be433b4 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -6,7 +6,7 @@ import locale from os.path import dirname, abspath, join from pelican.settings import (read_settings, configure_settings, - DEFAULT_CONFIG, DEFAULT_THEME) + DEFAULT_CONFIG, DEFAULT_THEME, SIMPLE_THEME) from pelican.tests.support import unittest @@ -73,6 +73,7 @@ class TestSettingsConfiguration(unittest.TestCase): # These 4 settings are required to run configure_settings 'PATH': '.', 'THEME': DEFAULT_THEME, + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], 'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': '', } @@ -90,6 +91,7 @@ class TestSettingsConfiguration(unittest.TestCase): 'LOCALE': '', 'PATH': os.curdir, 'THEME': DEFAULT_THEME, + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], } configure_settings(settings) # SITEURL should not have a trailing slash diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 7c9e6e5a..8a87de11 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -497,7 +497,7 @@ class TestDateFormatter(unittest.TestCase): generator = TemplatePagesGenerator( {'date': self.date}, settings, - self.temp_content, '', self.temp_output) + self.temp_content, '', '', self.temp_output) generator.env.filters.update({'strftime': utils.DateFormatter()}) writer = Writer(self.temp_output, settings=settings) @@ -526,7 +526,7 @@ class TestDateFormatter(unittest.TestCase): generator = TemplatePagesGenerator( {'date': self.date}, settings, - self.temp_content, '', self.temp_output) + self.temp_content, '', '', self.temp_output) generator.env.filters.update({'strftime': utils.DateFormatter()}) writer = Writer(self.temp_output, settings=settings)