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