Fix Pelican rendering and unit tests on Windows.

* Fix {filename} links on Windows.
  Otherwise '{filename}/foo/bar.jpg' doesn't work
* Clean up relative Posix path handling in contents.
* Use Posix paths in readers
* Environment for Popen must be strs, not unicodes.
* Ignore Git CRLF warnings.
* Replace CRLFs with LFs in inputs on Windows.
* Fix importer tests
* Fix test_contents
* Fix one last backslash in paginated output
* Skip the remaining failing locale tests on Windows.
* Document the use of forward slashes on Windows.
* Add some Fabric and ghp-import notes
This commit is contained in:
George V. Reilly 2015-01-02 23:45:44 -08:00
commit 4c25610cd8
14 changed files with 104 additions and 52 deletions

View file

@ -157,6 +157,9 @@ the other content will be placed after site generation).
To link to internal content (files in the ``content`` directory), use the To link to internal content (files in the ``content`` directory), use the
following syntax for the link target: ``{filename}path/to/file`` following syntax for the link target: ``{filename}path/to/file``
Note: forward slashes, ``/``,
are the required path separator in the ``{filename}`` directive
on all operating systems, including Windows.
For example, a Pelican project might be structured like this:: For example, a Pelican project might be structured like this::

View file

@ -100,6 +100,18 @@ separately. Use the following command to install Fabric, prefixing with
pip install Fabric pip install Fabric
.. note:: Installing PyCrypto on Windows
Fabric depends upon PyCrypto_, which is tricky to install
if your system doesn't have a C compiler.
For Windows users, before installing Fabric, use
``easy_install http://www.voidspace.org.uk/downloads/pycrypto26/pycrypto-2.6.win32-py2.7.exe``
per this `StackOverflow suggestion <http://stackoverflow.com/a/11405769/6364>`_
You're more likely to have success
with the Win32 versions of Python 2.7 and PyCrypto,
than with the Win64—\
even if your operating system is a 64-bit version of Windows.
Take a moment to open the ``fabfile.py`` file that was generated in your Take a moment to open the ``fabfile.py`` file that was generated in your
project root. You will see a number of commands, any one of which can be project root. You will see a number of commands, any one of which can be
renamed, removed, and/or customized to your liking. Using the out-of-the-box renamed, removed, and/or customized to your liking. Using the out-of-the-box
@ -179,3 +191,4 @@ executables, such as ``python3``, you can set the ``PY`` and ``PELICAN``
environment variables, respectively, to override the default executable names.) environment variables, respectively, to override the default executable names.)
.. _Fabric: http://fabfile.org/ .. _Fabric: http://fabfile.org/
.. _PyCrypto: http://pycrypto.org

View file

@ -36,9 +36,15 @@ already exist). The ``git push origin gh-pages`` command updates the remote
``gh-pages`` branch, effectively publishing the Pelican site. ``gh-pages`` branch, effectively publishing the Pelican site.
.. note:: .. note::
The ``github`` target of the Makefile (and the ``gh_pages`` task of the Fabfile)
created by the ``pelican-quickstart`` command
publishes the Pelican site as Project Pages, as described above.
The ``github`` target of the Makefile created by the ``pelican-quickstart`` .. note:: ghp-import on Windows
command publishes the Pelican site as Project Pages, as described above.
Until `ghp-import Pull Request #25 <https://github.com/davisp/ghp-import/pull/25>`_
is accepted, you will need to install a custom build of ghp-import:
``pip install https://github.com/chevah/ghp-import/archive/win-support.zip``
User Pages User Pages
---------- ----------
@ -86,6 +92,12 @@ output directory. For example::
STATIC_PATHS = ['images', 'extra/CNAME'] STATIC_PATHS = ['images', 'extra/CNAME']
EXTRA_PATH_METADATA = {'extra/CNAME': {'path': 'CNAME'},} EXTRA_PATH_METADATA = {'extra/CNAME': {'path': 'CNAME'},}
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.
How to add YouTube or Vimeo Videos How to add YouTube or Vimeo Videos
================================== ==================================

View file

