From 1194764ed1ddcce797fb1895668ccc5e97137c93 Mon Sep 17 00:00:00 2001 From: draftcode Date: Sat, 10 Mar 2012 21:18:01 +0900 Subject: [PATCH 01/22] Do not create feeds when their filenames are set to None. --- docs/settings.rst | 4 +++- pelican/generators.py | 47 ++++++++++++++++++++++------------------ tests/test_generators.py | 36 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 tests/test_generators.py diff --git a/docs/settings.rst b/docs/settings.rst index 69e2adc8..b08b5bcb 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -199,7 +199,6 @@ Pelican generates category feeds as well as feeds for all your articles. It does not generate feeds for tags by default, but it is possible to do so using the ``TAG_FEED`` and ``TAG_FEED_RSS`` settings: - ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== @@ -214,6 +213,9 @@ Setting name (default value) What does it do? quantity is unrestricted by default. ================================================ ===================================================== +If you don't want to generate some of these feeds, set ``None`` to the +variables above. + .. [2] %s is the name of the category. Pagination diff --git a/pelican/generators.py b/pelican/generators.py index 6ba12cf4..ccfdb39f 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -118,41 +118,46 @@ class ArticlesGenerator(Generator): def generate_feeds(self, writer): """Generate the feeds from the current context, and output files.""" - writer.write_feed(self.articles, self.context, self.settings['FEED']) - - if 'FEED_RSS' in self.settings: + if self.settings.get('FEED'): writer.write_feed(self.articles, self.context, - self.settings['FEED_RSS'], feed_type='rss') + self.settings['FEED']) + + if self.settings.get('FEED_RSS'): + writer.write_feed(self.articles, self.context, + self.settings['FEED_RSS'], feed_type='rss') for cat, arts in self.categories: arts.sort(key=attrgetter('date'), reverse=True) - writer.write_feed(arts, self.context, - self.settings['CATEGORY_FEED'] % cat) - - if 'CATEGORY_FEED_RSS' in self.settings: + if self.settings.get('CATEGORY_FEED'): writer.write_feed(arts, self.context, - self.settings['CATEGORY_FEED_RSS'] % cat, - feed_type='rss') + self.settings['CATEGORY_FEED'] % cat) - if 'TAG_FEED' in self.settings: + if self.settings.get('CATEGORY_FEED_RSS'): + writer.write_feed(arts, self.context, + self.settings['CATEGORY_FEED_RSS'] % cat, + feed_type='rss') + + if self.settings.get('TAG_FEED') or self.settings.get('TAG_FEED_RSS'): for tag, arts in self.tags.items(): arts.sort(key=attrgetter('date'), reverse=True) - writer.write_feed(arts, self.context, - self.settings['TAG_FEED'] % tag) + if self.settings.get('TAG_FEED'): + writer.write_feed(arts, self.context, + self.settings['TAG_FEED'] % tag) - if 'TAG_FEED_RSS' in self.settings: + if self.settings.get('TAG_FEED_RSS'): writer.write_feed(arts, self.context, self.settings['TAG_FEED_RSS'] % tag, feed_type='rss') - translations_feeds = defaultdict(list) - for article in chain(self.articles, self.translations): - translations_feeds[article.lang].append(article) + if self.settings.get('TRANSLATION_FEED'): + translations_feeds = defaultdict(list) + for article in chain(self.articles, self.translations): + translations_feeds[article.lang].append(article) - for lang, items in translations_feeds.items(): - items.sort(key=attrgetter('date'), reverse=True) - writer.write_feed(items, self.context, - self.settings['TRANSLATION_FEED'] % lang) + for lang, items in translations_feeds.items(): + items.sort(key=attrgetter('date'), reverse=True) + writer.write_feed(items, self.context, + self.settings['TRANSLATION_FEED'] % lang) def generate_pages(self, writer): """Generate the pages on the disk""" diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 00000000..94088c34 --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement +try: + from unittest2 import TestCase +except ImportError, e: + from unittest import TestCase + +from pelican.generators import ArticlesGenerator +from pelican.settings import _DEFAULT_CONFIG + +class TestArticlesGenerator(TestCase): + + def test_generate_feeds(self): + + class FakeWriter(object): + def __init__(self): + self.called = False + + def write_feed(self, *args, **kwargs): + self.called = True + + generator = ArticlesGenerator(None, {'FEED': _DEFAULT_CONFIG['FEED']}, + None, _DEFAULT_CONFIG['THEME'], None, + None) + writer = FakeWriter() + generator.generate_feeds(writer) + assert writer.called, ("The feed should be written, " + "if settings['FEED'] is specified.") + + generator = ArticlesGenerator(None, {'FEED': None}, None, + _DEFAULT_CONFIG['THEME'], None, None) + writer = FakeWriter() + generator.generate_feeds(writer) + assert not writer.called, ("If settings['FEED'] is None, " + "the feed should not be generated.") + From 3bdc769134a57d6ce62f2d80b095061d91bacb2f Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 01:14:22 +0100 Subject: [PATCH 02/22] Factorize some code about URL wrapping. --- pelican/contents.py | 69 +++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/pelican/contents.py b/pelican/contents.py index 99740168..900061ad 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -2,6 +2,7 @@ from datetime import datetime from os import getenv from sys import platform, stdin +import functools import locale from pelican.log import warning, error @@ -108,17 +109,13 @@ class Page(object): 'category': getattr(self, 'category', 'misc'), } - @property - def url(self): - if self.in_default_lang: - return self.settings['PAGE_URL'].format(**self.url_format) - return self.settings['PAGE_LANG_URL'].format(**self.url_format) + def _expand_settings(self, key): + fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper() + return self.settings[fq_key].format(**self.url_format) - @property - def save_as(self): - if self.in_default_lang: - return self.settings['PAGE_SAVE_AS'].format(**self.url_format) - return self.settings['PAGE_LANG_SAVE_AS'].format(**self.url_format) + def get_url_setting(self, key): + key = key if self.in_default_lang else 'lang_%s' % key + return self._expand_settings(key) @property def content(self): @@ -139,22 +136,13 @@ class Page(object): summary = property(_get_summary, _set_summary, "Summary of the article." "Based on the content. Can't be set") + url = property(functools.partial(get_url_setting, key='url')) + save_as = property(functools.partial(get_url_setting, key='save_as')) + class Article(Page): mandatory_properties = ('title', 'date', 'category') - @property - def url(self): - if self.in_default_lang: - return self.settings['ARTICLE_URL'].format(**self.url_format) - return self.settings['ARTICLE_LANG_URL'].format(**self.url_format) - - @property - def save_as(self): - if self.in_default_lang: - return self.settings['ARTICLE_SAVE_AS'].format(**self.url_format) - return self.settings['ARTICLE_LANG_SAVE_AS'].format(**self.url_format) - class Quote(Page): base_properties = ('author', 'date') @@ -163,8 +151,12 @@ class Quote(Page): class URLWrapper(object): def __init__(self, name, settings): self.name = unicode(name) + self.slug = slugify(self.name) self.settings = settings + def as_dict(self): + return self.__dict__ + def __hash__(self): return hash(self.name) @@ -177,42 +169,25 @@ class URLWrapper(object): def __unicode__(self): return self.name - @property - def url(self): - return '%s.html' % self.name + def _from_settings(self, key): + setting = "%s_%s" % (self.__class__.__name__.upper(), key) + return self.settings[setting].format(**self.as_dict()) + + url = property(functools.partial(_from_settings, key='URL')) + save_as = property(functools.partial(_from_settings, key='SAVE_AS')) class Category(URLWrapper): - @property - def url(self): - return self.settings['CATEGORY_URL'].format(name=self.name) - - @property - def save_as(self): - return self.settings['CATEGORY_SAVE_AS'].format(name=self.name) + pass class Tag(URLWrapper): def __init__(self, name, *args, **kwargs): super(Tag, self).__init__(unicode.strip(name), *args, **kwargs) - @property - def url(self): - return self.settings['TAG_URL'].format(name=self.name) - - @property - def save_as(self): - return self.settings['TAG_SAVE_AS'].format(name=self.name) - class Author(URLWrapper): - @property - def url(self): - return self.settings['AUTHOR_URL'].format(name=self.name) - - @property - def save_as(self): - return self.settings['AUTHOR_SAVE_AS'].format(name=self.name) + pass def is_valid_content(content, f): From 1ec2779f5e4470c6ed19b56d16185c6174ab520c Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 01:16:02 +0100 Subject: [PATCH 03/22] Make the readers tests a bit more verbose. --- tests/test_readers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_readers.py b/tests/test_readers.py index 120b3125..2d023462 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -1,8 +1,8 @@ # coding: utf-8 try: - import unittest2 + import unittest2 as unittest except ImportError, e: - import unittest as unittest2 + import unittest import datetime import os @@ -12,11 +12,12 @@ from pelican import readers CUR_DIR = os.path.dirname(__file__) CONTENT_PATH = os.path.join(CUR_DIR, 'content') + def _filename(*args): return os.path.join(CONTENT_PATH, *args) -class RstReaderTest(unittest2.TestCase): +class RstReaderTest(unittest.TestCase): def test_article_with_metadata(self): reader = readers.RstReader({}) @@ -29,4 +30,6 @@ class RstReaderTest(unittest2.TestCase): 'date': datetime.datetime(2010, 12, 2, 10, 14), 'tags': ['foo', 'bar', 'foobar'], } - self.assertDictEqual(metadata, expected) + + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) From 1c219d14bb36ea4f12dda3f987a7c843ba2cec40 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 01:16:32 +0100 Subject: [PATCH 04/22] Use the slug as default URL for tags and authors. --- pelican/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pelican/settings.py b/pelican/settings.py index 6ed76f46..d62acf42 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -37,10 +37,10 @@ _DEFAULT_CONFIG = {'PATH': None, 'PAGE_LANG_SAVE_AS': 'pages/{slug}-{lang}.html', 'CATEGORY_URL': 'category/{name}.html', 'CATEGORY_SAVE_AS': 'category/{name}.html', - 'TAG_URL': 'tag/{name}.html', - 'TAG_SAVE_AS': 'tag/{name}.html', - 'AUTHOR_URL': u'author/{name}.html', - 'AUTHOR_SAVE_AS': u'author/{name}.html', + 'TAG_URL': 'tag/{slug}.html', + 'TAG_SAVE_AS': 'tag/{slug}.html', + 'AUTHOR_URL': u'author/{slug}.html', + 'AUTHOR_SAVE_AS': u'author/{slug}.html', 'RELATIVE_URLS': True, 'DEFAULT_LANG': 'en', 'TAG_CLOUD_STEPS': 4, From 0ca9997e107f67fc4e8eebd0511809e254b11f58 Mon Sep 17 00:00:00 2001 From: Bruno Binet Date: Tue, 6 Mar 2012 00:29:56 +0100 Subject: [PATCH 05/22] paths for finding articles and pages are now parametrable --- pelican/generators.py | 10 ++++++---- pelican/settings.py | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pelican/generators.py b/pelican/generators.py index 6ba12cf4..ac2cd865 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -211,10 +211,10 @@ class ArticlesGenerator(Generator): def generate_context(self): """change the context""" - # return the list of files to use - files = self.get_files(self.path, exclude=['pages', ]) all_articles = [] - for f in files: + for f in self.get_files( + os.path.join(self.path, self.settings['ARTICLE_DIR']), + exclude=self.settings['ARTICLE_EXCLUDES']): try: content, metadata = read_file(f, settings=self.settings) except Exception, e: @@ -316,7 +316,9 @@ class PagesGenerator(Generator): def generate_context(self): all_pages = [] - for f in self.get_files(os.sep.join((self.path, 'pages'))): + for f in self.get_files( + os.path.join(self.path, self.settings['PAGE_DIR']), + exclude=self.settings['PAGE_EXCLUDES']): try: content, metadata = read_file(f) except Exception, e: diff --git a/pelican/settings.py b/pelican/settings.py index 6ed76f46..1b31582f 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -8,6 +8,10 @@ from pelican import log DEFAULT_THEME = os.sep.join([os.path.dirname(os.path.abspath(__file__)), "themes/notmyidea"]) _DEFAULT_CONFIG = {'PATH': None, + 'ARTICLE_DIR': '', + 'ARTICLE_EXCLUDES': ('pages',), + 'PAGE_DIR': 'pages', + 'PAGE_EXCLUDES': (), 'THEME': DEFAULT_THEME, 'OUTPUT_PATH': 'output/', 'MARKUP': ('rst', 'md'), From aef7418bdf2094b8e6edc5b2e6300bf8a60bb851 Mon Sep 17 00:00:00 2001 From: Bruno Binet Date: Tue, 6 Mar 2012 00:45:22 +0100 Subject: [PATCH 06/22] add docs for new page/article paths settings --- docs/settings.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index 69e2adc8..31183be3 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -52,6 +52,10 @@ Setting name (default value) What does it do? supported extensions. `OUTPUT_PATH` (``'output/'``) Where to output the generated files. `PATH` (``None``) Path to look at for input files. +`PAGE_DIR' (``'pages'``) Directory to look at for pages. +`PAGE_EXCLUDES' (``()``) A list of directories to exclude when looking for pages. +`ARTICLE_DIR' (``''``) Directory to look at for articles. +`ARTICLE_EXCLUDES': (``('pages',)``) A list of directories to exclude when looking for articles. `PDF_GENERATOR` (``False``) Set to True if you want to have PDF versions of your documents. You will need to install `rst2pdf`. From d6be2fb44cfb39afc41bf4a3ce1dd842d549da06 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 01:59:04 +0100 Subject: [PATCH 07/22] Put deprecation code in a separate place --- pelican/__init__.py | 83 +++++++++++++++++++++++++-------------------- pelican/settings.py | 2 +- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 8de68d69..dcdbdcb6 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -6,7 +6,7 @@ import time from pelican.generators import (ArticlesGenerator, PagesGenerator, StaticGenerator, PdfGenerator) -from pelican.settings import read_settings +from pelican.settings import read_settings, _DEFAULT_CONFIG from pelican.utils import clean_output_dir, files_changed from pelican.writers import Writer from pelican import log @@ -20,6 +20,9 @@ class Pelican(object): """Read the settings, and performs some checks on the environment before doing anything else. """ + if settings is None: + settings = _DEFAULT_CONFIG + self.path = path or settings['PATH'] if not self.path: raise Exception('you need to specify a path containing the content' @@ -28,44 +31,11 @@ class Pelican(object): if self.path.endswith('/'): self.path = self.path[:-1] - if settings.get('CLEAN_URLS', False): - log.warning('Found deprecated `CLEAN_URLS` in settings. Modifing' - ' the following settings for the same behaviour.') - - settings['ARTICLE_URL'] = '{slug}/' - settings['ARTICLE_LANG_URL'] = '{slug}-{lang}/' - settings['PAGE_URL'] = 'pages/{slug}/' - settings['PAGE_LANG_URL'] = 'pages/{slug}-{lang}/' - - for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', - 'PAGE_LANG_URL'): - log.warning("%s = '%s'" % (setting, settings[setting])) - - if settings.get('ARTICLE_PERMALINK_STRUCTURE', False): - log.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in' - ' settings. Modifing the following settings for' - ' the same behaviour.') - - structure = settings['ARTICLE_PERMALINK_STRUCTURE'] - - # Convert %(variable) into {variable}. - structure = re.sub('%\((\w+)\)s', '{\g<1>}', structure) - - # Convert %x into {date:%x} for strftime - structure = re.sub('(%[A-z])', '{date:\g<1>}', structure) - - # Strip a / prefix - structure = re.sub('^/', '', structure) - - for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', - 'PAGE_LANG_URL', 'ARTICLE_SAVE_AS', - 'ARTICLE_LANG_SAVE_AS', 'PAGE_SAVE_AS', - 'PAGE_LANG_SAVE_AS'): - settings[setting] = os.path.join(structure, settings[setting]) - log.warning("%s = '%s'" % (setting, settings[setting])) - # define the default settings self.settings = settings + + self._handle_deprecation() + self.theme = theme or settings['THEME'] output_path = output_path or settings['OUTPUT_PATH'] self.output_path = os.path.realpath(output_path) @@ -82,6 +52,45 @@ class Pelican(object): else: raise Exception("Impossible to find the theme %s" % theme) + def _handle_deprecation(self): + + if self.settings.get('CLEAN_URLS', False): + log.warning('Found deprecated `CLEAN_URLS` in settings. Modifing' + ' the following settings for the same behaviour.') + + self.settings['ARTICLE_URL'] = '{slug}/' + self.settings['ARTICLE_LANG_URL'] = '{slug}-{lang}/' + self.settings['PAGE_URL'] = 'pages/{slug}/' + self.settings['PAGE_LANG_URL'] = 'pages/{slug}-{lang}/' + + for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', + 'PAGE_LANG_URL'): + log.warning("%s = '%s'" % (setting, self.settings[setting])) + + if self.settings.get('ARTICLE_PERMALINK_STRUCTURE', False): + log.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in' + ' settings. Modifing the following settings for' + ' the same behaviour.') + + structure = self.settings['ARTICLE_PERMALINK_STRUCTURE'] + + # Convert %(variable) into {variable}. + structure = re.sub('%\((\w+)\)s', '{\g<1>}', structure) + + # Convert %x into {date:%x} for strftime + structure = re.sub('(%[A-z])', '{date:\g<1>}', structure) + + # Strip a / prefix + structure = re.sub('^/', '', structure) + + for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', + 'PAGE_LANG_URL', 'ARTICLE_SAVE_AS', + 'ARTICLE_LANG_SAVE_AS', 'PAGE_SAVE_AS', + 'PAGE_LANG_SAVE_AS'): + self.settings[setting] = os.path.join(structure, + self.settings[setting]) + log.warning("%s = '%s'" % (setting, self.settings[setting])) + def run(self): """Run the generators and return""" diff --git a/pelican/settings.py b/pelican/settings.py index d62acf42..bfc8e940 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -61,7 +61,7 @@ _DEFAULT_CONFIG = {'PATH': None, } -def read_settings(filename): +def read_settings(filename=None): """Load a Python file into a dictionary. """ context = _DEFAULT_CONFIG.copy() From fbf89687cc05c0656e7efc69befde6c790ec66b4 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 01:59:58 +0100 Subject: [PATCH 08/22] start functional testing --- tests/support.py | 18 ++++++++++++++++++ tests/test_pelican.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/support.py create mode 100644 tests/test_pelican.py diff --git a/tests/support.py b/tests/support.py new file mode 100644 index 00000000..0cd757a6 --- /dev/null +++ b/tests/support.py @@ -0,0 +1,18 @@ +from contextlib import contextmanager + +from tempfile import mkdtemp +from shutil import rmtree + + +@contextmanager +def temporary_folder(): + """creates a temporary folder, return it and delete it afterwards. + + This allows to do something like this in tests: + + >>> with temporary_folder() as d: + # do whatever you want + """ + tempdir = mkdtemp() + yield tempdir + rmtree(tempdir) diff --git a/tests/test_pelican.py b/tests/test_pelican.py new file mode 100644 index 00000000..dce4fadc --- /dev/null +++ b/tests/test_pelican.py @@ -0,0 +1,31 @@ +import unittest +import os + +from support import temporary_folder + +from pelican import Pelican +from pelican.settings import read_settings + +SAMPLES_PATH = os.path.abspath(os.sep.join( + (os.path.dirname(os.path.abspath(__file__)), "..", "samples"))) + +INPUT_PATH = os.path.join(SAMPLES_PATH, "content") +SAMPLE_CONFIG = os.path.join(SAMPLES_PATH, "pelican.conf.py") + + +class TestPelican(unittest.TestCase): + # general functional testing for pelican. Basically, this test case tries + # to run pelican in different situations and see how it behaves + + def test_basic_generation_works(self): + # when running pelican without settings, it should pick up the default + # ones and generate the output without raising any exception / issuing + # any warning. + with temporary_folder() as temp_path: + pelican = Pelican(path=INPUT_PATH, output_path=temp_path) + pelican.run() + + # the same thing with a specified set of settins should work + with temporary_folder() as temp_path: + pelican = Pelican(path=INPUT_PATH, output_path=temp_path, + settings=read_settings(SAMPLE_CONFIG)) From d43bd1dcb80801dfabfba661afa81a790816ee28 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 02:48:36 +0100 Subject: [PATCH 09/22] Add a way to use Typogrify to enhance the generated HTML. --- docs/settings.rst | 5 +++++ pelican/readers.py | 14 +++++++++++++- pelican/settings.py | 3 ++- tests/content/article.rst | 4 ++++ tests/test_readers.py | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/content/article.rst diff --git a/docs/settings.rst b/docs/settings.rst index 69e2adc8..6780c6ae 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -69,6 +69,11 @@ Setting name (default value) What does it do? `TIMEZONE` The timezone used in the date information, to generate Atom and RSS feeds. See the "timezone" section below for more info. +`TYPOGRIFY` (``False``) If set to true, some + additional transformations will be done on the + generated HTML, using the `Typogrify + `_ + library ================================================ ===================================================== .. [#] Default is the system locale. diff --git a/pelican/readers.py b/pelican/readers.py index 5bbbfb30..a581e458 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -143,12 +143,24 @@ def read_file(filename, fmt=None, settings=None): """Return a reader object using the given format.""" if not fmt: fmt = filename.split('.')[-1] + if fmt not in _EXTENSIONS.keys(): raise TypeError('Pelican does not know how to parse %s' % filename) + reader = _EXTENSIONS[fmt](settings) settings_key = '%s_EXTENSIONS' % fmt.upper() + if settings and settings_key in settings: reader.extensions = settings[settings_key] + if not reader.enabled: raise ValueError("Missing dependencies for %s" % fmt) - return reader.read(filename) + + content, metadata = reader.read(filename) + + # eventually filter the content with typogrify if asked so + if settings and settings['TYPOGRIFY']: + from typogrify import Typogrify + content = Typogrify.typogrify(content) + + return content, metadata diff --git a/pelican/settings.py b/pelican/settings.py index bfc8e940..8cb06e90 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -57,7 +57,8 @@ _DEFAULT_CONFIG = {'PATH': None, 'DEFAULT_METADATA': (), 'FILES_TO_COPY': (), 'DEFAULT_STATUS': 'published', - 'ARTICLE_PERMALINK_STRUCTURE': '' + 'ARTICLE_PERMALINK_STRUCTURE': '', + 'TYPOGRIFY': False, } diff --git a/tests/content/article.rst b/tests/content/article.rst new file mode 100644 index 00000000..1707ab03 --- /dev/null +++ b/tests/content/article.rst @@ -0,0 +1,4 @@ +Article title +############# + +This is some content. With some stuff to "typogrify". diff --git a/tests/test_readers.py b/tests/test_readers.py index 2d023462..d4f0aecf 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -33,3 +33,20 @@ class RstReaderTest(unittest.TestCase): for key, value in expected.items(): self.assertEquals(value, metadata[key], key) + + def test_typogrify(self): + # if nothing is specified in the settings, the content should be + # unmodified + content, _ = readers.read_file(_filename('article.rst')) + expected = "

