mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Merge branch 'master' of https://github.com/ametaireau/pelican
This commit is contained in:
commit
7c31b88f8f
21 changed files with 377 additions and 150 deletions
8
.travis.yml
Normal file
8
.travis.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
install:
|
||||
- pip install nose unittest2 mock --use-mirrors
|
||||
- pip install . --use-mirrors
|
||||
script: nosetests -s tests
|
||||
|
|
@ -3,6 +3,8 @@ X.X
|
|||
* Refactored the way URL are handled.
|
||||
* Improved the english documentation
|
||||
* Fixed packaging using setuptools entrypoints
|
||||
* Added typogrify support
|
||||
* Added a way to disable feed generation
|
||||
|
||||
2.8
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <http://www.python.org/>`_.
|
||||
|
||||
* Write your weblog entries directly with your editor of choice (vim!)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ docutils
|
|||
feedgenerator
|
||||
unittest2
|
||||
pytz
|
||||
mock
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -69,6 +73,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
|
||||
<http://static.mintchaos.com/projects/typogrify/>`_
|
||||
library
|
||||
================================================ =====================================================
|
||||
|
||||
.. [#] Default is the system locale.
|
||||
|
|
@ -199,7 +208,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 +222,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
|
||||
|
|
|
|||
|
|
@ -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,52 +20,22 @@ 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'
|
||||
raise Exception('You need to specify a path containing the content'
|
||||
' (see pelican --help for more information)')
|
||||
|
||||
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"""
|
||||
|
||||
|
|
@ -129,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.')
|
||||
|
|
@ -137,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -42,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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
@ -211,10 +216,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 +321,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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
@ -37,10 +41,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,
|
||||
|
|
@ -57,11 +61,12 @@ _DEFAULT_CONFIG = {'PATH': None,
|
|||
'DEFAULT_METADATA': (),
|
||||
'FILES_TO_COPY': (),
|
||||
'DEFAULT_STATUS': 'published',
|
||||
'ARTICLE_PERMALINK_STRUCTURE': ''
|
||||
'ARTICLE_PERMALINK_STRUCTURE': '',
|
||||
'TYPOGRIFY': False,
|
||||
}
|
||||
|
||||
|
||||
def read_settings(filename):
|
||||
def read_settings(filename=None):
|
||||
"""Load a Python file into a dictionary.
|
||||
"""
|
||||
context = _DEFAULT_CONFIG.copy()
|
||||
|
|
|
|||
|
|
@ -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 *****/
|
||||
|
|
|
|||
3
pelican/themes/notmyidea/static/css/typogrify.css
Normal file
3
pelican/themes/notmyidea/static/css/typogrify.css
Normal file
|
|
@ -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;}
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
4
tests/content/article.rst
Normal file
4
tests/content/article.rst
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Article title
|
||||
#############
|
||||
|
||||
This is some content. With some stuff to "typogrify".
|
||||
26
tests/support.py
Normal file
26
tests/support.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from contextlib import contextmanager
|
||||
from tempfile import mkdtemp
|
||||
from shutil import rmtree
|
||||
|
||||
from pelican.contents import Article
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
28
tests/test_generators.py
Normal file
28
tests/test_generators.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
try:
|
||||
import unittest2 as unittest
|
||||
except ImportError, e:
|
||||
import unittest # NOQA
|
||||
|
||||
from pelican.generators import ArticlesGenerator
|
||||
from pelican.settings import _DEFAULT_CONFIG
|
||||
|
||||
from mock import MagicMock
|
||||
|
||||
|
||||
class TestArticlesGenerator(unittest.TestCase):
|
||||
|
||||
def test_generate_feeds(self):
|
||||
|
||||
generator = ArticlesGenerator(None, {'FEED': _DEFAULT_CONFIG['FEED']},
|
||||
None, _DEFAULT_CONFIG['THEME'], None,
|
||||
None)
|
||||
writer = MagicMock()
|
||||
generator.generate_feeds(writer)
|
||||
writer.write_feed.assert_called_with([], None, 'feeds/all.atom.xml')
|
||||
|
||||
generator = ArticlesGenerator(None, {'FEED': None}, None,
|
||||
_DEFAULT_CONFIG['THEME'], None, None)
|
||||
writer = MagicMock()
|
||||
generator.generate_feeds(writer)
|
||||
self.assertFalse(writer.write_feed.called)
|
||||
31
tests/test_pelican.py
Normal file
31
tests/test_pelican.py
Normal file
|
|
@ -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))
|
||||
|
|
@ -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({})
|
||||
|
|
@ -25,8 +26,31 @@ class RstReaderTest(unittest2.TestCase):
|
|||
'category': 'yeah',
|
||||
'author': u'Alexis Métaireau',
|
||||
'title': 'This is a super article !',
|
||||
'summary': 'Multi-line metadata should be supported\nas well as <strong>inline markup</strong>.',
|
||||
'summary': 'Multi-line metadata should be supported\nas well as'\
|
||||
' <strong>inline markup</strong>.',
|
||||
'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)
|
||||
|
||||
def test_typogrify(self):
|
||||
# if nothing is specified in the settings, the content should be
|
||||
# unmodified
|
||||
content, _ = readers.read_file(_filename('article.rst'))
|
||||
expected = "<p>This is some content. With some stuff to "\
|
||||
""typogrify".</p>\n"
|
||||
|
||||
self.assertEqual(content, expected)
|
||||
|
||||
try:
|
||||
# otherwise, typogrify should be applied
|
||||
content, _ = readers.read_file(_filename('article.rst'),
|
||||
settings={'TYPOGRIFY': True})
|
||||
expected = "<p>This is some content. With some stuff to "\
|
||||
"“typogrify”.</p>\n"
|
||||
|
||||
self.assertEqual(content, expected)
|
||||
except ImportError:
|
||||
return unittest.skip('need the typogrify distribution')
|
||||
|
|
|
|||
75
tests/test_utils.py
Normal file
75
tests/test_utils.py
Normal file
|
|
@ -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)
|
||||
7
tox.ini
7
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue