Skip to content
This repository
Browse code

Port pelican to python 3.

Stays compatible with 2.x series, thanks to an unified codebase.
  • Loading branch information...
commit 71995d5e1bec6f34ef836e1dc7416e27dd3cfe30 1 parent 9847394
Dirk Makowski dmdm authored ametaireau committed

Showing 43 changed files with 495 additions and 287 deletions. Show diff stats Hide diff stats

  1. +2 0  .gitignore
  2. +3 1 .travis.yml
  3. +4 3 dev_requirements.txt
  4. +1 1  docs/changelog.rst
  5. +11 10 docs/conf.py
  6. +9 5 pelican/__init__.py
  7. +20 25 pelican/contents.py
  8. +18 18 pelican/generators.py
  9. +20 16 pelican/log.py
  10. +4 1 pelican/paginator.py
  11. +1 0  pelican/plugins/assets.py
  12. +1 0  pelican/plugins/github_activity.py
  13. +1 1  pelican/plugins/gzip_cache.py
  14. +3 2 pelican/plugins/html_rst_directive.py
  15. +3 1 pelican/plugins/initialized.py
  16. +2 2 pelican/plugins/related_posts.py
  17. +9 6 pelican/plugins/sitemap.py
  18. +7 3 pelican/readers.py
  19. +3 1 pelican/rstdirectives.py
  20. +20 0 pelican/server.py
  21. +9 7 pelican/settings.py
  22. +2 0  pelican/signals.py
  23. +35 16 pelican/tools/pelican_import.py
  24. +48 34 pelican/tools/pelican_quickstart.py
  25. +10 7 pelican/tools/pelican_themes.py
  26. +1 1  pelican/tools/templates/Makefile.in
  27. +6 6 pelican/tools/templates/develop_server.sh.in
  28. +101 19 pelican/utils.py
  29. +10 9 pelican/writers.py
  30. +6 4 samples/pelican.conf.py
  31. +2 2 setup.py
  32. +4 3 tests/default_conf.py
  33. +4 2 tests/support.py
  34. +3 3 tests/test_contents.py
  35. +22 21 tests/test_generators.py
  36. +2 21 tests/test_importer.py
  37. +4 2 tests/test_pelican.py
  38. +1 1  tests/test_plugins.py
  39. +13 26 tests/test_readers.py
  40. +3 1 tests/test_settings.py
  41. +4 3 tests/test_utils.py
  42. +1 0  tests/test_webassets.py
  43. +62 3 tox.ini
2  .gitignore
@@ -11,3 +11,5 @@ tags
11 11 .tox
12 12 .coverage
13 13 htmlcov
  14 +six-*.egg/
  15 +*.orig
4 .travis.yml
@@ -2,11 +2,13 @@ language: python
2 2 python:
3 3 - "2.6"
4 4 - "2.7"
  5 +# - "3.2"
5 6 before_install:
6 7 - sudo apt-get update -qq
7 8 - sudo apt-get install -qq ruby-sass
8 9 install:
9   - - pip install nose unittest2 mock --use-mirrors
  10 + - pip install nose mock --use-mirrors
  11 + - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --use-mirrors unittest2py3k; else pip install --use-mirrors unittest2; fi
10 12 - pip install . --use-mirrors
11 13 - pip install Markdown
12 14 - pip install webassets
7 dev_requirements.txt
... ... @@ -1,8 +1,9 @@
1 1 # Tests
2   -unittest2
3 2 mock
  3 +
4 4 # Optional Packages
5 5 Markdown
6   -BeautifulSoup
  6 +BeautifulSoup4
  7 +lxml
7 8 typogrify
8   -webassets
  9 +webassets
2  docs/changelog.rst
Source Rendered
@@ -4,7 +4,7 @@ Release history
4 4 3.2 (XXXX-XX-XX)
5 5 ================
6 6
7   -* [...]
  7 +* Support for Python 3!
8 8
9 9 3.1 (2012-12-04)
10 10 ================
21 docs/conf.py
... ... @@ -1,4 +1,5 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals
2 3 import sys, os
3 4
4 5 sys.path.append(os.path.abspath('..'))
@@ -10,8 +11,8 @@
10 11 extensions = ['sphinx.ext.autodoc',]
11 12 source_suffix = '.rst'
12 13 master_doc = 'index'
13   -project = u'Pelican'
14   -copyright = u'2010, Alexis Metaireau and contributors'
  14 +project = 'Pelican'
  15 +copyright = '2010, Alexis Metaireau and contributors'
15 16 exclude_patterns = ['_build']
16 17 version = __version__
17 18 release = __major__
@@ -34,16 +35,16 @@
34 35
35 36 # -- Options for LaTeX output --------------------------------------------------
36 37 latex_documents = [
37   - ('index', 'Pelican.tex', u'Pelican Documentation',
38   - u'Alexis Métaireau', 'manual'),
  38 + ('index', 'Pelican.tex', 'Pelican Documentation',
  39 + 'Alexis Métaireau', 'manual'),
39 40 ]
40 41
41 42 # -- Options for manual page output --------------------------------------------
42 43 man_pages = [
43   - ('index', 'pelican', u'pelican documentation',
44   - [u'Alexis Métaireau'], 1),
45   - ('pelican-themes', 'pelican-themes', u'A theme manager for Pelican',
46   - [u'Mickaël Raybaud'], 1),
47   - ('themes', 'pelican-theming', u'How to create themes for Pelican',
48   - [u'The Pelican contributors'], 1)
  44 + ('index', 'pelican', 'pelican documentation',
  45 + ['Alexis Métaireau'], 1),
  46 + ('pelican-themes', 'pelican-themes', 'A theme manager for Pelican',
  47 + ['Mickaël Raybaud'], 1),
  48 + ('themes', 'pelican-theming', 'How to create themes for Pelican',
  49 + ['The Pelican contributors'], 1)
49 50 ]
14 pelican/__init__.py
... ... @@ -1,3 +1,7 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +import six
  4 +
