diff --git a/.travis.yml b/.travis.yml index 8f5dc3a3..823c1172 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,8 @@ install: - pip install nose unittest2 mock --use-mirrors - pip install . --use-mirrors script: nosetests -s tests +notifications: + irc: + channels: + - "irc.freenode.org#pelican" + on_success: change diff --git a/MANIFEST.in b/MANIFEST.in index fc46d905..a092ecd0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,3 @@ include *.rst global-include *.py recursive-include pelican *.html *.css *png include LICENSE -global-include *.bat diff --git a/docs/conf.py b/docs/conf.py index 4c4530e2..ac2d67ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- import sys, os +sys.path.append(os.path.abspath('..')) + +from pelican import __version__, __major__ + # -- General configuration ----------------------------------------------------- templates_path = ['_templates'] extensions = ['sphinx.ext.autodoc',] @@ -9,12 +13,11 @@ master_doc = 'index' project = u'Pelican' copyright = u'2010, Alexis Metaireau and contributors' exclude_patterns = ['_build'] -version = "2" -release = version +version = __version__ +release = __major__ # -- Options for HTML output --------------------------------------------------- -sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'pelican' diff --git a/docs/settings.rst b/docs/settings.rst index 9eb46439..b1c35122 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -46,7 +46,7 @@ Setting name (default value) What does it do? `MARKUP` (``('rst', 'md')``) A list of available markup languages you want to use. For the moment, the only available values are `rst` and `md`. -`MD_EXTENSIONS` (``('codehilite','extra')``) A list of the extensions that the Markdown processor +`MD_EXTENSIONS` (``['codehilite','extra']``) A list of the extensions that the Markdown processor will use. Refer to the extensions chapter in the Python-Markdown documentation for a complete list of supported extensions. @@ -83,7 +83,7 @@ Setting name (default value) What does it do? .. [#] Default is the system locale. -URL Settings +URL settings ------------ You can customize the URL's and locations where files will be saved. The URL's and @@ -160,6 +160,8 @@ maintain multiple languages with different date formats, you can set this dict using language name (``lang`` in your posts) as key. Regarding available format codes, see `strftime document of python`_ : +.. parsed-literal:: + DATE_FORMAT = { 'en': '%a, %d %b %Y', 'jp': '%Y-%m-%d(%a)', @@ -167,6 +169,8 @@ codes, see `strftime document of python`_ : You can set locale to further control date format: +.. parsed-literal:: + LOCALE = ('usa', 'jpn', # On Windows 'en_US', 'ja_JP' # On Unix/Linux ) @@ -175,6 +179,7 @@ Also, it is possible to set different locale settings for each language. If you put (locale, format) tuples in the dict, this will override the LOCALE setting above: +.. parsed-literal:: # On Unix/Linux DATE_FORMAT = { 'en': ('en_US','%a, %d %b %Y'), diff --git a/pelican/__init__.py b/pelican/__init__.py index 0b53dbcc..780938a7 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -11,7 +11,9 @@ from pelican.utils import clean_output_dir, files_changed from pelican.writers import Writer from pelican import log -__version__ = "3.0" +__major__ = 3 +__minor__ = 0 +__version__ = "{0}.{1}".format(__major__, __minor__) class Pelican(object): @@ -135,7 +137,8 @@ class Pelican(object): def main(): parser = argparse.ArgumentParser(description="""A tool to generate a - static blog, with restructured text input files.""") + static blog, with restructured text input files.""", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument(dest='path', nargs='?', help='Path where to find the content files.') @@ -145,11 +148,11 @@ def main(): parser.add_argument('-o', '--output', dest='output', 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', + parser.add_argument('-m', '--markup', dest='markup', 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.') + parser.add_argument('-s', '--settings', dest='settings', + help='The settings of the application.') parser.add_argument('-d', '--delete-output-directory', dest='delete_outputdir', action='store_true', help='Delete the output directory.') diff --git a/pelican/contents.py b/pelican/contents.py index 4f424461..3386dba9 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -42,9 +42,10 @@ class Page(object): if 'AUTHOR' in settings: self.author = Author(settings['AUTHOR'], settings) else: + title = filename.decode('utf-8') if filename else self.title self.author = Author(getenv('USER', 'John Doe'), settings) warning(u"Author of `{0}' unknown, assuming that his name is " - "`{1}'".format(filename or self.title, self.author)) + "`{1}'".format(title, self.author)) # manage languages self.in_default_lang = True @@ -89,9 +90,9 @@ class Page(object): if hasattr(self, 'date') and self.date > datetime.now(): self.status = 'draft' - # set summary - if not hasattr(self, 'summary'): - self.summary = truncate_html_words(self.content, 50) + # store the summary metadata if it is set + if 'summary' in metadata: + self._summary = metadata['summary'] def check_properties(self): """test that each mandatory property is set.""" @@ -126,8 +127,12 @@ class Page(object): return content def _get_summary(self): - """Returns the summary of an article, based on to the content""" - return truncate_html_words(self.content, 50) + """Returns the summary of an article, based on the summary metadata + if it is set, else troncate the content.""" + if hasattr(self, '_summary'): + return self._summary + else: + return truncate_html_words(self.content, 50) def _set_summary(self, summary): """Dummy function""" diff --git a/pelican/generators.py b/pelican/generators.py index 2987dcfe..71208430 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -227,7 +227,7 @@ class ArticlesGenerator(Generator): continue # if no category is set, use the name of the path as a category - if 'category' not in metadata.keys(): + if 'category' not in metadata: if os.path.dirname(f) == self.path: category = self.settings['DEFAULT_CATEGORY'] @@ -238,8 +238,7 @@ class ArticlesGenerator(Generator): if category != '': metadata['category'] = Category(category, self.settings) - if 'date' not in metadata.keys()\ - and self.settings['FALLBACK_ON_FS_DATE']: + if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']: metadata['date'] = datetime.datetime.fromtimestamp( os.stat(f).st_ctime) diff --git a/pelican/log.py b/pelican/log.py index 1cb76e16..8811b372 100644 --- a/pelican/log.py +++ b/pelican/log.py @@ -1,38 +1,32 @@ import os import sys -from logging import CRITICAL, ERROR, WARN, INFO, DEBUG +from logging import CRITICAL, ERROR, WARN, INFO, DEBUG from logging import critical, error, info, warning, warn, debug from logging import Formatter, getLogger, StreamHandler -RESET_TERM = u'\033[1;m' +RESET_TERM = u'\033[0;m' -def term_color(code): - return lambda text: code + unicode(text) + RESET_TERM +def start_color(index): + return u'\033[1;{0}m'.format(index) + + +def term_color(color): + code = COLOR_CODES[color] + return lambda text: start_color(code) + unicode(text) + RESET_TERM COLOR_CODES = { - 'gray': u'\033[1;30m', - 'red': u'\033[1;31m', - 'green': u'\033[1;32m', - 'yellow': u'\033[1;33m', - 'blue': u'\033[1;34m', - 'magenta': u'\033[1;35m', - 'cyan': u'\033[1;36m', - 'white': u'\033[1;37m', - 'bgred': u'\033[1;41m', - 'bggreen': u'\033[1;42m', - 'bgbrown': u'\033[1;43m', - 'bgblue': u'\033[1;44m', - 'bgmagenta': u'\033[1;45m', - 'bgcyan': u'\033[1;46m', - 'bggray': u'\033[1;47m', - 'bgyellow': u'\033[1;43m', - 'bggrey': u'\033[1;100m', + 'red': 31, + 'yellow': 33, + 'cyan': 36, + 'white': 37, + 'bgred': 41, + 'bggrey': 100, } -ANSI = dict((col, term_color(code)) for col, code in COLOR_CODES.items()) +ANSI = dict((col, term_color(col)) for col in COLOR_CODES) class ANSIFormatter(Formatter): @@ -80,7 +74,7 @@ class DummyFormatter(object): and not sys.platform.startswith('win'): return ANSIFormatter(*args, **kwargs) else: - return TextFormatter( *args, **kwargs) + return TextFormatter(*args, **kwargs) def init(level=None, logger=getLogger(), handler=StreamHandler()): diff --git a/pelican/readers.py b/pelican/readers.py index a581e458..2e269647 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -36,8 +36,8 @@ class Reader(object): self.settings = settings def process_metadata(self, name, value): - if name.lower() in _METADATA_PROCESSORS: - return _METADATA_PROCESSORS[name.lower()](value, self.settings) + if name in _METADATA_PROCESSORS: + return _METADATA_PROCESSORS[name](value, self.settings) return value @@ -71,10 +71,14 @@ class RstReader(Reader): if element.tagname == 'field': # custom fields (e.g. summary) name_elem, body_elem = element.children name = name_elem.astext() - value = render_node_to_html(document, body_elem) + if name == 'summary': + value = render_node_to_html(document, body_elem) + else: + value = body_elem.astext() else: # standard fields (e.g. address) name = element.tagname value = element.astext() + name = name.lower() output[name] = self.process_metadata(name, value) return output @@ -144,7 +148,7 @@ def read_file(filename, fmt=None, settings=None): if not fmt: fmt = filename.split('.')[-1] - if fmt not in _EXTENSIONS.keys(): + if fmt not in _EXTENSIONS: raise TypeError('Pelican does not know how to parse %s' % filename) reader = _EXTENSIONS[fmt](settings) diff --git a/pelican/themes/notmyidea/templates/archives.html b/pelican/themes/notmyidea/templates/archives.html index 5ba2c817..f7f1c400 100644 --- a/pelican/themes/notmyidea/templates/archives.html +++ b/pelican/themes/notmyidea/templates/archives.html @@ -6,7 +6,7 @@
{% for article in dates %}
{{ article.locale_date }}
-
{{ article.title }}
+
{{ article.title }}
{% endfor %}
diff --git a/pelican/themes/simple/templates/archives.html b/pelican/themes/simple/templates/archives.html index e6364efa..6c9db183 100644 --- a/pelican/themes/simple/templates/archives.html +++ b/pelican/themes/simple/templates/archives.html @@ -1,11 +1,11 @@ {% extends "base.html" %} {% block content %} -

