Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Nico Di Rocco 2012-08-31 22:04:05 +02:00
commit 680e04b4a1
12 changed files with 132 additions and 96 deletions

View file

@ -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] $ 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 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 simple HTML without styling and is provided so folks may use it as a basis for
creating their own themes. 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 To do so, you have to use the following conventions (you need to put this in
your content files). your content files).
For RestructuredText:: For RestructuredText, use the code-block directive::
.. code-block:: identifier .. code-block:: identifier
your code goes here <indented code block goes here>
For Markdown, format your code blocks thusly:: For Markdown, include the language identifier just above code blocks::
:::identifier :::identifier
your code goes here <code goes here>
(indent both the identifier and code)
The specified identifier should be one that appears on the The specified identifier (e.g. ``python``, ``ruby``) should be one that
`list of available lexers <http://pygments.org/docs/lexers/>`_. appears on the `list of available lexers <http://pygments.org/docs/lexers/>`_.
Publishing drafts Publishing drafts
----------------- -----------------

View file

@ -73,7 +73,7 @@ Setting name (default value) What doe
`SITENAME` (``'A Pelican Blog'``) Your site name `SITENAME` (``'A Pelican Blog'``) Your site name
`SITEURL` Base URL of your website. Not defined by default, `SITEURL` Base URL of your website. Not defined by default,
so it is best to specify your SITEURL; if you do not, feeds 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 include ``http://`` and your domain, with no trailing
slash at the end. Example: ``SITEURL = 'http://mydomain.com'`` slash at the end. Example: ``SITEURL = 'http://mydomain.com'``
`STATIC_PATHS` (``['images']``) The static paths you want to have accessible `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 index pages for collections of content e.g. tags and
category index pages. category index pages.
`PAGINATED_DIRECT_TEMPLATES` (``('index',)``) Provides the direct templates that should be paginated. `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. be the default length in words of the text created.
This only applies if your content does not otherwise This only applies if your content does not otherwise
specify a summary. Setting to None will cause the summary specify a summary. Setting to None will cause the summary
to be a copy of the original content. to be a copy of the original content.
===================================================================== ===================================================================== ===================================================================== =====================================================================
.. [#] Default is the system locale. .. [#] Default is the system locale.
@ -367,7 +367,7 @@ Ordering content
================================================ ===================================================== ================================================ =====================================================
Setting name (default value) What does it do? 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.) orders by date with older articles first.)
`REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse `REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse
alphabetical order; default lists alphabetically.) alphabetical order; default lists alphabetically.)
@ -468,7 +468,7 @@ template tag, for example:
.. code-block:: jinja .. code-block:: jinja
{% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %} {% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %}
<link rel="stylesheet" href="{{ ASSETS_URL }}"> <link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %} {% endassets %}
will produce a minified css file with the version identifier: will produce a minified css file with the version identifier:
@ -477,6 +477,15 @@ will produce a minified css file with the version identifier:
<link href="http://{SITEURL}/theme/css/style.min.css?b3a7c807" rel="stylesheet"> <link href="http://{SITEURL}/theme/css/style.min.css?b3a7c807" rel="stylesheet">
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" %}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}
Another example for javascript: Another example for javascript:
.. code-block:: jinja .. code-block:: jinja
@ -491,6 +500,12 @@ will produce a minified and gzipped js file:
<script src="http://{SITEURL}/theme/js/packed.js?00703b9d"></script> <script src="http://{SITEURL}/theme/js/packed.js?00703b9d"></script>
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
<https://github.com/getpelican/pelican/issues/481>`_).
.. _webassets: https://github.com/miracle2k/webassets .. _webassets: https://github.com/miracle2k/webassets
.. _documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html .. _documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html

View file