1 5 import os
2 6 import re
3 7 import sys
@@ -55,7 +59,7 @@ def init_plugins(self):
55 59 self.plugins = self.settings['PLUGINS']
56 60 for plugin in self.plugins:
57 61 # if it's a string, then import it
58   - if isinstance(plugin, basestring):
  62 + if isinstance(plugin, six.string_types):
59 63 logger.debug("Loading plugin `{0}' ...".format(plugin))
60 64 plugin = __import__(plugin, globals(), locals(), 'module')
61 65
@@ -265,7 +269,7 @@ def get_instance(args):
265 269 settings = read_settings(args.settings, override=get_config(args))
266 270
267 271 cls = settings.get('PELICAN_CLASS')
268   - if isinstance(cls, basestring):
  272 + if isinstance(cls, six.string_types):
269 273 module, cls_name = cls.rsplit('.', 1)
270 274 module = __import__(module)
271 275 cls = getattr(module, cls_name)
@@ -311,15 +315,15 @@ def main():
311 315 "Nothing to generate.")
312 316 files_found_error = False
313 317 time.sleep(1) # sleep to avoid cpu load
314   - except Exception, e:
  318 + except Exception as e:
315 319 logger.warning(
316 320 "Caught exception \"{}\". Reloading.".format(e)
317 321 )
318 322 continue
319 323 else:
320 324 pelican.run()
321   - except Exception, e:
322   - logger.critical(unicode(e))
  325 + except Exception as e:
  326 + logger.critical(e)
323 327
324 328 if (args.verbosity == logging.DEBUG):
325 329 raise
45 pelican/contents.py
... ... @@ -1,4 +1,7 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +import six
  4 +
2 5 import copy
3 6 import locale
4 7 import logging
@@ -11,8 +14,10 @@
11 14
12 15
13 16 from pelican.settings import _DEFAULT_CONFIG
14   -from pelican.utils import slugify, truncate_html_words, memoized
  17 +from pelican.utils import (slugify, truncate_html_words, memoized,
  18 + python_2_unicode_compatible)
15 19 from pelican import signals
  20 +import pelican.utils
