{% endif %}
From 87d86d724c6ae01adf1488b2f65dd0ff1e48ca46 Mon Sep 17 00:00:00 2001
From: Kevin Yap
Date: Sat, 28 Feb 2015 15:33:54 -0800
Subject: [PATCH 0057/1094] Change phrasing and formatting of README
Made a few changes to the README to emphasize Pelican's position as a
general-purpose static site generator, and not just a blogging tool.
See #1645 for more details.
---
README.rst | 59 +++++++++++++++++++++++++++---------------------------
1 file changed, 30 insertions(+), 29 deletions(-)
diff --git a/README.rst b/README.rst
index 564cc77c..a5643514 100644
--- a/README.rst
+++ b/README.rst
@@ -3,57 +3,58 @@ Pelican |build-status| |coverage-status|
Pelican is a static site generator, written in Python_.
-* Write your weblog entries directly with your editor of choice (vim!)
- in reStructuredText_ or Markdown_
-* Includes a simple CLI tool to (re)generate the weblog
-* Easy to interface with DVCSes and web hooks
-* Completely static output is easy to host anywhere
+* Write content in reStructuredText_ or Markdown_ using your editor of choice.
+* Includes a simple command line tool to (re)generate site files.
+* Easy to interface with version control systems and web hooks.
+* Completely static output is simple to host anywhere.
+
Features
--------
Pelican currently supports:
-* Blog articles and pages
-* Comments, via an external service (Disqus). (Please note that while
- useful, Disqus is an external service, and thus the comment data will be
- somewhat outside of your control and potentially subject to data loss.)
-* Theming support (themes are created using Jinja2_ templates)
-* PDF generation of the articles/pages (optional)
+* Blog articles and static pages
+* Integration with external services (ex. Google Analytics and Disqus)
+* Site themes (created using Jinja2_ templates)
* Publication of articles in multiple languages
-* Atom/RSS feeds
-* Code syntax highlighting
-* Import from WordPress, Dotclear, or RSS feeds
-* Integration with external tools: Twitter, Google Analytics, etc. (optional)
-* Fast rebuild times thanks to content caching and selective output writing.
+* Generation of Atom and RSS feeds
+* Syntax highlighting via Pygments_
+* Importing existing content from WordPress, Dotclear, and more services
+* Fast rebuild times due to content caching and selective output writing
-Have a look at the `Pelican documentation`_ for more information.
+Check out `Pelican's documentation`_ for further information.
-Why the name "Pelican"?
------------------------
-
-"Pelican" is an anagram for *calepin*, which means "notebook" in French. ;)
-
-Source code
------------
-
-You can access the source code at: https://github.com/getpelican/pelican
-
-If you feel hackish, have a look at the explanation of `Pelican's internals`_.
How to get help, contribute, or provide feedback
------------------------------------------------
See our `contribution submission and feedback guidelines `_.
+
+Source code
+-----------
+
+Pelican's source code is `hosted on GitHub`_. If you're feeling hackish,
+take a look at `Pelican's internals`_.
+
+
+Why the name "Pelican"?
+-----------------------
+
+"Pelican" is an anagram of *calepin*, which means "notebook" in French.
+
+
.. Links
.. _Python: http://www.python.org/
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
.. _Markdown: http://daringfireball.net/projects/markdown/
.. _Jinja2: http://jinja.pocoo.org/
-.. _`Pelican documentation`: http://docs.getpelican.com/
+.. _Pygments: http://pygments.org/
+.. _`Pelican's documentation`: http://docs.getpelican.com/
.. _`Pelican's internals`: http://docs.getpelican.com/en/latest/internals.html
+.. _`hosted on GitHub`: https://github.com/getpelican/pelican
.. |build-status| image:: https://img.shields.io/travis/getpelican/pelican/master.svg
:target: https://travis-ci.org/getpelican/pelican
From e35ca1d6ff4fd9a31b6dd60b2bb345c2fee0828e Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Thu, 5 Mar 2015 12:04:39 -0800
Subject: [PATCH 0058/1094] Minor improvements to README
---
README.rst | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/README.rst b/README.rst
index a5643514..0bb3bcc8 100644
--- a/README.rst
+++ b/README.rst
@@ -3,10 +3,10 @@ Pelican |build-status| |coverage-status|
Pelican is a static site generator, written in Python_.
-* Write content in reStructuredText_ or Markdown_ using your editor of choice.
-* Includes a simple command line tool to (re)generate site files.
-* Easy to interface with version control systems and web hooks.
-* Completely static output is simple to host anywhere.
+* Write content in reStructuredText_ or Markdown_ using your editor of choice
+* Includes a simple command line tool to (re)generate site files
+* Easy to interface with version control systems and web hooks
+* Completely static output is simple to host anywhere
Features
@@ -14,13 +14,13 @@ Features
Pelican currently supports:
-* Blog articles and static pages
-* Integration with external services (ex. Google Analytics and Disqus)
+* Chronological content (e.g., articles, blog posts) as well as static pages
+* Integration with external services (e.g., Google Analytics and Disqus)
* Site themes (created using Jinja2_ templates)
* Publication of articles in multiple languages
* Generation of Atom and RSS feeds
* Syntax highlighting via Pygments_
-* Importing existing content from WordPress, Dotclear, and more services
+* Importing existing content from WordPress, Dotclear, and other services
* Fast rebuild times due to content caching and selective output writing
Check out `Pelican's documentation`_ for further information.
@@ -35,7 +35,7 @@ See our `contribution submission and feedback guidelines `_.
Source code
-----------
-Pelican's source code is `hosted on GitHub`_. If you're feeling hackish,
+Pelican's source code is `hosted on GitHub`_. If you feel like hacking,
take a look at `Pelican's internals`_.
From 3ea45420152a8465db33fd4a67ac88f1c1426df5 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Tue, 17 Feb 2015 20:05:00 -0500
Subject: [PATCH 0059/1094] Make sure Content uses URLWrappers
---
pelican/contents.py | 14 +++-----------
pelican/readers.py | 4 ++++
pelican/tests/test_contents.py | 11 ++++++-----
pelican/tests/test_generators.py | 32 ++++++++++++++++++++++++++++++++
pelican/tests/test_paginator.py | 4 ++--
5 files changed, 47 insertions(+), 18 deletions(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 074c28be..90121316 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -53,7 +53,7 @@ class Content(object):
self._context = context
self.translations = []
- local_metadata = dict(settings['DEFAULT_METADATA'])
+ local_metadata = dict()
local_metadata.update(metadata)
# set metadata as attributes
@@ -166,21 +166,13 @@ class Content(object):
"""Returns the URL, formatted with the proper values"""
metadata = copy.copy(self.metadata)
path = self.metadata.get('path', self.get_relative_source_path())
- default_category = self.settings['DEFAULT_CATEGORY']
- slug_substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())
metadata.update({
'path': path_to_url(path),
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
'date': getattr(self, 'date', SafeDatetime.now()),
- 'author': slugify(
- getattr(self, 'author', ''),
- slug_substitutions
- ),
- 'category': slugify(
- getattr(self, 'category', default_category),
- slug_substitutions
- )
+ 'author': self.author.slug if hasattr(self, 'author') else '',
+ 'category': self.category.slug if hasattr(self, 'category') else ''
})
return metadata
diff --git a/pelican/readers.py b/pelican/readers.py
index 731fb5da..a9b71bed 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -537,6 +537,10 @@ def find_empty_alt(content, path):
def default_metadata(settings=None, process=None):
metadata = {}
if settings:
+ for name, value in dict(settings.get('DEFAULT_METADATA', {})).items():
+ if process:
+ value = process(name, value)
+ metadata[name] = value
if 'DEFAULT_CATEGORY' in settings:
value = settings['DEFAULT_CATEGORY']
if process:
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 4b692e29..004d512e 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -8,7 +8,7 @@ import os.path
from pelican.tests.support import unittest, get_settings
-from pelican.contents import Page, Article, Static, URLWrapper
+from pelican.contents import Page, Article, Static, URLWrapper, Author, Category
from pelican.settings import DEFAULT_CONFIG
from pelican.utils import path_to_url, truncate_html_words, SafeDatetime, posix_join
from pelican.signals import content_object_init
@@ -33,7 +33,7 @@ class TestPage(unittest.TestCase):
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
- 'author': 'Blogger',
+ 'author': Author('Blogger', DEFAULT_CONFIG),
},
'source_path': '/path/to/file/foo.ext'
}
@@ -374,7 +374,8 @@ class TestPage(unittest.TestCase):
content = Page(**args)
assert content.authors == [content.author]
args['metadata'].pop('author')
- args['metadata']['authors'] = ['First Author', 'Second Author']
+ args['metadata']['authors'] = [Author('First Author', DEFAULT_CONFIG),
+ Author('Second Author', DEFAULT_CONFIG)]
content = Page(**args)
assert content.authors
assert content.author == content.authors[0]
@@ -396,8 +397,8 @@ class TestArticle(TestPage):
settings['ARTICLE_URL'] = '{author}/{category}/{slug}/'
settings['ARTICLE_SAVE_AS'] = '{author}/{category}/{slug}/index.html'
article_kwargs = self._copy_page_kwargs()
- article_kwargs['metadata']['author'] = "O'Brien"
- article_kwargs['metadata']['category'] = 'C# & stuff'
+ article_kwargs['metadata']['author'] = Author("O'Brien", settings)
+ article_kwargs['metadata']['category'] = Category('C# & stuff', settings)
article_kwargs['metadata']['title'] = 'fnord'
article_kwargs['settings'] = settings
article = Article(**article_kwargs)
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 9f38c002..acf767f2 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -413,6 +413,38 @@ class TestArticlesGenerator(unittest.TestCase):
generator.generate_context()
generator.readers.read_file.assert_called_count == orig_call_count
+ def test_standard_metadata_in_default_metadata(self):
+ settings = get_settings(filenames={})
+ settings['CACHE_CONTENT'] = False
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['DEFAULT_METADATA'] = (('author', 'Blogger'),
+ # category will be ignored in favor of
+ # DEFAULT_CATEGORY
+ ('category', 'Random'),
+ ('tags', 'general, untagged'))
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ authors = sorted([author.name for author, _ in generator.authors])
+ authors_expected = sorted(['Alexis Métaireau', 'Blogger',
+ 'First Author', 'Second Author'])
+ self.assertEqual(authors, authors_expected)
+
+ categories = sorted([category.name
+ for category, _ in generator.categories])
+ categories_expected = [
+ sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']),
+ sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書'])]
+ self.assertIn(categories, categories_expected)
+
+ tags = sorted([tag.name for tag in generator.tags])
+ tags_expected = sorted(['bar', 'foo', 'foobar', 'general', 'untagged',
+ 'パイソン', 'マック'])
+ self.assertEqual(tags, tags_expected)
+
class TestPageGenerator(unittest.TestCase):
# Note: Every time you want to test for a new field; Make sure the test
diff --git a/pelican/tests/test_paginator.py b/pelican/tests/test_paginator.py
index 5494fda8..002d9e07 100644
--- a/pelican/tests/test_paginator.py
+++ b/pelican/tests/test_paginator.py
@@ -5,7 +5,7 @@ import locale
from pelican.tests.support import unittest, get_settings
from pelican.paginator import Paginator
-from pelican.contents import Article
+from pelican.contents import Article, Author
from pelican.settings import DEFAULT_CONFIG
from jinja2.utils import generate_lorem_ipsum
@@ -26,7 +26,6 @@ class TestPage(unittest.TestCase):
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
- 'author': 'Blogger',
},
'source_path': '/path/to/file/foo.ext'
}
@@ -49,6 +48,7 @@ class TestPage(unittest.TestCase):
key=lambda r: r[0],
)
+ self.page_kwargs['metadata']['author'] = Author('Blogger', settings)
object_list = [Article(**self.page_kwargs), Article(**self.page_kwargs)]
paginator = Paginator('foobar.foo', object_list, settings)
page = paginator.page(1)
From 4e896c427ddef6b9a19088dfc653f7b8c15f5c08 Mon Sep 17 00:00:00 2001
From: Kevin Yap
Date: Fri, 6 Mar 2015 23:51:26 -0800
Subject: [PATCH 0060/1094] Standardize formatting of .travis.yml
Use 2 spaces for indentation.
---
.travis.yml | 25 ++++++++++++-------------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index a052252b..f5a7f04f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,25 +1,24 @@
language: python
python:
- - "2.7"
- - "3.3"
- - "3.4"
+ - "2.7"
+ - "3.3"
+ - "3.4"
addons:
apt_packages:
- pandoc
before_install:
- - sudo apt-get update -qq
- - sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
+ - sudo apt-get update -qq
+ - sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
install:
- - pip install .
- - pip install -r dev_requirements.txt
- - pip install nose-cov
+ - pip install .
+ - pip install -r dev_requirements.txt
+ - pip install nose-cov
script: nosetests -sv --with-coverage --cover-package=pelican pelican
after_success:
- # Report coverage results to coveralls.io
- pip install coveralls
- coveralls
notifications:
- irc:
- channels:
- - "irc.freenode.org#pelican"
- on_success: change
+ irc:
+ channels:
+ - "irc.freenode.org#pelican"
+ on_success: change
From ffe71d324d4812925b2eeddbc52b66f5ebbf3801 Mon Sep 17 00:00:00 2001
From: robertlagrant
Date: Fri, 13 Mar 2015 13:42:56 +0200
Subject: [PATCH 0061/1094] Change docs wording on cache regen for #1630
---
docs/settings.rst | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 11444d2e..9fb97883 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -847,13 +847,11 @@ can be invoked by passing the ``--archive`` flag).
The cache files are Python pickles, so they may not be readable by
different versions of Python as the pickle format often changes. If
-such an error is encountered, the cache files have to be rebuilt by
-removing them and re-running Pelican, or by using the Pelican
-command-line option ``--ignore-cache``. The cache files also have to
-be rebuilt when changing the ``GZIP_CACHE`` setting for cache file
-reading to work properly.
+such an error is encountered, it is caught and the cache file is
+rebuilt automatically in the new format. The cache files will also be
+rebuilt after the ``GZIP_CACHE`` setting has been changed.
-The ``--ignore-cache`` command-line option is also useful when the
+The ``--ignore-cache`` command-line option is useful when the
whole cache needs to be regenerated, such as when making modifications
to the settings file that will affect the cached content, or just for
debugging purposes. When Pelican runs in autoreload mode, modification
From 0f7f328206b4b3eb085335aa86c620150143ee6e Mon Sep 17 00:00:00 2001
From: Kevin Yap
Date: Fri, 13 Mar 2015 23:01:31 -0700
Subject: [PATCH 0062/1094] Remove a couple of unused imports
As reported by Pyflakes.
---
pelican/contents.py | 2 +-
pelican/writers.py | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 074c28be..a680c411 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
-from six.moves.urllib.parse import (unquote, urlparse, urlunparse)
+from six.moves.urllib.parse import urlparse, urlunparse
import copy
import locale
diff --git a/pelican/writers.py b/pelican/writers.py
index bf32e272..e90a0004 100644
--- a/pelican/writers.py
+++ b/pelican/writers.py
@@ -3,7 +3,6 @@ from __future__ import with_statement, unicode_literals, print_function
import six
import os
-import locale
import logging
if not six.PY3:
From ef737c22393174571fe17a6175eb98465c6ec246 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Sat, 14 Mar 2015 13:36:51 -0400
Subject: [PATCH 0063/1094] Use `--relative-urls` only if it is specified
Otherwise, `RELATIVE_URLS` in the config file is ignored and
`RELATIVE_URLS` is set to `False` if `--relative-urls` is not
specified.
Fixes an issue introduced in #1592
---
pelican/__init__.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 3013744d..056c45ef 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -321,7 +321,8 @@ def get_config(args):
config['CACHE_PATH'] = args.cache_path
if args.selected_paths:
config['WRITE_SELECTED'] = args.selected_paths.split(',')
- config['RELATIVE_URLS'] = args.relative_paths
+ if args.relative_paths:
+ config['RELATIVE_URLS'] = args.relative_paths
config['DEBUG'] = args.verbosity == logging.DEBUG
# argparse returns bytes in Py2. There is no definite answer as to which
From 875c4a5e05d818c776be3019506921b863b13dc0 Mon Sep 17 00:00:00 2001
From: Anton Antonov
Date: Tue, 17 Mar 2015 01:23:29 +0200
Subject: [PATCH 0064/1094] Nitpick Content decorators
A bit more readable this way.
---
pelican/contents.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 96466a94..005d045c 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -90,7 +90,7 @@ class Content(object):
self.in_default_lang = (self.lang == default_lang)
- # create the slug if not existing, generate slug according to
+ # create the slug if not existing, generate slug according to
# setting of SLUG_ATTRIBUTE
if not hasattr(self, 'slug'):
if settings['SLUGIFY_SOURCE'] == 'title' and hasattr(self, 'title'):
@@ -308,8 +308,13 @@ class Content(object):
"""Dummy function"""
pass
- url = property(functools.partial(get_url_setting, key='url'))
- save_as = property(functools.partial(get_url_setting, key='save_as'))
+ @property
+ def url(self):
+ return self.get_url_setting('url')
+
+ @property
+ def save_as(self):
+ return self.get_url_setting('save_as')
def _get_template(self):
if hasattr(self, 'template') and self.template is not None:
From db2e5174502787e447d3df32298cf950c6c894ae Mon Sep 17 00:00:00 2001
From: Forest
Date: Mon, 29 Sep 2014 22:51:13 -0700
Subject: [PATCH 0065/1094] Ignore empty metadata. Fixes #1469. Fixes #1398.
Some metadata values cause problems when empty. For example, a markdown file
containing a Slug: line with no additional text causing Pelican to produce a
file named ".html" instead of generating a proper file name. Others, like
those created by a PATH_METADATA regex, must be preserved even if empty,
so things like PAGE_URL="filename{customvalue}.html" will always work.
Essentially, we want to discard empty metadata that we know will be useless
or problematic. This is better than raising an exception because (a) it
allows users to deliberately keep empty metadata in their source files for
filling in later, and (b) users shouldn't be forced to fix empty metadata
created by blog migration tools (see #1398).
The metadata processors are the ideal place to do this, because they know
the type of data they are handling and whether an empty value is wanted.
Unfortunately, they can't discard items, and neither can process_metadata(),
because their return values are always saved by calling code. We can't
safely change the calling code, because some of it lives in custom reader
classes out in the field, and we don't want to break those working systems.
Discarding empty values at the time of use isn't good enough, because that
still allows useless empty values in a source file to override configured
defaults.
My solution:
- When processing a list of values, a metadata processor will omit any
unwanted empty ones from the list it returns.
- When processing an entirely unwanted value, it will return something easily
identifiable that will pass through the reader code.
- When collecting the processed metadata, read_file() will filter out items
identified as unwanted.
These metadata are affected by this change:
author, authors, category, slug, status, tags.
I also removed a bit of now-superfluous code from generators.py that was
discarding empty authors at the time of use.
---
pelican/generators.py | 4 +---
pelican/readers.py | 48 ++++++++++++++++++++++++++++++++++---------
2 files changed, 39 insertions(+), 13 deletions(-)
diff --git a/pelican/generators.py b/pelican/generators.py
index f0a6d264..75bd6b2a 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -544,10 +544,8 @@ class ArticlesGenerator(CachingGenerator):
if hasattr(article, 'tags'):
for tag in article.tags:
self.tags[tag].append(article)
- # ignore blank authors as well as undefined
for author in getattr(article, 'authors', []):
- if author.name != '':
- self.authors[author].append(article)
+ self.authors[author].append(article)
# sort the articles by date
self.articles.sort(key=attrgetter('date'), reverse=True)
self.dates = list(self.articles)
diff --git a/pelican/readers.py b/pelican/readers.py
index a9b71bed..3656cd96 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -28,16 +28,44 @@ from pelican.contents import Page, Category, Tag, Author
from pelican.utils import get_date, pelican_open, FileStampDataCacher, SafeDatetime, posixize_path
+def strip_split(text, sep=','):
+ """Return a list of stripped, non-empty substrings, delimited by sep."""
+ items = [x.strip() for x in text.split(sep)]
+ return [x for x in items if x]
+
+
+# Metadata processors have no way to discard an unwanted value, so we have
+# them return this value instead to signal that it should be discarded later.
+# This means that _filter_discardable_metadata() must be called on processed
+# metadata dicts before use, to remove the items with the special value.
+_DISCARD = object()
+
+
+def _process_if_nonempty(processor, name, settings):
+ """Removes extra whitespace from name and applies a metadata processor.
+ If name is empty or all whitespace, returns _DISCARD instead.
+ """
+ name = name.strip()
+ return processor(name, settings) if name else _DISCARD
+
+
METADATA_PROCESSORS = {
- 'tags': lambda x, y: [Tag(tag, y) for tag in x.split(',')],
+ 'tags': lambda x, y: [Tag(tag, y) for tag in strip_split(x)] or _DISCARD,
'date': lambda x, y: get_date(x.replace('_', ' ')),
'modified': lambda x, y: get_date(x),
- 'status': lambda x, y: x.strip(),
- 'category': Category,
- 'author': Author,
- 'authors': lambda x, y: [Author(author.strip(), y) for author in x.split(',')],
+ 'status': lambda x, y: x.strip() or _DISCARD,
+ 'category': lambda x, y: _process_if_nonempty(Category, x, y),
+ 'author': lambda x, y: _process_if_nonempty(Author, x, y),
+ 'authors': lambda x, y: [Author(a, y) for a in strip_split(x)] or _DISCARD,
+ 'slug': lambda x, y: x.strip() or _DISCARD,
}
+
+def _filter_discardable_metadata(metadata):
+ """Return a copy of a dict, minus any items marked as discardable."""
+ return {name: val for name, val in metadata.items() if val is not _DISCARD}
+
+
logger = logging.getLogger(__name__)
class BaseReader(object):
@@ -447,14 +475,14 @@ class Readers(FileStampDataCacher):
reader = self.readers[fmt]
- metadata = default_metadata(
- settings=self.settings, process=reader.process_metadata)
+ metadata = _filter_discardable_metadata(default_metadata(
+ settings=self.settings, process=reader.process_metadata))
metadata.update(path_metadata(
full_path=path, source_path=source_path,
settings=self.settings))
- metadata.update(parse_path_metadata(
+ metadata.update(_filter_discardable_metadata(parse_path_metadata(
source_path=source_path, settings=self.settings,
- process=reader.process_metadata))
+ process=reader.process_metadata)))
reader_name = reader.__class__.__name__
metadata['reader'] = reader_name.replace('Reader', '').lower()
@@ -462,7 +490,7 @@ class Readers(FileStampDataCacher):
if content is None:
content, reader_metadata = reader.read(path)
self.cache_data(path, (content, reader_metadata))
- metadata.update(reader_metadata)
+ metadata.update(_filter_discardable_metadata(reader_metadata))
if content:
# find images with empty alt
From 7ad649e3b79c20d9cbb783148e659824c46fbf8c Mon Sep 17 00:00:00 2001
From: Forest
Date: Fri, 31 Oct 2014 23:05:19 -0700
Subject: [PATCH 0066/1094] Guess mime type with python-magic if available.
Fixes #1514.
---
pelican/server.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/pelican/server.py b/pelican/server.py
index 0a8dc1b6..60252e1f 100644
--- a/pelican/server.py
+++ b/pelican/server.py
@@ -12,6 +12,11 @@ try:
except ImportError:
import socketserver # NOQA
+try:
+ from magic import from_file as magic_from_file
+except ImportError:
+ magic_from_file = None
+
PORT = len(sys.argv) in (2, 3) and int(sys.argv[1]) or 8000
SERVER = len(sys.argv) == 3 and sys.argv[2] or ""
SUFFIXES = ['', '.html', '/index.html']
@@ -39,6 +44,18 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
logging.warning("Unable to find `%s` or variations.",
self.original_path)
+ def guess_type(self, path):
+ """Guess at the mime type for the specified file.
+ """
+ mimetype = srvmod.SimpleHTTPRequestHandler.guess_type(self, path)
+
+ # If the default guess is too generic, try the python-magic library
+ if mimetype == 'application/octet-stream' and magic_from_file:
+ mimetype = magic_from_file(path, mime=True)
+
+ return mimetype
+
+
Handler = ComplexHTTPRequestHandler
socketserver.TCPServer.allow_reuse_address = True
From 7b4ceb29744b3a00f39925047a0dafef0176214f Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Fri, 3 Apr 2015 18:58:52 -0400
Subject: [PATCH 0067/1094] Make `pelican.server` importable and use it in `fab
serve`
`fab serve` and `make devserver` use different HTTP Handlers and as a
result they behave differently. This makes sure `fab serve` also uses
the Handler defined in `pelican.server` in order to get rid of the
inconsistency.
---
pelican/server.py | 46 ++++++++++++---------------
pelican/tools/templates/fabfile.py.in | 5 +--
2 files changed, 23 insertions(+), 28 deletions(-)
diff --git a/pelican/server.py b/pelican/server.py
index 60252e1f..f58ac085 100644
--- a/pelican/server.py
+++ b/pelican/server.py
@@ -2,30 +2,22 @@ from __future__ import print_function
import os
import sys
import logging
-try:
- import SimpleHTTPServer as srvmod
-except ImportError:
- import http.server as srvmod # NOQA
-try:
- import SocketServer as socketserver
-except ImportError:
- import socketserver # NOQA
+from six.moves import SimpleHTTPServer as srvmod
+from six.moves import socketserver
try:
from magic import from_file as magic_from_file
except ImportError:
magic_from_file = None
-PORT = len(sys.argv) in (2, 3) and int(sys.argv[1]) or 8000
-SERVER = len(sys.argv) == 3 and sys.argv[2] or ""
-SUFFIXES = ['', '.html', '/index.html']
-
class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
+ SUFFIXES = ['', '.html', '/index.html']
+
def do_GET(self):
# Try to detect file by applying various suffixes
- for suffix in SUFFIXES:
+ for suffix in self.SUFFIXES:
if not hasattr(self, 'original_path'):
self.original_path = self.path
@@ -56,19 +48,21 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
return mimetype
-Handler = ComplexHTTPRequestHandler
+if __name__ == '__main__':
+ PORT = len(sys.argv) in (2, 3) and int(sys.argv[1]) or 8000
+ SERVER = len(sys.argv) == 3 and sys.argv[2] or ""
-socketserver.TCPServer.allow_reuse_address = True
-try:
- httpd = socketserver.TCPServer((SERVER, PORT), Handler)
-except OSError as e:
- logging.error("Could not listen on port %s, server %s.", PORT, SERVER)
- sys.exit(getattr(e, 'exitcode', 1))
+ socketserver.TCPServer.allow_reuse_address = True
+ try:
+ httpd = socketserver.TCPServer((SERVER, PORT), ComplexHTTPRequestHandler)
+ except OSError as e:
+ logging.error("Could not listen on port %s, server %s.", PORT, SERVER)
+ sys.exit(getattr(e, 'exitcode', 1))
-logging.info("Serving at port %s, server %s.", PORT, SERVER)
-try:
- httpd.serve_forever()
-except KeyboardInterrupt as e:
- logging.info("Shutting down server.")
- httpd.socket.close()
+ logging.info("Serving at port %s, server %s.", PORT, SERVER)
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt as e:
+ logging.info("Shutting down server.")
+ httpd.socket.close()
diff --git a/pelican/tools/templates/fabfile.py.in b/pelican/tools/templates/fabfile.py.in
index a8ab586b..bcc66f6a 100644
--- a/pelican/tools/templates/fabfile.py.in
+++ b/pelican/tools/templates/fabfile.py.in
@@ -3,9 +3,10 @@ import fabric.contrib.project as project
import os
import shutil
import sys
-import SimpleHTTPServer
import SocketServer
+from pelican.server import ComplexHTTPRequestHandler
+
# Local path configuration (can be absolute or relative to fabfile)
env.deploy_path = 'output'
DEPLOY_PATH = env.deploy_path
@@ -51,7 +52,7 @@ def serve():
class AddressReuseTCPServer(SocketServer.TCPServer):
allow_reuse_address = True
- server = AddressReuseTCPServer(('', PORT), SimpleHTTPServer.SimpleHTTPRequestHandler)
+ server = AddressReuseTCPServer(('', PORT), ComplexHTTPRequestHandler)
sys.stderr.write('Serving on port {0} ...\n'.format(PORT))
server.serve_forever()
From 946e95172fa85e7d5967226f3dbc723f099f94c9 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Sat, 4 Apr 2015 15:17:59 -0400
Subject: [PATCH 0068/1094] Use pelican.server in Quickstart docs
---
docs/quickstart.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 122d65b5..c4f5a897 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -61,10 +61,10 @@ Preview your site
-----------------
Open a new terminal session and run the following commands to switch to your
-``output`` directory and launch Python's built-in web server::
+``output`` directory and launch Pelican's web server::
cd ~/projects/yoursite/output
- python -m SimpleHTTPServer # -m http.server if you use python3
+ python -m pelican.server
Preview your site by navigating to http://localhost:8000/ in your browser.
From 5ca808ed598679dfef4574892c79e1d262632051 Mon Sep 17 00:00:00 2001
From: winlu
Date: Mon, 6 Apr 2015 09:49:01 +0200
Subject: [PATCH 0069/1094] fix broken internal metadata link
---
docs/settings.rst | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 9fb97883..73837181 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -470,14 +470,14 @@ your resume, and a contact page — you could have::
Path metadata
=============
-Not all metadata needs to be `embedded in source file itself`__. For
-example, blog posts are often named following a ``YYYY-MM-DD-SLUG.rst``
-pattern, or nested into ``YYYY/MM/DD-SLUG`` directories. To extract
-metadata from the filename or path, set ``FILENAME_METADATA`` or
-``PATH_METADATA`` to regular expressions that use Python's `group name
-notation`_ ``(?P…)``. If you want to attach additional metadata
-but don't want to encode it in the path, you can set
-``EXTRA_PATH_METADATA``:
+Not all metadata needs to be :ref:`embedded in source file itself
+`. For example, blog posts are often named
+following a ``YYYY-MM-DD-SLUG.rst`` pattern, or nested into
+``YYYY/MM/DD-SLUG`` directories. To extract metadata from the
+filename or path, set ``FILENAME_METADATA`` or ``PATH_METADATA`` to
+regular expressions that use Python's `group name notation`_ ``(?P…)``.
+If you want to attach additional metadata but don't want to encode
+it in the path, you can set ``EXTRA_PATH_METADATA``:
.. parsed-literal::
@@ -506,7 +506,6 @@ particular file:
'static/robots.txt': {'path': 'robots.txt'},
}
-__ internal_metadata__
.. _group name notation:
http://docs.python.org/3/library/re.html#regular-expression-syntax
From 2e590d0e866a8679b99978819a75edded3c1bcef Mon Sep 17 00:00:00 2001
From: winlu
Date: Mon, 6 Apr 2015 14:59:06 +0200
Subject: [PATCH 0070/1094] maintain test_readers
* move all metadata tests to use a single function call (assertDictHasSubset)
* add tests for assertDictHasSubset
* correct some tests that iterated over metadata instead of expected metadata, resulting in metadata that was expected to be there but was not
* correct resulting broken tests
* add additional tests for EXTRA_PATH_METADATA
* make MdReaderTest fail if Markdown is not available instead of skipping each method individually
---
pelican/tests/test_readers.py | 181 ++++++++++++++++++++++++++--------
1 file changed, 140 insertions(+), 41 deletions(-)
diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py
index cb657673..d390fb48 100644
--- a/pelican/tests/test_readers.py
+++ b/pelican/tests/test_readers.py
@@ -22,6 +22,52 @@ class ReaderTest(unittest.TestCase):
r = readers.Readers(settings=get_settings(**kwargs))
return r.read_file(base_path=CONTENT_PATH, path=path)
+ def assertDictHasSubset(self, dictionary, subset):
+ for key, value in subset.items():
+ if key in dictionary:
+ real_value = dictionary.get(key)
+ self.assertEqual(
+ value,
+ real_value,
+ str('Expected %r to have value %r, but was %r')
+ % (key, value, real_value))
+ else:
+ self.fail(
+ str('Expected %r to have value %r, but was not in Dict')
+ % (key, value))
+
+class TestAssertDictHasSubset(ReaderTest):
+ def setUp(self):
+ self.dictionary = {
+ 'key-a' : 'val-a',
+ 'key-b' : 'val-b'}
+
+ def tearDown(self):
+ self.dictionary = None
+
+ def test_subset(self):
+ self.assertDictHasSubset(self.dictionary, {'key-a':'val-a'})
+
+ def test_equal(self):
+ self.assertDictHasSubset(self.dictionary, self.dictionary)
+
+ def test_fail_not_set(self):
+ self.assertRaisesRegexp(
+ AssertionError,
+ 'Expected.*key-c.*to have value.*val-c.*but was not in Dict',
+ self.assertDictHasSubset,
+ self.dictionary,
+ {'key-c':'val-c'}
+ )
+
+ def test_fail_wrong_val(self):
+ self.assertRaisesRegexp(
+ AssertionError,
+ 'Expected .*key-a.* to have value .*val-b.* but was .*val-a.*',
+ self.assertDictHasSubset,
+ self.dictionary,
+ {'key-a':'val-b'}
+ )
class DefaultReaderTest(ReaderTest):
@@ -48,8 +94,7 @@ class RstReaderTest(ReaderTest):
'custom_field': 'http://notmyidea.org',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_with_filename_metadata(self):
page = self.read_file(
@@ -61,8 +106,7 @@ class RstReaderTest(ReaderTest):
'title': 'Rst with filename metadata',
'reader': 'rst',
}
- for key, value in page.metadata.items():
- self.assertEqual(value, expected[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
page = self.read_file(
path='2012-11-29_rst_w_filename_meta#foo-bar.rst',
@@ -74,13 +118,12 @@ class RstReaderTest(ReaderTest):
'date': SafeDatetime(2012, 11, 29),
'reader': 'rst',
}
- for key, value in page.metadata.items():
- self.assertEqual(value, expected[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
page = self.read_file(
path='2012-11-29_rst_w_filename_meta#foo-bar.rst',
FILENAME_METADATA=(
- '(?P\d{4}-\d{2}-\d{2})_'
+ '(?P\d{4}-\d{2}-\d{2})'
'_(?P.*)'
'#(?P.*)-(?P.*)'))
expected = {
@@ -88,12 +131,11 @@ class RstReaderTest(ReaderTest):
'author': 'Alexis Métaireau',
'title': 'Rst with filename metadata',
'date': SafeDatetime(2012, 11, 29),
- 'slug': 'article_with_filename_metadata',
+ 'slug': 'rst_w_filename_meta',
'mymeta': 'foo',
'reader': 'rst',
}
- for key, value in page.metadata.items():
- self.assertEqual(value, expected[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_metadata_key_lowercase(self):
# Keys of metadata should be lowercase.
@@ -105,6 +147,81 @@ class RstReaderTest(ReaderTest):
self.assertEqual('Yeah', metadata.get('category'),
'Value keeps case.')
+ def test_article_extra_path_metadata(self):
+ input_with_metadata = '2012-11-29_rst_w_filename_meta#foo-bar.rst'
+ page_metadata = self.read_file(
+ path=input_with_metadata,
+ FILENAME_METADATA=(
+ '(?P\d{4}-\d{2}-\d{2})'
+ '_(?P.*)'
+ '#(?P.*)-(?P.*)'
+ ),
+ EXTRA_PATH_METADATA={
+ input_with_metadata: {
+ 'key-1a': 'value-1a',
+ 'key-1b': 'value-1b'
+ }
+ }
+ )
+ expected_metadata = {
+ 'category': 'yeah',
+ 'author' : 'Alexis Métaireau',
+ 'title': 'Rst with filename metadata',
+ 'date': SafeDatetime(2012, 11, 29),
+ 'slug': 'rst_w_filename_meta',
+ 'mymeta': 'foo',
+ 'reader': 'rst',
+ 'key-1a': 'value-1a',
+ 'key-1b': 'value-1b'
+ }
+ self.assertDictHasSubset(page_metadata.metadata, expected_metadata)
+
+ input_file_path_without_metadata = 'article.rst'
+ page_without_metadata = self.read_file(
+ path=input_file_path_without_metadata,
+ EXTRA_PATH_METADATA={
+ input_file_path_without_metadata: {
+ 'author': 'Charlès Overwrite'}
+ }
+ )
+ expected_without_metadata = {
+ 'category' : 'misc',
+ 'author' : 'Charlès Overwrite',
+ 'title' : 'Article title',
+ 'reader' : 'rst',
+ }
+ self.assertDictHasSubset(
+ page_without_metadata.metadata,
+ expected_without_metadata)
+
+ def test_article_extra_path_metadata_dont_overwrite(self):
+ #EXTRA_PATH_METADATA['author'] should get ignored
+ #since we don't overwrite already set values
+ input_file_path = '2012-11-29_rst_w_filename_meta#foo-bar.rst'
+ page = self.read_file(
+ path=input_file_path,
+ FILENAME_METADATA=(
+ '(?P\d{4}-\d{2}-\d{2})'
+ '_(?P.*)'
+ '#(?P.*)-(?P.*)'),
+ EXTRA_PATH_METADATA={
+ input_file_path: {
+ 'author': 'Charlès Overwrite',
+ 'key-1b': 'value-1b'}
+ }
+ )
+ expected = {
+ 'category': 'yeah',
+ 'author' : 'Alexis Métaireau',
+ 'title': 'Rst with filename metadata',
+ 'date': SafeDatetime(2012, 11, 29),
+ 'slug': 'rst_w_filename_meta',
+ 'mymeta': 'foo',
+ 'reader': 'rst',
+ 'key-1b': 'value-1b'
+ }
+ self.assertDictHasSubset(page.metadata, expected)
+
def test_typogrify(self):
# if nothing is specified in the settings, the content should be
# unmodified
@@ -205,13 +322,11 @@ class RstReaderTest(ReaderTest):
'authors': ['First Author', 'Second Author']
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
-
+ self.assertDictHasSubset(page.metadata, expected)
+@unittest.skipUnless(readers.Markdown, "markdown isn't installed")
class MdReaderTest(ReaderTest):
- @unittest.skipUnless(readers.Markdown, "markdown isn't installed")
def test_article_with_metadata(self):
reader = readers.MarkdownReader(settings=get_settings())
content, metadata = reader.read(
@@ -224,8 +339,7 @@ class MdReaderTest(ReaderTest):
'modified': SafeDatetime(2010, 12, 2, 10, 20),
'tags': ['foo', 'bar', 'foobar'],
}
- for key, value in metadata.items():
- self.assertEqual(value, expected[key], key)
+ self.assertDictHasSubset(metadata, expected)
content, metadata = reader.read(
_path('article_with_markdown_and_nonascii_summary.md'))
@@ -238,10 +352,8 @@ class MdReaderTest(ReaderTest):
'tags': ['パイソン', 'マック'],
'slug': 'python-virtualenv-on-mac-osx-mountain-lion-10.8',
}
- for key, value in metadata.items():
- self.assertEqual(value, expected[key], key)
+ self.assertDictHasSubset(metadata, expected)
- @unittest.skipUnless(readers.Markdown, "markdown isn't installed")
def test_article_with_footnote(self):
reader = readers.MarkdownReader(settings=get_settings())
content, metadata = reader.read(
@@ -271,7 +383,6 @@ class MdReaderTest(ReaderTest):
'should be supported.'),
'date': SafeDatetime(2012, 10, 31),
'modified': SafeDatetime(2012, 11, 1),
- 'slug': 'article-with-markdown-containing-footnotes',
'multiline': [
'Line Metadata should be handle properly.',
'See syntax of Meta-Data extension of Python Markdown package:',
@@ -282,10 +393,8 @@ class MdReaderTest(ReaderTest):
]
}
self.assertEqual(content, expected_content)
- for key, value in metadata.items():
- self.assertEqual(value, expected_metadata[key], key)
+ self.assertDictHasSubset(metadata, expected_metadata)
- @unittest.skipUnless(readers.Markdown, "markdown isn't installed")
def test_article_with_file_extensions(self):
reader = readers.MarkdownReader(settings=get_settings())
# test to ensure the md file extension is being processed by the
@@ -322,7 +431,6 @@ class MdReaderTest(ReaderTest):
" the mdown extension.")
self.assertEqual(content, expected)
- @unittest.skipUnless(readers.Markdown, "markdown isn't installed")
def test_article_with_markdown_markup_extension(self):
# test to ensure the markdown markup extension is being processed as
# expected
@@ -342,7 +450,6 @@ class MdReaderTest(ReaderTest):
self.assertEqual(page.content, expected)
- @unittest.skipUnless(readers.Markdown, "markdown isn't installed")
def test_article_with_filename_metadata(self):
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
@@ -351,8 +458,7 @@ class MdReaderTest(ReaderTest):
'category': 'yeah',
'author': 'Alexis Métaireau',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
@@ -362,8 +468,7 @@ class MdReaderTest(ReaderTest):
'author': 'Alexis Métaireau',
'date': SafeDatetime(2012, 11, 30),
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
@@ -378,8 +483,7 @@ class MdReaderTest(ReaderTest):
'slug': 'md_w_filename_meta',
'mymeta': 'foo',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
class HTMLReaderTest(ReaderTest):
@@ -397,8 +501,7 @@ class HTMLReaderTest(ReaderTest):
'tags': ['foo', 'bar', 'foobar'],
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_with_metadata(self):
page = self.read_file(path='article_with_metadata.html')
@@ -412,8 +515,7 @@ class HTMLReaderTest(ReaderTest):
'custom_field': 'http://notmyidea.org',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_with_multiple_authors(self):
page = self.read_file(path='article_with_multiple_authors.html')
@@ -421,8 +523,7 @@ class HTMLReaderTest(ReaderTest):
'authors': ['First Author', 'Second Author']
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_with_metadata_and_contents_attrib(self):
page = self.read_file(path='article_with_metadata_and_contents.html')
@@ -435,8 +536,7 @@ class HTMLReaderTest(ReaderTest):
'tags': ['foo', 'bar', 'foobar'],
'custom_field': 'http://notmyidea.org',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
def test_article_with_null_attributes(self):
page = self.read_file(path='article_with_null_attributes.html')
@@ -460,5 +560,4 @@ class HTMLReaderTest(ReaderTest):
'title': 'Article with Nonconformant HTML meta tags',
}
- for key, value in expected.items():
- self.assertEqual(value, page.metadata[key], key)
+ self.assertDictHasSubset(page.metadata, expected)
From 0f7938ccb71e34cd020026095fcb086b99c79856 Mon Sep 17 00:00:00 2001
From: Alberto Scotto
Date: Wed, 8 Apr 2015 03:32:48 +0200
Subject: [PATCH 0071/1094] fix the meta tags added in #1028
The attribute 'contents' should be 'content'.
See http://www.w3schools.com/tags/att_meta_content.asp
---
pelican/themes/simple/templates/article.html | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html
index e81261c5..d558183d 100644
--- a/pelican/themes/simple/templates/article.html
+++ b/pelican/themes/simple/templates/article.html
@@ -2,15 +2,15 @@
{% block head %}
{{ super() }}
{% for keyword in article.keywords %}
-
+
{% endfor %}
{% for description in article.description %}
-
+
{% endfor %}
{% for tag in article.tags %}
-
+
{% endfor %}
{% endblock %}
From adc5c4b572195d12cea1df4d0f8daa49ec2e168f Mon Sep 17 00:00:00 2001
From: winlu
Date: Fri, 10 Apr 2015 09:02:12 +0200
Subject: [PATCH 0072/1094] fix broken refs
---
docs/themes.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/themes.rst b/docs/themes.rst
index fd4ec8f9..7f2693ff 100644
--- a/docs/themes.rst
+++ b/docs/themes.rst
@@ -367,7 +367,7 @@ source_path Full system path of the article source file.
status The article status, can be any of 'published' or
'draft'.
summary Rendered summary content.
-tags List of :ref:`Tag `
+tags List of :ref:`Tag `
objects.
template Template name to use for rendering.
title Title of the article.
@@ -422,7 +422,7 @@ source_path Full system path of the page source file.
status The page status, can be any of 'published' or
'draft'.
summary Rendered summary content.
-tags List of :ref:`Tag `
+tags List of :ref:`Tag `
objects.
template Template name to use for rendering.
title Title of the page.
From bf7d113caaa9d9fcc860e6687a63589ea00aea10 Mon Sep 17 00:00:00 2001
From: winlu
Date: Sat, 11 Apr 2015 22:45:31 +0200
Subject: [PATCH 0073/1094] add skips for tests relying on dev_requirements
modules
---
pelican/tests/test_generators.py | 16 +++++++++++++++-
pelican/tests/test_importer.py | 7 +++++++
pelican/tests/test_rstdirectives.py | 10 ++++++++--
3 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index acf767f2..4fb70826 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -6,7 +6,10 @@ from codecs import open
try:
from unittest.mock import MagicMock
except ImportError:
- from mock import MagicMock
+ try:
+ from mock import MagicMock
+ except ImportError:
+ MagicMock = False
from shutil import rmtree
from tempfile import mkdtemp
@@ -112,6 +115,7 @@ class TestArticlesGenerator(unittest.TestCase):
return [[article.title, article.status, article.category.name,
article.template] for article in articles]
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_generate_feeds(self):
settings = get_settings()
settings['CACHE_PATH'] = self.temp_cache
@@ -215,6 +219,7 @@ class TestArticlesGenerator(unittest.TestCase):
categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu']
self.assertEqual(sorted(categories), sorted(categories_expected))
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_default(self):
settings = get_settings(filenames={})
@@ -228,6 +233,7 @@ class TestArticlesGenerator(unittest.TestCase):
generator.get_template("archives"), settings,
blog=True, paginated={}, page_name='archives')
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_modified(self):
settings = get_settings()
@@ -244,6 +250,7 @@ class TestArticlesGenerator(unittest.TestCase):
blog=True, paginated={},
page_name='archives/index')
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_false(self):
settings = get_settings()
@@ -268,6 +275,7 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertIn(custom_template, self.articles)
self.assertIn(standard_template, self.articles)
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_period_in_timeperiod_archive(self):
"""
Test that the context of a generated period_archive is passed
@@ -347,6 +355,7 @@ class TestArticlesGenerator(unittest.TestCase):
authors_expected = ['alexis-metaireau', 'first-author', 'second-author']
self.assertEqual(sorted(authors), sorted(authors_expected))
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_object_caching(self):
"""Test Article objects caching at the generator level"""
settings = get_settings(filenames={})
@@ -367,6 +376,7 @@ class TestArticlesGenerator(unittest.TestCase):
generator.generate_context()
generator.readers.read_file.assert_called_count == 0
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_reader_content_caching(self):
"""Test raw content caching at the reader level"""
settings = get_settings(filenames={})
@@ -389,6 +399,7 @@ class TestArticlesGenerator(unittest.TestCase):
for reader in readers.values():
reader.read.assert_called_count == 0
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_ignore_cache(self):
"""Test that all the articles are read again when not loading cache
@@ -492,6 +503,7 @@ class TestPageGenerator(unittest.TestCase):
self.assertEqual(sorted(pages_expected), sorted(pages))
self.assertEqual(sorted(hidden_pages_expected), sorted(hidden_pages))
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_object_caching(self):
"""Test Page objects caching at the generator level"""
settings = get_settings(filenames={})
@@ -512,6 +524,7 @@ class TestPageGenerator(unittest.TestCase):
generator.generate_context()
generator.readers.read_file.assert_called_count == 0
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_reader_content_caching(self):
"""Test raw content caching at the reader level"""
settings = get_settings(filenames={})
@@ -534,6 +547,7 @@ class TestPageGenerator(unittest.TestCase):
for reader in readers.values():
reader.read.assert_called_count == 0
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_ignore_cache(self):
"""Test that all the pages are read again when not loading cache
diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py
index c108bc52..4ace5ccc 100644
--- a/pelican/tests/test_importer.py
+++ b/pelican/tests/test_importer.py
@@ -26,6 +26,12 @@ try:
except ImportError:
BeautifulSoup = False # NOQA
+try:
+ import bs4.builder._lxml as LXML
+except ImportError:
+ LXML = False
+
+
@skipIfNoExecutable(['pandoc', '--version'])
@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
@@ -302,6 +308,7 @@ class TestBuildHeader(unittest.TestCase):
@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
+@unittest.skipUnless(LXML, 'Needs lxml module')
class TestWordpressXMLAttachements(unittest.TestCase):
def setUp(self):
self.old_locale = locale.setlocale(locale.LC_ALL)
diff --git a/pelican/tests/test_rstdirectives.py b/pelican/tests/test_rstdirectives.py
index ae863b30..7c5f8adf 100644
--- a/pelican/tests/test_rstdirectives.py
+++ b/pelican/tests/test_rstdirectives.py
@@ -1,9 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
-
-from mock import Mock
+try:
+ from unittest.mock import Mock
+except ImportError:
+ try:
+ from mock import Mock
+ except ImportError:
+ Mock = False
from pelican.tests.support import unittest
+@unittest.skipUnless(Mock, 'Needs Mock module')
class Test_abbr_role(unittest.TestCase):
def call_it(self, text):
from pelican.rstdirectives import abbr_role
From f864418b9e1af0a980052dc4c7d51519c57e3649 Mon Sep 17 00:00:00 2001
From: "Daniel M. Drucker"
Date: Mon, 13 Apr 2015 11:12:52 -0400
Subject: [PATCH 0074/1094] make the quickstart work
The quickstart was worded confusingly - it said "from your project directory", which implied doing a `cd ..` down to the `projects` dir, which would cause `pelican content` to fail. In fact, you need to be in the `yoursite` directory, which is the directory that has the `content` directory in it.
---
docs/quickstart.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index c4f5a897..ef5256c2 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -49,7 +49,7 @@ Given that this example article is in Markdown format, save it as
Generate your site
------------------
-From your project directory, run the ``pelican`` command to generate your site::
+From your site directory, run the ``pelican`` command to generate your site::
pelican content
From a45a917766089c9e733cf417f942a4b9c651a154 Mon Sep 17 00:00:00 2001
From: winlu
Date: Sat, 11 Apr 2015 15:02:53 +0200
Subject: [PATCH 0075/1094] test docs via travis
* move build environment into tox
* add new environment installing sphinx and testing for doc errors
* reorganize dependency installs for easier management
---
.travis.yml | 16 +++++++---------
dev_requirements.txt | 3 ---
tox.ini | 29 +++++++++++++++++++++++------
3 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index f5a7f04f..cc124ea4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,11 @@
language: python
python:
- "2.7"
- - "3.3"
- - "3.4"
+env:
+ - TOX_ENV=docs
+ - TOX_ENV=py27
+ - TOX_ENV=py33
+ - TOX_ENV=py34
addons:
apt_packages:
- pandoc
@@ -10,13 +13,8 @@ before_install:
- sudo apt-get update -qq
- sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
install:
- - pip install .
- - pip install -r dev_requirements.txt
- - pip install nose-cov
-script: nosetests -sv --with-coverage --cover-package=pelican pelican
-after_success:
- - pip install coveralls
- - coveralls
+ - pip install tox
+script: tox -e $TOX_ENV
notifications:
irc:
channels:
diff --git a/dev_requirements.txt b/dev_requirements.txt
index a7c10719..028cbebd 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -10,6 +10,3 @@ typogrify
# To perform release
bumpr==0.2.0
wheel
-
-# For docs theme
-sphinx_rtd_theme
diff --git a/tox.ini b/tox.ini
index 5dd36c36..11b3cae8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,28 @@
-# This tests the unified codebase (py27, py33) of Pelican.
-# depends on some external libraries that aren't released yet.
-
[tox]
-envlist = py27,py33,py34
+envlist = py27,py33,py34,docs
[testenv]
-commands =
- python -m unittest discover
+basepython =
+ py27: python2.7
+ py33: python3.3
+ py34: python3.4
+usedevelop=True
deps =
-rdev_requirements.txt
+ nose
+ nose-cov
+ coveralls
+
+commands =
+ {envpython} --version
+ nosetests -sv --with-coverage --cover-package=pelican pelican
+ coveralls
+
+[testenv:docs]
+basepython = python2.7
+deps =
+ sphinx
+ sphinx_rtd_theme
+changedir = docs
+commands =
+ sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
From 2e84e30ad38175819ef97103f9720693c0218fc7 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Tue, 14 Apr 2015 15:19:16 -0400
Subject: [PATCH 0076/1094] Fixes #1695: replace with list comprehension for
py3 compatibility
---
pelican/tools/pelican_quickstart.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
index 74633630..2f1129dc 100755
--- a/pelican/tools/pelican_quickstart.py
+++ b/pelican/tools/pelican_quickstart.py
@@ -162,7 +162,7 @@ def ask(question, answer=str_compat, default=None, l=None):
def ask_timezone(question, default, tzurl):
"""Prompt for time zone and validate input"""
- lower_tz = map(str.lower, pytz.all_timezones)
+ lower_tz = [tz.lower() for tz in pytz.all_timezones]
while True:
r = ask(question, str_compat, default)
r = r.strip().replace(' ', '_').lower()
From 90a283bc44da299e0860dcc27516bfefe327fd56 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Thu, 16 Apr 2015 19:05:15 -0400
Subject: [PATCH 0077/1094] Fixes #1686: posixize paths in context['filenames']
to fix windows issues
---
pelican/generators.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/pelican/generators.py b/pelican/generators.py
index 75bd6b2a..0ce6439d 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -22,7 +22,8 @@ from jinja2 import (Environment, FileSystemLoader, PrefixLoader, ChoiceLoader,
from pelican.contents import Article, Draft, Page, Static, is_valid_content
from pelican.readers import Readers
from pelican.utils import (copy, process_translations, mkdir_p, DateFormatter,
- FileStampDataCacher, python_2_unicode_compatible)
+ FileStampDataCacher, python_2_unicode_compatible,
+ posixize_path)
from pelican import signals
@@ -160,7 +161,7 @@ class Generator(object):
(For example, one that was missing mandatory metadata.)
The path argument is expected to be relative to self.path.
"""
- self.context['filenames'][os.path.normpath(path)] = None
+ self.context['filenames'][posixize_path(os.path.normpath(path))] = None
def _is_potential_source_path(self, path):
"""Return True if path was supposed to be used as a source file.
@@ -168,7 +169,7 @@ class Generator(object):
before this method is called, even if they failed to process.)
The path argument is expected to be relative to self.path.
"""
- return os.path.normpath(path) in self.context['filenames']
+ return posixize_path(os.path.normpath(path)) in self.context['filenames']
def _update_context(self, items):
"""Update the context with the given items from the currrent
From dceea3aabe36e206d014e2540f86572dcb7c8859 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Thu, 16 Apr 2015 19:16:23 -0400
Subject: [PATCH 0078/1094] Change URLWrapper.slug to a property
Main goal is to delay `slugify` call until `slug` is needed. `slugify`
can be expensive depending on the input string (see #1172). Changing it
to a property gives plugins time to place custom `slug`s before `name`
is unnecessarily slugified.
With this change, default behavior is same except the time slugification
happens. But if you set custom slug, `slugify` won't be used at all.
So, this is a partial solution to #1172. The rest, setting a custom slug,
would best be handled by a plugin.
---
pelican/urlwrappers.py | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py
index 06cfa2ab..60bc6a3a 100644
--- a/pelican/urlwrappers.py
+++ b/pelican/urlwrappers.py
@@ -13,12 +13,10 @@ logger = logging.getLogger(__name__)
@functools.total_ordering
class URLWrapper(object):
def __init__(self, name, settings):
- # next 2 lines are redundant with the setter of the name property
- # but are here for clarity
self.settings = settings
self._name = name
- self.slug = slugify(name, self.settings.get('SLUG_SUBSTITUTIONS', ()))
- self.name = name
+ self._slug = None
+ self._slug_from_name = True
@property
def name(self):
@@ -27,11 +25,28 @@ class URLWrapper(object):
@name.setter
def name(self, name):
self._name = name
- self.slug = slugify(name, self.settings.get('SLUG_SUBSTITUTIONS', ()))
+ # if slug wasn't explicitly set, it needs to be regenerated from name
+ # so, changing name should reset slug for slugification
+ if self._slug_from_name:
+ self._slug = None
+
+ @property
+ def slug(self):
+ if self._slug is None:
+ self._slug = slugify(self.name,
+ self.settings.get('SLUG_SUBSTITUTIONS', ()))
+ return self._slug
+
+ @slug.setter
+ def slug(self, slug):
+ # if slug is expliticly set, changing name won't alter slug
+ self._slug_from_name = False
+ self._slug = slug
def as_dict(self):
d = self.__dict__
d['name'] = self.name
+ d['slug'] = self.slug
return d
def __hash__(self):
From 9dd4080fe6162849aec0946cc8406b04da764157 Mon Sep 17 00:00:00 2001
From: winlu
Date: Mon, 20 Apr 2015 12:16:05 +0200
Subject: [PATCH 0079/1094] remove tag_cloud from core
---
docs/settings.rst | 47 -------------------------------------------
pelican/generators.py | 32 ++---------------------------
pelican/settings.py | 2 --
3 files changed, 2 insertions(+), 79 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 73837181..2eec1e50 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -620,53 +620,6 @@ This would cause the first page to be written to
``page/{number}`` directories.
-Tag cloud
-=========
-
-If you want to generate a tag cloud with all your tags, you can do so using the
-following settings.
-
-================================================ =====================================================
-Setting name (followed by default value) What does it do?
-================================================ =====================================================
-``TAG_CLOUD_STEPS = 4`` Count of different font sizes in the tag
- cloud.
-``TAG_CLOUD_MAX_ITEMS = 100`` Maximum number of tags in the cloud.
-================================================ =====================================================
-
-The default theme does not include a tag cloud, but it is pretty easy to add one::
-
-
@@ -79,7 +79,7 @@
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
- s.src = '//' + disqus_shortname + '.disqus.com/count.js';
+ s.src = 'https://' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
diff --git a/pelican/tests/output/custom_locale/theme/css/main.css b/pelican/tests/output/custom_locale/theme/css/main.css
index 9d7221a2..03a77e69 100644
--- a/pelican/tests/output/custom_locale/theme/css/main.css
+++ b/pelican/tests/output/custom_locale/theme/css/main.css
@@ -12,7 +12,7 @@
@import url("reset.css");
@import url("pygment.css");
@import url("typogrify.css");
-@import url(//fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
+@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
/***** Global *****/
/* Body */
diff --git a/pelican/themes/notmyidea/static/css/main.css b/pelican/themes/notmyidea/static/css/main.css
index 9d7221a2..03a77e69 100644
--- a/pelican/themes/notmyidea/static/css/main.css
+++ b/pelican/themes/notmyidea/static/css/main.css
@@ -12,7 +12,7 @@
@import url("reset.css");
@import url("pygment.css");
@import url("typogrify.css");
-@import url(//fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
+@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
/***** Global *****/
/* Body */
diff --git a/pelican/themes/notmyidea/templates/analytics.html b/pelican/themes/notmyidea/templates/analytics.html
index e27adf82..465754e2 100644
--- a/pelican/themes/notmyidea/templates/analytics.html
+++ b/pelican/themes/notmyidea/templates/analytics.html
@@ -5,7 +5,7 @@
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ ga.src = 'https://ssl.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html
index d6b4cb35..45ab4044 100644
--- a/pelican/themes/notmyidea/templates/base.html
+++ b/pelican/themes/notmyidea/templates/base.html
@@ -12,7 +12,7 @@
{% endif %}
diff --git a/pelican/themes/notmyidea/templates/disqus_script.html b/pelican/themes/notmyidea/templates/disqus_script.html
index 4ee419bb..23ec532a 100644
--- a/pelican/themes/notmyidea/templates/disqus_script.html
+++ b/pelican/themes/notmyidea/templates/disqus_script.html
@@ -4,7 +4,7 @@
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
- s.src = '//' + disqus_shortname + '.disqus.com/count.js';
+ s.src = 'https://' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
diff --git a/pelican/themes/notmyidea/templates/github.html b/pelican/themes/notmyidea/templates/github.html
index 75592ac6..ccc401fe 100644
--- a/pelican/themes/notmyidea/templates/github.html
+++ b/pelican/themes/notmyidea/templates/github.html
@@ -1,9 +1,9 @@
{% if GITHUB_URL %}
{% if GITHUB_POSITION != "left" %}
-
+
{% else %}
-
+
{% endif %}
{% endif %}
diff --git a/pelican/themes/notmyidea/templates/twitter.html b/pelican/themes/notmyidea/templates/twitter.html
index 7247a0c6..bf78c32d 100644
--- a/pelican/themes/notmyidea/templates/twitter.html
+++ b/pelican/themes/notmyidea/templates/twitter.html
@@ -1,3 +1,3 @@
{% if TWITTER_USERNAME %}
-Tweet
+Tweet
{% endif %}
diff --git a/pelican/themes/simple/templates/gosquared.html b/pelican/themes/simple/templates/gosquared.html
index f47efcf4..49ccbbef 100644
--- a/pelican/themes/simple/templates/gosquared.html
+++ b/pelican/themes/simple/templates/gosquared.html
@@ -5,7 +5,7 @@
(function(w){
function gs(){
w._gstc_lt=+(new Date); var d=document;
- var g = d.createElement("script"); g.type = "text/javascript"; g.async = true; g.src = "//d1l6p2sc9645hc.cloudfront.net/tracker.js";
+ var g = d.createElement("script"); g.type = "text/javascript"; g.async = true; g.src = "https://d1l6p2sc9645hc.cloudfront.net/tracker.js";
var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(g, s);
}
w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs);
From 39fd4936b59bdf92734880eee76a7f0a10a05f39 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Wed, 3 Jun 2015 08:58:59 +0200
Subject: [PATCH 0091/1094] improve result output of a pelican run
* display count of hidden pages
* pluralize only if necessary
* add maybe_pluralize to utils
* add tests for maybe_pluralize
---
pelican/__init__.py | 35 ++++++++++++++++++++++++++++-------
pelican/tests/test_utils.py | 6 ++++++
pelican/utils.py | 16 ++++++++++++++++
3 files changed, 50 insertions(+), 7 deletions(-)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 12da111a..de320897 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -22,7 +22,8 @@ from pelican.generators import (ArticlesGenerator, PagesGenerator,
TemplatePagesGenerator)
from pelican.readers import Readers
from pelican.settings import read_settings
-from pelican.utils import clean_output_dir, folder_watcher, file_watcher
+from pelican.utils import (clean_output_dir, folder_watcher,
+ file_watcher, maybe_pluralize)
from pelican.writers import Writer
__version__ = "3.5.0"
@@ -183,12 +184,32 @@ class Pelican(object):
pages_generator = next(g for g in generators
if isinstance(g, PagesGenerator))
- print('Done: Processed {} article(s), {} draft(s) and {} page(s) in ' \
- '{:.2f} seconds.'.format(
- len(articles_generator.articles) + len(articles_generator.translations),
- len(articles_generator.drafts) + \
- len(articles_generator.drafts_translations),
- len(pages_generator.pages) + len(pages_generator.translations),
+ pluralized_articles = maybe_pluralize(
+ len(articles_generator.articles) +
+ len(articles_generator.translations),
+ 'article',
+ 'articles')
+ pluralized_drafts = maybe_pluralize(
+ len(articles_generator.drafts) +
+ len(articles_generator.drafts_translations),
+ 'draft',
+ 'drafts')
+ pluralized_pages = maybe_pluralize(
+ len(pages_generator.pages) +
+ len(pages_generator.translations),
+ 'page',
+ 'pages')
+ pluralized_hidden_pages = maybe_pluralize(
+ len(pages_generator.hidden_pages) +
+ len(pages_generator.hidden_translations),
+ 'hidden page',
+ 'hidden pages')
+
+ print('Done: Processed {}, {}, {} and {} in {:.2f} seconds.'.format(
+ pluralized_articles,
+ pluralized_drafts,
+ pluralized_pages,
+ pluralized_hidden_pages,
time.time() - start_time))
def get_generator_classes(self):
diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py
index c6550459..f5a60584 100644
--- a/pelican/tests/test_utils.py
+++ b/pelican/tests/test_utils.py
@@ -358,6 +358,12 @@ class TestUtils(LoggedTestCase):
locale.setlocale(locale.LC_ALL, old_locale)
+ def test_maybe_pluralize(self):
+ self.assertEqual(utils.maybe_pluralize(0, 'Article', 'Articles'), '0 Articles')
+ self.assertEqual(utils.maybe_pluralize(1, 'Article', 'Articles'), '1 Article')
+ self.assertEqual(utils.maybe_pluralize(2, 'Article', 'Articles'), '2 Articles')
+
+
class TestCopy(unittest.TestCase):
'''Tests the copy utility'''
diff --git a/pelican/utils.py b/pelican/utils.py
index caac8e61..f8f52206 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -767,3 +767,19 @@ def path_to_file_url(path):
'''Convert file-system path to file:// URL'''
return six.moves.urllib_parse.urljoin(
"file://", six.moves.urllib.request.pathname2url(path))
+
+
+def maybe_pluralize(count, singular, plural):
+ '''
+ Returns a formatted string containing count and plural if count is not 1
+ Returns count and singular if count is 1
+
+ maybe_pluralize(0, 'Article', 'Articles') -> '0 Articles'
+ maybe_pluralize(1, 'Article', 'Articles') -> '1 Article'
+ maybe_pluralize(2, 'Article', 'Articles') -> '2 Articles'
+
+ '''
+ selection = plural
+ if count == 1:
+ selection = singular
+ return '{} {}'.format(count, selection)
From 551909893ef663c602a6562b18d96ec3f33a3c81 Mon Sep 17 00:00:00 2001
From: Steven Maude
Date: Thu, 28 May 2015 22:01:05 +0100
Subject: [PATCH 0092/1094] Document how to use a publish configuration
Explain how to import settings from pelicanconf.py into publishconf.py.
---
docs/publish.rst | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/publish.rst b/docs/publish.rst
index 8c7d4c19..309e8b42 100644
--- a/docs/publish.rst
+++ b/docs/publish.rst
@@ -61,6 +61,15 @@ feeds, etc.) that you may have defined::
pelican content -s publishconf.py
+To base your publish configuration on top of your ``pelicanconf.py``, you
+can import your ``pelicanconf`` settings by including the following line in
+your ``publishconf.py``::
+
+ from pelicanconf import *
+
+If you have generated a ``publishconf.py`` using ``pelican-quickstart``,
+this line is included by default.
+
The steps for deploying your site will depend on where it will be hosted.
If you have SSH access to a server running Nginx or Apache, you might use the
``rsync`` tool to transmit your site files::
From b6f391d8e93857850faef3e22a0c526945cfb6cf Mon Sep 17 00:00:00 2001
From: Steven Maude
Date: Thu, 28 May 2015 22:41:40 +0100
Subject: [PATCH 0093/1094] Document make publish
Also clarify the distinction between make html and make publish.
---
docs/publish.rst | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/docs/publish.rst b/docs/publish.rst
index 309e8b42..226662ed 100644
--- a/docs/publish.rst
+++ b/docs/publish.rst
@@ -160,10 +160,16 @@ and thus doesn't require installing anything else in order to use it. The
downside is that non-POSIX systems (e.g., Windows) do not include ``make``,
and installing it on those systems can be a non-trivial task.
-If you want to use ``make`` to generate your site, run::
+If you want to use ``make`` to generate your site using the settings in
+``pelicanconf.py``, run::
make html
+To generate the site for production, using the settings in ``publishconf.py``,
+run::
+
+ make publish
+
If you'd prefer to have Pelican automatically regenerate your site every time a
change is detected (which is handy when testing locally), use the following
command instead::
From 735a4452dfde6aa9327406f842692a171e72a92f Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Wed, 3 Jun 2015 17:01:06 -0400
Subject: [PATCH 0094/1094] Fix exception logging
Adds exception type to the exception logging.
Removes the extra logging for autoreload in debug mode, since `raise`
will make it caught by the global `try/except` below and it'll be
logged there.
This replaces #1723 and also removes extra exception logging caused
in #1718.
---
pelican/__init__.py | 1 -
pelican/log.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 12da111a..2073564c 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -444,7 +444,6 @@ def main():
except Exception as e:
if (args.verbosity == logging.DEBUG):
- logger.critical('Internal failure: %r', e, exc_info=True)
raise
logger.warning(
'Caught exception "%s". Reloading.', e)
diff --git a/pelican/log.py b/pelican/log.py
index cd750927..c83c5810 100644
--- a/pelican/log.py
+++ b/pelican/log.py
@@ -149,7 +149,7 @@ class SafeLogger(logging.Logger):
so convert the message to unicode with the correct encoding
'''
if isinstance(arg, Exception):
- text = str(arg)
+ text = '%s: %s' % (arg.__class__.__name__, arg)
if six.PY2:
text = text.decode(self._exc_encoding)
return text
From c9183808021e54aa9b949939ff289b07c4443b7e Mon Sep 17 00:00:00 2001
From: Zack Weinberg
Date: Sat, 21 Mar 2015 21:54:06 -0400
Subject: [PATCH 0095/1094] Support semicolon-separated author/tag lists.
Idea borrowed from Docutils. This allows one to write author lists in
lastname,firstname format. The code change also means that readers with
fancy metadata that can natively represent lists (e.g. Docutils itself,
or MD-Yaml) don't have to merge 'em back together for process_metadata's
sake.
---
docs/content.rst | 6 ++++
pelican/readers.py | 30 ++++++++++++++-----
.../article_with_multiple_authors_list.rst | 10 +++++++
...rticle_with_multiple_authors_semicolon.rst | 6 ++++
pelican/tests/test_generators.py | 7 +++--
pelican/tests/test_readers.py | 17 +++++++++++
6 files changed, 67 insertions(+), 9 deletions(-)
create mode 100644 pelican/tests/content/article_with_multiple_authors_list.rst
create mode 100644 pelican/tests/content/article_with_multiple_authors_semicolon.rst
diff --git a/docs/content.rst b/docs/content.rst
index 4b19515f..0e3310f1 100644
--- a/docs/content.rst
+++ b/docs/content.rst
@@ -35,6 +35,12 @@ this metadata in text files via the following syntax (give your file the
:authors: Alexis Metaireau, Conan Doyle
:summary: Short version for index and feeds
+Author and tag lists may be semicolon-separated instead, which allows
+you to write authors and tags containing commas::
+
+ :tags: pelican, publishing tool; pelican, bird
+ :authors: Metaireau, Alexis; Doyle, Conan
+
Pelican implements an extension to reStructuredText to enable support for the
``abbr`` HTML tag. To use it, write something like this in your post::
diff --git a/pelican/readers.py b/pelican/readers.py
index 3656cd96..bbc72a73 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -27,11 +27,25 @@ from pelican import signals
from pelican.contents import Page, Category, Tag, Author
from pelican.utils import get_date, pelican_open, FileStampDataCacher, SafeDatetime, posixize_path
+def ensure_metadata_list(text):
+ """Canonicalize the format of a list of authors or tags. This works
+ the same way as Docutils' "authors" field: if it's already a list,
+ those boundaries are preserved; otherwise, it must be a string;
+ if the string contains semicolons, it is split on semicolons;
+ otherwise, it is split on commas. This allows you to write
+ author lists in either "Jane Doe, John Doe" or "Doe, Jane; Doe, John"
+ format.
-def strip_split(text, sep=','):
- """Return a list of stripped, non-empty substrings, delimited by sep."""
- items = [x.strip() for x in text.split(sep)]
- return [x for x in items if x]
+ Regardless, all list items undergo .strip() before returning, and
+ empty items are discarded.
+ """
+ if isinstance(text, six.text_type):
+ if ';' in text:
+ text = text.split(';')
+ else:
+ text = text.split(',')
+
+ return [v for v in (w.strip() for w in text) if v]
# Metadata processors have no way to discard an unwanted value, so we have
@@ -50,13 +64,16 @@ def _process_if_nonempty(processor, name, settings):
METADATA_PROCESSORS = {
- 'tags': lambda x, y: [Tag(tag, y) for tag in strip_split(x)] or _DISCARD,
+ 'tags': lambda x, y: ([Tag(tag, y) for tag in ensure_metadata_list(x)]
+ or _DISCARD),
'date': lambda x, y: get_date(x.replace('_', ' ')),
'modified': lambda x, y: get_date(x),
'status': lambda x, y: x.strip() or _DISCARD,
'category': lambda x, y: _process_if_nonempty(Category, x, y),
'author': lambda x, y: _process_if_nonempty(Author, x, y),
- 'authors': lambda x, y: [Author(a, y) for a in strip_split(x)] or _DISCARD,
+ 'authors': lambda x, y: ([Author(author, y)
+ for author in ensure_metadata_list(x)]
+ or _DISCARD),
'slug': lambda x, y: x.strip() or _DISCARD,
}
@@ -179,7 +196,6 @@ class RstReader(BaseReader):
elif element.tagname == 'authors': # author list
name = element.tagname
value = [element.astext() for element in element.children]
- value = ','.join(value) # METADATA_PROCESSORS expects a string
else: # standard fields (e.g. address)
name = element.tagname
value = element.astext()
diff --git a/pelican/tests/content/article_with_multiple_authors_list.rst b/pelican/tests/content/article_with_multiple_authors_list.rst
new file mode 100644
index 00000000..7da5fae2
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors_list.rst
@@ -0,0 +1,10 @@
+This is an article with multiple authors in list format!
+########################################################
+
+:date: 2014-02-09 02:20
+:modified: 2014-02-09 02:20
+:authors: - Author, First
+ - Author, Second
+
+The author names are in last,first form to verify that
+they are not just getting split on commas.
diff --git a/pelican/tests/content/article_with_multiple_authors_semicolon.rst b/pelican/tests/content/article_with_multiple_authors_semicolon.rst
new file mode 100644
index 00000000..fa76ac4e
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors_semicolon.rst
@@ -0,0 +1,6 @@
+This is an article with multiple authors in lastname, firstname format!
+#######################################################################
+
+:date: 2014-02-09 02:20
+:modified: 2014-02-09 02:20
+:authors: Author, First; Author, Second
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 4fb70826..90261b7a 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -162,6 +162,8 @@ class TestArticlesGenerator(unittest.TestCase):
'article'],
['This is an article with multiple authors!', 'published', 'Default', 'article'],
['This is an article with multiple authors!', 'published', 'Default', 'article'],
+ ['This is an article with multiple authors in list format!', 'published', 'Default', 'article'],
+ ['This is an article with multiple authors in lastname, firstname format!', 'published', 'Default', 'article'],
['This is an article without category !', 'published', 'Default',
'article'],
['This is an article without category !', 'published',
@@ -348,11 +350,11 @@ class TestArticlesGenerator(unittest.TestCase):
def test_generate_authors(self):
"""Check authors generation."""
authors = [author.name for author, _ in self.generator.authors]
- authors_expected = sorted(['Alexis Métaireau', 'First Author', 'Second Author'])
+ authors_expected = sorted(['Alexis Métaireau', 'Author, First', 'Author, Second', 'First Author', 'Second Author'])
self.assertEqual(sorted(authors), authors_expected)
# test for slug
authors = [author.slug for author, _ in self.generator.authors]
- authors_expected = ['alexis-metaireau', 'first-author', 'second-author']
+ authors_expected = ['alexis-metaireau', 'author-first', 'author-second', 'first-author', 'second-author']
self.assertEqual(sorted(authors), sorted(authors_expected))
@unittest.skipUnless(MagicMock, 'Needs Mock module')
@@ -441,6 +443,7 @@ class TestArticlesGenerator(unittest.TestCase):
authors = sorted([author.name for author, _ in generator.authors])
authors_expected = sorted(['Alexis Métaireau', 'Blogger',
+ 'Author, First', 'Author, Second',
'First Author', 'Second Author'])
self.assertEqual(authors, authors_expected)
diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py
index d390fb48..18e5111e 100644
--- a/pelican/tests/test_readers.py
+++ b/pelican/tests/test_readers.py
@@ -324,6 +324,23 @@ class RstReaderTest(ReaderTest):
self.assertDictHasSubset(page.metadata, expected)
+ def test_article_with_multiple_authors_semicolon(self):
+ page = self.read_file(
+ path='article_with_multiple_authors_semicolon.rst')
+ expected = {
+ 'authors': ['Author, First', 'Author, Second']
+ }
+
+ self.assertDictHasSubset(page.metadata, expected)
+
+ def test_article_with_multiple_authors_list(self):
+ page = self.read_file(path='article_with_multiple_authors_list.rst')
+ expected = {
+ 'authors': ['Author, First', 'Author, Second']
+ }
+
+ self.assertDictHasSubset(page.metadata, expected)
+
@unittest.skipUnless(readers.Markdown, "markdown isn't installed")
class MdReaderTest(ReaderTest):
From 0a4837198510db52e3a56c9033f40b53067448f9 Mon Sep 17 00:00:00 2001
From: Zack Weinberg
Date: Thu, 4 Jun 2015 17:53:52 -0400
Subject: [PATCH 0096/1094] Give utils.copy the ability to ignore files (issue
1692)
This requires a significant overhaul because we want to be able to have
IGNORE_FILES apply at every level of a recursively copied directory
(e.g. the theme static directory). Since I was overhauling it anyway
I changed it to use os.walk, which should be more efficient.
---
pelican/utils.py | 80 ++++++++++++++++++++++++++++++------------------
1 file changed, 51 insertions(+), 29 deletions(-)
diff --git a/pelican/utils.py b/pelican/utils.py
index caac8e61..4ccdcc86 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -273,51 +273,73 @@ def slugify(value, substitutions=()):
return value.decode('ascii')
-def copy(source, destination):
+def copy(source, destination, ignores=None):
"""Recursively copy source into destination.
If source is a file, destination has to be a file as well.
-
The function is able to copy either files or directories.
:param source: the source file or directory
:param destination: the destination file or directory
+ :param ignores: either None, or a list of glob patterns;
+ files matching those patterns will _not_ be copied.
"""
+ def walk_error(err):
+ logger.warning("While copying %s: %s: %s",
+ source_, err.filename, err.strerror)
+
source_ = os.path.abspath(os.path.expanduser(source))
destination_ = os.path.abspath(os.path.expanduser(destination))
- if not os.path.exists(destination_) and not os.path.isfile(source_):
- os.makedirs(destination_)
+ if ignores is None:
+ ignores = []
- def recurse(source, destination):
- for entry in os.listdir(source):
- entry_path = os.path.join(source, entry)
- if os.path.isdir(entry_path):
- entry_dest = os.path.join(destination, entry)
- if os.path.exists(entry_dest):
- if not os.path.isdir(entry_dest):
- raise IOError('Failed to copy {0} a directory.'
- .format(entry_dest))
- recurse(entry_path, entry_dest)
- else:
- shutil.copytree(entry_path, entry_dest)
- else:
- shutil.copy2(entry_path, destination)
+ if any(fnmatch.fnmatch(os.path.basename(source), ignore)
+ for ignore in ignores):
+ logger.info('Not copying %s due to ignores', source_)
+ return
-
- if os.path.isdir(source_):
- recurse(source_, destination_)
-
- elif os.path.isfile(source_):
- dest_dir = os.path.dirname(destination_)
- if not os.path.exists(dest_dir):
- os.makedirs(dest_dir)
- shutil.copy2(source_, destination_)
+ if os.path.isfile(source_):
+ dst_dir = os.path.dirname(destination_)
+ if not os.path.exists(dst_dir):
+ logger.info('Creating directory %s', dst_dir)
+ os.makedirs(dst_dir)
logger.info('Copying %s to %s', source_, destination_)
- else:
- logger.warning('Skipped copy %s to %s', source_, destination_)
+ shutil.copy2(source_, destination_)
+ elif os.path.isdir(source_):
+ if not os.path.exists(destination_):
+ logger.info('Creating directory %s', destination_)
+ os.makedirs(destination_)
+ if not os.path.isdir(destination_):
+ logger.warning('Cannot copy %s (a directory) to %s (a file)',
+ source_, destination_)
+ return
+
+ for src_dir, subdirs, others in os.walk(source_):
+ dst_dir = os.path.join(destination_,
+ os.path.relpath(src_dir, source_))
+
+ subdirs[:] = (s for s in subdirs if not any(fnmatch.fnmatch(s, i)
+ for i in ignores))
+ others[:] = (o for o in others if not any(fnmatch.fnmatch(o, i)
+ for i in ignores))
+
+ if not os.path.isdir(dst_dir):
+ logger.info('Creating directory %s', dst_dir)
+ # Parent directories are known to exist, so 'mkdir' suffices.
+ os.mkdir(dst_dir)
+
+ for o in others:
+ src_path = os.path.join(src_dir, o)
+ dst_path = os.path.join(dst_dir, o)
+ if os.path.isfile(src_path):
+ logger.info('Copying %s to %s', src_path, dst_path)
+ shutil.copy2(src_path, dst_path)
+ else:
+ logger.warning('Skipped copy %s (not a file or directory) to %s',
+ src_path, dst_path)
def clean_output_dir(path, retention):
"""Remove all files from output directory except those in retention list"""
From c7b9a339eb2c23b08df5f7b2fb46a1387101e95d Mon Sep 17 00:00:00 2001
From: Zack Weinberg
Date: Thu, 4 Jun 2015 17:52:30 -0400
Subject: [PATCH 0097/1094] Apply 'IGNORE_FILES' globs to directories as well
(issue 1692)
This adjusts the only piece of code that currently looks at IGNORE_FILES.
A subsequent commit will add a new use, with the same semantics.
---
docs/settings.rst | 7 ++++---
pelican/generators.py | 14 +++++++++++---
2 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 99829258..66a44034 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -108,9 +108,10 @@ Setting name (followed by default value, if any)
process or ignore. For example, to avoid processing .html files,
set: ``READERS = {'html': None}``. To add a custom reader for the
``foo`` extension, set: ``READERS = {'foo': FooReader}``
-``IGNORE_FILES = ['.#*']`` A list of file globbing patterns to match against the
- source files to be ignored by the processor. For example,
- the default ``['.#*']`` will ignore emacs lock files.
+``IGNORE_FILES = ['.#*']`` A list of glob patterns. Files and directories matching any
+ of these patterns will be ignored by the processor. For example,
+ the default ``['.#*']`` will ignore emacs lock files, and
+ ``['__pycache__']`` would ignore Python 3's bytecode caches.
``MD_EXTENSIONS =`` ``['codehilite(css_class=highlight)','extra']`` A list of the extensions that the Markdown processor
will use. Refer to the Python Markdown documentation's
`Extensions section `_
diff --git a/pelican/generators.py b/pelican/generators.py
index 99961d6e..82a4b790 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -132,15 +132,23 @@ class Generator(object):
exclusions_by_dirpath.setdefault(parent_path, set()).add(subdir)
files = []
+ ignores = self.settings['IGNORE_FILES']
for path in paths:
# careful: os.path.join() will add a slash when path == ''.
root = os.path.join(self.path, path) if path else self.path
if os.path.isdir(root):
for dirpath, dirs, temp_files in os.walk(root, followlinks=True):
- for e in exclusions_by_dirpath.get(dirpath, ()):
- if e in dirs:
- dirs.remove(e)
+ drop = []
+ excl = exclusions_by_dirpath.get(dirpath, ())
+ for d in dirs:
+ if (d in excl or
+ any(fnmatch.fnmatch(d, ignore)
+ for ignore in ignores)):
+ drop.append(d)
+ for d in drop:
+ dirs.remove(d)
+
reldir = os.path.relpath(dirpath, self.path)
for f in temp_files:
fp = os.path.join(reldir, f)
From 1aac19caaf124cec1114b6a4eb27a56f9d1a5d31 Mon Sep 17 00:00:00 2001
From: Zack Weinberg
Date: Thu, 4 Jun 2015 17:55:40 -0400
Subject: [PATCH 0098/1094] Honor 'IGNORE_FILES' in the static generator (issue
1692)
This means, for instance, that editor backups and similar will no
longer be copied out of a theme static directory into the built
website.
---
pelican/generators.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/pelican/generators.py b/pelican/generators.py
index 82a4b790..d3c657bf 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -670,10 +670,12 @@ class StaticGenerator(Generator):
for path in paths:
if final_path:
copy(os.path.join(source, path),
- os.path.join(output_path, destination, final_path))
+ os.path.join(output_path, destination, final_path),
+ self.settings['IGNORE_FILES'])
else:
copy(os.path.join(source, path),
- os.path.join(output_path, destination, path))
+ os.path.join(output_path, destination, path),
+ self.settings['IGNORE_FILES'])
def generate_context(self):
self.staticfiles = []
From d082d94f25b4591b48c2119425513afba401a0d5 Mon Sep 17 00:00:00 2001
From: abrahamvarricatt
Date: Fri, 5 Jun 2015 22:22:25 +0530
Subject: [PATCH 0099/1094] Add download+PyPI banners to README
---
README.rst | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/README.rst b/README.rst
index 0bb3bcc8..f8b62732 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,5 @@
-Pelican |build-status| |coverage-status|
-========================================
+Pelican |build-status| |coverage-status| |pypi-version| |pypi-downloads|
+========================================================================
Pelican is a static site generator, written in Python_.
@@ -62,4 +62,9 @@ Why the name "Pelican"?
.. |coverage-status| image:: https://img.shields.io/coveralls/getpelican/pelican.svg
:target: https://coveralls.io/r/getpelican/pelican
:alt: Coveralls: code coverage status
-
+.. |pypi-version| image:: https://img.shields.io/pypi/v/pelican.svg
+ :target: https://pypi.python.org/pypi/pelican
+ :alt: PyPI: the Python Package Index
+.. |pypi-downloads| image:: https://img.shields.io/pypi/dm/pelican.svg
+ :target: https://pypi.python.org/pypi/pelican
+ :alt: PyPI: the Python Package Index
From b7e6390f04f73f5b9aac40ea258b84e24b87961c Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Fri, 5 Jun 2015 12:11:53 +0200
Subject: [PATCH 0100/1094] fix caching
* break out cache into cache.py
* break out cache-tests into test_cache.py
* fix broken cache tests
* replace non existing assert calls with self.assertEqual
* fix path for page caching test (was invalid)
* cleanup test code
* restructure generate_context in Article and Path Generator
* destinguish between valid/invalid files correctly and cache accordingly
* use cPickle if available for increased performance
---
pelican/cache.py | 140 +++++++++++++++++++++++
pelican/generators.py | 68 ++++++------
pelican/readers.py | 3 +-
pelican/tests/test_cache.py | 183 +++++++++++++++++++++++++++++++
pelican/tests/test_generators.py | 141 ------------------------
pelican/utils.py | 124 ---------------------
6 files changed, 362 insertions(+), 297 deletions(-)
create mode 100644 pelican/cache.py
create mode 100644 pelican/tests/test_cache.py
diff --git a/pelican/cache.py b/pelican/cache.py
new file mode 100644
index 00000000..d955ae08
--- /dev/null
+++ b/pelican/cache.py
@@ -0,0 +1,140 @@
+from __future__ import unicode_literals
+
+import hashlib
+import logging
+import os
+try:
+ import cPickle as pickle
+except:
+ import pickle
+
+from pelican.utils import mkdir_p
+
+
+logger = logging.getLogger(__name__)
+
+
+class FileDataCacher(object):
+ """Class that can cache data contained in files"""
+
+ def __init__(self, settings, cache_name, caching_policy, load_policy):
+ """Load the specified cache within CACHE_PATH in settings
+
+ only if *load_policy* is True,
+ May use gzip if GZIP_CACHE ins settings is True.
+ Sets caching policy according to *caching_policy*.
+ """
+ self.settings = settings
+ self._cache_path = os.path.join(self.settings['CACHE_PATH'],
+ cache_name)
+ self._cache_data_policy = caching_policy
+ if self.settings['GZIP_CACHE']:
+ import gzip
+ self._cache_open = gzip.open
+ else:
+ self._cache_open = open
+ if load_policy:
+ try:
+ with self._cache_open(self._cache_path, 'rb') as fhandle:
+ self._cache = pickle.load(fhandle)
+ except (IOError, OSError) as err:
+ logger.debug('Cannot load cache %s (this is normal on first '
+ 'run). Proceeding with empty cache.\n%s',
+ self._cache_path, err)
+ self._cache = {}
+ except pickle.PickleError as err:
+ logger.warning('Cannot unpickle cache %s, cache may be using '
+ 'an incompatible protocol (see pelican '
+ 'caching docs). '
+ 'Proceeding with empty cache.\n%s',
+ self._cache_path, err)
+ self._cache = {}
+ else:
+ self._cache = {}
+
+ def cache_data(self, filename, data):
+ """Cache data for given file"""
+ if self._cache_data_policy:
+ self._cache[filename] = data
+
+ def get_cached_data(self, filename, default=None):
+ """Get cached data for the given file
+
+ if no data is cached, return the default object
+ """
+ return self._cache.get(filename, default)
+
+ def save_cache(self):
+ """Save the updated cache"""
+ if self._cache_data_policy:
+ try:
+ mkdir_p(self.settings['CACHE_PATH'])
+ with self._cache_open(self._cache_path, 'wb') as fhandle:
+ pickle.dump(self._cache, fhandle)
+ except (IOError, OSError, pickle.PicklingError) as err:
+ logger.warning('Could not save cache %s\n ... %s',
+ self._cache_path, err)
+
+
+class FileStampDataCacher(FileDataCacher):
+ """Subclass that also caches the stamp of the file"""
+
+ def __init__(self, settings, cache_name, caching_policy, load_policy):
+ """This sublcass additionally sets filestamp function
+ and base path for filestamping operations
+ """
+ super(FileStampDataCacher, self).__init__(settings, cache_name,
+ caching_policy,
+ load_policy)
+
+ method = self.settings['CHECK_MODIFIED_METHOD']
+ if method == 'mtime':
+ self._filestamp_func = os.path.getmtime
+ else:
+ try:
+ hash_func = getattr(hashlib, method)
+
+ def filestamp_func(filename):
+ """return hash of file contents"""
+ with open(filename, 'rb') as fhandle:
+ return hash_func(fhandle.read()).digest()
+
+ self._filestamp_func = filestamp_func
+ except AttributeError as err:
+ logger.warning('Could not get hashing function\n\t%s', err)
+ self._filestamp_func = None
+
+ def cache_data(self, filename, data):
+ """Cache stamp and data for the given file"""
+ stamp = self._get_file_stamp(filename)
+ super(FileStampDataCacher, self).cache_data(filename, (stamp, data))
+
+ def _get_file_stamp(self, filename):
+ """Check if the given file has been modified
+ since the previous build.
+
+ depending on CHECK_MODIFIED_METHOD
+ a float may be returned for 'mtime',
+ a hash for a function name in the hashlib module
+ or an empty bytes string otherwise
+ """
+ try:
+ return self._filestamp_func(filename)
+ except (IOError, OSError, TypeError) as err:
+ logger.warning('Cannot get modification stamp for %s\n\t%s',
+ filename, err)
+ return ''
+
+ def get_cached_data(self, filename, default=None):
+ """Get the cached data for the given filename
+ if the file has not been modified.
+
+ If no record exists or file has been modified, return default.
+ Modification is checked by comparing the cached
+ and current file stamp.
+ """
+ stamp, data = super(FileStampDataCacher, self).get_cached_data(
+ filename, (None, default))
+ if stamp != self._get_file_stamp(filename):
+ return default
+ return data
diff --git a/pelican/generators.py b/pelican/generators.py
index 99961d6e..d4ec29c8 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -17,11 +17,11 @@ from operator import attrgetter
from jinja2 import (Environment, FileSystemLoader, PrefixLoader, ChoiceLoader,
BaseLoader, TemplateNotFound)
+from pelican.cache import FileStampDataCacher
from pelican.contents import Article, Draft, Page, Static, is_valid_content
from pelican.readers import Readers
from pelican.utils import (copy, process_translations, mkdir_p, DateFormatter,
- FileStampDataCacher, python_2_unicode_compatible,
- posixize_path)
+ python_2_unicode_compatible, posixize_path)
from pelican import signals
@@ -493,10 +493,11 @@ class ArticlesGenerator(CachingGenerator):
for f in self.get_files(
self.settings['ARTICLE_PATHS'],
exclude=self.settings['ARTICLE_EXCLUDES']):
- article = self.get_cached_data(f, None)
- if article is None:
+ article_or_draft = self.get_cached_data(f, None)
+ if article_or_draft is None:
+ #TODO needs overhaul, maybe nomad for read_file solution, unified behaviour
try:
- article = self.readers.read_file(
+ article_or_draft = self.readers.read_file(
base_path=self.path, path=f, content_class=Article,
context=self.context,
preread_signal=signals.article_generator_preread,
@@ -509,29 +510,32 @@ class ArticlesGenerator(CachingGenerator):
self._add_failed_source_path(f)
continue
- if not is_valid_content(article, f):
+ if not is_valid_content(article_or_draft, f):
self._add_failed_source_path(f)
continue
- self.cache_data(f, article)
+ if article_or_draft.status.lower() == "published":
+ all_articles.append(article_or_draft)
+ elif article_or_draft.status.lower() == "draft":
+ article_or_draft = self.readers.read_file(
+ base_path=self.path, path=f, content_class=Draft,
+ context=self.context,
+ preread_signal=signals.article_generator_preread,
+ preread_sender=self,
+ context_signal=signals.article_generator_context,
+ context_sender=self)
+ self.add_source_path(article_or_draft)
+ all_drafts.append(article_or_draft)
+ else:
+ logger.error("Unknown status '%s' for file %s, skipping it.",
+ article_or_draft.status, f)
+ self._add_failed_source_path(f)
+ continue
- self.add_source_path(article)
+ self.cache_data(f, article_or_draft)
+
+ self.add_source_path(article_or_draft)
- if article.status.lower() == "published":
- all_articles.append(article)
- elif article.status.lower() == "draft":
- draft = self.readers.read_file(
- base_path=self.path, path=f, content_class=Draft,
- context=self.context,
- preread_signal=signals.article_generator_preread,
- preread_sender=self,
- context_signal=signals.article_generator_context,
- context_sender=self)
- self.add_source_path(draft)
- all_drafts.append(draft)
- else:
- logger.error("Unknown status '%s' for file %s, skipping it.",
- article.status, f)
self.articles, self.translations = process_translations(all_articles,
order_by=self.settings['ARTICLE_ORDER_BY'])
@@ -613,18 +617,20 @@ class PagesGenerator(CachingGenerator):
self._add_failed_source_path(f)
continue
+ if page.status.lower() == "published":
+ all_pages.append(page)
+ elif page.status.lower() == "hidden":
+ hidden_pages.append(page)
+ else:
+ logger.error("Unknown status '%s' for file %s, skipping it.",
+ page.status, f)
+ self._add_failed_source_path(f)
+ continue
+
self.cache_data(f, page)
self.add_source_path(page)
- if page.status.lower() == "published":
- all_pages.append(page)
- elif page.status.lower() == "hidden":
- hidden_pages.append(page)
- else:
- logger.error("Unknown status '%s' for file %s, skipping it.",
- page.status, f)
-
self.pages, self.translations = process_translations(all_pages,
order_by=self.settings['PAGE_ORDER_BY'])
self.hidden_pages, self.hidden_translations = (
diff --git a/pelican/readers.py b/pelican/readers.py
index 3656cd96..e094a505 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -24,8 +24,9 @@ except ImportError:
from six.moves.html_parser import HTMLParser
from pelican import signals
+from pelican.cache import FileStampDataCacher
from pelican.contents import Page, Category, Tag, Author
-from pelican.utils import get_date, pelican_open, FileStampDataCacher, SafeDatetime, posixize_path
+from pelican.utils import get_date, pelican_open, SafeDatetime, posixize_path
def strip_split(text, sep=','):
diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py
new file mode 100644
index 00000000..9b4150cc
--- /dev/null
+++ b/pelican/tests/test_cache.py
@@ -0,0 +1,183 @@
+from __future__ import unicode_literals
+
+import os
+from codecs import open
+try:
+ from unittest.mock import MagicMock
+except ImportError:
+ try:
+ from mock import MagicMock
+ except ImportError:
+ MagicMock = False
+
+from shutil import rmtree
+from tempfile import mkdtemp
+
+from pelican.generators import ArticlesGenerator, PagesGenerator
+from pelican.tests.support import unittest, get_settings
+
+CUR_DIR = os.path.dirname(__file__)
+CONTENT_DIR = os.path.join(CUR_DIR, 'content')
+
+
+class TestCache(unittest.TestCase):
+
+ def setUp(self):
+ self.temp_cache = mkdtemp(prefix='pelican_cache.')
+
+ def tearDown(self):
+ rmtree(self.temp_cache)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_article_object_caching(self):
+ """Test Article objects caching at the generator level"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['READERS'] = {'asc': None}
+
+
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], 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)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ """
+ 3 Files don't get cached because they were not valid
+ - article_with_comments.html
+ - article_with_null_attributes.html
+ - 2012-11-30_md_w_filename_meta#foo-bar.md
+ """
+ self.assertEqual(generator.readers.read_file.call_count, 3)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_article_reader_content_caching(self):
+ """Test raw article content caching at the reader level"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['READERS'] = {'asc': None}
+
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], 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)
+ readers = generator.readers.readers
+ for reader in readers.values():
+ reader.read = MagicMock()
+ generator.generate_context()
+ for reader in readers.values():
+ self.assertEqual(reader.read.call_count, 0)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_article_ignore_cache(self):
+ """Test that all the articles are read again when not loading cache
+
+ used in --ignore-cache or autoreload mode"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['READERS'] = {'asc': None}
+
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache_open'))
+ orig_call_count = generator.readers.read_file.call_count
+
+ settings['LOAD_CONTENT_CACHE'] = False
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertEqual(generator.readers.read_file.call_count, orig_call_count)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_page_object_caching(self):
+ """Test Page objects caching at the generator level"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['READERS'] = {'asc': None}
+
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache'))
+
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ """
+ 1 File doesn't get cached because it was not valid
+ - bad_page.rst
+ """
+ self.assertEqual(generator.readers.read_file.call_count, 1)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_page_reader_content_caching(self):
+ """Test raw page content caching at the reader level"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['READERS'] = {'asc': None}
+
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator.readers, '_cache'))
+
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ readers = generator.readers.readers
+ for reader in readers.values():
+ reader.read = MagicMock()
+ generator.generate_context()
+ for reader in readers.values():
+ self.assertEqual(reader.read.call_count, 0)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_page_ignore_cache(self):
+ """Test that all the pages are read again when not loading cache
+
+ used in --ignore_cache or autoreload mode"""
+ settings = get_settings(filenames={})
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['READERS'] = {'asc': None}
+
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache_open'))
+ orig_call_count = generator.readers.read_file.call_count
+
+ settings['LOAD_CONTENT_CACHE'] = False
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertEqual(generator.readers.read_file.call_count, orig_call_count)
+
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 4fb70826..5266c075 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -135,7 +135,6 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertFalse(writer.write_feed.called)
def test_generate_context(self):
-
articles_expected = [
['Article title', 'published', 'Default', 'article'],
['Article with markdown and summary metadata multi', 'published',
@@ -174,7 +173,6 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertEqual(sorted(articles_expected), sorted(self.articles))
def test_generate_categories(self):
-
# test for name
# categories are grouped by slug; if two categories have the same slug
# but different names they will be grouped together, the first one in
@@ -192,7 +190,6 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertEqual(sorted(categories), sorted(categories_expected))
def test_do_not_use_folder_as_category(self):
-
settings = get_settings(filenames={})
settings['DEFAULT_CATEGORY'] = 'Default'
settings['DEFAULT_DATE'] = (1970, 1, 1)
@@ -355,75 +352,6 @@ class TestArticlesGenerator(unittest.TestCase):
authors_expected = ['alexis-metaireau', 'first-author', 'second-author']
self.assertEqual(sorted(authors), sorted(authors_expected))
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_article_object_caching(self):
- """Test Article objects caching at the generator level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['CONTENT_CACHING_LAYER'] = 'generator'
- settings['READERS'] = {'asc': None}
-
- generator = ArticlesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], 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)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- generator.readers.read_file.assert_called_count == 0
-
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_reader_content_caching(self):
- """Test raw content caching at the reader level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['READERS'] = {'asc': None}
-
- generator = ArticlesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], 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)
- readers = generator.readers.readers
- for reader in readers.values():
- reader.read = MagicMock()
- generator.generate_context()
- for reader in readers.values():
- reader.read.assert_called_count == 0
-
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_ignore_cache(self):
- """Test that all the articles are read again when not loading cache
-
- used in --ignore-cache or autoreload mode"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['READERS'] = {'asc': None}
-
- generator = ArticlesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- self.assertTrue(hasattr(generator, '_cache_open'))
- orig_call_count = generator.readers.read_file.call_count
-
- settings['LOAD_CONTENT_CACHE'] = False
- generator = ArticlesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- generator.readers.read_file.assert_called_count == orig_call_count
-
def test_standard_metadata_in_default_metadata(self):
settings = get_settings(filenames={})
settings['CACHE_CONTENT'] = False
@@ -503,75 +431,6 @@ class TestPageGenerator(unittest.TestCase):
self.assertEqual(sorted(pages_expected), sorted(pages))
self.assertEqual(sorted(hidden_pages_expected), sorted(hidden_pages))
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_page_object_caching(self):
- """Test Page objects caching at the generator level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['CONTENT_CACHING_LAYER'] = 'generator'
- settings['READERS'] = {'asc': None}
-
- generator = PagesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], 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)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- generator.readers.read_file.assert_called_count == 0
-
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_reader_content_caching(self):
- """Test raw content caching at the reader level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['READERS'] = {'asc': None}
-
- generator = PagesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], 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)
- readers = generator.readers.readers
- for reader in readers.values():
- reader.read = MagicMock()
- generator.generate_context()
- for reader in readers.values():
- reader.read.assert_called_count == 0
-
- @unittest.skipUnless(MagicMock, 'Needs Mock module')
- def test_ignore_cache(self):
- """Test that all the pages are read again when not loading cache
-
- used in --ignore_cache or autoreload mode"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['READERS'] = {'asc': None}
-
- generator = PagesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- self.assertTrue(hasattr(generator, '_cache_open'))
- orig_call_count = generator.readers.read_file.call_count
-
- settings['LOAD_CONTENT_CACHE'] = False
- generator = PagesGenerator(
- context=settings.copy(), settings=settings,
- path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
- generator.readers.read_file = MagicMock()
- generator.generate_context()
- generator.readers.read_file.assert_called_count == orig_call_count
-
def test_generate_sorted(self):
settings = get_settings(filenames={})
settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR
diff --git a/pelican/utils.py b/pelican/utils.py
index caac8e61..bc50308e 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -14,7 +14,6 @@ import shutil
import sys
import traceback
import pickle
-import hashlib
import datetime
from collections import Hashable
@@ -627,129 +626,6 @@ def split_all(path):
return components
-class FileDataCacher(object):
- '''Class that can cache data contained in files'''
-
- def __init__(self, settings, cache_name, caching_policy, load_policy):
- '''Load the specified cache within CACHE_PATH in settings
-
- only if *load_policy* is True,
- May use gzip if GZIP_CACHE ins settings is True.
- Sets caching policy according to *caching_policy*.
- '''
- self.settings = settings
- self._cache_path = os.path.join(self.settings['CACHE_PATH'],
- cache_name)
- self._cache_data_policy = caching_policy
- if self.settings['GZIP_CACHE']:
- import gzip
- self._cache_open = gzip.open
- else:
- self._cache_open = open
- if load_policy:
- try:
- with self._cache_open(self._cache_path, 'rb') as fhandle:
- self._cache = pickle.load(fhandle)
- except (IOError, OSError) as err:
- logger.debug('Cannot load cache %s (this is normal on first '
- 'run). Proceeding with empty cache.\n%s',
- self._cache_path, err)
- self._cache = {}
- except Exception as err:
- logger.warning(('Cannot unpickle cache %s, cache may be using '
- 'an incompatible protocol (see pelican caching docs). '
- 'Proceeding with empty cache.\n%s'),
- self._cache_path, err)
- self._cache = {}
- else:
- self._cache = {}
-
- def cache_data(self, filename, data):
- '''Cache data for given file'''
- if self._cache_data_policy:
- self._cache[filename] = data
-
- def get_cached_data(self, filename, default=None):
- '''Get cached data for the given file
-
- if no data is cached, return the default object
- '''
- return self._cache.get(filename, default)
-
- def save_cache(self):
- '''Save the updated cache'''
- if self._cache_data_policy:
- try:
- mkdir_p(self.settings['CACHE_PATH'])
- with self._cache_open(self._cache_path, 'wb') as fhandle:
- pickle.dump(self._cache, fhandle)
- except (IOError, OSError, pickle.PicklingError) as err:
- logger.warning('Could not save cache %s\n ... %s',
- self._cache_path, err)
-
-
-class FileStampDataCacher(FileDataCacher):
- '''Subclass that also caches the stamp of the file'''
-
- def __init__(self, settings, cache_name, caching_policy, load_policy):
- '''This sublcass additionally sets filestamp function
- and base path for filestamping operations
- '''
- super(FileStampDataCacher, self).__init__(settings, cache_name,
- caching_policy,
- load_policy)
-
- method = self.settings['CHECK_MODIFIED_METHOD']
- if method == 'mtime':
- self._filestamp_func = os.path.getmtime
- else:
- try:
- hash_func = getattr(hashlib, method)
- def filestamp_func(filename):
- '''return hash of file contents'''
- with open(filename, 'rb') as fhandle:
- return hash_func(fhandle.read()).digest()
- self._filestamp_func = filestamp_func
- except AttributeError as err:
- logger.warning('Could not get hashing function\n\t%s', err)
- self._filestamp_func = None
-
- def cache_data(self, filename, data):
- '''Cache stamp and data for the given file'''
- stamp = self._get_file_stamp(filename)
- super(FileStampDataCacher, self).cache_data(filename, (stamp, data))
-
- def _get_file_stamp(self, filename):
- '''Check if the given file has been modified
- since the previous build.
-
- depending on CHECK_MODIFIED_METHOD
- a float may be returned for 'mtime',
- a hash for a function name in the hashlib module
- or an empty bytes string otherwise
- '''
- try:
- return self._filestamp_func(filename)
- except (IOError, OSError, TypeError) as err:
- logger.warning('Cannot get modification stamp for %s\n\t%s',
- filename, err)
- return b''
-
- def get_cached_data(self, filename, default=None):
- '''Get the cached data for the given filename
- if the file has not been modified.
-
- If no record exists or file has been modified, return default.
- Modification is checked by comparing the cached
- and current file stamp.
- '''
- stamp, data = super(FileStampDataCacher, self).get_cached_data(
- filename, (None, default))
- if stamp != self._get_file_stamp(filename):
- return default
- return data
-
-
def is_selected_for_writing(settings, path):
'''Check whether path is selected for writing
according to the WRITE_SELECTED list
From db59c16f42aa6eb382ca683b6938a006e5c3b9f5 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Sat, 6 Jun 2015 20:07:12 +0200
Subject: [PATCH 0101/1094] set default for caching to false
ref #1689
* set default settigns in settings.py to False for
- LOAD_CONTENT_CACHE
- CACHE_CONTENT
* remove AUTORELOAD_IGNORE_CACHE and add deprecation warning
* update settings.rst to reflect the new default values
* update test_cache to enable caching options
---
docs/settings.rst | 6 ++----
pelican/__init__.py | 14 +++++---------
pelican/settings.py | 5 ++---
pelican/tests/test_cache.py | 28 +++++++++++++++-------------
4 files changed, 24 insertions(+), 29 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 99829258..b4e73316 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -204,7 +204,7 @@ Setting name (followed by default value, if any)
``SLUGIFY_SOURCE = 'title'`` Specifies where you want the slug to be automatically generated
from. Can be set to ``title`` to use the 'Title:' metadata tag or
``basename`` to use the article's file name when creating the slug.
-``CACHE_CONTENT = True`` If ``True``, save content in a cache file.
+``CACHE_CONTENT = False`` If ``True``, saves content in caches.
See :ref:`reading_only_modified_content` for details about caching.
``CONTENT_CACHING_LAYER = 'reader'`` If set to ``'reader'``, save only the raw content and metadata
returned by readers. If set to ``'generator'``, save processed
@@ -212,9 +212,7 @@ Setting name (followed by default value, if any)
``CACHE_PATH = 'cache'`` Directory in which to store cache files.
``GZIP_CACHE = True`` If ``True``, use gzip to (de)compress the cache files.
``CHECK_MODIFIED_METHOD = 'mtime'`` Controls how files are checked for modifications.
-``LOAD_CONTENT_CACHE = True`` If ``True``, load unmodified content from cache.
-``AUTORELOAD_IGNORE_CACHE = False`` If ``True``, do not load content cache in autoreload mode
- when the settings file changes.
+``LOAD_CONTENT_CACHE = False`` If ``True``, load unmodified content from caches.
``WRITE_SELECTED = []`` If this list is not empty, **only** output files with their paths
in this list are written. Paths should be either absolute or relative
to the current Pelican working directory. For possible use cases see
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 12da111a..98e783d7 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -101,6 +101,11 @@ class Pelican(object):
'PAGE_LANG_URL'):
logger.warning("%s = '%s'", setting, self.settings[setting])
+ if self.settings.get('AUTORELOAD_IGNORE_CACHE'):
+ logger.warning('Found deprecated `AUTORELOAD_IGNORE_CACHE` in '
+ 'settings. Use --ignore-cache instead.')
+ self.settings.pop('AUTORELOAD_IGNORE_CACHE')
+
if self.settings.get('ARTICLE_PERMALINK_STRUCTURE', False):
logger.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in'
' settings. Modifying the following settings for'
@@ -381,10 +386,6 @@ def main():
print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
' `settings` for changes. ---')
- def _ignore_cache(pelican_obj):
- if pelican_obj.settings['AUTORELOAD_IGNORE_CACHE']:
- pelican_obj.settings['LOAD_CONTENT_CACHE'] = False
-
while True:
try:
# Check source dir for changed files ending with the given
@@ -393,12 +394,9 @@ def main():
# have changed, no matter what extension the filenames
# have.
modified = {k: next(v) for k, v in watchers.items()}
- original_load_cache = settings['LOAD_CONTENT_CACHE']
if modified['settings']:
pelican, settings = get_instance(args)
- original_load_cache = settings['LOAD_CONTENT_CACHE']
- _ignore_cache(pelican)
# Adjust static watchers if there are any changes
new_static = settings.get("STATIC_PATHS", [])
@@ -435,8 +433,6 @@ def main():
'theme.')
pelican.run()
- # restore original caching policy
- pelican.settings['LOAD_CONTENT_CACHE'] = original_load_cache
except KeyboardInterrupt:
logger.warning("Keyboard interrupt, quitting.")
diff --git a/pelican/settings.py b/pelican/settings.py
index 0c54e89b..82955277 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -123,13 +123,12 @@ DEFAULT_CONFIG = {
'SLUG_SUBSTITUTIONS': (),
'INTRASITE_LINK_REGEX': '[{|](?P.*?)[|}]',
'SLUGIFY_SOURCE': 'title',
- 'CACHE_CONTENT': True,
+ 'CACHE_CONTENT': False,
'CONTENT_CACHING_LAYER': 'reader',
'CACHE_PATH': 'cache',
'GZIP_CACHE': True,
'CHECK_MODIFIED_METHOD': 'mtime',
- 'LOAD_CONTENT_CACHE': True,
- 'AUTORELOAD_IGNORE_CACHE': False,
+ 'LOAD_CONTENT_CACHE': False,
'WRITE_SELECTED': [],
'FORMATTED_FIELDS': ['summary'],
}
diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py
index 9b4150cc..8a20c36b 100644
--- a/pelican/tests/test_cache.py
+++ b/pelican/tests/test_cache.py
@@ -28,11 +28,18 @@ class TestCache(unittest.TestCase):
def tearDown(self):
rmtree(self.temp_cache)
+ def _get_cache_enabled_settings(self):
+ settings = get_settings(filenames={})
+ settings['CACHE_CONTENT'] = True
+ settings['LOAD_CONTENT_CACHE'] = True
+ settings['CACHE_PATH'] = self.temp_cache
+ return settings
+
+
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_object_caching(self):
"""Test Article objects caching at the generator level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
+ settings = self._get_cache_enabled_settings()
settings['CONTENT_CACHING_LAYER'] = 'generator'
settings['DEFAULT_DATE'] = (1970, 1, 1)
settings['READERS'] = {'asc': None}
@@ -60,8 +67,7 @@ class TestCache(unittest.TestCase):
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_reader_content_caching(self):
"""Test raw article content caching at the reader level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
+ settings = self._get_cache_enabled_settings()
settings['READERS'] = {'asc': None}
generator = ArticlesGenerator(
@@ -85,8 +91,7 @@ class TestCache(unittest.TestCase):
"""Test that all the articles are read again when not loading cache
used in --ignore-cache or autoreload mode"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
+ settings = self._get_cache_enabled_settings()
settings['READERS'] = {'asc': None}
generator = ArticlesGenerator(
@@ -108,10 +113,9 @@ class TestCache(unittest.TestCase):
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_object_caching(self):
"""Test Page objects caching at the generator level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
- settings['PAGE_PATHS'] = ['TestPages']
+ settings = self._get_cache_enabled_settings()
settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['PAGE_PATHS'] = ['TestPages']
settings['READERS'] = {'asc': None}
generator = PagesGenerator(
@@ -134,8 +138,7 @@ class TestCache(unittest.TestCase):
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_reader_content_caching(self):
"""Test raw page content caching at the reader level"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
+ settings = self._get_cache_enabled_settings()
settings['PAGE_PATHS'] = ['TestPages']
settings['READERS'] = {'asc': None}
@@ -160,8 +163,7 @@ class TestCache(unittest.TestCase):
"""Test that all the pages are read again when not loading cache
used in --ignore_cache or autoreload mode"""
- settings = get_settings(filenames={})
- settings['CACHE_PATH'] = self.temp_cache
+ settings = self._get_cache_enabled_settings()
settings['PAGE_PATHS'] = ['TestPages']
settings['READERS'] = {'asc': None}
From e3f5e39e17b6860e26287ab556476707e3abf96e Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Thu, 11 Jun 2015 19:40:34 +0200
Subject: [PATCH 0102/1094] fix outdated content_object_init refs
* remove content_object_init section from docs
* improve content_object_init test
The content_object_init signal used to set its class as sender and pass
the instance as additional arg in 6100773. Commit ed907b4 removed this
behaviour to bring it inline with other signals, on the basis that
you can test for the class of the object anyway.
We also had a test in place, checking this behaviour, but it was poorly
implemented, not actually checking if the function ever got called.
closes #1711
---
docs/plugins.rst | 22 +--------------------
pelican/tests/test_contents.py | 35 ++++++++++++++++++++--------------
2 files changed, 22 insertions(+), 35 deletions(-)
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 428ddfa6..6a850100 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -116,31 +116,11 @@ static_generator_preread static_generator invoked befor
staticfiles list.
static_generator_init static_generator invoked in the StaticGenerator.__init__
static_generator_finalized static_generator invoked at the end of StaticGenerator.generate_context
-content_object_init content_object invoked at the end of Content.__init__ (see note below)
+content_object_init content_object invoked at the end of Content.__init__
content_written path, context invoked each time a content file is written.
feed_written path, context, feed invoked each time a feed file is written.
================================= ============================ ===========================================================================
-The list is currently small, so don't hesitate to add signals and make a pull
-request if you need them!
-
-.. note::
-
- The signal ``content_object_init`` can send a different type of object as
- the argument. If you want to register only one type of object then you will
- need to specify the sender when you are connecting to the signal.
-
- ::
-
- from pelican import signals
- from pelican import contents
-
- def test(sender, instance):
- print "%s : %s content initialized !!" % (sender, instance)
-
- def register():
- signals.content_object_init.connect(test, sender=contents.Article)
-
.. warning::
Avoid ``content_object_init`` signal if you intend to read ``summary``
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 004d512e..5879bde2 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -1,18 +1,20 @@
-# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
-import six
-from sys import platform
import locale
import os.path
+import six
-from pelican.tests.support import unittest, get_settings
-
-from pelican.contents import Page, Article, Static, URLWrapper, Author, Category
-from pelican.settings import DEFAULT_CONFIG
-from pelican.utils import path_to_url, truncate_html_words, SafeDatetime, posix_join
-from pelican.signals import content_object_init
from jinja2.utils import generate_lorem_ipsum
+from sys import platform
+
+from pelican.contents import (Page, Article, Static, URLWrapper,
+ Author, Category)
+from pelican.settings import DEFAULT_CONFIG
+from pelican.signals import content_object_init
+from pelican.tests.support import unittest, get_settings
+from pelican.utils import (path_to_url, truncate_html_words, SafeDatetime,
+ posix_join)
+
# generate one paragraph, enclosed with
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
@@ -191,14 +193,19 @@ class TestPage(unittest.TestCase):
return page_kwargs
def test_signal(self):
- # If a title is given, it should be used to generate the slug.
-
- def receiver_test_function(sender, instance):
+ def receiver_test_function(sender):
+ receiver_test_function.has_been_called = True
pass
+ receiver_test_function.has_been_called = False
- content_object_init.connect(receiver_test_function, sender=Page)
+ content_object_init.connect(receiver_test_function)
+ self.assertIn(
+ receiver_test_function,
+ content_object_init.receivers_for(Page))
+
+ self.assertFalse(receiver_test_function.has_been_called)
Page(**self.page_kwargs)
- self.assertTrue(content_object_init.has_receivers_for(Page))
+ self.assertTrue(receiver_test_function.has_been_called)
def test_get_content(self):
# Test that the content is updated with the relative links to
From da8b469ab845917736186f070b955d08a5d7d488 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Fri, 12 Jun 2015 17:15:19 -0400
Subject: [PATCH 0103/1094] Fix #1647: Fix ARTICLE_ORDER_BY and add the ability
to reverse order
ARTICLE_ORDER_BY wasn't doing anything because the ArticlesGenerator
was sorting articles after ARTICLE_ORDER_BY was applied. This fixes
that by adding the ability to reverse metadata order by adding the
option prefix 'reversed-' to metadata and changing the default value
to 'reversed-date'.
Relevant documentation is also updated and moved into a more appropriate
place ('Ordering Content' instead of 'URL settings').
---
docs/settings.rst | 37 +++++++---------
pelican/generators.py | 3 +-
pelican/settings.py | 2 +-
pelican/tests/test_generators.py | 76 ++++++++++++++++++++++++++++++++
pelican/utils.py | 32 +++++++++-----
5 files changed, 115 insertions(+), 35 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index e08291f0..202fc45f 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -277,14 +277,6 @@ Setting name (followed by default value, if any) What does it do?
====================================================== ==============================================================
``ARTICLE_URL = '{slug}.html'`` The URL to refer to an article.
``ARTICLE_SAVE_AS = '{slug}.html'`` The place where we will save an article.
-``ARTICLE_ORDER_BY = 'slug'`` The metadata attribute used to sort articles. By default,
- the ``articles_page.object_list`` template variable is
- ordered by slug. If you modify this, make sure all
- articles contain the attribute you specify. You can also
- specify a "sorting" function of one argument that is used
- to extract a comparison key from each article. For example,
- sorting by title without using the built-in functionality
- would use the function ``operator.attrgetter('title')``.
``ARTICLE_LANG_URL = '{slug}-{lang}.html'`` The URL to refer to an article which doesn't use the
default language.
``ARTICLE_LANG_SAVE_AS = '{slug}-{lang}.html'`` The place where we will save an article which
@@ -299,17 +291,6 @@ Setting name (followed by default value, if any) What does it do?
``PAGE_SAVE_AS = 'pages/{slug}.html'`` The location we will save the page. This value has to be
the same as PAGE_URL or you need to use a rewrite in
your server config.
-
-``PAGE_ORDER_BY = 'basename'`` The metadata attribute used to sort pages. By default
- the ``PAGES`` template variable is ordered by basename
- (i.e., path not included). Note that the option ``'basename'``
- is a special option supported in the source code. If
- you modify this setting, make sure all pages contain
- the attribute you specify. You can also specify a "sorting"
- function of one argument that is used to extract a comparison
- key from each page. For example, the basename function looks
- similar to
- ``lambda x: os.path.basename(getattr(x, 'source_path', ''))``.
``PAGE_LANG_URL = 'pages/{slug}-{lang}.html'`` The URL we will use to link to a page which doesn't
use the default language.
``PAGE_LANG_SAVE_AS = 'pages/{slug}-{lang}.html'`` The location we will save the page which doesn't
@@ -644,14 +625,26 @@ Setting name (followed by default value, if any) What does it do?
Ordering content
================
-================================================ =====================================================
+================================================ ==============================================================
Setting name (followed by default value) What does it do?
-================================================ =====================================================
+================================================ ==============================================================
``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.)
-================================================ =====================================================
+``ARTICLE_ORDER_BY = 'reversed-date'`` Defines how the articles (``articles_page.object_list`` in
+ the template) are sorted. Valid options are: metadata as a
+ string (use ``reversed-`` prefix the reverse the sort order),
+ special option ``'basename'`` which will use the basename of
+ the file (without path) or a custom function to extract the
+ sorting key from articles. The default value,
+ ``'reversed-date'``, will sort articles by date in reverse
+ order (i.e. newest article comes first).
+``PAGE_ORDER_BY = 'basename'`` Defines how the pages (``PAGES`` variable in the template)
+ are sorted. Options are same as ``ARTICLE_ORDER_BY``.
+ The default value, ``'basename'`` will sort pages by their
+ basename.
+================================================ ==============================================================
Themes
diff --git a/pelican/generators.py b/pelican/generators.py
index 3692ef0b..a29555a5 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -561,8 +561,7 @@ class ArticlesGenerator(CachingGenerator):
self.tags[tag].append(article)
for author in getattr(article, 'authors', []):
self.authors[author].append(article)
- # sort the articles by date
- self.articles.sort(key=attrgetter('date'), reverse=True)
+
self.dates = list(self.articles)
self.dates.sort(key=attrgetter('date'),
reverse=self.context['NEWEST_FIRST_ARCHIVES'])
diff --git a/pelican/settings.py b/pelican/settings.py
index 82955277..c1a902cd 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -65,7 +65,7 @@ DEFAULT_CONFIG = {
'OUTPUT_RETENTION': [],
'ARTICLE_URL': '{slug}.html',
'ARTICLE_SAVE_AS': '{slug}.html',
- 'ARTICLE_ORDER_BY': 'slug',
+ 'ARTICLE_ORDER_BY': 'reversed-date',
'ARTICLE_LANG_URL': '{slug}-{lang}.html',
'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html',
'DRAFT_URL': 'drafts/{slug}.html',
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 7aac27a2..e93c8c14 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -387,6 +387,65 @@ class TestArticlesGenerator(unittest.TestCase):
'パイソン', 'マック'])
self.assertEqual(tags, tags_expected)
+ def test_article_order_by(self):
+ settings = get_settings(filenames={})
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['CACHE_CONTENT'] = False # cache not needed for this logic tests
+ settings['ARTICLE_ORDER_BY'] = 'title'
+
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ expected = [
+ 'An Article With Code Block To Test Typogrify Ignore',
+ 'Article title',
+ 'Article with Nonconformant HTML meta tags',
+ 'Article with markdown and summary metadata multi',
+ 'Article with markdown and summary metadata single',
+ 'Article with markdown containing footnotes',
+ 'Article with template',
+ 'Rst with filename metadata',
+ 'Test Markdown extensions',
+ 'Test markdown File',
+ 'Test md File',
+ 'Test mdown File',
+ 'Test mkd File',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is an article with category !',
+ 'This is an article with multiple authors in lastname, firstname format!',
+ 'This is an article with multiple authors in list format!',
+ 'This is an article with multiple authors!',
+ 'This is an article with multiple authors!',
+ 'This is an article without category !',
+ 'This is an article without category !',
+ 'マックOS X 10.8でパイソンとVirtualenvをインストールと設定']
+
+ articles = [article.title for article in generator.articles]
+ self.assertEqual(articles, expected)
+
+ # reversed title
+ settings = get_settings(filenames={})
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['CACHE_CONTENT'] = False # cache not needed for this logic tests
+ settings['ARTICLE_ORDER_BY'] = 'reversed-title'
+
+ generator = ArticlesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ articles = [article.title for article in generator.articles]
+ self.assertEqual(articles, list(reversed(expected)))
+
class TestPageGenerator(unittest.TestCase):
# Note: Every time you want to test for a new field; Make sure the test
@@ -473,6 +532,23 @@ class TestPageGenerator(unittest.TestCase):
pages = self.distill_pages(generator.pages)
self.assertEqual(pages_expected_sorted_by_title, pages)
+ # sort by title reversed
+ pages_expected_sorted_by_title = [
+ ['This is a test page with a preset template', 'published',
+ 'custom'],
+ ['This is a test page', 'published', 'page'],
+ ['This is a markdown test page', 'published', 'page'],
+ ['Page with a bunch of links', 'published', 'page'],
+ ['A Page (Test) for sorting', 'published', 'page'],
+ ]
+ settings['PAGE_ORDER_BY'] = 'reversed-title'
+ generator = PagesGenerator(
+ context=settings.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages = self.distill_pages(generator.pages)
+ self.assertEqual(pages_expected_sorted_by_title, pages)
+
def test_tag_and_category_links_on_generated_pages(self):
"""
Test to ensure links of the form {tag}tagname and {category}catname
diff --git a/pelican/utils.py b/pelican/utils.py
index f973a6ad..6ad4de24 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -541,16 +541,28 @@ def process_translations(content_list, order_by=None):
try:
index.sort(key=order_by)
except Exception:
- logger.error('Error sorting with function {}'.format(order_by))
- elif order_by == 'basename':
- index.sort(key=lambda x: os.path.basename(x.source_path or ''))
- elif order_by != 'slug':
- try:
- index.sort(key=attrgetter(order_by))
- except AttributeError:
- error_msg = ('There is no "{}" attribute in the item metadata.'
- 'Defaulting to slug order.')
- logger.warning(error_msg.format(order_by))
+ logger.error('Error sorting with function %s', order_by)
+ elif isinstance(order_by, six.string_types):
+ if order_by.startswith('reversed-'):
+ order_reversed = True
+ order_by = order_by.replace('reversed-', '', 1)
+ else:
+ order_reversed = False
+
+ if order_by == 'basename':
+ index.sort(key=lambda x: os.path.basename(x.source_path or ''),
+ reverse=order_reversed)
+ # already sorted by slug, no need to sort again
+ elif not (order_by == 'slug' and not order_reversed):
+ try:
+ index.sort(key=attrgetter(order_by),
+ reverse=order_reversed)
+ except AttributeError:
+ logger.warning('There is no "%s" attribute in the item '
+ 'metadata. Defaulting to slug order.', order_by)
+ else:
+ logger.warning('Invalid *_ORDER_BY setting (%s).'
+ 'Valid options are strings and functions.', order_by)
return index, translations
From 0c9e4764ea483bac7df4d731ab0c28f90338a12e Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Sun, 14 Jun 2015 09:57:57 -0700
Subject: [PATCH 0104/1094] Include setup.py when bumping version number
---
bumpr.rc | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/bumpr.rc b/bumpr.rc
index 97dbad22..00078ae0 100644
--- a/bumpr.rc
+++ b/bumpr.rc
@@ -6,7 +6,9 @@ clean =
rm -rf *egg-info build dist
tests = python -m unittest discover
publish = python setup.py sdist bdist_wheel register upload
-files = README.rst
+files =
+ README.rst
+ setup.py
[bump]
unsuffix = true
From 14611978263b7eeb8922da7b56f9eb39abe391da Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Sun, 14 Jun 2015 10:12:52 -0700
Subject: [PATCH 0105/1094] Match space with POSIX sed when bumping via bumpr
`\s` will match a space via GNU sed but not the version of sed that
ships with Mac OS X or BSD.
---
bumpr.rc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bumpr.rc b/bumpr.rc
index 00078ae0..eebe4dd8 100644
--- a/bumpr.rc
+++ b/bumpr.rc
@@ -29,4 +29,4 @@ prepare = Next release
url = http://docs.getpelican.com/{tag}
[commands]
-bump = sed -i "" "s/last_stable\s*=.*/last_stable = '{version}'/" docs/conf.py
+bump = sed -i "" "s/last_stable[[:space:]]*=.*/last_stable = '{version}'/" docs/conf.py
From d19854f4dc69972484f7f521bccf88cf7b68bbf5 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Mon, 15 Jun 2015 07:08:05 -0700
Subject: [PATCH 0106/1094] Remove references to easy_install in documentation
---
docs/install.rst | 5 ++---
docs/tips.rst | 2 +-
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/docs/install.rst b/docs/install.rst
index 418c8ca6..067a5cec 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -79,9 +79,8 @@ automatically installed without any action on your part:
Upgrading
---------
-If you installed a stable Pelican release via ``pip`` or ``easy_install`` and
-wish to upgrade to the latest stable release, you can do so by adding
-``--upgrade`` to the relevant command. For pip, that would be::
+If you installed a stable Pelican release via ``pip`` and wish to upgrade to
+the latest stable release, you can do so by adding ``--upgrade``::
pip install --upgrade pelican
diff --git a/docs/tips.rst b/docs/tips.rst
index 99b5f6a8..15d68769 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -46,7 +46,7 @@ the ``output`` dir generated by Pelican to a repository's ``gh-pages`` branch
on GitHub.
The excellent `ghp-import `_, which can
-be installed with ``easy_install`` or ``pip``, makes this process really easy.
+be installed with ``pip``, makes this process really easy.
For example, if the source of your Pelican site is contained in a GitHub
repository, and if you want to publish that Pelican site in the form of Project
From c7ec93466da5bc52eb254981c8f260e248e5f249 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Mon, 15 Jun 2015 07:53:52 -0700
Subject: [PATCH 0107/1094] Update changelog for imminent 3.6 release
---
docs/changelog.rst | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4e297562..f4f1d0aa 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,24 @@
Release history
###############
+Next release
+============
+
+* Improve caching behavior and disable by default
+* Allow Markdown or reST content in metadata fields other than "summary"
+* Support semicolon-separated author/tag lists
+* Improve flexibility of article sorting
+* Add ``--relative-urls`` argument
+* Support listening on addresses other than localhost
+* Unify HTTP server handlers to ``pelican.server`` throughout
+* Handle intra-site links to draft posts
+* Move ``tag_cloud`` from core to plugin
+* Load all external resources via HTTPS
+* Import drafts from WordPress XML
+* Improve support for Windows users
+* Enhance logging and test suite
+* New signals: ``all_generators_finalized`` and ``page_writer_finalized``
+
3.5.0 (2014-11-04)
==================
From 68354eb94e7ec272bd8cef4873b206022d6a0756 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Mon, 15 Jun 2015 13:07:49 -0700
Subject: [PATCH 0108/1094] Minor fixes to 3.6 release changelog
---
docs/changelog.rst | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index f4f1d0aa..7bf766a4 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,19 +4,21 @@ Release history
Next release
============
-* Improve caching behavior and disable by default
-* Allow Markdown or reST content in metadata fields other than "summary"
+* Disable caching by default in order to prevent potential confusion
+* Improve caching behavior, replacing ``pickle`` with ``cpickle``
+* Allow Markdown or reST content in metadata fields other than ``summary``
* Support semicolon-separated author/tag lists
* Improve flexibility of article sorting
* Add ``--relative-urls`` argument
-* Support listening on addresses other than localhost
+* Support devserver listening on addresses other than localhost
* Unify HTTP server handlers to ``pelican.server`` throughout
* Handle intra-site links to draft posts
* Move ``tag_cloud`` from core to plugin
-* Load all external resources via HTTPS
+* Load default theme's external resources via HTTPS
* Import drafts from WordPress XML
* Improve support for Windows users
* Enhance logging and test suite
+* Clean up and refactor codebase
* New signals: ``all_generators_finalized`` and ``page_writer_finalized``
3.5.0 (2014-11-04)
From fba2826b7041dab9513542331ee17d3641e67505 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Mon, 15 Jun 2015 13:45:50 -0700
Subject: [PATCH 0109/1094] Bump version 3.6.0
---
docs/changelog.rst | 4 ++--
docs/conf.py | 2 +-
pelican/__init__.py | 2 +-
setup.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 7bf766a4..7d385670 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,8 +1,8 @@
Release history
###############
-Next release
-============
+3.6.0 (2015-06-15)
+==================
* Disable caching by default in order to prevent potential confusion
* Improve caching behavior, replacing ``pickle`` with ``cpickle``
diff --git a/docs/conf.py b/docs/conf.py
index 4ec0f832..43af8da4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,7 +18,7 @@ copyright = '2015, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
release = __version__
version = '.'.join(release.split('.')[:1])
-last_stable = '3.3.0'
+last_stable = '3.6.0'
rst_prolog = '''
.. |last_stable| replace:: :pelican-doc:`{0}`
'''.format(last_stable)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 42c89bf0..f80c2ddb 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -26,7 +26,7 @@ from pelican.utils import (clean_output_dir, folder_watcher,
file_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "3.5.0"
+__version__ = "3.6.0"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
diff --git a/setup.py b/setup.py
index 8e4eff34..37aca23f 100755
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ CHANGELOG = open('docs/changelog.rst').read()
setup(
name="pelican",
- version="3.5.0",
+ version="3.6.0",
url='http://getpelican.com/',
author='Alexis Metaireau',
author_email='authors@getpelican.com',
From 780ccfe137a910b404d651a4973f68c85adf01f2 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Mon, 15 Jun 2015 13:46:17 -0700
Subject: [PATCH 0110/1094] Prepare version 3.6.1.dev for next development
cycle
---
docs/changelog.rst | 5 +++++
pelican/__init__.py | 2 +-
setup.py | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 7d385670..f08f67ec 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,11 @@
Release history
###############
+Next release
+============
+
+- Nothing yet
+
3.6.0 (2015-06-15)
==================
diff --git a/pelican/__init__.py b/pelican/__init__.py
index f80c2ddb..932974db 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -26,7 +26,7 @@ from pelican.utils import (clean_output_dir, folder_watcher,
file_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "3.6.0"
+__version__ = "3.6.1.dev"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
diff --git a/setup.py b/setup.py
index 37aca23f..93d7e5f6 100755
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ CHANGELOG = open('docs/changelog.rst').read()
setup(
name="pelican",
- version="3.6.0",
+ version="3.6.1.dev",
url='http://getpelican.com/',
author='Alexis Metaireau',
author_email='authors@getpelican.com',
From f588c565e22030ab2746c23fd0e9a8235d536cff Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Fri, 19 Jun 2015 19:20:48 +0200
Subject: [PATCH 0111/1094] keep docs output
---
tox.ini | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index 775241b8..ff16929e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,4 +26,4 @@ deps =
sphinx_rtd_theme
changedir = docs
commands =
- sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
+ sphinx-build -W -b html -d {envtmpdir}/doctrees . _build/html
From 108b67282ad41416bef48125249ff293efaa8469 Mon Sep 17 00:00:00 2001
From: Jonathan Lange
Date: Tue, 16 Jun 2015 22:33:39 +0100
Subject: [PATCH 0112/1094] Export hidden pages in context
The `PageGenerator` was building hidden pages, but was not making them
available in the context. This makes it difficult for other plugins to
operate on hidden pages.
This patch updates `PageGenerator` to export the hidden pages it finds
in the context as `hidden_pages`.
It also updates the article generator to export `drafts`.
---
docs/themes.rst | 2 ++
pelican/generators.py | 4 ++--
pelican/tests/test_generators.py | 6 ++++++
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/docs/themes.rst b/docs/themes.rst
index d64683bb..6ca753c6 100644
--- a/docs/themes.rst
+++ b/docs/themes.rst
@@ -77,11 +77,13 @@ articles The list of articles, ordered descending by date.
in the `all_articles` variable.
dates The same list of articles, but ordered by date,
ascending.
+drafts The list of draft articles
tags A list of (tag, articles) tuples, containing all
the tags.
categories A list of (category, articles) tuples, containing
all the categories and corresponding articles (values)
pages The list of pages
+hidden_pages The list of hidden pages
============= ===================================================
diff --git a/pelican/generators.py b/pelican/generators.py
index a29555a5..0a5298e4 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -577,7 +577,7 @@ class ArticlesGenerator(CachingGenerator):
self.authors.sort()
self._update_context(('articles', 'dates', 'tags', 'categories',
- 'authors', 'related_posts'))
+ 'authors', 'related_posts', 'drafts'))
self.save_cache()
self.readers.save_cache()
signals.article_generator_finalized.send(self)
@@ -643,7 +643,7 @@ class PagesGenerator(CachingGenerator):
self.hidden_pages, self.hidden_translations = (
process_translations(hidden_pages))
- self._update_context(('pages', ))
+ self._update_context(('pages', 'hidden_pages'))
self.context['PAGES'] = self.pages
self.save_cache()
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index e93c8c14..c424b60f 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -491,7 +491,13 @@ class TestPageGenerator(unittest.TestCase):
]
self.assertEqual(sorted(pages_expected), sorted(pages))
+ self.assertEqual(
+ sorted(pages_expected),
+ sorted(self.distill_pages(generator.context['pages'])))
self.assertEqual(sorted(hidden_pages_expected), sorted(hidden_pages))
+ self.assertEqual(
+ sorted(hidden_pages_expected),
+ sorted(self.distill_pages(generator.context['hidden_pages'])))
def test_generate_sorted(self):
settings = get_settings(filenames={})
From 22bc56ddf165a3a696895c3bfe328ca1cc837dd3 Mon Sep 17 00:00:00 2001
From: Elana Hashman
Date: Sat, 20 Jun 2015 20:47:37 -0600
Subject: [PATCH 0113/1094] Fixes #1068, #1572: template page navigation bugs
Updates the template logic for when page navigation is included in the
generated HTML in the notmyidea theme, fixing:
* Issue #1068: useless pagination controls should not be displayed when a
single page is generated (i.e. "Page 1/1"). New logic prevents the
generation of these superfluous page navigation controls. Tests updated
accordingly.
* Issue #1572: when multiple pages are generated and the last page contains
only one item, the closing and tags are not generated,
resulting in page breakage. We need to check if
articles_page.has_other_pages(); if it does, a list has been generated per
line 19 or 25 and the tags must be closed.
---
pelican/tests/output/custom/category/bar.html | 3 ---
pelican/tests/output/custom/category/cat1.html | 3 ---
pelican/tests/output/custom/category/misc.html | 3 ---
pelican/tests/output/custom/category/yeah.html | 3 ---
pelican/tests/output/custom/tag/bar.html | 3 ---
pelican/tests/output/custom/tag/foo.html | 3 ---
pelican/tests/output/custom/tag/foobar.html | 3 ---
pelican/tests/output/custom/tag/yeah.html | 3 ---
.../tests/output/custom_locale/category/bar.html | 3 ---
.../tests/output/custom_locale/category/cat1.html | 3 ---
.../tests/output/custom_locale/category/misc.html | 3 ---
.../tests/output/custom_locale/category/yeah.html | 3 ---
pelican/tests/output/custom_locale/tag/bar.html | 3 ---
pelican/tests/output/custom_locale/tag/foo.html | 3 ---
pelican/tests/output/custom_locale/tag/foobar.html | 3 ---
pelican/tests/output/custom_locale/tag/yeah.html | 3 ---
pelican/themes/notmyidea/templates/index.html | 13 ++++---------
17 files changed, 4 insertions(+), 57 deletions(-)
diff --git a/pelican/tests/output/custom/category/bar.html b/pelican/tests/output/custom/category/bar.html
index 0bc5b02e..9666d2e2 100644
--- a/pelican/tests/output/custom/category/bar.html
+++ b/pelican/tests/output/custom/category/bar.html
@@ -51,9 +51,6 @@ YEAH !
diff --git a/pelican/tests/output/custom/tag/foo.html b/pelican/tests/output/custom/tag/foo.html
index 7c38c24c..9c58956b 100644
--- a/pelican/tests/output/custom/tag/foo.html
+++ b/pelican/tests/output/custom/tag/foo.html
@@ -80,9 +80,6 @@ as well as inline markup.
diff --git a/pelican/tests/output/custom_locale/tag/foo.html b/pelican/tests/output/custom_locale/tag/foo.html
index ed67ede0..288f1768 100644
--- a/pelican/tests/output/custom_locale/tag/foo.html
+++ b/pelican/tests/output/custom_locale/tag/foo.html
@@ -80,9 +80,6 @@ as well as inline markup.
{% include 'article_infos.html' %}{{ article.content }}{% include 'comments.html' %}
- {% if loop.length == 1 %}
- {% include 'pagination.html' %}
- {% endif %}
{% if loop.length > 1 %}
@@ -42,13 +39,11 @@
{% endif %}
{% if loop.last %}
- {% if loop.length > 1 %}
+ {% if loop.length > 1 or articles_page.has_other_pages() %}
- {% endif %}
- {% if articles_page.has_previous() or loop.length > 1 %}
- {% include 'pagination.html' %}
- {% endif %}
- {% if loop.length > 1 %}
+ {% if articles_page.has_other_pages() %}
+ {% include 'pagination.html' %}
+ {% endif %}
{% endif %}
{% endif %}
From 21544a404cf51b60d407b0fd4eeadfc1d8339a9c Mon Sep 17 00:00:00 2001
From: Elana Hashman
Date: Sun, 21 Jun 2015 13:35:41 -0600
Subject: [PATCH 0114/1094] Update simple theme to fix #1068
---
pelican/themes/simple/templates/index.html | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/pelican/themes/simple/templates/index.html b/pelican/themes/simple/templates/index.html
index 20104db9..4bd81985 100644
--- a/pelican/themes/simple/templates/index.html
+++ b/pelican/themes/simple/templates/index.html
@@ -21,6 +21,8 @@
{% endfor %}
-{% include 'pagination.html' %}
+{% if articles_page.has_other_pages() %}
+ {% include 'pagination.html' %}
+{% endif %}
{% endblock content %}
From ff88c4bb34779d979b8ce4ba042c07621204ea1a Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Fri, 26 Jun 2015 09:44:45 -0700
Subject: [PATCH 0115/1094] Document enabling per-block Markdown line numbers
Since users frequently ask how to enable line numbers in
Markdown-formatted code blocks, adding instructions to the docs will
allow us to point users there.
Fixes #1773
---
docs/content.rst | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/docs/content.rst b/docs/content.rst
index 0e3310f1..1e6ba099 100644
--- a/docs/content.rst
+++ b/docs/content.rst
@@ -420,22 +420,29 @@ which posts are translations::
Syntax highlighting
===================
-Pelican is able to provide colorized syntax highlighting for your code blocks.
-To do so, you have to use the following conventions inside your content files.
+Pelican can provide colorized syntax highlighting for your code blocks.
+To do so, you must use the following conventions inside your content files.
-For reStructuredText, use the code-block directive::
+For reStructuredText, use the ``code-block`` directive to specify the type
+of code to be highlighted (in these examples, we'll use ``python``)::
- .. code-block:: identifier
+ .. code-block:: python
-
+ print("Pelican is a static site generator.")
-For Markdown, include the language identifier just above the code block,
-indenting both the identifier and code::
+For Markdown, which utilizes the `CodeHilite extension`_ to provide syntax
+highlighting, include the language identifier just above the code block,
+indenting both the identifier and the code::
- A block of text.
+ There are two ways to specify the identifier:
- :::identifier
-
+ :::python
+ print("The triple-colon syntax will *not* show line numbers.")
+
+ To display line numbers, use a path-less shebang instead of colons:
+
+ #!python
+ print("The path-less shebang syntax *will* show line numbers.")
The specified identifier (e.g. ``python``, ``ruby``) should be one that
appears on the `list of available lexers `_.
@@ -521,4 +528,5 @@ metadata to include ``Status: published``.
.. _AsciiDoc: http://www.methods.co.nz/asciidoc/
.. _pelican-plugins: http://github.com/getpelican/pelican-plugins
.. _Markdown Extensions: http://pythonhosted.org/Markdown/extensions/
+.. _CodeHilite extension: http://pythonhosted.org/Markdown/extensions/code_hilite.html#syntax
.. _i18n_subsites plugin: http://github.com/getpelican/pelican-plugins/tree/master/i18n_subsites
From ec5c77b25145f7c20fee24c6b85b478295dbc956 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Thu, 18 Jun 2015 23:33:20 +0200
Subject: [PATCH 0116/1094] remove PAGES; use pages instead
* remove PAGES from context as pages is available
* add section to FAQ to provide guidance
---
docs/faq.rst | 13 +++++++++++++
docs/themes.rst | 2 ++
pelican/generators.py | 1 -
pelican/themes/notmyidea/templates/base.html | 2 +-
pelican/themes/simple/templates/base.html | 2 +-
5 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/docs/faq.rst b/docs/faq.rst
index ff473624..08df017d 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -250,3 +250,16 @@ moved out of the pelican core and into a separate `plugin
`_.
See the :ref:`plugins` documentation further information about the
Pelican plugin system.
+
+Since I upgraded Pelican my Pages are no longer rendered
+========================================================
+Pages were available to Themes as lowercase ``pages`` and uppercase
+``PAGES``. To bring this inline with the :ref:`templates-variables` section,
+``PAGES`` has been removed. This is quickly resolved by updating your theme
+to iterate over ``pages`` instead of ``PAGES``. Just replace::
+
+ {% for pg in PAGES %}
+
+with something like::
+
+ {% for pg in pages %}
diff --git a/docs/themes.rst b/docs/themes.rst
index 6ca753c6..3978e693 100644
--- a/docs/themes.rst
+++ b/docs/themes.rst
@@ -47,6 +47,8 @@ To make your own theme, you must follow the following structure::
if it helps you keep things organized while creating your theme.
+.. _templates-variables:
+
Templates and variables
=======================
diff --git a/pelican/generators.py b/pelican/generators.py
index 0a5298e4..da651252 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -644,7 +644,6 @@ class PagesGenerator(CachingGenerator):
process_translations(hidden_pages))
self._update_context(('pages', 'hidden_pages'))
- self.context['PAGES'] = self.pages
self.save_cache()
self.readers.save_cache()
diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html
index 45ab4044..188715d4 100644
--- a/pelican/themes/notmyidea/templates/base.html
+++ b/pelican/themes/notmyidea/templates/base.html
@@ -25,7 +25,7 @@