From 424ddcea1df46a9896ad701941fa9ef79663f337 Mon Sep 17 00:00:00 2001 From: th3aftermath Date: Wed, 23 Apr 2014 23:39:12 -0400 Subject: [PATCH] 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 | 27 +++++++++++++---- pelican/settings.py | 21 ++++++++++++++ pelican/tests/test_generators.py | 50 ++++++++++++++++---------------- pelican/tests/test_settings.py | 2 ++ pelican/tests/test_utils.py | 4 +-- 8 files changed, 108 insertions(+), 38 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 1b4bae94..df728c5b 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -653,6 +653,12 @@ Setting name (default value) 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 b029e816..690c149d 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -298,15 +298,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`` themes 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 @@ -315,6 +322,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 8cae468c..2c93d6c7 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' @@ -152,6 +157,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() ] @@ -336,9 +342,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): @@ -348,7 +357,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 3cc84fa8..bff6a87d 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -31,12 +31,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(): @@ -55,14 +56,22 @@ class Generator(object): simple_loader = FileSystemLoader(os.path.join(theme_path, "themes", "simple", "templates")) + + themes = {} + for theme in self.themes: + themes[theme] = FileSystemLoader(os.path.join(self.themes[theme], "templates")) + + 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'], ) @@ -676,6 +685,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 abf16b32..8af7e590 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -34,6 +34,7 @@ DEFAULT_CONFIG = { 'PAGE_DIR': 'pages', 'PAGE_EXCLUDES': (), 'THEME': DEFAULT_THEME, + 'THEMES': {}, 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images', ], @@ -154,6 +155,14 @@ def read_settings(path=None, override=None): if 'PLUGIN_PATH' in local_settings and local_settings['PLUGIN_PATH'] is not None: local_settings['PLUGIN_PATH'] = [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_PATH']] + + 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) @@ -213,6 +222,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 9463047e..757c6f45 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -27,7 +27,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) @@ -53,7 +53,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 = [[page.title, page.status, page.category.name, page.template] for page in cls.generator.articles] @@ -69,7 +69,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_DIRECTORY'] = 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, @@ -77,7 +77,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) @@ -147,7 +147,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 @@ -170,7 +170,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_DIRECTORY'] = 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", @@ -185,7 +185,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_DIRECTORY'] = 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", @@ -201,7 +201,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_DIRECTORY'] = 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_count == 0 @@ -228,7 +228,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['CACHE_DIRECTORY'] = 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) @@ -245,7 +245,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) @@ -263,7 +263,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) @@ -297,13 +297,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 @@ -316,13 +316,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() @@ -340,7 +340,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')) @@ -349,7 +349,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 @@ -378,7 +378,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) @@ -408,13 +408,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 @@ -427,13 +427,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() @@ -451,7 +451,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')) @@ -460,7 +460,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 @@ -492,7 +492,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') diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 930e0fea..1e93f55f 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -70,6 +70,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': '', } @@ -87,6 +88,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 3c12a15b..eb039b49 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -488,7 +488,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) @@ -517,7 +517,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)