This is some content. With some stuff to "\ + ""typogrify".

\n" + + self.assertEqual(content, expected) + + # otherwise, typogrify should be applied + content, _ = readers.read_file(_filename('article.rst'), + settings={'TYPOGRIFY': True}) + expected = "

This is some content. With some stuff to "\ + "“typogrify”.

\n" + + self.assertEqual(content, expected) From 6a4f4a55b421f71acfb915d3e3b3027b5934322f Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 02:52:40 +0100 Subject: [PATCH 10/22] updated the changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index e68c6f0d..ef9bc070 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ X.X * Refactored the way URL are handled. * Improved the english documentation * Fixed packaging using setuptools entrypoints +* Added typogrify support 2.8 From cfd050b0f29928dd31e4a994c58566f49e23d2c2 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 03:02:12 +0100 Subject: [PATCH 11/22] Add a CSS file for typogrify on the notmyidea theme --- pelican/themes/notmyidea/static/css/main.css | 1 + pelican/themes/notmyidea/static/css/typogrify.css | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 pelican/themes/notmyidea/static/css/typogrify.css diff --git a/pelican/themes/notmyidea/static/css/main.css b/pelican/themes/notmyidea/static/css/main.css index b3677771..7534790f 100644 --- a/pelican/themes/notmyidea/static/css/main.css +++ b/pelican/themes/notmyidea/static/css/main.css @@ -10,6 +10,7 @@ /* Imports */ @import url("reset.css"); @import url("pygment.css"); +@import url("typogrify.css"); @import url(http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin); /***** Global *****/ diff --git a/pelican/themes/notmyidea/static/css/typogrify.css b/pelican/themes/notmyidea/static/css/typogrify.css new file mode 100644 index 00000000..c9b34dc8 --- /dev/null +++ b/pelican/themes/notmyidea/static/css/typogrify.css @@ -0,0 +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;} +.dquo {margin-left:-.38em;} From 3c983d62c9ef426e9d08e9a7f3f05ae9090cb304 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 11:25:30 +0100 Subject: [PATCH 12/22] change the tests to use the mock library instead of a custom mocking system --- CHANGELOG | 1 + tests/test_generators.py | 28 ++++++++++------------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef9bc070..46aa68a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ X.X * Improved the english documentation * Fixed packaging using setuptools entrypoints * Added typogrify support +* Added a way to disable feed generation 2.8 diff --git a/tests/test_generators.py b/tests/test_generators.py index 94088c34..20929622 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,36 +1,28 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement try: - from unittest2 import TestCase + import unittest2 as unittest except ImportError, e: - from unittest import TestCase + import unittest # NOQA from pelican.generators import ArticlesGenerator from pelican.settings import _DEFAULT_CONFIG -class TestArticlesGenerator(TestCase): +from mock import MagicMock + + +class TestArticlesGenerator(unittest.TestCase): def test_generate_feeds(self): - class FakeWriter(object): - def __init__(self): - self.called = False - - def write_feed(self, *args, **kwargs): - self.called = True - generator = ArticlesGenerator(None, {'FEED': _DEFAULT_CONFIG['FEED']}, None, _DEFAULT_CONFIG['THEME'], None, None) - writer = FakeWriter() + writer = MagicMock() generator.generate_feeds(writer) - assert writer.called, ("The feed should be written, " - "if settings['FEED'] is specified.") + writer.write_feed.assert_called_with([], None, 'feeds/all.atom.xml') generator = ArticlesGenerator(None, {'FEED': None}, None, _DEFAULT_CONFIG['THEME'], None, None) - writer = FakeWriter() + writer = MagicMock() generator.generate_feeds(writer) - assert not writer.called, ("If settings['FEED'] is None, " - "the feed should not be generated.") - + self.assertFalse(writer.write_feed.called) From c393b011c43fb359844484b5e7dcf971e00e7226 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 15:50:53 +0100 Subject: [PATCH 13/22] cleaning --- pelican/utils.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pelican/utils.py b/pelican/utils.py index 93541cc0..eead1ac9 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -169,13 +169,11 @@ def truncate_html_words(s, num, end_text='...'): def process_translations(content_list): - """ Finds all translation and returns - tuple with two lists (index, translations). - Index list includes items in default language - or items which have no variant in default language. + """ Finds all translation and returns tuple with two lists (index, + translations). Index list includes items in default language or items + which have no variant in default language. - Also, for each content_list item, it - sets attribute 'translations' + Also, for each content_list item, it sets attribute 'translations' """ content_list.sort(key=attrgetter('slug')) grouped_by_slugs = groupby(content_list, attrgetter('slug')) @@ -185,10 +183,7 @@ def process_translations(content_list): for slug, items in grouped_by_slugs: items = list(items) # find items with default language - default_lang_items = filter( - attrgetter('in_default_lang'), - items - ) + default_lang_items = filter(attrgetter('in_default_lang'), items) len_ = len(default_lang_items) if len_ > 1: warning(u'there are %s variants of "%s"' % (len_, slug)) From 32355f546373a1dcc4faa4d426f71c4552642956 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 15:51:48 +0100 Subject: [PATCH 14/22] add some more tests for the utils module --- tests/support.py | 10 +++++- tests/test_utils.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/test_utils.py diff --git a/tests/support.py b/tests/support.py index 0cd757a6..5829fe78 100644 --- a/tests/support.py +++ b/tests/support.py @@ -1,8 +1,9 @@ from contextlib import contextmanager - from tempfile import mkdtemp from shutil import rmtree +from pelican.contents import Article + @contextmanager def temporary_folder(): @@ -16,3 +17,10 @@ def temporary_folder(): tempdir = mkdtemp() yield tempdir rmtree(tempdir) + + +def get_article(title, slug, content, lang, extra_metadata=None): + metadata = {'slug': slug, 'title': title, 'lang': lang} + if extra_metadata is not None: + metadata.update(extra_metadata) + return Article(content, metadata=metadata) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..9654825e --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +try: + import unittest2 as unittest +except ImportError: + import unittest # NOQA +import datetime + +from pelican import utils +from pelican.contents import Article + +from support import get_article + + +class TestUtils(unittest.TestCase): + + def test_get_date(self): + # valid ones + date = datetime.datetime(year=2012, month=11, day=22) + date_hour = datetime.datetime(year=2012, month=11, day=22, hour=22, + minute=11) + date_hour_sec = datetime.datetime(year=2012, month=11, day=22, hour=22, + minute=11, second=10) + dates = {'2012-11-22': date, + '2012/11/22': date, + '2012-11-22 22:11': date_hour, + '2012/11/22 22:11': date_hour, + '22-11-2012': date, + '22/11/2012': date, + '22.11.2012': date, + '2012-22-11': date, + '22.11.2012 22:11': date_hour, + '2012-11-22 22:11:10': date_hour_sec} + + for value, expected in dates.items(): + self.assertEquals(utils.get_date(value), expected, value) + + # invalid ones + invalid_dates = ('2010-110-12', 'yay') + for item in invalid_dates: + self.assertRaises(ValueError, utils.get_date, item) + + def test_slugify(self): + + samples = (('this is a test', 'this-is-a-test'), + ('this is a test', 'this-is-a-test'), + (u'this → is ← a ↑ test', 'this-is-a-test'), + ('this--is---a test', 'this-is-a-test')) + + for value, expected in samples: + self.assertEquals(utils.slugify(value), expected) + + def test_get_relative_path(self): + + samples = (('/test/test', '../../.'), + ('/test/test/', '../../../.'), + ('/', '../.')) + + for value, expected in samples: + self.assertEquals(utils.get_relative_path(value), expected) + + def test_process_translations(self): + # create a bunch of articles + fr_article1 = get_article(lang='fr', slug='yay', title='Un titre', + content='en français') + en_article1 = get_article(lang='en', slug='yay', title='A title', + content='in english') + + articles = [fr_article1, en_article1] + + index, trans = utils.process_translations(articles) + + self.assertIn(en_article1, index) + self.assertIn(fr_article1, trans) + self.assertNotIn(en_article1, trans) + self.assertNotIn(fr_article1, index) From 48b318d29e8ba9d84897997ee5d1982b574afb6a Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 17:05:46 +0100 Subject: [PATCH 15/22] skip typogrify if not installed --- tests/test_readers.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_readers.py b/tests/test_readers.py index d4f0aecf..c0b8cc41 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -26,7 +26,8 @@ class RstReaderTest(unittest.TestCase): 'category': 'yeah', 'author': u'Alexis Métaireau', 'title': 'This is a super article !', - 'summary': 'Multi-line metadata should be supported\nas well as inline markup.', + 'summary': 'Multi-line metadata should be supported\nas well as'\ + ' inline markup.', 'date': datetime.datetime(2010, 12, 2, 10, 14), 'tags': ['foo', 'bar', 'foobar'], } @@ -43,10 +44,13 @@ class RstReaderTest(unittest.TestCase): self.assertEqual(content, expected) - # otherwise, typogrify should be applied - content, _ = readers.read_file(_filename('article.rst'), - settings={'TYPOGRIFY': True}) - expected = "