16 21
17 22 logger = logging.getLogger(__name__)
18 23
@@ -85,13 +90,8 @@ def __init__(self, content, metadata=None, settings=None,
85 90 self.date_format = self.date_format[1]
86 91
87 92 if hasattr(self, 'date'):
88   - encoded_date = self.date.strftime(
89   - self.date_format.encode('ascii', 'xmlcharrefreplace'))
90   -
91   - if platform == 'win32':
92   - self.locale_date = encoded_date.decode(stdin.encoding)
93   - else:
94   - self.locale_date = encoded_date.decode('utf')
  93 + self.locale_date = pelican.utils.strftime(self.date,
  94 + self.date_format)
95 95
96 96 # manage status
97 97 if not hasattr(self, 'status'):
@@ -167,7 +167,7 @@ def replacer(m):
167 167 origin = '/'.join((siteurl,
168 168 self._context['filenames'][value].url))
169 169 else:
170   - logger.warning(u"Unable to find {fn}, skipping url"
  170 + logger.warning("Unable to find {fn}, skipping url"
171 171 " replacement".format(fn=value))
172 172
173 173 return m.group('markup') + m.group('quote') + origin \
@@ -243,10 +243,10 @@ class Article(Page):
243 243 class Quote(Page):
244 244 base_properties = ('author', 'date')
245 245
246   -
  246 +@python_2_unicode_compatible
247 247 class URLWrapper(object):
248 248 def __init__(self, name, settings):
249   - self.name = unicode(name)
  249 + self.name = name
250 250 self.slug = slugify(self.name)
251 251 self.settings = settings
252 252
@@ -257,12 +257,9 @@ def __hash__(self):
257 257 return hash(self.name)
258 258
259 259 def __eq__(self, other):
260   - return self.name == unicode(other)
  260 + return self.name == other
261 261
262 262 def __str__(self):
263   - return str(self.name.encode('utf-8', 'replace'))
264   -
265   - def __unicode__(self):
266 263 return self.name
267 264
268 265 def _from_settings(self, key, get_page_name=False):
@@ -272,14 +269,14 @@ def _from_settings(self, key, get_page_name=False):
272 269 Useful for pagination."""
273 270 setting = "%s_%s" % (self.__class__.__name__.upper(), key)
274 271 value = self.settings[setting]
275   - if not isinstance(value, basestring):
276   - logger.warning(u'%s is set to %s' % (setting, value))
  272 + if not isinstance(value, six.string_types):
  273 + logger.warning('%s is set to %s' % (setting, value))
277 274 return value
278 275 else:
279 276 if get_page_name:
280   - return unicode(os.path.splitext(value)[0]).format(**self.as_dict())
  277 + return os.path.splitext(value)[0].format(**self.as_dict())
281 278 else:
282   - return unicode(value).format(**self.as_dict())
  279 + return value.format(**self.as_dict())
283 280
284 281 page_name = property(functools.partial(_from_settings, key='URL', get_page_name=True))
285 282 url = property(functools.partial(_from_settings, key='URL'))
@@ -292,13 +289,14 @@ class Category(URLWrapper):
292 289
293 290 class Tag(URLWrapper):
294 291 def __init__(self, name, *args, **kwargs):
295   - super(Tag, self).__init__(unicode.strip(name), *args, **kwargs)
  292 + super(Tag, self).__init__(name.strip(), *args, **kwargs)
296 293
297 294
298 295 class Author(URLWrapper):
299 296 pass
300 297
301 298
  299 +@python_2_unicode_compatible
302 300 class StaticContent(object):
303 301 def __init__(self, src, dst=None, settings=None):
304 302 if not settings:
@@ -309,9 +307,6 @@ def __init__(self, src, dst=None, settings=None):
309 307 self.save_as = os.path.join(settings['OUTPUT_PATH'], self.url)
310 308
311 309 def __str__(self):
312   - return str(self.filepath.encode('utf-8', 'replace'))
313   -
314   - def __unicode__(self):
315 310 return self.filepath
316 311
317 312
@@ -319,7 +314,7 @@ def is_valid_content(content, f):
319 314 try:
320 315 content.check_properties()
321 316 return True
322   - except NameError, e:
323   - logger.error(u"Skipping %s: impossible to find informations about"
  317 + except NameError as e:
  318 + logger.error("Skipping %s: impossible to find informations about"
324 319 "'%s'" % (f, e))
325 320 return False
36 pelican/generators.py
... ... @@ -1,10 +1,11 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +
2 4 import os
3 5 import math
4 6 import random
5 7 import logging
6 8 import datetime
7   -import subprocess
8 9 import shutil
9 10
10 11 from codecs import open
@@ -119,7 +120,7 @@ def _update_context(self, items):
119 120 for item in items:
120 121 value = getattr(self, item)
121 122 if hasattr(value, 'items'):
122   - value = value.items()
  123 + value = list(value.items())
123 124 self.context[item] = value
124 125
125 126
@@ -133,8 +134,8 @@ def get_source(self, environment, template):
133 134 if template != self.path or not os.path.exists(self.fullpath):
134 135 raise TemplateNotFound(template)
135 136 mtime = os.path.getmtime(self.fullpath)
136   - with file(self.fullpath) as f:
137   - source = f.read().decode('utf-8')
  137 + with open(self.fullpath, 'r', encoding='utf-8') as f:
  138 + source = f.read()
138 139 return source, self.fullpath, \
139 140 lambda: mtime == os.path.getmtime(self.fullpath)
140 141
@@ -323,8 +324,8 @@ def generate_context(self):
323 324 try:
324 325 signals.article_generate_preread.send(self)
325 326 content, metadata = read_file(f, settings=self.settings)
326   - except Exception, e:
327   - logger.warning(u'Could not process %s\n%s' % (f, str(e)))
  327 + except Exception as e:
  328 + logger.warning('Could not process %s\n%s' % (f, str(e)))
328 329 continue
329 330
330 331 # if no category is set, use the name of the path as a category
@@ -333,8 +334,7 @@ def generate_context(self):
333 334 if (self.settings['USE_FOLDER_AS_CATEGORY']
334 335 and os.path.dirname(f) != article_path):
335 336 # if the article is in a subdirectory
336   - category = os.path.basename(os.path.dirname(f))\
337   - .decode('utf-8')
  337 + category = os.path.basename(os.path.dirname(f))
338 338 else:
339 339 # if the article is not in a subdirectory
340 340 category = self.settings['DEFAULT_CATEGORY']
@@ -366,8 +366,8 @@ def generate_context(self):
366 366 elif article.status == "draft":
367 367 self.drafts.append(article)
368 368 else:
369   - logger.warning(u"Unknown status %s for file %s, skipping it." %
370   - (repr(unicode.encode(article.status, 'utf-8')),
  369 + logger.warning("Unknown status %s for file %s, skipping it." %
  370 + (repr(article.status),
371 371 repr(f)))
372 372
373 373 self.articles, self.translations = process_translations(all_articles)
@@ -394,7 +394,7 @@ def generate_context(self):
394 394 tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
395 395 tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
396 396
397   - tags = map(itemgetter(1), tag_cloud)
  397 + tags = list(map(itemgetter(1), tag_cloud))
398 398 if tags:
399 399 max_count = max(tags)
400 400 steps = self.settings.get('TAG_CLOUD_STEPS')
@@ -450,8 +450,8 @@ def generate_context(self):
450 450 exclude=self.settings['PAGE_EXCLUDES']):
451 451 try:
452 452 content, metadata = read_file(f, settings=self.settings)
453   - except Exception, e:
454   - logger.warning(u'Could not process %s\n%s' % (f, str(e)))
  453 + except Exception as e:
  454 + logger.warning('Could not process %s\n%s' % (f, str(e)))
455 455 continue
456 456 signals.pages_generate_context.send(self, metadata=metadata)
457 457 page = Page(content, metadata, settings=self.settings,
@@ -466,8 +466,8 @@ def generate_context(self):
466 466 elif page.status == "hidden":
467 467 hidden_pages.append(page)
468 468 else:
469   - logger.warning(u"Unknown status %s for file %s, skipping it." %
470   - (repr(unicode.encode(page.status, 'utf-8')),
  469 + logger.warning("Unknown status %s for file %s, skipping it." %
  470 + (repr(page.status),
471 471 repr(f)))
472 472
473 473 self.pages, self.translations = process_translations(all_pages)
@@ -550,7 +550,7 @@ def _create_pdf(self, obj, output_path):
550 550 # print "Generating pdf for", obj.filename, " in ", output_pdf
551 551 with open(obj.filename) as f:
552 552 self.pdfcreator.createPdf(text=f.read(), output=output_pdf)
553   - logger.info(u' [ok] writing %s' % output_pdf)
  553 + logger.info(' [ok] writing %s' % output_pdf)
554 554
555 555 def generate_context(self):
556 556 pass
@@ -558,7 +558,7 @@ def generate_context(self):
558 558 def generate_output(self, writer=None):
559 559 # we don't use the writer passed as argument here
560 560 # since we write our own files
561   - logger.info(u' Generating PDF files...')
  561 + logger.info(' Generating PDF files...')
562 562 pdf_path = os.path.join(self.output_path, 'pdf')
563 563 if not os.path.exists(pdf_path):
564 564 try:
@@ -583,6 +583,6 @@ def _create_source(self, obj, output_path):
583 583 copy('', obj.filename, dest)
584 584
585 585 def generate_output(self, writer=None):
586   - logger.info(u' Generating source files...')
  586 + logger.info(' Generating source files...')
587 587 for object in chain(self.context['articles'], self.context['pages']):
588 588 self._create_source(object, self.output_path)
36 pelican/log.py
... ... @@ -1,3 +1,6 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +
1 4 __all__ = [
2 5 'init'
3 6 ]
@@ -9,7 +12,7 @@
9 12 from logging import Formatter, getLogger, StreamHandler, DEBUG
10 13
11 14
12   -RESET_TERM = u'\033[0;m'
  15 +RESET_TERM = '\033[0;m'
13 16
14 17 COLOR_CODES = {
15 18 'red': 31,
@@ -24,37 +27,38 @@
24 27 def ansi(color, text):
25 28 """Wrap text in an ansi escape sequence"""
26 29 code = COLOR_CODES[color]
27   - return u'\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
  30 + return '\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
28 31
29 32
30 33 class ANSIFormatter(Formatter):
31 34 """
32   - Convert a `logging.LogReport' object into colored text, using ANSI escape sequences.
  35 + Convert a `logging.LogRecord' object into colored text, using ANSI escape sequences.
33 36 """
34 37 ## colors:
35 38
36 39 def format(self, record):
37   - if record.levelname is 'INFO':
38   - return ansi('cyan', '-> ') + unicode(record.msg)
39   - elif record.levelname is 'WARNING':
40   - return ansi('yellow', record.levelname) + ': ' + unicode(record.msg)
41   - elif record.levelname is 'ERROR':
42   - return ansi('red', record.levelname) + ': ' + unicode(record.msg)
43   - elif record.levelname is 'CRITICAL':
44   - return ansi('bgred', record.levelname) + ': ' + unicode(record.msg)
45   - elif record.levelname is 'DEBUG':
46   - return ansi('bggrey', record.levelname) + ': ' + unicode(record.msg)
  40 + msg = str(record.msg)
  41 + if record.levelname == 'INFO':
  42 + return ansi('cyan', '-> ') + msg
  43 + elif record.levelname == 'WARNING':
  44 + return ansi('yellow', record.levelname) + ': ' + msg
  45 + elif record.levelname == 'ERROR':
  46 + return ansi('red', record.levelname) + ': ' + msg
  47 + elif record.levelname == 'CRITICAL':
  48 + return ansi('bgred', record.levelname) + ': ' + msg
  49 + elif record.levelname == 'DEBUG':
  50 + return ansi('bggrey', record.levelname) + ': ' + msg
47 51 else:
48   - return ansi('white', record.levelname) + ': ' + unicode(record.msg)
  52 + return ansi('white', record.levelname) + ': ' + msg
49 53
50 54
51 55 class TextFormatter(Formatter):
52 56 """
53   - Convert a `logging.LogReport' object into text.
  57 + Convert a `logging.LogRecord' object into text.
54 58 """
55 59
56 60 def format(self, record):
57   - if not record.levelname or record.levelname is 'INFO':
  61 + if not record.levelname or record.levelname == 'INFO':
58 62 return record.msg
59 63 else:
60 64 return record.levelname + ': ' + record.msg
5 pelican/paginator.py
... ... @@ -1,3 +1,6 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +
1 4 # From django.core.paginator
2 5 from math import ceil
3 6
@@ -37,7 +40,7 @@ def _get_page_range(self):
37 40 Returns a 1-based range of pages for iterating through within
38 41 a template for loop.
39 42 """
40   - return range(1, self.num_pages + 1)
  43 + return list(range(1, self.num_pages + 1))
41 44 page_range = property(_get_page_range)
42 45
43 46
1  pelican/plugins/assets.py
... ... @@ -1,4 +1,5 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals
2 3 """
3 4 Asset management plugin for Pelican
4 5 ===================================
1  pelican/plugins/github_activity.py
... ... @@ -1,4 +1,5 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
2 3 """
3 4 Copyright (c) Marco Milanesi <kpanic@gnufunk.org>
4 5
2  pelican/plugins/gzip_cache.py
@@ -68,7 +68,7 @@ def create_gzip_file(filepath):
68 68 logger.debug('Compressing: %s' % filepath)
69 69 compressed = gzip.open(compressed_path, 'wb')
70 70 compressed.writelines(uncompressed)
71   - except Exception, ex:
  71 + except Exception as ex:
72 72 logger.critical('Gzip compression failed: %s' % ex)
73 73 finally:
74 74 compressed.close()
5 pelican/plugins/html_rst_directive.py
... ... @@ -1,6 +1,7 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals
1 3 from docutils import nodes
2 4 from docutils.parsers.rst import directives, Directive
3   -from pelican import log
4 5
5 6 """
6 7 HTML tags for reStructuredText
@@ -52,7 +53,7 @@ class RawHtml(Directive):
52 53 has_content = True
53 54
54 55 def run(self):
55   - html = u' '.join(self.content)
  56 + html = ' '.join(self.content)
56 57 node = nodes.raw('', html, format='html')
57 58 return [node]
58 59
4 pelican/plugins/initialized.py
... ... @@ -1,7 +1,9 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
1 3 from pelican import signals
2 4
3 5 def test(sender):
4   - print "%s initialized !!" % sender
  6 + print("%s initialized !!" % sender)
5 7
6 8 def register():
7 9 signals.initialized.connect(test)
4 pelican/plugins/related_posts.py
@@ -41,8 +41,8 @@ def add_related_posts(generator, metadata):
15 pelican/plugins/sitemap.py
... ... @@ -1,3 +1,6 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals
  3 +
1 4 import collections
2 5 import os.path
3 6
@@ -7,19 +10,19 @@
7 10
8 11 from pelican import signals, contents
9 12
10   -TXT_HEADER = u"""{0}/index.html
  13 +TXT_HEADER = """{0}/index.html
11 14 {0}/archives.html
12 15 {0}/tags.html
13 16 {0}/categories.html
14 17 """
15 18
16   -XML_HEADER = u"""<?xml version="1.0" encoding="utf-8"?>
  19 +XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>
17 20 <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18 21 xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
19 22 xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
20 23 """
21 24
22   -XML_URL = u"""
  25 +XML_URL = """
23 26 <url>
24 27 <loc>{0}/{1}</loc>
25 28 <lastmod>{2}</lastmod>
@@ -28,7 +31,7 @@
28 31 </url>
29 32 """
30 33
31   -XML_FOOTER = u"""
  34 +XML_FOOTER = """
32 35 </urlset>
33 36 """
34 37
@@ -86,7 +89,7 @@ def __init__(self, context, settings, path, theme, output_path, *null):
86 89 'yearly', 'never')
87 90
88 91 if isinstance(pris, dict):
89   - for k, v in pris.iteritems():
  92 + for k, v in pris.items():
90 93 if k in valid_keys and not isinstance(v, (int, float)):
91 94 default = self.priorities[k]
92 95 warning("sitemap plugin: priorities must be numbers")
@@ -99,7 +102,7 @@ def __init__(self, context, settings, path, theme, output_path, *null):
99 102 warning("sitemap plugin: using the default values")
100 103
101 104 if isinstance(chfreqs, dict):
102   - for k, v in chfreqs.iteritems():
  105 + for k, v in chfreqs.items():
103 106 if k in valid_keys and v not in valid_chfreqs:
104 107 default = self.changefreqs[k]
105 108 warning("sitemap plugin: invalid changefreq `{0}'".format(v))
10 pelican/readers.py
... ... @@ -1,4 +1,7 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +import six
  4 +
2 5 import os
3 6 import re
4 7 try:
@@ -20,15 +23,16 @@
20 23 asciidoc = True
21 24 except ImportError:
22 25 asciidoc = False
  26 +import re
23 27
24 28 from pelican.contents import Category, Tag, Author
25 29 from pelican.utils import get_date, pelican_open
26 30
27 31
28 32 _METADATA_PROCESSORS = {
29   - 'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')],
  33 + 'tags': lambda x, y: [Tag(tag, y) for tag in x.split(',')],