@ -17,7 +17,7 @@ from pelican import signals
from pelican.settings import DEFAULT_CONFIG from pelican.settings import DEFAULT_CONFIG
from pelican.utils import (slugify, truncate_html_words, memoized, strftime, from pelican.utils import (slugify, truncate_html_words, memoized, strftime,
python_2_unicode_compatible, deprecated_attribute, python_2_unicode_compatible, deprecated_attribute,
path_to_url, set_date_tzinfo, SafeDatetime) path_to_url, posixize_path, set_date_tzinfo, SafeDatetime)
# Import these so that they're avalaible when you import from pelican.contents. # Import these so that they're avalaible when you import from pelican.contents.
from pelican.urlwrappers import (URLWrapper, Author, Category, Tag) # NOQA from pelican.urlwrappers import (URLWrapper, Author, Category, Tag) # NOQA
@ -337,17 +337,19 @@ class Content(object):
if source_path is None: if source_path is None:
return None return None
return os.path.relpath( return posixize_path(
os.path.abspath(os.path.join(self.settings['PATH'], source_path)), os.path.relpath(
os.path.abspath(self.settings['PATH']) os.path.abspath(os.path.join(self.settings['PATH'], source_path)),
) os.path.abspath(self.settings['PATH'])
))
@property @property
def relative_dir(self): def relative_dir(self):
return os.path.dirname(os.path.relpath( return posixize_path(
os.path.abspath(self.source_path), os.path.dirname(
os.path.abspath(self.settings['PATH'])) os.path.relpath(
) os.path.abspath(self.source_path),
os.path.abspath(self.settings['PATH']))))
class Page(Content): class Page(Content):

View file

