Merge branch 'getpelican:master' into Chinese-translation

This commit is contained in:
GeorgeHu 2023-11-19 20:05:28 +08:00 committed by GitHub
commit 4da471211a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
306 changed files with 8621 additions and 6489 deletions

View file

@ -1,3 +1,2 @@
[report]
omit = pelican/tests/*

View file

@ -9,7 +9,7 @@ insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
max_line_length = 79
max_line_length = 88
[*.{yml,yaml}]
indent_size = 2

7
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,7 @@
# .git-blame-ignore-revs
# Apply code style to project via: ruff format .
cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7
# Upgrade code base for Python 3.8 and above
ecd598f293161a52564aa6e8dfdcc8284dc93970
# Apply Ruff and pyupgrade to Jinja templates
db241feaa445375dc05e189e69287000ffe5fa8e

View file

@ -1,7 +1,8 @@
---
# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: true
contact_links:
- name: '💬 Pelican IRC Channel on Freenode'
url: https://kiwiirc.com/client/irc.freenode.net/?#pelican
- name: '💬 Pelican IRC Channel'
url: https://web.libera.chat/?#pelican
about: |
Chat with the community, ask questions, and learn about best practices.

65
.github/workflows/github_pages.yml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Deploy to GitHub Pages
on:
workflow_call:
inputs:
settings:
required: true
description: "The path to your Pelican settings file (`pelican`'s `--settings` option), for example: 'publishconf.py'"
type: string
requirements:
required: false
default: "pelican"
description: "The Python requirements to install, for example to enable markdown and typogrify use: 'pelican[markdown] typogrify' or if you have a requirements file use: '-r requirements.txt'"
type: string
output-path:
required: false
default: "output/"
description: "Where to output the generated files (`pelican`'s `--output` option)"
type: string
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Configure GitHub Pages
id: pages
uses: actions/configure-pages@v3
- name: Install requirements
run: pip install ${{ inputs.requirements }}
- name: Build Pelican site
run: |
pelican \
--settings "${{ inputs.settings }}" \
--extra-settings SITEURL='"${{ steps.pages.outputs.base_url }}"' \
--output "${{ inputs.output-path }}"
- name: Fix permissions
run: |
chmod -c -R +rX "${{ inputs.output-path }}" | while read line; do
echo "::warning title=Invalid file permissions automatically fixed::$line"
done
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ${{ inputs.output-path }}
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View file

@ -9,35 +9,25 @@ env:
jobs:
test:
name: Test - ${{ matrix.config.python }} - ${{ matrix.config.os }}
runs-on: ${{ matrix.config.os }}-latest
name: Test - ${{ matrix.python }} - ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
config:
- os: ubuntu
python: "3.7"
os: [ubuntu, macos, windows]
python: ["3.10", "3.11", "3.12"]
include:
- os: ubuntu
python: "3.8"
- os: ubuntu
python: "3.9"
- os: ubuntu
python: "3.10"
- os: ubuntu
python: "3.11"
- os: ubuntu
python: "3.12"
- os: macos
python: "3.7"
- os: windows
python: "3.7"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.config.python }}
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.config.python }}
python-version: ${{ matrix.python }}
cache: "pip"
cache-dependency-path: "**/requirements/*"
- name: Install locale (Linux)
@ -56,24 +46,42 @@ jobs:
echo "===== PANDOC ====="
pandoc --version | head -2
- name: Run tests
run: tox -e py${{ matrix.config.python }}
run: tox -e py${{ matrix.python }}
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
- uses: pdm-project/setup-pdm@v3
with:
python-version: "3.9"
cache: "pip"
cache-dependency-path: "**/requirements/*"
- name: Install tox
run: python -m pip install -U pip tox
- name: Check
run: tox -e flake8
python-version: 3.9
cache: true
cache-dependency-path: ./pyproject.toml
- name: Install dependencies
run: |
pdm install --no-default --dev
- name: Run linters
run: pdm lint --diff
- name: Run pre-commit checks on all files
uses: pre-commit/action@v3.0.0
build:
name: Test build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pdm-project/setup-pdm@v3
with:
python-version: 3.9
cache: true
cache-dependency-path: ./pyproject.toml
- name: Install dependencies
run: pdm install --dev
- name: Build package
run: pdm build
- name: Test build
run: pdm run pytest --check-build=dist pelican/tests/build_test
docs:
name: Build docs
@ -91,13 +99,18 @@ jobs:
run: python -m pip install -U pip tox
- name: Check
run: tox -e docs
- name: cache the docs for inspection
uses: actions/upload-artifact@v3
with:
name: docs
path: docs/_build/html/
deploy:
name: Deploy
environment: Deployment
needs: [test, lint, docs]
needs: [test, lint, docs, build]
runs-on: ubuntu-latest
if: github.ref=='refs/heads/master' && github.event_name!='pull_request'
if: github.ref=='refs/heads/master' && github.event_name!='pull_request' && github.repository == 'getpelican/pelican'
permissions:
contents: write

4
.gitignore vendored
View file

@ -15,4 +15,6 @@ htmlcov
venv
samples/output
*.pem
poetry.lock
*.lock
.pdm-python
.venv

View file

@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for info on hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
@ -13,13 +13,11 @@ repos:
- id: end-of-file-fixer
- id: forbid-new-submodules
- id: trailing-whitespace
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.5
hooks:
- id: flake8
name: Flake8 on commit diff
description: This hook limits Flake8 checks to changed lines of code.
entry: bash
args: [-c, 'git diff HEAD | flake8 --diff --max-line-length=88']
- id: ruff
- id: ruff-format
args: ["--check"]
exclude: ^pelican/tests/output/

View file

@ -79,6 +79,10 @@ don't spend a lot of time working on something that would be rejected for a
known reason. Consider also whether your new feature might be better suited as
a ':pelican-doc:`plugins` — you can `ask for help`_ to make that determination.
Also, if you intend to submit a pull request to address something for which there
is no existing issue, there is no need to create a new issue and then immediately
submit a pull request that closes it. You can submit the pull request by itself.
Using Git and GitHub
--------------------
@ -87,7 +91,8 @@ Using Git and GitHub
* **Don't put multiple unrelated fixes/features in the same branch / pull request.**
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.
request** for the bugfix. Similarly, any proposed changes to code style
formatting should be in a completely separate pull request.
* 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::
@ -106,15 +111,8 @@ Using Git and GitHub
detailed explanation (when relevant).
* `Squash your commits`_ to eliminate merge commits and ensure a clean and
readable commit history.
* If you have previously filed a GitHub issue and want to contribute code that
addresses that issue, **please use** ``hub pull-request`` instead of using
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 <https://github.com/github/hub/#installation>`_ and then run
`hub pull-request -i [ISSUE] <https://hub.github.com/hub-pull-request.1.html>`_
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
will run the test suite on all supported Python versions and check for code style
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.)
@ -122,13 +120,7 @@ Using Git and GitHub
Contribution quality standards
------------------------------
* Adhere to `PEP8 coding standards`_. This can be eased via the `pycodestyle
<https://pypi.org/project/pycodestyle>`_ or `flake8
<https://pypi.org/project/flake8/>`_ tools, the latter of which in
particular will give you some useful hints about ways in which the
code/formatting can be improved. We try to keep line length within the
79-character maximum specified by PEP8. Because that can sometimes compromise
readability, the hard/enforced maximum is 88 characters.
* Adhere to the project's code style standards. See: `Development Environment`_
* 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.
@ -142,6 +134,6 @@ need assistance or have any questions about these guidelines.
.. _`Create a new 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
.. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips
.. _`PEP8 coding standards`: https://www.python.org/dev/peps/pep-0008/
.. _`ask for help`: `How to get help`_
.. _`officially-supported Python releases`: https://devguide.python.org/#status-of-python-branches
.. _`Development Environment`: https://docs.getpelican.com/en/latest/contribute.html#setting-up-the-development-environment
.. _`officially-supported Python releases`: https://devguide.python.org/versions/#versions

View file

@ -1,6 +0,0 @@
include *.rst
recursive-include pelican *.html *.css *png *.rst *.markdown *.md *.mkd *.xml *.py *.jinja2
include LICENSE THANKS docs/changelog.rst pyproject.toml
graft samples
global-exclude __pycache__
global-exclude *.py[co]

View file

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="svg33" width="64" height="64" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.4142" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="g864" transform="matrix(.10228 0 0 .10228 2.441 6.0098e-5)"><g id="g4" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path2" d="m210.35 607.22c-.34-2.106-.842-4.303-1.491-6.591-1.537-5.441-4.918-10.074-9.506-13.854-1.205-1.001-2.503-1.946-3.88-2.823-5.293-3.381-11.692-5.851-18.522-7.32-4.588-.99-9.367-1.525-14.139-1.593-34.662-.774-56.234.387-90.373-.911.012.023.012.046.022.068 1.56 1.264 3.154 2.471 4.782 3.643 3.573 2.584 7.297 4.952 11.155 7.127 7.184 4.04 14.845 7.342 22.859 9.801.956.295 1.912.58 2.87.842 5.6.603 10.631 1.206 14.648 3.074 1.015.455 1.959 1.001 2.835 1.639 2.87 2.106 6.057 6.124 8.152 8.936 4.497 5.999 3.551 10.928 8.88 13.887.557.308 1.182.604 1.889.866 1.696.638 4.119 1.491 5.225-.91.16-.342.283-.764.387-1.264-.446-1.434-1.981-2.675-2.905-3.29-1.638-1.07-2.287-1.719-3.47-2.937-2.186-2.243-2.333-6.056-3.871-8.708 1.935-.82 12.146-2.186 14.287-1.89 4.576.204 8.185.557 10.939 3.392 1.08.854 1.672 1.594 2.652 2.334.069.057.125.114.194.159 4.338 3.153 8.343 4.28 11.894 5.362.936.284 1.822.558 2.69.876 1.332.478 2.582 1.048 3.754 1.81 1.39.922 3.748 3.336 3.849 5.419-3.496-1.116-1.185.296-6.342-.102-2.515-.285-5.087-.456-7.671-.638-4.018-.284-8.038-.581-11.805-1.297-.627-.115-1.254-.251-1.867-.399-.479-.102-.946-.227-1.401-.353-.011.193-.021.376-.021.546-.104 3.939 2.674 5.908-3.678 13.399-.057.08-.137.159-.205.25-1.686 1.97-10.449 5.715-13.182 6.432-11.634 2.334-20.502-5.237-34.515-1.423-4.929 1.833-8.549 9.824-10.815 15.8-3.016 7.936-5.406 17.576-8.139 27.06 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445.911-1.231 1.912-2.29 2.994-3.108.284-.217.58-.422.877-.603.215-.137.956-.286 2.127-.502 10.861-1.924 58.5-8.377 61.597-42.962.319-3.494.172-7.285-.513-11.372zm-106.94 18.59c-6.375-1.924-8.003-2.243-12.055-5.385.067.33.17.695.307 1.081 10.779 6.068 22.608 10.462 35.141 12.842-3.893-9.051-8.502-7.445-23.393-8.538zm29.518-4.099c-2.779-6.738-10.313-10.575-16.813-12.464-8.721-3.12-15.061-.125-33.458-8.811.147.239.284.467.432.694 3.575 2.584 7.297 4.963 11.157 7.126 7.184 4.041 14.844 7.343 22.857 9.802 4.167.489 8.175 1.184 11.863 2.96 1.639.773 3.21 1.764 4.702 3.039-.183-.82-.434-1.605-.74-2.346z" style="fill-rule:nonzero;fill:url(#_Linear1)"/></g><g id="g8" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path6" d="m114.13 595.61c-.958-.262-1.914-.547-2.87-.842-8.014-2.459-15.675-5.761-22.859-9.801-3.858-2.175-7.582-4.543-11.155-7.127-1.628-1.172-3.222-2.379-4.782-3.643 2.14 6.603 11.634 13.57 18.078 16.313 8.218 3.495 16.381 4.303 23.588 5.1z" style="fill:#90d4d1"/></g><g id="g12" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path10" d="m94.253 608.25c-3.86-2.163-7.582-4.542-11.157-7.126 10.006 15.823 22.575 15.584 34.014 16.928-8.013-2.459-15.673-5.761-22.857-9.802z" style="fill:#90d4d1"/></g><g id="g16" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path14" d="m126.81 634.34c-12.533-2.38-24.362-6.774-35.141-12.842 1.376 3.973 6.351 10.257 12.943 11.658 2.858 1.024 2.094.762 6.967.614 7.137-.364 10.552-.592 15.608 1.469-.126-.308-.251-.604-.377-.899z" style="fill:#90d4d1"/></g><g id="g20" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path18" d="m143.27 665.76c-.081.101-.159.204-.239.318-13.844 14.093-31.179 24.69-50.59 30.393 1.492-4.132 2.824-8.468 4.076-12.839 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445z" style="fill:#90d4d1"/></g><g id="g24" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path22" d="m143.03 666.08c-6.046 8.287-9.118 24.122-12.659 33.274-5.144 13.342-12.294 22.95-27.958 24.317-3.928.351-27.582 1.24-30.11-.035.159-1.344 4.098-2.961 5.123-3.747 6.852-4.847 11.416-13.5 15.014-23.416 19.411-5.703 36.746-16.3 50.59-30.393z" style="fill:#14a0c4"/></g></g><defs id="defs31"><linearGradient id="_Linear1" x2="1" gradientTransform="matrix(138.58 0 0 138.58 72.442 628.88)" gradientUnits="userSpaceOnUse"><stop id="stop26" style="stop-color:rgb(84,196,198)" offset="0"/><stop id="stop28" style="stop-color:rgb(18,186,213)" offset="1"/></linearGradient></defs></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="svg33" width="64" height="64" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.4142" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="g864" transform="matrix(.10228 0 0 .10228 2.441 6.0098e-5)"><g id="g4" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path2" d="m210.35 607.22c-.34-2.106-.842-4.303-1.491-6.591-1.537-5.441-4.918-10.074-9.506-13.854-1.205-1.001-2.503-1.946-3.88-2.823-5.293-3.381-11.692-5.851-18.522-7.32-4.588-.99-9.367-1.525-14.139-1.593-34.662-.774-56.234.387-90.373-.911.012.023.012.046.022.068 1.56 1.264 3.154 2.471 4.782 3.643 3.573 2.584 7.297 4.952 11.155 7.127 7.184 4.04 14.845 7.342 22.859 9.801.956.295 1.912.58 2.87.842 5.6.603 10.631 1.206 14.648 3.074 1.015.455 1.959 1.001 2.835 1.639 2.87 2.106 6.057 6.124 8.152 8.936 4.497 5.999 3.551 10.928 8.88 13.887.557.308 1.182.604 1.889.866 1.696.638 4.119 1.491 5.225-.91.16-.342.283-.764.387-1.264-.446-1.434-1.981-2.675-2.905-3.29-1.638-1.07-2.287-1.719-3.47-2.937-2.186-2.243-2.333-6.056-3.871-8.708 1.935-.82 12.146-2.186 14.287-1.89 4.576.204 8.185.557 10.939 3.392 1.08.854 1.672 1.594 2.652 2.334.069.057.125.114.194.159 4.338 3.153 8.343 4.28 11.894 5.362.936.284 1.822.558 2.69.876 1.332.478 2.582 1.048 3.754 1.81 1.39.922 3.748 3.336 3.849 5.419-3.496-1.116-1.185.296-6.342-.102-2.515-.285-5.087-.456-7.671-.638-4.018-.284-8.038-.581-11.805-1.297-.627-.115-1.254-.251-1.867-.399-.479-.102-.946-.227-1.401-.353-.011.193-.021.376-.021.546-.104 3.939 2.674 5.908-3.678 13.399-.057.08-.137.159-.205.25-1.686 1.97-10.449 5.715-13.182 6.432-11.634 2.334-20.502-5.237-34.515-1.423-4.929 1.833-8.549 9.824-10.815 15.8-3.016 7.936-5.406 17.576-8.139 27.06 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445.911-1.231 1.912-2.29 2.994-3.108.284-.217.58-.422.877-.603.215-.137.956-.286 2.127-.502 10.861-1.924 58.5-8.377 61.597-42.962.319-3.494.172-7.285-.513-11.372zm-106.94 18.59c-6.375-1.924-8.003-2.243-12.055-5.385.067.33.17.695.307 1.081 10.779 6.068 22.608 10.462 35.141 12.842-3.893-9.051-8.502-7.445-23.393-8.538zm29.518-4.099c-2.779-6.738-10.313-10.575-16.813-12.464-8.721-3.12-15.061-.125-33.458-8.811.147.239.284.467.432.694 3.575 2.584 7.297 4.963 11.157 7.126 7.184 4.041 14.844 7.343 22.857 9.802 4.167.489 8.175 1.184 11.863 2.96 1.639.773 3.21 1.764 4.702 3.039-.183-.82-.434-1.605-.74-2.346z" style="fill-rule:nonzero;fill:url(#_Linear1)"/></g><g id="g8" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path6" d="m114.13 595.61c-.958-.262-1.914-.547-2.87-.842-8.014-2.459-15.675-5.761-22.859-9.801-3.858-2.175-7.582-4.543-11.155-7.127-1.628-1.172-3.222-2.379-4.782-3.643 2.14 6.603 11.634 13.57 18.078 16.313 8.218 3.495 16.381 4.303 23.588 5.1z" style="fill:#90d4d1"/></g><g id="g12" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path10" d="m94.253 608.25c-3.86-2.163-7.582-4.542-11.157-7.126 10.006 15.823 22.575 15.584 34.014 16.928-8.013-2.459-15.673-5.761-22.857-9.802z" style="fill:#90d4d1"/></g><g id="g16" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path14" d="m126.81 634.34c-12.533-2.38-24.362-6.774-35.141-12.842 1.376 3.973 6.351 10.257 12.943 11.658 2.858 1.024 2.094.762 6.967.614 7.137-.364 10.552-.592 15.608 1.469-.126-.308-.251-.604-.377-.899z" style="fill:#90d4d1"/></g><g id="g20" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path18" d="m143.27 665.76c-.081.101-.159.204-.239.318-13.844 14.093-31.179 24.69-50.59 30.393 1.492-4.132 2.824-8.468 4.076-12.839 5.329-.797 10.53-1.936 15.585-3.427 11.167-3.279 21.651-8.185 31.168-14.445z" style="fill:#90d4d1"/></g><g id="g24" transform="matrix(4.1667 0 0 4.1667 -301.27 -2392.2)"><path id="path22" d="m143.03 666.08c-6.046 8.287-9.118 24.122-12.659 33.274-5.144 13.342-12.294 22.95-27.958 24.317-3.928.351-27.582 1.24-30.11-.035.159-1.344 4.098-2.961 5.123-3.747 6.852-4.847 11.416-13.5 15.014-23.416 19.411-5.703 36.746-16.3 50.59-30.393z" style="fill:#14a0c4"/></g></g><defs id="defs31"><linearGradient id="_Linear1" x2="1" gradientTransform="matrix(138.58 0 0 138.58 72.442 628.88)" gradientUnits="userSpaceOnUse"><stop id="stop26" style="stop-color:rgb(84,196,198)" offset="0"/><stop id="stop28" style="stop-color:rgb(18,186,213)" offset="1"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

@ -9,4 +9,3 @@
.wy-table-responsive {
overflow: visible !important;
}

View file

@ -1,6 +1,34 @@
Release history
###############
4.9.1 - 2023-11-15
==================
* Ensure ``tzdata`` dependency is installed on Windows
4.9.0 - 2023-11-12
==================
* Upgrade code to new minimum supported Python version: 3.8
* Settings support for ``pathlib.Path`` `(#2758) <https://github.com/getpelican/pelican/pull/2758>`_
* Various improvements to Simple theme (`#2976 <https://github.com/getpelican/pelican/pull/2976>`_ & `#3234 <https://github.com/getpelican/pelican/pull/3234>`_)
* Use Furo as Sphinx documentation theme `(#3023) <https://github.com/getpelican/pelican/pull/3023>`_
* Default to 100 articles maximum in feeds `(#3127) <https://github.com/getpelican/pelican/pull/3127>`_
* Add ``period_archives common context`` variable `(#3148) <https://github.com/getpelican/pelican/pull/3148>`_
* Use ``watchfiles`` as the file-watching backend `(#3151) <https://github.com/getpelican/pelican/pull/3151>`_
* Add GitHub Actions workflow for GitHub Pages `(#3189) <https://github.com/getpelican/pelican/pull/3189>`_
* Allow dataclasses in settings `(#3204) <https://github.com/getpelican/pelican/pull/3204>`_
* Switch build tool to PDM instead of Setuptools/Poetry `(#3220) <https://github.com/getpelican/pelican/pull/3220>`_
* Provide a ``plugin_enabled`` Jinja test for themes `(#3235) <https://github.com/getpelican/pelican/pull/3235>`_
* Preserve connection order in Blinker `(#3238) <https://github.com/getpelican/pelican/pull/3238>`_
* Remove social icons from default ``notmyidea`` theme `(#3240) <https://github.com/getpelican/pelican/pull/3240>`_
* Remove unreliable ``WRITE_SELECTED`` feature `(#3243) <https://github.com/getpelican/pelican/pull/3243>`_
* Importer: Report broken embedded video links when importing from Tumblr `(#3177) <https://github.com/getpelican/pelican/issues/3177>`_
* Importer: Remove newline addition when iterating Photo post types `(#3178) <https://github.com/getpelican/pelican/issues/3178>`_
* Importer: Force timestamp conversion in Tumblr importer to be UTC with offset `(#3221) <https://github.com/getpelican/pelican/pull/3221>`_
* Importer: Use tempfile for intermediate HTML file for Pandoc `(#3221) <https://github.com/getpelican/pelican/pull/3221>`_
* Switch linters to Ruff `(#3223) <https://github.com/getpelican/pelican/pull/3223>`_
4.8.0 - 2022-07-11
==================

View file

@ -2,45 +2,58 @@ import datetime
import os
import sys
from pelican import __version__
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
sys.path.append(os.path.abspath(os.pardir))
# -- General configuration ----------------------------------------------------
templates_path = ['_templates']
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.ifconfig',
'sphinx.ext.extlinks']
source_suffix = '.rst'
master_doc = 'index'
project = 'Pelican'
year = datetime.datetime.now().date().year
copyright = f'2010{year}'
exclude_patterns = ['_build']
release = __version__
version = '.'.join(release.split('.')[:1])
last_stable = __version__
rst_prolog = '''
.. |last_stable| replace:: :pelican-doc:`{}`
'''.format(last_stable)
extlinks = {
'pelican-doc': ('https://docs.getpelican.com/en/latest/%s.html', '%s')
}
with open("../pyproject.toml", "rb") as f:
project_data = tomllib.load(f).get("project")
if project_data is None:
raise KeyError("project data is not found")
# -- General configuration ----------------------------------------------------
templates_path = ["_templates"]
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.ifconfig",
"sphinx.ext.extlinks",
"sphinxext.opengraph",
]
source_suffix = ".rst"
master_doc = "index"
project = project_data.get("name").upper()
year = datetime.datetime.now().date().year
copyright = f"2010{year}"
exclude_patterns = ["_build"]
release = project_data.get("version")
version = ".".join(release.split(".")[:1])
last_stable = project_data.get("version")
rst_prolog = f"""
.. |last_stable| replace:: :pelican-doc:`{last_stable}`
.. |min_python| replace:: {project_data.get('requires-python').split(",")[0]}
"""
extlinks = {"pelican-doc": ("https://docs.getpelican.com/en/latest/%s.html", "%s")}
# -- Options for HTML output --------------------------------------------------
html_theme = 'furo'
html_title = f'<strong>{project}</strong> <i>{release}</i>'
html_static_path = ['_static']
html_theme = "furo"
html_title = f"<strong>{project}</strong> <i>{release}</i>"
html_static_path = ["_static"]
html_theme_options = {
'light_logo': 'pelican-logo.svg',
'dark_logo': 'pelican-logo.svg',
'navigation_with_keys': True,
"light_logo": "pelican-logo.svg",
"dark_logo": "pelican-logo.svg",
"navigation_with_keys": True,
}
# Output file base name for HTML help builder.
htmlhelp_basename = 'Pelicandoc'
htmlhelp_basename = "Pelicandoc"
html_use_smartypants = True
@ -56,21 +69,29 @@ html_show_sourcelink = False
def setup(app):
# overrides for wide tables in RTD theme
app.add_css_file('theme_overrides.css') # path relative to _static
app.add_css_file("theme_overrides.css") # path relative to _static
# -- Options for LaTeX output -------------------------------------------------
latex_documents = [
('index', 'Pelican.tex', 'Pelican Documentation', 'Justin Mayer',
'manual'),
("index", "Pelican.tex", "Pelican Documentation", "Justin Mayer", "manual"),
]
# -- Options for manual page output -------------------------------------------
man_pages = [
('index', 'pelican', 'pelican documentation',
['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',
['The Pelican contributors'], 1)
("index", "pelican", "pelican documentation", ["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",
["The Pelican contributors"],
1,
),
]

View file

@ -15,16 +15,16 @@ Setting up the development environment
======================================
While there are many ways to set up one's development environment, the following
instructions will utilize Pip_ and Poetry_. These tools facilitate managing
instructions will utilize Pip_ and PDM_. 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.
Please note that Python 3.7+ is required for Pelican development.
Please note that Python |min_python| is required for Pelican development.
*(Optional)* If you prefer to `install Poetry <https://python-poetry.org/docs/master/#installation>`_ once for use with multiple projects,
*(Optional)* If you prefer to `install PDM <https://pdm.fming.dev/latest/#installation>`_ once for use with multiple projects,
you can install it via::
curl -sSL https://install.python-poetry.org | python3 -
curl -sSL https://pdm.fming.dev/install-pdm.py | python3 -
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
@ -35,7 +35,7 @@ as a Git remote::
cd ~/projects/pelican
git remote add upstream https://github.com/getpelican/pelican.git
While Poetry can dynamically create and manage virtual environments, we're going
While PDM can dynamically create and manage virtual environments, we're going
to manually create and activate a virtual environment::
mkdir ~/virtualenvs && cd ~/virtualenvs
@ -46,12 +46,11 @@ Install the needed dependencies and set up the project::
python -m pip install invoke
invoke setup
python -m pip install -e ~/projects/pelican
Your local environment should now be ready to go!
.. _Pip: https://pip.pypa.io/
.. _Poetry: https://python-poetry.org/
.. _PDM: https://pdm.fming.dev/latest/
.. _Pelican repository: https://github.com/getpelican/pelican
Development
@ -75,6 +74,9 @@ via::
invoke tests
(For more on Invoke, see ``invoke -l`` to list tasks, or
https://pyinvoke.org for documentation.)
In addition to running the test suite, it is important to also ensure that any
lines you changed conform to code style guidelines. You can check that via::
@ -156,8 +158,7 @@ check for code style compliance via::
If style violations are found, many of them can be addressed automatically via::
invoke black
invoke isort
invoke format
If style violations are found even after running the above auto-formatters,
you will need to make additional manual changes until ``invoke lint`` no longer

View file

@ -217,10 +217,6 @@ changed. A simple solution is to make ``rsync`` use the ``--checksum`` option,
which will make it compare the file checksums in a much faster way than Pelican
would.
When only several specific output files are of interest (e.g. when working on
some specific page or the theme templates), the ``WRITE_SELECTED`` option may
help, see :ref:`writing_only_selected_content`.
How to process only a subset of all articles?
=============================================

View file

@ -11,7 +11,6 @@ software to reStructuredText or Markdown. The supported import formats are:
- Blogger XML export
- Dotclear export
- Posterous API
- Tumblr API
- WordPress XML export
- RSS/Atom feed
@ -48,16 +47,15 @@ Usage
::
pelican-import [-h] [--blogger] [--dotclear] [--posterous] [--tumblr] [--wpfile] [--feed]
pelican-import [-h] [--blogger] [--dotclear] [--tumblr] [--wpfile] [--feed]
[-o OUTPUT] [-m MARKUP] [--dir-cat] [--dir-page] [--strip-raw] [--wp-custpost]
[--wp-attach] [--disable-slugs] [-e EMAIL] [-p PASSWORD] [-b BLOGNAME]
input|api_token|api_key
[--wp-attach] [--disable-slugs] [-b BLOGNAME]
input|api_key
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 https://www.tumblr.com/oauth/apps
============= ============================================================================
@ -67,7 +65,6 @@ Optional arguments
-h, --help Show this help message and exit
--blogger Blogger XML export (default: False)
--dotclear Dotclear export (default: False)
--posterous Posterous API (default: False)
--tumblr Tumblr API (default: False)
--wpfile WordPress XML export (default: False)
--feed Feed to parse (default: False)
@ -101,10 +98,6 @@ Optional arguments
output. With this disabled, your Pelican URLs may not
be consistent with your original posts. (default:
False)
-e EMAIL, --email=EMAIL
Email used to authenticate Posterous API
-p PASSWORD, --password=PASSWORD
Password used to authenticate Posterous API
-b BLOGNAME, --blogname=BLOGNAME
Blog name used in Tumblr API
@ -120,13 +113,9 @@ For Dotclear::
$ pelican-import --dotclear -o ~/output ~/backup.txt
for Posterous::
$ pelican-import --posterous -o ~/output --email=<email_address> --password=<password> <api_token>
For Tumblr::
$ pelican-import --tumblr -o ~/output --blogname=<blogname> <api_token>
$ pelican-import --tumblr -o ~/output --blogname=<blogname> <api_key>
For WordPress::

View file

@ -1,7 +1,7 @@
Installing Pelican
##################
Pelican currently runs best on 3.7+; earlier versions of Python are not supported.
Pelican currently runs best on |min_python|; earlier versions of Python are not supported.
You can install Pelican via several different methods. The simplest is via Pip_::

View file

@ -94,9 +94,12 @@ 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.
If multiple plugins connect to the same signal, there is no way to guarantee or
control in which order the plugins will be executed. This is a limitation
inherited from Blinker_, the dependency Pelican uses to implement signals.
If multiple plugins connect to the same signal, plugins will be executed in the
order they are connected. With ``PLUGINS`` setting, order will be as defined in
the setting. If you rely on auto-discovered namespace plugins, no ``PLUGINS``
setting, they will be connected in the same order they are discovered (same
order as ``pelican-plugins`` output). If you want to specify the order
explicitly, disable auto-discovery by defining ``PLUGINS`` in the desired order.
Namespace plugin structure
--------------------------
@ -341,4 +344,3 @@ custom article, using the ``article_generator_pretaxonomy`` signal::
.. _Pip: https://pip.pypa.io/
.. _pelican-plugins bug #314: https://github.com/getpelican/pelican-plugins/issues/314
.. _Blinker: https://pythonhosted.org/blinker/

View file

@ -18,18 +18,6 @@ folder, using the default theme to produce a simple site. The default theme
consists of very simple HTML without styling and is provided so folks may use
it as a basis for creating their own themes.
When working on a single article or page, it is possible to generate only the
file that corresponds to that content. To do this, use the ``--write-selected``
argument, like so::
pelican --write-selected output/posts/my-post-title.html
Note that you must specify the path to the generated *output* file — not the
source content. To determine the output file name and location, use the
``--debug`` flag. If desired, ``--write-selected`` can take a comma-separated
list of paths or can be configured as a setting. (See:
:ref:`writing_only_selected_content`)
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. On non-Windows

View file

@ -8,7 +8,7 @@ Installation
------------
Install Pelican (and optionally Markdown if you intend to use it) on Python
3.7+ by running the following command in your preferred terminal, prefixing
|min_python| by running the following command in your preferred terminal, prefixing
with ``sudo`` if permissions warrant::
python -m pip install "pelican[markdown]"

View file

@ -362,13 +362,6 @@ Basic settings
If ``True``, load unmodified content from caches.
.. data:: WRITE_SELECTED = []
If this list is not empty, **only** output files with their paths in this
list are written. Paths should be either absolute or relative to the current
Pelican working directory. For possible use cases see
:ref:`writing_only_selected_content`.
.. data:: FORMATTED_FIELDS = ['summary']
A list of metadata fields containing reST/Markdown content to be parsed and
@ -564,44 +557,47 @@ written over time.
Example usage::
YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html'
YEAR_ARCHIVE_URL = 'posts/{date:%Y}/'
MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html'
MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/'
With these settings, Pelican will create an archive of all your posts for the
year at (for instance) ``posts/2011/index.html`` and an archive of all your
posts for the month at ``posts/2011/Aug/index.html``.
posts for the month at ``posts/2011/Aug/index.html``. These can be accessed
through the URLs ``posts/2011/`` and ``posts/2011/Aug/``, respectively.
.. note::
Period archives work best when the final path segment is ``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 = ''
.. data:: YEAR_ARCHIVE_URL = ''
The URL to use for per-month archives of your posts. Used only if you have
the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
The URL to use for per-year archives of your posts. You should set this if
you enable per-year archives.
.. data:: MONTH_ARCHIVE_SAVE_AS = ''
The location to save per-month archives of your posts.
.. data:: DAY_ARCHIVE_URL = ''
.. data:: MONTH_ARCHIVE_URL = ''
The URL to use for per-day archives of your posts. Used only if you have the
``{url}`` placeholder in ``PAGINATION_PATTERNS``.
The URL to use for per-month archives of your posts. You should set this if
you enable per-month archives.
.. 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. You should set this if
you enable per-day archives.
``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the
``_SAVE_AS`` settings are available, but it is available for any direct
template.
@ -1001,10 +997,10 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
placeholder. If not set, ``TAG_FEED_RSS`` is used both for save location and
URL.
.. data:: FEED_MAX_ITEMS
.. data:: FEED_MAX_ITEMS = 100
Maximum number of items allowed in a feed. Feed item quantity is
unrestricted by default.
Maximum number of items allowed in a feed. Setting to ``None`` will cause the
feed to contains every article. 100 if not specified.
.. data:: RSS_FEED_SUMMARY_ONLY = True
@ -1228,6 +1224,12 @@ Following are example ways to specify your preferred theme::
# Specify a customized theme, via absolute path
THEME = "/home/myuser/projects/mysite/themes/mycustomtheme"
The built-in ``simple`` theme can be customized using the following settings.
.. data:: STYLESHEET_URL
The URL of the stylesheet to use.
The built-in ``notmyidea`` theme can make good use of the following settings.
Feel free to use them in your themes as well.
@ -1391,21 +1393,6 @@ modification times of the generated ``*.html`` files will always change.
Therefore, ``rsync``-based uploading may benefit from the ``--checksum``
option.
.. _writing_only_selected_content:
Writing only selected content
=============================
When only working on a single article or page, or making tweaks to your theme,
it is often desirable to generate and review your work as quickly as possible.
In such cases, generating and writing the entire site output is often
unnecessary. By specifying only the desired files as output paths in the
``WRITE_SELECTED`` list, **only** those files will be written. This list can be
also specified on the command line using the ``--write-selected`` option, which
accepts a comma-separated list of output file paths. By default this list is
empty, so all output is written. See :ref:`site_generation` for more details.
Example settings
================

View file

@ -71,6 +71,8 @@ All templates will receive the variables defined in your settings file, as long
as they are in all-caps. You can access them directly.
.. _common_variables:
Common Variables
----------------
@ -92,6 +94,10 @@ dates The same list of articles, but ordered by date,
ascending.
hidden_articles The list of hidden articles
drafts The list of draft articles
period_archives A dictionary containing elements related to
time-period archives (if enabled). See the section
:ref:`Listing and Linking to Period Archives
<period_archives_variable>` for details.
authors A list of (author, articles) tuples, containing all
the authors and corresponding articles (values)
categories A list of (category, articles) tuples, containing
@ -134,6 +140,23 @@ your date according to the locale given in your settings::
.. _datetime: https://docs.python.org/3/library/datetime.html#datetime-objects
.. _strftime: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
Checking Loaded Plugins
-----------------------
Pelican provides a ``plugin_enabled`` Jinja test for checking if a certain plugin
is enabled. This test accepts a plugin name as a string and will return a Boolean.
Namespace plugins can be specified by full name (``pelican.plugins.plugin_name``)
or short name (``plugin_name``). The following example uses the ``webassets`` plugin
to minify CSS if the plugin is enabled and otherwise falls back to regular CSS::
{% if "webassets" is plugin_enabled %}
{% assets filters="cssmin", output="css/style.min.css", "css/style.scss" %}
<link rel="stylesheet" href="{{SITEURL}}/{{ASSET_URL}}">
{% endassets %}
{% else %}
<link rel="stylesheet" href="{{SITEURL}}/css/style.css}">
{% endif %}
index.html
----------
@ -348,6 +371,63 @@ period_archives.html template
<https://github.com/getpelican/pelican/blob/master/pelican/themes/simple/templates/period_archives.html>`_.
.. _period_archives_variable:
Listing and Linking to Period Archives
""""""""""""""""""""""""""""""""""""""
The ``period_archives`` variable can be used to generate a list of links to
the set of period archives that Pelican generates. As a :ref:`common variable
<common_variables>`, it is available for use in any template, so you
can implement such an index in a custom direct template, or in a sidebar
visible across different site pages.
``period_archives`` is a dict that may contain ``year``, ``month``, and/or
``day`` keys, depending on which ``*_ARCHIVE_SAVE_AS`` settings are enabled.
The corresponding value is a list of dicts, where each dict in turn represents
a time period (ordered according to the ``NEWEST_FIRST_ARCHIVES`` setting)
with the following keys and values:
=================== ===================================================
Key Value
=================== ===================================================
period The same tuple as described in
``period_archives.html``, e.g.
``(2023, 'June', 18)``.
period_num The same tuple as described in
``period_archives.html``, e.g. ``(2023, 6, 18)``.
url The URL to the period archive page, e.g.
``posts/2023/06/18/``. This is controlled by the
corresponding ``*_ARCHIVE_URL`` setting.
save_as The path to the save location of the period archive
page file, e.g. ``posts/2023/06/18/index.html``.
This is used internally by Pelican and is usually
not relevant to themes.
articles A list of :ref:`Article <object-article>` objects
that fall under the time period.
dates Same list as ``articles``, but ordered according
to the ``NEWEST_FIRST_ARCHIVES`` setting.
=================== ===================================================
Here is an example of how ``period_archives`` can be used in a template:
.. code-block:: html+jinja
<ul>
{% for archive in period_archives.month %}
<li>
<a href="{{ SITEURL }}/{{ archive.url }}">
{{ archive.period | reverse | join(' ') }} ({{ archive.articles|count }})
</a>
</li>
{% endfor %}
</ul>
You can change ``period_archives.month`` in the ``for`` statement to
``period_archives.year`` or ``period_archives.day`` as appropriate, depending
on the time period granularity desired.
Objects
=======

View file

@ -34,17 +34,30 @@ settings on your AWS console. From there::
Error Document: 404.html
Publishing to GitHub
====================
Publishing to GitHub Pages
==========================
`GitHub Pages <https://help.github.com/categories/20/articles>`_ offer an easy
and convenient way to publish Pelican sites. There are `two types of GitHub
Pages <https://help.github.com/articles/user-organization-and-project-pages>`_:
*Project Pages* and *User Pages*. Pelican sites can be published as both
Project Pages and User Pages.
If you use `GitHub <https://github.com/>`_ for your Pelican site you can
publish your site to `GitHub Pages <https://pages.github.com/>`_ for free.
Your site will be published to ``https://<username>.github.io`` if it's a user or
organization site or to ``https://<username>.github.io/<repository>`` if it's a
project site. It's also possible to `use a custom domain with GitHub Pages <https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site>`_.
Project Pages
-------------
There are `two ways to publish a site to GitHub Pages <https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site>`_:
1. **Publishing from a branch:** run ``pelican`` locally and push the output
directory to a special branch of your GitHub repo. GitHub will then publish
the contents of this branch to your GitHub Pages site.
2. **Publishing with a custom GitHub Actions workflow:** just push the source
files of your Pelican site to your GitHub repo's default branch and have a
custom GitHub Actions workflow run ``pelican`` for you to generate the
output directory and publish it to your GitHub Pages site. This way you
don't need to run ``pelican`` locally. You can even edit your site's source
files using GitHub's web interface and any changes that you commit will be
published.
Publishing a Project Site to GitHub Pages from a Branch
-------------------------------------------------------
To publish a Pelican site as a Project Page you need to *push* the content of
the ``output`` dir generated by Pelican to a repository's ``gh-pages`` branch
@ -72,8 +85,8 @@ already exist). The ``git push origin gh-pages`` command updates the remote
``tasks.py``) created by the ``pelican-quickstart`` command publishes the
Pelican site as Project Pages, as described above.
User Pages
----------
Publishing a User Site to GitHub Pages from a Branch
----------------------------------------------------
To publish a Pelican site in the form of User Pages, you need to *push* the
content of the ``output`` dir generated by Pelican to the ``master`` branch of
@ -110,6 +123,87 @@ branch of your GitHub repository::
(assuming origin is set to your remote repository).
Publishing to GitHub Pages Using a Custom GitHub Actions Workflow
-----------------------------------------------------------------
Pelican comes with a `custom workflow <https://github.com/getpelican/pelican/blob/master/.github/workflows/github_pages.yml>`_
for publishing a Pelican site. To use it:
1. Enable GitHub Pages in your repo: go to **Settings → Pages** and choose
**GitHub Actions** for the **Source** setting.
2. Commit a ``.github/workflows/pelican.yml`` file to your repo with these contents:
.. code-block:: yaml
name: Deploy to GitHub Pages
on:
push:
branches: ["main"]
workflow_dispatch:
jobs:
deploy:
uses: "getpelican/pelican/.github/workflows/github_pages.yml@master"
permissions:
contents: "read"
pages: "write"
id-token: "write"
with:
settings: "publishconf.py"
3. Go to the **Actions** tab in your repo
(``https://github.com/<username>/<repository>/actions``) and you should see a
**Deploy to GitHub Pages** action running.
4. Once the action completes you should see your Pelican site deployed at your
repo's GitHub Pages URL: ``https://<username>.github.io`` for a user or
organization site or ``https://<username>.github.io/<repository>>`` for a
project site.
Notes:
* You don't need to set ``SITEURL`` in your Pelican settings: the workflow will
set it for you
* You don't need to commit your ``--output`` / ``OUTPUT_PATH`` directory
(``output/``) to git: the workflow will run ``pelican`` to build the output
directory for you on GitHub Actions
See `GitHub's docs about reusable workflows <https://docs.github.com/en/actions/using-workflows/reusing-workflows>`_
for more information.
A number of optional inputs can be added to the ``with:`` block when calling
the workflow:
+--------------+----------+-----------------------------------+--------+---------------+
| Name | Required | Description | Type | Default |
+==============+==========+===================================+========+===============+
| settings | Yes | The path to your Pelican settings | string | |
| | | file (``pelican``'s | | |
| | | ``--settings`` option), | | |
| | | for example: ``"publishconf.py"`` | | |
+--------------+----------+-----------------------------------+--------+---------------+
| requirements | No | The Python requirements to | string | ``"pelican"`` |
| | | install, for example to enable | | |
| | | markdown and typogrify use: | | |
| | | ``"pelican[markdown] typogrify"`` | | |
| | | or if you have a requirements | | |
| | | file: ``"-r requirements.txt"`` | | |
+--------------+----------+-----------------------------------+--------+---------------+
| output-path | No | Where to output the generated | string | ``"output/"`` |
| | | files (``pelican``'s ``--output`` | | |
| | | option) | | |
+--------------+----------+-----------------------------------+--------+---------------+
For example:
.. code-block:: yaml
with:
settings: "publishconf.py"
requirements: "pelican[markdown] typogrify"
output-path: "__output__/"
Custom 404 Pages
----------------

View file

@ -1,4 +1,5 @@
import argparse
import importlib.metadata
import json
import logging
import multiprocessing
@ -8,39 +9,43 @@ import sys
import time
import traceback
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 console
from pelican.log import init as init_logging
from pelican.generators import (ArticlesGenerator, # noqa: I100
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 get_plugin_name, load_plugins
from pelican.readers import Readers
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
from pelican.settings import read_settings
from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize)
from pelican.utils import clean_output_dir, maybe_pluralize, wait_for_changes
from pelican.writers import Writer
try:
__version__ = __import__('pkg_resources') \
.get_distribution('pelican').version
__version__ = importlib.metadata.version("pelican")
except Exception:
__version__ = "unknown"
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
DEFAULT_CONFIG_NAME = "pelicanconf.py"
logger = logging.getLogger(__name__)
class Pelican:
def __init__(self, settings):
"""Pelican initialization
@ -50,35 +55,34 @@ class Pelican:
# define the default settings
self.settings = settings
self.path = settings['PATH']
self.theme = settings['THEME']
self.output_path = settings['OUTPUT_PATH']
self.ignore_files = settings['IGNORE_FILES']
self.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY']
self.output_retention = settings['OUTPUT_RETENTION']
self.path = settings["PATH"]
self.theme = settings["THEME"]
self.output_path = settings["OUTPUT_PATH"]
self.ignore_files = settings["IGNORE_FILES"]
self.delete_outputdir = settings["DELETE_OUTPUT_DIRECTORY"]
self.output_retention = settings["OUTPUT_RETENTION"]
self.init_path()
self.init_plugins()
signals.initialized.send(self)
def init_path(self):
if not any(p in sys.path for p in ['', os.curdir]):
if not any(p in sys.path for p in ["", os.curdir]):
logger.debug("Adding current directory to system path")
sys.path.insert(0, '')
sys.path.insert(0, "")
def init_plugins(self):
self.plugins = []
for plugin in load_plugins(self.settings):
name = get_plugin_name(plugin)
logger.debug('Registering plugin `%s`', name)
logger.debug("Registering plugin `%s`", name)
try:
plugin.register()
self.plugins.append(plugin)
except Exception as e:
logger.error('Cannot register plugin `%s`\n%s',
name, e)
logger.error("Cannot register plugin `%s`\n%s", name, e)
self.settings['PLUGINS'] = [get_plugin_name(p) for p in self.plugins]
self.settings["PLUGINS"] = [get_plugin_name(p) for p in self.plugins]
def run(self):
"""Run the generators and return"""
@ -87,10 +91,10 @@ class Pelican:
context = self.settings.copy()
# Share these among all the generators and content objects
# They map source paths to Content objects or None
context['generated_content'] = {}
context['static_links'] = set()
context['static_content'] = {}
context['localsiteurl'] = self.settings['SITEURL']
context["generated_content"] = {}
context["static_links"] = set()
context["static_content"] = {}
context["localsiteurl"] = self.settings["SITEURL"]
generators = [
cls(
@ -99,23 +103,25 @@ class Pelican:
path=self.path,
theme=self.theme,
output_path=self.output_path,
) for cls in self._get_generator_classes()
)
for cls in self._get_generator_classes()
]
# Delete the output directory if (1) the appropriate setting is True
# and (2) that directory is not the parent of the source directory
if (self.delete_outputdir
and os.path.commonpath([os.path.realpath(self.output_path)]) !=
os.path.commonpath([os.path.realpath(self.output_path),
os.path.realpath(self.path)])):
if self.delete_outputdir and os.path.commonpath(
[os.path.realpath(self.output_path)]
) != os.path.commonpath(
[os.path.realpath(self.output_path), os.path.realpath(self.path)]
):
clean_output_dir(self.output_path, self.output_retention)
for p in generators:
if hasattr(p, 'generate_context'):
if hasattr(p, "generate_context"):
p.generate_context()
for p in generators:
if hasattr(p, 'refresh_metadata_intersite_links'):
if hasattr(p, "refresh_metadata_intersite_links"):
p.refresh_metadata_intersite_links()
signals.all_generators_finalized.send(generators)
@ -123,61 +129,75 @@ class Pelican:
writer = self._get_writer()
for p in generators:
if hasattr(p, 'generate_output'):
if hasattr(p, "generate_output"):
p.generate_output(writer)
signals.finalized.send(self)
articles_generator = next(g for g in generators
if isinstance(g, ArticlesGenerator))
pages_generator = next(g for g in generators
if isinstance(g, PagesGenerator))
articles_generator = next(
g for g in generators if isinstance(g, ArticlesGenerator)
)
pages_generator = next(g for g in generators if isinstance(g, PagesGenerator))
pluralized_articles = maybe_pluralize(
(len(articles_generator.articles) +
len(articles_generator.translations)),
'article',
'articles')
(len(articles_generator.articles) + len(articles_generator.translations)),
"article",
"articles",
)
pluralized_drafts = maybe_pluralize(
(len(articles_generator.drafts) +
len(articles_generator.drafts_translations)),
'draft',
'drafts')
(
len(articles_generator.drafts)
+ len(articles_generator.drafts_translations)
),
"draft",
"drafts",
)
pluralized_hidden_articles = maybe_pluralize(
(len(articles_generator.hidden_articles) +
len(articles_generator.hidden_translations)),
'hidden article',
'hidden articles')
(
len(articles_generator.hidden_articles)
+ len(articles_generator.hidden_translations)
),
"hidden article",
"hidden articles",
)
pluralized_pages = maybe_pluralize(
(len(pages_generator.pages) +
len(pages_generator.translations)),
'page',
'pages')
(len(pages_generator.pages) + len(pages_generator.translations)),
"page",
"pages",
)
pluralized_hidden_pages = maybe_pluralize(
(len(pages_generator.hidden_pages) +
len(pages_generator.hidden_translations)),
'hidden page',
'hidden pages')
(
len(pages_generator.hidden_pages)
+ len(pages_generator.hidden_translations)
),
"hidden page",
"hidden pages",
)
pluralized_draft_pages = maybe_pluralize(
(len(pages_generator.draft_pages) +
len(pages_generator.draft_translations)),
'draft page',
'draft pages')
(
len(pages_generator.draft_pages)
+ len(pages_generator.draft_translations)
),
"draft page",
"draft pages",
)
console.print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.'
.format(
pluralized_articles,
pluralized_drafts,
pluralized_hidden_articles,
pluralized_pages,
pluralized_hidden_pages,
pluralized_draft_pages,
time.time() - start_time))
console.print(
"Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.".format(
pluralized_articles,
pluralized_drafts,
pluralized_hidden_articles,
pluralized_pages,
pluralized_hidden_pages,
pluralized_draft_pages,
time.time() - start_time,
)
)
def _get_generator_classes(self):
discovered_generators = [
(ArticlesGenerator, "internal"),
(PagesGenerator, "internal")
(PagesGenerator, "internal"),
]
if self.settings["TEMPLATE_PAGES"]:
@ -236,7 +256,7 @@ class PrintSettings(argparse.Action):
except Exception as e:
logger.critical("%s: %s", e.__class__.__name__, e)
console.print_exception()
sys.exit(getattr(e, 'exitcode', 1))
sys.exit(getattr(e, "exitcode", 1))
if values:
# One or more arguments provided, so only print those settings
@ -244,14 +264,16 @@ class PrintSettings(argparse.Action):
if setting in settings:
# Only add newline between setting name and value if dict
if isinstance(settings[setting], (dict, tuple, list)):
setting_format = '\n{}:\n{}'
setting_format = "\n{}:\n{}"
else:
setting_format = '\n{}: {}'
console.print(setting_format.format(
setting,
pprint.pformat(settings[setting])))
setting_format = "\n{}: {}"
console.print(
setting_format.format(
setting, pprint.pformat(settings[setting])
)
)
else:
console.print('\n{} is not a recognized setting.'.format(setting))
console.print(f"\n{setting} is not a recognized setting.")
break
else:
# No argument was given to --print-settings, so print all settings
@ -268,170 +290,247 @@ class ParseOverrides(argparse.Action):
k, v = item.split("=", 1)
except ValueError:
raise ValueError(
'Extra settings must be specified as KEY=VALUE pairs '
f'but you specified {item}'
"Extra settings must be specified as KEY=VALUE pairs "
f"but you specified {item}"
)
try:
overrides[k] = json.loads(v)
except json.decoder.JSONDecodeError:
raise ValueError(
f'Invalid JSON value: {v}. '
'Values specified via -e / --extra-settings flags '
'must be in JSON notation. '
'Use -e KEY=\'"string"\' to specify a string value; '
'-e KEY=null to specify None; '
'-e KEY=false (or true) to specify False (or True).'
f"Invalid JSON value: {v}. "
"Values specified via -e / --extra-settings flags "
"must be in JSON notation. "
"Use -e KEY='\"string\"' to specify a string value; "
"-e KEY=null to specify None; "
"-e KEY=false (or true) to specify False (or True)."
)
setattr(namespace, self.dest, overrides)
def parse_arguments(argv=None):
parser = argparse.ArgumentParser(
description='A tool to generate a static blog, '
' with restructured text input files.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
description="A tool to generate a static blog, "
" with restructured text input files.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(dest='path', nargs='?',
help='Path where to find the content files.',
default=None)
parser.add_argument(
dest="path",
nargs="?",
help="Path where to find the content files.",
default=None,
)
parser.add_argument('-t', '--theme-path', dest='theme',
help='Path where to find the theme templates. If not '
'specified, it will use the default one included with '
'pelican.')
parser.add_argument(
"-t",
"--theme-path",
dest="theme",
help="Path where to find the theme templates. If not "
"specified, it will use the default one included with "
"pelican.",
)
parser.add_argument('-o', '--output', dest='output',
help='Where to output the generated files. If not '
'specified, a directory will be created, named '
'"output" in the current path.')
parser.add_argument(
"-o",
"--output",
dest="output",
help="Where to output the generated files. If not "
"specified, a directory will be created, named "
'"output" in the current path.',
)
parser.add_argument('-s', '--settings', dest='settings',
help='The settings of the application, this is '
'automatically set to {} if a file exists with this '
'name.'.format(DEFAULT_CONFIG_NAME))
parser.add_argument(
"-s",
"--settings",
dest="settings",
help="The settings of the application, this is "
"automatically set to {} if a file exists with this "
"name.".format(DEFAULT_CONFIG_NAME),
)
parser.add_argument('-d', '--delete-output-directory',
dest='delete_outputdir', action='store_true',
default=None, help='Delete the output directory.')
parser.add_argument(
"-d",
"--delete-output-directory",
dest="delete_outputdir",
action="store_true",
default=None,
help="Delete the output directory.",
)
parser.add_argument('-v', '--verbose', action='store_const',
const=logging.INFO, dest='verbosity',
help='Show all messages.')
parser.add_argument(
"-v",
"--verbose",
action="store_const",
const=logging.INFO,
dest="verbosity",
help="Show all messages.",
)
parser.add_argument('-q', '--quiet', action='store_const',
const=logging.CRITICAL, dest='verbosity',
help='Show only critical errors.')
parser.add_argument(
"-q",
"--quiet",
action="store_const",
const=logging.CRITICAL,
dest="verbosity",
help="Show only critical errors.",
)
parser.add_argument('-D', '--debug', action='store_const',
const=logging.DEBUG, dest='verbosity',
help='Show all messages, including debug messages.')
parser.add_argument(
"-D",
"--debug",
action="store_const",
const=logging.DEBUG,
dest="verbosity",
help="Show all messages, including debug messages.",
)
parser.add_argument('--version', action='version', version=__version__,
help='Print the pelican version and exit.')
parser.add_argument(
"--version",
action="version",
version=__version__,
help="Print the pelican version and exit.",
)
parser.add_argument('-r', '--autoreload', dest='autoreload',
action='store_true',
help='Relaunch pelican each time a modification occurs'
' on the content files.')
parser.add_argument(
"-r",
"--autoreload",
dest="autoreload",
action="store_true",
help="Relaunch pelican each time a modification occurs"
" on the content files.",
)
parser.add_argument('--print-settings', dest='print_settings', nargs='*',
action=PrintSettings, metavar='SETTING_NAME',
help='Print current configuration settings and exit. '
'Append one or more setting name arguments to see the '
'values for specific settings only.')
parser.add_argument(
"--print-settings",
dest="print_settings",
nargs="*",
action=PrintSettings,
metavar="SETTING_NAME",
help="Print current configuration settings and exit. "
"Append one or more setting name arguments to see the "
"values for specific settings only.",
)
parser.add_argument('--relative-urls', dest='relative_paths',
action='store_true',
help='Use relative urls in output, '
'useful for site development')
parser.add_argument(
"--relative-urls",
dest="relative_paths",
action="store_true",
help="Use relative urls in output, " "useful for site development",
)
parser.add_argument('--cache-path', dest='cache_path',
help=('Directory in which to store cache files. '
'If not specified, defaults to "cache".'))
parser.add_argument(
"--cache-path",
dest="cache_path",
help=(
"Directory in which to store cache files. "
'If not specified, defaults to "cache".'
),
)
parser.add_argument('--ignore-cache', action='store_true',
dest='ignore_cache', help='Ignore content cache '
'from previous runs by not loading cache files.')
parser.add_argument(
"--ignore-cache",
action="store_true",
dest="ignore_cache",
help="Ignore content cache " "from previous runs by not loading cache files.",
)
parser.add_argument('-w', '--write-selected', type=str,
dest='selected_paths', default=None,
help='Comma separated list of selected paths to write')
parser.add_argument(
"--fatal",
metavar="errors|warnings",
choices=("errors", "warnings"),
default="",
help=(
"Exit the program with non-zero status if any "
"errors/warnings encountered."
),
)
parser.add_argument('--fatal', metavar='errors|warnings',
choices=('errors', 'warnings'), default='',
help=('Exit the program with non-zero status if any '
'errors/warnings encountered.'))
parser.add_argument(
"--logs-dedup-min-level",
default="WARNING",
choices=("DEBUG", "INFO", "WARNING", "ERROR"),
help=(
"Only enable log de-duplication for levels equal"
" to or above the specified value"
),
)
parser.add_argument('--logs-dedup-min-level', default='WARNING',
choices=('DEBUG', 'INFO', 'WARNING', 'ERROR'),
help=('Only enable log de-duplication for levels equal'
' to or above the specified value'))
parser.add_argument(
"-l",
"--listen",
dest="listen",
action="store_true",
help="Serve content files via HTTP and port 8000.",
)
parser.add_argument('-l', '--listen', dest='listen', action='store_true',
help='Serve content files via HTTP and port 8000.')
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
help="Port to serve HTTP files at. (default: 8000)",
)
parser.add_argument('-p', '--port', dest='port', type=int,
help='Port to serve HTTP files at. (default: 8000)')
parser.add_argument(
"-b",
"--bind",
dest="bind",
help="IP to bind to when serving files via HTTP " "(default: 127.0.0.1)",
)
parser.add_argument('-b', '--bind', dest='bind',
help='IP to bind to when serving files via HTTP '
'(default: 127.0.0.1)')
parser.add_argument('-e', '--extra-settings', dest='overrides',
help='Specify one or more SETTING=VALUE pairs to '
'override settings. VALUE must be in JSON notation: '
'specify string values as SETTING=\'"some string"\'; '
'booleans as SETTING=true or SETTING=false; '
'None as SETTING=null.',
nargs='*',
action=ParseOverrides,
default={})
parser.add_argument(
"-e",
"--extra-settings",
dest="overrides",
help="Specify one or more SETTING=VALUE pairs to "
"override settings. VALUE must be in JSON notation: "
"specify string values as SETTING='\"some string\"'; "
"booleans as SETTING=true or SETTING=false; "
"None as SETTING=null.",
nargs="*",
action=ParseOverrides,
default={},
)
args = parser.parse_args(argv)
if args.port is not None and not args.listen:
logger.warning('--port without --listen has no effect')
logger.warning("--port without --listen has no effect")
if args.bind is not None and not args.listen:
logger.warning('--bind without --listen has no effect')
logger.warning("--bind without --listen has no effect")
return args
def get_config(args):
"""Builds a config dictionary based on supplied `args`.
"""
"""Builds a config dictionary based on supplied `args`."""
config = {}
if args.path:
config['PATH'] = os.path.abspath(os.path.expanduser(args.path))
config["PATH"] = os.path.abspath(os.path.expanduser(args.path))
if args.output:
config['OUTPUT_PATH'] = \
os.path.abspath(os.path.expanduser(args.output))
config["OUTPUT_PATH"] = os.path.abspath(os.path.expanduser(args.output))
if args.theme:
abstheme = os.path.abspath(os.path.expanduser(args.theme))
config['THEME'] = abstheme if os.path.exists(abstheme) else args.theme
config["THEME"] = abstheme if os.path.exists(abstheme) else args.theme
if args.delete_outputdir is not None:
config['DELETE_OUTPUT_DIRECTORY'] = args.delete_outputdir
config["DELETE_OUTPUT_DIRECTORY"] = args.delete_outputdir
if args.ignore_cache:
config['LOAD_CONTENT_CACHE'] = False
config["LOAD_CONTENT_CACHE"] = False
if args.cache_path:
config['CACHE_PATH'] = args.cache_path
if args.selected_paths:
config['WRITE_SELECTED'] = args.selected_paths.split(',')
config["CACHE_PATH"] = args.cache_path
if args.relative_paths:
config['RELATIVE_URLS'] = args.relative_paths
config["RELATIVE_URLS"] = args.relative_paths
if args.port is not None:
config['PORT'] = args.port
config["PORT"] = args.port
if args.bind is not None:
config['BIND'] = args.bind
config['DEBUG'] = args.verbosity == logging.DEBUG
config["BIND"] = args.bind
config["DEBUG"] = args.verbosity == logging.DEBUG
config.update(args.overrides)
return config
def get_instance(args):
config_file = args.settings
if config_file is None and os.path.isfile(DEFAULT_CONFIG_NAME):
config_file = DEFAULT_CONFIG_NAME
@ -439,9 +538,9 @@ def get_instance(args):
settings = read_settings(config_file, override=get_config(args))
cls = settings['PELICAN_CLASS']
cls = settings["PELICAN_CLASS"]
if isinstance(cls, str):
module, cls_name = cls.rsplit('.', 1)
module, cls_name = cls.rsplit(".", 1)
module = __import__(module)
cls = getattr(module, cls_name)
@ -449,29 +548,25 @@ def get_instance(args):
def autoreload(args, excqueue=None):
console.print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
' `settings` for changes. ---')
console.print(
" --- AutoReload Mode: Monitoring `content`, `theme` and"
" `settings` for changes. ---"
)
pelican, settings = get_instance(args)
watcher = FileSystemWatcher(args.settings, Readers, settings)
sleep = False
settings_file = os.path.abspath(args.settings)
while True:
try:
# Don't sleep first time, but sleep afterwards to reduce cpu load
if sleep:
time.sleep(0.5)
else:
sleep = True
pelican.run()
modified = watcher.check()
changed_files = wait_for_changes(args.settings, Readers, settings)
changed_files = {c[1] for c in changed_files}
if modified['settings']:
if settings_file in changed_files:
pelican, settings = get_instance(args)
watcher.update_watchers(settings)
if any(modified.values()):
console.print('\n-> Modified: {}. re-generating...'.format(
', '.join(k for k, v in modified.items() if v)))
pelican.run()
console.print(
"\n-> Modified: {}. re-generating...".format(", ".join(changed_files))
)
except KeyboardInterrupt:
if excqueue is not None:
@ -480,15 +575,14 @@ def autoreload(args, excqueue=None):
raise
except Exception as e:
if (args.verbosity == logging.DEBUG):
if args.verbosity == logging.DEBUG:
if excqueue is not None:
excqueue.put(
traceback.format_exception_only(type(e), e)[-1])
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
else:
raise
logger.warning(
'Caught exception:\n"%s".', e,
exc_info=settings.get('DEBUG', False))
'Caught exception:\n"%s".', e, exc_info=settings.get("DEBUG", False)
)
def listen(server, port, output, excqueue=None):
@ -498,8 +592,7 @@ def listen(server, port, output, excqueue=None):
RootedHTTPServer.allow_reuse_address = True
try:
httpd = RootedHTTPServer(
output, (server, port), ComplexHTTPRequestHandler)
httpd = RootedHTTPServer(output, (server, port), ComplexHTTPRequestHandler)
except OSError as e:
logging.error("Could not listen on port %s, server %s.", port, server)
if excqueue is not None:
@ -507,8 +600,7 @@ def listen(server, port, output, excqueue=None):
return
try:
console.print("Serving site at: http://{}:{} - Tap CTRL-C to stop".format(
server, port))
console.print(f"Serving site at: http://{server}:{port} - Tap CTRL-C to stop")
httpd.serve_forever()
except Exception as e:
if excqueue is not None:
@ -525,24 +617,31 @@ def listen(server, port, output, excqueue=None):
def main(argv=None):
args = parse_arguments(argv)
logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level)
init_logging(level=args.verbosity, fatal=args.fatal,
name=__name__, logs_dedup_min_level=logs_dedup_min_level)
init_logging(
level=args.verbosity,
fatal=args.fatal,
name=__name__,
logs_dedup_min_level=logs_dedup_min_level,
)
logger.debug('Pelican version: %s', __version__)
logger.debug('Python version: %s', sys.version.split()[0])
logger.debug("Pelican version: %s", __version__)
logger.debug("Python version: %s", sys.version.split()[0])
try:
pelican, settings = get_instance(args)
if args.autoreload and args.listen:
excqueue = multiprocessing.Queue()
p1 = multiprocessing.Process(
target=autoreload,
args=(args, excqueue))
p1 = multiprocessing.Process(target=autoreload, args=(args, excqueue))
p2 = multiprocessing.Process(
target=listen,
args=(settings.get('BIND'), settings.get('PORT'),
settings.get("OUTPUT_PATH"), excqueue))
args=(
settings.get("BIND"),
settings.get("PORT"),
settings.get("OUTPUT_PATH"),
excqueue,
),
)
try:
p1.start()
p2.start()
@ -555,18 +654,17 @@ def main(argv=None):
elif args.autoreload:
autoreload(args)
elif args.listen:
listen(settings.get('BIND'), settings.get('PORT'),
settings.get("OUTPUT_PATH"))
listen(
settings.get("BIND"), settings.get("PORT"), settings.get("OUTPUT_PATH")
)
else:
watcher = FileSystemWatcher(args.settings, Readers, settings)
watcher.check()
with console.status("Generating..."):
pelican.run()
except KeyboardInterrupt:
logger.warning('Keyboard interrupt received. Exiting.')
logger.warning("Keyboard interrupt received. Exiting.")
except Exception as e:
logger.critical("%s: %s", e.__class__.__name__, e)
if args.verbosity == logging.DEBUG:
console.print_exception()
sys.exit(getattr(e, 'exitcode', 1))
sys.exit(getattr(e, "exitcode", 1))

View file

@ -5,5 +5,5 @@ python -m pelican module entry point to run via python -m
from . import main
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -19,29 +19,35 @@ class FileDataCacher:
Sets caching policy according to *caching_policy*.
"""
self.settings = settings
self._cache_path = os.path.join(self.settings['CACHE_PATH'],
cache_name)
self._cache_path = os.path.join(self.settings["CACHE_PATH"], cache_name)
self._cache_data_policy = caching_policy
if self.settings['GZIP_CACHE']:
if self.settings["GZIP_CACHE"]:
import gzip
self._cache_open = gzip.open
else:
self._cache_open = open
if load_policy:
try:
with self._cache_open(self._cache_path, 'rb') as fhandle:
with self._cache_open(self._cache_path, "rb") as fhandle:
self._cache = pickle.load(fhandle)
except (OSError, UnicodeDecodeError) as err:
logger.debug('Cannot load cache %s (this is normal on first '
'run). Proceeding with empty cache.\n%s',
self._cache_path, err)
logger.debug(
"Cannot load cache %s (this is normal on first "
"run). Proceeding with empty cache.\n%s",
self._cache_path,
err,
)
self._cache = {}
except pickle.PickleError as err:
logger.warning('Cannot unpickle cache %s, cache may be using '
'an incompatible protocol (see pelican '
'caching docs). '
'Proceeding with empty cache.\n%s',
self._cache_path, err)
logger.warning(
"Cannot unpickle cache %s, cache may be using "
"an incompatible protocol (see pelican "
"caching docs). "
"Proceeding with empty cache.\n%s",
self._cache_path,
err,
)
self._cache = {}
else:
self._cache = {}
@ -62,12 +68,13 @@ class FileDataCacher:
"""Save the updated cache"""
if self._cache_data_policy:
try:
mkdir_p(self.settings['CACHE_PATH'])
with self._cache_open(self._cache_path, 'wb') as fhandle:
mkdir_p(self.settings["CACHE_PATH"])
with self._cache_open(self._cache_path, "wb") as fhandle:
pickle.dump(self._cache, fhandle)
except (OSError, pickle.PicklingError, TypeError) as err:
logger.warning('Could not save cache %s\n ... %s',
self._cache_path, err)
logger.warning(
"Could not save cache %s\n ... %s", self._cache_path, err
)
class FileStampDataCacher(FileDataCacher):
@ -80,8 +87,8 @@ class FileStampDataCacher(FileDataCacher):
super().__init__(settings, cache_name, caching_policy, load_policy)
method = self.settings['CHECK_MODIFIED_METHOD']
if method == 'mtime':
method = self.settings["CHECK_MODIFIED_METHOD"]
if method == "mtime":
self._filestamp_func = os.path.getmtime
else:
try:
@ -89,12 +96,12 @@ class FileStampDataCacher(FileDataCacher):
def filestamp_func(filename):
"""return hash of file contents"""
with open(filename, 'rb') as fhandle:
with open(filename, "rb") as fhandle:
return hash_func(fhandle.read()).digest()
self._filestamp_func = filestamp_func
except AttributeError as err:
logger.warning('Could not get hashing function\n\t%s', err)
logger.warning("Could not get hashing function\n\t%s", err)
self._filestamp_func = None
def cache_data(self, filename, data):
@ -115,9 +122,8 @@ class FileStampDataCacher(FileDataCacher):
try:
return self._filestamp_func(filename)
except (OSError, TypeError) as err:
logger.warning('Cannot get modification stamp for %s\n\t%s',
filename, err)
return ''
logger.warning("Cannot get modification stamp for %s\n\t%s", filename, err)
return ""
def get_cached_data(self, filename, default=None):
"""Get the cached data for the given filename

View file

@ -16,12 +16,19 @@ except ModuleNotFoundError:
from pelican.plugins import signals
from pelican.settings import DEFAULT_CONFIG
from pelican.utils import (deprecated_attribute, memoized, path_to_url,
posixize_path, sanitised_join, set_date_tzinfo,
slugify, 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 available when you import from pelican.contents.
from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
from pelican.urlwrappers import Author, Category, Tag, URLWrapper # NOQA
logger = logging.getLogger(__name__)
@ -36,12 +43,14 @@ class Content:
:param context: The shared context between generators.
"""
@deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
@deprecated_attribute(old="filename", new="source_path", since=(3, 2, 0))
def filename():
return None
def __init__(self, content, metadata=None, settings=None,
source_path=None, context=None):
def __init__(
self, content, metadata=None, settings=None, source_path=None, context=None
):
if metadata is None:
metadata = {}
if settings is None:
@ -59,8 +68,8 @@ class Content:
# set metadata as attributes
for key, value in local_metadata.items():
if key in ('save_as', 'url'):
key = 'override_' + key
if key in ("save_as", "url"):
key = "override_" + key
setattr(self, key.lower(), value)
# also keep track of the metadata attributes available
@ -71,53 +80,52 @@ class Content:
# First, read the authors from "authors", if not, fallback to "author"
# and if not use the settings defined one, if any.
if not hasattr(self, 'author'):
if hasattr(self, 'authors'):
if not hasattr(self, "author"):
if hasattr(self, "authors"):
self.author = self.authors[0]
elif 'AUTHOR' in settings:
self.author = Author(settings['AUTHOR'], settings)
elif "AUTHOR" in settings:
self.author = Author(settings["AUTHOR"], settings)
if not hasattr(self, 'authors') and hasattr(self, 'author'):
if not hasattr(self, "authors") and hasattr(self, "author"):
self.authors = [self.author]
# XXX Split all the following code into pieces, there is too much here.
# manage languages
self.in_default_lang = True
if 'DEFAULT_LANG' in settings:
default_lang = settings['DEFAULT_LANG'].lower()
if not hasattr(self, 'lang'):
if "DEFAULT_LANG" in settings:
default_lang = settings["DEFAULT_LANG"].lower()
if not hasattr(self, "lang"):
self.lang = default_lang
self.in_default_lang = (self.lang == default_lang)
self.in_default_lang = self.lang == default_lang
# create the slug if not existing, generate slug according to
# setting of SLUG_ATTRIBUTE
if not hasattr(self, 'slug'):
if (settings['SLUGIFY_SOURCE'] == 'title' and
hasattr(self, 'title')):
if not hasattr(self, "slug"):
if settings["SLUGIFY_SOURCE"] == "title" and hasattr(self, "title"):
value = self.title
elif (settings['SLUGIFY_SOURCE'] == 'basename' and
source_path is not None):
elif settings["SLUGIFY_SOURCE"] == "basename" and source_path is not None:
value = os.path.basename(os.path.splitext(source_path)[0])
else:
value = None
if value is not None:
self.slug = slugify(
value,
regex_subs=settings.get('SLUG_REGEX_SUBSTITUTIONS', []),
preserve_case=settings.get('SLUGIFY_PRESERVE_CASE', False),
use_unicode=settings.get('SLUGIFY_USE_UNICODE', False))
regex_subs=settings.get("SLUG_REGEX_SUBSTITUTIONS", []),
preserve_case=settings.get("SLUGIFY_PRESERVE_CASE", False),
use_unicode=settings.get("SLUGIFY_USE_UNICODE", False),
)
self.source_path = source_path
self.relative_source_path = self.get_relative_source_path()
# manage the date format
if not hasattr(self, 'date_format'):
if hasattr(self, 'lang') and self.lang in settings['DATE_FORMATS']:
self.date_format = settings['DATE_FORMATS'][self.lang]
if not hasattr(self, "date_format"):
if hasattr(self, "lang") and self.lang in settings["DATE_FORMATS"]:
self.date_format = settings["DATE_FORMATS"][self.lang]
else:
self.date_format = settings['DEFAULT_DATE_FORMAT']
self.date_format = settings["DEFAULT_DATE_FORMAT"]
if isinstance(self.date_format, tuple):
locale_string = self.date_format[0]
@ -129,22 +137,22 @@ class Content:
timezone = getattr(self, "timezone", default_timezone)
self.timezone = ZoneInfo(timezone)
if hasattr(self, 'date'):
if hasattr(self, "date"):
self.date = set_date_tzinfo(self.date, timezone)
self.locale_date = self.date.strftime(self.date_format)
if hasattr(self, 'modified'):
if hasattr(self, "modified"):
self.modified = set_date_tzinfo(self.modified, timezone)
self.locale_modified = self.modified.strftime(self.date_format)
# manage status
if not hasattr(self, 'status'):
if not hasattr(self, "status"):
# Previous default of None broke comment plugins and perhaps others
self.status = getattr(self, 'default_status', '')
self.status = getattr(self, "default_status", "")
# store the summary metadata if it is set
if 'summary' in metadata:
self._summary = metadata['summary']
if "summary" in metadata:
self._summary = metadata["summary"]
signals.content_object_init.send(self)
@ -156,8 +164,8 @@ class Content:
for prop in self.mandatory_properties:
if not hasattr(self, prop):
logger.error(
"Skipping %s: could not find information about '%s'",
self, prop)
"Skipping %s: could not find information about '%s'", self, prop
)
return False
return True
@ -183,12 +191,13 @@ class Content:
return True
def _has_valid_status(self):
if hasattr(self, 'allowed_statuses'):
if hasattr(self, "allowed_statuses"):
if self.status not in self.allowed_statuses:
logger.error(
"Unknown status '%s' for file %s, skipping it. (Not in %s)",
self.status,
self, self.allowed_statuses
self,
self.allowed_statuses,
)
return False
@ -198,42 +207,48 @@ class Content:
def is_valid(self):
"""Validate Content"""
# Use all() to not short circuit and get results of all validations
return all([self._has_valid_mandatory_properties(),
self._has_valid_save_as(),
self._has_valid_status()])
return all(
[
self._has_valid_mandatory_properties(),
self._has_valid_save_as(),
self._has_valid_status(),
]
)
@property
def url_format(self):
"""Returns the URL, formatted with the proper values"""
metadata = copy.copy(self.metadata)
path = self.metadata.get('path', self.get_relative_source_path())
metadata.update({
'path': path_to_url(path),
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
'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 ''
})
path = self.metadata.get("path", self.get_relative_source_path())
metadata.update(
{
"path": path_to_url(path),
"slug": getattr(self, "slug", ""),
"lang": getattr(self, "lang", "en"),
"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 "",
}
)
return metadata
def _expand_settings(self, key, klass=None):
if not klass:
klass = self.__class__.__name__
fq_key = ('{}_{}'.format(klass, key)).upper()
return self.settings[fq_key].format(**self.url_format)
fq_key = (f"{klass}_{key}").upper()
return str(self.settings[fq_key]).format(**self.url_format)
def get_url_setting(self, key):
if hasattr(self, 'override_' + key):
return getattr(self, 'override_' + key)
key = key if self.in_default_lang else 'lang_%s' % key
if hasattr(self, "override_" + key):
return getattr(self, "override_" + key)
key = key if self.in_default_lang else "lang_%s" % key
return self._expand_settings(key)
def _link_replacer(self, siteurl, m):
what = m.group('what')
value = urlparse(m.group('value'))
what = m.group("what")
value = urlparse(m.group("value"))
path = value.path
origin = m.group('path')
origin = m.group("path")
# urllib.parse.urljoin() produces `a.html` for urljoin("..", "a.html")
# so if RELATIVE_URLS are enabled, we fall back to os.path.join() to
@ -241,7 +256,7 @@ class Content:
# `baz/http://foo/bar.html` for join("baz", "http://foo/bar.html")
# instead of correct "http://foo/bar.html", so one has to pick a side
# as there is no silver bullet.
if self.settings['RELATIVE_URLS']:
if self.settings["RELATIVE_URLS"]:
joiner = os.path.join
else:
joiner = urljoin
@ -251,16 +266,17 @@ class Content:
# os.path.join()), so in order to get a correct answer one needs to
# append a trailing slash to siteurl in that case. This also makes
# the new behavior fully compatible with Pelican 3.7.1.
if not siteurl.endswith('/'):
siteurl += '/'
if not siteurl.endswith("/"):
siteurl += "/"
# XXX Put this in a different location.
if what in {'filename', 'static', 'attach'}:
if what in {"filename", "static", "attach"}:
def _get_linked_content(key, url):
nonlocal value
def _find_path(path):
if path.startswith('/'):
if path.startswith("/"):
path = path[1:]
else:
# relative to the source path of this content
@ -287,59 +303,64 @@ class Content:
return result
# check if a static file is linked with {filename}
if what == 'filename' and key == 'generated_content':
linked_content = _get_linked_content('static_content', value)
if what == "filename" and key == "generated_content":
linked_content = _get_linked_content("static_content", value)
if linked_content:
logger.warning(
'{filename} used for linking to static'
' content %s in %s. Use {static} instead',
"{filename} used for linking to static"
" content %s in %s. Use {static} instead",
value.path,
self.get_relative_source_path())
self.get_relative_source_path(),
)
return linked_content
return None
if what == 'filename':
key = 'generated_content'
if what == "filename":
key = "generated_content"
else:
key = 'static_content'
key = "static_content"
linked_content = _get_linked_content(key, value)
if linked_content:
if what == 'attach':
if what == "attach":
linked_content.attach_to(self)
origin = joiner(siteurl, linked_content.url)
origin = origin.replace('\\', '/') # for Windows paths.
origin = origin.replace("\\", "/") # for Windows paths.
else:
logger.warning(
"Unable to find '%s', skipping url replacement.",
value.geturl(), extra={
'limit_msg': ("Other resources were not found "
"and their urls not replaced")})
elif what == 'category':
value.geturl(),
extra={
"limit_msg": (
"Other resources were not found "
"and their urls not replaced"
)
},
)
elif what == "category":
origin = joiner(siteurl, Category(path, self.settings).url)
elif what == 'tag':
elif what == "tag":
origin = joiner(siteurl, Tag(path, self.settings).url)
elif what == 'index':
origin = joiner(siteurl, self.settings['INDEX_SAVE_AS'])
elif what == 'author':
elif what == "index":
origin = joiner(siteurl, self.settings["INDEX_SAVE_AS"])
elif what == "author":
origin = joiner(siteurl, Author(path, self.settings).url)
else:
logger.warning(
"Replacement Indicator '%s' not recognized, "
"skipping replacement",
what)
"Replacement Indicator '%s' not recognized, " "skipping replacement",
what,
)
# keep all other parts, such as query, fragment, etc.
parts = list(value)
parts[2] = origin
origin = urlunparse(parts)
return ''.join((m.group('markup'), m.group('quote'), origin,
m.group('quote')))
return "".join((m.group("markup"), m.group("quote"), origin, m.group("quote")))
def _get_intrasite_link_regex(self):
intrasite_link_regex = self.settings['INTRASITE_LINK_REGEX']
intrasite_link_regex = self.settings["INTRASITE_LINK_REGEX"]
regex = r"""
(?P<markup><[^\>]+ # match tag with all url-value attributes
(?:href|src|poster|data|cite|formaction|action|content)\s*=\s*)
@ -369,28 +390,28 @@ class Content:
static_links = set()
hrefs = self._get_intrasite_link_regex()
for m in hrefs.finditer(self._content):
what = m.group('what')
value = urlparse(m.group('value'))
what = m.group("what")
value = urlparse(m.group("value"))
path = value.path
if what not in {'static', 'attach'}:
if what not in {"static", "attach"}:
continue
if path.startswith('/'):
if path.startswith("/"):
path = path[1:]
else:
# relative to the source path of this content
path = self.get_relative_source_path(
os.path.join(self.relative_dir, path)
)
path = path.replace('%20', ' ')
path = path.replace("%20", " ")
static_links.add(path)
return static_links
def get_siteurl(self):
return self._context.get('localsiteurl', '')
return self._context.get("localsiteurl", "")
@memoized
def get_content(self, siteurl):
if hasattr(self, '_get_content'):
if hasattr(self, "_get_content"):
content = self._get_content()
else:
content = self._content
@ -407,15 +428,17 @@ class Content:
This is based on the summary metadata if set, otherwise truncate the
content.
"""
if 'summary' in self.metadata:
return self.metadata['summary']
if "summary" in self.metadata:
return self.metadata["summary"]
if self.settings['SUMMARY_MAX_LENGTH'] is None:
if self.settings["SUMMARY_MAX_LENGTH"] is None:
return self.content
return truncate_html_words(self.content,
self.settings['SUMMARY_MAX_LENGTH'],
self.settings['SUMMARY_END_SUFFIX'])
return truncate_html_words(
self.content,
self.settings["SUMMARY_MAX_LENGTH"],
self.settings["SUMMARY_END_SUFFIX"],
)
@property
def summary(self):
@ -424,8 +447,10 @@ class Content:
def _get_summary(self):
"""deprecated function to access summary"""
logger.warning('_get_summary() has been deprecated since 3.6.4. '
'Use the summary decorator instead')
logger.warning(
"_get_summary() has been deprecated since 3.6.4. "
"Use the summary decorator instead"
)
return self.summary
@summary.setter
@ -444,14 +469,14 @@ class Content:
@property
def url(self):
return self.get_url_setting('url')
return self.get_url_setting("url")
@property
def save_as(self):
return self.get_url_setting('save_as')
return self.get_url_setting("save_as")
def _get_template(self):
if hasattr(self, 'template') and self.template is not None:
if hasattr(self, "template") and self.template is not None:
return self.template
else:
return self.default_template
@ -470,11 +495,10 @@ class Content:
return posixize_path(
os.path.relpath(
os.path.abspath(os.path.join(
self.settings['PATH'],
source_path)),
os.path.abspath(self.settings['PATH'])
))
os.path.abspath(os.path.join(self.settings["PATH"], source_path)),
os.path.abspath(self.settings["PATH"]),
)
)
@property
def relative_dir(self):
@ -482,85 +506,84 @@ class Content:
os.path.dirname(
os.path.relpath(
os.path.abspath(self.source_path),
os.path.abspath(self.settings['PATH']))))
os.path.abspath(self.settings["PATH"]),
)
)
)
def refresh_metadata_intersite_links(self):
for key in self.settings['FORMATTED_FIELDS']:
if key in self.metadata and key != 'summary':
value = self._update_content(
self.metadata[key],
self.get_siteurl()
)
for key in self.settings["FORMATTED_FIELDS"]:
if key in self.metadata and key != "summary":
value = self._update_content(self.metadata[key], self.get_siteurl())
self.metadata[key] = value
setattr(self, key.lower(), value)
# _summary is an internal variable that some plugins may be writing to,
# so ensure changes to it are picked up
if ('summary' in self.settings['FORMATTED_FIELDS'] and
'summary' in self.metadata):
self._summary = self._update_content(
self._summary,
self.get_siteurl()
)
self.metadata['summary'] = self._summary
if (
"summary" in self.settings["FORMATTED_FIELDS"]
and "summary" in self.metadata
):
self._summary = self._update_content(self._summary, self.get_siteurl())
self.metadata["summary"] = self._summary
class Page(Content):
mandatory_properties = ('title',)
allowed_statuses = ('published', 'hidden', 'draft')
default_status = 'published'
default_template = 'page'
mandatory_properties = ("title",)
allowed_statuses = ("published", "hidden", "draft")
default_status = "published"
default_template = "page"
def _expand_settings(self, key):
klass = 'draft_page' if self.status == 'draft' else None
klass = "draft_page" if self.status == "draft" else None
return super()._expand_settings(key, klass)
class Article(Content):
mandatory_properties = ('title', 'date', 'category')
allowed_statuses = ('published', 'hidden', 'draft')
default_status = 'published'
default_template = 'article'
mandatory_properties = ("title", "date", "category")
allowed_statuses = ("published", "hidden", "draft")
default_status = "published"
default_template = "article"
def __init__(self, *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 not self.settings["WITH_FUTURE_DATES"] and hasattr(self, "date"):
if self.date.tzinfo is None:
now = datetime.datetime.now()
else:
now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc)
if self.date > now:
self.status = 'draft'
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':
if not hasattr(self, "date") and self.status == "draft":
self.date = datetime.datetime.max.replace(tzinfo=self.timezone)
def _expand_settings(self, key):
klass = 'draft' if self.status == 'draft' else 'article'
klass = "draft" if self.status == "draft" else "article"
return super()._expand_settings(key, klass)
class Static(Content):
mandatory_properties = ('title',)
default_status = 'published'
mandatory_properties = ("title",)
default_status = "published"
default_template = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._output_location_referenced = False
@deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
@deprecated_attribute(old="filepath", new="source_path", since=(3, 2, 0))
def filepath():
return None
@deprecated_attribute(old='src', new='source_path', since=(3, 2, 0))
@deprecated_attribute(old="src", new="source_path", since=(3, 2, 0))
def src():
return None
@deprecated_attribute(old='dst', new='save_as', since=(3, 2, 0))
@deprecated_attribute(old="dst", new="save_as", since=(3, 2, 0))
def dst():
return None
@ -577,8 +600,7 @@ class Static(Content):
return super().save_as
def attach_to(self, content):
"""Override our output directory with that of the given content object.
"""
"""Override our output directory with that of the given content object."""
# Determine our file's new output path relative to the linking
# document. If it currently lives beneath the linking
@ -589,8 +611,7 @@ class Static(Content):
tail_path = os.path.relpath(self.source_path, linking_source_dir)
if tail_path.startswith(os.pardir + os.sep):
tail_path = os.path.basename(tail_path)
new_save_as = os.path.join(
os.path.dirname(content.save_as), tail_path)
new_save_as = os.path.join(os.path.dirname(content.save_as), tail_path)
# We do not build our new url by joining tail_path with the linking
# document's url, because we cannot know just by looking at the latter
@ -609,12 +630,14 @@ class Static(Content):
"%s because %s. Falling back to "
"{filename} link behavior instead.",
content.get_relative_source_path(),
self.get_relative_source_path(), reason,
extra={'limit_msg': "More {attach} warnings silenced."})
self.get_relative_source_path(),
reason,
extra={"limit_msg": "More {attach} warnings silenced."},
)
# We never override an override, because we don't want to interfere
# with user-defined overrides that might be in EXTRA_PATH_METADATA.
if hasattr(self, 'override_save_as') or hasattr(self, 'override_url'):
if hasattr(self, "override_save_as") or hasattr(self, "override_url"):
if new_save_as != self.save_as or new_url != self.url:
_log_reason("its output location was already overridden")
return

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,7 @@ from collections import defaultdict
from rich.console import Console
from rich.logging import RichHandler
__all__ = [
'init'
]
__all__ = ["init"]
console = Console()
@ -34,8 +32,8 @@ class LimitFilter(logging.Filter):
return True
# extract group
group = record.__dict__.get('limit_msg', None)
group_args = record.__dict__.get('limit_args', ())
group = record.__dict__.get("limit_msg", None)
group_args = record.__dict__.get("limit_args", ())
# ignore record if it was already raised
message_key = (record.levelno, record.getMessage())
@ -50,7 +48,7 @@ class LimitFilter(logging.Filter):
if logger_level > logging.DEBUG:
template_key = (record.levelno, record.msg)
message_key = (record.levelno, record.getMessage())
if (template_key in self._ignore or message_key in self._ignore):
if template_key in self._ignore or message_key in self._ignore:
return False
# check if we went over threshold
@ -90,12 +88,12 @@ class FatalLogger(LimitLogger):
def warning(self, *args, **kwargs):
super().warning(*args, **kwargs)
if FatalLogger.warnings_fatal:
raise RuntimeError('Warning encountered')
raise RuntimeError("Warning encountered")
def error(self, *args, **kwargs):
super().error(*args, **kwargs)
if FatalLogger.errors_fatal:
raise RuntimeError('Error encountered')
raise RuntimeError("Error encountered")
logging.setLoggerClass(FatalLogger)
@ -103,17 +101,19 @@ logging.setLoggerClass(FatalLogger)
logging.getLogger().__class__ = FatalLogger
def init(level=None, fatal='', handler=RichHandler(console=console), name=None,
logs_dedup_min_level=None):
FatalLogger.warnings_fatal = fatal.startswith('warning')
def init(
level=None,
fatal="",
handler=RichHandler(console=console),
name=None,
logs_dedup_min_level=None,
):
FatalLogger.warnings_fatal = fatal.startswith("warning")
FatalLogger.errors_fatal = bool(fatal)
LOG_FORMAT = "%(message)s"
logging.basicConfig(
level=level,
format=LOG_FORMAT,
datefmt="[%H:%M:%S]",
handlers=[handler]
level=level, format=LOG_FORMAT, datefmt="[%H:%M:%S]", handlers=[handler]
)
logger = logging.getLogger(name)
@ -126,17 +126,18 @@ def init(level=None, fatal='', handler=RichHandler(console=console), name=None,
def log_warnings():
import warnings
logging.captureWarnings(True)
warnings.simplefilter("default", DeprecationWarning)
init(logging.DEBUG, name='py.warnings')
init(logging.DEBUG, name="py.warnings")
if __name__ == '__main__':
if __name__ == "__main__":
init(level=logging.DEBUG, name=__name__)
root_logger = logging.getLogger(__name__)
root_logger.debug('debug')
root_logger.info('info')
root_logger.warning('warning')
root_logger.error('error')
root_logger.critical('critical')
root_logger.debug("debug")
root_logger.info("info")
root_logger.warning("warning")
root_logger.error("error")
root_logger.critical("critical")

View file

@ -6,8 +6,8 @@ from math import ceil
logger = logging.getLogger(__name__)
PaginationRule = namedtuple(
'PaginationRule',
'min_page URL SAVE_AS',
"PaginationRule",
"min_page URL SAVE_AS",
)
@ -19,7 +19,7 @@ class Paginator:
self.settings = settings
if per_page:
self.per_page = per_page
self.orphans = settings['DEFAULT_ORPHANS']
self.orphans = settings["DEFAULT_ORPHANS"]
else:
self.per_page = len(object_list)
self.orphans = 0
@ -32,14 +32,21 @@ class Paginator:
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return Page(self.name, self.url, self.object_list[bottom:top], number,
self, self.settings)
return Page(
self.name,
self.url,
self.object_list[bottom:top],
number,
self,
self.settings,
)
def _get_count(self):
"Returns the total number of objects, across all pages."
if self._count is None:
self._count = len(self.object_list)
return self._count
count = property(_get_count)
def _get_num_pages(self):
@ -48,6 +55,7 @@ class Paginator:
hits = max(1, self.count - self.orphans)
self._num_pages = int(ceil(hits / (float(self.per_page) or 1)))
return self._num_pages
num_pages = property(_get_num_pages)
def _get_page_range(self):
@ -56,6 +64,7 @@ class Paginator:
a template for loop.
"""
return list(range(1, self.num_pages + 1))
page_range = property(_get_page_range)
@ -64,7 +73,7 @@ class Page:
self.full_name = name
self.name, self.extension = os.path.splitext(name)
dn, fn = os.path.split(name)
self.base_name = dn if fn in ('index.htm', 'index.html') else self.name
self.base_name = dn if fn in ("index.htm", "index.html") else self.name
self.base_url = url
self.object_list = object_list
self.number = number
@ -72,7 +81,7 @@ class Page:
self.settings = settings
def __repr__(self):
return '<Page {} of {}>'.format(self.number, self.paginator.num_pages)
return f"<Page {self.number} of {self.paginator.num_pages}>"
def has_next(self):
return self.number < self.paginator.num_pages
@ -117,7 +126,7 @@ class Page:
rule = None
# find the last matching pagination rule
for p in self.settings['PAGINATION_PATTERNS']:
for p in self.settings["PAGINATION_PATTERNS"]:
if p.min_page == -1:
if not self.has_next():
rule = p
@ -127,22 +136,22 @@ class Page:
rule = p
if not rule:
return ''
return ""
prop_value = getattr(rule, key)
if not isinstance(prop_value, str):
logger.warning('%s is set to %s', key, prop_value)
logger.warning("%s is set to %s", key, prop_value)
return prop_value
# URL or SAVE_AS is a string, format it with a controlled context
context = {
'save_as': self.full_name,
'url': self.base_url,
'name': self.name,
'base_name': self.base_name,
'extension': self.extension,
'number': self.number,
"save_as": self.full_name,
"url": self.base_url,
"name": self.name,
"base_name": self.base_name,
"extension": self.extension,
"number": self.number,
}
ret = prop_value.format(**context)
@ -155,9 +164,9 @@ class Page:
# changed to lstrip() because that would remove all leading slashes and
# thus make the workaround impossible. See
# test_custom_pagination_pattern() for a verification of this.
if ret.startswith('/'):
if ret.startswith("/"):
ret = ret[1:]
return ret
url = property(functools.partial(_from_settings, key='URL'))
save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
url = property(functools.partial(_from_settings, key="URL"))
save_as = property(functools.partial(_from_settings, key="SAVE_AS"))

View file

@ -24,26 +24,42 @@ def get_namespace_plugins(ns_pkg=None):
return {
name: importlib.import_module(name)
for finder, name, ispkg
in iter_namespace(ns_pkg)
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))
logger.info("Plugins found:\n" + "\n".join(ns_plugins))
else:
logger.info('No plugins are installed')
logger.info("No plugins are installed")
def plugin_enabled(name, plugin_list=None):
if plugin_list is None or not plugin_list:
# no plugins are loaded
return False
if name in plugin_list:
# search name as is
return True
if f"pelican.plugins.{name}" in plugin_list:
# check if short name is a namespace plugin
return True
return False
def load_legacy_plugin(plugin, plugin_paths):
if '.' in plugin:
if "." in plugin:
# it is in a package, try to resolve package first
package, _, _ = plugin.rpartition('.')
package, _, _ = plugin.rpartition(".")
load_legacy_plugin(package, plugin_paths)
# Try to find plugin in PLUGIN_PATHS
@ -52,7 +68,7 @@ def load_legacy_plugin(plugin, plugin_paths):
# 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))
raise ImportError(f"Cannot import plugin `{plugin}`")
else:
# Avoid loading the same plugin twice
if spec.name in sys.modules:
@ -78,30 +94,28 @@ def load_legacy_plugin(plugin, plugin_paths):
def load_plugins(settings):
logger.debug('Finding namespace plugins')
logger.debug("Finding namespace plugins")
namespace_plugins = get_namespace_plugins()
if namespace_plugins:
logger.debug('Namespace plugins found:\n' +
'\n'.join(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 settings.get("PLUGINS") is not None:
for plugin in settings["PLUGINS"]:
if isinstance(plugin, str):
logger.debug('Loading plugin `%s`', plugin)
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)]
elif f"pelican.plugins.{plugin}" in namespace_plugins:
plugin = namespace_plugins[f"pelican.plugins.{plugin}"]
# try to import it
else:
try:
plugin = load_legacy_plugin(
plugin,
settings.get('PLUGIN_PATHS', []))
plugin, settings.get("PLUGIN_PATHS", [])
)
except ImportError as e:
logger.error('Cannot load plugin `%s`\n%s', plugin, e)
logger.error("Cannot load plugin `%s`\n%s", plugin, e)
continue
plugins.append(plugin)
else:

View file

@ -1,49 +1,53 @@
from blinker import signal
from blinker import signal, Signal
from ordered_set import OrderedSet
# Signals will call functions in the order of connection, i.e. plugin order
Signal.set_class = OrderedSet
# Run-level signals:
initialized = signal('pelican_initialized')
get_generators = signal('get_generators')
all_generators_finalized = signal('all_generators_finalized')
get_writer = signal('get_writer')
finalized = signal('pelican_finalized')
initialized = signal("pelican_initialized")
get_generators = signal("get_generators")
all_generators_finalized = signal("all_generators_finalized")
get_writer = signal("get_writer")
finalized = signal("pelican_finalized")
# Reader-level signals
readers_init = signal('readers_init')
readers_init = signal("readers_init")
# Generator-level signals
generator_init = signal('generator_init')
generator_init = signal("generator_init")
article_generator_init = signal('article_generator_init')
article_generator_pretaxonomy = signal('article_generator_pretaxonomy')
article_generator_finalized = signal('article_generator_finalized')
article_generator_write_article = signal('article_generator_write_article')
article_writer_finalized = signal('article_writer_finalized')
article_generator_init = signal("article_generator_init")
article_generator_pretaxonomy = signal("article_generator_pretaxonomy")
article_generator_finalized = signal("article_generator_finalized")
article_generator_write_article = signal("article_generator_write_article")
article_writer_finalized = signal("article_writer_finalized")
page_generator_init = signal('page_generator_init')
page_generator_finalized = signal('page_generator_finalized')
page_generator_write_page = signal('page_generator_write_page')
page_writer_finalized = signal('page_writer_finalized')
page_generator_init = signal("page_generator_init")
page_generator_finalized = signal("page_generator_finalized")
page_generator_write_page = signal("page_generator_write_page")
page_writer_finalized = signal("page_writer_finalized")
static_generator_init = signal('static_generator_init')
static_generator_finalized = signal('static_generator_finalized')
static_generator_init = signal("static_generator_init")
static_generator_finalized = signal("static_generator_finalized")
# Page-level signals
article_generator_preread = signal('article_generator_preread')
article_generator_context = signal('article_generator_context')
article_generator_preread = signal("article_generator_preread")
article_generator_context = signal("article_generator_context")
page_generator_preread = signal('page_generator_preread')
page_generator_context = signal('page_generator_context')
page_generator_preread = signal("page_generator_preread")
page_generator_context = signal("page_generator_context")
static_generator_preread = signal('static_generator_preread')
static_generator_context = signal('static_generator_context')
static_generator_preread = signal("static_generator_preread")
static_generator_context = signal("static_generator_context")
content_object_init = signal('content_object_init')
content_object_init = signal("content_object_init")
# Writers signals
content_written = signal('content_written')
feed_generated = signal('feed_generated')
feed_written = signal('feed_written')
content_written = signal("content_written")
feed_generated = signal("feed_generated")
feed_written = signal("feed_written")

View file

@ -31,33 +31,29 @@ except ImportError:
_DISCARD = object()
DUPLICATES_DEFINITIONS_ALLOWED = {
'tags': False,
'date': False,
'modified': False,
'status': False,
'category': False,
'author': False,
'save_as': False,
'url': False,
'authors': False,
'slug': False
"tags": False,
"date": False,
"modified": False,
"status": False,
"category": False,
"author": False,
"save_as": False,
"url": False,
"authors": False,
"slug": False,
}
METADATA_PROCESSORS = {
'tags': lambda x, y: ([
Tag(tag, y)
for tag in ensure_metadata_list(x)
] or _DISCARD),
'date': lambda x, y: get_date(x.replace('_', ' ')),
'modified': lambda x, y: get_date(x),
'status': lambda x, y: x.strip() or _DISCARD,
'category': lambda x, y: _process_if_nonempty(Category, x, y),
'author': lambda x, y: _process_if_nonempty(Author, x, y),
'authors': lambda x, y: ([
Author(author, y)
for author in ensure_metadata_list(x)
] or _DISCARD),
'slug': lambda x, y: x.strip() or _DISCARD,
"tags": lambda x, y: ([Tag(tag, y) for tag in ensure_metadata_list(x)] or _DISCARD),
"date": lambda x, y: get_date(x.replace("_", " ")),
"modified": lambda x, y: get_date(x),
"status": lambda x, y: x.strip() or _DISCARD,
"category": lambda x, y: _process_if_nonempty(Category, x, y),
"author": lambda x, y: _process_if_nonempty(Author, x, y),
"authors": lambda x, y: (
[Author(author, y) for author in ensure_metadata_list(x)] or _DISCARD
),
"slug": lambda x, y: x.strip() or _DISCARD,
}
logger = logging.getLogger(__name__)
@ -65,25 +61,23 @@ logger = logging.getLogger(__name__)
def ensure_metadata_list(text):
"""Canonicalize the format of a list of authors or tags. This works
the same way as Docutils' "authors" field: if it's already a list,
those boundaries are preserved; otherwise, it must be a string;
if the string contains semicolons, it is split on semicolons;
otherwise, it is split on commas. This allows you to write
author lists in either "Jane Doe, John Doe" or "Doe, Jane; Doe, John"
format.
the same way as Docutils' "authors" field: if it's already a list,
those boundaries are preserved; otherwise, it must be a string;
if the string contains semicolons, it is split on semicolons;
otherwise, it is split on commas. This allows you to write
author lists in either "Jane Doe, John Doe" or "Doe, Jane; Doe, John"
format.
Regardless, all list items undergo .strip() before returning, and
empty items are discarded.
Regardless, all list items undergo .strip() before returning, and
empty items are discarded.
"""
if isinstance(text, str):
if ';' in text:
text = text.split(';')
if ";" in text:
text = text.split(";")
else:
text = text.split(',')
text = text.split(",")
return list(OrderedDict.fromkeys(
[v for v in (w.strip() for w in text) if v]
))
return list(OrderedDict.fromkeys([v for v in (w.strip() for w in text) if v]))
def _process_if_nonempty(processor, name, settings):
@ -112,8 +106,9 @@ class BaseReader:
Markdown).
"""
enabled = True
file_extensions = ['static']
file_extensions = ["static"]
extensions = None
def __init__(self, settings):
@ -132,13 +127,12 @@ class BaseReader:
class _FieldBodyTranslator(HTMLTranslator):
def __init__(self, document):
super().__init__(document)
self.compact_p = None
def astext(self):
return ''.join(self.body)
return "".join(self.body)
def visit_field_body(self, node):
pass
@ -154,27 +148,25 @@ def render_node_to_html(document, node, field_body_translator_class):
class PelicanHTMLWriter(Writer):
def __init__(self):
super().__init__()
self.translator_class = PelicanHTMLTranslator
class PelicanHTMLTranslator(HTMLTranslator):
def visit_abbreviation(self, node):
attrs = {}
if node.hasattr('explanation'):
attrs['title'] = node['explanation']
self.body.append(self.starttag(node, 'abbr', '', **attrs))
if node.hasattr("explanation"):
attrs["title"] = node["explanation"]
self.body.append(self.starttag(node, "abbr", "", **attrs))
def depart_abbreviation(self, node):
self.body.append('</abbr>')
self.body.append("</abbr>")
def visit_image(self, node):
# set an empty alt if alt is not specified
# avoids that alt is taken from src
node['alt'] = node.get('alt', '')
node["alt"] = node.get("alt", "")
return HTMLTranslator.visit_image(self, node)
@ -194,7 +186,7 @@ class RstReader(BaseReader):
"""
enabled = bool(docutils)
file_extensions = ['rst']
file_extensions = ["rst"]
writer_class = PelicanHTMLWriter
field_body_translator_class = _FieldBodyTranslator
@ -202,25 +194,28 @@ class RstReader(BaseReader):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
lang_code = self.settings.get('DEFAULT_LANG', 'en')
lang_code = self.settings.get("DEFAULT_LANG", "en")
if get_docutils_lang(lang_code):
self._language_code = lang_code
else:
logger.warning("Docutils has no localization for '%s'."
" Using 'en' instead.", lang_code)
self._language_code = 'en'
logger.warning(
"Docutils has no localization for '%s'." " Using 'en' instead.",
lang_code,
)
self._language_code = "en"
def _parse_metadata(self, document, source_path):
"""Return the dict containing document metadata"""
formatted_fields = self.settings['FORMATTED_FIELDS']
formatted_fields = self.settings["FORMATTED_FIELDS"]
output = {}
if document.first_child_matching_class(docutils.nodes.title) is None:
logger.warning(
'Document title missing in file %s: '
'Ensure exactly one top level section',
source_path)
"Document title missing in file %s: "
"Ensure exactly one top level section",
source_path,
)
try:
# docutils 0.18.1+
@ -231,16 +226,16 @@ class RstReader(BaseReader):
for docinfo in nodes:
for element in docinfo.children:
if element.tagname == 'field': # custom fields (e.g. summary)
if element.tagname == "field": # custom fields (e.g. summary)
name_elem, body_elem = element.children
name = name_elem.astext()
if name.lower() in formatted_fields:
value = render_node_to_html(
document, body_elem,
self.field_body_translator_class)
document, body_elem, self.field_body_translator_class
)
else:
value = body_elem.astext()
elif element.tagname == 'authors': # author list
elif element.tagname == "authors": # author list
name = element.tagname
value = [element.astext() for element in element.children]
else: # standard fields (e.g. address)
@ -252,22 +247,24 @@ class RstReader(BaseReader):
return output
def _get_publisher(self, source_path):
extra_params = {'initial_header_level': '2',
'syntax_highlight': 'short',
'input_encoding': 'utf-8',
'language_code': self._language_code,
'halt_level': 2,
'traceback': True,
'warning_stream': StringIO(),
'embed_stylesheet': False}
user_params = self.settings.get('DOCUTILS_SETTINGS')
extra_params = {
"initial_header_level": "2",
"syntax_highlight": "short",
"input_encoding": "utf-8",
"language_code": self._language_code,
"halt_level": 2,
"traceback": True,
"warning_stream": StringIO(),
"embed_stylesheet": False,
}
user_params = self.settings.get("DOCUTILS_SETTINGS")
if user_params:
extra_params.update(user_params)
pub = docutils.core.Publisher(
writer=self.writer_class(),
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
writer=self.writer_class(), destination_class=docutils.io.StringOutput
)
pub.set_components("standalone", "restructuredtext", "html")
pub.process_programmatic_settings(None, extra_params, None)
pub.set_source(source_path=source_path)
pub.publish()
@ -277,10 +274,10 @@ class RstReader(BaseReader):
"""Parses restructured text"""
pub = self._get_publisher(source_path)
parts = pub.writer.parts
content = parts.get('body')
content = parts.get("body")
metadata = self._parse_metadata(pub.document, source_path)
metadata.setdefault('title', parts.get('title'))
metadata.setdefault("title", parts.get("title"))
return content, metadata
@ -289,26 +286,26 @@ class MarkdownReader(BaseReader):
"""Reader for Markdown files"""
enabled = bool(Markdown)
file_extensions = ['md', 'markdown', 'mkd', 'mdown']
file_extensions = ["md", "markdown", "mkd", "mdown"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
settings = self.settings['MARKDOWN']
settings.setdefault('extension_configs', {})
settings.setdefault('extensions', [])
for extension in settings['extension_configs'].keys():
if extension not in settings['extensions']:
settings['extensions'].append(extension)
if 'markdown.extensions.meta' not in settings['extensions']:
settings['extensions'].append('markdown.extensions.meta')
settings = self.settings["MARKDOWN"]
settings.setdefault("extension_configs", {})
settings.setdefault("extensions", [])
for extension in settings["extension_configs"].keys():
if extension not in settings["extensions"]:
settings["extensions"].append(extension)
if "markdown.extensions.meta" not in settings["extensions"]:
settings["extensions"].append("markdown.extensions.meta")
self._source_path = None
def _parse_metadata(self, meta):
"""Return the dict containing document metadata"""
formatted_fields = self.settings['FORMATTED_FIELDS']
formatted_fields = self.settings["FORMATTED_FIELDS"]
# prevent metadata extraction in fields
self._md.preprocessors.deregister('meta')
self._md.preprocessors.deregister("meta")
output = {}
for name, value in meta.items():
@ -323,9 +320,10 @@ class MarkdownReader(BaseReader):
elif not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
if len(value) > 1:
logger.warning(
'Duplicate definition of `%s` '
'for %s. Using first one.',
name, self._source_path)
"Duplicate definition of `%s` " "for %s. Using first one.",
name,
self._source_path,
)
output[name] = self.process_metadata(name, value[0])
elif len(value) > 1:
# handle list metadata as list of string
@ -339,11 +337,11 @@ class MarkdownReader(BaseReader):
"""Parse content and metadata of markdown files"""
self._source_path = source_path
self._md = Markdown(**self.settings['MARKDOWN'])
self._md = Markdown(**self.settings["MARKDOWN"])
with pelican_open(source_path) as text:
content = self._md.convert(text)
if hasattr(self._md, 'Meta'):
if hasattr(self._md, "Meta"):
metadata = self._parse_metadata(self._md.Meta)
else:
metadata = {}
@ -353,17 +351,17 @@ class MarkdownReader(BaseReader):
class HTMLReader(BaseReader):
"""Parses HTML files as input, looking for meta, title, and body tags"""
file_extensions = ['htm', 'html']
file_extensions = ["htm", "html"]
enabled = True
class _HTMLParser(HTMLParser):
def __init__(self, settings, filename):
super().__init__(convert_charrefs=False)
self.body = ''
self.body = ""
self.metadata = {}
self.settings = settings
self._data_buffer = ''
self._data_buffer = ""
self._filename = filename
@ -374,94 +372,100 @@ class HTMLReader(BaseReader):
self._in_tags = False
def handle_starttag(self, tag, attrs):
if tag == 'head' and self._in_top_level:
if tag == "head" and self._in_top_level:
self._in_top_level = False
self._in_head = True
elif tag == 'title' and self._in_head:
elif tag == "title" and self._in_head:
self._in_title = True
self._data_buffer = ''
elif tag == 'body' and self._in_top_level:
self._data_buffer = ""
elif tag == "body" and self._in_top_level:
self._in_top_level = False
self._in_body = True
self._data_buffer = ''
elif tag == 'meta' and self._in_head:
self._data_buffer = ""
elif tag == "meta" and self._in_head:
self._handle_meta_tag(attrs)
elif self._in_body:
self._data_buffer += self.build_tag(tag, attrs, False)
def handle_endtag(self, tag):
if tag == 'head':
if tag == "head":
if self._in_head:
self._in_head = False
self._in_top_level = True
elif self._in_head and tag == 'title':
elif self._in_head and tag == "title":
self._in_title = False
self.metadata['title'] = self._data_buffer
elif tag == 'body':
self.metadata["title"] = self._data_buffer
elif tag == "body":
self.body = self._data_buffer
self._in_body = False
self._in_top_level = True
elif self._in_body:
self._data_buffer += '</{}>'.format(escape(tag))
self._data_buffer += f"</{escape(tag)}>"
def handle_startendtag(self, tag, attrs):
if tag == 'meta' and self._in_head:
if tag == "meta" and self._in_head:
self._handle_meta_tag(attrs)
if self._in_body:
self._data_buffer += self.build_tag(tag, attrs, True)
def handle_comment(self, data):
self._data_buffer += '<!--{}-->'.format(data)
self._data_buffer += f"<!--{data}-->"
def handle_data(self, data):
self._data_buffer += data
def handle_entityref(self, data):
self._data_buffer += '&{};'.format(data)
self._data_buffer += f"&{data};"
def handle_charref(self, data):
self._data_buffer += '&#{};'.format(data)
self._data_buffer += f"&#{data};"
def build_tag(self, tag, attrs, close_tag):
result = '<{}'.format(escape(tag))
result = f"<{escape(tag)}"
for k, v in attrs:
result += ' ' + escape(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(v, quote=False))
result += f"='{escape(v, quote=False)}'"
else:
result += '="{}"'.format(escape(v, quote=False))
result += f'="{escape(v, quote=False)}"'
if close_tag:
return result + ' />'
return result + '>'
return result + " />"
return result + ">"
def _handle_meta_tag(self, attrs):
name = self._attr_value(attrs, 'name')
name = self._attr_value(attrs, "name")
if name is None:
attr_list = ['{}="{}"'.format(k, v) for k, v in attrs]
attr_serialized = ', '.join(attr_list)
logger.warning("Meta tag in file %s does not have a 'name' "
"attribute, skipping. Attributes: %s",
self._filename, attr_serialized)
attr_list = [f'{k}="{v}"' for k, v in attrs]
attr_serialized = ", ".join(attr_list)
logger.warning(
"Meta tag in file %s does not have a 'name' "
"attribute, skipping. Attributes: %s",
self._filename,
attr_serialized,
)
return
name = name.lower()
contents = self._attr_value(attrs, 'content', '')
contents = self._attr_value(attrs, "content", "")
if not contents:
contents = self._attr_value(attrs, 'contents', '')
contents = self._attr_value(attrs, "contents", "")
if contents:
logger.warning(
"Meta tag attribute 'contents' used in file %s, should"
" be changed to 'content'",
self._filename,
extra={'limit_msg': "Other files have meta tag "
"attribute 'contents' that should "
"be changed to 'content'"})
extra={
"limit_msg": "Other files have meta tag "
"attribute 'contents' that should "
"be changed to 'content'"
},
)
if name == 'keywords':
name = 'tags'
if name == "keywords":
name = "tags"
if name in self.metadata:
# if this metadata already exists (i.e. a previous tag with the
@ -501,22 +505,23 @@ class Readers(FileStampDataCacher):
"""
def __init__(self, settings=None, cache_name=''):
def __init__(self, settings=None, cache_name=""):
self.settings = settings or {}
self.readers = {}
self.reader_classes = {}
for cls in [BaseReader] + BaseReader.__subclasses__():
if not cls.enabled:
logger.debug('Missing dependencies for %s',
', '.join(cls.file_extensions))
logger.debug(
"Missing dependencies for %s", ", ".join(cls.file_extensions)
)
continue
for ext in cls.file_extensions:
self.reader_classes[ext] = cls
if self.settings['READERS']:
self.reader_classes.update(self.settings['READERS'])
if self.settings["READERS"]:
self.reader_classes.update(self.settings["READERS"])
signals.readers_init.send(self)
@ -527,53 +532,67 @@ class Readers(FileStampDataCacher):
self.readers[fmt] = reader_class(self.settings)
# set up caching
cache_this_level = (cache_name != '' and
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']
cache_this_level = (
cache_name != "" and 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().__init__(settings, cache_name, caching_policy, load_policy)
@property
def extensions(self):
return self.readers.keys()
def read_file(self, base_path, path, content_class=Page, fmt=None,
context=None, preread_signal=None, preread_sender=None,
context_signal=None, context_sender=None):
def read_file(
self,
base_path,
path,
content_class=Page,
fmt=None,
context=None,
preread_signal=None,
preread_sender=None,
context_signal=None,
context_sender=None,
):
"""Return a content object parsed with the given format."""
path = os.path.abspath(os.path.join(base_path, path))
source_path = posixize_path(os.path.relpath(path, base_path))
logger.debug(
'Read file %s -> %s',
source_path, content_class.__name__)
logger.debug("Read file %s -> %s", source_path, content_class.__name__)
if not fmt:
_, ext = os.path.splitext(os.path.basename(path))
fmt = ext[1:]
if fmt not in self.readers:
raise TypeError(
'Pelican does not know how to parse %s', path)
raise TypeError("Pelican does not know how to parse %s", path)
if preread_signal:
logger.debug(
'Signal %s.send(%s)',
preread_signal.name, preread_sender)
logger.debug("Signal %s.send(%s)", preread_signal.name, preread_sender)
preread_signal.send(preread_sender)
reader = self.readers[fmt]
metadata = _filter_discardable_metadata(default_metadata(
settings=self.settings, process=reader.process_metadata))
metadata.update(path_metadata(
full_path=path, source_path=source_path,
settings=self.settings))
metadata.update(_filter_discardable_metadata(parse_path_metadata(
source_path=source_path, settings=self.settings,
process=reader.process_metadata)))
metadata = _filter_discardable_metadata(
default_metadata(settings=self.settings, process=reader.process_metadata)
)
metadata.update(
path_metadata(
full_path=path, source_path=source_path, settings=self.settings
)
)
metadata.update(
_filter_discardable_metadata(
parse_path_metadata(
source_path=source_path,
settings=self.settings,
process=reader.process_metadata,
)
)
)
reader_name = reader.__class__.__name__
metadata['reader'] = reader_name.replace('Reader', '').lower()
metadata["reader"] = reader_name.replace("Reader", "").lower()
content, reader_metadata = self.get_cached_data(path, (None, None))
if content is None:
@ -587,14 +606,14 @@ class Readers(FileStampDataCacher):
find_empty_alt(content, path)
# eventually filter the content with typogrify if asked so
if self.settings['TYPOGRIFY']:
if self.settings["TYPOGRIFY"]:
from typogrify.filters import typogrify
import smartypants
typogrify_dashes = self.settings['TYPOGRIFY_DASHES']
if typogrify_dashes == 'oldschool':
typogrify_dashes = self.settings["TYPOGRIFY_DASHES"]
if typogrify_dashes == "oldschool":
smartypants.Attr.default = smartypants.Attr.set2
elif typogrify_dashes == 'oldschool_inverted':
elif typogrify_dashes == "oldschool_inverted":
smartypants.Attr.default = smartypants.Attr.set3
else:
smartypants.Attr.default = smartypants.Attr.set1
@ -608,31 +627,32 @@ class Readers(FileStampDataCacher):
def typogrify_wrapper(text):
"""Ensures ignore_tags feature is backward compatible"""
try:
return typogrify(
text,
self.settings['TYPOGRIFY_IGNORE_TAGS'])
return typogrify(text, self.settings["TYPOGRIFY_IGNORE_TAGS"])
except TypeError:
return typogrify(text)
if content:
content = typogrify_wrapper(content)
if 'title' in metadata:
metadata['title'] = typogrify_wrapper(metadata['title'])
if "title" in metadata:
metadata["title"] = typogrify_wrapper(metadata["title"])
if 'summary' in metadata:
metadata['summary'] = typogrify_wrapper(metadata['summary'])
if "summary" in metadata:
metadata["summary"] = typogrify_wrapper(metadata["summary"])
if context_signal:
logger.debug(
'Signal %s.send(%s, <metadata>)',
context_signal.name,
context_sender)
"Signal %s.send(%s, <metadata>)", context_signal.name, context_sender
)
context_signal.send(context_sender, metadata=metadata)
return content_class(content=content, metadata=metadata,
settings=self.settings, source_path=path,
context=context)
return content_class(
content=content,
metadata=metadata,
settings=self.settings,
source_path=path,
context=context,
)
def find_empty_alt(content, path):
@ -642,7 +662,8 @@ def find_empty_alt(content, path):
as they are really likely to be accessibility flaws.
"""
imgs = re.compile(r"""
imgs = re.compile(
r"""
(?:
# src before alt
<img
@ -658,53 +679,57 @@ def find_empty_alt(content, path):
[^\>]*
src=(['"])(.*?)\5
)
""", re.X)
""",
re.X,
)
for match in re.findall(imgs, content):
logger.warning(
'Empty alt attribute for image %s in %s',
os.path.basename(match[1] + match[5]), path,
extra={'limit_msg': 'Other images have empty alt attributes'})
"Empty alt attribute for image %s in %s",
os.path.basename(match[1] + match[5]),
path,
extra={"limit_msg": "Other images have empty alt attributes"},
)
def default_metadata(settings=None, process=None):
metadata = {}
if settings:
for name, value in dict(settings.get('DEFAULT_METADATA', {})).items():
for name, value in dict(settings.get("DEFAULT_METADATA", {})).items():
if process:
value = process(name, value)
metadata[name] = value
if 'DEFAULT_CATEGORY' in settings:
value = settings['DEFAULT_CATEGORY']
if "DEFAULT_CATEGORY" in settings:
value = settings["DEFAULT_CATEGORY"]
if process:
value = process('category', value)
metadata['category'] = value
if settings.get('DEFAULT_DATE', None) and \
settings['DEFAULT_DATE'] != 'fs':
if isinstance(settings['DEFAULT_DATE'], str):
metadata['date'] = get_date(settings['DEFAULT_DATE'])
value = process("category", value)
metadata["category"] = value
if settings.get("DEFAULT_DATE", None) and settings["DEFAULT_DATE"] != "fs":
if isinstance(settings["DEFAULT_DATE"], str):
metadata["date"] = get_date(settings["DEFAULT_DATE"])
else:
metadata['date'] = datetime.datetime(*settings['DEFAULT_DATE'])
metadata["date"] = datetime.datetime(*settings["DEFAULT_DATE"])
return metadata
def path_metadata(full_path, source_path, settings=None):
metadata = {}
if settings:
if settings.get('DEFAULT_DATE', None) == 'fs':
metadata['date'] = datetime.datetime.fromtimestamp(
os.stat(full_path).st_mtime)
metadata['modified'] = metadata['date']
if settings.get("DEFAULT_DATE", None) == "fs":
metadata["date"] = datetime.datetime.fromtimestamp(
os.stat(full_path).st_mtime
)
metadata["modified"] = metadata["date"]
# Apply EXTRA_PATH_METADATA for the source path and the paths of any
# parent directories. Sorting EPM first ensures that the most specific
# path wins conflicts.
epm = settings.get('EXTRA_PATH_METADATA', {})
epm = settings.get("EXTRA_PATH_METADATA", {})
for path, meta in sorted(epm.items()):
# Enforce a trailing slash when checking for parent directories.
# This prevents false positives when one file or directory's name
# is a prefix of another's.
dirpath = posixize_path(os.path.join(path, ''))
dirpath = posixize_path(os.path.join(path, ""))
if source_path == path or source_path.startswith(dirpath):
metadata.update(meta)
@ -736,11 +761,10 @@ def parse_path_metadata(source_path, settings=None, process=None):
subdir = os.path.basename(dirname)
if settings:
checks = []
for key, data in [('FILENAME_METADATA', base),
('PATH_METADATA', source_path)]:
for key, data in [("FILENAME_METADATA", base), ("PATH_METADATA", source_path)]:
checks.append((settings.get(key, None), data))
if settings.get('USE_FOLDER_AS_CATEGORY', None):
checks.append(('(?P<category>.*)', subdir))
if settings.get("USE_FOLDER_AS_CATEGORY", None):
checks.append(("(?P<category>.*)", subdir))
for regexp, data in checks:
if regexp and data:
match = re.match(regexp, data)

View file

@ -11,26 +11,26 @@ import pelican.settings as pys
class Pygments(Directive):
""" Source code syntax highlighting.
"""
"""Source code syntax highlighting."""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
'anchorlinenos': directives.flag,
'classprefix': directives.unchanged,
'hl_lines': directives.unchanged,
'lineanchors': directives.unchanged,
'linenos': directives.unchanged,
'linenospecial': directives.nonnegative_int,
'linenostart': directives.nonnegative_int,
'linenostep': directives.nonnegative_int,
'lineseparator': directives.unchanged,
'linespans': directives.unchanged,
'nobackground': directives.flag,
'nowrap': directives.flag,
'tagsfile': directives.unchanged,
'tagurlformat': directives.unchanged,
"anchorlinenos": directives.flag,
"classprefix": directives.unchanged,
"hl_lines": directives.unchanged,
"lineanchors": directives.unchanged,
"linenos": directives.unchanged,
"linenospecial": directives.nonnegative_int,
"linenostart": directives.nonnegative_int,
"linenostep": directives.nonnegative_int,
"lineseparator": directives.unchanged,
"linespans": directives.unchanged,
"nobackground": directives.flag,
"nowrap": directives.flag,
"tagsfile": directives.unchanged,
"tagurlformat": directives.unchanged,
}
has_content = True
@ -49,28 +49,30 @@ class Pygments(Directive):
if k not in self.options:
self.options[k] = v
if ('linenos' in self.options and
self.options['linenos'] not in ('table', 'inline')):
if self.options['linenos'] == 'none':
self.options.pop('linenos')
if "linenos" in self.options and self.options["linenos"] not in (
"table",
"inline",
):
if self.options["linenos"] == "none":
self.options.pop("linenos")
else:
self.options['linenos'] = 'table'
self.options["linenos"] = "table"
for flag in ('nowrap', 'nobackground', 'anchorlinenos'):
for flag in ("nowrap", "nobackground", "anchorlinenos"):
if flag in self.options:
self.options[flag] = True
# noclasses should already default to False, but just in case...
formatter = HtmlFormatter(noclasses=False, **self.options)
parsed = highlight('\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')]
parsed = highlight("\n".join(self.content), lexer, formatter)
return [nodes.raw("", parsed, format="html")]
directives.register_directive('code-block', Pygments)
directives.register_directive('sourcecode', Pygments)
directives.register_directive("code-block", Pygments)
directives.register_directive("sourcecode", Pygments)
_abbr_re = re.compile(r'\((.*)\)$', re.DOTALL)
_abbr_re = re.compile(r"\((.*)\)$", re.DOTALL)
class abbreviation(nodes.Inline, nodes.TextElement):
@ -82,9 +84,9 @@ def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
m = _abbr_re.search(text)
if m is None:
return [abbreviation(text, text)], []
abbr = text[:m.start()].strip()
abbr = text[: m.start()].strip()
expl = m.group(1)
return [abbreviation(abbr, abbr, explanation=expl)], []
roles.register_local_role('abbr', abbr_role)
roles.register_local_role("abbr", abbr_role)

View file

@ -14,38 +14,47 @@ except ImportError:
from pelican.log import console # noqa: F401
from pelican.log import init as init_logging
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(
description='Pelican Development Server',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
description="Pelican Development Server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"port", default=8000, type=int, nargs="?", help="Port to Listen On"
)
parser.add_argument("server", default="", nargs="?", help="Interface to Listen On")
parser.add_argument("--ssl", action="store_true", help="Activate SSL listener")
parser.add_argument(
"--cert",
default="./cert.pem",
nargs="?",
help="Path to certificate file. " + "Relative to current directory",
)
parser.add_argument(
"--key",
default="./key.pem",
nargs="?",
help="Path to certificate key file. " + "Relative to current directory",
)
parser.add_argument(
"--path",
default=".",
help="Path to pelican source directory to serve. "
+ "Relative to current directory",
)
parser.add_argument("port", default=8000, type=int, nargs="?",
help="Port to Listen On")
parser.add_argument("server", default="", nargs="?",
help="Interface to Listen On")
parser.add_argument('--ssl', action="store_true",
help='Activate SSL listener')
parser.add_argument('--cert', default="./cert.pem", nargs="?",
help='Path to certificate file. ' +
'Relative to current directory')
parser.add_argument('--key', default="./key.pem", nargs="?",
help='Path to certificate key file. ' +
'Relative to current directory')
parser.add_argument('--path', default=".",
help='Path to pelican source directory to serve. ' +
'Relative to current directory')
return parser.parse_args()
class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
SUFFIXES = ['.html', '/index.html', '/', '']
SUFFIXES = [".html", "/index.html", "/", ""]
extensions_map = {
**server.SimpleHTTPRequestHandler.extensions_map,
** {
**{
# web fonts
".oft": "font/oft",
".sfnt": "font/sfnt",
@ -57,13 +66,13 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
def translate_path(self, path):
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = path.split("?", 1)[0]
path = path.split("#", 1)[0]
# Don't forget explicit trailing slash when normalizing. Issue17324
trailing_slash = path.rstrip().endswith('/')
trailing_slash = path.rstrip().endswith("/")
path = urllib.parse.unquote(path)
path = posixpath.normpath(path)
words = path.split('/')
words = path.split("/")
words = filter(None, words)
path = self.base_path
for word in words:
@ -72,12 +81,12 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
continue
path = os.path.join(path, word)
if trailing_slash:
path += '/'
path += "/"
return path
def do_GET(self):
# cut off a query string
original_path = self.path.split('?', 1)[0]
original_path = self.path.split("?", 1)[0]
# try to find file
self.path = self.get_path_that_exists(original_path)
@ -88,12 +97,12 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
def get_path_that_exists(self, original_path):
# Try to strip trailing slash
trailing_slash = original_path.endswith('/')
original_path = original_path.rstrip('/')
trailing_slash = original_path.endswith("/")
original_path = original_path.rstrip("/")
# Try to detect file by applying various suffixes
tries = []
for suffix in self.SUFFIXES:
if not trailing_slash and suffix == '/':
if not trailing_slash and suffix == "/":
# if original request does not have trailing slash, skip the '/' suffix
# so that base class can redirect if needed
continue
@ -101,18 +110,17 @@ class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
if os.path.exists(self.translate_path(path)):
return path
tries.append(path)
logger.warning("Unable to find `%s` or variations:\n%s",
original_path,
'\n'.join(tries))
logger.warning(
"Unable to find `%s` or variations:\n%s", original_path, "\n".join(tries)
)
return None
def guess_type(self, path):
"""Guess at the mime type for the specified file.
"""
"""Guess at the mime type for the specified file."""
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:
if mimetype == "application/octet-stream" and magic_from_file:
mimetype = magic_from_file(path, mime=True)
return mimetype
@ -127,31 +135,33 @@ class RootedHTTPServer(server.HTTPServer):
self.RequestHandlerClass.base_path = base_path
if __name__ == '__main__':
if __name__ == "__main__":
init_logging(level=logging.INFO)
logger.warning("'python -m pelican.server' is deprecated.\nThe "
"Pelican development server should be run via "
"'pelican --listen' or 'pelican -l'.\nThis can be combined "
"with regeneration as 'pelican -lr'.\nRerun 'pelican-"
"quickstart' to get new Makefile and tasks.py files.")
logger.warning(
"'python -m pelican.server' is deprecated.\nThe "
"Pelican development server should be run via "
"'pelican --listen' or 'pelican -l'.\nThis can be combined "
"with regeneration as 'pelican -lr'.\nRerun 'pelican-"
"quickstart' to get new Makefile and tasks.py files."
)
args = parse_arguments()
RootedHTTPServer.allow_reuse_address = True
try:
httpd = RootedHTTPServer(
args.path, (args.server, args.port), ComplexHTTPRequestHandler)
args.path, (args.server, args.port), ComplexHTTPRequestHandler
)
if args.ssl:
httpd.socket = ssl.wrap_socket(
httpd.socket, keyfile=args.key,
certfile=args.cert, server_side=True)
httpd.socket, keyfile=args.key, certfile=args.cert, server_side=True
)
except ssl.SSLError as e:
logger.error("Couldn't open certificate file %s or key file %s",
args.cert, args.key)
logger.error("Could not listen on port %s, server %s.",
args.port, args.server)
sys.exit(getattr(e, 'exitcode', 1))
logger.error(
"Couldn't open certificate file %s or key file %s", args.cert, args.key
)
logger.error("Could not listen on port %s, server %s.", args.port, args.server)
sys.exit(getattr(e, "exitcode", 1))
logger.info("Serving at port %s, server %s.",
args.port, args.server)
logger.info("Serving at port %s, server %s.", args.port, args.server)
try:
httpd.serve_forever()
except KeyboardInterrupt:

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
raise ImportError(
'Importing from `pelican.signals` is deprecated. '
'Use `from pelican import signals` or `import pelican.plugins.signals` instead.'
"Importing from `pelican.signals` is deprecated. "
"Use `from pelican import signals` or `import pelican.plugins.signals` instead."
)

View file

@ -9,4 +9,4 @@ Used for pelican test
The quick brown fox .
This page is a draft
This page is a draft

View file

@ -0,0 +1,7 @@
def pytest_addoption(parser):
parser.addoption(
"--check-build",
action="store",
default=False,
help="Check wheel contents.",
)

View file

@ -0,0 +1,66 @@
from re import match
import tarfile
from pathlib import Path
from zipfile import ZipFile
import pytest
@pytest.mark.skipif(
"not config.getoption('--check-build')",
reason="Only run when --check-build is given",
)
def test_wheel_contents(pytestconfig):
"""
This test should test the contents of the wheel to make sure
that everything that is needed is included in the final build
"""
dist_folder = pytestconfig.getoption("--check-build")
wheels = Path(dist_folder).rglob("*.whl")
for wheel_file in wheels:
files_list = ZipFile(wheel_file).namelist()
# Check if theme files are copied to wheel
simple_theme = Path("./pelican/themes/simple/templates")
for x in simple_theme.iterdir():
assert str(x) in files_list
# Check if tool templates are copied to wheel
tools = Path("./pelican/tools/templates")
for x in tools.iterdir():
assert str(x) in files_list
assert "pelican/tools/templates/tasks.py.jinja2" in files_list
@pytest.mark.skipif(
"not config.getoption('--check-build')",
reason="Only run when --check-build is given",
)
@pytest.mark.parametrize(
"expected_file",
[
("THANKS"),
("README.rst"),
("CONTRIBUTING.rst"),
("docs/changelog.rst"),
("samples/"),
],
)
def test_sdist_contents(pytestconfig, expected_file):
"""
This test should test the contents of the source distribution to make sure
that everything that is needed is included in the final build.
"""
dist_folder = pytestconfig.getoption("--check-build")
sdist_files = Path(dist_folder).rglob("*.tar.gz")
for dist in sdist_files:
files_list = tarfile.open(dist, "r:gz").getnames()
dir_matcher = ""
if expected_file.endswith("/"):
dir_matcher = ".*"
filtered_values = [
path
for path in files_list
if match(rf"^pelican-\d\.\d\.\d/{expected_file}{dir_matcher}$", path)
]
assert len(filtered_values) > 0

View file

@ -3,4 +3,3 @@ author: Alexis Métaireau
Markdown with filename metadata
===============================

View file

@ -5,4 +5,3 @@ Title: Test Markdown extensions
## Level1
### Level2

View file

@ -3,4 +3,3 @@ This is a super article !
#########################
:Category: Yeah

View file

@ -3,4 +3,3 @@ This is an article without category !
#####################################
This article should be in the DEFAULT_CATEGORY.

View file

@ -1064,4 +1064,4 @@
<gd:extendedProperty name="blogger.itemClass" value="pid-944253050"/>
<gd:extendedProperty name="blogger.displayTime" value="29 november 2010 om 12:35"/>
</entry>
</feed>
</feed>

View file

@ -1 +1 @@



View file

@ -52,4 +52,3 @@ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

View file

@ -685,7 +685,52 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]></con
<wp:meta_key>_edit_last</wp:meta_key>
<wp:meta_value><![CDATA[3]]></wp:meta_value>
</wp:postmeta>
</item>
</item>
<item>
<title>Caption on image</title>
<link>http://thisisa.test/?p=176</link>
<pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
<dc:creator>bob</dc:creator>
<guid isPermaLink="false">http://thisisa.test/?p=176</guid>
<description></description>
<content:encoded><![CDATA[Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
[caption attachment_id="42" align="aligncenter" width="300" caption="This is a pelican"]<img src="/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png"/>[/caption]
[caption attachment_id="43" align="aligncenter" width="300"]<img src="/theme/img/xpelican-3.png.pagespeed.ic.m-NAIdRCOM.png" width="300" height="216" class="size-medium wp-image-1055" /> This also a pelican[/caption]
[caption attachment_id="44" align="aligncenter" width="300"]<a href="https://getpelican.com/"><img src="/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png" alt=""/> Yet another pelican[/caption]
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]></content:encoded>
<excerpt:encoded><![CDATA[]]></excerpt:encoded>
<wp:post_id>176</wp:post_id>
<wp:post_date>2012-02-16 15:52:55</wp:post_date>
<wp:post_date_gmt>0000-00-00 00:00:00</wp:post_date_gmt>
<wp:comment_status>open</wp:comment_status>
<wp:ping_status>open</wp:ping_status>
<wp:post_name>caption-on-image</wp:post_name>
<wp:status>publish</wp:status>
<wp:post_parent>0</wp:post_parent>
<wp:menu_order>0</wp:menu_order>
<wp:post_type>post</wp:post_type>
<wp:post_password></wp:post_password>
<wp:is_sticky>0</wp:is_sticky>
<category domain="category" nicename="category-2"><![CDATA[Category 2]]></category>
<wp:postmeta>
<wp:meta_key>_edit_last</wp:meta_key>
<wp:meta_value><![CDATA[3]]></wp:meta_value>
</wp:postmeta>
</item>
<item>
<title>A custom post in category 4</title>
<link>http://thisisa.test/?p=175</link>
@ -793,7 +838,7 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]></con
<wp:meta_key>_edit_last</wp:meta_key>
<wp:meta_value><![CDATA[3]]></wp:meta_value>
</wp:postmeta>
</item>
</item>
<item>
<title>A 2nd custom post type also in category 5</title>
<link>http://thisisa.test/?p=177</link>

View file

@ -1,43 +1,47 @@
AUTHOR = 'Alexis Métaireau'
AUTHOR = "Alexis Métaireau"
SITENAME = "Alexis' log"
SITEURL = 'http://blog.notmyidea.org'
TIMEZONE = 'UTC'
SITEURL = "http://blog.notmyidea.org"
TIMEZONE = "UTC"
GITHUB_URL = 'http://github.com/ametaireau/'
GITHUB_URL = "http://github.com/ametaireau/"
DISQUS_SITENAME = "blog-notmyidea"
PDF_GENERATOR = False
REVERSE_CATEGORY_ORDER = True
DEFAULT_PAGINATION = 2
FEED_RSS = 'feeds/all.rss.xml'
CATEGORY_FEED_RSS = 'feeds/{slug}.rss.xml'
FEED_RSS = "feeds/all.rss.xml"
CATEGORY_FEED_RSS = "feeds/{slug}.rss.xml"
LINKS = (('Biologeek', 'http://biologeek.org'),
('Filyb', "http://filyb.info/"),
('Libert-fr', "http://www.libert-fr.com"),
('N1k0', "http://prendreuncafe.com/blog/"),
('Tarek Ziadé', "http://ziade.org/blog"),
('Zubin Mithra', "http://zubin71.wordpress.com/"),)
LINKS = (
("Biologeek", "http://biologeek.org"),
("Filyb", "http://filyb.info/"),
("Libert-fr", "http://www.libert-fr.com"),
("N1k0", "http://prendreuncafe.com/blog/"),
("Tarek Ziadé", "http://ziade.org/blog"),
("Zubin Mithra", "http://zubin71.wordpress.com/"),
)
SOCIAL = (('twitter', 'http://twitter.com/ametaireau'),
('lastfm', 'http://lastfm.com/user/akounet'),
('github', 'http://github.com/ametaireau'),)
SOCIAL = (
("twitter", "http://twitter.com/ametaireau"),
("lastfm", "http://lastfm.com/user/akounet"),
("github", "http://github.com/ametaireau"),
)
# global metadata to all the contents
DEFAULT_METADATA = {'yeah': 'it is'}
DEFAULT_METADATA = {"yeah": "it is"}
# path-specific metadata
EXTRA_PATH_METADATA = {
'extra/robots.txt': {'path': 'robots.txt'},
"extra/robots.txt": {"path": "robots.txt"},
}
# static paths will be copied without parsing their contents
STATIC_PATHS = [
'pictures',
'extra/robots.txt',
"pictures",
"extra/robots.txt",
]
FORMATTED_FIELDS = ['summary', 'custom_formatted_field']
FORMATTED_FIELDS = ["summary", "custom_formatted_field"]
# foobar will not be used, because it's not in caps. All configuration keys
# have to be in caps

View file

@ -1,4 +1,4 @@
NAME = 'namespace plugin'
NAME = "namespace plugin"
def register():

View file

@ -58,10 +58,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -60,10 +60,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -57,10 +57,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -57,10 +57,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -57,10 +57,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -102,10 +102,10 @@ YEAH !</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -42,10 +42,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -45,10 +45,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -58,10 +58,10 @@ YEAH !</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -115,10 +115,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -126,10 +126,10 @@ pelican.conf, it will …</p></div>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -66,10 +66,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -58,10 +58,10 @@ listed anywhere else.</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -58,10 +58,10 @@ listed anywhere else.</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -57,10 +57,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -265,10 +265,10 @@ pelican.conf, it will …</p></div>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -61,10 +61,10 @@ Translations:
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -69,10 +69,10 @@ YEAH !</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -41,10 +41,10 @@ at a custom location.</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -41,10 +41,10 @@ Anyone can see this page but it's not linked to anywhere!</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -42,10 +42,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -61,10 +61,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -61,10 +61,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -114,10 +114,10 @@ YEAH !</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -57,10 +57,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -84,10 +84,10 @@ as well as <strong>inline markup</strong>.</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -66,10 +66,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -40,10 +40,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -58,10 +58,10 @@ YEAH !</p>
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -47,10 +47,10 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a href="https://www.python.org/">Python</a>.
Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
<p>The theme is by <a rel="nofollow" href="https://www.smashingmagazine.com/2009/08/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>
</footer><!-- /#contentinfo -->
</body>

View file

@ -322,39 +322,6 @@ div.figure p.caption, figure p.caption { /* margin provided by figure */
max-width: 175px;
}
#extras div[class='social'] a {
background-repeat: no-repeat;
background-position: 3px 6px;
padding-left: 25px;
}
/* Icons */
.social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');}
.social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');}
.social a[href*='delicious.com'] {background-image: url('../images/icons/delicious.png');}
.social a[href*='facebook.com'] {background-image: url('../images/icons/facebook.png');}
.social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');}
.social a[href*='github.com'],
.social a[href*='git.io'] {
background-image: url('../images/icons/github.png');
background-size: 16px 16px;
}
.social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
.social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');}
.social a[href*='groups.google.com'] {background-image: url('../images/icons/google-groups.png');}
.social a[href*='news.ycombinator.com'],
.social a[href*='hackernewsers.com'] {background-image: url('../images/icons/hackernews.png');}
.social a[href*='last.fm'], .social a[href*='lastfm.'] {background-image: url('../images/icons/lastfm.png');}
.social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');}
.social a[href*='reddit.com'] {background-image: url('../images/icons/reddit.png');}
.social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');}
.social a[href*='slideshare.net'] {background-image: url('../images/icons/slideshare.png');}
.social a[href*='speakerdeck.com'] {background-image: url('../images/icons/speakerdeck.png');}
.social a[href*='stackoverflow.com'] {background-image: url('../images/icons/stackoverflow.png');}
.social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');}
.social a[href*='vimeo.com'] {background-image: url('../images/icons/vimeo.png');}
.social a[href*='youtube.com'] {background-image: url('../images/icons/youtube.png');}
/*
About
*****************/

View file

@ -0,0 +1,93 @@
Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 B

Some files were not shown because too many files have changed in this diff Show more