30 34 'date': lambda x, y: get_date(x),
31   - 'status': lambda x, y: unicode.strip(x),
  35 + 'status': lambda x, y: x.strip(),
32 36 'category': Category,
33 37 'author': Author,
34 38 }
@@ -242,7 +246,7 @@ def read_file(filename, fmt=None, settings=None):
242 246 if filename_metadata:
243 247 match = re.match(filename_metadata, base)
244 248 if match:
245   - for k, v in match.groupdict().iteritems():
  249 + for k, v in match.groupdict().items():
246 250 if k not in metadata:
247 251 k = k.lower() # metadata must be lowercase
248 252 metadata[k] = reader.process_metadata(k, v)
4 pelican/rstdirectives.py
... ... @@ -1,4 +1,6 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +
2 4 from docutils import nodes, utils
3 5 from docutils.parsers.rst import directives, roles, Directive
4 6 from pygments.formatters import HtmlFormatter
@@ -32,7 +34,7 @@ def run(self):
32 34 # take an arbitrary option if more than one is given
33 35 formatter = self.options and VARIANTS[self.options.keys()[0]] \
34 36 or DEFAULT
35   - parsed = highlight(u'\n'.join(self.content), lexer, formatter)
  37 + parsed = highlight('\n'.join(self.content), lexer, formatter)