This is some content. With some stuff to "\ - "“typogrify”.

\n" + try: + # otherwise, typogrify should be applied + content, _ = readers.read_file(_filename('article.rst'), + settings={'TYPOGRIFY': True}) + expected = "

This is some content. With some stuff to "\ + "“typogrify”.

\n" - self.assertEqual(content, expected) + self.assertEqual(content, expected) + except ImportError: + return unittest.skip('need the typogrify distribution') From 912b1dbc1a25a9a5079b21649a8f0aa8b3db3b68 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 17:05:59 +0100 Subject: [PATCH 16/22] test travis-ci --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..bb1f5af1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + - "2.6" + - "2.7" +install: + - pip install nose --use-mirrors + - pip install . --use-mirrors +script: nosetests -s tests From d42b6d9ad7682c55e5beae9d90f9f6607cfc9b65 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 17:20:04 +0100 Subject: [PATCH 17/22] fix nose --- tox.ini | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 462abf09..e1ca32f2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,13 @@ [tox] -envlist = py25,py26,py27 +envlist = py26,py27 [testenv] -commands=py.test +commands = nosetests -s tests deps = + nose Jinja2 Pygments docutils feedgenerator unittest2 - pytest + mock From 2827a6df47e2c9a08f286183dcb3b5323c98b1b7 Mon Sep 17 00:00:00 2001 From: draftcode Date: Mon, 12 Mar 2012 01:22:54 +0900 Subject: [PATCH 18/22] Fixed some typos. --- pelican/__init__.py | 20 ++++++++++---------- pelican/contents.py | 2 +- pelican/utils.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index dcdbdcb6..0b53dbcc 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -25,7 +25,7 @@ class Pelican(object): self.path = path or settings['PATH'] if not self.path: - raise Exception('you need to specify a path containing the content' + raise Exception('You need to specify a path containing the content' ' (see pelican --help for more information)') if self.path.endswith('/'): @@ -138,7 +138,7 @@ def main(): static blog, with restructured text input files.""") parser.add_argument(dest='path', nargs='?', - help='Path where to find the content files') + help='Path where to find the content files.') parser.add_argument('-t', '--theme-path', dest='theme', help='Path where to find the theme templates. If not specified, it' 'will use the default one included with pelican.') @@ -146,28 +146,28 @@ def main(): help='Where to output the generated files. If not specified, a ' 'directory will be created, named "output" in the current path.') parser.add_argument('-m', '--markup', default=None, dest='markup', - help='the list of markup language to use (rst or md). Please indicate ' - 'them separated by commas') + help='The list of markup language to use (rst or md). Please indicate ' + 'them separated by commas.') parser.add_argument('-s', '--settings', dest='settings', default='', - help='the settings of the application. Default to False.') + help='The settings of the application. Default to False.') parser.add_argument('-d', '--delete-output-directory', dest='delete_outputdir', action='store_true', help='Delete the output directory.') parser.add_argument('-v', '--verbose', action='store_const', const=log.INFO, dest='verbosity', - help='Show all messages') + help='Show all messages.') parser.add_argument('-q', '--quiet', action='store_const', const=log.CRITICAL, dest='verbosity', - help='Show only critical errors') + help='Show only critical errors.') parser.add_argument('-D', '--debug', action='store_const', const=log.DEBUG, dest='verbosity', - help='Show all message, including debug messages') + help='Show all message, including debug messages.') parser.add_argument('--version', action='version', version=__version__, - help='Print the pelican version and exit') + help='Print the pelican version and exit.') parser.add_argument('-r', '--autoreload', dest='autoreload', action='store_true', help="Relaunch pelican each time a modification occurs" - " on the content files") + " on the content files.") args = parser.parse_args() log.init(args.verbosity) diff --git a/pelican/contents.py b/pelican/contents.py index 900061ad..4f424461 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -43,7 +43,7 @@ class Page(object): self.author = Author(settings['AUTHOR'], settings) else: self.author = Author(getenv('USER', 'John Doe'), settings) - warning(u"Author of `{0}' unknow, assuming that his name is " + warning(u"Author of `{0}' unknown, assuming that his name is " "`{1}'".format(filename or self.title, self.author)) # manage languages diff --git a/pelican/utils.py b/pelican/utils.py index eead1ac9..1b84f108 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -14,7 +14,7 @@ from pelican.log import warning, info def get_date(string): """Return a datetime object from a string. - If no format matches the given date, raise a ValuEerror + If no format matches the given date, raise a ValueError. """ string = re.sub(' +', ' ', string) formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M', @@ -58,8 +58,8 @@ def copy(path, source, destination, destination_path=None, overwrite=False): :param source: the source dir :param destination: the destination dir :param destination_path: the destination path (optional) - :param overwrite: wether to overwrite the destination if already exists or - not + :param overwrite: whether to overwrite the destination if already exists + or not """ if not destination_path: destination_path = path From 3cb18303f6203f84b32e2f4c8bdf48430d7311ff Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 17:26:59 +0100 Subject: [PATCH 19/22] fix python 2.6 support --- tests/test_contents.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/test_contents.py b/tests/test_contents.py index e058e721..ed9885b6 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -3,18 +3,19 @@ from __future__ import with_statement try: from unittest2 import TestCase, skip except ImportError, e: - from unittest import TestCase, skip + from unittest import TestCase, skip # NOQA from pelican.contents import Page from pelican.settings import _DEFAULT_CONFIG + class TestPage(TestCase): def setUp(self): super(TestPage, self).setUp() self.page_kwargs = { 'content': 'content', - 'metadata':{ + 'metadata': { 'title': 'foo bar', 'author': 'Blogger', }, @@ -72,32 +73,38 @@ class TestPage(TestCase): """ from datetime import datetime from sys import platform - dt = datetime(2015,9,13) + dt = datetime(2015, 9, 13) # make a deep copy of page_kawgs - page_kwargs = {key:self.page_kwargs[key] for key in self.page_kwargs} + page_kwargs = dict([(key, self.page_kwargs[key]) for key in + self.page_kwargs]) for key in page_kwargs: - if not isinstance(page_kwargs[key], dict): break - page_kwargs[key] = {subkey:page_kwargs[key][subkey] for subkey in page_kwargs[key]} + if not isinstance(page_kwargs[key], dict): + break + page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey]) + for subkey in page_kwargs[key]]) # set its date to dt page_kwargs['metadata']['date'] = dt - page = Page( **page_kwargs) + page = Page(**page_kwargs) self.assertEqual(page.locale_date, - unicode(dt.strftime(_DEFAULT_CONFIG['DEFAULT_DATE_FORMAT']), 'utf-8')) + unicode(dt.strftime(_DEFAULT_CONFIG['DEFAULT_DATE_FORMAT']), + 'utf-8')) + page_kwargs['settings'] = dict([(x, _DEFAULT_CONFIG[x]) for x in + _DEFAULT_CONFIG]) - page_kwargs['settings'] = {x:_DEFAULT_CONFIG[x] for x in _DEFAULT_CONFIG} # I doubt this can work on all platforms ... if platform == "win32": locale = 'jpn' else: locale = 'ja_JP.utf8' - page_kwargs['settings']['DATE_FORMATS'] = {'jp':(locale,'%Y-%m-%d(%a)')} + page_kwargs['settings']['DATE_FORMATS'] = {'jp': (locale, + '%Y-%m-%d(%a)')} page_kwargs['metadata']['lang'] = 'jp' import locale as locale_module try: - page = Page( **page_kwargs) + page = Page(**page_kwargs) self.assertEqual(page.locale_date, u'2015-09-13(\u65e5)') # above is unicode in Japanese: 2015-09-13() except locale_module.Error: From c05b743fa64696625762a09fe2aabb05195a1dfd Mon Sep 17 00:00:00 2001 From: draftcode Date: Mon, 12 Mar 2012 01:40:27 +0900 Subject: [PATCH 20/22] Add mock to dev_requirements. --- dev_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_requirements.txt b/dev_requirements.txt index 198880ec..c7f53682 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,3 +4,4 @@ docutils feedgenerator unittest2 pytz +mock From 9cc7efbe12fea30018eafd0a92c7186304ec8600 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 18:02:57 +0100 Subject: [PATCH 21/22] add requirements to travis-ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb1f5af1..8f5dc3a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ python: - "2.6" - "2.7" install: - - pip install nose --use-mirrors + - pip install nose unittest2 mock --use-mirrors - pip install . --use-mirrors script: nosetests -s tests From e95b26bf204d684882b711ad6cc817fba16dda7a Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sun, 11 Mar 2012 18:07:08 +0100 Subject: [PATCH 22/22] Add travis-ci build-image support on the README --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 2f66e54c..5012bb9c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ Pelican ####### +.. image:: https://secure.travis-ci.org/ametaireau/pelican.png?branch=master + Pelican is a simple weblog generator, written in `Python `_. * Write your weblog entries directly with your editor of choice (vim!)