mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Merge branch 'getpelican:master' into master
This commit is contained in:
commit
34ca2e1de2
14 changed files with 154 additions and 68 deletions
|
|
@ -1,6 +1,13 @@
|
||||||
Release history
|
Release history
|
||||||
###############
|
###############
|
||||||
|
|
||||||
|
4.7.2 - 2022-02-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix incorrect parsing of parameters specified via `-e` / `--extra-settings` option flags `(#2938) <https://github.com/getpelican/pelican/pull/2938>`_
|
||||||
|
* Add ``categories.html`` template to default theme `(#2973) <https://github.com/getpelican/pelican/pull/2973>`_
|
||||||
|
* Document how to use plugins to inject content `(#2922) <https://github.com/getpelican/pelican/pull/2922>`_
|
||||||
|
|
||||||
4.7.1 - 2021-10-12
|
4.7.1 - 2021-10-12
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@ line::
|
||||||
If you used the ``pelican-quickstart`` command, your primary settings file will
|
If you used the ``pelican-quickstart`` command, your primary settings file will
|
||||||
be named ``pelicanconf.py`` by default.
|
be named ``pelicanconf.py`` by default.
|
||||||
|
|
||||||
You can also specify extra settings via ``-e`` / ``--extra-settings`` option
|
You can also specify settings via ``-e`` / ``--extra-settings`` option
|
||||||
flags, which will override default settings as well as any defined within
|
flags. It will override default settings as well as any defined within the
|
||||||
settings files::
|
setting file. Note that values must follow JSON notation::
|
||||||
|
|
||||||
|
pelican content -e SITENAME='"A site"' READERS='{"html": null}' CACHE_CONTENT=true
|
||||||
|
|
||||||
pelican content -e DELETE_OUTPUT_DIRECTORY=true
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
|
@ -24,7 +25,7 @@ from pelican.plugins import signals
|
||||||
from pelican.plugins._utils import get_plugin_name, load_plugins
|
from pelican.plugins._utils import get_plugin_name, load_plugins
|
||||||
from pelican.readers import Readers
|
from pelican.readers import Readers
|
||||||
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
|
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
|
||||||
from pelican.settings import coerce_overrides, read_settings
|
from pelican.settings import read_settings
|
||||||
from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize)
|
from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize)
|
||||||
from pelican.writers import Writer
|
from pelican.writers import Writer
|
||||||
|
|
||||||
|
|
@ -259,16 +260,29 @@ class PrintSettings(argparse.Action):
|
||||||
parser.exit()
|
parser.exit()
|
||||||
|
|
||||||
|
|
||||||
class ParseDict(argparse.Action):
|
class ParseOverrides(argparse.Action):
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
d = {}
|
overrides = {}
|
||||||
if values:
|
for item in values:
|
||||||
for item in values:
|
try:
|
||||||
split_items = item.split("=", 1)
|
k, v = item.split("=", 1)
|
||||||
key = split_items[0].strip()
|
except ValueError:
|
||||||
value = split_items[1].strip()
|
raise ValueError(
|
||||||
d[key] = value
|
'Extra settings must be specified as KEY=VALUE pairs '
|
||||||
setattr(namespace, self.dest, d)
|
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).'
|
||||||
|
)
|
||||||
|
setattr(namespace, self.dest, overrides)
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments(argv=None):
|
def parse_arguments(argv=None):
|
||||||
|
|
@ -366,13 +380,13 @@ def parse_arguments(argv=None):
|
||||||
|
|
||||||
parser.add_argument('-e', '--extra-settings', dest='overrides',
|
parser.add_argument('-e', '--extra-settings', dest='overrides',
|
||||||
help='Specify one or more SETTING=VALUE pairs to '
|
help='Specify one or more SETTING=VALUE pairs to '
|
||||||
'override settings. If VALUE contains spaces, '
|
'override settings. VALUE must be in JSON notation: '
|
||||||
'add quotes: SETTING="VALUE". Values other than '
|
'specify string values as SETTING=\'"some string"\'; '
|
||||||
'integers and strings can be specified via JSON '
|
'booleans as SETTING=true or SETTING=false; '
|
||||||
'notation. (e.g., SETTING=none)',
|
'None as SETTING=null.',
|
||||||
nargs='*',
|
nargs='*',
|
||||||
action=ParseDict
|
action=ParseOverrides,
|
||||||
)
|
default={})
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
@ -385,6 +399,8 @@ def parse_arguments(argv=None):
|
||||||
|
|
||||||
|
|
||||||
def get_config(args):
|
def get_config(args):
|
||||||
|
"""Builds a config dictionary based on supplied `args`.
|
||||||
|
"""
|
||||||
config = {}
|
config = {}
|
||||||
if args.path:
|
if args.path:
|
||||||
config['PATH'] = os.path.abspath(os.path.expanduser(args.path))
|
config['PATH'] = os.path.abspath(os.path.expanduser(args.path))
|
||||||
|
|
@ -409,7 +425,7 @@ def get_config(args):
|
||||||
if args.bind is not None:
|
if args.bind is not None:
|
||||||
config['BIND'] = args.bind
|
config['BIND'] = args.bind
|
||||||
config['DEBUG'] = args.verbosity == logging.DEBUG
|
config['DEBUG'] = args.verbosity == logging.DEBUG
|
||||||
config.update(coerce_overrides(args.overrides))
|
config.update(args.overrides)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class FileStampDataCacher(FileDataCacher):
|
||||||
"""Subclass that also caches the stamp of the file"""
|
"""Subclass that also caches the stamp of the file"""
|
||||||
|
|
||||||
def __init__(self, settings, cache_name, caching_policy, load_policy):
|
def __init__(self, settings, cache_name, caching_policy, load_policy):
|
||||||
"""This sublcass additionally sets filestamp function
|
"""This subclass additionally sets filestamp function
|
||||||
and base path for filestamping operations
|
and base path for filestamping operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import copy
|
import copy
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
|
||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -659,25 +658,3 @@ def configure_settings(settings):
|
||||||
continue # setting not specified, nothing to do
|
continue # setting not specified, nothing to do
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
def coerce_overrides(overrides):
|
|
||||||
if overrides is None:
|
|
||||||
return {}
|
|
||||||
coerced = {}
|
|
||||||
types_to_cast = {int, str, bool}
|
|
||||||
for k, v in overrides.items():
|
|
||||||
if k not in DEFAULT_CONFIG:
|
|
||||||
logger.warning('Override for unknown setting %s, ignoring', k)
|
|
||||||
continue
|
|
||||||
setting_type = type(DEFAULT_CONFIG[k])
|
|
||||||
if setting_type not in types_to_cast:
|
|
||||||
coerced[k] = json.loads(v)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
coerced[k] = setting_type(v)
|
|
||||||
except ValueError:
|
|
||||||
logger.debug('ValueError for %s override with %s, try to '
|
|
||||||
'load as json', k, v)
|
|
||||||
coerced[k] = json.loads(v)
|
|
||||||
return coerced
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,17 @@
|
||||||
<li><a href="/category/yeah.html">yeah</a></li>
|
<li><a href="/category/yeah.html">yeah</a></li>
|
||||||
</ul></nav>
|
</ul></nav>
|
||||||
</header><!-- /#banner -->
|
</header><!-- /#banner -->
|
||||||
<h1>Categories on A Pelican Blog</h1>
|
|
||||||
|
<section id="content" class="body">
|
||||||
|
<h1>Categories for A Pelican Blog</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/category/bar.html">bar</a> (1)</li>
|
<li><a href="/category/bar.html">bar</a> (1)</li>
|
||||||
<li><a href="/category/cat1.html">cat1</a> (4)</li>
|
<li><a href="/category/cat1.html">cat1</a> (4)</li>
|
||||||
<li><a href="/category/misc.html">misc</a> (4)</li>
|
<li><a href="/category/misc.html">misc</a> (4)</li>
|
||||||
<li><a href="/category/yeah.html">yeah</a> (1)</li>
|
<li><a href="/category/yeah.html">yeah</a> (1)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="extras" class="body">
|
<section id="extras" class="body">
|
||||||
<div class="social">
|
<div class="social">
|
||||||
<h2>social</h2>
|
<h2>social</h2>
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,17 @@
|
||||||
<li><a href="./category/bar.html">bar</a></li>
|
<li><a href="./category/bar.html">bar</a></li>
|
||||||
</ul></nav>
|
</ul></nav>
|
||||||
</header><!-- /#banner -->
|
</header><!-- /#banner -->
|
||||||
<h1>Categories on Alexis' log</h1>
|
|
||||||
|
<section id="content" class="body">
|
||||||
|
<h1>Categories for Alexis' log</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./category/bar.html">bar</a> (1)</li>
|
<li><a href="./category/bar.html">bar</a> (1)</li>
|
||||||
<li><a href="./category/cat1.html">cat1</a> (4)</li>
|
<li><a href="./category/cat1.html">cat1</a> (4)</li>
|
||||||
<li><a href="./category/misc.html">misc</a> (4)</li>
|
<li><a href="./category/misc.html">misc</a> (4)</li>
|
||||||
<li><a href="./category/yeah.html">yeah</a> (1)</li>
|
<li><a href="./category/yeah.html">yeah</a> (1)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="extras" class="body">
|
<section id="extras" class="body">
|
||||||
<div class="blogroll">
|
<div class="blogroll">
|
||||||
<h2>links</h2>
|
<h2>links</h2>
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,17 @@
|
||||||
<li><a href="./category/bar.html">bar</a></li>
|
<li><a href="./category/bar.html">bar</a></li>
|
||||||
</ul></nav>
|
</ul></nav>
|
||||||
</header><!-- /#banner -->
|
</header><!-- /#banner -->
|
||||||
<h1>Categories on Alexis' log</h1>
|
|
||||||
|
<section id="content" class="body">
|
||||||
|
<h1>Categories for Alexis' log</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./category/bar.html">bar</a> (1)</li>
|
<li><a href="./category/bar.html">bar</a> (1)</li>
|
||||||
<li><a href="./category/cat1.html">cat1</a> (4)</li>
|
<li><a href="./category/cat1.html">cat1</a> (4)</li>
|
||||||
<li><a href="./category/misc.html">misc</a> (4)</li>
|
<li><a href="./category/misc.html">misc</a> (4)</li>
|
||||||
<li><a href="./category/yeah.html">yeah</a> (1)</li>
|
<li><a href="./category/yeah.html">yeah</a> (1)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="extras" class="body">
|
<section id="extras" class="body">
|
||||||
<div class="blogroll">
|
<div class="blogroll">
|
||||||
<h2>links</h2>
|
<h2>links</h2>
|
||||||
|
|
|
||||||
72
pelican/tests/test_cli.py
Normal file
72
pelican/tests/test_cli.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pelican import get_config, parse_arguments
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseOverrides(unittest.TestCase):
|
||||||
|
def test_flags(self):
|
||||||
|
for flag in ['-e', '--extra-settings']:
|
||||||
|
args = parse_arguments([flag, 'k=1'])
|
||||||
|
self.assertDictEqual(args.overrides, {'k': 1})
|
||||||
|
|
||||||
|
def test_parse_multiple_items(self):
|
||||||
|
args = parse_arguments('-e k1=1 k2=2'.split())
|
||||||
|
self.assertDictEqual(args.overrides, {'k1': 1, 'k2': 2})
|
||||||
|
|
||||||
|
def test_parse_valid_json(self):
|
||||||
|
json_values_python_values_map = {
|
||||||
|
'""': '',
|
||||||
|
'null': None,
|
||||||
|
'"string"': 'string',
|
||||||
|
'["foo", 12, "4", {}]': ['foo', 12, '4', {}]
|
||||||
|
}
|
||||||
|
for k, v in json_values_python_values_map.items():
|
||||||
|
args = parse_arguments(['-e', 'k=' + k])
|
||||||
|
self.assertDictEqual(args.overrides, {'k': v})
|
||||||
|
|
||||||
|
def test_parse_invalid_syntax(self):
|
||||||
|
invalid_items = ['k= 1', 'k =1', 'k', 'k v']
|
||||||
|
for item in invalid_items:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_arguments(f'-e {item}'.split())
|
||||||
|
|
||||||
|
def test_parse_invalid_json(self):
|
||||||
|
invalid_json = {
|
||||||
|
'', 'False', 'True', 'None', 'some other string',
|
||||||
|
'{"foo": bar}', '[foo]'
|
||||||
|
}
|
||||||
|
for v in invalid_json:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_arguments(['-e ', 'k=' + v])
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetConfigFromArgs(unittest.TestCase):
|
||||||
|
def test_overrides_known_keys(self):
|
||||||
|
args = parse_arguments([
|
||||||
|
'-e',
|
||||||
|
'DELETE_OUTPUT_DIRECTORY=false',
|
||||||
|
'OUTPUT_RETENTION=["1.txt"]',
|
||||||
|
'SITENAME="Title"'
|
||||||
|
])
|
||||||
|
config = get_config(args)
|
||||||
|
config_must_contain = {
|
||||||
|
'DELETE_OUTPUT_DIRECTORY': False,
|
||||||
|
'OUTPUT_RETENTION': ['1.txt'],
|
||||||
|
'SITENAME': 'Title'
|
||||||
|
}
|
||||||
|
self.assertDictEqual(config, {**config, **config_must_contain})
|
||||||
|
|
||||||
|
def test_overrides_non_default_type(self):
|
||||||
|
args = parse_arguments([
|
||||||
|
'-e',
|
||||||
|
'DISPLAY_PAGES_ON_MENU=123',
|
||||||
|
'PAGE_TRANSLATION_ID=null',
|
||||||
|
'TRANSLATION_FEED_RSS_URL="someurl"'
|
||||||
|
])
|
||||||
|
config = get_config(args)
|
||||||
|
config_must_contain = {
|
||||||
|
'DISPLAY_PAGES_ON_MENU': 123,
|
||||||
|
'PAGE_TRANSLATION_ID': None,
|
||||||
|
'TRANSLATION_FEED_RSS_URL': 'someurl'
|
||||||
|
}
|
||||||
|
self.assertDictEqual(config, {**config, **config_must_contain})
|
||||||
|
|
@ -7,7 +7,7 @@ from sys import platform
|
||||||
|
|
||||||
from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME,
|
from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME,
|
||||||
_printf_s_to_format_field,
|
_printf_s_to_format_field,
|
||||||
coerce_overrides, configure_settings,
|
configure_settings,
|
||||||
handle_deprecated_settings, read_settings)
|
handle_deprecated_settings, read_settings)
|
||||||
from pelican.tests.support import unittest
|
from pelican.tests.support import unittest
|
||||||
|
|
||||||
|
|
@ -304,18 +304,3 @@ class TestSettingsConfiguration(unittest.TestCase):
|
||||||
[(r'C\+\+', 'cpp')] +
|
[(r'C\+\+', 'cpp')] +
|
||||||
self.settings['SLUG_REGEX_SUBSTITUTIONS'])
|
self.settings['SLUG_REGEX_SUBSTITUTIONS'])
|
||||||
self.assertNotIn('SLUG_SUBSTITUTIONS', settings)
|
self.assertNotIn('SLUG_SUBSTITUTIONS', settings)
|
||||||
|
|
||||||
def test_coerce_overrides(self):
|
|
||||||
overrides = coerce_overrides({
|
|
||||||
'ARTICLE_EXCLUDES': '["testexcl"]',
|
|
||||||
'READERS': '{"foo": "bar"}',
|
|
||||||
'STATIC_EXCLUDE_SOURCES': 'true',
|
|
||||||
'THEME_STATIC_DIR': 'theme',
|
|
||||||
})
|
|
||||||
expected = {
|
|
||||||
'ARTICLE_EXCLUDES': ["testexcl"],
|
|
||||||
'READERS': {"foo": "bar"},
|
|
||||||
'STATIC_EXCLUDE_SOURCES': True,
|
|
||||||
'THEME_STATIC_DIR': 'theme',
|
|
||||||
}
|
|
||||||
self.assertDictEqual(overrides, expected)
|
|
||||||
|
|
|
||||||
16
pelican/themes/notmyidea/templates/categories.html
Normal file
16
pelican/themes/notmyidea/templates/categories.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ SITENAME }} - Categories{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section id="content" class="body">
|
||||||
|
<h1>Categories for {{ SITENAME }}</h1>
|
||||||
|
<ul>
|
||||||
|
{% for category, articles in categories|sort %}
|
||||||
|
<li><a href="{{ SITEURL }}/{{ category.url }}">{{ category }}</a> ({{ articles|count }})</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pelican"
|
name = "pelican"
|
||||||
version = "4.7.1"
|
version = "4.7.2"
|
||||||
description = "Static site generator supporting Markdown and reStructuredText"
|
description = "Static site generator supporting Markdown and reStructuredText"
|
||||||
authors = ["Justin Mayer <entrop@gmail.com>"]
|
authors = ["Justin Mayer <entrop@gmail.com>"]
|
||||||
license = "AGPLv3"
|
license = "AGPLv3"
|
||||||
|
|
@ -48,7 +48,7 @@ jinja2 = "~2.11"
|
||||||
lxml = "^4.3"
|
lxml = "^4.3"
|
||||||
markdown = "~3.3.4"
|
markdown = "~3.3.4"
|
||||||
typogrify = "^2.0"
|
typogrify = "^2.0"
|
||||||
sphinx = "^3.0"
|
sphinx = "<4.4.0"
|
||||||
sphinx_rtd_theme = "^0.5"
|
sphinx_rtd_theme = "^0.5"
|
||||||
livereload = "^2.6"
|
livereload = "^2.6"
|
||||||
psutil = {version = "^5.7", optional = true}
|
psutil = {version = "^5.7", optional = true}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
sphinx
|
sphinx<4.4.0
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
livereload
|
livereload
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -6,7 +6,7 @@ from os.path import join, relpath
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
version = "4.7.1"
|
version = "4.7.2"
|
||||||
|
|
||||||
requires = ['feedgenerator >= 1.9', 'jinja2 >= 2.7', 'pygments',
|
requires = ['feedgenerator >= 1.9', 'jinja2 >= 2.7', 'pygments',
|
||||||
'docutils>=0.15', 'pytz >= 0a', 'blinker', 'unidecode',
|
'docutils>=0.15', 'pytz >= 0a', 'blinker', 'unidecode',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue