diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..b42ca8c2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.py]
+max_line_length = 79
+
+[*.yml]
+indent_size = 2
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..7e0d5359
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+custom: https://donate.getpelican.com
+liberapay: pelican
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 00000000..b6991ba2
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,31 @@
+# Configuration for probot-stale - https://github.com/probot/stale
+
+# Number of days of inactivity before an Issue or Pull Request becomes stale
+daysUntilStale: 60
+
+# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
+daysUntilClose: 30
+
+# Set to true to ignore issues in a project (defaults to false)
+exemptProjects: true
+
+# Set to true to ignore issues in a milestone (defaults to false)
+exemptMilestones: true
+
+# Set to true to ignore issues with an assignee (defaults to false)
+exemptAssignees: true
+
+# Label to use when marking as stale
+staleLabel: stale
+
+# Comment to post when marking as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your participation and understanding.
+
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 1
+
+# Limit to only `issues` or `pulls`
+only: issues
diff --git a/.gitignore b/.gitignore
index 45946946..b94526d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,8 @@ tags
.tox
.coverage
htmlcov
-six-*.egg/
*.orig
venv
samples/output
*.pem
+poetry.lock
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..ef27cfcf
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,13 @@
+# See https://pre-commit.com/hooks.html for info on hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.3.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-ast
+ - id: check-toml
+ - id: check-yaml
+ - id: debug-statements
+ - id: detect-private-key
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
diff --git a/.travis.yml b/.travis.yml
index 31b3ac8b..af3a6ca8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,20 @@
language: python
python:
- - "3.5"
+ - "3.6"
env:
- - TOX_ENV=docs
- - TOX_ENV=flake8
- - TOX_ENV=py27
- - TOX_ENV=py35
+ global:
+ - PYPI_USERNAME=autopub
+ - secure: "h5V/+YL+CrqvfAesNkSb824Ngk5x+f0eFzj/LBbmnzjvArKAmc6R6WGyx8SDD7WF/PlaTf0M1fH3a7pjIS8Ee+TS1Rb0Lt1HPqUs1yntg1+Js2ZQp3p20wfsDc+bZ4/2g8xLsSMv1EJ4np7/GJ5fXqpSxjr/Xs5LYA7ZLwNNwDw="
+ - secure: "GiDFfmjH7uzYNnkjQMV/mIkbRdmgkGmtbFPeaj9taBNA5tPp3IBt3GOOS6UL/zm9xiwu9Xo6sxZWkGzY19Hsdv28YPH34N3abo0QSnz4IGiHs152Hi7Qi6Tb0QkT5D3OxuSIm8LmFL7+su89Q7vBFowrT6HL1Mn8CDDWSj3eqbo="
+ - TWINE_USERNAME=$PYPI_USERNAME
+ - TWINE_PASSWORD=$PYPI_PASSWORD
+ matrix:
+ - TOX_ENV=docs
+ - TOX_ENV=flake8
+ - TOX_ENV=py35
+ - TOX_ENV=py36
matrix:
include:
- - python: 3.6
- env:
- - TOX_ENV=py36
- python: 3.7
sudo: true
dist: xenial
@@ -25,8 +29,25 @@ before_install:
install:
- pip install tox==2.5.0
script: tox -e $TOX_ENV
+before_deploy:
+ - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then travis_terminate 0; fi'
+ - pip install githubrelease
+ - pip install --pre autopub
+ - autopub check || travis_terminate 0
+ - pip install poetry
+ - pip install twine
+ - git checkout ${TRAVIS_BRANCH}
+ - git remote set-url origin https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG
+deploy:
+ provider: script
+ script: autopub deploy
+ skip_cleanup: true
+ on:
+ branch: master
+ python: "3.7"
+# The channel name "irc.freenode.org#pelican" is encrypted against getpelican/pelican to prevent IRC spam of forks
notifications:
irc:
channels:
- - "irc.freenode.org#pelican"
+ - secure: "JP57f61QovrhmLoAF6oPOzIK2aXGfSO06FHg7yiuKBOWMiaxQejZUGJX919muCLhWJXDugsviIqCMoAWwNV3o1WQbqIr+G5TR+N9MrtCs4Zi6vpGj09bR8giKUKx+PPKEoe1Ew56E4y2LxzGO4Lj9hZx8M2YVdwPNWrWZgp6WXE="
on_success: change
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 37fc7ffb..945a5303 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -22,7 +22,7 @@ Before you ask for help, please make sure you do the following:
3. Try reproducing the issue in a clean environment, ensuring you are using:
-* latest Pelican release (or an up-to-date git clone of Pelican master)
+* latest Pelican release (or an up-to-date Git clone of Pelican master)
* latest releases of libraries used by Pelican
* no plugins or only those related to the issue
@@ -58,7 +58,7 @@ publicly-accessible location:
* Upload detailed and **complete** output logs and backtraces (remember to add
the ``--debug`` flag: ``pelican --debug content [...]``)
-.. _documentation: http://docs.getpelican.com/
+.. _documentation: https://docs.getpelican.com/
.. _`paste service`: https://dpaste.de/
Once the above preparation is ready, you can contact people willing to help via
@@ -71,7 +71,7 @@ The #pelican IRC channel
* Because of differing time zones, you may not get an immediate response to your
question, but please be patient and stay logged into IRC — someone will almost
always respond if you wait long enough (it may take a few hours).
-* If you don't have an IRC client handy, use the webchat_ for quick feedback.
+* If you don't have an IRC client handy, use the webchat_.
* You can direct your IRC client to the channel using this `IRC link`_ or you
can manually join the ``#pelican`` IRC channel on the `freenode IRC network`_.
@@ -94,9 +94,17 @@ Using Git and GitHub
* `Create a new git branch`_ specific to your change (as opposed to making
your commits in the master branch).
* **Don't put multiple unrelated fixes/features in the same branch / pull request.**
- For example, if you're hacking on a new feature and find a bugfix that
+ For example, if you're working on a new feature and find a bugfix that
doesn't *require* your new feature, **make a new distinct branch and pull
request** for the bugfix.
+* Add a ``RELEASE.md`` file in the root of the project that contains the
+ release type (major, minor, patch) and a summary of the changes that will be
+ used as the release changelog entry. For example::
+
+ Release type: minor
+
+ Reload browser window upon changes to content, settings, or theme
+
* Check for unnecessary whitespace via ``git diff --check`` before committing.
* First line of your commit message should start with present-tense verb, be 50
characters or less, and include the relevant issue number(s) if applicable.
@@ -112,25 +120,25 @@ Using Git and GitHub
GitHub's web UI to submit the pull request. This isn't an absolute
requirement, but makes the maintainers' lives much easier! Specifically:
`install hub `_ and then run
- `hub pull-request `_ to
- turn your GitHub issue into a pull request containing your code.
-* After you have issued a pull request, Travis will run the test suite for all
- supported Python versions and check for PEP8 compliance. If any of these
- checks fail, you should fix them. (If tests fail on Travis but seem to pass
- locally, ensure that local test runs aren't skipping any tests.)
+ `hub pull-request -i [ISSUE] `_
+ to turn your GitHub issue into a pull request containing your code.
+* After you have issued a pull request, the continuous integration (CI) system
+ will run the test suite for all supported Python versions and check for PEP8
+ compliance. If any of these checks fail, you should fix them. (If tests fail
+ on the CI system but seem to pass locally, ensure that local test runs aren't
+ skipping any tests.)
Contribution quality standards
------------------------------
* Adhere to `PEP8 coding standards`_. This can be eased via the `pycodestyle
`_ or `flake8
- `_ tools, the latter of which in
+ `_ tools, the latter of which in
particular will give you some useful hints about ways in which the
code/formatting can be improved. If you are relying on your editor for PEP8
compliance, note that the line length specified by PEP8 is 79 (excluding the
line break).
-* Ensure your code is compatible with the latest Python 2.7 and 3.x releases — see our
- `compatibility cheatsheet`_ for more details.
+* Ensure your code is compatible with the `officially-supported Python releases`_.
* Add docs and tests for your changes. Undocumented and untested features will
not be accepted.
* `Run all the tests`_ **on all versions of Python supported by Pelican** to
@@ -139,12 +147,12 @@ Contribution quality standards
Check out our `Git Tips`_ page or `ask for help`_ if you
need assistance or have any questions about these guidelines.
-.. _`plugin`: http://docs.getpelican.com/en/latest/plugins.html
-.. _`#pelican IRC channel`: http://webchat.freenode.net/?channels=pelican&uio=d4
+.. _`plugin`: https://docs.getpelican.com/en/latest/plugins.html
+.. _`#pelican IRC channel`: https://webchat.freenode.net/?channels=pelican&uio=d4
.. _`Create a new git branch`: https://github.com/getpelican/pelican/wiki/Git-Tips#making-your-changes
.. _`Squash your commits`: https://github.com/getpelican/pelican/wiki/Git-Tips#squashing-commits
-.. _`Run all the tests`: http://docs.getpelican.com/en/latest/contribute.html#running-the-test-suite
+.. _`Run all the tests`: https://docs.getpelican.com/en/latest/contribute.html#running-the-test-suite
.. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips
-.. _`PEP8 coding standards`: http://www.python.org/dev/peps/pep-0008/
+.. _`PEP8 coding standards`: https://www.python.org/dev/peps/pep-0008/
.. _`ask for help`: `How to get help`_
-.. _`compatibility cheatsheet`: http://docs.getpelican.com/en/latest/contribute.html#python-3-development-tips
+.. _`officially-supported Python releases`: https://devguide.python.org/#status-of-python-branches
diff --git a/LICENSE b/LICENSE
index dba13ed2..c03a4e46 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
- Copyright (C) 2007 Free Software Foundation, Inc.
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
@@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
-.
+.
diff --git a/MANIFEST.in b/MANIFEST.in
index 49e2bef9..138c8f00 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
include *.rst
recursive-include pelican *.html *.css *png *.rst *.markdown *.md *.mkd *.xml *.py
-include LICENSE THANKS docs/changelog.rst
+include LICENSE THANKS docs/changelog.rst pyproject.toml
diff --git a/README.rst b/README.rst
index 05063251..6f83188c 100644
--- a/README.rst
+++ b/README.rst
@@ -47,13 +47,13 @@ Why the name "Pelican"?
.. Links
-.. _Python: http://www.python.org/
+.. _Python: https://www.python.org/
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
-.. _Markdown: http://daringfireball.net/projects/markdown/
-.. _Jinja2: http://jinja.pocoo.org/
-.. _Pygments: http://pygments.org/
-.. _`Pelican's documentation`: http://docs.getpelican.com/
-.. _`Pelican's internals`: http://docs.getpelican.com/en/latest/internals.html
+.. _Markdown: https://daringfireball.net/projects/markdown/
+.. _Jinja2: https://palletsprojects.com/p/jinja/
+.. _Pygments: https://pygments.org/
+.. _`Pelican's documentation`: https://docs.getpelican.com/
+.. _`Pelican's internals`: https://docs.getpelican.com/en/latest/internals.html
.. _`hosted on GitHub`: https://github.com/getpelican/pelican
.. |build-status| image:: https://img.shields.io/travis/getpelican/pelican/master.svg
diff --git a/THANKS b/THANKS
index 348c0b59..08ac7bb2 100644
--- a/THANKS
+++ b/THANKS
@@ -1,11 +1,12 @@
Pelican is a project originally created by Alexis Métaireau
-, but there are a large number of people that have
+ and subsequently maintained by Justin Mayer
+, but there are a large number of people that have
contributed or implemented key features over time. We do our best to keep this
list up-to-date, but you can also have a look at the nice contributor graphs
produced by GitHub: https://github.com/getpelican/pelican/graphs/contributors
If you want to contribute, check the documentation section about how to do so:
-
+
Aaron Kavlie
Abhishek L
@@ -91,6 +92,7 @@ Joshua Adelman
Julian Berman
Justin Mayer
Kevin Deldycke
+Kevin Yap
Kyle Fuller
Laureline Guerin
Leonard Huang
@@ -116,6 +118,7 @@ Nico Di Rocco
Nicolas Duhamel
Nicolas Perriault
Nicolas Steinmetz
+Paolo Melchiorre
Paul Asselin
Pavel Puchkin
Perry Roper
diff --git a/bumpr.rc b/bumpr.rc
deleted file mode 100644
index eebe4dd8..00000000
--- a/bumpr.rc
+++ /dev/null
@@ -1,32 +0,0 @@
-[bumpr]
-file = pelican/__init__.py
-vcs = git
-clean =
- python setup.py clean
- rm -rf *egg-info build dist
-tests = python -m unittest discover
-publish = python setup.py sdist bdist_wheel register upload
-files =
- README.rst
- setup.py
-
-[bump]
-unsuffix = true
-message = Bump version {version}
-
-[prepare]
-part = patch
-suffix = dev
-message = Prepare version {version} for next development cycle
-
-[changelog]
-file = docs/changelog.rst
-separator = =
-bump = {version} ({date:%Y-%m-%d})
-prepare = Next release
-
-[readthedoc]
-url = http://docs.getpelican.com/{tag}
-
-[commands]
-bump = sed -i "" "s/last_stable[[:space:]]*=.*/last_stable = '{version}'/" docs/conf.py
diff --git a/docs/changelog.rst b/docs/changelog.rst
index cdd3697c..febc5322 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,9 +1,50 @@
Release history
###############
-Next Release
+4.2.0 - 2019-10-17
==================
+* Support inline SVGs; don't treat titles in SVGs as HTML titles
+* Add category to feeds (in addition to tags)
+* Improve content metadata field docs
+* Add docs for including other Markdown/reST files in content
+
+4.1.3 - 2019-10-09
+==================
+
+* Fix quick-start docs regarding `pelican --listen`
+* Set default listen address to 127.0.0.1
+* Add extra/optional Markdown dependency to setup.py
+* Use correct SSH port syntax for rsync in tasks.py
+* Place all deprecated settings handling together
+* Add related project URLs for display on PyPI
+* Skip some tests on Windows that can't pass due to filesystem differences
+
+4.1.2 - 2019-09-23
+==================
+
+Fix pelican.settings.load_source to avoid caching issues - PR #2621
+
+4.1.1 - 2019-08-23
+==================
+
+* Add AutoPub to auto-publish releases on PR merge
+* Add CSS classes for reStructuredText figures
+* Pass `argv` to Pelican `main` entrypoint
+* Set default content status to a blank string rather than `None`
+
+4.1.0 - 2019-07-14
+==================
+
+* Live browser reload upon changed files (provided via Invoke task)
+* Add ``pyproject.toml``, managed by Poetry
+* Support for invoking ``python -m pelican``
+* Add relative source path attribute to content
+* Allow directories in ``EXTRA_PATH_METADATA``
+* Add ``all_articles`` variable to period pages (for recent posts functionality)
+* Improve debug mode output
+* Remove blank or duplicate summaries from Atom feed
+* Fix bugs in pagination, pelican-import, pelican-quickstart, and feed importer
4.0.1 (2018-11-30)
==================
diff --git a/docs/conf.py b/docs/conf.py
index bac5d9bf..2be4fbe1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
import os
import sys
@@ -18,11 +17,11 @@ extensions = ['sphinx.ext.autodoc',
source_suffix = '.rst'
master_doc = 'index'
project = 'Pelican'
-copyright = '2010, Alexis Metaireau and contributors'
+copyright = '2010 – present, Justin Mayer, Alexis Metaireau, and contributors'
exclude_patterns = ['_build']
release = __version__
version = '.'.join(release.split('.')[:1])
-last_stable = '4.0.1'
+last_stable = __version__
rst_prolog = '''
.. |last_stable| replace:: :pelican-doc:`{0}`
'''.format(last_stable)
@@ -31,7 +30,7 @@ rst_prolog = '''
pygments_style = 'sphinx'
extlinks = {
- 'pelican-doc': ('http://docs.getpelican.com/%s/', '')
+ 'pelican-doc': ('https://docs.getpelican.com/%s/', '')
}
# -- Options for HTML output --------------------------------------------------
@@ -69,14 +68,14 @@ def setup(app):
# -- Options for LaTeX output -------------------------------------------------
latex_documents = [
- ('index', 'Pelican.tex', 'Pelican Documentation', 'Alexis Métaireau',
+ ('index', 'Pelican.tex', 'Pelican Documentation', 'Justin Mayer',
'manual'),
]
# -- Options for manual page output -------------------------------------------
man_pages = [
('index', 'pelican', 'pelican documentation',
- ['Alexis Métaireau'], 1),
+ ['Justin Mayer'], 1),
('pelican-themes', 'pelican-themes', 'A theme manager for Pelican',
['Mickaël Raybaud'], 1),
('themes', 'pelican-theming', 'How to create themes for Pelican',
diff --git a/docs/content.rst b/docs/content.rst
index 78fc7993..a52425a4 100644
--- a/docs/content.rst
+++ b/docs/content.rst
@@ -71,22 +71,29 @@ Metadata syntax for Markdown posts should follow this pattern::
This is the content of my super blog post.
You can also have your own metadata keys (so long as they don't conflict with
-reserved metadata keywords) for use in your python templates. The following is
-the list of reserved metadata keywords:
+reserved metadata keywords) for use in your templates. The following table
+contains a list of reserved metadata keywords:
-* `Title`
-* `Tags`
-* `Date`
-* `Modified`
-* `Status`
-* `Category`
-* `Author`
-* `Authors`
-* `Slug`
-* `Summary`
-* `Template`
-* `Save_as`
-* `Url`
+=============== ===============================================================
+ Metadata Description
+=============== ===============================================================
+``title`` Title of the article or page
+``date`` Publication date (e.g., ``YYYY-MM-DD HH:SS``)
+``modified`` Modification date (e.g., ``YYYY-MM-DD HH:SS``)
+``tags`` Content tags, separated by commas
+``keywords`` Content keywords, separated by commas (HTML content only)
+``category`` Content category (one only — not multiple)
+``slug`` Identifier used in URLs and translations
+``author`` Content author, when there is only one
+``authors`` Content authors, when there are multiple
+``summary`` Brief description of content for index pages
+``lang`` Content language ID (``en``, ``fr``, etc.)
+``translation`` Is content is a translation of another (``true`` or ``false``)
+``status`` Content status: ``draft``, ``hidden``, or ``published``
+``template`` Name of template to use to generate content (without extension)
+``save_as`` Save content to this relative file path
+``url`` URL to use for this article/page
+=============== ===============================================================
Readers for additional formats (such as AsciiDoc_) are available via plugins.
Refer to `pelican-plugins`_ repository for those.
@@ -116,7 +123,7 @@ specified either via the ``tags`` metadata, as is standard in Pelican, or via
the ``keywords`` metadata, as is standard in HTML. The two can be used
interchangeably.
-Note that, aside from the title, none of this article metadata is mandatory:
+Note that, aside from the title, none of this content metadata is mandatory:
if the date is not specified and ``DEFAULT_DATE`` is set to ``'fs'``, Pelican
will rely on the file's "mtime" timestamp, and the category can be determined
by the directory in which the file resides. For example, a file located at
@@ -126,6 +133,15 @@ not be a good category name, you can set the setting ``USE_FOLDER_AS_CATEGORY``
to ``False``. When parsing dates given in the page metadata, Pelican supports
the W3C's `suggested subset ISO 8601`__.
+So the title is the only required metadata. If that bothers you, worry not.
+Instead of manually specifying a title in your metadata each time, you can use
+the source content file name as the title. For example, a Markdown source file
+named ``Publishing via Pelican.md`` would automatically be assigned a title of
+*Publishing via Pelican*. If you would prefer this behavior, add the following
+line to your settings file::
+
+ FILENAME_METADATA = '(?P.*)'
+
.. note::
When experimenting with different settings (especially the metadata
@@ -373,6 +389,40 @@ to allow linking to both generated articles and pages and their static sources.
Support for the old syntax may eventually be removed.
+Including other files
+---------------------
+Both Markdown and reStructuredText syntaxes provide mechanisms for this.
+
+Following below are some examples for **reStructuredText** using `the include directive`_:
+
+ .. code-block:: rst
+
+ .. include:: file.rst
+
+Include a fragment of a file delimited by two identifiers, highlighted as C++ (slicing based on line numbers is also possible):
+
+ .. code-block:: rst
+
+ .. include:: main.cpp
+ :code: c++
+ :start-after: // begin
+ :end-before: // end
+
+Include a raw HTML file (or an inline SVG) and put it directly into the output without any processing:
+
+ .. code-block:: rst
+
+ .. raw:: html
+ :file: table.html
+
+For **Markdown**, one must rely on an extension. For example, using the `mdx_include plugin`_:
+
+ .. code-block:: none
+
+ ```html
+ {! template.html !}
+ ```
+
Importing an existing site
==========================
@@ -474,7 +524,7 @@ indenting both the identifier and the code::
print("The path-less shebang syntax *will* show line numbers.")
The specified identifier (e.g. ``python``, ``ruby``) should be one that
-appears on the `list of available lexers `_.
+appears on the `list of available lexers `_.
When using reStructuredText the following options are available in the
code-block directive:
@@ -515,7 +565,7 @@ tagurlformat string format for the ctag links.
Note that, depending on the version, your Pygments module might not have
all of these options available. Refer to the *HtmlFormatter* section of the
-`Pygments documentation `_ for more
+`Pygments documentation `_ for more
details on each of the options.
For example, the following code block enables line numbers, starting at 153,
@@ -560,9 +610,11 @@ the ``DEFAULT_METADATA``::
To publish a post when the default status is ``draft``, update the post's
metadata to include ``Status: published``.
-.. _W3C ISO 8601: http://www.w3.org/TR/NOTE-datetime
-.. _AsciiDoc: http://www.methods.co.nz/asciidoc/
-.. _pelican-plugins: http://github.com/getpelican/pelican-plugins
+.. _W3C ISO 8601: https://www.w3.org/TR/NOTE-datetime
+.. _AsciiDoc: https://www.methods.co.nz/asciidoc/
+.. _pelican-plugins: https://github.com/getpelican/pelican-plugins
.. _Markdown Extensions: https://python-markdown.github.io/extensions/
.. _CodeHilite extension: https://python-markdown.github.io/extensions/code_hilite/#syntax
-.. _i18n_subsites plugin: http://github.com/getpelican/pelican-plugins/tree/master/i18n_subsites
+.. _i18n_subsites plugin: https://github.com/getpelican/pelican-plugins/tree/master/i18n_subsites
+.. _the include directive: http://docutils.sourceforge.net/docs/ref/rst/directives.html#include
+.. _mdx_include plugin: https://github.com/neurobin/mdx_include
diff --git a/docs/contribute.rst b/docs/contribute.rst
index 5a314751..221c405b 100644
--- a/docs/contribute.rst
+++ b/docs/contribute.rst
@@ -7,72 +7,78 @@ can also help out by reviewing and commenting on
`existing issues `_.
Don't hesitate to fork Pelican and submit an issue or pull request on GitHub.
-When doing so, please adhere to the following guidelines.
+When doing so, please consider the following guidelines.
.. include:: ../CONTRIBUTING.rst
Setting up the development environment
======================================
-While there are many ways to set up one's development environment, we recommend
-using `Virtualenv `_. This tool allows
-you to set up separate environments for separate Python projects that are
-isolated from one another so you can use different packages (and package
-versions) for each.
+While there are many ways to set up one's development environment, the following
+instructions will utilize Pip_ and Poetry_. These tools facilitate managing
+virtual environments for separate Python projects that are isolated from one
+another, so you can use different packages (and package versions) for each.
-If you don't have ``virtualenv`` installed, you can install it via::
+Please note that Python 3.6+ is required for Pelican development.
- $ pip install virtualenv
+*(Optional)* If you prefer to install Poetry once for use with multiple projects,
+you can install it via::
-Use ``virtualenv`` to create and activate a virtual environment::
+ curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
- $ virtualenv ~/virtualenvs/pelican
- $ cd ~/virtualenvs/pelican
- $ . bin/activate
+Point your web browser to the `Pelican repository`_ and tap the **Fork** button
+at top-right. Then clone the source for your fork and add the upstream project
+as a Git remote::
-Clone the Pelican source into a subfolder called ``src/pelican``::
+ mkdir ~/projects
+ git clone https://github.com/YOUR_USERNAME/pelican.git ~/projects/pelican
+ cd ~/projects/pelican
+ git remote add upstream https://github.com/getpelican/pelican.git
- $ git clone https://github.com/getpelican/pelican.git src/pelican
- $ cd src/pelican
+While Poetry can dynamically create and manage virtual environments, we're going
+to manually create and activate a virtual environment::
-Install the development dependencies::
+ mkdir ~/virtualenvs
+ python3 -m venv ~/virtualenvs/pelican
+ source ~/virtualenvs/pelican/bin/activate
- $ pip install -r requirements/developer.pip
+Install the needed dependencies and set up the project::
-Install Pelican and its dependencies::
+ pip install -e ~/projects/pelican invoke
+ invoke setup
- $ python setup.py develop
+Your local environment should now be ready to go!
-Or using ``pip``::
+.. _Pip: https://pip.pypa.io/
+.. _Poetry: https://poetry.eustace.io/docs/#installation
+.. _Pelican repository: https://github.com/getpelican/pelican
- $ pip install -e .
+Development
+===========
-To conveniently test on multiple Python versions, we also provide a ``.tox``
-file.
+Once Pelican has been set up for local development, create a topic branch for
+your bug fix or feature::
+ git checkout -b name-of-your-bugfix-or-feature
-Building the docs
-=================
-
-If you make changes to the documentation, you should preview your changes
-before committing them::
-
- $ pip install -r requirements/docs.pip
- $ cd docs
- $ make html
-
-Open ``_build/html/index.html`` in your browser to preview the documentation.
+Now you can make changes to Pelican, its documentation, and/or other aspects of
+the project.
Running the test suite
-======================
+----------------------
-Each time you add a feature, there are two things to do regarding tests: check
-that the existing tests pass, and add tests for the new feature or bugfix.
+Each time you make changes to Pelican, there are two things to do regarding
+tests: check that the existing tests pass, and add tests for any new features
+or bug fixes. The tests are located in ``pelican/tests``, and you can run them
+via::
-The tests live in ``pelican/tests`` and you can run them using the
-"discover" feature of ``unittest``::
+ invoke tests
- $ python -Wd -m unittest discover
+In addition to running the test suite, the above invocation will also check code
+style and let you know whether non-conforming patterns were found. In some cases
+these linters will make the needed changes directly, while in other cases you
+may need to make additional changes until ``invoke tests`` no longer reports any
+code style violations.
After making your changes and running the tests, you may see a test failure
mentioning that "some generated files differ from the expected functional tests
@@ -80,14 +86,9 @@ output." If you have made changes that affect the HTML output generated by
Pelican, and the changes to that output are expected and deemed correct given
the nature of your changes, then you should update the output used by the
functional tests. To do so, **make sure you have both** ``en_EN.utf8`` **and**
-``fr_FR.utf8`` **locales installed**, and then run the following two commands::
+``fr_FR.utf8`` **locales installed**, and then run the following command::
- $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/custom/ \
- -s samples/pelican.conf.py samples/content/
- $ LC_ALL=fr_FR.utf8 pelican -o pelican/tests/output/custom_locale/ \
- -s samples/pelican.conf_FR.py samples/content/
- $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/basic/ \
- samples/content/
+ invoke update-functional-tests
You may also find that some tests are skipped because some dependency (e.g.,
Pandoc) is not installed. This does not automatically mean that these tests
@@ -101,48 +102,74 @@ environments.
.. _Tox: https://tox.readthedocs.io/en/latest/
-Python 2/3 compatibility development tips
-=========================================
+Building the docs
+-----------------
-Here are some tips that may be useful for writing code that is compatible with
-both Python 2.7 and Python 3:
+If you make changes to the documentation, you should build and inspect your
+changes before committing them::
-- Use new syntax. For example:
+ invoke docserve
- - ``print .. -> print(..)``
- - ``except .., e -> except .. as e``
+Open http://localhost:8000 in your browser to review the documentation. While
+the above task is running, any changes you make and save to the documentation
+should automatically appear in the browser, as it live-reloads when it detects
+changes to the documentation source files.
-- Use new methods. For example:
+Plugin development
+------------------
- - ``dict.iteritems() -> dict.items()``
- - ``xrange(..) - > list(range(..))``
+To create a *new* Pelican plugin, please refer to the `plugin template`_
+repository for detailed instructions.
-- Use ``six`` where necessary. For example:
+If you want to contribute to an *existing* Pelican plugin, follow the steps
+above to set up Pelican for local development, and then create a directory to
+store cloned plugin repositories::
- - ``isinstance(.., basestring) -> isinstance(.., six.string_types)``
- - ``isinstance(.., unicode) -> isinstance(.., six.text_type)``
+ mkdir -p ~/projects/pelican-plugins
-- Assume every string and literal is Unicode:
+Assuming you wanted to contribute to the Simple Footnotes plugin, you would
+first browse to the `Simple Footnotes`_ repository on GitHub and tap the **Fork**
+button at top-right. Then clone the source for your fork and add the upstream
+project as a Git remote::
- - Use ``from __future__ import unicode_literals``
- - Do not use the prefix ``u'`` before strings.
- - Do not encode/decode strings in the middle of something. Follow the code to
- the source/target of a string and encode/decode at the first/last possible
- point.
- - In particular, write your functions to expect and to return Unicode.
- - Encode/decode strings if the string is the output of a Python function that
- is known to handle this badly. For example, ``strftime()`` in Python 2.
- - Do not use the magic method ``__unicode()__`` in new classes. Use only
- ``__str()__`` and decorate the class with ``@python_2_unicode_compatible``.
+ git clone https://github.com/YOUR_USERNAME/simple-footnotes.git ~/projects/pelican-plugins/simple-footnotes
+ cd ~/projects/pelican-plugins/simple-footnotes
+ git remote add upstream https://github.com/pelican-plugins/simple-footnotes.git
-- ``setlocale()`` in Python 2 fails when we give the locale name as Unicode,
- and since we are using ``from __future__ import unicode_literals``, we do
- that everywhere! As a workaround, enclose the locale name with ``str()``;
- in Python 2 this casts the name to a byte string, while in Python 3 this
- should do nothing, because the locale name was already Unicode.
-- Do not start integer literals with a zero. This is a syntax error in Python 3.
-- Unfortunately there seems to be no octal notation that is valid in both
- Python 2 and 3. Use decimal notation instead.
+Install the needed dependencies and set up the project::
+
+ invoke setup
+
+Create a topic branch for your plugin bug fix or feature::
+
+ git checkout -b name-of-your-bugfix-or-feature
+
+After writing new tests for your plugin changes, run the plugin test suite::
+
+ invoke tests
+
+.. _plugin template: https://github.com/getpelican/cookiecutter-pelican-plugin
+.. _Simple Footnotes: https://github.com/pelican-plugins/simple-footnotes
+
+Submitting your changes
+-----------------------
+
+Assuming linting validation and tests pass, add a ``RELEASE.md`` file in the root
+of the project that contains the release type (major, minor, patch) and a
+summary of the changes that will be used as the release changelog entry.
+For example::
+
+ Release type: patch
+
+ Fix browser reloading upon changes to content, settings, or theme
+
+Commit your changes and push your topic branch::
+
+ git add .
+ git commit -m "Your detailed description of your changes"
+ git push origin name-of-your-bugfix-or-feature
+
+Finally, browse to your repository fork on GitHub and submit a pull request.
Logging tips
@@ -160,8 +187,8 @@ For logging messages that are not repeated, use the usual Python way::
logger.warning("A warning with %s formatting", arg_to_be_formatted)
Do not format log messages yourself. Use ``%s`` formatting in messages and pass
-arguments to logger. This is important, because Pelican logger will preprocess
-some arguments (like Exceptions) for Py2/Py3 compatibility.
+arguments to logger. This is important, because the Pelican logger will
+preprocess some arguments, such as exceptions.
Limiting extraneous log messages
--------------------------------
diff --git a/docs/faq.rst b/docs/faq.rst
index d469f386..bfe51ec0 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -48,7 +48,7 @@ themes to style code syntax highlighting via CSS. Specifically, you can
customize the appearance of your syntax highlighting via the ``.highlight pre``
class in your theme's CSS file. To see how various styles can be used to render
Django code, for example, use the style selector drop-down at top-right on the
-`Pygments project demo site `_.
+`Pygments project demo site `_.
You can use the following example commands to generate a starting CSS file from
a Pygments built-in style (in this case, "monokai") and then copy the generated
@@ -167,7 +167,7 @@ I'm getting a warning about feeds generated without SITEURL being set properly
==============================================================================
`RSS and Atom feeds require all URL links to be absolute
-`_. In order to properly
+`_. In order to properly
generate links in Pelican you will need to set ``SITEURL`` to the full path of
your site.
diff --git a/docs/importer.rst b/docs/importer.rst
index 674dd9b1..88588d17 100644
--- a/docs/importer.rst
+++ b/docs/importer.rst
@@ -39,8 +39,8 @@ Dependencies
- *Pandoc*, see the `Pandoc site`_ for installation instructions on your
operating system.
-.. _Pandoc: http://johnmacfarlane.net/pandoc/
-.. _Pandoc site: http://johnmacfarlane.net/pandoc/installing.html
+.. _Pandoc: https://pandoc.org/
+.. _Pandoc site: https://pandoc.org/installing.html
Usage
@@ -58,7 +58,7 @@ Positional arguments
============= ============================================================================
``input`` The input file to read
``api_token`` (Posterous only) api_token can be obtained from http://posterous.com/api/
- ``api_key`` (Tumblr only) api_key can be obtained from http://www.tumblr.com/oauth/apps
+ ``api_key`` (Tumblr only) api_key can be obtained from https://www.tumblr.com/oauth/apps
============= ============================================================================
Optional arguments
@@ -137,7 +137,7 @@ Tests
To test the module, one can use sample files:
-- for WordPress: http://www.wpbeginner.com/wp-themes/how-to-add-dummy-content-for-theme-development-in-wordpress/
+- for WordPress: https://www.wpbeginner.com/wp-themes/how-to-add-dummy-content-for-theme-development-in-wordpress/
- for Dotclear: http://media.dotaddict.org/tda/downloads/lorem-backup.txt
-.. _more_categories: http://github.com/getpelican/pelican-plugins/tree/master/more_categories
\ No newline at end of file
+.. _more_categories: https://github.com/getpelican/pelican-plugins/tree/master/more_categories
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index a0084ffb..4cbe41ae 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -76,10 +76,10 @@ Documentation
.. Links
-.. _Python: http://www.python.org/
+.. _Python: https://www.python.org/
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
-.. _Markdown: http://daringfireball.net/projects/markdown/
-.. _Jinja2: http://jinja.pocoo.org/
-.. _`Pelican documentation`: http://docs.getpelican.com/latest/
-.. _`Pelican's internals`: http://docs.getpelican.com/en/latest/internals.html
+.. _Markdown: https://daringfireball.net/projects/markdown/
+.. _Jinja2: https://palletsprojects.com/p/jinja/
+.. _`Pelican documentation`: https://docs.getpelican.com/latest/
+.. _`Pelican's internals`: https://docs.getpelican.com/en/latest/internals.html
.. _`Pelican plugins`: https://github.com/getpelican/pelican-plugins
diff --git a/docs/install.rst b/docs/install.rst
index 571de95e..03480e79 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -5,10 +5,14 @@ Pelican currently runs best on Python 2.7.x and 3.5+; earlier versions of
Python are not supported.
You can install Pelican via several different methods. The simplest is via
-`pip `_::
+`pip `_::
pip install pelican
+Or, if you plan on using Markdown::
+
+ pip install pelican[Markdown]
+
(Keep in mind that operating systems will often require you to prefix the above
command with ``sudo`` in order to install Pelican system-wide.)
@@ -39,13 +43,17 @@ options. For more detail, refer to the :doc:`Publish` section.
Optional packages
-----------------
-If you plan on using `Markdown `_ as a
-markup format, you'll need to install the Markdown library::
+If you plan on using `Markdown `_ as a
+markup format, you can install Pelican with Markdown support::
+
+ pip install pelican[Markdown]
+
+Or you might need to install it a posteriori::
pip install Markdown
Typographical enhancements can be enabled in your settings file, but first the
-requisite `Typogrify `_ library must be
+requisite `Typogrify `_ library must be
installed::
pip install typogrify
@@ -56,22 +64,22 @@ Dependencies
When Pelican is installed, the following dependent Python packages should be
automatically installed without any action on your part:
-* `feedgenerator `_, to generate the
+* `feedgenerator `_, to generate the
Atom feeds
-* `jinja2 `_, for templating support
-* `pygments `_, for syntax highlighting
-* `docutils `_, for supporting
+* `jinja2 `_, for templating support
+* `pygments `_, for syntax highlighting
+* `docutils `_, for supporting
reStructuredText as an input format
-* `pytz `_, for timezone definitions
-* `blinker `_, an object-to-object and
+* `pytz `_, for timezone definitions
+* `blinker `_, an object-to-object and
broadcast signaling system
-* `unidecode `_, for ASCII
+* `unidecode `_, for ASCII
transliterations of Unicode text
-* `six `_, for Python 2 and 3 compatibility
+* `six `_, for Python 2 and 3 compatibility
utilities
-* `MarkupSafe `_, for a markup safe
+* `MarkupSafe `_, for a markup safe
string implementation
-* `python-dateutil `_, to read
+* `python-dateutil `_, to read
the date metadata
Upgrading
@@ -94,6 +102,13 @@ your site::
pelican-quickstart
+If run inside an activated virtual environment, ``pelican-quickstart`` will
+look for an associated project path inside ``$VIRTUAL_ENV/.project``. If that
+file exists and contains a valid directory path, the new Pelican project will
+be saved at that location. Otherwise, the default is the current working
+directory. To set the new project path on initial invocation, use:
+``pelican-quickstart --path /your/desired/directory``
+
Once you finish answering all the questions, your project will consist of the
following hierarchy (except for *pages* — shown in parentheses below — which
you can optionally add yourself if you plan to create non-chronological
@@ -111,4 +126,4 @@ content)::
The next step is to begin to adding content to the *content* folder that has
been created for you.
-.. _virtualenv: http://www.virtualenv.org/
+.. _virtualenv: https://virtualenv.pypa.io/en/latest/
diff --git a/docs/internals.rst b/docs/internals.rst
index e8d35148..5b41070e 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -32,7 +32,7 @@ The logic is separated into different classes and concepts:
inputs.
* Pelican also uses templates, so it's easy to write your own theme. The
- syntax is `Jinja2 `_ and is very easy to learn, so
+ syntax is `Jinja2 `_ and is very easy to learn, so
don't hesitate to jump in and build your own theme.
How to implement a new reader?
diff --git a/docs/pelican-themes.rst b/docs/pelican-themes.rst
index 2802777a..e63f6a19 100644
--- a/docs/pelican-themes.rst
+++ b/docs/pelican-themes.rst
@@ -6,7 +6,8 @@ pelican-themes
Description
===========
-``pelican-themes`` is a command line tool for managing themes for Pelican.
+``pelican-themes`` is a command line tool for managing themes for Pelican. See
+:ref:`settings/themes` for settings related to themes.
Usage
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 5a6f9e55..4d44eea7 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -9,16 +9,31 @@ features to Pelican without having to directly modify the Pelican core.
How to use plugins
==================
-To load plugins, you have to specify them in your settings file. There are two
-ways to do so. The first method is to specify strings with the path to the
-callables::
+Starting with version 5.0, Pelican moved to a new plugin structure utilizing
+namespace packages. Plugins supporting this structure will install under the
+namespace package ``pelican.plugins`` and can be automatically discovered
+by Pelican.
- PLUGINS = ['package.myplugin',]
+If you leave the ``PLUGINS`` setting as default (``None``), Pelican will then
+collect the namespace plugins and register them. If on the other hand you
+specify a ``PLUGINS`` settings as a list of plugins, this autodiscovery will
+be disabled and only listed plugins will be registered and you will have to
+explicitly list the namespace plugins as well.
-Alternatively, another method is to import them and add them to the list::
+If you are using ``PLUGINS`` setting, you can specify plugins in two ways.
+The first method specifies plugins as a list of strings. Namespace plugins can
+be specified either by their full names (``pelican.plugins.myplugin``) or by
+their short names (``myplugin``)::
+
+ PLUGINS = ['package.myplugin',
+ 'namespace_plugin1',
+ 'pelican.plugins.namespace_plugin2']
+
+Alternatively, you can import them in your settings file and pass the modules::
from package import myplugin
- PLUGINS = [myplugin,]
+ from pelican.plugins import namespace_plugin1, namespace_plugin2
+ PLUGINS = [myplugin, namespace_plugin1, namespace_plugin2]
.. note::
@@ -36,11 +51,13 @@ the ``PLUGIN_PATHS`` list can be absolute or relative to the settings file::
Where to find plugins
=====================
+Namespace plugins can be found in the `pelican-plugins organization`_ as
+individual repositories. Legacy plugins are collected in the `pelican-plugins
+repository`_ and they will be slowly phased out in favor of the namespace
+versions.
-We maintain a separate repository of plugins for people to share and use.
-Please visit the `pelican-plugins`_ repository for a list of available plugins.
-
-.. _pelican-plugins: https://github.com/getpelican/pelican-plugins
+.. _pelican-plugins organization: https://github.com/pelican-plugins
+.. _pelican-plugins repository: https://github.com/getpelican/pelican-plugins
Please note that while we do our best to review and maintain these plugins,
they are submitted by the Pelican community and thus may have varying levels of
@@ -70,6 +87,33 @@ which you map the signals to your plugin logic. Let's take a simple example::
your ``register`` callable or they will be garbage-collected before the
signal is emitted.
+Namespace plugin structure
+--------------------------
+
+Namespace plugins must adhere to a certain structure in order to function
+properly. They need to be installable (i.e. contain ``setup.py`` or equivalent)
+and have a folder structure as follows::
+
+ myplugin
+ ├── pelican
+ │ └── plugins
+ │ └── myplugin
+ │ ├── __init__.py
+ │ └── ...
+ ├── ...
+ └── setup.py
+
+It is crucial that ``pelican`` or ``pelican/plugins`` folder **not**
+contain an ``__init__.py`` file. In fact, it is best to have those folders
+empty besides the listed folders in the above structure and keep your
+plugin related files contained solely in the ``pelican/plugins/myplugin``
+folder to avoid any issues.
+
+For easily setting up the proper structure, a `cookiecutter template for
+plugins`_ is provided. Refer to the README in the link for how to use it.
+
+.. _cookiecutter template for plugins: https://github.com/getpelican/cookiecutter-pelican-plugin
+
List of signals
===============
diff --git a/docs/publish.rst b/docs/publish.rst
index 4dc1e3ef..9bea8938 100644
--- a/docs/publish.rst
+++ b/docs/publish.rst
@@ -32,7 +32,12 @@ list of paths or can be configured as a setting. (See:
You can also tell Pelican to watch for your modifications, instead of manually
re-running it every time you want to see your changes. To enable this, run the
-``pelican`` command with the ``-r`` or ``--autoreload`` option.
+``pelican`` command with the ``-r`` or ``--autoreload`` option. On non-Windows
+environments, this option can also be combined with the ``-l`` or ``--listen``
+option to simultaneously both auto-regenerate *and* serve the output at
+http://localhost:8000::
+
+ pelican --autoreload --listen
Pelican has other command-line switches available. Have a look at the help to
see all the options you can use::
@@ -49,20 +54,12 @@ HTML files directly::
firefox output/index.html
Because the above method may have trouble locating your CSS and other linked
-assets, running a simple web server using Python will often provide a more
-reliable previewing experience.
+assets, running Pelican's simple built-in web server will often provide a more
+reliable previewing experience::
-For Python 2, run::
+ pelican --listen
- cd output
- python -m SimpleHTTPServer
-
-For Python 3, run::
-
- cd output
- python -m http.server
-
-Once the basic server has been started, you can preview your site at
+Once the web server has been started, you can preview your site at:
http://localhost:8000/
Deployment
@@ -141,6 +138,12 @@ http://localhost:8000/::
invoke serve
+To serve the generated site with automatic browser reloading every time a
+change is detected, first ``pip install livereload``, then use the
+following command::
+
+ invoke livereload
+
If during the ``pelican-quickstart`` process you answered "yes" when asked
whether you want to upload your site via SSH, you can use the following command
to publish your site via rsync over SSH::
@@ -204,4 +207,4 @@ That's it! Your site should now be live.
executables, such as ``python3``, you can set the ``PY`` and ``PELICAN``
environment variables, respectively, to override the default executable names.)
-.. _Invoke: http://www.pyinvoke.org
+.. _Invoke: https://www.pyinvoke.org/
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index a5fdfa60..1f6358a7 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -8,10 +8,10 @@ Installation
------------
Install Pelican (and optionally Markdown if you intend to use it) on Python
-2.7.x or Python 3.3+ by running the following command in your preferred
+2.7.x or Python 3.5+ by running the following command in your preferred
terminal, prefixing with ``sudo`` if permissions warrant::
- pip install pelican markdown
+ pip install pelican[Markdown]
Create a project
----------------
@@ -30,7 +30,7 @@ by asking some questions about your site::
For questions that have default values denoted in brackets, feel free to use
the Return key to accept those default values [#tzlocal_fn]_. When asked for
your URL prefix, enter your domain name as indicated (e.g.,
-``http://example.com``).
+``https://example.com``).
Create an article
-----------------
@@ -50,19 +50,19 @@ Given that this example article is in Markdown format, save it as
Generate your site
------------------
-From your site directory, run the ``pelican`` command to generate your site::
+From your project root directory, run the ``pelican`` command to generate your site::
pelican content
-Your site has now been generated inside the ``output`` directory. (You may see
+Your site has now been generated inside the ``output/`` directory. (You may see
a warning related to feeds, but that is normal when developing locally and can
be ignored for now.)
Preview your site
-----------------
-Open a new terminal session, navigate to your site directory and run the
-following command to launch Pelican's web server::
+Open a new terminal session, navigate to your project root directory, and
+run the following command to launch Pelican's web server::
pelican --listen
@@ -78,5 +78,5 @@ Footnotes
---------
.. [#tzlocal_fn] You can help localize default fields by installing the
- optional `tzlocal `_
+ optional `tzlocal `_
module.
diff --git a/docs/settings.rst b/docs/settings.rst
index 3652788b..befb6f05 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -101,7 +101,18 @@ Basic settings
JINJA_FILTERS = {'urlencode': urlencode_filter}
- See `Jinja custom filters documentation`_.
+ See: `Jinja custom filters documentation`_.
+
+.. data:: JINJA_GLOBALS = {}
+
+ A dictionary of custom objects to map into the Jinja2 global environment
+ namespace. The dictionary should map the global name to the global
+ variable/function. See: `Jinja global namespace documentation`_.
+
+.. data:: JINJA_TESTS = {}
+
+ A dictionary of custom Jinja2 tests you want to use. The dictionary should
+ map test names to test functions. See: `Jinja custom tests documentation`_.
.. data:: LOG_FILTER = []
@@ -152,7 +163,7 @@ Basic settings
}
.. Note::
- The dictionary defined in your settings file will update this default
+ The dictionary defined in your settings file will replace this default
one.
.. data:: OUTPUT_PATH = 'output/'
@@ -194,7 +205,7 @@ Basic settings
Controls the extension that will be used by the SourcesGenerator. Defaults
to ``.text``. If not a valid string the default value will be used.
-.. data:: PLUGINS = []
+.. data:: PLUGINS = None
The list of plugins to load. See :ref:`plugins`.
@@ -271,6 +282,11 @@ Basic settings
does not otherwise specify a summary. Setting to ``None`` will cause the
summary to be a copy of the original content.
+.. data:: SUMMARY_END_MARKER = '…'
+
+ When creating a short summary of an article and the result was truncated to
+ match the required word length, this will be used as the truncation marker.
+
.. data:: WITH_FUTURE_DATES = True
If disabled, content with dates in the future will get a default status of
@@ -362,16 +378,15 @@ variables allow you to place your articles in a location such as
example below). These settings give you the flexibility to place your articles
and pages anywhere you want.
-.. note::
- If you specify a ``datetime`` directive, it will be substituted using the
- input files' date metadata attribute. If the date is not specified for a
- particular file, Pelican will rely on the file's ``mtime`` timestamp. Check
- the `Python datetime documentation`_ for more information.
+If you don't want that flexibility and instead prefer that your generated
+output paths mirror your source content's filesystem path hierarchy, try the
+following settings::
-.. _Python datetime documentation:
- http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
+ PATH_METADATA = '(?P.*)\..*'
+ ARTICLE_URL = ARTICLE_SAVE_AS = PAGE_URL = PAGE_SAVE_AS = '{path_no_ext}.html'
-Also, you can use other file metadata attributes as well:
+Otherwise, you can use a variety of file metadata attributes within URL-related
+settings:
* slug
* date
@@ -391,6 +406,15 @@ This would save your articles into something like
``/pages/about/index.html``, and render them available at URLs of
``/posts/2011/Aug/07/sample-post/`` and ``/pages/about/``, respectively.
+.. note::
+ If you specify a ``datetime`` directive, it will be substituted using the
+ input files' date metadata attribute. If the date is not specified for a
+ particular file, Pelican will rely on the file's ``mtime`` timestamp. Check
+ the `Python datetime documentation`_ for more information.
+
+.. _Python datetime documentation:
+ https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
+
.. data:: RELATIVE_URLS = False
Defines whether Pelican should use document-relative URLs or not. Only set
@@ -467,6 +491,14 @@ This would save your articles into something like
The actual location a page draft which doesn't use the default language is
saved at.
+.. data:: AUTHOR_URL = 'author/{slug}.html'
+
+ The URL to use for an author.
+
+.. data:: AUTHOR_SAVE_AS = 'author/{slug}.html'
+
+ The location to save an author.
+
.. data:: CATEGORY_URL = 'category/{slug}.html'
The URL to use for a category.
@@ -483,41 +515,6 @@ This would save your articles into something like
The location to save the tag page.
-.. data:: AUTHOR_URL = 'author/{slug}.html'
-
- The URL to use for an author.
-
-.. data:: AUTHOR_SAVE_AS = 'author/{slug}.html'
-
- The location to save an author.
-
-.. data:: YEAR_ARCHIVE_SAVE_AS = ''
-
- The location to save per-year archives of your posts.
-
-.. data:: YEAR_ARCHIVE_URL = ''
-
- The URL to use for per-year archives of your posts. Used only if you have
- the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
-
-.. data:: MONTH_ARCHIVE_SAVE_AS = ''
-
- The location to save per-month archives of your posts.
-
-.. data:: MONTH_ARCHIVE_URL = ''
-
- The URL to use for per-month archives of your posts. Used only if you have
- the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
-
-.. data:: DAY_ARCHIVE_SAVE_AS = ''
-
- The location to save per-day archives of your posts.
-
-.. data:: DAY_ARCHIVE_URL = ''
-
- The URL to use for per-day archives of your posts. Used only if you have the
- ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
-
.. note::
If you do not want one or more of the default pages to be created (e.g.,
@@ -546,6 +543,33 @@ posts for the month at ``posts/2011/Aug/index.html``.
This way a reader can remove a portion of your URL and automatically arrive
at an appropriate archive of posts, without having to specify a page name.
+.. data:: YEAR_ARCHIVE_URL = ''
+
+ The URL to use for per-year archives of your posts. Used only if you have
+ the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: YEAR_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-year archives of your posts.
+
+.. data:: MONTH_ARCHIVE_URL = ''
+
+ The URL to use for per-month archives of your posts. Used only if you have
+ the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: MONTH_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-month archives of your posts.
+
+.. data:: DAY_ARCHIVE_URL = ''
+
+ The URL to use for per-day archives of your posts. Used only if you have the
+ ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: DAY_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-day archives of your posts.
+
``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the
``_SAVE_AS`` settings are available, but it is available for any direct
template.
@@ -554,18 +578,6 @@ template.
The location to save the article archives page.
-.. data:: YEAR_ARCHIVE_SAVE_AS = ''
-
- The location to save per-year archives of your posts.
-
-.. data:: MONTH_ARCHIVE_SAVE_AS = ''
-
- The location to save per-month archives of your posts.
-
-.. data:: DAY_ARCHIVE_SAVE_AS = ''
-
- The location to save per-day archives of your posts.
-
.. data:: AUTHORS_SAVE_AS = 'authors.html'
The location to save the author list.
@@ -632,7 +644,7 @@ Time and Date
Have a look at `the wikipedia page`_ to get a list of valid timezone values.
-.. _the wikipedia page: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+.. _the wikipedia page: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
.. data:: DEFAULT_DATE = None
@@ -657,7 +669,7 @@ Time and Date
the language name (``lang`` metadata in your post content) as the key.
In addition to the standard C89 strftime format codes that are listed in
- `Python strftime documentation`_, you can use the ``-`` character between
+ `Python datetime documentation`_, you can use the ``-`` character between
``%`` and the format character to remove any leading zeros. For example,
``%d/%m/%Y`` will output ``01/01/2014`` whereas ``%-d/%-m/%Y`` will result
in ``1/1/2014``.
@@ -708,11 +720,13 @@ Time and Date
.. [#] Default is the system locale.
-.. _Python strftime documentation: http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+.. _Jinja custom filters documentation: https://jinja.palletsprojects.com/en/master/api/#custom-filters
+.. _Jinja global namespace documentation: https://jinja.palletsprojects.com/en/master/api/#the-global-namespace
+.. _Jinja custom tests documentation: https://jinja.palletsprojects.com/en/master/api/#custom-tests
-.. _locales on Windows: http://msdn.microsoft.com/en-us/library/cdax410z%28VS.71%29.aspx
+.. _locales on Windows: https://www.microsoft.com/en-us/download/details.aspx?id=55979
-.. _locale(1): http://linux.die.net/man/1/locale
+.. _locale(1): https://linux.die.net/man/1/locale
.. _template_pages:
@@ -736,15 +750,15 @@ Template pages
'src/resume.html': 'dest/resume.html',
'src/contact.html': 'dest/contact.html'}
-.. data:: TEMPLATE_EXTENSION = ['.html']
+.. data:: TEMPLATE_EXTENSIONS = ['.html']
The extensions to use when looking up template files from template names.
-.. data:: DIRECT_TEMPLATES = ['index', 'categories', 'authors', 'archives']
+.. data:: DIRECT_TEMPLATES = ['index', 'authors', 'categories', 'tags', 'archives']
List of templates that are used directly to render content. Typically direct
templates are used to generate index pages for collections of content (e.g.,
- tags and category index pages). If the tag and category collections are not
+ category and tag index pages). If the author, category and tag collections are not
needed, set ``DIRECT_TEMPLATES = ['index', 'archives']``
``DIRECT_TEMPLATES`` are searched for over paths maintained in
@@ -823,7 +837,7 @@ file:
}
.. _group name notation:
- http://docs.python.org/3/library/re.html#regular-expression-syntax
+ https://docs.python.org/3/library/re.html#regular-expression-syntax
Feed settings
@@ -840,7 +854,7 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
The domain prepended to feed URLs. Since feed URLs should always be
absolute, it is highly recommended to define this (e.g.,
- "http://feeds.example.com"). If you have already explicitly defined SITEURL
+ "https://feeds.example.com"). If you have already explicitly defined SITEURL
(see above) and want to use the same domain for your feeds, you can just
set: ``FEED_DOMAIN = SITEURL``.
@@ -955,27 +969,6 @@ variables to ``None``.
.. [2] ``{slug}`` is replaced by name of the category / author / tag.
-FeedBurner
-----------
-
-If you want to use FeedBurner for your feed, you will likely need to decide
-upon a unique identifier. For example, if your site were called "Thyme" and
-hosted on the www.example.com domain, you might use "thymefeeds" as your unique
-identifier, which we'll use throughout this section for illustrative purposes.
-In your Pelican settings, set the ``FEED_ATOM`` attribute to
-``thymefeeds/main.xml`` to create an Atom feed with an original address of
-``http://www.example.com/thymefeeds/main.xml``. Set the ``FEED_DOMAIN``
-attribute to ``http://feeds.feedburner.com``, or ``http://feeds.example.com``
-if you are using a CNAME on your own domain (i.e., FeedBurner's "MyBrand"
-feature).
-
-There are two fields to configure in the `FeedBurner
-`_ interface: "Original Feed" and "Feed Address".
-In this example, the "Original Feed" would be
-``http://www.example.com/thymefeeds/main.xml`` and the "Feed Address" suffix
-would be ``thymefeeds/main.xml``.
-
-
Pagination
==========
@@ -1016,7 +1009,7 @@ By default, pages subsequent to ``.../foo.html`` are created as
``.../foo2.html``, etc. The ``PAGINATION_PATTERNS`` setting can be used to
change this. It takes a sequence of triples, where each triple consists of::
- (minimum_page, page_url, page_save_as,)
+ (minimum_page, page_url, page_save_as,)
For ``page_url`` and ``page_save_as``, you may use a number of variables.
``{url}`` and ``{save_as}`` correspond respectively to the ``*_URL`` and
@@ -1030,7 +1023,7 @@ subsequent pages at ``.../page/2/`` etc, you could set ``PAGINATION_PATTERNS``
as follows::
PAGINATION_PATTERNS = (
- (1, '{url}', '{save_as}`,
+ (1, '{url}', '{save_as}'),
(2, '{base_name}/page/{number}/', '{base_name}/page/{number}/index.html'),
)
@@ -1109,6 +1102,7 @@ Ordering content
will sort pages by their basename.
+.. _settings/themes:
Themes
======
@@ -1121,7 +1115,7 @@ themes.
Theme to use to produce the output. Can be a relative or absolute path to a
theme folder, or the name of a default theme or a theme installed via
- ``pelican-themes`` (see below).
+ :doc:`pelican-themes` (see below).
.. data:: THEME_STATIC_DIR = 'theme'
@@ -1271,7 +1265,7 @@ ignored. Simply populate the list with the log messages you want to hide, and
they will be filtered out.
For example::
-
+
import logging
LOG_FILTER = [(logging.WARN, 'TAG_SAVE_AS is set to False')]
@@ -1378,6 +1372,5 @@ Example settings
:language: python
-.. _Jinja custom filters documentation: http://jinja.pocoo.org/docs/api/#custom-filters
-.. _Jinja Environment documentation: http://jinja.pocoo.org/docs/dev/api/#jinja2.Environment
+.. _Jinja Environment documentation: https://jinja.palletsprojects.com/en/master/api/#jinja2.Environment
.. _Docutils Configuration: http://docutils.sourceforge.net/docs/user/config.html
diff --git a/docs/themes.rst b/docs/themes.rst
index dcd72d75..a2332615 100644
--- a/docs/themes.rst
+++ b/docs/themes.rst
@@ -4,7 +4,7 @@ Creating themes
###############
To generate its HTML output, Pelican uses the `Jinja
-`_ templating engine due to its flexibility and
+`_ templating engine due to its flexibility and
straightforward syntax. If you want to create your own theme, feel free to take
inspiration from the `"simple" theme
`_.
@@ -75,16 +75,18 @@ output_file The name of the file currently being generated. For
articles The list of articles, ordered descending by date.
All the elements are `Article` objects, so you can
access their attributes (e.g. title, summary, author
- etc.). Sometimes this is shadowed (for instance in
+ etc.). Sometimes this is shadowed (for instance, in
the tags page). You will then find info about it
in the `all_articles` variable.
dates The same list of articles, but ordered by date,
ascending.
drafts The list of draft articles
-tags A list of (tag, articles) tuples, containing all
- the tags.
+authors A list of (author, articles) tuples, containing all
+ the authors and corresponding articles (values)
categories A list of (category, articles) tuples, containing
all the categories and corresponding articles (values)
+tags A list of (tag, articles) tuples, containing all
+ the tags and corresponding articles (values)
pages The list of pages
hidden_pages The list of hidden pages
draft_pages The list of draft pages
@@ -102,7 +104,7 @@ that allow them to be easily sorted by name::
If you want to sort based on different criteria, `Jinja's sort command`__ has a
number of options.
-__ http://jinja.pocoo.org/docs/templates/#sort
+__ https://jinja.palletsprojects.com/en/master/templates/#sort
Date Formatting
@@ -118,8 +120,8 @@ your date according to the locale given in your settings::
{{ article.date|strftime('%d %B %Y') }}
-.. _datetime: http://docs.python.org/2/library/datetime.html#datetime-objects
-.. _strftime: http://docs.python.org/2/library/datetime.html#strftime-strptime-behavior
+.. _datetime: https://docs.python.org/3/library/datetime.html#datetime-objects
+.. _strftime: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
index.html
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 97d5fffa..97135a62 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -1,27 +1,28 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import argparse
-import collections
-import locale
import logging
import multiprocessing
import os
import pprint
-import re
import sys
import time
import traceback
-
-import six
+from collections.abc import Iterable
+# Combines all paths to `pelican` package accessible from `sys.path`
+# Makes it possible to install `pelican` and namespace plugins into different
+# locations in the file system (e.g. pip with `-e` or `--user`)
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
# pelican.log has to be the first pelican module to be loaded
# because logging.setLoggerClass has to be called before logging.getLogger
from pelican.log import init as init_logging
-from pelican import signals # noqa
-from pelican.generators import (ArticlesGenerator, PagesGenerator,
- SourceFileGenerator, StaticGenerator,
- TemplatePagesGenerator)
+from pelican.generators import (ArticlesGenerator, # noqa: I100
+ PagesGenerator, SourceFileGenerator,
+ StaticGenerator, TemplatePagesGenerator)
+from pelican.plugins import signals
+from pelican.plugins._utils import load_plugins
from pelican.readers import Readers
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
from pelican.settings import read_settings
@@ -29,7 +30,12 @@ from pelican.utils import (clean_output_dir, file_watcher,
folder_watcher, maybe_pluralize)
from pelican.writers import Writer
-__version__ = "4.0.2.dev0"
+try:
+ __version__ = __import__('pkg_resources') \
+ .get_distribution('pelican').version
+except Exception:
+ __version__ = "unknown"
+
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
logger = logging.getLogger(__name__)
@@ -44,7 +50,6 @@ class Pelican(object):
# define the default settings
self.settings = settings
- self._handle_deprecation()
self.path = settings['PATH']
self.theme = settings['THEME']
@@ -63,87 +68,14 @@ class Pelican(object):
sys.path.insert(0, '')
def init_plugins(self):
- self.plugins = []
- logger.debug('Temporarily adding PLUGIN_PATHS to system path')
- _sys_path = sys.path[:]
- for pluginpath in self.settings['PLUGIN_PATHS']:
- sys.path.insert(0, pluginpath)
- for plugin in self.settings['PLUGINS']:
- # if it's a string, then import it
- if isinstance(plugin, six.string_types):
- logger.debug("Loading plugin `%s`", plugin)
- try:
- plugin = __import__(plugin, globals(), locals(),
- str('module'))
- except ImportError as e:
- logger.error(
- "Cannot load plugin `%s`\n%s", plugin, e)
- continue
-
- logger.debug("Registering plugin `%s`", plugin.__name__)
- plugin.register()
- self.plugins.append(plugin)
- logger.debug('Restoring system path')
- sys.path = _sys_path
-
- def _handle_deprecation(self):
-
- if self.settings.get('CLEAN_URLS', False):
- logger.warning('Found deprecated `CLEAN_URLS` in settings.'
- ' Modifying 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'):
- logger.warning("%s = '%s'", setting, self.settings[setting])
-
- if self.settings.get('AUTORELOAD_IGNORE_CACHE'):
- logger.warning('Found deprecated `AUTORELOAD_IGNORE_CACHE` in '
- 'settings. Use --ignore-cache instead.')
- self.settings.pop('AUTORELOAD_IGNORE_CACHE')
-
- if self.settings.get('ARTICLE_PERMALINK_STRUCTURE', False):
- logger.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in'
- ' settings. Modifying the following settings for'
- ' the same behaviour.')
-
- structure = self.settings['ARTICLE_PERMALINK_STRUCTURE']
-
- # Convert %(variable) into {variable}.
- structure = re.sub(r'%\((\w+)\)s', r'{\g<1>}', structure)
-
- # Convert %x into {date:%x} for strftime
- structure = re.sub(r'(%[A-z])', r'{date:\g<1>}', structure)
-
- # Strip a / prefix
- structure = re.sub('^/', '', structure)
-
- for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL',
- 'PAGE_LANG_URL', 'DRAFT_URL', 'DRAFT_LANG_URL',
- 'ARTICLE_SAVE_AS', 'ARTICLE_LANG_SAVE_AS',
- 'DRAFT_SAVE_AS', 'DRAFT_LANG_SAVE_AS',
- 'PAGE_SAVE_AS', 'PAGE_LANG_SAVE_AS'):
- self.settings[setting] = os.path.join(structure,
- self.settings[setting])
- logger.warning("%s = '%s'", setting, self.settings[setting])
-
- for new, old in [('FEED', 'FEED_ATOM'), ('TAG_FEED', 'TAG_FEED_ATOM'),
- ('CATEGORY_FEED', 'CATEGORY_FEED_ATOM'),
- ('TRANSLATION_FEED', 'TRANSLATION_FEED_ATOM')]:
- if self.settings.get(new, False):
- logger.warning(
- 'Found deprecated `%(new)s` in settings. Modify %(new)s '
- 'to %(old)s in your settings and theme for the same '
- 'behavior. Temporarily setting %(old)s for backwards '
- 'compatibility.',
- {'new': new, 'old': old}
- )
- self.settings[old] = self.settings[new]
+ self.plugins = load_plugins(self.settings)
+ for plugin in self.plugins:
+ logger.debug('Registering plugin `%s`', plugin.__name__)
+ try:
+ plugin.register()
+ except Exception as e:
+ logger.error('Cannot register plugin `%s`\n%s',
+ plugin.__name__, e)
def run(self):
"""Run the generators and return"""
@@ -242,7 +174,7 @@ class Pelican(object):
for pair in signals.get_generators.send(self):
(funct, value) = pair
- if not isinstance(value, collections.Iterable):
+ if not isinstance(value, Iterable):
value = (value, )
for v in value:
@@ -299,7 +231,7 @@ class PrintSettings(argparse.Action):
parser.exit()
-def parse_arguments():
+def parse_arguments(argv=None):
parser = argparse.ArgumentParser(
description='A tool to generate a static blog, '
' with restructured text input files.',
@@ -392,7 +324,7 @@ def parse_arguments():
help='IP to bind to when serving files via HTTP '
'(default: 127.0.0.1)')
- args = parser.parse_args()
+ args = parser.parse_args(argv)
if args.port is not None and not args.listen:
logger.warning('--port without --listen has no effect')
@@ -428,15 +360,6 @@ def get_config(args):
config['BIND'] = args.bind
config['DEBUG'] = args.verbosity == logging.DEBUG
- # argparse returns bytes in Py2. There is no definite answer as to which
- # encoding argparse (or sys.argv) uses.
- # "Best" option seems to be locale.getpreferredencoding()
- # http://mail.python.org/pipermail/python-list/2006-October/405766.html
- if not six.PY3:
- enc = locale.getpreferredencoding()
- for key in config:
- if key in ('PATH', 'OUTPUT_PATH', 'THEME'):
- config[key] = config[key].decode(enc)
return config
@@ -450,7 +373,7 @@ def get_instance(args):
settings = read_settings(config_file, override=get_config(args))
cls = settings['PELICAN_CLASS']
- if isinstance(cls, six.string_types):
+ if isinstance(cls, str):
module, cls_name = cls.rsplit('.', 1)
module = __import__(module)
cls = getattr(module, cls_name)
@@ -543,17 +466,22 @@ def listen(server, port, output, excqueue=None):
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
return
- logging.info("Serving at port %s, server %s.", port, server)
try:
+ print("\nServing site at: {}:{} - Tap CTRL-C to stop".format(
+ server, port))
httpd.serve_forever()
except Exception as e:
if excqueue is not None:
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
return
+ except KeyboardInterrupt:
+ print("\nKeyboard interrupt received. Shutting down server.")
+ httpd.socket.close()
-def main():
- args = parse_arguments()
+
+def main(argv=None):
+ args = parse_arguments(argv)
logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level)
init_logging(args.verbosity, args.fatal,
logs_dedup_min_level=logs_dedup_min_level)
diff --git a/pelican/__main__.py b/pelican/__main__.py
new file mode 100644
index 00000000..69a5b95d
--- /dev/null
+++ b/pelican/__main__.py
@@ -0,0 +1,9 @@
+"""
+python -m pelican module entry point to run via python -m
+"""
+
+from . import main
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pelican/cache.py b/pelican/cache.py
index e6c10cb9..f6adbc35 100644
--- a/pelican/cache.py
+++ b/pelican/cache.py
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
import hashlib
import logging
import os
-
-from six.moves import cPickle as pickle
+import pickle
from pelican.utils import mkdir_p
@@ -82,9 +80,7 @@ class FileStampDataCacher(FileDataCacher):
and base path for filestamping operations
"""
- super(FileStampDataCacher, self).__init__(settings, cache_name,
- caching_policy,
- load_policy)
+ super().__init__(settings, cache_name, caching_policy, load_policy)
method = self.settings['CHECK_MODIFIED_METHOD']
if method == 'mtime':
@@ -106,7 +102,7 @@ class FileStampDataCacher(FileDataCacher):
def cache_data(self, filename, data):
"""Cache stamp and data for the given file"""
stamp = self._get_file_stamp(filename)
- super(FileStampDataCacher, self).cache_data(filename, (stamp, data))
+ super().cache_data(filename, (stamp, data))
def _get_file_stamp(self, filename):
"""Check if the given file has been modified
@@ -134,8 +130,7 @@ class FileStampDataCacher(FileDataCacher):
and current file stamp.
"""
- stamp, data = super(FileStampDataCacher, self).get_cached_data(
- filename, (None, default))
+ stamp, data = super().get_cached_data(filename, (None, default))
if stamp != self._get_file_stamp(filename):
return default
return data
diff --git a/pelican/contents.py b/pelican/contents.py
index 235c814a..620fa304 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -1,25 +1,20 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import copy
+import datetime
import locale
import logging
import os
import re
-import sys
+from urllib.parse import urljoin, urlparse, urlunparse
import pytz
-import six
-from six.moves.urllib.parse import urljoin, urlparse, urlunparse
-
-from pelican import signals
+from pelican.plugins import signals
from pelican.settings import DEFAULT_CONFIG
-from pelican.utils import (SafeDatetime, deprecated_attribute, memoized,
- path_to_url, posixize_path,
- python_2_unicode_compatible, sanitised_join,
- set_date_tzinfo, slugify, strftime,
- truncate_html_words)
+from pelican.utils import (deprecated_attribute, memoized, path_to_url,
+ posixize_path, sanitised_join, set_date_tzinfo,
+ slugify, truncate_html_words)
# Import these so that they're avalaible when you import from pelican.contents.
from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
@@ -27,7 +22,6 @@ from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
logger = logging.getLogger(__name__)
-@python_2_unicode_compatible
class Content(object):
"""Represents a content.
@@ -121,9 +115,6 @@ class Content(object):
if isinstance(self.date_format, tuple):
locale_string = self.date_format[0]
- if sys.version_info < (3, ) and isinstance(locale_string,
- six.text_type):
- locale_string = locale_string.encode('ascii')
locale.setlocale(locale.LC_ALL, locale_string)
self.date_format = self.date_format[1]
@@ -133,15 +124,16 @@ class Content(object):
if hasattr(self, 'date'):
self.date = set_date_tzinfo(self.date, timezone)
- self.locale_date = strftime(self.date, self.date_format)
+ self.locale_date = self.date.strftime(self.date_format)
if hasattr(self, 'modified'):
self.modified = set_date_tzinfo(self.modified, timezone)
- self.locale_modified = strftime(self.modified, self.date_format)
+ self.locale_modified = self.modified.strftime(self.date_format)
# manage status
if not hasattr(self, 'status'):
- self.status = getattr(self, 'default_status', None)
+ # Previous default of None broke comment plugins and perhaps others
+ self.status = getattr(self, 'default_status', '')
# store the summary metadata if it is set
if 'summary' in metadata:
@@ -212,7 +204,7 @@ class Content(object):
'path': path_to_url(path),
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
- 'date': getattr(self, 'date', SafeDatetime.now()),
+ 'date': getattr(self, 'date', datetime.datetime.now()),
'author': self.author.slug if hasattr(self, 'author') else '',
'category': self.category.slug if hasattr(self, 'category') else ''
})
@@ -282,7 +274,7 @@ class Content(object):
if linked_content:
logger.warning(
'{filename} used for linking to static'
- 'content %s in %s. Use {static} instead',
+ ' content %s in %s. Use {static} instead',
path,
self.get_relative_source_path())
return linked_content
@@ -398,7 +390,8 @@ class Content(object):
return self.content
return truncate_html_words(self.content,
- self.settings['SUMMARY_MAX_LENGTH'])
+ self.settings['SUMMARY_MAX_LENGTH'],
+ self.settings['SUMMARY_END_MARKER'])
@property
def summary(self):
@@ -496,7 +489,7 @@ class Page(Content):
def _expand_settings(self, key):
klass = 'draft_page' if self.status == 'draft' else None
- return super(Page, self)._expand_settings(key, klass)
+ return super()._expand_settings(key, klass)
class Article(Content):
@@ -506,34 +499,33 @@ class Article(Content):
default_template = 'article'
def __init__(self, *args, **kwargs):
- super(Article, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
# handle WITH_FUTURE_DATES (designate article to draft based on date)
if not self.settings['WITH_FUTURE_DATES'] and hasattr(self, 'date'):
if self.date.tzinfo is None:
- now = SafeDatetime.now()
+ now = datetime.datetime.now()
else:
- now = SafeDatetime.utcnow().replace(tzinfo=pytz.utc)
+ now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
if self.date > now:
self.status = 'draft'
# if we are a draft and there is no date provided, set max datetime
if not hasattr(self, 'date') and self.status == 'draft':
- self.date = SafeDatetime.max
+ self.date = datetime.datetime.max
def _expand_settings(self, key):
klass = 'draft' if self.status == 'draft' else 'article'
- return super(Article, self)._expand_settings(key, klass)
+ return super()._expand_settings(key, klass)
-@python_2_unicode_compatible
class Static(Content):
mandatory_properties = ('title',)
default_status = 'published'
default_template = None
def __init__(self, *args, **kwargs):
- super(Static, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self._output_location_referenced = False
@deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
@@ -552,13 +544,13 @@ class Static(Content):
def url(self):
# Note when url has been referenced, so we can avoid overriding it.
self._output_location_referenced = True
- return super(Static, self).url
+ return super().url
@property
def save_as(self):
# Note when save_as has been referenced, so we can avoid overriding it.
self._output_location_referenced = True
- return super(Static, self).save_as
+ return super().save_as
def attach_to(self, content):
"""Override our output directory with that of the given content object.
diff --git a/pelican/generators.py b/pelican/generators.py
index 0e772a62..43f6d41a 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import calendar
import errno
import fnmatch
import logging
import os
-from codecs import open
from collections import defaultdict
from functools import partial
from itertools import chain, groupby
@@ -15,15 +13,12 @@ from operator import attrgetter
from jinja2 import (BaseLoader, ChoiceLoader, Environment, FileSystemLoader,
PrefixLoader, TemplateNotFound)
-import six
-
-from pelican import signals
from pelican.cache import FileStampDataCacher
from pelican.contents import Article, Page, Static
+from pelican.plugins import signals
from pelican.readers import Readers
from pelican.utils import (DateFormatter, copy, mkdir_p, order_content,
- posixize_path, process_translations,
- python_2_unicode_compatible)
+ posixize_path, process_translations)
logger = logging.getLogger(__name__)
@@ -33,7 +28,6 @@ class PelicanTemplateNotFound(Exception):
pass
-@python_2_unicode_compatible
class Generator(object):
"""Baseclass generator"""
@@ -84,6 +78,14 @@ class Generator(object):
custom_filters = self.settings['JINJA_FILTERS']
self.env.filters.update(custom_filters)
+ # get custom Jinja globals from user settings
+ custom_globals = self.settings['JINJA_GLOBALS']
+ self.env.globals.update(custom_globals)
+
+ # get custom Jinja tests from user settings
+ custom_tests = self.settings['JINJA_TESTS']
+ self.env.tests.update(custom_tests)
+
signals.generator_init.send(self)
def get_template(self, name):
@@ -138,7 +140,7 @@ class Generator(object):
extensions are allowed)
"""
# backward compatibility for older generators
- if isinstance(paths, six.string_types):
+ if isinstance(paths, str):
paths = [paths]
# group the exclude dir names by parent path, for use with os.walk()
@@ -155,16 +157,15 @@ class Generator(object):
if os.path.isdir(root):
for dirpath, dirs, temp_files in os.walk(
- root, followlinks=True):
- drop = []
+ root, topdown=True, followlinks=True):
excl = exclusions_by_dirpath.get(dirpath, ())
- for d in dirs:
+ # We copy the `dirs` list as we will modify it in the loop:
+ for d in list(dirs):
if (d in excl or
any(fnmatch.fnmatch(d, ignore)
for ignore in ignores)):
- drop.append(d)
- for d in drop:
- dirs.remove(d)
+ if d in dirs:
+ dirs.remove(d)
reldir = os.path.relpath(dirpath, self.path)
for f in temp_files:
@@ -248,7 +249,7 @@ class CachingGenerator(Generator, FileStampDataCacher):
def _get_file_stamp(self, filename):
'''Get filestamp for path relative to generator.path'''
filename = os.path.join(self.path, filename)
- return super(CachingGenerator, self)._get_file_stamp(filename)
+ return super()._get_file_stamp(filename)
class _FileLoader(BaseLoader):
@@ -295,7 +296,7 @@ class ArticlesGenerator(CachingGenerator):
self.authors = defaultdict(list)
self.drafts = [] # only drafts in default language
self.drafts_translations = []
- super(ArticlesGenerator, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
signals.article_generator_init.send(self)
def generate_feeds(self, writer):
@@ -514,8 +515,6 @@ class ArticlesGenerator(CachingGenerator):
context["period"] = (_period,)
else:
month_name = calendar.month_name[_period[1]]
- if not six.PY3:
- month_name = month_name.decode('utf-8')
if key == period_date_key['month']:
context["period"] = (_period[0],
month_name)
@@ -526,7 +525,7 @@ class ArticlesGenerator(CachingGenerator):
write(save_as, template, context, articles=articles,
dates=archive, template_name='period_archives',
- blog=True, url=url)
+ blog=True, url=url, all_articles=self.articles)
for period in 'year', 'month', 'day':
save_as = period_save_as[period]
@@ -708,7 +707,7 @@ class PagesGenerator(CachingGenerator):
self.hidden_translations = []
self.draft_pages = []
self.draft_translations = []
- super(PagesGenerator, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
signals.page_generator_init.send(self)
def generate_context(self):
@@ -794,7 +793,7 @@ class StaticGenerator(Generator):
to output"""
def __init__(self, *args, **kwargs):
- super(StaticGenerator, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self.fallback_to_symlinks = False
signals.static_generator_init.send(self)
diff --git a/pelican/log.py b/pelican/log.py
index e7a6f317..0576af2a 100644
--- a/pelican/log.py
+++ b/pelican/log.py
@@ -1,13 +1,9 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
-import locale
import logging
import os
import sys
-from collections import Mapping, defaultdict
-
-import six
+from collections import defaultdict
__all__ = [
'init'
@@ -17,7 +13,7 @@ __all__ = [
class BaseFormatter(logging.Formatter):
def __init__(self, fmt=None, datefmt=None):
FORMAT = '%(customlevelname)s %(message)s'
- super(BaseFormatter, self).__init__(fmt=FORMAT, datefmt=datefmt)
+ super().__init__(fmt=FORMAT, datefmt=datefmt)
def format(self, record):
customlevel = self._get_levelname(record.levelname)
@@ -25,20 +21,17 @@ class BaseFormatter(logging.Formatter):
# format multiline messages 'nicely' to make it clear they are together
record.msg = record.msg.replace('\n', '\n | ')
record.args = tuple(arg.replace('\n', '\n | ') if
- isinstance(arg, six.string_types) else
+ isinstance(arg, str) else
arg for arg in record.args)
- return super(BaseFormatter, self).format(record)
+ return super().format(record)
def formatException(self, ei):
''' prefix traceback info for better representation '''
- # .formatException returns a bytestring in py2 and unicode in py3
- # since .format will handle unicode conversion,
- # str() calls are used to normalize formatting string
- s = super(BaseFormatter, self).formatException(ei)
+ s = super().formatException(ei)
# fancy format traceback
- s = str('\n').join(str(' | ') + line for line in s.splitlines())
+ s = '\n'.join(' | ' + line for line in s.splitlines())
# separate the traceback from the preceding lines
- s = str(' |___\n{}').format(s)
+ s = ' |___\n{}'.format(s)
return s
def _get_levelname(self, name):
@@ -117,11 +110,13 @@ class LimitFilter(logging.Filter):
else:
self._raised_messages.add(message_key)
- # ignore LOG_FILTER records by templates when "debug" isn't enabled
+ # ignore LOG_FILTER records by templates or messages
+ # when "debug" isn't enabled
logger_level = logging.getLogger().getEffectiveLevel()
if logger_level > logging.DEBUG:
- ignore_key = (record.levelno, record.msg)
- if ignore_key in self._ignore:
+ template_key = (record.levelno, record.msg)
+ message_key = (record.levelno, record.getMessage())
+ if (template_key in self._ignore or message_key in self._ignore):
return False
# check if we went over threshold
@@ -136,41 +131,7 @@ class LimitFilter(logging.Filter):
return True
-class SafeLogger(logging.Logger):
- """
- Base Logger which properly encodes Exceptions in Py2
- """
- _exc_encoding = locale.getpreferredencoding()
-
- def _log(self, level, msg, args, exc_info=None, extra=None):
- # if the only argument is a Mapping, Logger uses that for formatting
- # format values for that case
- if args and len(args) == 1 and isinstance(args[0], Mapping):
- args = ({k: self._decode_arg(v) for k, v in args[0].items()},)
- # otherwise, format each arg
- else:
- args = tuple(self._decode_arg(arg) for arg in args)
- super(SafeLogger, self)._log(
- level, msg, args, exc_info=exc_info, extra=extra)
-
- def _decode_arg(self, arg):
- '''
- properly decode an arg for Py2 if it's Exception
-
-
- localized systems have errors in native language if locale is set
- so convert the message to unicode with the correct encoding
- '''
- if isinstance(arg, Exception):
- text = str('%s: %s') % (arg.__class__.__name__, arg)
- if six.PY2:
- text = text.decode(self._exc_encoding)
- return text
- else:
- return arg
-
-
-class LimitLogger(SafeLogger):
+class LimitLogger(logging.Logger):
"""
A logger which adds LimitFilter automatically
"""
@@ -178,7 +139,7 @@ class LimitLogger(SafeLogger):
limit_filter = LimitFilter()
def __init__(self, *args, **kwargs):
- super(LimitLogger, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self.enable_filter()
def disable_filter(self):
@@ -193,12 +154,12 @@ class FatalLogger(LimitLogger):
errors_fatal = False
def warning(self, *args, **kwargs):
- super(FatalLogger, self).warning(*args, **kwargs)
+ super().warning(*args, **kwargs)
if FatalLogger.warnings_fatal:
raise RuntimeError('Warning encountered')
def error(self, *args, **kwargs):
- super(FatalLogger, self).error(*args, **kwargs)
+ super().error(*args, **kwargs)
if FatalLogger.errors_fatal:
raise RuntimeError('Error encountered')
diff --git a/pelican/paginator.py b/pelican/paginator.py
index fe63863e..61899056 100644
--- a/pelican/paginator.py
+++ b/pelican/paginator.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import functools
import logging
@@ -7,8 +6,6 @@ import os
from collections import namedtuple
from math import ceil
-import six
-
logger = logging.getLogger(__name__)
PaginationRule = namedtuple(
'PaginationRule',
@@ -131,7 +128,7 @@ class Page(object):
prop_value = getattr(rule, key)
- if not isinstance(prop_value, six.string_types):
+ if not isinstance(prop_value, str):
logger.warning('%s is set to %s', key, prop_value)
return prop_value
diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py
new file mode 100644
index 00000000..a3c9ee77
--- /dev/null
+++ b/pelican/plugins/_utils.py
@@ -0,0 +1,85 @@
+import importlib
+import importlib.machinery
+import importlib.util
+import logging
+import pkgutil
+
+
+logger = logging.getLogger(__name__)
+
+
+def iter_namespace(ns_pkg):
+ # Specifying the second argument (prefix) to iter_modules makes the
+ # returned name an absolute name instead of a relative one. This allows
+ # import_module to work without having to do additional modification to
+ # the name.
+ return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")
+
+
+def get_namespace_plugins(ns_pkg=None):
+ if ns_pkg is None:
+ import pelican.plugins as ns_pkg
+
+ return {
+ name: importlib.import_module(name)
+ for finder, name, ispkg
+ in iter_namespace(ns_pkg)
+ if ispkg
+ }
+
+
+def list_plugins(ns_pkg=None):
+ from pelican.log import init as init_logging
+ init_logging(logging.INFO)
+ ns_plugins = get_namespace_plugins(ns_pkg)
+ if ns_plugins:
+ logger.info('Plugins found:\n' + '\n'.join(ns_plugins))
+ else:
+ logger.info('No plugins are installed')
+
+
+def load_legacy_plugin(plugin, plugin_paths):
+ # Try to find plugin in PLUGIN_PATHS
+ spec = importlib.machinery.PathFinder.find_spec(plugin, plugin_paths)
+ if spec is None:
+ # If failed, try to find it in normal importable locations
+ spec = importlib.util.find_spec(plugin)
+ if spec is None:
+ raise ImportError('Cannot import plugin `{}`'.format(plugin))
+ else:
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+
+def load_plugins(settings):
+ logger.debug('Finding namespace plugins')
+ namespace_plugins = get_namespace_plugins()
+ if namespace_plugins:
+ logger.debug('Namespace plugins found:\n' +
+ '\n'.join(namespace_plugins))
+ plugins = []
+ if settings.get('PLUGINS') is not None:
+ for plugin in settings['PLUGINS']:
+ if isinstance(plugin, str):
+ logger.debug('Loading plugin `%s`', plugin)
+ # try to find in namespace plugins
+ if plugin in namespace_plugins:
+ plugin = namespace_plugins[plugin]
+ elif 'pelican.plugins.{}'.format(plugin) in namespace_plugins:
+ plugin = namespace_plugins['pelican.plugins.{}'.format(
+ plugin)]
+ # try to import it
+ else:
+ try:
+ plugin = load_legacy_plugin(
+ plugin,
+ settings.get('PLUGIN_PATHS', []))
+ except ImportError as e:
+ logger.error('Cannot load plugin `%s`\n%s', plugin, e)
+ continue
+ plugins.append(plugin)
+ else:
+ plugins = list(namespace_plugins.values())
+
+ return plugins
diff --git a/pelican/signals.py b/pelican/plugins/signals.py
similarity index 100%
rename from pelican/signals.py
rename to pelican/plugins/signals.py
diff --git a/pelican/readers.py b/pelican/readers.py
index 0edfed0e..6d9923f2 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
+import datetime
import logging
import os
import re
from collections import OrderedDict
+from html import escape
+from html.parser import HTMLParser
+from io import StringIO
import docutils
import docutils.core
@@ -12,16 +15,11 @@ import docutils.io
from docutils.parsers.rst.languages import get_language as get_docutils_lang
from docutils.writers.html4css1 import HTMLTranslator, Writer
-import six
-from six import StringIO
-from six.moves.html_parser import HTMLParser
-
from pelican import rstdirectives # NOQA
-from pelican import signals
from pelican.cache import FileStampDataCacher
from pelican.contents import Author, Category, Page, Tag
-from pelican.utils import SafeDatetime, escape_html, get_date, pelican_open, \
- posixize_path
+from pelican.plugins import signals
+from pelican.utils import get_date, pelican_open, posixize_path
try:
from markdown import Markdown
@@ -79,7 +77,7 @@ def ensure_metadata_list(text):
Regardless, all list items undergo .strip() before returning, and
empty items are discarded.
"""
- if isinstance(text, six.text_type):
+ if isinstance(text, str):
if ';' in text:
text = text.split(';')
else:
@@ -138,7 +136,7 @@ class BaseReader(object):
class _FieldBodyTranslator(HTMLTranslator):
def __init__(self, document):
- HTMLTranslator.__init__(self, document)
+ super().__init__(document)
self.compact_p = None
def astext(self):
@@ -160,7 +158,7 @@ def render_node_to_html(document, node, field_body_translator_class):
class PelicanHTMLWriter(Writer):
def __init__(self):
- Writer.__init__(self)
+ super().__init__()
self.translator_class = PelicanHTMLTranslator
@@ -203,21 +201,8 @@ class RstReader(BaseReader):
writer_class = PelicanHTMLWriter
field_body_translator_class = _FieldBodyTranslator
- class FileInput(docutils.io.FileInput):
- """Patch docutils.io.FileInput to remove "U" mode in py3.
-
- Universal newlines is enabled by default and "U" mode is deprecated
- in py3.
-
- """
-
- def __init__(self, *args, **kwargs):
- if six.PY3:
- kwargs['mode'] = kwargs.get('mode', 'r').replace('U', '')
- docutils.io.FileInput.__init__(self, *args, **kwargs)
-
def __init__(self, *args, **kwargs):
- super(RstReader, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
lang_code = self.settings.get('DEFAULT_LANG', 'en')
if get_docutils_lang(lang_code):
@@ -276,7 +261,6 @@ class RstReader(BaseReader):
pub = docutils.core.Publisher(
writer=self.writer_class(),
- source_class=self.FileInput,
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
pub.process_programmatic_settings(None, extra_params, None)
@@ -303,7 +287,7 @@ class MarkdownReader(BaseReader):
file_extensions = ['md', 'markdown', 'mkd', 'mdown']
def __init__(self, *args, **kwargs):
- super(MarkdownReader, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
settings = self.settings['MARKDOWN']
settings.setdefault('extension_configs', {})
settings.setdefault('extensions', [])
@@ -318,6 +302,9 @@ class MarkdownReader(BaseReader):
"""Return the dict containing document metadata"""
formatted_fields = self.settings['FORMATTED_FIELDS']
+ # prevent metadata extraction in fields
+ self._md.preprocessors.deregister('meta')
+
output = {}
for name, value in meta.items():
name = name.lower()
@@ -366,11 +353,7 @@ class HTMLReader(BaseReader):
class _HTMLParser(HTMLParser):
def __init__(self, settings, filename):
- try:
- # Python 3.5+
- HTMLParser.__init__(self, convert_charrefs=False)
- except TypeError:
- HTMLParser.__init__(self)
+ super().__init__(convert_charrefs=False)
self.body = ''
self.metadata = {}
self.settings = settings
@@ -407,7 +390,7 @@ class HTMLReader(BaseReader):
if self._in_head:
self._in_head = False
self._in_top_level = True
- elif tag == 'title':
+ elif self._in_head and tag == 'title':
self._in_title = False
self.metadata['title'] = self._data_buffer
elif tag == 'body':
@@ -415,7 +398,7 @@ class HTMLReader(BaseReader):
self._in_body = False
self._in_top_level = True
elif self._in_body:
- self._data_buffer += '{}>'.format(escape_html(tag))
+ self._data_buffer += '{}>'.format(escape(tag))
def handle_startendtag(self, tag, attrs):
if tag == 'meta' and self._in_head:
@@ -436,16 +419,16 @@ class HTMLReader(BaseReader):
self._data_buffer += '{};'.format(data)
def build_tag(self, tag, attrs, close_tag):
- result = '<{}'.format(escape_html(tag))
+ result = '<{}'.format(escape(tag))
for k, v in attrs:
- result += ' ' + escape_html(k)
+ result += ' ' + escape(k)
if v is not None:
# If the attribute value contains a double quote, surround
# with single quotes, otherwise use double quotes.
if '"' in v:
- result += "='{}'".format(escape_html(v, quote=False))
+ result += "='{}'".format(escape(v, quote=False))
else:
- result += '="{}"'.format(escape_html(v, quote=False))
+ result += '="{}"'.format(escape(v, quote=False))
if close_tag:
return result + ' />'
return result + '>'
@@ -543,9 +526,7 @@ class Readers(FileStampDataCacher):
self.settings['CONTENT_CACHING_LAYER'] == 'reader')
caching_policy = cache_this_level and self.settings['CACHE_CONTENT']
load_policy = cache_this_level and self.settings['LOAD_CONTENT_CACHE']
- super(Readers, self).__init__(settings, cache_name,
- caching_policy, load_policy,
- )
+ super().__init__(settings, cache_name, caching_policy, load_policy)
@property
def extensions(self):
@@ -685,10 +666,10 @@ def default_metadata(settings=None, process=None):
metadata['category'] = value
if settings.get('DEFAULT_DATE', None) and \
settings['DEFAULT_DATE'] != 'fs':
- if isinstance(settings['DEFAULT_DATE'], six.string_types):
+ if isinstance(settings['DEFAULT_DATE'], str):
metadata['date'] = get_date(settings['DEFAULT_DATE'])
else:
- metadata['date'] = SafeDatetime(*settings['DEFAULT_DATE'])
+ metadata['date'] = datetime.datetime(*settings['DEFAULT_DATE'])
return metadata
@@ -696,7 +677,7 @@ def path_metadata(full_path, source_path, settings=None):
metadata = {}
if settings:
if settings.get('DEFAULT_DATE', None) == 'fs':
- metadata['date'] = SafeDatetime.fromtimestamp(
+ metadata['date'] = datetime.datetime.fromtimestamp(
os.stat(full_path).st_mtime)
# Apply EXTRA_PATH_METADATA for the source path and the paths of any
@@ -731,7 +712,7 @@ def parse_path_metadata(source_path, settings=None, process=None):
... process=reader.process_metadata)
>>> pprint.pprint(metadata) # doctest: +ELLIPSIS
{'category': ,
- 'date': SafeDatetime(2013, 1, 1, 0, 0),
+ 'date': datetime.datetime(2013, 1, 1, 0, 0),
'slug': 'my-slug'}
"""
metadata = {}
diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py
index b4f44aa1..dda0e6a7 100644
--- a/pelican/rstdirectives.py
+++ b/pelican/rstdirectives.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import re
@@ -10,8 +9,6 @@ from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import TextLexer, get_lexer_by_name
-import six
-
import pelican.settings as pys
@@ -49,7 +46,7 @@ class Pygments(Directive):
# Fetch the defaults
if pys.PYGMENTS_RST_OPTIONS is not None:
- for k, v in six.iteritems(pys.PYGMENTS_RST_OPTIONS):
+ for k, v in pys.PYGMENTS_RST_OPTIONS.items():
# Locally set options overrides the defaults
if k not in self.options:
self.options[k] = v
diff --git a/pelican/server.py b/pelican/server.py
index 82ad70cd..8434ded5 100644
--- a/pelican/server.py
+++ b/pelican/server.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import argparse
import logging
@@ -7,16 +6,14 @@ import os
import posixpath
import ssl
import sys
+import urllib
+from http import server
try:
from magic import from_file as magic_from_file
except ImportError:
magic_from_file = None
-from six.moves import BaseHTTPServer
-from six.moves import SimpleHTTPServer as srvmod
-from six.moves import urllib
-
from pelican.log import init as init_logging
logger = logging.getLogger(__name__)
@@ -44,7 +41,7 @@ def parse_arguments():
return parser.parse_args()
-class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
+class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
SUFFIXES = ['.html', '/index.html', '/', '']
def translate_path(self, path):
@@ -76,7 +73,7 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
if not self.path:
return
- srvmod.SimpleHTTPRequestHandler.do_GET(self)
+ server.SimpleHTTPRequestHandler.do_GET(self)
def get_path_that_exists(self, original_path):
# Try to strip trailing slash
@@ -96,7 +93,7 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
def guess_type(self, path):
"""Guess at the mime type for the specified file.
"""
- mimetype = srvmod.SimpleHTTPRequestHandler.guess_type(self, path)
+ mimetype = server.SimpleHTTPRequestHandler.guess_type(self, path)
# If the default guess is too generic, try the python-magic library
if mimetype == 'application/octet-stream' and magic_from_file:
@@ -105,9 +102,9 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler):
return mimetype
-class RootedHTTPServer(BaseHTTPServer.HTTPServer):
+class RootedHTTPServer(server.HTTPServer):
def __init__(self, base_path, *args, **kwargs):
- BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
+ server.HTTPServer.__init__(self, *args, **kwargs)
self.RequestHandlerClass.base_path = base_path
@@ -138,6 +135,6 @@ if __name__ == '__main__':
args.port, args.server)
try:
httpd.serve_forever()
- except KeyboardInterrupt as e:
+ except KeyboardInterrupt:
logger.info("Shutting down server.")
httpd.socket.close()
diff --git a/pelican/settings.py b/pelican/settings.py
index a957b26c..c3b29ca5 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
import copy
+import importlib.util
import inspect
import locale
import logging
@@ -10,20 +10,14 @@ import re
from os.path import isabs
from posixpath import join as posix_join
-import six
-
from pelican.log import LimitFilter
-try:
- # SourceFileLoader is the recommended way in Python 3.3+
- from importlib.machinery import SourceFileLoader
- def load_source(name, path):
- return SourceFileLoader(name, path).load_module()
-except ImportError:
- # but it does not exist in Python 2.7, so fall back to imp
- import imp
- load_source = imp.load_source
+def load_source(name, path):
+ spec = importlib.util.spec_from_file_location(name, path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
logger = logging.getLogger(__name__)
@@ -126,6 +120,8 @@ DEFAULT_CONFIG = {
'output_format': 'html5',
},
'JINJA_FILTERS': {},
+ 'JINJA_GLOBALS': {},
+ 'JINJA_TESTS': {},
'JINJA_ENVIRONMENT': {
'trim_blocks': True,
'lstrip_blocks': True,
@@ -142,9 +138,10 @@ DEFAULT_CONFIG = {
'ARTICLE_PERMALINK_STRUCTURE': '',
'TYPOGRIFY': False,
'TYPOGRIFY_IGNORE_TAGS': [],
+ 'SUMMARY_END_MARKER': '…',
'SUMMARY_MAX_LENGTH': 50,
'PLUGIN_PATHS': [],
- 'PLUGINS': [],
+ 'PLUGINS': None,
'PYGMENTS_RST_OPTIONS': {},
'TEMPLATE_PAGES': {},
'TEMPLATE_EXTENSIONS': ['.html'],
@@ -166,7 +163,7 @@ DEFAULT_CONFIG = {
'WRITE_SELECTED': [],
'FORMATTED_FIELDS': ['summary'],
'PORT': 8000,
- 'BIND': '',
+ 'BIND': '127.0.0.1',
}
PYGMENTS_RST_OPTIONS = None
@@ -274,7 +271,7 @@ def handle_deprecated_settings(settings):
del settings['PLUGIN_PATH']
# PLUGIN_PATHS: str -> [str]
- if isinstance(settings.get('PLUGIN_PATHS'), six.string_types):
+ if isinstance(settings.get('PLUGIN_PATHS'), str):
logger.warning("Defining PLUGIN_PATHS setting as string "
"has been deprecated (should be a list)")
settings['PLUGIN_PATHS'] = [settings['PLUGIN_PATHS']]
@@ -438,6 +435,67 @@ def handle_deprecated_settings(settings):
'Falling back to default.', key)
settings[key] = DEFAULT_CONFIG[key]
+ # CLEAN_URLS
+ if settings.get('CLEAN_URLS', False):
+ logger.warning('Found deprecated `CLEAN_URLS` in settings.'
+ ' Modifying 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'):
+ logger.warning("%s = '%s'", setting, settings[setting])
+
+ # AUTORELOAD_IGNORE_CACHE -> --ignore-cache
+ if settings.get('AUTORELOAD_IGNORE_CACHE'):
+ logger.warning('Found deprecated `AUTORELOAD_IGNORE_CACHE` in '
+ 'settings. Use --ignore-cache instead.')
+ settings.pop('AUTORELOAD_IGNORE_CACHE')
+
+ # ARTICLE_PERMALINK_STRUCTURE
+ if settings.get('ARTICLE_PERMALINK_STRUCTURE', False):
+ logger.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in'
+ ' settings. Modifying the following settings for'
+ ' the same behaviour.')
+
+ structure = settings['ARTICLE_PERMALINK_STRUCTURE']
+
+ # Convert %(variable) into {variable}.
+ structure = re.sub(r'%\((\w+)\)s', r'{\g<1>}', structure)
+
+ # Convert %x into {date:%x} for strftime
+ structure = re.sub(r'(%[A-z])', r'{date:\g<1>}', structure)
+
+ # Strip a / prefix
+ structure = re.sub('^/', '', structure)
+
+ for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL',
+ 'PAGE_LANG_URL', 'DRAFT_URL', 'DRAFT_LANG_URL',
+ 'ARTICLE_SAVE_AS', 'ARTICLE_LANG_SAVE_AS',
+ 'DRAFT_SAVE_AS', 'DRAFT_LANG_SAVE_AS',
+ 'PAGE_SAVE_AS', 'PAGE_LANG_SAVE_AS'):
+ settings[setting] = os.path.join(structure,
+ settings[setting])
+ logger.warning("%s = '%s'", setting, settings[setting])
+
+ # {,TAG,CATEGORY,TRANSLATION}_FEED -> {,TAG,CATEGORY,TRANSLATION}_FEED_ATOM
+ for new, old in [('FEED', 'FEED_ATOM'), ('TAG_FEED', 'TAG_FEED_ATOM'),
+ ('CATEGORY_FEED', 'CATEGORY_FEED_ATOM'),
+ ('TRANSLATION_FEED', 'TRANSLATION_FEED_ATOM')]:
+ if settings.get(new, False):
+ logger.warning(
+ 'Found deprecated `%(new)s` in settings. Modify %(new)s '
+ 'to %(old)s in your settings and theme for the same '
+ 'behavior. Temporarily setting %(old)s for backwards '
+ 'compatibility.',
+ {'new': new, 'old': old}
+ )
+ settings[old] = settings[new]
+
return settings
@@ -482,13 +540,13 @@ def configure_settings(settings):
# standardize strings to lists
for key in ['LOCALE']:
- if key in settings and isinstance(settings[key], six.string_types):
+ if key in settings and isinstance(settings[key], str):
settings[key] = [settings[key]]
# check settings that must be a particular type
for key, types in [
- ('OUTPUT_SOURCES_EXTENSION', six.string_types),
- ('FILENAME_METADATA', six.string_types),
+ ('OUTPUT_SOURCES_EXTENSION', str),
+ ('FILENAME_METADATA', str),
]:
if key in settings and not isinstance(settings[key], types):
value = settings.pop(key)
@@ -582,7 +640,7 @@ def configure_settings(settings):
'PAGE_PATHS',
)
for PATH_KEY in filter(lambda k: k in settings, path_keys):
- if isinstance(settings[PATH_KEY], six.string_types):
+ if isinstance(settings[PATH_KEY], str):
logger.warning("Detected misconfiguration with %s setting "
"(must be a list), falling back to the default",
PATH_KEY)
diff --git a/pelican/tests/content/article_with_inline_svg.html b/pelican/tests/content/article_with_inline_svg.html
new file mode 100644
index 00000000..07f97a8a
--- /dev/null
+++ b/pelican/tests/content/article_with_inline_svg.html
@@ -0,0 +1,17 @@
+
+
+ Article with an inline SVG
+
+
+ Ensure that the title attribute in an inline svg is not handled as an HTML title.
+
+
+
diff --git a/pelican/tests/content/article_with_markdown_and_nested_metadata.md b/pelican/tests/content/article_with_markdown_and_nested_metadata.md
new file mode 100644
index 00000000..3968027b
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_nested_metadata.md
@@ -0,0 +1,5 @@
+Title: Article with markdown and nested summary metadata
+Date: 2012-10-30
+Summary: Test: This metadata value looks like metadata
+
+This is some content.
diff --git a/pelican/tests/default_conf.py b/pelican/tests/default_conf.py
index a567dc10..13014572 100644
--- a/pelican/tests/default_conf.py
+++ b/pelican/tests/default_conf.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function, unicode_literals
AUTHOR = 'Alexis Métaireau'
SITENAME = "Alexis' log"
SITEURL = 'http://blog.notmyidea.org'
diff --git a/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py
new file mode 100644
index 00000000..c514861d
--- /dev/null
+++ b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py
@@ -0,0 +1,5 @@
+NAME = 'namespace plugin'
+
+
+def register():
+ pass
diff --git a/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py
new file mode 100644
index 00000000..15896087
--- /dev/null
+++ b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py
@@ -0,0 +1,5 @@
+NAME = 'normal plugin'
+
+
+def register():
+ pass
diff --git a/pelican/tests/output/basic/a-markdown-powered-article.html b/pelican/tests/output/basic/a-markdown-powered-article.html
index 9053d5fe..400a639a 100644
--- a/pelican/tests/output/basic/a-markdown-powered-article.html
+++ b/pelican/tests/output/basic/a-markdown-powered-article.html
@@ -9,7 +9,7 @@
-
@@ -39,10 +39,10 @@ Anyone can see this page but it's not linked to anywhere!
diff --git a/pelican/tests/output/basic/pages/this-is-a-test-page.html b/pelican/tests/output/basic/pages/this-is-a-test-page.html
index ed9bce91..dfc6136a 100644
--- a/pelican/tests/output/basic/pages/this-is-a-test-page.html
+++ b/pelican/tests/output/basic/pages/this-is-a-test-page.html
@@ -9,7 +9,7 @@
-
@@ -54,7 +54,7 @@ pelican.conf, it will have nothing in default.
Testing more sourcecode directives
-
8 defrun(self): self.assert_has_content() 10 try: lexer=get_lexer_by_name(self.arguments[0]) 12 exceptValueError: # no lexer found - use the text one instead of an exception 14 lexer=TextLexer() 16 if('linenos'inself.optionsand self.options['linenos']notin('table','inline')): 18 self.options['linenos']='table' 20 forflagin('nowrap','nobackground','anchorlinenos'): ifflaginself.options: 22 self.options[flag]=True 24 # noclasses should already default to False, but just in case... formatter=HtmlFormatter(noclasses=False,**self.options) 26 parsed=highlight('\n'.join(self.content),lexer,formatter) return[nodes.raw('',parsed,format='html')]
+
8 defrun(self): self.assert_has_content() 10 try: lexer=get_lexer_by_name(self.arguments[0]) 12 exceptValueError: # no lexer found - use the text one instead of an exception 14 lexer=TextLexer() 16 if('linenos'inself.optionsand self.options['linenos']notin('table','inline')): 18 self.options['linenos']='table' 20 forflagin('nowrap','nobackground','anchorlinenos'): ifflaginself.options: 22 self.options[flag]=True 24 # noclasses should already default to False, but just in case... formatter=HtmlFormatter(noclasses=False,**self.options) 26 parsed=highlight('\n'.join(self.content),lexer,formatter) return[nodes.raw('',parsed,format='html')]
Lovely.
@@ -86,10 +86,10 @@ pelican.conf, it will have nothing in default.
diff --git a/pelican/tests/output/custom/a-markdown-powered-article.html b/pelican/tests/output/custom/a-markdown-powered-article.html
index 5c416f2f..e5fda57c 100644
--- a/pelican/tests/output/custom/a-markdown-powered-article.html
+++ b/pelican/tests/output/custom/a-markdown-powered-article.html
@@ -92,10 +92,10 @@