From d7993c0e3fbc7e08b06bfb57f3ff3b824baab07a Mon Sep 17 00:00:00 2001 From: th3aftermath Date: Mon, 17 Mar 2014 11:39:57 -0400 Subject: [PATCH] Add a Base Theme Setting Adds a setting to specify the base theme you want to use. No longer do you have to use simple. Can be specified from your settings file and the command line. The base theme directory is also watched for changes so that autoreload works. --- pelican/__init__.py | 27 ++++++++++++++++++++++++--- pelican/generators.py | 17 ++++++++++++++--- pelican/settings.py | 21 +++++++++++++++++++-- pelican/tests/test_generators.py | 26 +++++++++++++------------- pelican/tests/test_settings.py | 4 +++- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 08dd484e..cc75b6be 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -44,6 +44,7 @@ class Pelican(object): self.path = settings['PATH'] self.theme = settings['THEME'] + self.base_theme = settings['BASE_THEME'] self.output_path = settings['OUTPUT_PATH'] self.ignore_files = settings['IGNORE_FILES'] self.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY'] @@ -148,6 +149,7 @@ class Pelican(object): settings=self.settings, path=self.path, theme=self.theme, + base_theme=self.base_theme, output_path=self.output_path, ) for cls in self.get_generator_classes() ] @@ -224,6 +226,11 @@ def parse_arguments(): 'specified, it will use the default one included with ' 'pelican.') + parser.add_argument('-b', '--base-theme-path', dest='base_theme', + help='Path where to find the base theme templates. If not ' + 'specified, it will use the default one included with ' + 'pelican.') + parser.add_argument('-o', '--output', dest='output', help='Where to output the generated files. If not ' 'specified, a directory will be created, named ' @@ -270,6 +277,9 @@ def get_config(args): if args.theme: abstheme = os.path.abspath(os.path.expanduser(args.theme)) config['THEME'] = abstheme if os.path.exists(abstheme) else args.theme + if args.base_theme: + absbasetheme = os.path.abspath(os.path.expanduser(args.base_theme)) + config['BASE_THEME'] = absbasetheme if os.path.exists(absbasetheme) else args.base_theme if args.delete_outputdir is not None: config['DELETE_OUTPUT_DIRECTORY'] = args.delete_outputdir @@ -280,7 +290,7 @@ def get_config(args): if not six.PY3: enc = locale.getpreferredencoding() for key in config: - if key in ('PATH', 'OUTPUT_PATH', 'THEME'): + if key in ('PATH', 'OUTPUT_PATH', 'THEME', 'BASE_THEME'): config[key] = config[key].decode(enc) return config @@ -314,6 +324,9 @@ def main(): 'theme': folder_watcher(pelican.theme, [''], pelican.ignore_files), + 'base_theme': folder_watcher(pelican.base_theme, + [''], + pelican.ignore_files), 'settings': file_watcher(args.settings)} for static_path in settings.get("STATIC_PATHS", []): @@ -321,13 +334,13 @@ def main(): try: if args.autoreload: - print(' --- AutoReload Mode: Monitoring `content`, `theme` and' + print(' --- AutoReload Mode: Monitoring `content`, `theme`, `base_theme` and' ' `settings` for changes. ---') 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 and base_theme dir there is no such # restriction; all files are recursively checked if they # have changed, no matter what extension the filenames # have. @@ -347,6 +360,10 @@ def main(): logger.warning('Empty theme folder. Using `basic` ' 'theme.') + if modified['base_theme'] is None: + logger.warning('Empty base_theme folder. Using `simple` ' + 'theme.') + pelican.run() except KeyboardInterrupt: @@ -370,6 +387,10 @@ def main(): if next(watchers['theme']) is None: logger.warning('Empty theme folder. Using `basic` theme.') + if next(watchers['base_theme']) is None: + logger.warning('Empty base_theme folder. Using `simple` ' + 'theme.') + pelican.run() except Exception as e: diff --git a/pelican/generators.py b/pelican/generators.py index bfdac1a5..bb397d61 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -30,11 +30,12 @@ logger = logging.getLogger(__name__) class Generator(object): """Baseclass generator""" - def __init__(self, context, settings, path, theme, output_path, **kwargs): + def __init__(self, context, settings, path, theme, base_theme, output_path, **kwargs): self.context = context self.settings = settings self.path = path self.theme = theme + self.base_theme = base_theme self.output_path = output_path for arg, value in kwargs.items(): @@ -53,13 +54,19 @@ class Generator(object): simple_loader = FileSystemLoader(os.path.join(theme_path, "themes", "simple", "templates")) + + base_theme = self.settings['BASE_THEME'] + + base_loader = FileSystemLoader(os.path.join(theme_path, + "themes", base_theme, "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 + base_loader, # implicit inheritance + PrefixLoader({'!simple': simple_loader}), # explicit one + PrefixLoader({'!base': base_loader}) # explicit one ]), extensions=self.settings['JINJA_EXTENSIONS'], ) @@ -606,6 +613,10 @@ class StaticGenerator(Generator): self._update_context(('staticfiles',)) def generate_output(self, writer): + + self._copy_paths(self.settings['BASE_THEME_STATIC_PATHS'], self.base_theme, + self.settings['THEME_STATIC_DIR'], self.output_path, + os.curdir) self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme, self.settings['THEME_STATIC_DIR'], self.output_path, os.curdir) diff --git a/pelican/settings.py b/pelican/settings.py index 796678e0..92c6da3e 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -25,18 +25,23 @@ logger = logging.getLogger(__name__) DEFAULT_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'themes', 'notmyidea') +DEFAULT_BASE_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'themes', 'simple') + DEFAULT_CONFIG = { 'PATH': os.curdir, 'ARTICLE_DIR': '', 'ARTICLE_EXCLUDES': ('pages',), 'PAGE_DIR': 'pages', 'PAGE_EXCLUDES': (), + 'BASE_THEME': DEFAULT_BASE_THEME, 'THEME': DEFAULT_THEME, 'OUTPUT_PATH': 'output', 'READERS': {}, 'STATIC_PATHS': ['images', ], 'THEME_STATIC_DIR': 'theme', 'THEME_STATIC_PATHS': ['static', ], + 'BASE_THEME_STATIC_PATHS': ['static', ], 'FEED_ALL_ATOM': os.path.join('feeds', 'all.atom.xml'), 'CATEGORY_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'), 'TRANSLATION_FEED_ATOM': os.path.join('feeds', 'all-%s.atom.xml'), @@ -126,12 +131,12 @@ def read_settings(path=None, override=None): if path: local_settings = get_settings_from_file(path) # Make the paths relative to the settings file - for p in ['PATH', 'OUTPUT_PATH', 'THEME', 'PLUGIN_PATH']: + for p in ['PATH', 'OUTPUT_PATH', 'THEME', 'BASE_THEME', 'PLUGIN_PATH']: if p in local_settings and local_settings[p] is not None \ and not isabs(local_settings[p]): absp = os.path.abspath(os.path.normpath(os.path.join( os.path.dirname(path), local_settings[p]))) - if p not in ('THEME', 'PLUGIN_PATH') or os.path.exists(absp): + if p not in ('THEME', 'BASE_THEME', 'PLUGIN_PATH') or os.path.exists(absp): local_settings[p] = absp else: local_settings = copy.deepcopy(DEFAULT_CONFIG) @@ -188,6 +193,18 @@ def configure_settings(settings): raise Exception("Could not find the theme %s" % settings['THEME']) + # lookup the base theme in "pelican/themes" if the given one doesn't exist + if not os.path.isdir(settings['BASE_THEME']): + theme_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'themes', + settings['BASE_THEME']) + if os.path.exists(theme_path): + settings['BASE_THEME'] = theme_path + else: + raise Exception("Could not find the base theme %s" + % settings['BASE_THEME']) + # standardize strings to lowercase strings for key in [ 'DEFAULT_LANG', diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 6f13aeb6..e063ed00 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -24,7 +24,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['BASE_THEME'], None) def test_include_path(self): filename = os.path.join(CUR_DIR, 'content', 'article.rst') @@ -45,7 +45,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'], base_theme=settings['BASE_THEME'], output_path=None) cls.generator.generate_context() cls.articles = [[page.title, page.status, page.category.name, page.template] for page in cls.generator.articles] @@ -54,7 +54,7 @@ class TestArticlesGenerator(unittest.TestCase): settings = get_settings() generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], base_theme=settings['BASE_THEME'], output_path=None) writer = MagicMock() generator.generate_feeds(writer) writer.write_feed.assert_called_with([], settings, @@ -62,7 +62,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'], base_theme=settings['BASE_THEME'], output_path=None) writer = MagicMock() generator.generate_feeds(writer) self.assertFalse(writer.write_feed.called) @@ -131,7 +131,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'], base_theme=settings['BASE_THEME'], output_path=None) generator.generate_context() # test for name # categories are grouped by slug; if two categories have the same slug @@ -153,7 +153,7 @@ class TestArticlesGenerator(unittest.TestCase): settings = get_settings(filenames={}) generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], base_theme=settings['BASE_THEME'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives.html", @@ -167,7 +167,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['ARCHIVES_SAVE_AS'] = 'archives/index.html' generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], base_theme=settings['BASE_THEME'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives/index.html", @@ -182,7 +182,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['ARCHIVES_SAVE_AS'] = 'archives/index.html' generator = ArticlesGenerator( context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + path=None, theme=settings['THEME'], base_theme=settings['BASE_THEME'], output_path=None) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_count == 0 @@ -208,7 +208,7 @@ class TestArticlesGenerator(unittest.TestCase): settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' generator = ArticlesGenerator( context=settings, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + path=CONTENT_DIR, theme=settings['THEME'], base_theme=settings['BASE_THEME'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -225,7 +225,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'], base_theme=settings['BASE_THEME'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -243,7 +243,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'], base_theme=settings['BASE_THEME'], output_path=None) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -285,7 +285,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'], base_theme=settings['BASE_THEME'], output_path=None) generator.generate_context() pages = self.distill_pages(generator.pages) hidden_pages = self.distill_pages(generator.hidden_pages) @@ -329,7 +329,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='', base_theme=settings['BASE_THEME'], 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 7907a551..56b91f36 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, DEFAULT_BASE_THEME) from pelican.tests.support import unittest @@ -65,6 +65,7 @@ class TestSettingsConfiguration(unittest.TestCase): # These 4 settings are required to run configure_settings 'PATH': '.', 'THEME': DEFAULT_THEME, + 'BASE_THEME': DEFAULT_BASE_THEME, 'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': '', } @@ -82,6 +83,7 @@ class TestSettingsConfiguration(unittest.TestCase): 'LOCALE': '', 'PATH': os.curdir, 'THEME': DEFAULT_THEME, + 'BASE_THEME': DEFAULT_BASE_THEME, } configure_settings(settings) # SITEURL should not have a trailing slash