@ -1,3 +1,4 @@
import copy
import os import os
import re import re
import sys import sys
@ -11,7 +12,7 @@ from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator, LessCSSGenerator) StaticGenerator, PdfGenerator, LessCSSGenerator)
from pelican.log import init from pelican.log import init
from pelican.settings import read_settings, _DEFAULT_CONFIG 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 from pelican.writers import Writer
__major__ = 3 __major__ = 3
@ -29,7 +30,7 @@ class Pelican(object):
before doing anything else. before doing anything else.
""" """
if settings is None: if settings is None:
settings = _DEFAULT_CONFIG settings = copy.deepcopy(_DEFAULT_CONFIG)
self.path = path or settings['PATH'] self.path = path or settings['PATH']
if not self.path: if not self.path:
@ -267,6 +268,7 @@ def main():
try: try:
if args.autoreload: if args.autoreload:
files_found_error = True
while True: while True:
try: try:
# Check source dir for changed files ending with the given # Check source dir for changed files ending with the given
@ -276,6 +278,8 @@ def main():
# have. # have.
if files_changed(pelican.path, pelican.markup) or \ if files_changed(pelican.path, pelican.markup) or \
files_changed(pelican.theme, ['']): files_changed(pelican.theme, ['']):
if files_found_error == False:
files_found_error = True
pelican.run() pelican.run()
# reload also if settings.py changed # reload also if settings.py changed
@ -289,6 +293,10 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
logger.warning("Keyboard interrupt, quitting.") logger.warning("Keyboard interrupt, quitting.")
break 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: except Exception, e:
logger.warning( logger.warning(
"Caught exception \"{}\". Reloading.".format(e) "Caught exception \"{}\". Reloading.".format(e)

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy
import locale import locale
import logging import logging
import functools import functools
@ -29,7 +30,7 @@ class Page(object):
if not metadata: if not metadata:
metadata = {} metadata = {}
if not settings: if not settings:
settings = _DEFAULT_CONFIG settings = copy.deepcopy(_DEFAULT_CONFIG)
self.settings = settings self.settings = settings
self._content = content self._content = content

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy
import imp import imp
import inspect import inspect
import os import os
@ -81,7 +82,7 @@ def read_settings(filename=None):
if filename: if filename:
local_settings = get_settings_from_file(filename) local_settings = get_settings_from_file(filename)
else: else:
local_settings = _DEFAULT_CONFIG local_settings = copy.deepcopy(_DEFAULT_CONFIG)
configured_settings = configure_settings(local_settings, None, filename) configured_settings = configure_settings(local_settings, None, filename)
return configured_settings return configured_settings
@ -89,10 +90,9 @@ def read_settings(filename=None):
def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG): def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
""" """
Load settings from a module, returning a dict. Load settings from a module, returning a dict.
""" """
context = default_settings.copy() context = copy.deepcopy(default_settings)
if module is not None: if module is not None:
context.update( context.update(
(k, v) for k, v in inspect.getmembers(module) if k.isupper() (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): def configure_settings(settings, default_settings=None, filename=None):
"""Provide optimizations, error checking, and warnings for loaded settings""" """Provide optimizations, error checking, and warnings for loaded settings"""
if default_settings is None: if default_settings is None:
default_settings = _DEFAULT_CONFIG default_settings = copy.deepcopy(_DEFAULT_CONFIG)
# Make the paths relative to the settings file # Make the paths relative to the settings file
if filename: if filename:
@ -138,7 +138,7 @@ def configure_settings(settings, default_settings=None, filename=None):
for locale_ in locales: for locale_ in locales:
try: try:
locale.setlocale(locale.LC_ALL, locale_) locale.setlocale(locale.LC_ALL, locale_)
break # break if it is successfull break # break if it is successful
except locale.Error: except locale.Error:
pass pass
else: else:

View file

@ -1,7 +1,7 @@
PELICAN=$pelican PELICAN=$pelican
PELICANOPTS=$pelicanopts PELICANOPTS=$pelicanopts
BASEDIR=$$(PWD) BASEDIR=$$(CURDIR)
INPUTDIR=$$(BASEDIR)/content INPUTDIR=$$(BASEDIR)/content
OUTPUTDIR=$$(BASEDIR)/output OUTPUTDIR=$$(BASEDIR)/output
CONFFILE=$$(BASEDIR)/pelicanconf.py CONFFILE=$$(BASEDIR)/pelicanconf.py

View file

@ -1,11 +1,11 @@
#!/bin/bash #!/usr/bin/env bash
## ##
# This section should match your Makefile # This section should match your Makefile
## ##
PELICAN=$pelican PELICAN=$pelican
PELICANOPTS=$pelicanopts PELICANOPTS=$pelicanopts
BASEDIR=$$(PWD) BASEDIR=$$(pwd)
INPUTDIR=$$BASEDIR/content INPUTDIR=$$BASEDIR/content
OUTPUTDIR=$$BASEDIR/output OUTPUTDIR=$$BASEDIR/output
CONFFILE=$$BASEDIR/pelicanconf.py CONFFILE=$$BASEDIR/pelicanconf.py
@ -65,6 +65,7 @@ function start_up(){
python -m SimpleHTTPServer & python -m SimpleHTTPServer &
echo $$! > $$SRV_PID echo $$! > $$SRV_PID
cd $$BASEDIR cd $$BASEDIR
sleep 1 && echo 'Pelican and SimpleHTTPServer processes now running in background.'
} }
### ###

View file

@ -14,6 +14,9 @@ from operator import attrgetter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NoFilesError(Exception):
pass
def get_date(string): def get_date(string):
"""Return a datetime object from a 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 yield os.stat(os.path.join(root, f)).st_mtime
global LAST_MTIME global LAST_MTIME
mtime = max(file_times(path)) try:
if mtime > LAST_MTIME: mtime = max(file_times(path))
LAST_MTIME = mtime if mtime > LAST_MTIME:
return True LAST_MTIME = mtime
return True
except ValueError:
raise NoFilesError("No files with the given extension(s) found.")
return False return False

View file

@ -63,29 +63,26 @@ class TestArticlesGenerator(unittest.TestCase):
def test_generate_context(self): def test_generate_context(self):
settings = _DEFAULT_CONFIG.copy() generator = self.get_populated_generator()
settings['ARTICLE_DIR'] = 'content' articles = self.distill_articles(generator.articles)
settings['DEFAULT_CATEGORY'] = 'Default' articles_expected = [
generator = ArticlesGenerator(settings.copy(), settings, CUR_DIR, [u'Article title', 'published', 'Default', 'article'],
_DEFAULT_CONFIG['THEME'], None, [u'Article with template', 'published', 'Default', 'custom'],
_DEFAULT_CONFIG['MARKUP']) [u'Test md File', 'published', 'test', 'article'],
generator.generate_context() [u'This is a super article !', 'published', 'Yeah', 'article'],
for article in generator.articles: [u'This is an article with category !', 'published', 'yeah', 'article'],
relfilepath = os.path.relpath(article.filename, CUR_DIR) [u'This is an article without category !', 'published', 'Default', 'article'],
if relfilepath == os.path.join("TestCategory", [u'This is an article without category !', 'published', 'TestCategory', 'article'],
"article_with_category.rst"): [u'This is a super article !', 'published', 'yeah', 'article']
self.assertEquals(article.category.name, 'yeah') ]
elif relfilepath == os.path.join("TestCategory", self.assertItemsEqual(articles_expected, articles)
"article_without_category.rst"):
self.assertEquals(article.category.name, 'TestCategory')
elif relfilepath == "article_without_category.rst":
self.assertEquals(article.category.name, 'Default')
def test_generate_categories(self):
generator = self.get_populated_generator()
categories = [cat.name for cat, _ in generator.categories] categories = [cat.name for cat, _ in generator.categories]
# assert that the categories are ordered as expected categories_expected = ['Default', 'TestCategory', 'Yeah', 'test', 'yeah']
self.assertEquals( self.assertEquals(categories, categories_expected)
categories, ['Default', 'TestCategory', 'Yeah', 'test',
'yeah'])
def test_direct_templates_save_as_default(self): def test_direct_templates_save_as_default(self):

View file

@ -35,6 +35,18 @@ class TestPelican(unittest.TestCase):
rmtree(self.temp_path) rmtree(self.temp_path)
locale.setlocale(locale.LC_ALL, self.old_locale) 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") @unittest.skip("Test failing")
def test_basic_generation_works(self): def test_basic_generation_works(self):
# when running pelican without settings, it should pick up the default # when running pelican without settings, it should pick up the default
@ -47,27 +59,7 @@ class TestPelican(unittest.TestCase):
pelican.run() pelican.run()
diff = dircmp( diff = dircmp(
self.temp_path, os.sep.join((OUTPUT_PATH, "basic"))) self.temp_path, os.sep.join((OUTPUT_PATH, "basic")))
self.assertEqual(diff.left_only, [], msg="some generated " \ self.assertFilesEqual(diff)
"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.")
def test_custom_generation_works(self): def test_custom_generation_works(self):
# the same thing with a specified set of settings should work # the same thing with a specified set of settings should work
@ -75,24 +67,4 @@ class TestPelican(unittest.TestCase):
settings=read_settings(SAMPLE_CONFIG)) settings=read_settings(SAMPLE_CONFIG))
pelican.run() pelican.run()
diff = dircmp(self.temp_path, os.sep.join((OUTPUT_PATH, "custom"))) diff = dircmp(self.temp_path, os.sep.join((OUTPUT_PATH, "custom")))
self.assertEqual(diff.left_only, [], msg="some generated " \ self.assertFilesEqual(diff)
"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.")

View file

@ -1,6 +1,7 @@
import copy
from os.path import dirname, abspath, join 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 from .support import unittest
@ -31,7 +32,26 @@ class TestSettingsConfiguration(unittest.TestCase):
def test_read_empty_settings(self): def test_read_empty_settings(self):
"""providing no file should return the default values.""" """providing no file should return the default values."""
settings = read_settings(None) 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): def test_configure_settings(self):
"""Manipulations to settings should be applied correctly.""" """Manipulations to settings should be applied correctly."""

View file

@ -6,6 +6,7 @@ import time
from pelican import utils from pelican import utils
from .support import get_article, unittest from .support import get_article, unittest
from pelican.utils import NoFilesError
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
@ -74,7 +75,8 @@ class TestUtils(unittest.TestCase):
self.assertNotIn(fr_article1, index) self.assertNotIn(fr_article1, index)
def test_files_changed(self): 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') path = os.path.join(os.path.dirname(__file__), 'content')
filename = os.path.join(path, 'article_with_metadata.rst') filename = os.path.join(path, 'article_with_metadata.rst')
@ -90,6 +92,18 @@ class TestUtils(unittest.TestCase):
self.assertEquals(changed, True) self.assertEquals(changed, True)
self.assertAlmostEqual(utils.LAST_MTIME, t, delta=1) 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): def test_clean_output_dir(self):
test_directory = os.path.join(os.path.dirname(__file__), 'clean_output') test_directory = os.path.join(os.path.dirname(__file__), 'clean_output')
content = os.path.join(os.path.dirname(__file__), 'content') content = os.path.join(os.path.dirname(__file__), 'content')