@ -136,7 +136,7 @@ class Page(object):
# URL or SAVE_AS is a string, format it with a controlled context # URL or SAVE_AS is a string, format it with a controlled context
context = { context = {
'name': self.name, 'name': self.name.replace(os.sep, '/'),
'object_list': self.object_list, 'object_list': self.object_list,
'number': self.number, 'number': self.number,
'paginator': self.paginator, 'paginator': self.paginator,

View file

@ -25,7 +25,7 @@ from six.moves.html_parser import HTMLParser
from pelican import signals from pelican import signals
from pelican.contents import Page, Category, Tag, Author from pelican.contents import Page, Category, Tag, Author
from pelican.utils import get_date, pelican_open, FileStampDataCacher, SafeDatetime from pelican.utils import get_date, pelican_open, FileStampDataCacher, SafeDatetime, posixize_path
METADATA_PROCESSORS = { METADATA_PROCESSORS = {
@ -424,7 +424,7 @@ class Readers(FileStampDataCacher):
"""Return a content object parsed with the given format.""" """Return a content object parsed with the given format."""
path = os.path.abspath(os.path.join(base_path, path)) path = os.path.abspath(os.path.join(base_path, path))
source_path = os.path.relpath(path, base_path) source_path = posixize_path(os.path.relpath(path, base_path))
logger.debug('Read file %s -> %s', logger.debug('Read file %s -> %s',
source_path, content_class.__name__) source_path, content_class.__name__)

View file

@ -18,6 +18,7 @@ except ImportError:
load_source = imp.load_source load_source = imp.load_source
from os.path import isabs from os.path import isabs
from pelican.utils import posix_join
from pelican.log import LimitFilter from pelican.log import LimitFilter
@ -41,11 +42,11 @@ DEFAULT_CONFIG = {
'STATIC_EXCLUDE_SOURCES': True, 'STATIC_EXCLUDE_SOURCES': True,
'THEME_STATIC_DIR': 'theme', 'THEME_STATIC_DIR': 'theme',
'THEME_STATIC_PATHS': ['static', ], 'THEME_STATIC_PATHS': ['static', ],
'FEED_ALL_ATOM': os.path.join('feeds', 'all.atom.xml'), 'FEED_ALL_ATOM': posix_join('feeds', 'all.atom.xml'),
'CATEGORY_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'), 'CATEGORY_FEED_ATOM': posix_join('feeds', '%s.atom.xml'),
'AUTHOR_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'), 'AUTHOR_FEED_ATOM': posix_join('feeds', '%s.atom.xml'),
'AUTHOR_FEED_RSS': os.path.join('feeds', '%s.rss.xml'), 'AUTHOR_FEED_RSS': posix_join('feeds', '%s.rss.xml'),
'TRANSLATION_FEED_ATOM': os.path.join('feeds', 'all-%s.atom.xml'), 'TRANSLATION_FEED_ATOM': posix_join('feeds', 'all-%s.atom.xml'),
'FEED_MAX_ITEMS': '', 'FEED_MAX_ITEMS': '',
'SITEURL': '', 'SITEURL': '',
'SITENAME': 'A Pelican Blog', 'SITENAME': 'A Pelican Blog',
@ -68,25 +69,25 @@ DEFAULT_CONFIG = {
'ARTICLE_LANG_URL': '{slug}-{lang}.html', 'ARTICLE_LANG_URL': '{slug}-{lang}.html',
'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html', 'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html',
'DRAFT_URL': 'drafts/{slug}.html', 'DRAFT_URL': 'drafts/{slug}.html',
'DRAFT_SAVE_AS': os.path.join('drafts', '{slug}.html'), 'DRAFT_SAVE_AS': posix_join('drafts', '{slug}.html'),
'DRAFT_LANG_URL': 'drafts/{slug}-{lang}.html', 'DRAFT_LANG_URL': 'drafts/{slug}-{lang}.html',
'DRAFT_LANG_SAVE_AS': os.path.join('drafts', '{slug}-{lang}.html'), 'DRAFT_LANG_SAVE_AS': posix_join('drafts', '{slug}-{lang}.html'),
'PAGE_URL': 'pages/{slug}.html', 'PAGE_URL': 'pages/{slug}.html',
'PAGE_SAVE_AS': os.path.join('pages', '{slug}.html'), 'PAGE_SAVE_AS': posix_join('pages', '{slug}.html'),
'PAGE_ORDER_BY': 'basename', 'PAGE_ORDER_BY': 'basename',
'PAGE_LANG_URL': 'pages/{slug}-{lang}.html', 'PAGE_LANG_URL': 'pages/{slug}-{lang}.html',
'PAGE_LANG_SAVE_AS': os.path.join('pages', '{slug}-{lang}.html'), 'PAGE_LANG_SAVE_AS': posix_join('pages', '{slug}-{lang}.html'),
'STATIC_URL': '{path}', 'STATIC_URL': '{path}',
'STATIC_SAVE_AS': '{path}', 'STATIC_SAVE_AS': '{path}',
'PDF_GENERATOR': False, 'PDF_GENERATOR': False,
'PDF_STYLE_PATH': '', 'PDF_STYLE_PATH': '',
'PDF_STYLE': 'twelvepoint', 'PDF_STYLE': 'twelvepoint',
'CATEGORY_URL': 'category/{slug}.html', 'CATEGORY_URL': 'category/{slug}.html',
'CATEGORY_SAVE_AS': os.path.join('category', '{slug}.html'), 'CATEGORY_SAVE_AS': posix_join('category', '{slug}.html'),
'TAG_URL': 'tag/{slug}.html', 'TAG_URL': 'tag/{slug}.html',
'TAG_SAVE_AS': os.path.join('tag', '{slug}.html'), 'TAG_SAVE_AS': posix_join('tag', '{slug}.html'),
'AUTHOR_URL': 'author/{slug}.html', 'AUTHOR_URL': 'author/{slug}.html',
'AUTHOR_SAVE_AS': os.path.join('author', '{slug}.html'), 'AUTHOR_SAVE_AS': posix_join('author', '{slug}.html'),
'PAGINATION_PATTERNS': [ 'PAGINATION_PATTERNS': [
(0, '{name}{number}{extension}', '{name}{number}{extension}'), (0, '{name}{number}{extension}', '{name}{number}{extension}'),
], ],

View file

@ -10,7 +10,7 @@ from pelican.tests.support import unittest, get_settings
from pelican.contents import Page, Article, Static, URLWrapper from pelican.contents import Page, Article, Static, URLWrapper
from pelican.settings import DEFAULT_CONFIG from pelican.settings import DEFAULT_CONFIG
from pelican.utils import path_to_url, truncate_html_words, SafeDatetime from pelican.utils import path_to_url, truncate_html_words, SafeDatetime, posix_join
from pelican.signals import content_object_init from pelican.signals import content_object_init
from jinja2.utils import generate_lorem_ipsum from jinja2.utils import generate_lorem_ipsum
@ -417,7 +417,7 @@ class TestStatic(unittest.TestCase):
self.context = self.settings.copy() self.context = self.settings.copy()
self.static = Static(content=None, metadata={}, settings=self.settings, self.static = Static(content=None, metadata={}, settings=self.settings,
source_path=os.path.join('dir', 'foo.jpg'), context=self.context) source_path=posix_join('dir', 'foo.jpg'), context=self.context)
self.context['filenames'] = {self.static.source_path: self.static} self.context['filenames'] = {self.static.source_path: self.static}

View file

@ -9,7 +9,7 @@ from pelican.tools.pelican_import import wp2fields, fields2pelican, decode_wp_co
from pelican.tests.support import (unittest, temporary_folder, mute, from pelican.tests.support import (unittest, temporary_folder, mute,
skipIfNoExecutable) skipIfNoExecutable)
from pelican.utils import slugify from pelican.utils import slugify, path_to_file_url
CUR_DIR = os.path.abspath(os.path.dirname(__file__)) CUR_DIR = os.path.abspath(os.path.dirname(__file__))
WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml') WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml')
@ -293,12 +293,11 @@ class TestWordpressXMLAttachements(unittest.TestCase):
def test_download_attachments(self): def test_download_attachments(self):
real_file = os.path.join(CUR_DIR, 'content/article.rst') real_file = os.path.join(CUR_DIR, 'content/article.rst')
good_url = 'file://' + real_file good_url = path_to_file_url(real_file)
bad_url = 'http://localhost:1/not_a_file.txt' bad_url = 'http://localhost:1/not_a_file.txt'
silent_da = mute()(download_attachments) silent_da = mute()(download_attachments)
with temporary_folder() as temp: with temporary_folder() as temp:
#locations = download_attachments(temp, [good_url, bad_url])
locations = list(silent_da(temp, [good_url, bad_url])) locations = list(silent_da(temp, [good_url, bad_url]))
self.assertTrue(len(locations) == 1) self.assertEqual(1, len(locations))
directory = locations[0] directory = locations[0]
self.assertTrue(directory.endswith('content/article.rst')) self.assertTrue(directory.endswith(os.path.join('content', 'article.rst')), directory)

View file

@ -58,22 +58,22 @@ class TestPelican(LoggedTestCase):
locale.setlocale(locale.LC_ALL, self.old_locale) locale.setlocale(locale.LC_ALL, self.old_locale)
super(TestPelican, self).tearDown() super(TestPelican, self).tearDown()
def assertFilesEqual(self, diff):
msg = ("some generated files differ from the expected functional "
"tests output.\n"
"This is probably because the HTML generated files "
"changed. If these changes are normal, please refer "
"to docs/contribute.rst to update the expected "
"output of the functional tests.")
self.assertEqual(diff['left_only'], [], msg=msg)
self.assertEqual(diff['right_only'], [], msg=msg)
self.assertEqual(diff['diff_files'], [], msg=msg)
def assertDirsEqual(self, left_path, right_path): def assertDirsEqual(self, left_path, right_path):
out, err = subprocess.Popen( out, err = subprocess.Popen(
['git', 'diff', '--no-ext-diff', '--exit-code', '-w', left_path, right_path], env={'PAGER': ''}, ['git', 'diff', '--no-ext-diff', '--exit-code', '-w', left_path, right_path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() env={b'PAGER': b''}, stdout=subprocess.PIPE, stderr=subprocess.PIPE
).communicate()
def ignorable_git_crlf_errors(line):
# Work around for running tests on Windows
for msg in [
"LF will be replaced by CRLF",
"The file will have its original line endings"]:
if msg in line:
return True
return False
if err:
err = '\n'.join([l for l in err.decode('utf8').splitlines()
if not ignorable_git_crlf_errors(l)])
assert not out, out assert not out, out
assert not err, err assert not err, err

View file

@ -3,6 +3,7 @@ from __future__ import unicode_literals, print_function
import copy import copy
import os import os
import locale import locale
from sys import platform
from os.path import dirname, abspath, join from os.path import dirname, abspath, join
from pelican.settings import (read_settings, configure_settings, from pelican.settings import (read_settings, configure_settings,
@ -107,6 +108,8 @@ class TestSettingsConfiguration(unittest.TestCase):
# locale is not specified in the settings # locale is not specified in the settings
#reset locale to python default #reset locale to python default
if platform == 'win32':
return unittest.skip("Doesn't work on Windows")
locale.setlocale(locale.LC_ALL, str('C')) locale.setlocale(locale.LC_ALL, str('C'))
self.assertEqual(self.settings['LOCALE'], DEFAULT_CONFIG['LOCALE']) self.assertEqual(self.settings['LOCALE'], DEFAULT_CONFIG['LOCALE'])

View file

@ -301,7 +301,7 @@ class TestUtils(LoggedTestCase):
old_locale = locale.setlocale(locale.LC_TIME) old_locale = locale.setlocale(locale.LC_TIME)
if platform == 'win32': if platform == 'win32':
locale.setlocale(locale.LC_TIME, str('Turkish')) return unittest.skip("Doesn't work on Windows")
else: else:
locale.setlocale(locale.LC_TIME, str('tr_TR.UTF-8')) locale.setlocale(locale.LC_TIME, str('tr_TR.UTF-8'))
@ -471,6 +471,8 @@ class TestDateFormatter(unittest.TestCase):
locale_available('French'), locale_available('French'),
'French locale needed') 'French locale needed')
def test_french_strftime(self): def test_french_strftime(self):
if platform == 'win32':
return unittest.skip("Doesn't work on Windows")
# This test tries to reproduce an issue that occurred with python3.3 under macos10 only # This test tries to reproduce an issue that occurred with python3.3 under macos10 only
locale.setlocale(locale.LC_ALL, str('fr_FR.UTF-8')) locale.setlocale(locale.LC_ALL, str('fr_FR.UTF-8'))
date = utils.SafeDatetime(2014,8,14) date = utils.SafeDatetime(2014,8,14)

View file

@ -588,7 +588,8 @@ def download_attachments(output_path, urls):
filename = path.pop(-1) filename = path.pop(-1)
localpath = '' localpath = ''
for item in path: for item in path:
localpath = os.path.join(localpath, item) if sys.platform != 'win32' or ':' not in item:
localpath = os.path.join(localpath, item)
full_path = os.path.join(output_path, localpath) full_path = os.path.join(output_path, localpath)
if not os.path.exists(full_path): if not os.path.exists(full_path):
os.makedirs(full_path) os.makedirs(full_path)

View file

@ -11,6 +11,7 @@ import os
import pytz import pytz
import re import re
import shutil import shutil
import sys
import traceback import traceback
import pickle import pickle
import hashlib import hashlib
@ -23,6 +24,7 @@ from functools import partial
from itertools import groupby from itertools import groupby
from jinja2 import Markup from jinja2 import Markup
from operator import attrgetter from operator import attrgetter
from posixpath import join as posix_join
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -230,13 +232,15 @@ def get_date(string):
@contextmanager @contextmanager
def pelican_open(filename): def pelican_open(filename, mode='rb', strip_crs=(sys.platform == 'win32')):
"""Open a file and return its content""" """Open a file and return its content"""
with codecs.open(filename, encoding='utf-8') as infile: with codecs.open(filename, mode, encoding='utf-8') as infile:
content = infile.read() content = infile.read()
if content[0] == codecs.BOM_UTF8.decode('utf8'): if content[0] == codecs.BOM_UTF8.decode('utf8'):
content = content[1:] content = content[1:]
if strip_crs:
content = content.replace('\r\n', '\n')
yield content yield content
@ -370,6 +374,13 @@ def path_to_url(path):
return '/'.join(split_all(path)) return '/'.join(split_all(path))
def posixize_path(rel_path):
"""Use '/' as path separator, so that source references,
like '{filename}/foo/bar.jpg' or 'extras/favicon.ico',
will work on Windows as well as on Mac and Linux."""
return rel_path.replace(os.sep, '/')
def truncate_html_words(s, num, end_text='...'): def truncate_html_words(s, num, end_text='...'):
"""Truncates HTML to a certain number of words. """Truncates HTML to a certain number of words.
@ -750,4 +761,9 @@ def is_selected_for_writing(settings, path):
return path in settings['WRITE_SELECTED'] return path in settings['WRITE_SELECTED']
else: else:
return True return True
def path_to_file_url(path):
'''Convert file-system path to file:// URL'''
return six.moves.urllib_parse.urljoin(
"file://", six.moves.urllib.request.pathname2url(path))