36 38 return [nodes.raw('', parsed, format='html')]
37 39
38 40 directives.register_directive('code-block', Pygments)
20 pelican/server.py
... ... @@ -0,0 +1,20 @@
  1 +from __future__ import print_function
  2 +try:
  3 + import SimpleHTTPServer as srvmod
  4 +except ImportError:
  5 + import http.server as srvmod
  6 +
  7 +try:
  8 + import SocketServer as socketserver
  9 +except ImportError:
  10 + import socketserver
  11 +
  12 +PORT = 8000
  13 +
  14 +Handler = srvmod.SimpleHTTPRequestHandler
  15 +
  16 +httpd = socketserver.TCPServer(("", PORT), Handler)
  17 +
  18 +print("serving at port", PORT)
  19 +httpd.serve_forever()
  20 +
16 pelican/settings.py
... ... @@ -1,11 +1,13 @@
1 1 # -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
  3 +import six
  4 +
2 5 import copy
3 6 import imp
4 7 import inspect
5 8 import os
6 9 import locale
7 10 import logging
8   -import re
9 11
10 12 from os.path import isabs
11 13
@@ -54,8 +56,8 @@
54 56 'CATEGORY_SAVE_AS': 'category/{slug}.html',
55 57 'TAG_URL': 'tag/{slug}.html',
56 58 'TAG_SAVE_AS': 'tag/{slug}.html',
57   - 'AUTHOR_URL': u'author/{slug}.html',
58   - 'AUTHOR_SAVE_AS': u'author/{slug}.html',
  59 + 'AUTHOR_URL': 'author/{slug}.html',
  60 + 'AUTHOR_SAVE_AS': 'author/{slug}.html',
59 61 'RELATIVE_URLS': True,
60 62 'DEFAULT_LANG': 'en',
61 63 'TAG_CLOUD_STEPS': 4,
@@ -146,7 +148,7 @@ def configure_settings(settings):
146 148 # if locales is not a list, make it one
147 149 locales = settings['LOCALE']
148 150
149   - if isinstance(locales, basestring):
  151 + if isinstance(locales, six.string_types):