Archives for {{ SITENAME }}

+

Archives for {{ SITENAME }}

{% for article in dates %}
{{ article.locale_date }}
-
{{ article.title }}
+
{{ article.title }}
{% endfor %}
{% endblock %} diff --git a/tools/__init__.py b/pelican/tools/__init__.py similarity index 100% rename from tools/__init__.py rename to pelican/tools/__init__.py diff --git a/tools/pelican_import.py b/pelican/tools/pelican_import.py similarity index 88% rename from tools/pelican_import.py rename to pelican/tools/pelican_import.py index b883f7fc..b34fd9a5 100755 --- a/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -2,6 +2,8 @@ import argparse import os +import subprocess +import sys import time from codecs import open @@ -38,7 +40,7 @@ def wp2fields(xml): def dc2fields(file): """Opens a Dotclear export file, and yield pelican fields""" - from BeautifulSoup import BeautifulStoneSoup, BeautifulSoup + from BeautifulSoup import BeautifulStoneSoup in_cat = False in_post = False @@ -85,10 +87,10 @@ def dc2fields(file): post_creadt = fields[6] # post_upddt = fields[7] # post_password = fields[8] - post_type = fields[9] + # post_type = fields[9] post_format = fields[10] - post_url = fields[11] - post_lang = fields[12] + # post_url = fields[11] + # post_lang = fields[12] post_title = fields[13] post_excerpt = fields[14] post_excerpt_xhtml = fields[15] @@ -216,7 +218,20 @@ def fields2pelican(fields, out_markup, output_path, dircat=False): content = content.replace("\n", "
\n") fp.write(content) - os.system('pandoc --normalize --reference-links --from=html --to=%s -o "%s" "%s"' % (out_markup, out_filename, html_filename)) + cmd = 'pandoc --normalize --reference-links --from=html --to={0} -o "{1}" "{2}"'.format( + out_markup, out_filename, html_filename) + + try: + rc = subprocess.call(cmd, shell=True) + if rc < 0: + print("Child was terminated by signal %d" % -rc) + exit() + elif rc > 0: + print("Please, check your Pandoc installation.") + exit() + except OSError, e: + print("Pandoc execution failed: %s" % e) + exit() os.remove(html_filename) @@ -234,7 +249,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False): def main(): parser = argparse.ArgumentParser( description="Transform feed, Wordpress or Dotclear files to rst files." - "Be sure to have pandoc installed") + "Be sure to have pandoc installed", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument(dest='input', help='The input file to read') parser.add_argument('--wpfile', action='store_true', dest='wpfile', @@ -259,24 +275,21 @@ def main(): elif args.feed: input_type = 'feed' else: - print("you must provide either --wpfile, --dotclear or --feed options") + print("You must provide either --wpfile, --dotclear or --feed options") exit() if not os.path.exists(args.output): try: os.mkdir(args.output) except OSError: - error("Couldn't create the output folder: " + args.output) + print("Unable to create the output folder: " + args.output) exit() - # TODO: refactor this long assignment - input_type, input, out_markup, output_path, dircat=False = input_type, args.input, args.markup, args.output, args.dircat - if input_type == 'wordpress': - fields = wp2fields(input) + fields = wp2fields(args.input) elif input_type == 'dotclear': - fields = dc2fields(input) + fields = dc2fields(args.input) elif input_type == 'feed': - fields = feed2fields(input) + fields = feed2fields(args.input) - fields2pelican(fields, out_markup, output_path, dircat=dircat) + fields2pelican(fields, args.markup, args.output, dircat=args.dircat or False) diff --git a/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py similarity index 93% rename from tools/pelican_quickstart.py rename to pelican/tools/pelican_quickstart.py index 04fc20be..4f78ac60 100755 --- a/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -1,10 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -import os, sys, argparse, string +import os +import string +import argparse + from pelican import __version__ -TEMPLATES={ +TEMPLATES = { 'Makefile' : ''' PELICAN=$pelican PELICANOPTS=$pelicanopts @@ -60,7 +63,7 @@ github: $$(OUTPUTDIR)/index.html \tgit push origin gh-pages .PHONY: html help clean ftp_upload ssh_upload dropbox_upload github - ''', +''', 'pelican.conf.py': '''#!/usr/bin/env python # -*- coding: utf-8 -*- # @@ -87,9 +90,7 @@ SOCIAL = ( ) DEFAULT_PAGINATION = $default_pagination - - - ''' +''' } CONF = { @@ -108,17 +109,6 @@ CONF = { } -class _dict(dict): - def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - - def __getitem__(self, i): - return dict.get(self,i,None) - - def has_key(k): - return True - - def ask(question, answer=str, default=None, l=None): if answer == str: r = '' @@ -193,14 +183,16 @@ def ask(question, answer=str, default=None, l=None): def main(): - parser = argparse.ArgumentParser(description="A kickstarter for pelican") + parser = argparse.ArgumentParser( + description="A kickstarter for pelican", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-p', '--path', default=".", help="The path to generate the blog into") - parser.add_argument('-t', '--title', default=None, metavar="title", + parser.add_argument('-t', '--title', metavar="title", help='Set the title of the website') - parser.add_argument('-a', '--author', default=None, metavar="author", + parser.add_argument('-a', '--author', metavar="author", help='Set the author name of the website') - parser.add_argument('-l', '--lang', default=None, metavar="lang", + parser.add_argument('-l', '--lang', metavar="lang", help='Set the default lang of the website') args = parser.parse_args() diff --git a/tools/pelican_themes.py b/pelican/tools/pelican_themes.py similarity index 100% rename from tools/pelican_themes.py rename to pelican/tools/pelican_themes.py diff --git a/pelican/utils.py b/pelican/utils.py index 1b84f108..7ffd9eb9 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -210,9 +210,6 @@ LAST_MTIME = 0 def files_changed(path, extensions): """Return True if the files have changed since the last check""" - def with_extension(f): - return any(f.endswith(ext) for ext in extensions) - def file_times(path): """Return the last time files have been modified""" for root, dirs, files in os.walk(path): diff --git a/setup.py b/setup.py index 910499de..67bbae80 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ #!/usr/bin/env python from setuptools import setup -VERSION = "3.0" # find a better way to do so. - requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz'] try: @@ -13,21 +11,21 @@ except ImportError: entry_points = { 'console_scripts': [ 'pelican = pelican:main', - 'pelican-import = tools.pelican_import:main', - 'pelican-quickstart = tools.pelican_quickstart:main', - 'pelican-themes = tools.pelican_themes:main' - ] + 'pelican-import = pelican.tools.pelican_import:main', + 'pelican-quickstart = pelican.tools.pelican_quickstart:main', + 'pelican-themes = pelican.tools.pelican_themes:main' + ] } setup( name = "pelican", - version = VERSION, + version = "3.0", url = 'http://pelican.notmyidea.org/', author = 'Alexis Metaireau', author_email = 'alexis@notmyidea.org', description = "A tool to generate a static blog from reStructuredText or Markdown input files.", long_description=open('README.rst').read(), - packages = ['pelican'], + packages = ['pelican', 'pelican.tools'], include_package_data = True, install_requires = requires, entry_points = entry_points, diff --git a/tests/content/article_with_metadata.rst b/tests/content/article_with_metadata.rst index 3410885e..d4bac1c0 100644 --- a/tests/content/article_with_metadata.rst +++ b/tests/content/article_with_metadata.rst @@ -9,3 +9,4 @@ This is a super article ! :summary: Multi-line metadata should be supported as well as **inline markup**. +:custom_field: http://notmyidea.org diff --git a/tests/content/article_with_uppercase_metadata.rst b/tests/content/article_with_uppercase_metadata.rst new file mode 100644 index 00000000..e26cdd13 --- /dev/null +++ b/tests/content/article_with_uppercase_metadata.rst @@ -0,0 +1,6 @@ + +This is a super article ! +######################### + +:Category: Yeah + diff --git a/tests/test_contents.py b/tests/test_contents.py index ed9885b6..8e1407dc 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -8,14 +8,20 @@ except ImportError, e: from pelican.contents import Page from pelican.settings import _DEFAULT_CONFIG +from jinja2.utils import generate_lorem_ipsum + +# generate one paragraph, enclosed with

+TEST_CONTENT = str(generate_lorem_ipsum(n=1)) +TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False) class TestPage(TestCase): def setUp(self): super(TestPage, self).setUp() self.page_kwargs = { - 'content': 'content', + 'content': TEST_CONTENT, 'metadata': { + 'summary': TEST_SUMMARY, 'title': 'foo bar', 'author': 'Blogger', }, @@ -27,11 +33,11 @@ class TestPage(TestCase): """ metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', } - page = Page('content', metadata=metadata) + page = Page(TEST_CONTENT, metadata=metadata) for key, value in metadata.items(): self.assertTrue(hasattr(page, key)) self.assertEqual(value, getattr(page, key)) - self.assertEqual(page.content, 'content') + self.assertEqual(page.content, TEST_CONTENT) def test_mandatory_properties(self): """If the title is not set, must throw an exception.""" @@ -39,6 +45,11 @@ class TestPage(TestCase): page = Page(**self.page_kwargs) page.check_properties() + def test_summary_from_metadata(self): + """If a :summary: metadata is given, it should be used.""" + page = Page(**self.page_kwargs) + self.assertEqual(page.summary, TEST_SUMMARY) + def test_slug(self): """If a title is given, it should be used to generate the slug.""" page = Page(**self.page_kwargs) diff --git a/tests/test_readers.py b/tests/test_readers.py index c0b8cc41..4c04a212 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -30,11 +30,20 @@ class RstReaderTest(unittest.TestCase): ' inline markup.', 'date': datetime.datetime(2010, 12, 2, 10, 14), 'tags': ['foo', 'bar', 'foobar'], + 'custom_field': 'http://notmyidea.org', } for key, value in expected.items(): self.assertEquals(value, metadata[key], key) + def test_article_metadata_key_lowercase(self): + """Keys of metadata should be lowercase.""" + reader = readers.RstReader({}) + content, metadata = reader.read(_filename('article_with_uppercase_metadata.rst')) + + self.assertIn('category', metadata, "Key should be lowercase.") + self.assertEquals('Yeah', metadata.get('category'), "Value keeps cases.") + def test_typogrify(self): # if nothing is specified in the settings, the content should be # unmodified diff --git a/tests/test_utils.py b/tests/test_utils.py index 9654825e..40f710d9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,11 +3,12 @@ try: import unittest2 as unittest except ImportError: import unittest # NOQA + +import os import datetime +import time from pelican import utils -from pelican.contents import Article - from support import get_article @@ -73,3 +74,20 @@ class TestUtils(unittest.TestCase): self.assertIn(fr_article1, trans) self.assertNotIn(en_article1, trans) self.assertNotIn(fr_article1, index) + + def test_files_changed(self): + "Test if file changes are correctly detected" + + path = os.path.join(os.path.dirname(__file__), 'content') + filename = os.path.join(path, 'article_with_metadata.rst') + changed = utils.files_changed(path, 'rst') + self.assertEquals(changed, True) + + changed = utils.files_changed(path, 'rst') + self.assertEquals(changed, False) + + t = time.time() + os.utime(filename, (t, t)) + changed = utils.files_changed(path, 'rst') + self.assertEquals(changed, True) + self.assertAlmostEqual(utils.LAST_MTIME, t, places=2)