From b00d9ef6d1e3bb580f1e2a3e36dcb65ce139cf9a Mon Sep 17 00:00:00 2001 From: th3aftermath Date: Wed, 23 Apr 2014 23:39:12 -0400 Subject: [PATCH 1/7] Add Multi-theme support Adds multi-theme support using the new THEMES setting. You can specify all the themes that you will be using in python dicionary form. You can then inherit from the themes specified in THEMES using the corresponding key in the dictionary. --- docs/settings.rst | 6 ++++ docs/themes.rst | 23 ++++++++++-- pelican/__init__.py | 13 +++++-- pelican/generators.py | 30 ++++++++++++---- pelican/settings.py | 20 +++++++++++ pelican/tests/test_generators.py | 62 +++++++++++++++++--------------- pelican/tests/test_settings.py | 2 ++ pelican/tests/test_utils.py | 4 +-- 8 files changed, 119 insertions(+), 41 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 02f4359f..5e91711f 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 = {}`` Extra themes that can be inherited from. They can also + inherit from each other. Use a dictionary to make a list + of all the available list. The key in the dictionary will + also be the prefix you use to inherit from the theme. + + Example: ``THEMES = {'!foo': foo, '!foobar':foobar}`` ================================================ ===================================================== diff --git a/docs/themes.rst b/docs/themes.rst index c9fc2b37..4aa4da6b 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -350,15 +350,22 @@ Here is a complete list of the feed variables:: 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.4, Pelican supports inheritance from the ``simple`` theme, as well +as any themes specified in the ``THEMES`` setting. You can re-use +their theme templates in your own themes. + +Implicit Inheritance +-------------------- 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, 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 +Explicit Inheritance +-------------------- + +You explicitly extend templates from the ``simple`` themes in your own themes by using the ``{% extends %}`` directive as in the following example: .. code-block:: html+jinja @@ -367,6 +374,16 @@ using the ``{% extends %}`` directive as in the following example: {% extends "index.html" %} +You can extend from a user created theme by adding that theme to the ``THEMES`` +setting. + +.. code-block:: python + + THEMES = {'!foo': 'foo'} + +.. code-block:: html+jinja + + {% extends "!foo/index.html" %} Example ------- diff --git a/pelican/__init__.py b/pelican/__init__.py index 076375ba..4570dc08 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,12 @@ 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: + watchers[theme] = folder_watcher(pelican.themes[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 +381,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..f22b0463 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(): @@ -57,14 +58,25 @@ class Generator(object): simple_loader = FileSystemLoader(os.path.join(theme_path, "themes", "simple", "templates")) + + themes = {} + for theme in self.themes: + templates_path = os.path.join(self.themes[theme], "templates") + logger.debug('Template path for theme %s: %s', theme, + templates_path) + themes[theme] = FileSystemLoader(templates_path) + + loader=ChoiceLoader([ + FileSystemLoader(self._templates_path), + simple_loader, # implicit inheritance + PrefixLoader({'!simple':simple_loader}), + PrefixLoader(themes) # explicit one + ]) + 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 +729,12 @@ 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: + self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.themes[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..3ecc89f9 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -34,6 +34,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, + 'THEMES': {}, 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], @@ -164,6 +165,13 @@ 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 p in local_settings['THEMES']: + if not isabs(local_settings['THEMES'][p]): + absp = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), local_settings['THEMES'][p]))) + if os.path.exists(absp): + local_settings['THEMES'][p] = absp else: local_settings = copy.deepcopy(DEFAULT_CONFIG) @@ -223,6 +231,18 @@ def configure_settings(settings): raise Exception("Could not find the theme %s" % settings['THEME']) + for theme in settings['THEMES']: + if not os.path.isdir(settings['THEMES'][theme]): + theme_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'themes', + settings['THEMES'][theme]) + if os.path.exists(theme_path): + settings['THEMES'][theme] = 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..c7802844 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -73,6 +73,7 @@ class TestSettingsConfiguration(unittest.TestCase): # These 4 settings are required to run configure_settings 'PATH': '.', 'THEME': DEFAULT_THEME, + 'THEMES': {'!simple': 'simple'}, 'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': '', } @@ -90,6 +91,7 @@ class TestSettingsConfiguration(unittest.TestCase): 'LOCALE': '', 'PATH': os.curdir, 'THEME': DEFAULT_THEME, + 'THEMES': {'!simple': 'simple'}, } 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) From 45945e614208c85a88fa2250d1ef591db391f0ec Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 08:49:43 +0100 Subject: [PATCH 2/7] bugfix: if block with absp in THEMES was wronlgy indented if the local absp variable was used earlier in the code, it would override the path set in THEMES --- pelican/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pelican/settings.py b/pelican/settings.py index 3ecc89f9..9bb4e185 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -170,8 +170,8 @@ def read_settings(path=None, override=None): for p in local_settings['THEMES']: if not isabs(local_settings['THEMES'][p]): absp = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), local_settings['THEMES'][p]))) - if os.path.exists(absp): - local_settings['THEMES'][p] = absp + if os.path.exists(absp): + local_settings['THEMES'][p] = absp else: local_settings = copy.deepcopy(DEFAULT_CONFIG) From b22f760042b87f13028c569060d4fa7c56afe381 Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 09:53:20 +0100 Subject: [PATCH 3/7] support both implicit and explicit inheritance with THEMES --- docs/settings.rst | 12 +++++------ docs/themes.rst | 47 ++++++++++++++++++++++++------------------- pelican/generators.py | 35 ++++++++++++++++++-------------- pelican/settings.py | 2 +- 4 files changed, 53 insertions(+), 43 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 5e91711f..e8743238 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -720,12 +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 = {}`` Extra themes that can be inherited from. They can also - inherit from each other. Use a dictionary to make a list - of all the available list. The key in the dictionary will - also be the prefix you use to inherit from the theme. - - Example: ``THEMES = {'!foo': foo, '!foobar':foobar}`` +``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 4aa4da6b..81854382 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -346,45 +346,50 @@ Here is a complete list of the feed variables:: TRANSLATION_FEED_ATOM TRANSLATION_FEED_RSS +.. _theme_inheritance: -Inheritance -=========== +Theme inheritance +================= -Since version 3.4, Pelican supports inheritance from the ``simple`` theme, as well -as any themes specified in the ``THEMES`` setting. You can re-use -their 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. Implicit Inheritance -------------------- -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, +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. Explicit Inheritance -------------------- -You explicitly extend templates from the ``simple`` themes in your own themes by -using the ``{% extends %}`` directive as in the following example: +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" %} - -You can extend from a user created theme by adding that theme to the ``THEMES`` -setting. - -.. code-block:: python - - THEMES = {'!foo': 'foo'} - -.. code-block:: html+jinja - {% extends "!foo/index.html" %} + {% extends "index.html" %} + + Example ------- diff --git a/pelican/generators.py b/pelican/generators.py index f22b0463..d9d463ad 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -54,24 +54,28 @@ 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__)) + pelican_theme_path = os.path.dirname(os.path.abspath(__file__)) + simple_theme_path = os.path.join(pelican_theme_path, + "themes", "simple", "templates") - simple_loader = FileSystemLoader(os.path.join(theme_path, - "themes", "simple", "templates")) - - themes = {} + explicit_themes = {} + themes = [FileSystemLoader(self._templates_path)] for theme in self.themes: - templates_path = os.path.join(self.themes[theme], "templates") - logger.debug('Template path for theme %s: %s', theme, + if isinstance(theme, tuple): # explicit inheritance + prefix, theme_path = theme + if prefix == '!simple': + theme_path = simple_theme_path + templates_path = os.path.join(theme_path, "templates") + logger.debug('Template path for prefix %s: %s', prefix, templates_path) - themes[theme] = FileSystemLoader(templates_path) + explicit_themes[prefix] = FileSystemLoader(templates_path) + else: # implicit inheritance + if theme == 'simple': + theme = simple_theme_path + themes.append(FileSystemLoader(theme)) - loader=ChoiceLoader([ - FileSystemLoader(self._templates_path), - simple_loader, # implicit inheritance - PrefixLoader({'!simple':simple_loader}), - PrefixLoader(themes) # explicit one - ]) + themes.append(PrefixLoader(explicit_themes)) + loader=ChoiceLoader(themes) self.env = Environment( trim_blocks=True, @@ -731,7 +735,8 @@ class StaticGenerator(Generator): os.curdir) for theme in self.themes: - self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.themes[theme], + theme = theme[1] if isinstance(theme, tuple) else theme + self._copy_paths(self.settings['THEME_STATIC_PATHS'], theme, self.settings['THEME_STATIC_DIR'], self.output_path, os.curdir) diff --git a/pelican/settings.py b/pelican/settings.py index 9bb4e185..70ad3f82 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, - 'THEMES': {}, + 'THEMES': ('simple', ('!simple', 'simple')), 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], From ad6b4837468777a613817af3bc6e3e1b6eab0d87 Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 10:16:12 +0100 Subject: [PATCH 4/7] fix special handling of THEMES paths --- docs/settings.rst | 2 +- docs/themes.rst | 2 +- pelican/__init__.py | 3 ++- pelican/generators.py | 7 +++++-- pelican/settings.py | 25 +++++++++++++++---------- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index e8743238..7d20a962 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -720,7 +720,7 @@ 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 +``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, diff --git a/docs/themes.rst b/docs/themes.rst index 81854382..72923ff4 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -377,7 +377,7 @@ extra explicitly inherited theme .. code-block:: python - THEMES = ('simple', ('!simple', 'simple'), ('!foo', 'foo')) + THEMES = ['simple', ('!simple', 'simple'), ('!foo', 'foo')] You can extend parent (inherited) or sibling (your own theme) templates diff --git a/pelican/__init__.py b/pelican/__init__.py index 4570dc08..db92acca 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -367,7 +367,8 @@ def main(): watchers[static_path] = folder_watcher(static_path, [''], pelican.ignore_files) for theme in pelican.themes: - watchers[theme] = folder_watcher(pelican.themes[theme], [''], pelican.ignore_files) + theme = theme[1] if isinstance(theme, tuple) else theme + watchers[theme] = folder_watcher(theme, [''], pelican.ignore_files) try: if args.autoreload: diff --git a/pelican/generators.py b/pelican/generators.py index d9d463ad..a523663b 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -71,8 +71,11 @@ class Generator(object): explicit_themes[prefix] = FileSystemLoader(templates_path) else: # implicit inheritance if theme == 'simple': - theme = simple_theme_path - themes.append(FileSystemLoader(theme)) + templates_path = simple_theme_path + else: + 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) diff --git a/pelican/settings.py b/pelican/settings.py index 70ad3f82..464883dd 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, - 'THEMES': ('simple', ('!simple', 'simple')), + 'THEMES': ['simple', ('!simple', 'simple')], 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], @@ -167,11 +167,16 @@ def read_settings(path=None, override=None): if not isabs(pluginpath) else pluginpath for pluginpath in local_settings['PLUGIN_PATHS']] if 'THEMES' in local_settings and local_settings['THEMES']: - for p in local_settings['THEMES']: - if not isabs(local_settings['THEMES'][p]): - absp = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), local_settings['THEMES'][p]))) + for i, p in enumerate(local_settings['THEMES']): + explicit = isinstance(p, tuple) + 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): - local_settings['THEMES'][p] = absp + if explicit: + local_settings['THEMES'][i][1] = absp + else: + local_settings['THEMES'][i] = absp else: local_settings = copy.deepcopy(DEFAULT_CONFIG) @@ -231,14 +236,14 @@ def configure_settings(settings): raise Exception("Could not find the theme %s" % settings['THEME']) - for theme in settings['THEMES']: - if not os.path.isdir(settings['THEMES'][theme]): + for i, theme in enumerate(settings['THEMES']): + theme = theme[1] if isinstance(theme, tuple) else theme + if not os.path.isdir(theme): theme_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), - 'themes', - settings['THEMES'][theme]) + 'themes', theme) if os.path.exists(theme_path): - settings['THEMES'][theme] = theme_path + settings['THEMES'][i] = theme_path else: raise Exception("Could not find the theme %s" % theme) From d88cc86df20764711df38e6dfa8df5ee293a5c52 Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 10:40:49 +0100 Subject: [PATCH 5/7] fix some more test still using dict --- pelican/settings.py | 2 +- pelican/tests/test_settings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pelican/settings.py b/pelican/settings.py index 464883dd..f6385589 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, - 'THEMES': ['simple', ('!simple', 'simple')], + 'THEMES': [DEFAULT_THEME, ('!simple', DEFAULT_THEME)], 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index c7802844..11387418 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -73,7 +73,7 @@ class TestSettingsConfiguration(unittest.TestCase): # These 4 settings are required to run configure_settings 'PATH': '.', 'THEME': DEFAULT_THEME, - 'THEMES': {'!simple': 'simple'}, + 'THEMES': ['simple', ('!simple', 'simple')], 'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': '', } @@ -91,7 +91,7 @@ class TestSettingsConfiguration(unittest.TestCase): 'LOCALE': '', 'PATH': os.curdir, 'THEME': DEFAULT_THEME, - 'THEMES': {'!simple': 'simple'}, + 'THEMES': ['simple', ('!simple', 'simple')], } configure_settings(settings) # SITEURL should not have a trailing slash From 5642f113679b6dc24db34e284383f2ed5729db29 Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 11:05:33 +0100 Subject: [PATCH 6/7] fix theme path construction --- docs/settings.rst | 2 +- docs/themes.rst | 2 +- pelican/generators.py | 17 ++++------------- pelican/settings.py | 17 +++++++++++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 7d20a962..e2e938af 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -720,7 +720,7 @@ 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 +``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, diff --git a/docs/themes.rst b/docs/themes.rst index 72923ff4..2076ad91 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -377,7 +377,7 @@ extra explicitly inherited theme .. code-block:: python - THEMES = ['simple', ('!simple', 'simple'), ('!foo', 'foo')] + THEMES = ['simple', ['!simple', 'simple'], ['!foo', 'foo']] You can extend parent (inherited) or sibling (your own theme) templates diff --git a/pelican/generators.py b/pelican/generators.py index a523663b..33d4b0d1 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -54,27 +54,18 @@ class Generator(object): os.path.join(self.theme, 'templates'))) self._templates_path += self.settings['EXTRA_TEMPLATES_PATHS'] - pelican_theme_path = os.path.dirname(os.path.abspath(__file__)) - simple_theme_path = os.path.join(pelican_theme_path, - "themes", "simple", "templates") - explicit_themes = {} themes = [FileSystemLoader(self._templates_path)] for theme in self.themes: - if isinstance(theme, tuple): # explicit inheritance + if isinstance(theme, list): # explicit inheritance prefix, theme_path = theme - if prefix == '!simple': - theme_path = simple_theme_path 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 - if theme == 'simple': - templates_path = simple_theme_path - else: - templates_path = os.path.join(theme, "templates") - logger.debug('Implicit template path: %s', templates_path) + 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)) @@ -738,7 +729,7 @@ class StaticGenerator(Generator): os.curdir) for theme in self.themes: - theme = theme[1] if isinstance(theme, tuple) else theme + 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) diff --git a/pelican/settings.py b/pelican/settings.py index f6385589..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,7 +39,7 @@ DEFAULT_CONFIG = { 'PAGE_PATHS': ['pages'], 'PAGE_EXCLUDES': [], 'THEME': DEFAULT_THEME, - 'THEMES': [DEFAULT_THEME, ('!simple', DEFAULT_THEME)], + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images'], @@ -168,7 +173,7 @@ def read_settings(path=None, override=None): if 'THEMES' in local_settings and local_settings['THEMES']: for i, p in enumerate(local_settings['THEMES']): - explicit = isinstance(p, tuple) + 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))) @@ -237,13 +242,17 @@ def configure_settings(settings): % settings['THEME']) for i, theme in enumerate(settings['THEMES']): - theme = theme[1] if isinstance(theme, tuple) else theme + 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): - settings['THEMES'][i] = 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) From d71bae7ee523a8be12209e09af169173de4fb0b7 Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Wed, 5 Nov 2014 19:45:58 +0100 Subject: [PATCH 7/7] use also SIMPLE_THEME in test_settings.py --- pelican/tests/test_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 11387418..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,7 +73,7 @@ class TestSettingsConfiguration(unittest.TestCase): # These 4 settings are required to run configure_settings 'PATH': '.', 'THEME': DEFAULT_THEME, - 'THEMES': ['simple', ('!simple', 'simple')], + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], 'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': '', } @@ -91,7 +91,7 @@ class TestSettingsConfiguration(unittest.TestCase): 'LOCALE': '', 'PATH': os.curdir, 'THEME': DEFAULT_THEME, - 'THEMES': ['simple', ('!simple', 'simple')], + 'THEMES': [SIMPLE_THEME, ['!simple', SIMPLE_THEME]], } configure_settings(settings) # SITEURL should not have a trailing slash