150 152 locales = [locales]
151 153
152 154 # try to set the different locales, fallback on the default.
@@ -155,7 +157,7 @@ def configure_settings(settings):
155 157
156 158 for locale_ in locales:
157 159 try:
158   - locale.setlocale(locale.LC_ALL, locale_)
  160 + locale.setlocale(locale.LC_ALL, str(locale_))
159 161 break # break if it is successful
160 162 except locale.Error:
161 163 pass
@@ -200,14 +202,14 @@ def configure_settings(settings):
200 202 "of the Webassets plugin")
201 203
202 204 if 'OUTPUT_SOURCES_EXTENSION' in settings:
203   - if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], str):
  205 + if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], six.string_types):
204 206 settings['OUTPUT_SOURCES_EXTENSION'] = _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION']
205 207 logger.warn("Detected misconfiguration with OUTPUT_SOURCES_EXTENSION."
206 208 " falling back to the default extension " +
207 209 _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION'])
208 210
209 211 filename_metadata = settings.get('FILENAME_METADATA')
210   - if filename_metadata and not isinstance(filename_metadata, basestring):
  212 + if filename_metadata and not isinstance(filename_metadata, six.string_types):
211 213 logger.error("Detected misconfiguration with FILENAME_METADATA"
212 214 " setting (must be string or compiled pattern), falling"
213 215 "back to the default")
2  pelican/signals.py
... ... @@ -1,3 +1,5 @@
  1 +# -*- coding: utf-8 -*-
  2 +from __future__ import unicode_literals, print_function
1 3 from blinker import signal
2 4
3 5 initialized = signal('pelican_initialized')
51 pelican/tools/pelican_import.py
... ... @@ -1,7 +1,12 @@
1 1 #!/usr/bin/env python
2 2
  3 +# -*- coding: utf-8 -*-
  4 +from __future__ import unicode_literals, print_function
3 5 import argparse
4   -from HTMLParser import HTMLParser
  6 +try:
  7 + from html.parser import HTMLParser
  8 +except ImportError:
  9 + from HTMLParser import HTMLParser
5 10 import os
6 11 import subprocess
7 12 import sys
@@ -15,14 +20,14 @@
15 20 def wp2fields(xml):
16 21 """Opens a wordpress XML file, and yield pelican fields"""
17 22 try:
18   - from BeautifulSoup import BeautifulStoneSoup
  23 + from bs4 import BeautifulSoup
19 24 except ImportError:
20 25 error = ('Missing dependency '
21   - '"BeautifulSoup" required to import Wordpress XML files.')
  26 + '"BeautifulSoup4" and "lxml" required to import Wordpress XML files.')
22 27 sys.exit(error)
23 28
24 29 xmlfile = open(xml, encoding='utf-8').read()
25   - soup = BeautifulStoneSoup(xmlfile)
  30 + soup = BeautifulSoup(xmlfile, "xml")
26 31 items = soup.rss.channel.findAll('item')
27 32
28 33 for item in items:
@@ -54,10 +59,10 @@ def wp2fields(xml):
54 59 def dc2fields(file):
55 60 """Opens a Dotclear export file, and yield pelican fields"""
56 61 try:
57   - from BeautifulSoup import BeautifulStoneSoup
  62 + from bs4 import BeautifulSoup
58 63 except ImportError:
59 64 error = ('Missing dependency '
60   - '"BeautifulSoup" required to import Dotclear files.')
  65 + '"BeautifulSoup4" and "lxml" required to import Dotclear files.')
61 66 sys.exit(error)
62 67
63 68
@@ -142,13 +147,27 @@ def dc2fields(file):
142 147 if len(tag) > 1:
143 148 if int(tag[:1]) == 1:
144 149 newtag = tag.split('"')[1]
145   - tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
  150 + tags.append(
  151 + BeautifulSoup(
  152 + newtag
  153 + , "xml"
  154 + )
  155 + # bs4 always outputs UTF-8
  156 + .decode('utf-8')
  157 + )
146 158 else:
147 159 i=1
148 160 j=1
149 161 while(i <= int(tag[:1])):
150 162 newtag = tag.split('"')[j].replace('\\','')
151   - tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
  163 + tags.append(
  164 + BeautifulSoup(
  165 + newtag
  166 + , "xml"
  167 + )
  168 + # bs4 always outputs UTF-8
  169 + .decode('utf-8')
  170 + )
152 171 i=i+1
153 172 if j < int(tag[:1])*2:
154 173 j=j+2
@@ -244,7 +263,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
244 263 # Replace newlines with paragraphs wrapped with <p> so
245 264 # HTML is valid before conversion
246 265 paragraphs = content.splitlines()
247   - paragraphs = [u'<p>{0}</p>'.format(p) for p in paragraphs]
  266 + paragraphs = ['<p>{0}</p>'.format(p) for p in paragraphs]
248 267 new_content = ''.join(paragraphs)
249 268
250 269 fp.write(new_content)
@@ -264,7 +283,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
264 283 elif rc > 0:
265 284 error = "Please, check your Pandoc installation."
266 285 exit(error)
267   - except OSError, e:
  286 + except OSError as e:
268 287 error = "Pandoc execution failed: %s" % e
269 288 exit(error)
270 289
@@ -284,7 +303,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
284 303 def main():
285 304 parser = argparse.ArgumentParser(
286 305 description="Transform feed, Wordpress or Dotclear files to reST (rst) "
287   - "or Markdown (md) files. Be sure to have pandoc installed.",
  306 + "or Markdown (md) files. Be sure to have pandoc installed",
288 307 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
289 308
290 309 parser.add_argument(dest='input', help='The input file to read')
@@ -304,10 +323,10 @@ def main():
304 323 help="Strip raw HTML code that can't be converted to "
305 324 "markup such as flash embeds or iframes (wordpress import only)")
306 325 parser.add_argument('--disable-slugs', action='store_true',
307   - dest='disable_slugs',
308   - help='Disable storing slugs from imported posts within output. '
309   - 'With this disabled, your Pelican URLs may not be consistent '
310   - 'with your original posts.')
  326 + dest='disable_slugs',
  327 + help='Disable storing slugs from imported posts within output. '
  328 + 'With this disabled, your Pelican URLs may not be consistent '
  329 + 'with your original posts.')
