diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 282d6c23..b7cbe951 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -186,7 +186,7 @@ the content. The ``pelican`` command can also be run directly:: $ pelican /path/to/your/content/ [-s path/to/your/settings.py] -The above command will generate your weblog and save it in the ``content/`` +The above command will generate your weblog and save it in the ``output/`` folder, using the default theme to produce a simple site. The default theme is simple HTML without styling and is provided so folks may use it as a basis for creating their own themes. @@ -271,19 +271,21 @@ Pelican is able to provide colorized syntax highlighting for your code blocks. To do so, you have to use the following conventions (you need to put this in your content files). -For RestructuredText:: +For RestructuredText, use the code-block directive:: .. code-block:: identifier - your code goes here + -For Markdown, format your code blocks thusly:: +For Markdown, include the language identifier just above code blocks:: - :::identifier - your code goes here + :::identifier + + + (indent both the identifier and code) -The specified identifier should be one that appears on the -`list of available lexers `_. +The specified identifier (e.g. ``python``, ``ruby``) should be one that +appears on the `list of available lexers `_. Publishing drafts ----------------- diff --git a/docs/settings.rst b/docs/settings.rst index c219ed12..ad08f020 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -73,7 +73,7 @@ Setting name (default value) What doe `SITENAME` (``'A Pelican Blog'``) Your site name `SITEURL` Base URL of your website. Not defined by default, so it is best to specify your SITEURL; if you do not, feeds - will not be generated with properly-formed URLs. You should + will not be generated with properly-formed URLs. You should include ``http://`` and your domain, with no trailing slash at the end. Example: ``SITEURL = 'http://mydomain.com'`` `STATIC_PATHS` (``['images']``) The static paths you want to have accessible @@ -95,12 +95,12 @@ Setting name (default value) What doe index pages for collections of content e.g. tags and category index pages. `PAGINATED_DIRECT_TEMPLATES` (``('index',)``) Provides the direct templates that should be paginated. -`SUMMARY_MAX_LENGTH` (``50``) When creating a short summary of an article, this will +`SUMMARY_MAX_LENGTH` (``50``) When creating a short summary of an article, this will be the default length in words of the text created. - This only applies if your content does not otherwise - specify a summary. Setting to None will cause the summary + This only applies if your content does not otherwise + specify a summary. Setting to None will cause the summary to be a copy of the original content. - + ===================================================================== ===================================================================== .. [#] Default is the system locale. @@ -367,7 +367,7 @@ Ordering content ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`NEWEST_FIRST_ARCHIVES` (``True``) Order archives by newest first by date. (False: +`NEWEST_FIRST_ARCHIVES` (``True``) Order archives by newest first by date. (False: orders by date with older articles first.) `REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse alphabetical order; default lists alphabetically.) @@ -468,7 +468,7 @@ template tag, for example: .. code-block:: jinja {% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %} - + {% endassets %} will produce a minified css file with the version identifier: @@ -477,6 +477,15 @@ will produce a minified css file with the version identifier: +The filters can be combined, for example to use the `sass` compiler and minify +the output:: + +.. code-block:: jinja + +{% assets filters="sass,cssmin", output="css/style.min.css", "css/style.scss" %} + +{% endassets %} + Another example for javascript: .. code-block:: jinja @@ -491,6 +500,12 @@ will produce a minified and gzipped js file: +Pelican's debug mode is propagated to webassets to disable asset packaging, +and instead work with the uncompressed assets. However, this also means that +the `less` and `sass` files are not compiled, this should be fixed in a future +version of webassets (cf. the related `bug report +`_). + .. _webassets: https://github.com/miracle2k/webassets .. _documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html diff --git a/pelican/__init__.py b/pelican/__init__.py index 803e289a..056612a8 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -1,3 +1,4 @@ +import copy import os import re import sys @@ -11,7 +12,7 @@ from pelican.generators import (ArticlesGenerator, PagesGenerator, StaticGenerator, PdfGenerator, LessCSSGenerator) from pelican.log import init from pelican.settings import read_settings, _DEFAULT_CONFIG -from pelican.utils import clean_output_dir, files_changed, file_changed +from pelican.utils import clean_output_dir, files_changed, file_changed, NoFilesError from pelican.writers import Writer __major__ = 3 @@ -29,7 +30,7 @@ class Pelican(object): before doing anything else. """ if settings is None: - settings = _DEFAULT_CONFIG + settings = copy.deepcopy(_DEFAULT_CONFIG) self.path = path or settings['PATH'] if not self.path: @@ -267,6 +268,7 @@ def main(): try: if args.autoreload: + files_found_error = True while True: try: # Check source dir for changed files ending with the given @@ -276,6 +278,8 @@ def main(): # have. if files_changed(pelican.path, pelican.markup) or \ files_changed(pelican.theme, ['']): + if files_found_error == False: + files_found_error = True pelican.run() # reload also if settings.py changed @@ -289,6 +293,10 @@ def main(): except KeyboardInterrupt: logger.warning("Keyboard interrupt, quitting.") break + except NoFilesError: + if files_found_error == True: + logger.warning("No valid files found in content. Nothing to generate.") + files_found_error = False except Exception, e: logger.warning( "Caught exception \"{}\". Reloading.".format(e) diff --git a/pelican/contents.py b/pelican/contents.py index a5e3be8f..851607a5 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import copy import locale import logging import functools @@ -29,7 +30,7 @@ class Page(object): if not metadata: metadata = {} if not settings: - settings = _DEFAULT_CONFIG + settings = copy.deepcopy(_DEFAULT_CONFIG) self.settings = settings self._content = content diff --git a/pelican/settings.py b/pelican/settings.py index 645a9809..92c68ddc 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import copy import imp import inspect import os @@ -81,7 +82,7 @@ def read_settings(filename=None): if filename: local_settings = get_settings_from_file(filename) else: - local_settings = _DEFAULT_CONFIG + local_settings = copy.deepcopy(_DEFAULT_CONFIG) configured_settings = configure_settings(local_settings, None, filename) return configured_settings @@ -89,10 +90,9 @@ def read_settings(filename=None): def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG): """ Load settings from a module, returning a dict. - """ - context = default_settings.copy() + context = copy.deepcopy(default_settings) if module is not None: context.update( (k, v) for k, v in inspect.getmembers(module) if k.isupper() @@ -114,7 +114,7 @@ def get_settings_from_file(filename, default_settings=_DEFAULT_CONFIG): def configure_settings(settings, default_settings=None, filename=None): """Provide optimizations, error checking, and warnings for loaded settings""" if default_settings is None: - default_settings = _DEFAULT_CONFIG + default_settings = copy.deepcopy(_DEFAULT_CONFIG) # Make the paths relative to the settings file if filename: @@ -138,7 +138,7 @@ def configure_settings(settings, default_settings=None, filename=None): for locale_ in locales: try: locale.setlocale(locale.LC_ALL, locale_) - break # break if it is successfull + break # break if it is successful except locale.Error: pass else: diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in index 9a26e315..4c5a4fcb 100644 --- a/pelican/tools/templates/Makefile.in +++ b/pelican/tools/templates/Makefile.in @@ -1,7 +1,7 @@ PELICAN=$pelican PELICANOPTS=$pelicanopts -BASEDIR=$$(PWD) +BASEDIR=$$(CURDIR) INPUTDIR=$$(BASEDIR)/content OUTPUTDIR=$$(BASEDIR)/output CONFFILE=$$(BASEDIR)/pelicanconf.py diff --git a/pelican/tools/templates/develop_server.sh.in b/pelican/tools/templates/develop_server.sh.in index 2f8c07dd..3e97610b 100755 --- a/pelican/tools/templates/develop_server.sh.in +++ b/pelican/tools/templates/develop_server.sh.in @@ -1,11 +1,11 @@ -#!/bin/bash +#!/usr/bin/env bash ## # This section should match your Makefile ## PELICAN=$pelican PELICANOPTS=$pelicanopts -BASEDIR=$$(PWD) +BASEDIR=$$(pwd) INPUTDIR=$$BASEDIR/content OUTPUTDIR=$$BASEDIR/output CONFFILE=$$BASEDIR/pelicanconf.py @@ -65,6 +65,7 @@ function start_up(){ python -m SimpleHTTPServer & echo $$! > $$SRV_PID cd $$BASEDIR + sleep 1 && echo 'Pelican and SimpleHTTPServer processes now running in background.' } ### diff --git a/pelican/utils.py b/pelican/utils.py index 53e6e52b..ca3015ce 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -14,6 +14,9 @@ from operator import attrgetter logger = logging.getLogger(__name__) +class NoFilesError(Exception): + pass + def get_date(string): """Return a datetime object from a string. @@ -241,10 +244,13 @@ def files_changed(path, extensions): yield os.stat(os.path.join(root, f)).st_mtime global LAST_MTIME - mtime = max(file_times(path)) - if mtime > LAST_MTIME: - LAST_MTIME = mtime - return True + try: + mtime = max(file_times(path)) + if mtime > LAST_MTIME: + LAST_MTIME = mtime + return True + except ValueError: + raise NoFilesError("No files with the given extension(s) found.") return False diff --git a/tests/test_generators.py b/tests/test_generators.py index e984b484..3a4ea1e3 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -63,29 +63,26 @@ class TestArticlesGenerator(unittest.TestCase): def test_generate_context(self): - settings = _DEFAULT_CONFIG.copy() - settings['ARTICLE_DIR'] = 'content' - settings['DEFAULT_CATEGORY'] = 'Default' - generator = ArticlesGenerator(settings.copy(), settings, CUR_DIR, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) - generator.generate_context() - for article in generator.articles: - relfilepath = os.path.relpath(article.filename, CUR_DIR) - if relfilepath == os.path.join("TestCategory", - "article_with_category.rst"): - self.assertEquals(article.category.name, 'yeah') - elif relfilepath == os.path.join("TestCategory", - "article_without_category.rst"): - self.assertEquals(article.category.name, 'TestCategory') - elif relfilepath == "article_without_category.rst": - self.assertEquals(article.category.name, 'Default') + generator = self.get_populated_generator() + articles = self.distill_articles(generator.articles) + articles_expected = [ + [u'Article title', 'published', 'Default', 'article'], + [u'Article with template', 'published', 'Default', 'custom'], + [u'Test md File', 'published', 'test', 'article'], + [u'This is a super article !', 'published', 'Yeah', 'article'], + [u'This is an article with category !', 'published', 'yeah', 'article'], + [u'This is an article without category !', 'published', 'Default', 'article'], + [u'This is an article without category !', 'published', 'TestCategory', 'article'], + [u'This is a super article !', 'published', 'yeah', 'article'] + ] + self.assertItemsEqual(articles_expected, articles) + def test_generate_categories(self): + + generator = self.get_populated_generator() categories = [cat.name for cat, _ in generator.categories] - # assert that the categories are ordered as expected - self.assertEquals( - categories, ['Default', 'TestCategory', 'Yeah', 'test', - 'yeah']) + categories_expected = ['Default', 'TestCategory', 'Yeah', 'test', 'yeah'] + self.assertEquals(categories, categories_expected) def test_direct_templates_save_as_default(self): diff --git a/tests/test_pelican.py b/tests/test_pelican.py index 15088ed0..78f083f9 100644 --- a/tests/test_pelican.py +++ b/tests/test_pelican.py @@ -35,6 +35,18 @@ class TestPelican(unittest.TestCase): rmtree(self.temp_path) locale.setlocale(locale.LC_ALL, self.old_locale) + def assertFilesEqual(self, diff): + msg = "some generated files differ from the expected functional " \ + "tests output.\n" \ + "This is probably because the HTML generated files " \ + "changed. If these changes are normal, please refer " \ + "to docs/contribute.rst to update the expected " \ + "output of the functional tests." + + self.assertEqual(diff.left_only, [], msg=msg) + self.assertEqual(diff.right_only, [], msg=msg) + self.assertEqual(diff.diff_files, [], msg=msg) + @unittest.skip("Test failing") def test_basic_generation_works(self): # when running pelican without settings, it should pick up the default @@ -47,27 +59,7 @@ class TestPelican(unittest.TestCase): pelican.run() diff = dircmp( self.temp_path, os.sep.join((OUTPUT_PATH, "basic"))) - self.assertEqual(diff.left_only, [], msg="some generated " \ - "files are absent from the expected functional " \ - "tests output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") - self.assertEqual(diff.right_only, [], msg="some files from " \ - "the expected functional tests output are absent " \ - "from the current output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") - self.assertEqual(diff.diff_files, [], msg="some generated " \ - "files differ from the expected functional tests " \ - "output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") + self.assertFilesEqual(diff) def test_custom_generation_works(self): # the same thing with a specified set of settings should work @@ -75,24 +67,4 @@ class TestPelican(unittest.TestCase): settings=read_settings(SAMPLE_CONFIG)) pelican.run() diff = dircmp(self.temp_path, os.sep.join((OUTPUT_PATH, "custom"))) - self.assertEqual(diff.left_only, [], msg="some generated " \ - "files are absent from the expected functional " \ - "tests output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") - self.assertEqual(diff.right_only, [], msg="some files from " \ - "the expected functional tests output are absent " \ - "from the current output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") - self.assertEqual(diff.diff_files, [], msg="some generated " \ - "files differ from the expected functional tests " \ - "output.\n" \ - "This is probably because the HTML generated files " \ - "changed. If these changes are normal, please refer " \ - "to docs/contribute.rst to update the expected " \ - "output of the functional tests.") + self.assertFilesEqual(diff) diff --git a/tests/test_settings.py b/tests/test_settings.py index 25df74bd..873df824 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,7 @@ +import copy from os.path import dirname, abspath, join -from pelican.settings import read_settings, configure_settings, _DEFAULT_CONFIG +from pelican.settings import read_settings, configure_settings, _DEFAULT_CONFIG, DEFAULT_THEME from .support import unittest @@ -31,7 +32,26 @@ class TestSettingsConfiguration(unittest.TestCase): def test_read_empty_settings(self): """providing no file should return the default values.""" settings = read_settings(None) - self.assertDictEqual(settings, _DEFAULT_CONFIG) + expected = copy.deepcopy(_DEFAULT_CONFIG) + expected["FEED_DOMAIN"] = '' #This is added by configure settings + self.maxDiff = None + self.assertDictEqual(settings, expected) + + def test_settings_return_independent(self): + """Make sure that the results from one settings call doesn't + effect past or future instances.""" + self.PATH = abspath(dirname(__file__)) + default_conf = join(self.PATH, 'default_conf.py') + settings = read_settings(default_conf) + settings['SITEURL'] = 'new-value' + new_settings = read_settings(default_conf) + self.assertNotEqual(new_settings['SITEURL'], settings['SITEURL']) + + def test_defaults_not_overwritten(self): + """This assumes 'SITENAME': 'A Pelican Blog'""" + settings = read_settings(None) + settings['SITENAME'] = 'Not a Pelican Blog' + self.assertNotEqual(settings['SITENAME'], _DEFAULT_CONFIG['SITENAME']) def test_configure_settings(self): """Manipulations to settings should be applied correctly.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index 2ea756dc..148e322a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,6 +6,7 @@ import time from pelican import utils from .support import get_article, unittest +from pelican.utils import NoFilesError class TestUtils(unittest.TestCase): @@ -74,7 +75,8 @@ class TestUtils(unittest.TestCase): self.assertNotIn(fr_article1, index) def test_files_changed(self): - "Test if file changes are correctly detected" + """Test if file changes are correctly detected + Make sure to handle not getting any files correctly""" path = os.path.join(os.path.dirname(__file__), 'content') filename = os.path.join(path, 'article_with_metadata.rst') @@ -90,6 +92,18 @@ class TestUtils(unittest.TestCase): self.assertEquals(changed, True) self.assertAlmostEqual(utils.LAST_MTIME, t, delta=1) + empty_path = os.path.join(os.path.dirname(__file__), 'empty') + try: + os.mkdir(empty_path) + os.mkdir(os.path.join(empty_path, "empty_folder")) + shutil.copy(__file__, empty_path) + with self.assertRaises(NoFilesError): + utils.files_changed(empty_path, 'rst') + except OSError: + self.fail("OSError Exception in test_files_changed test") + finally: + shutil.rmtree(empty_path, True) + def test_clean_output_dir(self): test_directory = os.path.join(os.path.dirname(__file__), 'clean_output') content = os.path.join(os.path.dirname(__file__), 'content')