From 24a1254f034a238f481f8b889a961dbde99c551d Mon Sep 17 00:00:00 2001
From: Julien Palard
Date: Fri, 30 Sep 2016 15:29:14 +0200
Subject: [PATCH 001/867] Explicitly disallow duplications of URL and save_as.
---
pelican/readers.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index 415e7558..56e54355 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -31,6 +31,20 @@ except ImportError:
# 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()
+
+DUPLICATES_DEFINITIONS_ALLOWED = {
+ 'tags': False,
+ 'date': False,
+ 'modified': False,
+ 'status': False,
+ 'category': False,
+ 'author': False,
+ 'save_as': False,
+ 'URL': False,
+ 'authors': False,
+ 'slug': False
+}
+
METADATA_PROCESSORS = {
'tags': lambda x, y: ([
Tag(tag, y)
@@ -264,7 +278,7 @@ class MarkdownReader(BaseReader):
self._md.reset()
formatted = self._md.convert(formatted_values)
output[name] = self.process_metadata(name, formatted)
- elif name in METADATA_PROCESSORS:
+ elif not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
if len(value) > 1:
logger.warning(
'Duplicate definition of `%s` '
From 9e574e9d8c6e4d6377a6fcefe1579bb998c7223a Mon Sep 17 00:00:00 2001
From: Julien Palard
Date: Fri, 30 Sep 2016 15:33:05 +0200
Subject: [PATCH 002/867] Just in case someone forgot the
DUPLICATES_DEFINITIONS_ALLOWED but add in METADATA_PROCESSORS.
---
pelican/readers.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index 56e54355..5220f1a0 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -278,7 +278,8 @@ class MarkdownReader(BaseReader):
self._md.reset()
formatted = self._md.convert(formatted_values)
output[name] = self.process_metadata(name, formatted)
- elif not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
+ elif (not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True) or
+ name in METADATA_PROCESSORS):
if len(value) > 1:
logger.warning(
'Duplicate definition of `%s` '
From e8a87e5d3cb82081127a8a07da574059668688de Mon Sep 17 00:00:00 2001
From: Julien Palard
Date: Mon, 10 Oct 2016 12:23:26 +0200
Subject: [PATCH 003/867] As not allowing duplicates in processed items is
counter intuitive, let's allow it.
Also it may be allowed in the future (to process multiple values).
Also @avaris think it's bad to test something twice (see
https://github.com/getpelican/pelican/pull/2017), but for me confusion
lies in the "Why is list processing forbidden?", so, in a way, our
ideas converges in "let's not disallow processed items to be lists".
This reverts commit 9e574e9d8c6e4d6377a6fcefe1579bb998c7223a.
---
pelican/readers.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index 503db679..a7712f0b 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -278,8 +278,7 @@ class MarkdownReader(BaseReader):
self._md.reset()
formatted = self._md.convert(formatted_values)
output[name] = self.process_metadata(name, formatted)
- elif (not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True) or
- name in METADATA_PROCESSORS):
+ elif not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
if len(value) > 1:
logger.warning(
'Duplicate definition of `%s` '
From e07c53a09dab16ec33e44a84a1c5db5e58deb61c Mon Sep 17 00:00:00 2001
From: Julien Palard
Date: Wed, 26 Oct 2016 08:34:52 +0200
Subject: [PATCH 004/867] FIX: Those keys are looked up lowercased.
---
pelican/readers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index a7712f0b..74b617e6 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -40,7 +40,7 @@ DUPLICATES_DEFINITIONS_ALLOWED = {
'category': False,
'author': False,
'save_as': False,
- 'URL': False,
+ 'url': False,
'authors': False,
'slug': False
}
From 0936d5f6ee19908d455d9bae35552019237f13da Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Wed, 14 Dec 2016 13:56:47 -0800
Subject: [PATCH 005/867] Prepare version 3.7.1.dev0 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 129499c2..31868353 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,11 @@
Release history
###############
+Next release
+============
+
+- Nothing yet
+
3.7.0 (2016-12-12)
==================
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 159121fa..cdcd98b1 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -25,7 +25,7 @@ from pelican.utils import (clean_output_dir, file_watcher,
folder_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "3.7.0"
+__version__ = "3.7.1.dev0"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
logger = logging.getLogger(__name__)
diff --git a/setup.py b/setup.py
index 345ba0db..e69080f5 100755
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@ CHANGELOG = open('docs/changelog.rst').read()
setup(
name='pelican',
- version='3.7.0',
+ version='3.7.1.dev0',
url='http://getpelican.com/',
author='Alexis Metaireau',
maintainer='Justin Mayer',
From 5c6ae32f99c807364c0a3c439985e30c1c0f572d Mon Sep 17 00:00:00 2001
From: "L. E. Segovia"
Date: Wed, 14 Dec 2016 14:08:09 +0100
Subject: [PATCH 006/867] For python 2+, initialize locale. Fixes #2043
---
pelican/tools/pelican_quickstart.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
index 6b4eb5a5..ecbc3510 100755
--- a/pelican/tools/pelican_quickstart.py
+++ b/pelican/tools/pelican_quickstart.py
@@ -21,6 +21,8 @@ import six
from pelican import __version__
+if (sys.version_info.major == 2):
+ locale.setlocale(locale.LC_ALL, '')
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"templates")
From c96edef571aaddc138f8e15946b99fd15c8cca64 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Fri, 23 Dec 2016 07:41:52 -0800
Subject: [PATCH 007/867] Ignore samples for GitHub language stats analysis
Fixes #2069
---
.gitattributes | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.gitattributes b/.gitattributes
index dfe07704..1b6f7c89 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
+
+# Improve accuracy of GitHub's Linguist-powered language statistics
+samples/* linguist-vendored
From fc9227215b86914baf2082dba36f485743183679 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Fri, 23 Dec 2016 08:00:45 -0800
Subject: [PATCH 008/867] Ignore test content & output in language analysis
Refs #2069
---
.gitattributes | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitattributes b/.gitattributes
index 1b6f7c89..da974e4b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,4 +2,6 @@
* text=auto
# Improve accuracy of GitHub's Linguist-powered language statistics
+pelican/tests/content/*
+pelican/tests/output/*
samples/* linguist-vendored
From 98d1d4e3387dc70a062f1897fa5d8f71d5431e2c Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Fri, 23 Dec 2016 08:03:57 -0800
Subject: [PATCH 009/867] Add missing .gitattributes attributes. Refs #2069
---
.gitattributes | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.gitattributes b/.gitattributes
index da974e4b..9053428d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,6 @@
* text=auto
# Improve accuracy of GitHub's Linguist-powered language statistics
-pelican/tests/content/*
-pelican/tests/output/*
+pelican/tests/content/* linguist-vendored
+pelican/tests/output/* linguist-vendored
samples/* linguist-vendored
From 84920e8fdf754b642b9a137cad38c38f9417bde5 Mon Sep 17 00:00:00 2001
From: Lucas Chavez
Date: Wed, 21 Dec 2016 13:22:29 -0700
Subject: [PATCH 010/867] Set locale to LC_ALL in Quickstart script. Fixes
#2043
---
pelican/tools/pelican_quickstart.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
index ecbc3510..39e58f6f 100755
--- a/pelican/tools/pelican_quickstart.py
+++ b/pelican/tools/pelican_quickstart.py
@@ -21,8 +21,12 @@ import six
from pelican import __version__
-if (sys.version_info.major == 2):
- locale.setlocale(locale.LC_ALL, '')
+locale.setlocale(locale.LC_ALL, '')
+_DEFAULT_LANGUAGE = locale.getlocale()[0]
+if _DEFAULT_LANGUAGE is None:
+ _DEFAULT_LANGUAGE = 'English'
+else:
+ _DEFAULT_LANGUAGE = _DEFAULT_LANGUAGE.split('_')[0]
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"templates")
@@ -51,7 +55,7 @@ CONF = {
'github_pages_branch': _GITHUB_PAGES_BRANCHES['project'],
'default_pagination': 10,
'siteurl': '',
- 'lang': locale.getlocale()[0].split('_')[0],
+ 'lang': _DEFAULT_LANGUAGE,
'timezone': _DEFAULT_TIMEZONE
}
From b46fbb78793a873e6280a791b37ab2d42055eefc Mon Sep 17 00:00:00 2001
From: Alexandre de Verteuil
Date: Sat, 23 Jul 2016 03:04:04 -0400
Subject: [PATCH 011/867] Add two STATIC_ settings. Fix #1982
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
STATIC_CREATE_LINKS = False
Create links instead of copying files. If the content and output
directories are on the same device, then create hard links. Falls
back to symbolic links if the output directory is on a different
filesystem. If symlinks are created, don’t forget to add the -L or
--copy-links option to rsync when uploading your site.
STATIC_CHECK_IF_MODIFIED = False
If set to True, and STATIC_CREATE_LINKS is False, compare mtimes of
content and output files, and only copy content files that are newer
than existing output files.
---
docs/settings.rst | 14 +++
pelican/generators.py | 107 +++++++++++++++-----
pelican/settings.py | 2 +
pelican/tests/test_generators.py | 164 ++++++++++++++++++++++++++++++-
4 files changed, 261 insertions(+), 26 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index aef9f674..54797c83 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -230,6 +230,20 @@ Basic settings
``PAGE_PATHS``. If you are trying to publish your site's source files,
consider using the ``OUTPUT_SOURCES`` setting instead.
+.. data:: STATIC_CREATE_LINKS = False
+
+ Create links instead of copying files. If the content and output
+ directories are on the same device, then create hard links. Falls
+ back to symbolic links if the output directory is on a different
+ filesystem. If symlinks are created, don't forget to add the ``-L``
+ or ``--copy-links`` option to rsync when uploading your site.
+
+.. data:: STATIC_CHECK_IF_MODIFIED = False
+
+ If set to ``True``, and ``STATIC_CREATE_LINKS`` is ``False``, compare
+ mtimes of content and output files, and only copy content files that
+ are newer than existing output files.
+
.. data:: TYPOGRIFY = False
If set to True, several typographical improvements will be incorporated into
diff --git a/pelican/generators.py b/pelican/generators.py
index 88752392..f3590155 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import calendar
+import errno
import fnmatch
import logging
import os
@@ -20,9 +21,8 @@ from pelican import signals
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 (DateFormatter, copy, copy_file_metadata, mkdir_p,
- posixize_path, process_translations,
- python_2_unicode_compatible)
+from pelican.utils import (DateFormatter, copy, mkdir_p, posixize_path,
+ process_translations, python_2_unicode_compatible)
logger = logging.getLogger(__name__)
@@ -682,21 +682,9 @@ class StaticGenerator(Generator):
def __init__(self, *args, **kwargs):
super(StaticGenerator, self).__init__(*args, **kwargs)
+ self.fallback_to_symlinks = False
signals.static_generator_init.send(self)
- def _copy_paths(self, paths, source, destination, output_path,
- final_path=None):
- """Copy all the paths from source to destination"""
- for path in paths:
- if final_path:
- copy(os.path.join(source, 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),
- self.settings['IGNORE_FILES'])
-
def generate_context(self):
self.staticfiles = []
for f in self.get_files(self.settings['STATIC_PATHS'],
@@ -724,13 +712,88 @@ class StaticGenerator(Generator):
self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme,
self.settings['THEME_STATIC_DIR'], self.output_path,
os.curdir)
- # copy all Static files
for sc in self.context['staticfiles']:
- source_path = os.path.join(self.path, sc.source_path)
- save_as = os.path.join(self.output_path, sc.save_as)
- mkdir_p(os.path.dirname(save_as))
- logger.info('Copying %s to %s', sc.source_path, sc.save_as)
- copy_file_metadata(source_path, save_as)
+ if self._file_update_required(sc):
+ self._link_or_copy_staticfile(sc)
+ else:
+ logger.debug('%s is up to date, not copying', sc.source_path)
+
+ def _copy_paths(self, paths, source, destination, output_path,
+ final_path=None):
+ """Copy all the paths from source to destination"""
+ for path in paths:
+ if final_path:
+ copy(os.path.join(source, 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),
+ self.settings['IGNORE_FILES'])
+
+ def _file_update_required(self, staticfile):
+ source_path = os.path.join(self.path, staticfile.source_path)
+ save_as = os.path.join(self.output_path, staticfile.save_as)
+ if not os.path.exists(save_as):
+ return True
+ elif (self.settings['STATIC_CREATE_LINKS'] and
+ os.path.samefile(source_path, save_as)):
+ return False
+ elif (self.settings['STATIC_CREATE_LINKS'] and
+ os.path.realpath(save_as) == source_path):
+ return False
+ elif not self.settings['STATIC_CHECK_IF_MODIFIED']:
+ return True
+ else:
+ return self._source_is_newer(staticfile)
+
+ def _source_is_newer(self, staticfile):
+ source_path = os.path.join(self.path, staticfile.source_path)
+ save_as = os.path.join(self.output_path, staticfile.save_as)
+ s_mtime = os.path.getmtime(source_path)
+ d_mtime = os.path.getmtime(save_as)
+ return s_mtime > d_mtime
+
+ def _link_or_copy_staticfile(self, sc):
+ if self.settings['STATIC_CREATE_LINKS']:
+ self._link_staticfile(sc)
+ else:
+ self._copy_staticfile(sc)
+
+ def _copy_staticfile(self, sc):
+ source_path = os.path.join(self.path, sc.source_path)
+ save_as = os.path.join(self.output_path, sc.save_as)
+ self._mkdir(os.path.dirname(save_as))
+ copy(source_path, save_as)
+ logger.info('Copying %s to %s', sc.source_path, sc.save_as)
+
+ def _link_staticfile(self, sc):
+ source_path = os.path.join(self.path, sc.source_path)
+ save_as = os.path.join(self.output_path, sc.save_as)
+ self._mkdir(os.path.dirname(save_as))
+ try:
+ if os.path.lexists(save_as):
+ os.unlink(save_as)
+ logger.info('Linking %s and %s', sc.source_path, sc.save_as)
+ if self.fallback_to_symlinks:
+ os.symlink(source_path, save_as)
+ else:
+ os.link(source_path, save_as)
+ except OSError as err:
+ if err.errno == errno.EXDEV: # 18: Invalid cross-device link
+ logger.debug(
+ "Cross-device links not valid. "
+ "Creating symbolic links instead."
+ )
+ self.fallback_to_symlinks = True
+ self._link_staticfile(sc)
+ else:
+ raise err
+
+ def _mkdir(self, path):
+ if os.path.lexists(path) and not os.path.isdir(path):
+ os.unlink(path)
+ mkdir_p(path)
class SourceFileGenerator(Generator):
diff --git a/pelican/settings.py b/pelican/settings.py
index 1b0bd67d..0088d3d2 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -82,6 +82,8 @@ DEFAULT_CONFIG = {
'PAGE_LANG_SAVE_AS': posix_join('pages', '{slug}-{lang}.html'),
'STATIC_URL': '{path}',
'STATIC_SAVE_AS': '{path}',
+ 'STATIC_CREATE_LINKS': False,
+ 'STATIC_CHECK_IF_MODIFIED': False,
'CATEGORY_URL': 'category/{slug}.html',
'CATEGORY_SAVE_AS': posix_join('category', '{slug}.html'),
'TAG_URL': 'tag/{slug}.html',
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 3c4684df..5f2151c3 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -5,7 +5,7 @@ import locale
import os
from codecs import open
-from shutil import rmtree
+from shutil import copy, rmtree
from tempfile import mkdtemp
from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator,
@@ -674,6 +674,30 @@ class TestStaticGenerator(unittest.TestCase):
def setUp(self):
self.content_path = os.path.join(CUR_DIR, 'mixed_content')
+ self.temp_content = mkdtemp(prefix='testcontent.')
+ self.temp_output = mkdtemp(prefix='testoutput.')
+ self.settings = get_settings()
+ self.settings['PATH'] = self.temp_content
+ self.settings['STATIC_PATHS'] = ["static"]
+ self.settings['OUTPUT_PATH'] = self.temp_output
+ os.mkdir(os.path.join(self.temp_content, "static"))
+ self.startfile = os.path.join(self.temp_content,
+ "static", "staticfile")
+ self.endfile = os.path.join(self.temp_output, "static", "staticfile")
+ self.generator = StaticGenerator(
+ context={'filenames': {}},
+ settings=self.settings,
+ path=self.temp_content,
+ theme="",
+ output_path=self.temp_output,
+ )
+
+ def tearDown(self):
+ rmtree(self.temp_content)
+ rmtree(self.temp_output)
+
+ def set_ancient_mtime(self, path, timestamp=1):
+ os.utime(path, (timestamp, timestamp))
def test_static_excludes(self):
"""Test that StaticGenerator respects STATIC_EXCLUDES.
@@ -687,7 +711,7 @@ class TestStaticGenerator(unittest.TestCase):
StaticGenerator(
context=context, settings=settings,
- path=settings['PATH'], output_path=None,
+ path=settings['PATH'], output_path=self.temp_output,
theme=settings['THEME']).generate_context()
staticnames = [os.path.basename(c.source_path)
@@ -716,7 +740,7 @@ class TestStaticGenerator(unittest.TestCase):
for generator_class in (PagesGenerator, StaticGenerator):
generator_class(
context=context, settings=settings,
- path=settings['PATH'], output_path=None,
+ path=settings['PATH'], output_path=self.temp_output,
theme=settings['THEME']).generate_context()
staticnames = [os.path.basename(c.source_path)
@@ -733,7 +757,7 @@ class TestStaticGenerator(unittest.TestCase):
for generator_class in (PagesGenerator, StaticGenerator):
generator_class(
context=context, settings=settings,
- path=settings['PATH'], output_path=None,
+ path=settings['PATH'], output_path=self.temp_output,
theme=settings['THEME']).generate_context()
staticnames = [os.path.basename(c.source_path)
@@ -742,3 +766,135 @@ class TestStaticGenerator(unittest.TestCase):
self.assertTrue(
any(name.endswith(".md") for name in staticnames),
"STATIC_EXCLUDE_SOURCES=False failed to include a markdown file")
+
+ def test_copy_one_file(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ with open(self.endfile, "r") as f:
+ self.assertEqual(f.read(), "staticcontent")
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_file_update_required_when_dest_does_not_exist(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ update_required = self.generator._file_update_required(staticfile)
+ self.assertTrue(update_required)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_dest_and_source_mtimes_are_equal(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ self.settings['STATIC_CHECK_IF_MODIFIED'] = True
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ copy(staticfile.source_path, staticfile.save_as)
+ isnewer = self.generator._source_is_newer(staticfile)
+ self.assertFalse(isnewer)
+
+ @unittest.skipUnless(MagicMock, 'Needs Mock module')
+ def test_source_is_newer(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ copy(staticfile.source_path, staticfile.save_as)
+ self.set_ancient_mtime(staticfile.save_as)
+ isnewer = self.generator._source_is_newer(staticfile)
+ self.assertTrue(isnewer)
+
+ def test_skip_file_when_source_is_not_newer(self):
+ self.settings['STATIC_CHECK_IF_MODIFIED'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ with open(self.endfile, "w") as f:
+ f.write("staticcontent")
+ expected = os.path.getmtime(self.endfile)
+ self.set_ancient_mtime(self.startfile)
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertEqual(os.path.getmtime(self.endfile), expected)
+
+ def test_dont_link_by_default(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertFalse(os.path.samefile(self.startfile, self.endfile))
+
+ def test_output_file_is_linked_to_source(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.samefile(self.startfile, self.endfile))
+
+ def test_output_file_exists_and_is_newer(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ with open(self.endfile, "w") as f:
+ f.write("othercontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.samefile(self.startfile, self.endfile))
+
+ def test_can_symlink_when_hardlink_not_possible(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ self.generator.fallback_to_symlinks = True
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.islink(self.endfile))
+
+ def test_existing_symlink_is_considered_up_to_date(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ os.symlink(self.startfile, self.endfile)
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ requires_update = self.generator._file_update_required(staticfile)
+ self.assertFalse(requires_update)
+
+ def test_invalid_symlink_is_overwritten(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ os.symlink("invalid", self.endfile)
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ requires_update = self.generator._file_update_required(staticfile)
+ self.assertTrue(requires_update)
+ self.generator.fallback_to_symlinks = True
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertEqual(os.path.realpath(self.endfile), self.startfile)
+
+ def test_delete_existing_file_before_mkdir(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ with open(os.path.join(self.temp_output, "static"), "w") as f:
+ f.write("This file should be a directory")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(
+ os.path.isdir(os.path.join(self.temp_output, "static")))
+ self.assertTrue(os.path.isfile(self.endfile))
From 01d536d7c9ddd6e1090d69b5f4445e37d4019d40 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Sat, 31 Dec 2016 15:10:46 -0800
Subject: [PATCH 012/867] Replace m-dash with semi-colon in changelog
Stop-gap measure to address:
https://github.com/getpelican/pelican/commit/e3ab685a26b3dd2d198319b625d380c4f80afbf7#commitcomment-20333961
---
docs/changelog.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 31868353..1956e47c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -14,7 +14,7 @@ Next release
```` for modifications
* Simplify Atom feed ID generation and support URL fragments
* Produce category feeds with category-specific titles
-* RSS feeds now default to summary instead of full content —
+* RSS feeds now default to summary instead of full content;
set ``RSS_FEED_SUMMARY_ONLY = False`` to revert to previous behavior
* Replace ``MD_EXTENSIONS`` with ``MARKDOWN`` setting
* Replace ``JINJA_EXTENSIONS`` with more-robust ``JINJA_ENVIRONMENT`` setting
From 6fe2fecb1327e8a72008e58b68329b8c25dbb667 Mon Sep 17 00:00:00 2001
From: Deniz Turgut
Date: Tue, 3 Jan 2017 21:51:23 +0300
Subject: [PATCH 013/867] Specify encoding for README and CHANGELOG in setup.py
---
setup.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/setup.py b/setup.py
index e69080f5..f221930d 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python
+from io import open
from os import walk
from os.path import join, relpath
+import sys
from setuptools import setup
@@ -17,8 +19,12 @@ entry_points = {
]
}
-README = open('README.rst').read()
-CHANGELOG = open('docs/changelog.rst').read()
+README = open('README.rst', encoding='utf-8').read()
+CHANGELOG = open('docs/changelog.rst', encoding='utf-8').read()
+
+description = u'\n'.join([README, CHANGELOG])
+if sys.version_info.major < 3:
+ description = description.encode('utf-8')
setup(
name='pelican',
@@ -29,7 +35,7 @@ setup(
author_email='authors@getpelican.com',
description="Static site generator supporting reStructuredText and "
"Markdown source content.",
- long_description=README + '\n' + CHANGELOG,
+ long_description=description,
packages=['pelican', 'pelican.tools'],
package_data={
# we manually collect the package data, as opposed to using,
From ce38e318bd4905f75c988ee930620ebcd65588c0 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Tue, 10 Jan 2017 13:34:09 -0800
Subject: [PATCH 014/867] Bump version 3.7.1
---
docs/changelog.rst | 7 ++++---
docs/conf.py | 2 +-
pelican/__init__.py | 2 +-
setup.py | 2 +-
4 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 1956e47c..31c50182 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,10 +1,11 @@
Release history
###############
-Next release
-============
+3.7.1 (2017-01-10)
+==================
-- Nothing yet
+* Fix locale issues in Quickstart script
+* Specify encoding for README and CHANGELOG in setup.py
3.7.0 (2016-12-12)
==================
diff --git a/docs/conf.py b/docs/conf.py
index 63a4a269..81a06e3a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,7 +21,7 @@ copyright = '2015, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
release = __version__
version = '.'.join(release.split('.')[:1])
-last_stable = '3.7.0'
+last_stable = '3.7.1'
rst_prolog = '''
.. |last_stable| replace:: :pelican-doc:`{0}`
'''.format(last_stable)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index cdcd98b1..25da26e2 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -25,7 +25,7 @@ from pelican.utils import (clean_output_dir, file_watcher,
folder_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "3.7.1.dev0"
+__version__ = "3.7.1"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
logger = logging.getLogger(__name__)
diff --git a/setup.py b/setup.py
index f221930d..62ce8124 100755
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ if sys.version_info.major < 3:
setup(
name='pelican',
- version='3.7.1.dev0',
+ version='3.7.1',
url='http://getpelican.com/',
author='Alexis Metaireau',
maintainer='Justin Mayer',
From 25732f7be68bfcfd1b761f10b56ff55a14c44dde Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Tue, 10 Jan 2017 13:51:37 -0800
Subject: [PATCH 015/867] Prepare version 3.7.2.dev0 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 31c50182..2b365a45 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,11 @@
Release history
###############
+Next release
+============
+
+- Nothing yet
+
3.7.1 (2017-01-10)
==================
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 25da26e2..69332706 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -25,7 +25,7 @@ from pelican.utils import (clean_output_dir, file_watcher,
folder_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "3.7.1"
+__version__ = "3.7.2.dev0"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
logger = logging.getLogger(__name__)
diff --git a/setup.py b/setup.py
index 62ce8124..3645dc04 100755
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ if sys.version_info.major < 3:
setup(
name='pelican',
- version='3.7.1',
+ version='3.7.2.dev0',
url='http://getpelican.com/',
author='Alexis Metaireau',
maintainer='Justin Mayer',
From d61e2ccd6ba051ff581cc356c384936d240ea495 Mon Sep 17 00:00:00 2001
From: Sjoerd
Date: Sat, 14 Jan 2017 13:51:10 +0100
Subject: [PATCH 016/867] Use better headers in tips docs
Instead of "Tip 1", "Tip 2" under "Extra tips", use headers describing
the content.
Also, remove the "hint" pointer as it is not really a hint and the
document consists of tips anyway.
---
docs/tips.rst | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/docs/tips.rst b/docs/tips.rst
index 9f45f877..50160380 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -105,10 +105,8 @@ Custom 404 Pages
GitHub Pages will display the custom 404 page described above, as noted in the
relevant `GitHub docs `_.
-Extra Tips
-----------
-
-Tip #1:
+Update your site on each commit
+-------------------------------
To automatically update your Pelican site on each commit, you can create
a post-commit hook. For example, you can add the following to
@@ -116,7 +114,8 @@ a post-commit hook. For example, you can add the following to
pelican content -o output -s pelicanconf.py && ghp-import output && git push origin gh-pages
-Tip #2:
+Copy static files to the root of your site
+------------------------------------------
To use a `custom domain
`_ with
@@ -131,9 +130,8 @@ output directory. For example::
Note: use forward slashes, ``/``, even on Windows.
-.. hint::
- You can also use the ``EXTRA_PATH_METADATA`` mechanism
- to place a ``favicon.ico`` or ``robots.txt`` at the root of any site.
+You can also use the ``EXTRA_PATH_METADATA`` mechanism
+to place a ``favicon.ico`` or ``robots.txt`` at the root of any site.
How to add YouTube or Vimeo Videos
==================================
From 927d9c7ea5d40e5c1f930746d0f6d003a8503579 Mon Sep 17 00:00:00 2001
From: Bernhard Scheirle
Date: Mon, 16 Jan 2017 11:33:13 +0100
Subject: [PATCH 017/867] Add new signal: feed_generated
This signal gets emitted before a feed gets written to disk.
Therefore it allows plugins to do arbitrary changes to the feed.
---
docs/changelog.rst | 2 +-
docs/plugins.rst | 2 ++
pelican/signals.py | 1 +
pelican/writers.py | 1 +
4 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2b365a45..aa594a2c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,7 +4,7 @@ Release history
Next release
============
-- Nothing yet
+* New signal: ``feed_generated``
3.7.1 (2017-01-10)
==================
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 6a850100..008e7551 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -118,6 +118,8 @@ static_generator_init static_generator invoked in th
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__
content_written path, context invoked each time a content file is written.
+feed_generated context, feed invoked each time a feed gets generated. Can be used to modify a feed
+ object before it gets written.
feed_written path, context, feed invoked each time a feed file is written.
================================= ============================ ===========================================================================
diff --git a/pelican/signals.py b/pelican/signals.py
index aeeea9f6..0b10fdfa 100644
--- a/pelican/signals.py
+++ b/pelican/signals.py
@@ -47,4 +47,5 @@ content_object_init = signal('content_object_init')
# Writers signals
content_written = signal('content_written')
+feed_generated = signal('feed_generated')
feed_written = signal('feed_written')
diff --git a/pelican/writers.py b/pelican/writers.py
index d1c8069a..48388481 100644
--- a/pelican/writers.py
+++ b/pelican/writers.py
@@ -122,6 +122,7 @@ class Writer(object):
for i in range(max_items):
self._add_item_to_the_feed(feed, elements[i])
+ signals.feed_generated.send(context, feed=feed)
if path:
complete_path = os.path.join(self.output_path, path)
try:
From 9cca567bede872ad3383683478f0994576bb8ad5 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Mon, 23 Jan 2017 23:45:08 +0100
Subject: [PATCH 018/867] Add python3.6 test environment
* Add py36 environment to tox
* Unify dependencies across manual installation and tox
* Mention tox in the docs
---
docs/contribute.rst | 5 ++++-
requirements/developer.pip | 7 ++-----
requirements/docs.pip | 2 ++
requirements/style.pip | 2 ++
tox.ini | 11 +++++------
5 files changed, 15 insertions(+), 12 deletions(-)
create mode 100644 requirements/docs.pip
create mode 100644 requirements/style.pip
diff --git a/docs/contribute.rst b/docs/contribute.rst
index 0ed82dfd..1fdc8212 100644
--- a/docs/contribute.rst
+++ b/docs/contribute.rst
@@ -47,13 +47,16 @@ Or using ``pip``::
$ pip install -e .
+To conveniently test vs multiple python versions we also provide a tox file.
+
+
Building the docs
=================
If you make changes to the documentation, you should preview your changes
before committing them::
- $ pip install sphinx
+ $ pip install -r requirements/docs.pip
$ cd src/pelican/docs
$ make html
diff --git a/requirements/developer.pip b/requirements/developer.pip
index e5507151..5c2f5a69 100644
--- a/requirements/developer.pip
+++ b/requirements/developer.pip
@@ -1,6 +1,3 @@
-r test.pip
-
-# Development
-flake8
-flake8-import-order
-sphinx==1.4.9
+-r docs.pip
+-r style.pip
diff --git a/requirements/docs.pip b/requirements/docs.pip
new file mode 100644
index 00000000..525bdc40
--- /dev/null
+++ b/requirements/docs.pip
@@ -0,0 +1,2 @@
+sphinx==1.4.9
+sphinx_rtd_theme
diff --git a/requirements/style.pip b/requirements/style.pip
new file mode 100644
index 00000000..90225d01
--- /dev/null
+++ b/requirements/style.pip
@@ -0,0 +1,2 @@
+flake8
+flake8-import-order
diff --git a/tox.ini b/tox.ini
index cb400ea7..95cd5273 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py{27,33,34,35},docs,flake8
+envlist = py{27,33,34,35,36},docs,flake8
[testenv]
basepython =
@@ -7,10 +7,11 @@ basepython =
py33: python3.3
py34: python3.4
py35: python3.5
+ py36: python3.6
passenv = *
usedevelop=True
deps =
- -rrequirements/developer.pip
+ -rrequirements/test.pip
nose
nose-cov
coveralls
@@ -24,8 +25,7 @@ commands =
[testenv:docs]
basepython = python2.7
deps =
- sphinx==1.4.9
- sphinx_rtd_theme
+ -rrequirements/docs.pip
changedir = docs
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . _build/html
@@ -37,8 +37,7 @@ import-order-style = cryptography
[testenv:flake8]
basepython = python2.7
deps =
- flake8 <= 2.4.1
- git+https://github.com/public/flake8-import-order@2ac7052a4e02b4a8a0125a106d87465a3b9fd688
+ -rrequirements/style.pip
commands =
flake8 --version
flake8 pelican
From a982e0c4f437328f625e45b21762971c4ec94654 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Tue, 24 Jan 2017 00:04:58 +0100
Subject: [PATCH 019/867] Add py36 to travis builds
---
.travis.yml | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 7bb5a89f..5699cff1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,11 +7,12 @@ env:
- TOX_ENV=py27
- TOX_ENV=py33
- TOX_ENV=py34
+ - TOX_ENV=py35
matrix:
- include:
- - python: 3.5
- env:
- - TOX_ENV=py35
+ include:
+ - python: 3.6
+ env:
+ - TOX_ENV=py36
addons:
apt_packages:
- pandoc
From 5a19887a22eedc7060fe39711b383a61f57c4fa2 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Tue, 24 Jan 2017 00:07:15 +0100
Subject: [PATCH 020/867] Correct new flake8 warnings
---
pelican/log.py | 1 +
pelican/rstdirectives.py | 2 ++
pelican/tools/pelican_quickstart.py | 3 +++
pelican/tools/pelican_themes.py | 1 +
4 files changed, 7 insertions(+)
diff --git a/pelican/log.py b/pelican/log.py
index cec07bf0..70e069d3 100644
--- a/pelican/log.py
+++ b/pelican/log.py
@@ -197,6 +197,7 @@ class FatalLogger(LimitLogger):
if FatalLogger.errors_fatal:
raise RuntimeError('Error encountered')
+
logging.setLoggerClass(FatalLogger)
diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py
index b52785dd..def67cc7 100644
--- a/pelican/rstdirectives.py
+++ b/pelican/rstdirectives.py
@@ -70,6 +70,7 @@ class Pygments(Directive):
parsed = highlight('\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')]
+
directives.register_directive('code-block', Pygments)
directives.register_directive('sourcecode', Pygments)
@@ -90,4 +91,5 @@ def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
expl = m.group(1)
return [abbreviation(abbr, abbr, explanation=expl)], []
+
roles.register_local_role('abbr', abbr_role)
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
index 39e58f6f..ae0e8c9d 100755
--- a/pelican/tools/pelican_quickstart.py
+++ b/pelican/tools/pelican_quickstart.py
@@ -70,6 +70,7 @@ def _input_compat(prompt):
r = raw_input(prompt)
return r
+
if six.PY3:
str_compat = str
else:
@@ -81,6 +82,7 @@ else:
class _DEFAULT_PATH_TYPE(str_compat):
is_default_path = True
+
_DEFAULT_PATH = _DEFAULT_PATH_TYPE(os.curdir)
@@ -412,5 +414,6 @@ needed by Pelican.
print('Done. Your new project is available at %s' % CONF['basedir'])
+
if __name__ == "__main__":
main()
diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py
index e4bcb7c9..fd60c424 100755
--- a/pelican/tools/pelican_themes.py
+++ b/pelican/tools/pelican_themes.py
@@ -14,6 +14,7 @@ def err(msg, die=None):
if die:
sys.exit((die if type(die) is int else 1))
+
try:
import pelican
except:
From dcb6882fbcd243cc146df73b04b1066c332aa9db Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Tue, 24 Jan 2017 00:32:37 +0100
Subject: [PATCH 021/867] Update tox to 2.5.0
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 5699cff1..f4b68501 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,7 +20,7 @@ before_install:
- sudo apt-get update -qq
- sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
install:
- - pip install tox==2.0.1
+ - pip install tox==2.5.0
script: tox -e $TOX_ENV
notifications:
irc:
From 22208a9471b37a75af113fdede8d9bdb13608f4c Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Mon, 23 Jan 2017 23:54:38 +0100
Subject: [PATCH 022/867] Add test checking url replcmnt of unkn. file
There was no test case checking the behaviour of what happens when
trying to {filename} an unknown file.
Also changed the braces to `'` chars that we use elseware.
---
pelican/contents.py | 2 +-
pelican/tests/test_contents.py | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 1ded6cdb..0e842b39 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -246,7 +246,7 @@ class Content(object):
origin = origin.replace('\\', '/') # for Windows paths.
else:
logger.warning(
- "Unable to find `%s`, skipping url replacement.",
+ "Unable to find '%s', skipping url replacement.",
value.geturl(), extra={
'limit_msg': ("Other resources were not found "
"and their urls not replaced")})
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 2f774a6e..29a219f5 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -731,3 +731,19 @@ class TestStatic(LoggedTestCase):
msg="Replacement Indicator 'unknown' not recognized, "
"skipping replacement",
level=logging.WARNING)
+
+ def test_link_to_unknown_file(self):
+ "{filename} link to unknown file should trigger warning."
+
+ html = 'link'
+ page = Page(content=html,
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertEqual(content, html)
+ self.assertLogCountEqual(
+ count=1,
+ msg="Unable to find 'foo', skipping url replacement.",
+ level=logging.WARNING)
From 4006554a493f81e2303f63c1bf803915828e125c Mon Sep 17 00:00:00 2001
From: Jonas Wielicki
Date: Thu, 2 Feb 2017 20:38:42 +0100
Subject: [PATCH 023/867] Prevent to write outside the output directory
This is crude and simply raises RuntimeError. We would generally
want to have earlier checks which log a warning and do not call
write at all.
---
pelican/writers.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/pelican/writers.py b/pelican/writers.py
index d1c8069a..88a2bcfd 100644
--- a/pelican/writers.py
+++ b/pelican/writers.py
@@ -21,6 +21,18 @@ if not six.PY3:
logger = logging.getLogger(__name__)
+def _sanitised_join(base_directory, *parts):
+ joined = os.path.abspath(os.path.join(base_directory, *parts))
+ if not joined.startswith(base_directory):
+ raise RuntimeError(
+ "attempt to break out of output directory to {}".format(
+ joined
+ )
+ )
+
+ return joined
+
+
class Writer(object):
def __init__(self, output_path, settings=None):
@@ -123,7 +135,8 @@ class Writer(object):
self._add_item_to_the_feed(feed, elements[i])
if path:
- complete_path = os.path.join(self.output_path, path)
+ complete_path = _sanitised_join(self.output_path, path)
+
try:
os.makedirs(os.path.dirname(complete_path))
except Exception:
@@ -169,7 +182,8 @@ class Writer(object):
if localcontext['localsiteurl']:
context['localsiteurl'] = localcontext['localsiteurl']
output = template.render(localcontext)
- path = os.path.join(output_path, name)
+ path = _sanitised_join(output_path, name)
+
try:
os.makedirs(os.path.dirname(path))
except Exception:
From 018f4468cc480b533c9a7c6c8d48904e2019b4da Mon Sep 17 00:00:00 2001
From: Jonas Wielicki
Date: Fri, 3 Feb 2017 09:13:14 +0100
Subject: [PATCH 024/867] Check safety of save_as earlier if possible
The check in the writer still serves as a safety net.
---
pelican/contents.py | 34 +++++++++++++++++++++++++++++++---
pelican/tests/test_contents.py | 24 ++++++++++++++++++++++++
pelican/tests/test_utils.py | 33 +++++++++++++++++++++++++++++++++
pelican/utils.py | 12 ++++++++++++
pelican/writers.py | 18 +++---------------
5 files changed, 103 insertions(+), 18 deletions(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 1ded6cdb..db63dd9e 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -17,8 +17,9 @@ from pelican import signals
from pelican.settings import DEFAULT_CONFIG
from pelican.utils import (SafeDatetime, deprecated_attribute, memoized,
path_to_url, posixize_path,
- python_2_unicode_compatible, set_date_tzinfo,
- slugify, strftime, truncate_html_words)
+ python_2_unicode_compatible, sanitised_join,
+ set_date_tzinfo, slugify, strftime,
+ truncate_html_words)
# Import these so that they're avalaible when you import from pelican.contents.
from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
@@ -161,6 +162,22 @@ class Content(object):
if not hasattr(self, prop):
raise NameError(prop)
+ def valid_save_as(self):
+ """Return true if save_as doesn't write outside output path, false
+ otherwise."""
+ try:
+ output_path = self.settings["OUTPUT_PATH"]
+ except KeyError:
+ # we cannot check
+ return True
+
+ try:
+ sanitised_join(output_path, self.save_as)
+ except RuntimeError: # outside output_dir
+ return False
+
+ return True
+
@property
def url_format(self):
"""Returns the URL, formatted with the proper values"""
@@ -470,9 +487,20 @@ class Static(Page):
def is_valid_content(content, f):
try:
content.check_properties()
- return True
except NameError as e:
logger.error(
"Skipping %s: could not find information about '%s'",
f, six.text_type(e))
return False
+
+ if not content.valid_save_as():
+ logger.error(
+ "Skipping %s: file %r would be written outside output path",
+ f,
+ content.save_as,
+ )
+ # Note: future code might want to use a result variable instead, to
+ # allow showing multiple error messages at once.
+ return False
+
+ return True
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 2f774a6e..7f37edf1 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -497,6 +497,30 @@ class TestArticle(TestPage):
article = Article(**article_kwargs)
self.assertEqual(article.url, 'fedora.qa/this-week-in-fedora-qa/')
+ def test_valid_save_as_detects_breakout(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = '../foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertFalse(article.valid_save_as())
+
+ def test_valid_save_as_detects_breakout_to_root(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = '/foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertFalse(article.valid_save_as())
+
+ def test_valid_save_as_passes_valid(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = 'foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertTrue(article.valid_save_as())
+
class TestStatic(LoggedTestCase):
diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py
index 040707ca..9a7109d6 100644
--- a/pelican/tests/test_utils.py
+++ b/pelican/tests/test_utils.py
@@ -11,6 +11,8 @@ from tempfile import mkdtemp
import pytz
+import six
+
from pelican import utils
from pelican.generators import TemplatePagesGenerator
from pelican.settings import read_settings
@@ -666,3 +668,34 @@ class TestDateFormatter(unittest.TestCase):
with utils.pelican_open(output_path) as output_file:
self.assertEqual(output_file,
utils.strftime(self.date, 'date = %A, %d %B %Y'))
+
+
+class TestSanitisedJoin(unittest.TestCase):
+ def test_detect_parent_breakout(self):
+ with six.assertRaisesRegex(
+ self,
+ RuntimeError,
+ "Attempted to break out of output directory to /foo/test"):
+ utils.sanitised_join(
+ "/foo/bar",
+ "../test"
+ )
+
+ def test_detect_root_breakout(self):
+ with six.assertRaisesRegex(
+ self,
+ RuntimeError,
+ "Attempted to break out of output directory to /test"):
+ utils.sanitised_join(
+ "/foo/bar",
+ "/test"
+ )
+
+ def test_pass_deep_subpaths(self):
+ self.assertEqual(
+ utils.sanitised_join(
+ "/foo/bar",
+ "test"
+ ),
+ os.path.join("/foo/bar", "test")
+ )
diff --git a/pelican/utils.py b/pelican/utils.py
index 9d780039..a521d3a8 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -36,6 +36,18 @@ except ImportError:
logger = logging.getLogger(__name__)
+def sanitised_join(base_directory, *parts):
+ joined = os.path.abspath(os.path.join(base_directory, *parts))
+ if not joined.startswith(os.path.abspath(base_directory)):
+ raise RuntimeError(
+ "Attempted to break out of output directory to {}".format(
+ joined
+ )
+ )
+
+ return joined
+
+
def strftime(date, date_format):
'''
Replacement for built-in strftime
diff --git a/pelican/writers.py b/pelican/writers.py
index 88a2bcfd..049b05b2 100644
--- a/pelican/writers.py
+++ b/pelican/writers.py
@@ -13,7 +13,7 @@ import six
from pelican import signals
from pelican.paginator import Paginator
from pelican.utils import (get_relative_path, is_selected_for_writing,
- path_to_url, set_date_tzinfo)
+ path_to_url, sanitised_join, set_date_tzinfo)
if not six.PY3:
from codecs import open
@@ -21,18 +21,6 @@ if not six.PY3:
logger = logging.getLogger(__name__)
-def _sanitised_join(base_directory, *parts):
- joined = os.path.abspath(os.path.join(base_directory, *parts))
- if not joined.startswith(base_directory):
- raise RuntimeError(
- "attempt to break out of output directory to {}".format(
- joined
- )
- )
-
- return joined
-
-
class Writer(object):
def __init__(self, output_path, settings=None):
@@ -135,7 +123,7 @@ class Writer(object):
self._add_item_to_the_feed(feed, elements[i])
if path:
- complete_path = _sanitised_join(self.output_path, path)
+ complete_path = sanitised_join(self.output_path, path)
try:
os.makedirs(os.path.dirname(complete_path))
@@ -182,7 +170,7 @@ class Writer(object):
if localcontext['localsiteurl']:
context['localsiteurl'] = localcontext['localsiteurl']
output = template.render(localcontext)
- path = _sanitised_join(output_path, name)
+ path = sanitised_join(output_path, name)
try:
os.makedirs(os.path.dirname(path))
From 490e3646ba4274a60e261319e5ce42235f6a3696 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Mon, 27 Feb 2017 22:29:27 +0100
Subject: [PATCH 025/867] Implement review feedback
* improve wording on testing with fox (contribute.rst)
* fix whitespace (.travis.xml)
---
.travis.yml | 8 ++++----
docs/contribute.rst | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index f4b68501..a7a8732d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,10 +9,10 @@ env:
- TOX_ENV=py34
- TOX_ENV=py35
matrix:
- include:
- - python: 3.6
- env:
- - TOX_ENV=py36
+ include:
+ - python: 3.6
+ env:
+ - TOX_ENV=py36
addons:
apt_packages:
- pandoc
diff --git a/docs/contribute.rst b/docs/contribute.rst
index 1fdc8212..187c3007 100644
--- a/docs/contribute.rst
+++ b/docs/contribute.rst
@@ -47,7 +47,7 @@ Or using ``pip``::
$ pip install -e .
-To conveniently test vs multiple python versions we also provide a tox file.
+To conveniently test on multiple Python versions, we also provide a .tox file.
Building the docs
From b65a790b500a62a660afc54a0b49812013fe9794 Mon Sep 17 00:00:00 2001
From: fxfitz
Date: Mon, 13 Mar 2017 17:59:43 -0500
Subject: [PATCH 026/867] Switch over to use awscli
---
pelican/tools/templates/Makefile.in | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in
index 079b2844..06880c44 100644
--- a/pelican/tools/templates/Makefile.in
+++ b/pelican/tools/templates/Makefile.in
@@ -111,7 +111,7 @@ ftp_upload: publish
lftp ftp://$$(FTP_USER)@$$(FTP_HOST) -e "mirror -R $$(OUTPUTDIR) $$(FTP_TARGET_DIR) ; quit"
s3_upload: publish
- s3cmd sync $(OUTPUTDIR)/ s3://$(S3_BUCKET) --acl-public --delete-removed --guess-mime-type --no-mime-magic --no-preserve
+ aws s3 sync $(OUTPUTDIR)/ s3://$(S3_BUCKET) --acl public-read --delete
cf_upload: publish
cd $(OUTPUTDIR) && swift -v -A https://auth.api.rackspacecloud.com/v1.0 -U $(CLOUDFILES_USERNAME) -K $(CLOUDFILES_API_KEY) upload -c $(CLOUDFILES_CONTAINER) .
From 4917b8618aa71221f640bbf749cee7944eba32f3 Mon Sep 17 00:00:00 2001
From: Tim Wienk
Date: Sat, 11 Mar 2017 13:38:54 +0100
Subject: [PATCH 027/867] Fix setting None metadata from FILENAME_METADATA
matches.
This is relevant when using optional items in the expression. E.g. if an
optional captured group is not matched, the result of
`match.groupdict()` contains the captured group with value `None`.
---
pelican/readers.py | 2 +-
pelican/tests/test_readers.py | 38 +++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index e899838a..a620a1e8 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -672,7 +672,7 @@ def parse_path_metadata(source_path, settings=None, process=None):
# .items() for py3k compat.
for k, v in match.groupdict().items():
k = k.lower() # metadata must be lowercase
- if k not in metadata:
+ if v is not None and k not in metadata:
if process:
v = process(k, v)
metadata[k] = v
diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py
index ac0f81dc..0d22bf44 100644
--- a/pelican/tests/test_readers.py
+++ b/pelican/tests/test_readers.py
@@ -166,6 +166,25 @@ class RstReaderTest(ReaderTest):
}
self.assertDictHasSubset(page.metadata, expected)
+ def test_article_with_optional_filename_metadata(self):
+ page = self.read_file(
+ path='2012-11-29_rst_w_filename_meta#foo-bar.rst',
+ FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ expected = {
+ 'date': SafeDatetime(2012, 11, 29),
+ 'reader': 'rst',
+ }
+ self.assertDictHasSubset(page.metadata, expected)
+
+ page = self.read_file(
+ path='article.rst',
+ FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ expected = {
+ 'reader': 'rst',
+ }
+ self.assertDictHasSubset(page.metadata, expected)
+ self.assertNotIn('date', page.metadata, 'Date should not be set.')
+
def test_article_metadata_key_lowercase(self):
# Keys of metadata should be lowercase.
reader = readers.RstReader(settings=get_settings())
@@ -561,6 +580,25 @@ class MdReaderTest(ReaderTest):
}
self.assertDictHasSubset(page.metadata, expected)
+ def test_article_with_optional_filename_metadata(self):
+ page = self.read_file(
+ path='2012-11-30_md_w_filename_meta#foo-bar.md',
+ FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ expected = {
+ 'date': SafeDatetime(2012, 11, 30),
+ 'reader': 'markdown',
+ }
+ self.assertDictHasSubset(page.metadata, expected)
+
+ page = self.read_file(
+ path='empty.md',
+ FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ expected = {
+ 'reader': 'markdown',
+ }
+ self.assertDictHasSubset(page.metadata, expected)
+ self.assertNotIn('date', page.metadata, 'Date should not be set.')
+
def test_duplicate_tags_or_authors_are_removed(self):
reader = readers.MarkdownReader(settings=get_settings())
content, metadata = reader.read(
From 89b28fd36b38e61fcea6d4057b47d173bfd19dc0 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Mon, 27 Mar 2017 16:06:07 +0200
Subject: [PATCH 028/867] Fix warnings originating from bad regexes
Starting with python 3.6 warnings are issued for invalid escape
sequences in regular expressions. This commit corrects all
DeprecationWarning's via properly declaring the offending
regular expressions as raw strings.
Resolves #2095.
---
docs/settings.rst | 4 ++--
pelican/__init__.py | 4 ++--
pelican/readers.py | 6 +++---
pelican/rstdirectives.py | 2 +-
pelican/settings.py | 2 +-
pelican/utils.py | 4 ++--
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index aef9f674..4d09ec4c 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -712,7 +712,7 @@ Metadata
The default metadata you want to use for all articles and pages.
-.. data:: FILENAME_METADATA = '(?P\d{4}-\d{2}-\d{2}).*'
+.. data:: FILENAME_METADATA = r'(?P\d{4}-\d{2}-\d{2}).*'
The regexp that will be used to extract any metadata from the filename. All
named groups that are matched will be set in the metadata object. The
@@ -720,7 +720,7 @@ Metadata
For example, to extract both the date and the slug::
- FILENAME_METADATA = '(?P\d{4}-\d{2}-\d{2})_(?P.*)'
+ FILENAME_METADATA = r'(?P\d{4}-\d{2}-\d{2})_(?P.*)'
See also ``SLUGIFY_SOURCE``.
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 69332706..fa636bb0 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -111,10 +111,10 @@ class Pelican(object):
structure = self.settings['ARTICLE_PERMALINK_STRUCTURE']
# Convert %(variable) into {variable}.
- structure = re.sub('%\((\w+)\)s', '{\g<1>}', structure)
+ structure = re.sub(r'%\((\w+)\)s', '{\g<1>}', structure)
# Convert %x into {date:%x} for strftime
- structure = re.sub('(%[A-z])', '{date:\g<1>}', structure)
+ structure = re.sub(r'(%[A-z])', '{date:\g<1>}', structure)
# Strip a / prefix
structure = re.sub('^/', '', structure)
diff --git a/pelican/readers.py b/pelican/readers.py
index a620a1e8..afc0c4bf 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -650,9 +650,9 @@ def parse_path_metadata(source_path, settings=None, process=None):
... settings=settings,
... process=reader.process_metadata)
>>> pprint.pprint(metadata) # doctest: +ELLIPSIS
- {'category': ,
- 'date': SafeDatetime(2013, 1, 1, 0, 0),
- 'slug': 'my-slug'}
+ ... {'category': ,
+ ... 'date': SafeDatetime(2013, 1, 1, 0, 0),
+ ... 'slug': 'my-slug'}
"""
metadata = {}
dirname, basename = os.path.split(source_path)
diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py
index def67cc7..b4f44aa1 100644
--- a/pelican/rstdirectives.py
+++ b/pelican/rstdirectives.py
@@ -75,7 +75,7 @@ directives.register_directive('code-block', Pygments)
directives.register_directive('sourcecode', Pygments)
-_abbr_re = re.compile('\((.*)\)$', re.DOTALL)
+_abbr_re = re.compile(r'\((.*)\)$', re.DOTALL)
class abbreviation(nodes.Inline, nodes.TextElement):
diff --git a/pelican/settings.py b/pelican/settings.py
index 1b0bd67d..99840119 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -121,7 +121,7 @@ DEFAULT_CONFIG = {
'DEFAULT_PAGINATION': False,
'DEFAULT_ORPHANS': 0,
'DEFAULT_METADATA': {},
- 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2}).*',
+ 'FILENAME_METADATA': r'(?P\d{4}-\d{2}-\d{2}).*',
'PATH_METADATA': '',
'EXTRA_PATH_METADATA': {},
'DEFAULT_STATUS': 'published',
diff --git a/pelican/utils.py b/pelican/utils.py
index a521d3a8..ef9da23b 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -305,8 +305,8 @@ def slugify(value, substitutions=()):
replace = replace and not skip
if replace:
- value = re.sub('[^\w\s-]', '', value).strip()
- value = re.sub('[-\s]+', '-', value)
+ value = re.sub(r'[^\w\s-]', '', value).strip()
+ value = re.sub(r'[-\s]+', '-', value)
else:
value = value.strip()
From 623eb0a4c0ff817bf656f15d513628af1d143668 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Wed, 29 Mar 2017 10:19:47 +0200
Subject: [PATCH 029/867] Fix more python 3.6 regex DeprecationWarning's
---
pelican/__init__.py | 4 ++--
pelican/readers.py | 4 ++--
pelican/tests/test_contents.py | 2 +-
pelican/tests/test_readers.py | 40 ++++++++++++++++-----------------
pelican/tools/pelican_import.py | 2 +-
5 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index fa636bb0..70013804 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -111,10 +111,10 @@ class Pelican(object):
structure = self.settings['ARTICLE_PERMALINK_STRUCTURE']
# Convert %(variable) into {variable}.
- structure = re.sub(r'%\((\w+)\)s', '{\g<1>}', structure)
+ structure = re.sub(r'%\((\w+)\)s', r'{\g<1>}', structure)
# Convert %x into {date:%x} for strftime
- structure = re.sub(r'(%[A-z])', '{date:\g<1>}', structure)
+ structure = re.sub(r'(%[A-z])', r'{date:\g<1>}', structure)
# Strip a / prefix
structure = re.sub('^/', '', structure)
diff --git a/pelican/readers.py b/pelican/readers.py
index afc0c4bf..fda60596 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -640,9 +640,9 @@ def parse_path_metadata(source_path, settings=None, process=None):
>>> import pprint
>>> settings = {
- ... 'FILENAME_METADATA': '(?P[^.]*).*',
+ ... 'FILENAME_METADATA': r'(?P[^.]*).*',
... 'PATH_METADATA':
- ... '(?P[^/]*)/(?P\d{4}-\d{2}-\d{2})/.*',
+ ... r'(?P[^/]*)/(?P\d{4}-\d{2}-\d{2})/.*',
... }
>>> reader = BaseReader(settings=settings)
>>> metadata = parse_path_metadata(
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index b4146150..ac59f5a4 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -105,7 +105,7 @@ class TestPage(LoggedTestCase):
self.assertEqual(page._get_summary(), TEST_SUMMARY)
self.assertLogCountEqual(
count=1,
- msg="_get_summary\(\) has been deprecated since 3\.6\.4\. "
+ msg=r"_get_summary\(\) has been deprecated since 3\.6\.4\. "
"Use the summary decorator instead",
level=logging.WARNING)
diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py
index 0d22bf44..4db4938e 100644
--- a/pelican/tests/test_readers.py
+++ b/pelican/tests/test_readers.py
@@ -67,7 +67,7 @@ class TestAssertDictHasSubset(ReaderTest):
six.assertRaisesRegex(
self,
AssertionError,
- 'Expected.*key-c.*to have value.*val-c.*but was not in Dict',
+ r'Expected.*key-c.*to have value.*val-c.*but was not in Dict',
self.assertDictHasSubset,
self.dictionary,
{'key-c': 'val-c'})
@@ -76,7 +76,7 @@ class TestAssertDictHasSubset(ReaderTest):
six.assertRaisesRegex(
self,
AssertionError,
- 'Expected .*key-a.* to have value .*val-b.* but was .*val-a.*',
+ r'Expected .*key-a.* to have value .*val-b.* but was .*val-a.*',
self.assertDictHasSubset,
self.dictionary,
{'key-a': 'val-b'})
@@ -139,7 +139,7 @@ class RstReaderTest(ReaderTest):
page = self.read_file(
path='2012-11-29_rst_w_filename_meta#foo-bar.rst',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2}).*')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2}).*')
expected = {
'category': 'yeah',
'author': 'Alexis Métaireau',
@@ -152,9 +152,9 @@ class RstReaderTest(ReaderTest):
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.*)'
- '#(?P.*)-(?P.*)'))
+ r'(?P\d{4}-\d{2}-\d{2})'
+ r'_(?P.*)'
+ r'#(?P.*)-(?P.*)'))
expected = {
'category': 'yeah',
'author': 'Alexis Métaireau',
@@ -169,7 +169,7 @@ class RstReaderTest(ReaderTest):
def test_article_with_optional_filename_metadata(self):
page = self.read_file(
path='2012-11-29_rst_w_filename_meta#foo-bar.rst',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?')
expected = {
'date': SafeDatetime(2012, 11, 29),
'reader': 'rst',
@@ -178,7 +178,7 @@ class RstReaderTest(ReaderTest):
page = self.read_file(
path='article.rst',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?')
expected = {
'reader': 'rst',
}
@@ -200,9 +200,9 @@ class RstReaderTest(ReaderTest):
page_metadata = self.read_file(
path=input_with_metadata,
FILENAME_METADATA=(
- '(?P\d{4}-\d{2}-\d{2})'
- '_(?P.*)'
- '#(?P.*)-(?P.*)'
+ r'(?P\d{4}-\d{2}-\d{2})'
+ r'_(?P.*)'
+ r'#(?P.*)-(?P.*)'
),
EXTRA_PATH_METADATA={
input_with_metadata: {
@@ -250,9 +250,9 @@ class RstReaderTest(ReaderTest):
page = self.read_file(
path=input_file_path,
FILENAME_METADATA=(
- '(?P\d{4}-\d{2}-\d{2})'
- '_(?P.*)'
- '#(?P.*)-(?P.*)'
+ r'(?P\d{4}-\d{2}-\d{2})'
+ r'_(?P.*)'
+ r'#(?P.*)-(?P.*)'
),
EXTRA_PATH_METADATA={
input_file_path: {
@@ -557,7 +557,7 @@ class MdReaderTest(ReaderTest):
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2}).*')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2}).*')
expected = {
'category': 'yeah',
'author': 'Alexis Métaireau',
@@ -568,9 +568,9 @@ class MdReaderTest(ReaderTest):
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
FILENAME_METADATA=(
- '(?P\d{4}-\d{2}-\d{2})'
- '_(?P.*)'
- '#(?P.*)-(?P.*)'))
+ r'(?P\d{4}-\d{2}-\d{2})'
+ r'_(?P.*)'
+ r'#(?P.*)-(?P.*)'))
expected = {
'category': 'yeah',
'author': 'Alexis Métaireau',
@@ -583,7 +583,7 @@ class MdReaderTest(ReaderTest):
def test_article_with_optional_filename_metadata(self):
page = self.read_file(
path='2012-11-30_md_w_filename_meta#foo-bar.md',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?')
expected = {
'date': SafeDatetime(2012, 11, 30),
'reader': 'markdown',
@@ -592,7 +592,7 @@ class MdReaderTest(ReaderTest):
page = self.read_file(
path='empty.md',
- FILENAME_METADATA='(?P\d{4}-\d{2}-\d{2})?')
+ FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?')
expected = {
'reader': 'markdown',
}
diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py
index 204ab7b0..ef4a20e0 100755
--- a/pelican/tools/pelican_import.py
+++ b/pelican/tools/pelican_import.py
@@ -88,7 +88,7 @@ def decode_wp_content(content, br=True):
content = re.sub(r']*)>', "", content)
content = content.replace('
', '')
content = re.sub(r'\s*(?' + allblocks + '[^>]*>)', "\\1", content)
- content = re.sub(r'(?' + allblocks + '[^>]*>)\s*
', "\\1", content)
+ content = re.sub(r'(?' + allblocks + r'[^>]*>)\s*', "\\1", content)
if br:
def _preserve_newline(match):
return match.group(0).replace("\n", "")
From f49037e0ca1f1786b123fe25ca35a978653f410d Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Wed, 29 Mar 2017 10:45:41 +0200
Subject: [PATCH 030/867] Fixup 89b28fd
We need to mark the whole doctest string as raw as it contains
regular expressions.
---
pelican/readers.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index fda60596..46055962 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -636,7 +636,7 @@ def path_metadata(full_path, source_path, settings=None):
def parse_path_metadata(source_path, settings=None, process=None):
- """Extract a metadata dictionary from a file's path
+ r"""Extract a metadata dictionary from a file's path
>>> import pprint
>>> settings = {
@@ -650,9 +650,9 @@ def parse_path_metadata(source_path, settings=None, process=None):
... settings=settings,
... process=reader.process_metadata)
>>> pprint.pprint(metadata) # doctest: +ELLIPSIS
- ... {'category': ,
- ... 'date': SafeDatetime(2013, 1, 1, 0, 0),
- ... 'slug': 'my-slug'}
+ {'category': ,
+ 'date': SafeDatetime(2013, 1, 1, 0, 0),
+ 'slug': 'my-slug'}
"""
metadata = {}
dirname, basename = os.path.split(source_path)
From ad38d602c710355fe5051f7857793fcb036d8bad Mon Sep 17 00:00:00 2001
From: jvoisin
Date: Wed, 8 Mar 2017 14:05:57 +0100
Subject: [PATCH 031/867] Improve the regexp used in _update_content
a html tag always starts with <[a-z], < [a-z] is invalid
a space can be found after the = in href='bleh'
This function is taking 10% of the compilation time, with caching enabled,
maybe it's worth optimising the regexp a bit more, I don't know.
---
pelican/contents.py | 4 ++--
pelican/tests/test_contents.py | 21 +++++++++++++++++++++
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/pelican/contents.py b/pelican/contents.py
index 3187f328..3d1128c9 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -219,8 +219,8 @@ class Content(object):
instrasite_link_regex = self.settings['INTRASITE_LINK_REGEX']
regex = r"""
- (?P<\s*[^\>]* # match tag with all url-value attributes
- (?:href|src|poster|data|cite|formaction|action)\s*=)
+ (?P<[^\>]+ # match tag with all url-value attributes
+ (?:href|src|poster|data|cite|formaction|action)\s*=\s*)
(?P["\']) # require value to be quoted
(?P{0}(?P.*?)) # the url value
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index b4146150..11fa958a 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -771,3 +771,24 @@ class TestStatic(LoggedTestCase):
count=1,
msg="Unable to find 'foo', skipping url replacement.",
level=logging.WARNING)
+
+ def test_index_link_syntax_with_spaces(self):
+ """{index} link syntax triggers url replacement
+ with spaces around the equal sign."""
+
+ html = 'link'
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ expected_html = ('link')
+ self.assertEqual(content, expected_html)
From 77faffa6f75e69c7028f798b80e7a0bbe68ad794 Mon Sep 17 00:00:00 2001
From: Jorge Maldonado Ventura
Date: Sun, 16 Apr 2017 13:45:45 +0200
Subject: [PATCH 032/867] Add Python 3.6 classifier to setup.py
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index 3645dc04..f3a0ebcb 100755
--- a/setup.py
+++ b/setup.py
@@ -63,6 +63,7 @@ setup(
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Software Development :: Libraries :: Python Modules',
],
From 651ccbcddfbaef74de2e5a2b247686e525ebe017 Mon Sep 17 00:00:00 2001
From: MinchinWeb
Date: Thu, 20 Apr 2017 13:40:19 -0600
Subject: [PATCH 033/867] Pelican trove classifier now on PyPI
https://github.com/pypa/warehouse/issues/1650
(now sorted alphabetically)
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index f3a0ebcb..c26f1e71 100755
--- a/setup.py
+++ b/setup.py
@@ -55,6 +55,7 @@ setup(
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
+ 'Framework :: Pelican',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
From 24c41195a669070d0970ad5d62f3fa5887902900 Mon Sep 17 00:00:00 2001
From: MinchinWeb
Date: Wed, 26 Apr 2017 10:34:47 -0600
Subject: [PATCH 034/867] Use 4-space indentation
---
setup.py | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/setup.py b/setup.py
index c26f1e71..be038971 100755
--- a/setup.py
+++ b/setup.py
@@ -53,20 +53,20 @@ setup(
install_requires=requires,
entry_points=entry_points,
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Framework :: Pelican',
- 'License :: OSI Approved :: GNU Affero General Public License v3',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Topic :: Internet :: WWW/HTTP',
- 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Framework :: Pelican',
+ 'License :: OSI Approved :: GNU Affero General Public License v3',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
],
test_suite='pelican.tests',
)
From 32c154be95cd71ea96284b1dbee1d3fa04a9adb1 Mon Sep 17 00:00:00 2001
From: Brandon B
Date: Fri, 28 Apr 2017 19:18:44 -0700
Subject: [PATCH 035/867] Update tips.rst
I found that one of the easiest ways to publish to GitHub User Pages is to make Pelican a subdirectory within the ``.github.io`` project, then generate the output files in the root level of ``.github.io`` and push the entire project to GitHub.
---
docs/tips.rst | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/tips.rst b/docs/tips.rst
index 50160380..bf0b3599 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -98,6 +98,18 @@ by the ``ghp-import`` command) to the ``elemoine.github.io`` repository's
To publish your Pelican site as User Pages, feel free to adjust the
``github`` target of the Makefile.
+
+Another option for publishing to User Pages is to generate the output files in the root directory of the project.
+
+For example, your main project folder is ``.github.io`` and you can create the Pelican project in a subdirectory called ``Pelican``. Then from inside the ``Pelican`` folder you can run::
+
+ $ pelican content -o .. -s pelicanconf.py
+
+Now you can push the whole project ``.github.io`` to the master branch of your GitHub repository::
+
+ $ git push origin master
+
+(assuming origin is set to your remote repository).
Custom 404 Pages
----------------
From c68958e6b89459bbd0488208279d843a41cc5681 Mon Sep 17 00:00:00 2001
From: derwinlu
Date: Fri, 5 May 2017 16:02:48 +0200
Subject: [PATCH 036/867] Fixup ec5c77b25145f7c20fee24c6b85b478295dbc956
fix forgotten PAGES -> pages
---
pelican/themes/notmyidea/templates/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pelican/themes/notmyidea/templates/index.html b/pelican/themes/notmyidea/templates/index.html
index 6019987b..20ca7eee 100644
--- a/pelican/themes/notmyidea/templates/index.html
+++ b/pelican/themes/notmyidea/templates/index.html
@@ -51,7 +51,7 @@
{% else %}
Pages
- {% for page in PAGES %}
+ {% for page in pages %}
{{ page.title }}
{% endfor %}
From fdf355c377627d82026765872b8660086715a419 Mon Sep 17 00:00:00 2001
From: evilroot
Date: Thu, 8 Jun 2017 22:54:12 +0100
Subject: [PATCH 037/867] Fix rsync_upload in Makefile template
---
pelican/tools/templates/Makefile.in | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in
index 06880c44..5dc81baa 100644
--- a/pelican/tools/templates/Makefile.in
+++ b/pelican/tools/templates/Makefile.in
@@ -102,7 +102,7 @@ ssh_upload: publish
scp -P $$(SSH_PORT) -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR)
rsync_upload: publish
- rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --delete $(OUTPUTDIR)/ $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) --cvs-exclude
+ rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --cvs-exclude --delete $(OUTPUTDIR)/ $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)
dropbox_upload: publish
cp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR)
From 7336de45cbb5f60e934b65f823d0583b48a6c96b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?=
Date: Tue, 6 Jun 2017 20:34:56 +0200
Subject: [PATCH 038/867] Ability to override docutils HTML writer/translator.
The RstReader class can now use user-specified writer/translator classes
instead of the hardcoded ones from docutils. This allows for far easier
overriding of the default HTML output -- in the past one would need to
override the internal _parse_metadata() and _get_publisher() functions.
With hypothetical Html5Writer and Html5FieldBodyTranslator classes,
based for example on docutils.writers.html5_polyglot.Writer and
docutils.writers.html5_polyglot.HTMLTranslator, a plugin that overrides
the default behavior would now look just like this:
# (definition of Writer / Translator classes omitted)
class Html5RstReader(RstReader):
writer_class = Html5Writer
field_body_translator_class = Html5FieldBodyTranslator
def add_reader(readers):
readers.reader_classes['rst'] = Html5RstReader
def register():
pelican.signals.readers_init.connect(add_reader)
---
pelican/readers.py | 36 ++++++++++++++++++++++++++++++------
1 file changed, 30 insertions(+), 6 deletions(-)
diff --git a/pelican/readers.py b/pelican/readers.py
index 46055962..61126c9c 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -9,7 +9,7 @@ from collections import OrderedDict
import docutils
import docutils.core
import docutils.io
-from docutils.writers.html4css1 import HTMLTranslator
+from docutils.writers.html4css1 import HTMLTranslator, Writer
import six
from six.moves.html_parser import HTMLParser
@@ -135,12 +135,19 @@ class _FieldBodyTranslator(HTMLTranslator):
pass
-def render_node_to_html(document, node):
- visitor = _FieldBodyTranslator(document)
+def render_node_to_html(document, node, field_body_translator_class):
+ visitor = field_body_translator_class(document)
node.walkabout(visitor)
return visitor.astext()
+class PelicanHTMLWriter(Writer):
+
+ def __init__(self):
+ Writer.__init__(self)
+ self.translator_class = PelicanHTMLTranslator
+
+
class PelicanHTMLTranslator(HTMLTranslator):
def visit_abbreviation(self, node):
@@ -160,11 +167,26 @@ class PelicanHTMLTranslator(HTMLTranslator):
class RstReader(BaseReader):
- """Reader for reStructuredText files"""
+ """Reader for reStructuredText files
+
+ By default the output HTML is written using
+ docutils.writers.html4css1.Writer and translated using a subclass of
+ docutils.writers.html4css1.HTMLTranslator. If you want to override it with
+ your own writer/translator (e.g. a HTML5-based one), pass your classes to
+ these two attributes. Look in the source code for details.
+
+ writer_class Used for writing contents
+ field_body_translator_class Used for translating metadata such
+ as article summary
+
+ """
enabled = bool(docutils)
file_extensions = ['rst']
+ writer_class = PelicanHTMLWriter
+ field_body_translator_class = _FieldBodyTranslator
+
class FileInput(docutils.io.FileInput):
"""Patch docutils.io.FileInput to remove "U" mode in py3.
@@ -192,7 +214,9 @@ class RstReader(BaseReader):
name_elem, body_elem = element.children
name = name_elem.astext()
if name in formatted_fields:
- value = render_node_to_html(document, body_elem)
+ value = render_node_to_html(
+ document, body_elem,
+ self.field_body_translator_class)
else:
value = body_elem.astext()
elif element.tagname == 'authors': # author list
@@ -217,10 +241,10 @@ class RstReader(BaseReader):
extra_params.update(user_params)
pub = docutils.core.Publisher(
+ writer=self.writer_class(),
source_class=self.FileInput,
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
- pub.writer.translator_class = PelicanHTMLTranslator
pub.process_programmatic_settings(None, extra_params, None)
pub.set_source(source_path=source_path)
pub.publish(enable_exit_status=True)
From 43ec3c4f7dd188bfa5a9ad430618a7b1fe570ffd Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Wed, 19 Jul 2017 13:29:04 -0700
Subject: [PATCH 039/867] Fix copyright year to date of first publication
---
docs/conf.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/conf.py b/docs/conf.py
index 81a06e3a..3748fd75 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -17,7 +17,7 @@ extensions = ['sphinx.ext.autodoc',
source_suffix = '.rst'
master_doc = 'index'
project = 'Pelican'
-copyright = '2015, Alexis Metaireau and contributors'
+copyright = '2010, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
release = __version__
version = '.'.join(release.split('.')[:1])
From 1c96d8c933aaf8770b83a228d22936e36e40da3d Mon Sep 17 00:00:00 2001
From: Jonas Lundholm Bertelsen
Date: Mon, 24 Jul 2017 12:25:21 +0200
Subject: [PATCH 040/867] Correct import of socketserver on Python 3
---
pelican/tools/templates/fabfile.py.in | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/pelican/tools/templates/fabfile.py.in b/pelican/tools/templates/fabfile.py.in
index 18131d5f..d3c50a83 100644
--- a/pelican/tools/templates/fabfile.py.in
+++ b/pelican/tools/templates/fabfile.py.in
@@ -3,7 +3,10 @@ import fabric.contrib.project as project
import os
import shutil
import sys
-import SocketServer
+try:
+ import socketserver
+except ImportError:
+ import SocketServer as socketserver
from pelican.server import ComplexHTTPRequestHandler
@@ -48,7 +51,7 @@ def serve():
"""Serve site at http://localhost:8000/"""
os.chdir(env.deploy_path)
- class AddressReuseTCPServer(SocketServer.TCPServer):
+ class AddressReuseTCPServer(socketserver.TCPServer):
allow_reuse_address = True
server = AddressReuseTCPServer(('', PORT), ComplexHTTPRequestHandler)
From 089b46b7eb86cb55491d3c8e8bccb956b6dceeff Mon Sep 17 00:00:00 2001
From: winlu
Date: Mon, 24 Jul 2017 19:01:14 +0200
Subject: [PATCH 041/867] Consolidate validation of content (#2128)
* Consolidate validation of content
Previously we validated content outside of the content class via
calls to `is_valid_content` and some additional checks in page /
article generators (valid status).
This commit moves those checks all into content.valid() resulting
in a cleaner code structure.
This allows us to restructure how generators interact with content,
removing several old bugs in pelican (#1748, #1356, #2098).
- move verification function into content class
- move generator verifying content to contents class
- remove unused quote class
- remove draft class (no more rereading drafts)
- move auto draft status setter into Article.__init__
- add now parsing draft to basic test output
- remove problematic DEFAULT_STATUS setting
- add setter/getter for content.status
removes need for lower() calls when verifying status
* expand c4b184fa32f73a737ff9bade440ce0f7914dc350
Mostly implement feedback by @iKevinY.
* rename content.valid to content.is_valid
* rename valid_* functions to has_valid_*
* update tests and function calls in code accordingly
---
pelican/contents.py | 108 +++++++++++-------
pelican/generators.py | 54 +++------
pelican/settings.py | 1 -
.../output/basic/drafts/a-draft-article.html | 69 +++++++++++
pelican/tests/test_contents.py | 16 +--
5 files changed, 159 insertions(+), 89 deletions(-)
create mode 100644 pelican/tests/output/basic/drafts/a-draft-article.html
diff --git a/pelican/contents.py b/pelican/contents.py
index 3d1128c9..15770fc8 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -138,14 +138,7 @@ class Content(object):
# manage status
if not hasattr(self, 'status'):
- self.status = settings['DEFAULT_STATUS']
- if not settings['WITH_FUTURE_DATES'] and hasattr(self, 'date'):
- if self.date.tzinfo is None:
- now = SafeDatetime.now()
- else:
- now = SafeDatetime.utcnow().replace(tzinfo=pytz.utc)
- if self.date > now:
- self.status = 'draft'
+ self.status = getattr(self, 'default_status', None)
# store the summary metadata if it is set
if 'summary' in metadata:
@@ -156,13 +149,17 @@ class Content(object):
def __str__(self):
return self.source_path or repr(self)
- def check_properties(self):
+ def _has_valid_mandatory_properties(self):
"""Test mandatory properties are set."""
for prop in self.mandatory_properties:
if not hasattr(self, prop):
- raise NameError(prop)
+ logger.error(
+ "Skipping %s: could not find information about '%s'",
+ self, prop)
+ return False
+ return True
- def valid_save_as(self):
+ def _has_valid_save_as(self):
"""Return true if save_as doesn't write outside output path, false
otherwise."""
try:
@@ -174,10 +171,35 @@ class Content(object):
try:
sanitised_join(output_path, self.save_as)
except RuntimeError: # outside output_dir
+ logger.error(
+ "Skipping %s: file %r would be written outside output path",
+ self,
+ self.save_as,
+ )
return False
return True
+ def _has_valid_status(self):
+ if hasattr(self, 'allowed_statuses'):
+ if self.status not in self.allowed_statuses:
+ logger.error(
+ "Unknown status '%s' for file %s, skipping it.",
+ self.status,
+ self
+ )
+ return False
+
+ # if undefined we allow all
+ return True
+
+ def is_valid(self):
+ """Validate Content"""
+ # Use all() to not short circuit and get results of all validations
+ return all([self._has_valid_mandatory_properties(),
+ self._has_valid_save_as(),
+ self._has_valid_status()])
+
@property
def url_format(self):
"""Returns the URL, formatted with the proper values"""
@@ -194,8 +216,10 @@ class Content(object):
})
return metadata
- def _expand_settings(self, key):
- fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()
+ def _expand_settings(self, key, klass=None):
+ if not klass:
+ klass = self.__class__.__name__
+ fq_key = ('%s_%s' % (klass, key)).upper()
return self.settings[fq_key].format(**self.url_format)
def get_url_setting(self, key):
@@ -338,6 +362,15 @@ class Content(object):
"""Dummy function"""
pass
+ @property
+ def status(self):
+ return self._status
+
+ @status.setter
+ def status(self, value):
+ # TODO maybe typecheck
+ self._status = value.lower()
+
@property
def url(self):
return self.get_url_setting('url')
@@ -383,21 +416,36 @@ class Content(object):
class Page(Content):
mandatory_properties = ('title',)
+ allowed_statuses = ('published', 'hidden')
+ default_status = 'published'
default_template = 'page'
-class Article(Page):
+class Article(Content):
mandatory_properties = ('title', 'date', 'category')
+ allowed_statuses = ('published', 'draft')
+ default_status = 'published'
default_template = 'article'
+ def __init__(self, *args, **kwargs):
+ super(Article, self).__init__(*args, **kwargs)
-class Draft(Page):
- mandatory_properties = ('title', 'category')
- default_template = 'article'
+ # handle WITH_FUTURE_DATES (designate article to draft based on date)
+ if not self.settings['WITH_FUTURE_DATES'] and hasattr(self, 'date'):
+ if self.date.tzinfo is None:
+ now = SafeDatetime.now()
+ else:
+ now = SafeDatetime.utcnow().replace(tzinfo=pytz.utc)
+ if self.date > now:
+ self.status = 'draft'
+ # if we are a draft and there is no date provided, set max datetime
+ if not hasattr(self, 'date') and self.status == 'draft':
+ self.date = SafeDatetime.max
-class Quote(Page):
- base_properties = ('author', 'date')
+ def _expand_settings(self, key):
+ klass = 'article' if self.status == 'published' else 'draft'
+ return super(Article, self)._expand_settings(key, klass)
@python_2_unicode_compatible
@@ -482,25 +530,3 @@ class Static(Page):
self.override_save_as = new_save_as
self.override_url = new_url
-
-
-def is_valid_content(content, f):
- try:
- content.check_properties()
- except NameError as e:
- logger.error(
- "Skipping %s: could not find information about '%s'",
- f, six.text_type(e))
- return False
-
- if not content.valid_save_as():
- logger.error(
- "Skipping %s: file %r would be written outside output path",
- f,
- content.save_as,
- )
- # Note: future code might want to use a result variable instead, to
- # allow showing multiple error messages at once.
- return False
-
- return True
diff --git a/pelican/generators.py b/pelican/generators.py
index f3590155..eb97c115 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -19,7 +19,7 @@ import six
from pelican import signals
from pelican.cache import FileStampDataCacher
-from pelican.contents import Article, Draft, Page, Static, is_valid_content
+from pelican.contents import Article, Page, Static
from pelican.readers import Readers
from pelican.utils import (DateFormatter, copy, mkdir_p, posixize_path,
process_translations, python_2_unicode_compatible)
@@ -509,12 +509,10 @@ class ArticlesGenerator(CachingGenerator):
for f in self.get_files(
self.settings['ARTICLE_PATHS'],
exclude=self.settings['ARTICLE_EXCLUDES']):
- 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
+ article = self.get_cached_data(f, None)
+ if article is None:
try:
- article_or_draft = self.readers.read_file(
+ article = self.readers.read_file(
base_path=self.path, path=f, content_class=Article,
context=self.context,
preread_signal=signals.article_generator_preread,
@@ -528,34 +526,17 @@ class ArticlesGenerator(CachingGenerator):
self._add_failed_source_path(f)
continue
- if not is_valid_content(article_or_draft, f):
+ if not article.is_valid():
self._add_failed_source_path(f)
continue
- if article_or_draft.status.lower() == "published":
- pass
- 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)
- else:
- logger.error(
- "Unknown status '%s' for file %s, skipping it.",
- article_or_draft.status, f)
- self._add_failed_source_path(f)
- continue
+ self.cache_data(f, article)
- self.cache_data(f, article_or_draft)
-
- if article_or_draft.status.lower() == "published":
- all_articles.append(article_or_draft)
- else:
- all_drafts.append(article_or_draft)
- self.add_source_path(article_or_draft)
+ if article.status == "published":
+ all_articles.append(article)
+ elif article.status == "draft":
+ all_drafts.append(article)
+ self.add_source_path(article)
self.articles, self.translations = process_translations(
all_articles,
@@ -634,22 +615,15 @@ class PagesGenerator(CachingGenerator):
self._add_failed_source_path(f)
continue
- if not is_valid_content(page, f):
- self._add_failed_source_path(f)
- continue
-
- if page.status.lower() not in ("published", "hidden"):
- logger.error(
- "Unknown status '%s' for file %s, skipping it.",
- page.status, f)
+ if not page.is_valid():
self._add_failed_source_path(f)
continue
self.cache_data(f, page)
- if page.status.lower() == "published":
+ if page.status == "published":
all_pages.append(page)
- elif page.status.lower() == "hidden":
+ elif page.status == "hidden":
hidden_pages.append(page)
self.add_source_path(page)
diff --git a/pelican/settings.py b/pelican/settings.py
index 831e1851..d1249a24 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -126,7 +126,6 @@ DEFAULT_CONFIG = {
'FILENAME_METADATA': r'(?P\d{4}-\d{2}-\d{2}).*',
'PATH_METADATA': '',
'EXTRA_PATH_METADATA': {},
- 'DEFAULT_STATUS': 'published',
'ARTICLE_PERMALINK_STRUCTURE': '',
'TYPOGRIFY': False,
'TYPOGRIFY_IGNORE_TAGS': [],
diff --git a/pelican/tests/output/basic/drafts/a-draft-article.html b/pelican/tests/output/basic/drafts/a-draft-article.html
new file mode 100644
index 00000000..64d56ff9
--- /dev/null
+++ b/pelican/tests/output/basic/drafts/a-draft-article.html
@@ -0,0 +1,69 @@
+
+
+
+
+ A draft article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 56928b81..d028c7a1 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -69,11 +69,13 @@ class TestPage(LoggedTestCase):
def test_mandatory_properties(self):
# If the title is not set, must throw an exception.
page = Page('content')
- with self.assertRaises(NameError):
- page.check_properties()
-
+ self.assertFalse(page._has_valid_mandatory_properties())
+ self.assertLogCountEqual(
+ count=1,
+ msg="Skipping .*: could not find information about 'title'",
+ level=logging.ERROR)
page = Page('content', metadata={'title': 'foobar'})
- page.check_properties()
+ self.assertTrue(page._has_valid_mandatory_properties())
def test_summary_from_metadata(self):
# If a :summary: metadata is given, it should be used
@@ -503,7 +505,7 @@ class TestArticle(TestPage):
article_kwargs['metadata']['slug'] = '../foo'
article_kwargs['settings'] = settings
article = Article(**article_kwargs)
- self.assertFalse(article.valid_save_as())
+ self.assertFalse(article._has_valid_save_as())
def test_valid_save_as_detects_breakout_to_root(self):
settings = get_settings()
@@ -511,7 +513,7 @@ class TestArticle(TestPage):
article_kwargs['metadata']['slug'] = '/foo'
article_kwargs['settings'] = settings
article = Article(**article_kwargs)
- self.assertFalse(article.valid_save_as())
+ self.assertFalse(article._has_valid_save_as())
def test_valid_save_as_passes_valid(self):
settings = get_settings()
@@ -519,7 +521,7 @@ class TestArticle(TestPage):
article_kwargs['metadata']['slug'] = 'foo'
article_kwargs['settings'] = settings
article = Article(**article_kwargs)
- self.assertTrue(article.valid_save_as())
+ self.assertTrue(article._has_valid_save_as())
class TestStatic(LoggedTestCase):
From 68724e9682f91763104811ff72e092f07d9ad6b5 Mon Sep 17 00:00:00 2001
From: Justin Mayer
Date: Tue, 25 Jul 2017 09:00:41 -0700
Subject: [PATCH 042/867] Encourage use of https:// in SITEURL
Refs #2183 #2186
---
docs/settings.rst | 8 +++++---
pelican/tools/pelican_quickstart.py | 2 +-
pelican/tools/templates/publishconf.py.in | 1 +
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/docs/settings.rst b/docs/settings.rst
index 462f9d36..e5ce19f5 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -203,10 +203,12 @@ Basic settings
.. data:: SITEURL
- Base URL of your website. Not defined by default, so it is best to specify
+ Base URL of your web site. Not defined by default, so it is best to specify
your SITEURL; if you do not, feeds will not be generated with
- properly-formed URLs. You should include ``http://`` and your domain, with
- no trailing slash at the end. Example: ``SITEURL = 'http://mydomain.com'``
+ properly-formed URLs. If your site is available via HTTPS, this setting
+ should begin with ``https://`` — otherwise use ``http://``. Then append your
+ domain, with no trailing slash at the end.
+ Example: ``SITEURL = 'https://example.com'``
.. data:: STATIC_PATHS = ['images']
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
index ae0e8c9d..eb63ed1f 100755
--- a/pelican/tools/pelican_quickstart.py
+++ b/pelican/tools/pelican_quickstart.py
@@ -246,7 +246,7 @@ needed by Pelican.
CONF['lang'] = ask('What will be the default language of this web site?',
str_compat, args.lang or CONF['lang'], 2)
- if ask('Do you want to specify a URL prefix? e.g., http://example.com ',
+ if ask('Do you want to specify a URL prefix? e.g., https://example.com ',
answer=bool, default=True):
CONF['siteurl'] = ask('What is your URL prefix? (see '
'above example; no trailing slash)',
diff --git a/pelican/tools/templates/publishconf.py.in b/pelican/tools/templates/publishconf.py.in
index d1ed994d..473490a9 100755
--- a/pelican/tools/templates/publishconf.py.in
+++ b/pelican/tools/templates/publishconf.py.in
@@ -10,6 +10,7 @@ import sys
sys.path.append(os.curdir)
from pelicanconf import *
+# If your site is available via HTTPS, make sure SITEURL begins with https://
SITEURL = '$siteurl'
RELATIVE_URLS = False
From 9495a6c3df8d0417568a78fb03f6c0eccc7adf72 Mon Sep 17 00:00:00 2001
From: Jorge Maldonado Ventura
Date: Sun, 30 Jul 2017 17:41:03 +0200
Subject: [PATCH 043/867] Remove trailing whitespaces from notmyidea
---
pelican/themes/notmyidea/static/css/main.css | 64 +++++++++----------
.../themes/notmyidea/static/css/typogrify.css | 2 +-
2 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/pelican/themes/notmyidea/static/css/main.css b/pelican/themes/notmyidea/static/css/main.css
index 03a77e69..8f3deef0 100644
--- a/pelican/themes/notmyidea/static/css/main.css
+++ b/pelican/themes/notmyidea/static/css/main.css
@@ -43,7 +43,7 @@ h1, h2, h3, h4, h5, h6 {
}
h3, h4, h5, h6 { margin-top: .8em; }
-
+
hr { border: 2px solid #EEEEEE; }
/* Anchors */
@@ -64,7 +64,7 @@ a:hover, a:active {
h1 a:hover {
background-color: inherit
}
-
+
/* Paragraphs */
div.line-block,
p { margin-top: 1em;
@@ -124,7 +124,7 @@ div.note {
/* Tables */
table {margin: .5em auto 1.5em auto; width: 98%;}
-
+
/* Thead */
thead th {padding: .5em .4em; text-align: left;}
thead td {}
@@ -132,14 +132,14 @@ table {margin: .5em auto 1.5em auto; width: 98%;}
/* Tbody */
tbody td {padding: .5em .4em;}
tbody th {}
-
+
tbody .alt td {}
tbody .alt th {}
-
+
/* Tfoot */
tfoot th {}
tfoot td {}
-
+
/* HTML5 tags */
header, section, footer,
aside, nav, article, figure {
@@ -173,9 +173,9 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
color: #C74350;
text-shadow: none;
}
-
+
#banner h1 strong {font-size: 0.36em; font-weight: normal;}
-
+
/* Main Nav */
#banner nav {
background: #000305;
@@ -186,15 +186,15 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
padding: 0;
text-align: center;
width: 800px;
-
+
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
-
+
#banner nav ul {list-style: none; margin: 0 auto; width: 800px;}
#banner nav li {float: left; display: inline; margin: 0;}
-
+
#banner nav a:link, #banner nav a:visited {
color: #fff;
display: inline-block;
@@ -208,12 +208,12 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
color: #fff;
text-shadow: none !important;
}
-
+
#banner nav li:first-child a {
border-top-left-radius: 5px;
-moz-border-radius-topleft: 5px;
-webkit-border-top-left-radius: 5px;
-
+
border-bottom-left-radius: 5px;
-moz-border-radius-bottomleft: 5px;
-webkit-border-bottom-left-radius: 5px;
@@ -228,7 +228,7 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
overflow: hidden;
padding: 20px;
width: 760px;
-
+
border-radius: 10px;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
@@ -257,7 +257,7 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
overflow: hidden;
padding: 20px 20px;
width: 760px;
-
+
border-radius: 10px;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
@@ -292,21 +292,21 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
float: left;
width: 615px;
}
-
+
#extras .blogroll li {float: left; margin: 0 20px 0 0; width: 185px;}
-
+
/* Social */
#extras .social {
float: right;
width: 175px;
}
-
+
#extras div[class='social'] a {
background-repeat: no-repeat;
background-position: 3px 6px;
padding-left: 25px;
}
-
+
/* Icons */
.social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');}
.social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');}
@@ -346,7 +346,7 @@ img.left, figure.left {float: left; margin: 0 2em 2em 0;}
padding: 20px;
text-align: left;
width: 760px;
-
+
border-radius: 10px;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
@@ -390,7 +390,7 @@ li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
/* Blog Index */
#posts-list {list-style: none; margin: 0;}
#posts-list .hentry {padding-left: 10px; position: relative;}
-
+
#posts-list footer {
left: 10px;
position: relative;
@@ -398,7 +398,7 @@ li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
top: 0.5em;
width: 190px;
}
-
+
/* About the Author */
#about-author {
background: #f9f9f9;
@@ -406,21 +406,21 @@ li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
font-style: normal;
margin: 2em 0;
padding: 10px 20px 15px 20px;
-
+
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
-
+
#about-author strong {
color: #C64350;
clear: both;
display: block;
font-size: 1.429em;
}
-
+
#about-author .photo {border: 1px solid #ddd; float: left; margin: 5px 1em 0 0;}
-
+
/* Comments */
#comments-list {list-style: none; margin: 0 1em;}
#comments-list blockquote {
@@ -429,24 +429,24 @@ li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
font-style: normal;
margin: 0;
padding: 15px 20px;
-
+
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
#comments-list footer {color: #888; padding: .5em 1em 0 0; text-align: right;}
-
+
#comments-list li:nth-child(2n) blockquote {background: #F5f5f5;}
-
+
/* Add a Comment */
#add-comment label {clear: left; float: left; text-align: left; width: 150px;}
#add-comment input[type='text'],
#add-comment input[type='email'],
#add-comment input[type='url'] {float: left; width: 200px;}
-
+
#add-comment textarea {float: left; height: 150px; width: 495px;}
-
+
#add-comment p.req {clear: both; margin: 0 .5em 1em 0; text-align: right;}
-
+
#add-comment input[type='submit'] {float: right; margin: 0 .5em;}
#add-comment * {margin-bottom: .5em;}
diff --git a/pelican/themes/notmyidea/static/css/typogrify.css b/pelican/themes/notmyidea/static/css/typogrify.css
index c9b34dc8..3bae4976 100644
--- a/pelican/themes/notmyidea/static/css/typogrify.css
+++ b/pelican/themes/notmyidea/static/css/typogrify.css
@@ -1,3 +1,3 @@
.caps {font-size:.92em;}
-.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
+.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
.dquo {margin-left:-.38em;}
From 9ce09c07151e6876025bab2460fcd6b79af3e9da Mon Sep 17 00:00:00 2001
From: Jorge Maldonado Ventura
Date: Sun, 30 Jul 2017 17:46:18 +0200
Subject: [PATCH 044/867] Store fonts of notmyidea theme locally
Used google-font-download ((commit ecc521e894c55e83773351264fe5bbef99ae70ad))
to download the fonts.
font-download -f 'woff woff2' -l 'latin' 'Yanone Kaffeesatz:400'
---
pelican/themes/notmyidea/static/css/fonts.css | 12 +
pelican/themes/notmyidea/static/css/main.css | 2 +-
.../static/fonts/Yanone_Kaffeesatz_400.eot | Bin 0 -> 20932 bytes
.../static/fonts/Yanone_Kaffeesatz_400.svg | 407 ++++++++++++++++++
.../static/fonts/Yanone_Kaffeesatz_400.ttf | Bin 0 -> 39168 bytes
.../static/fonts/Yanone_Kaffeesatz_400.woff | Bin 0 -> 22256 bytes
.../static/fonts/Yanone_Kaffeesatz_400.woff2 | Bin 0 -> 18320 bytes
.../themes/notmyidea/static/fonts/font.css | 12 +
8 files changed, 432 insertions(+), 1 deletion(-)
create mode 100644 pelican/themes/notmyidea/static/css/fonts.css
create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.eot
create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.svg
create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.ttf
create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.woff
create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.woff2
create mode 100644 pelican/themes/notmyidea/static/fonts/font.css
diff --git a/pelican/themes/notmyidea/static/css/fonts.css b/pelican/themes/notmyidea/static/css/fonts.css
new file mode 100644
index 00000000..56015076
--- /dev/null
+++ b/pelican/themes/notmyidea/static/css/fonts.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('../fonts/Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('../fonts/Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/themes/notmyidea/static/css/main.css b/pelican/themes/notmyidea/static/css/main.css
index 8f3deef0..9673ca45 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(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
+@import url("fonts.css");
/***** Global *****/
/* Body */
diff --git a/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.eot b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.eot
new file mode 100644
index 0000000000000000000000000000000000000000..b3b90dbc3ef34775a8689ab6a4494810c14c5a59
GIT binary patch
literal 20932
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
literal 0
HcmV?d00001
diff --git a/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.svg b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.svg
new file mode 100644
index 00000000..a69669b5
--- /dev/null
+++ b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.svg
@@ -0,0 +1,407 @@
+
+
+
diff --git a/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.ttf b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_400.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..6f4feb02d14c849ba1fb7dc112243386b2ad8f2f
GIT binary patch
literal 39168
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 |