311 330
312 331 args = parser.parse_args()
313 332
@@ -339,4 +358,4 @@ def main():
339 358 fields2pelican(fields, args.markup, args.output,
340 359 dircat=args.dircat or False,
341 360 strip_raw=args.strip_raw or False,
342   - disable_slugs=args.disable_slugs or False)
  361 + strip_slugs=args.disable_slugs or False)
82 pelican/tools/pelican_quickstart.py
... ... @@ -1,5 +1,8 @@
1 1 #!/usr/bin/env python
2   -# -*- coding: utf-8 -*- #
  2 +
  3 +# -*- coding: utf-8 -*-
  4 +from __future__ import unicode_literals, print_function
  5 +import six
3 6
4 7 import os
5 8 import string
@@ -29,11 +32,22 @@
29 32 'lang': 'en'
30 33 }
31 34
  35 +def _input_compat(prompt):
  36 + if six.PY3:
  37 + r = input(prompt)
  38 + else:
  39 + r = raw_input(prompt).decode('utf-8')
  40 + return r
  41 +
  42 +if six.PY3:
  43 + str_compat = str
  44 +else:
  45 + str_compat = unicode
32 46
33 47 def decoding_strings(f):
34 48 def wrapper(*args, **kwargs):
35 49 out = f(*args, **kwargs)
36   - if isinstance(out, basestring):
  50 + if isinstance(out, six.string_types):
37 51 # todo: make encoding configurable?
38 52 return out.decode(sys.stdin.encoding)
39 53 return out
@@ -55,14 +69,14 @@ def get_template(name, as_encoding='utf-8'):
55 69
56 70
57 71 @decoding_strings
58   -def ask(question, answer=str, default=None, l=None):
59   - if answer == str:
  72 +def ask(question, answer=str_compat, default=None, l=None):
  73 + if answer == str_compat:
60 74 r = ''
61 75 while True:
62 76 if default:
63   - r = raw_input('> {0} [{1}] '.format(question, default))
  77 + r = _input_compat('> {0} [{1}] '.format(question, default))
64 78 else:
65   - r = raw_input('> {0} '.format(question, default))
  79 + r = _input_compat('> {0} '.format(question, default))
66 80
67 81 r = r.strip()
68 82
@@ -84,11 +98,11 @@ def ask(question, answer=str, default=None, l=None):
84 98 r = None
85 99 while True:
86 100 if default is True:
87   - r = raw_input('> {0} (Y/n) '.format(question))
  101 + r = _input_compat('> {0} (Y/n) '.format(question))
88 102 elif default is False:
89   - r = raw_input('> {0} (y/N) '.format(question))
  103 + r = _input_compat('> {0} (y/N) '.format(question))
90 104 else:
91   - r = raw_input('> {0} (y/n) '.format(question))
  105 + r = _input_compat('> {0} (y/n) '.format(question))
92 106
93 107 r = r.strip().lower()
94 108
@@ -108,9 +122,9 @@ def ask(question, answer=str, default=None, l=None):
108 122 r = None
109 123 while True:
110 124 if default:
111   - r = raw_input('> {0} [{1}] '.format(question, default))
  125 + r = _input_compat('> {0} [{1}] '.format(question, default))
112 126 else:
113   - r = raw_input('> {0} '.format(question))
  127 + r = _input_compat('> {0} '.format(question))
114 128
115 129 r = r.strip()
116 130
@@ -125,7 +139,7 @@ def ask(question, answer=str, default=None, l=None):
125 139 print('You must enter an integer')
126 140 return r
127 141 else:
128   - raise NotImplemented('Argument `answer` must be str, bool, or integer')
  142 + raise NotImplemented('Argument `answer` must be str_compat, bool, or integer')
129 143
130 144
131 145 def main():
@@ -158,14 +172,14 @@ def main():
158 172 print('Using project associated with current virtual environment.'
159 173 'Will save to:\n%s\n' % CONF['basedir'])
160 174 else:
161   - CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str, default=args.path))
  175 + CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str_compat, default=args.path))
162 176
163   - CONF['sitename'] = ask('What will be the title of this web site?', answer=str, default=args.title)
164   - CONF['author'] = ask('Who will be the author of this web site?', answer=str, default=args.author)
165   - CONF['lang'] = ask('What will be the default language of this web site?', str, args.lang or CONF['lang'], 2)
  177 + CONF['sitename'] = ask('What will be the title of this web site?', answer=str_compat, default=args.title)
  178 + CONF['author'] = ask('Who will be the author of this web site?', answer=str_compat, default=args.author)
  179 + CONF['lang'] = ask('What will be the default language of this web site?', str_compat, args.lang or CONF['lang'], 2)
166 180
167 181 if ask('Do you want to specify a URL prefix? e.g., http://example.com ', answer=bool, default=True):
168   - CONF['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str, CONF['siteurl'])
  182 + CONF['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str_compat, CONF['siteurl'])
169 183
170 184 CONF['with_pagination'] = ask('Do you want to enable article pagination?', bool, bool(CONF['default_pagination']))
171 185
@@ -179,38 +193,38 @@ def main():
179 193
180 194 if mkfile:
181 195 if ask('Do you want to upload your website using FTP?', answer=bool, default=False):
182   - CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str, CONF['ftp_host'])
183   - CONF['ftp_user'] = ask('What is your username on that server?', str, CONF['ftp_user'])
184   - CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ftp_target_dir'])
  196 + CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str_compat, CONF['ftp_host'])
  197 + CONF['ftp_user'] = ask('What is your username on that server?', str_compat, CONF['ftp_user'])
  198 + CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, CONF['ftp_target_dir'])
185 199 if ask('Do you want to upload your website using SSH?', answer=bool, default=False):
186   - CONF['ssh_host'] = ask('What is the hostname of your SSH server?', str, CONF['ssh_host'])
  200 + CONF['ssh_host'] = ask('What is the hostname of your SSH server?', str_compat, CONF['ssh_host'])
187 201 CONF['ssh_port'] = ask('What is the port of your SSH server?', int, CONF['ssh_port'])
188   - CONF['ssh_user'] = ask('What is your username on that server?', str, CONF['ssh_user'])
189   - CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ssh_target_dir'])
  202 + CONF['ssh_user'] = ask('What is your username on that server?', str_compat, CONF['ssh_user'])
  203 + CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, CONF['ssh_target_dir'])
190 204 if ask('Do you want to upload your website using Dropbox?', answer=bool, default=False):
191   - CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str, CONF['dropbox_dir'])
  205 + CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str_compat, CONF['dropbox_dir'])
192 206
193 207 try:
194 208 os.makedirs(os.path.join(CONF['basedir'], 'content'))
195   - except OSError, e:
  209 + except OSError as e:
196 210 print('Error: {0}'.format(e))
197 211
198 212 try:
199 213 os.makedirs(os.path.join(CONF['basedir'], 'output'))
200   - except OSError, e:
  214 + except OSError as e:
201 215 print('Error: {0}'.format(e))
202 216
203 217 try:
204 218 with codecs.open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w', 'utf-8') as fd:
205 219 conf_python = dict()
206   - for key, value in CONF.iteritems():
  220 + for key, value in CONF.items():
207 221 conf_python[key] = repr(value)
208 222
209 223 for line in get_template('pelicanconf.py'):
210 224 template = string.Template(line)
211 225 fd.write(template.safe_substitute(conf_python))
212 226 fd.close()
213   - except OSError, e:
  227 + except OSError as e:
214 228 print('Error: {0}'.format(e))
215 229
216 230 try:
@@ -219,7 +233,7 @@ def main():
219 233 template = string.Template(line)
220 234 fd.write(template.safe_substitute(CONF))
221 235 fd.close()
222   - except OSError, e:
  236 + except OSError as e:
223 237 print('Error: {0}'.format(e))
224 238
225 239 if mkfile:
@@ -229,13 +243,13 @@ def main():
229 243 template = string.Template(line)
230 244 fd.write(template.safe_substitute(CONF))
231 245 fd.close()
232   - except OSError, e:
  246 + except OSError as e:
233 247 print('Error: {0}'.format(e))
234 248
235 249 if develop:
236 250 conf_shell = dict()
237   - for key, value in CONF.iteritems():
238   - if isinstance(value, basestring) and ' ' in value:
  251 + for key, value in CONF.items():
  252 + if isinstance(value, six.string_types) and ' ' in value:
239 253 value = '"' + value.replace('"', '\\"') + '"'
240 254 conf_shell[key] = value
241 255 try:
@@ -244,8 +258,8 @@ def main():
244 258 template = string.Template(line)
245 259 fd.write(template.safe_substitute(conf_shell))
246 260 fd.close()
247   - os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 0755)
248   - except OSError, e:
  261 + os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 493) # mode 0o755
  262 + except OSError as e:
249 263 print('Error: {0}'.format(e))
250 264
251 265 print('Done. Your new project is available at %s' % CONF['basedir'])
17 pelican/tools/pelican_themes.py
... ... @@ -1,5 +1,8 @@
1 1 #!/usr/bin/env python
2 2 # -*- coding: utf-8 -*-
  3 +from __future__ import unicode_literals, print_function
  4 +
  5 +import six
3 6
4 7 import argparse
5 8 import os
@@ -28,7 +31,7 @@
28 31
29 32 def err(msg, die=None):
30 33 """Print an error message and exits if an exit code is given"""
31   - sys.stderr.write(str(msg) + '\n')
  34 + sys.stderr.write(msg + '\n')
32 35 if die:
33 36 sys.exit((die if type(die) is int else 1))
34 37
@@ -186,13 +189,13 @@ def install(path, v=False, u=False):
186 189 for root, dirs, files in os.walk(theme_path):
187 190 for d in dirs:
188 191 dname = os.path.join(root, d)
189   - os.chmod(dname, 0755)
  192 + os.chmod(dname, 493) # 0o755
190 193 for f in files:
191 194 fname = os.path.join(root, f)
192   - os.chmod(fname, 0644)
193   - except OSError, e:
  195 + os.chmod(fname, 420) # 0o644
  196 + except OSError as e:
194 197 err("Cannot change permissions of files or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)), die=False)
195   - except Exception, e:
  198 + except Exception as e:
196 199 err("Cannot copy `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e)))
197 200
198 201
@@ -212,7 +215,7 @@ def symlink(path, v=False):
212 215 print("Linking `{p}' to `{t}' ...".format(p=path, t=theme_path))
213 216 try:
214 217 os.symlink(path, theme_path)
215   - except Exception, e:
  218 + except Exception as e:
216 219 err("Cannot link `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e)))
217 220
218 221
@@ -233,7 +236,7 @@ def clean(v=False):
233 236 print('Removing {0}'.format(path))
234 237 try:
235 238 os.remove(path)
236   - except OSError, e:
  239 + except OSError as e:
237 240 print('Error: cannot remove {0}'.format(path))
238 241 else:
239 242 c+=1
2  pelican/tools/templates/Makefile.in
@@ -49,7 +49,7 @@ regenerate: clean
49 49 $$(PELICAN) -r $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS)
50 50