forked from github/pelican
Apply code style to project via: ruff format .
This commit is contained in:
parent
8ea27b82f6
commit
cabdb26cee
41 changed files with 6505 additions and 5163 deletions
|
|
@ -9,19 +9,25 @@ 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
|
||||
|
|
@ -35,12 +41,11 @@ try:
|
|||
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("\n{} is not a recognized setting.".format(setting))
|
||||
break
|
||||
else:
|
||||
# No argument was given to --print-settings, so print all settings
|
||||
|
|
@ -268,170 +290,258 @@ 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(
|
||||
"-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
|
||||
config["CACHE_PATH"] = args.cache_path
|
||||
if args.selected_paths:
|
||||
config['WRITE_SELECTED'] = args.selected_paths.split(',')
|
||||
config["WRITE_SELECTED"] = args.selected_paths.split(",")
|
||||
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 +549,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,8 +559,10 @@ 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)
|
||||
settings_file = os.path.abspath(args.settings)
|
||||
while True:
|
||||
|
|
@ -463,8 +575,9 @@ def autoreload(args, excqueue=None):
|
|||
if settings_file in changed_files:
|
||||
pelican, settings = get_instance(args)
|
||||
|
||||
console.print('\n-> Modified: {}. re-generating...'.format(
|
||||
', '.join(changed_files)))
|
||||
console.print(
|
||||
"\n-> Modified: {}. re-generating...".format(", ".join(changed_files))
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if excqueue is not None:
|
||||
|
|
@ -473,15 +586,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):
|
||||
|
|
@ -491,8 +603,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:
|
||||
|
|
@ -500,8 +611,9 @@ 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(
|
||||
"Serving site at: http://{}:{} - Tap CTRL-C to stop".format(server, port)
|
||||
)
|
||||
httpd.serve_forever()
|
||||
except Exception as e:
|
||||
if excqueue is not None:
|
||||
|
|
@ -518,24 +630,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()
|
||||
|
|
@ -548,16 +667,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:
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
fq_key = ("{}_{}".format(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
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 "<Page {} of {}>".format(self.number, 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"))
|
||||
|
|
|
|||
|
|
@ -24,26 +24,26 @@ 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 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 +52,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("Cannot import plugin `{}`".format(plugin))
|
||||
else:
|
||||
# Avoid loading the same plugin twice
|
||||
if spec.name in sys.modules:
|
||||
|
|
@ -78,30 +78,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 "pelican.plugins.{}".format(plugin) in namespace_plugins:
|
||||
plugin = namespace_plugins["pelican.plugins.{}".format(plugin)]
|
||||
# try to import it
|
||||
else:
|
||||
try:
|
||||
plugin = load_legacy_plugin(
|
||||
plugin,
|
||||
settings.get('PLUGIN_PATHS', []))
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -2,48 +2,48 @@ from blinker import signal
|
|||
|
||||
# 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")
|
||||
|
|
|
|||
|
|
@ -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,59 +372,59 @@ 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 += "</{}>".format(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 += "<!--{}-->".format(data)
|
||||
|
||||
def handle_data(self, data):
|
||||
self._data_buffer += data
|
||||
|
||||
def handle_entityref(self, data):
|
||||
self._data_buffer += '&{};'.format(data)
|
||||
self._data_buffer += "&{};".format(data)
|
||||
|
||||
def handle_charref(self, data):
|
||||
self._data_buffer += '&#{};'.format(data)
|
||||
self._data_buffer += "&#{};".format(data)
|
||||
|
||||
def build_tag(self, tag, attrs, close_tag):
|
||||
result = '<{}'.format(escape(tag))
|
||||
result = "<{}".format(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.
|
||||
|
|
@ -435,33 +433,39 @@ class HTMLReader(BaseReader):
|
|||
else:
|
||||
result += '="{}"'.format(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_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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
NAME = 'namespace plugin'
|
||||
NAME = "namespace plugin"
|
||||
|
||||
|
||||
def register():
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ from pelican.contents import Article
|
|||
from pelican.readers import default_metadata
|
||||
from pelican.settings import DEFAULT_CONFIG
|
||||
|
||||
__all__ = ['get_article', 'unittest', ]
|
||||
__all__ = [
|
||||
"get_article",
|
||||
"unittest",
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -51,7 +54,7 @@ def isplit(s, sep=None):
|
|||
True
|
||||
|
||||
"""
|
||||
sep, hardsep = r'\s+' if sep is None else re.escape(sep), sep is not None
|
||||
sep, hardsep = r"\s+" if sep is None else re.escape(sep), sep is not None
|
||||
exp, pos, length = re.compile(sep), 0, len(s)
|
||||
while True:
|
||||
m = exp.search(s, pos)
|
||||
|
|
@ -89,10 +92,8 @@ def mute(returns_output=False):
|
|||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
saved_stdout = sys.stdout
|
||||
sys.stdout = StringIO()
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ def mute(returns_output=False):
|
|||
|
||||
def get_article(title, content, **extra_metadata):
|
||||
metadata = default_metadata(settings=DEFAULT_CONFIG)
|
||||
metadata['title'] = title
|
||||
metadata["title"] = title
|
||||
if extra_metadata:
|
||||
metadata.update(extra_metadata)
|
||||
return Article(content, metadata=metadata)
|
||||
|
|
@ -125,14 +126,14 @@ def skipIfNoExecutable(executable):
|
|||
and skips the tests if not found (if subprocess raises a `OSError`).
|
||||
"""
|
||||
|
||||
with open(os.devnull, 'w') as fnull:
|
||||
with open(os.devnull, "w") as fnull:
|
||||
try:
|
||||
res = subprocess.call(executable, stdout=fnull, stderr=fnull)
|
||||
except OSError:
|
||||
res = None
|
||||
|
||||
if res is None:
|
||||
return unittest.skip('{} executable not found'.format(executable))
|
||||
return unittest.skip("{} executable not found".format(executable))
|
||||
|
||||
return lambda func: func
|
||||
|
||||
|
|
@ -164,10 +165,7 @@ def can_symlink():
|
|||
res = True
|
||||
try:
|
||||
with temporary_folder() as f:
|
||||
os.symlink(
|
||||
f,
|
||||
os.path.join(f, 'symlink')
|
||||
)
|
||||
os.symlink(f, os.path.join(f, "symlink"))
|
||||
except OSError:
|
||||
res = False
|
||||
return res
|
||||
|
|
@ -186,9 +184,9 @@ def get_settings(**kwargs):
|
|||
|
||||
def get_context(settings=None, **kwargs):
|
||||
context = settings.copy() if settings else {}
|
||||
context['generated_content'] = {}
|
||||
context['static_links'] = set()
|
||||
context['static_content'] = {}
|
||||
context["generated_content"] = {}
|
||||
context["static_links"] = set()
|
||||
context["static_content"] = {}
|
||||
context.update(kwargs)
|
||||
return context
|
||||
|
||||
|
|
@ -200,22 +198,24 @@ class LogCountHandler(BufferingHandler):
|
|||
super().__init__(capacity)
|
||||
|
||||
def count_logs(self, msg=None, level=None):
|
||||
return len([
|
||||
rec
|
||||
for rec
|
||||
in self.buffer
|
||||
if (msg is None or re.match(msg, rec.getMessage())) and
|
||||
(level is None or rec.levelno == level)
|
||||
])
|
||||
return len(
|
||||
[
|
||||
rec
|
||||
for rec in self.buffer
|
||||
if (msg is None or re.match(msg, rec.getMessage()))
|
||||
and (level is None or rec.levelno == level)
|
||||
]
|
||||
)
|
||||
|
||||
def count_formatted_logs(self, msg=None, level=None):
|
||||
return len([
|
||||
rec
|
||||
for rec
|
||||
in self.buffer
|
||||
if (msg is None or re.search(msg, self.format(rec))) and
|
||||
(level is None or rec.levelno == level)
|
||||
])
|
||||
return len(
|
||||
[
|
||||
rec
|
||||
for rec in self.buffer
|
||||
if (msg is None or re.search(msg, self.format(rec)))
|
||||
and (level is None or rec.levelno == level)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def diff_subproc(first, second):
|
||||
|
|
@ -228,8 +228,16 @@ def diff_subproc(first, second):
|
|||
>>> didCheckFail = proc.returnCode != 0
|
||||
"""
|
||||
return subprocess.Popen(
|
||||
['git', '--no-pager', 'diff', '--no-ext-diff', '--exit-code',
|
||||
'-w', first, second],
|
||||
[
|
||||
"git",
|
||||
"--no-pager",
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
"--exit-code",
|
||||
"-w",
|
||||
first,
|
||||
second,
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
|
|
@ -251,9 +259,12 @@ class LoggedTestCase(unittest.TestCase):
|
|||
def assertLogCountEqual(self, count=None, msg=None, **kwargs):
|
||||
actual = self._logcount_handler.count_logs(msg=msg, **kwargs)
|
||||
self.assertEqual(
|
||||
actual, count,
|
||||
msg='expected {} occurrences of {!r}, but found {}'.format(
|
||||
count, msg, actual))
|
||||
actual,
|
||||
count,
|
||||
msg="expected {} occurrences of {!r}, but found {}".format(
|
||||
count, msg, actual
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestCaseWithCLocale(unittest.TestCase):
|
||||
|
|
@ -261,9 +272,10 @@ class TestCaseWithCLocale(unittest.TestCase):
|
|||
|
||||
Use utils.temporary_locale if you want a context manager ("with" statement).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
|
||||
def tearDown(self):
|
||||
locale.setlocale(locale.LC_ALL, self.old_locale)
|
||||
|
|
|
|||
|
|
@ -8,31 +8,30 @@ from pelican.tests.support import get_context, get_settings, unittest
|
|||
|
||||
|
||||
CUR_DIR = os.path.dirname(__file__)
|
||||
CONTENT_DIR = os.path.join(CUR_DIR, 'content')
|
||||
CONTENT_DIR = os.path.join(CUR_DIR, "content")
|
||||
|
||||
|
||||
class TestCache(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_cache = mkdtemp(prefix='pelican_cache.')
|
||||
self.temp_cache = mkdtemp(prefix="pelican_cache.")
|
||||
|
||||
def tearDown(self):
|
||||
rmtree(self.temp_cache)
|
||||
|
||||
def _get_cache_enabled_settings(self):
|
||||
settings = get_settings()
|
||||
settings['CACHE_CONTENT'] = True
|
||||
settings['LOAD_CONTENT_CACHE'] = True
|
||||
settings['CACHE_PATH'] = self.temp_cache
|
||||
settings["CACHE_CONTENT"] = True
|
||||
settings["LOAD_CONTENT_CACHE"] = True
|
||||
settings["CACHE_PATH"] = self.temp_cache
|
||||
return settings
|
||||
|
||||
def test_generator_caching(self):
|
||||
"""Test that cached and uncached content is same in generator level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['CONTENT_CACHING_LAYER'] = 'generator'
|
||||
settings['PAGE_PATHS'] = ['TestPages']
|
||||
settings['DEFAULT_DATE'] = (1970, 1, 1)
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["CONTENT_CACHING_LAYER"] = "generator"
|
||||
settings["PAGE_PATHS"] = ["TestPages"]
|
||||
settings["DEFAULT_DATE"] = (1970, 1, 1)
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
def sorted_titles(items):
|
||||
|
|
@ -40,15 +39,23 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
# Articles
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
uncached_articles = sorted_titles(generator.articles)
|
||||
uncached_drafts = sorted_titles(generator.drafts)
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
cached_articles = sorted_titles(generator.articles)
|
||||
cached_drafts = sorted_titles(generator.drafts)
|
||||
|
|
@ -58,16 +65,24 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
# Pages
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
uncached_pages = sorted_titles(generator.pages)
|
||||
uncached_hidden_pages = sorted_titles(generator.hidden_pages)
|
||||
uncached_draft_pages = sorted_titles(generator.draft_pages)
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
cached_pages = sorted_titles(generator.pages)
|
||||
cached_hidden_pages = sorted_titles(generator.hidden_pages)
|
||||
|
|
@ -80,10 +95,10 @@ class TestCache(unittest.TestCase):
|
|||
def test_reader_caching(self):
|
||||
"""Test that cached and uncached content is same in reader level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['CONTENT_CACHING_LAYER'] = 'reader'
|
||||
settings['PAGE_PATHS'] = ['TestPages']
|
||||
settings['DEFAULT_DATE'] = (1970, 1, 1)
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["CONTENT_CACHING_LAYER"] = "reader"
|
||||
settings["PAGE_PATHS"] = ["TestPages"]
|
||||
settings["DEFAULT_DATE"] = (1970, 1, 1)
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
def sorted_titles(items):
|
||||
|
|
@ -91,15 +106,23 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
# Articles
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
uncached_articles = sorted_titles(generator.articles)
|
||||
uncached_drafts = sorted_titles(generator.drafts)
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
cached_articles = sorted_titles(generator.articles)
|
||||
cached_drafts = sorted_titles(generator.drafts)
|
||||
|
|
@ -109,15 +132,23 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
# Pages
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
uncached_pages = sorted_titles(generator.pages)
|
||||
uncached_hidden_pages = sorted_titles(generator.hidden_pages)
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
cached_pages = sorted_titles(generator.pages)
|
||||
cached_hidden_pages = sorted_titles(generator.hidden_pages)
|
||||
|
|
@ -128,20 +159,28 @@ class TestCache(unittest.TestCase):
|
|||
def test_article_object_caching(self):
|
||||
"""Test Article objects caching at the generator level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['CONTENT_CACHING_LAYER'] = 'generator'
|
||||
settings['DEFAULT_DATE'] = (1970, 1, 1)
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["CONTENT_CACHING_LAYER"] = "generator"
|
||||
settings["DEFAULT_DATE"] = (1970, 1, 1)
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator, '_cache'))
|
||||
self.assertTrue(hasattr(generator, "_cache"))
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
"""
|
||||
|
|
@ -158,18 +197,26 @@ class TestCache(unittest.TestCase):
|
|||
def test_article_reader_content_caching(self):
|
||||
"""Test raw article content caching at the reader level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator.readers, '_cache'))
|
||||
self.assertTrue(hasattr(generator.readers, "_cache"))
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
readers = generator.readers.readers
|
||||
for reader in readers.values():
|
||||
reader.read = MagicMock()
|
||||
|
|
@ -182,44 +229,58 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
used in --ignore-cache or autoreload mode"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator, '_cache_open'))
|
||||
self.assertTrue(hasattr(generator, "_cache_open"))
|
||||
orig_call_count = generator.readers.read_file.call_count
|
||||
|
||||
settings['LOAD_CONTENT_CACHE'] = False
|
||||
settings["LOAD_CONTENT_CACHE"] = False
|
||||
generator = ArticlesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CONTENT_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
self.assertEqual(
|
||||
generator.readers.read_file.call_count,
|
||||
orig_call_count)
|
||||
self.assertEqual(generator.readers.read_file.call_count, orig_call_count)
|
||||
|
||||
def test_page_object_caching(self):
|
||||
"""Test Page objects caching at the generator level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['CONTENT_CACHING_LAYER'] = 'generator'
|
||||
settings['PAGE_PATHS'] = ['TestPages']
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["CONTENT_CACHING_LAYER"] = "generator"
|
||||
settings["PAGE_PATHS"] = ["TestPages"]
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator, '_cache'))
|
||||
self.assertTrue(hasattr(generator, "_cache"))
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
"""
|
||||
|
|
@ -231,19 +292,27 @@ class TestCache(unittest.TestCase):
|
|||
def test_page_reader_content_caching(self):
|
||||
"""Test raw page content caching at the reader level"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['PAGE_PATHS'] = ['TestPages']
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["PAGE_PATHS"] = ["TestPages"]
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator.readers, '_cache'))
|
||||
self.assertTrue(hasattr(generator.readers, "_cache"))
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
readers = generator.readers.readers
|
||||
for reader in readers.values():
|
||||
reader.read = MagicMock()
|
||||
|
|
@ -256,24 +325,30 @@ class TestCache(unittest.TestCase):
|
|||
|
||||
used in --ignore_cache or autoreload mode"""
|
||||
settings = self._get_cache_enabled_settings()
|
||||
settings['PAGE_PATHS'] = ['TestPages']
|
||||
settings['READERS'] = {'asc': None}
|
||||
settings["PAGE_PATHS"] = ["TestPages"]
|
||||
settings["READERS"] = {"asc": None}
|
||||
context = get_context(settings)
|
||||
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
self.assertTrue(hasattr(generator, '_cache_open'))
|
||||
self.assertTrue(hasattr(generator, "_cache_open"))
|
||||
orig_call_count = generator.readers.read_file.call_count
|
||||
|
||||
settings['LOAD_CONTENT_CACHE'] = False
|
||||
settings["LOAD_CONTENT_CACHE"] = False
|
||||
generator = PagesGenerator(
|
||||
context=context.copy(), settings=settings,
|
||||
path=CUR_DIR, theme=settings['THEME'], output_path=None)
|
||||
context=context.copy(),
|
||||
settings=settings,
|
||||
path=CUR_DIR,
|
||||
theme=settings["THEME"],
|
||||
output_path=None,
|
||||
)
|
||||
generator.readers.read_file = MagicMock()
|
||||
generator.generate_context()
|
||||
self.assertEqual(
|
||||
generator.readers.read_file.call_count,
|
||||
orig_call_count)
|
||||
self.assertEqual(generator.readers.read_file.call_count, orig_call_count)
|
||||
|
|
|
|||
|
|
@ -5,68 +5,77 @@ 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})
|
||||
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})
|
||||
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', {}]
|
||||
'""': "",
|
||||
"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})
|
||||
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']
|
||||
invalid_items = ["k= 1", "k =1", "k", "k v"]
|
||||
for item in invalid_items:
|
||||
with self.assertRaises(ValueError):
|
||||
parse_arguments(f'-e {item}'.split())
|
||||
parse_arguments(f"-e {item}".split())
|
||||
|
||||
def test_parse_invalid_json(self):
|
||||
invalid_json = {
|
||||
'', 'False', 'True', 'None', 'some other string',
|
||||
'{"foo": bar}', '[foo]'
|
||||
"",
|
||||
"False",
|
||||
"True",
|
||||
"None",
|
||||
"some other string",
|
||||
'{"foo": bar}',
|
||||
"[foo]",
|
||||
}
|
||||
for v in invalid_json:
|
||||
with self.assertRaises(ValueError):
|
||||
parse_arguments(['-e ', 'k=' + v])
|
||||
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"'
|
||||
])
|
||||
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'
|
||||
"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"'
|
||||
])
|
||||
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'
|
||||
"DISPLAY_PAGES_ON_MENU": 123,
|
||||
"PAGE_TRANSLATION_ID": None,
|
||||
"TRANSLATION_FEED_RSS_URL": "someurl",
|
||||
}
|
||||
self.assertDictEqual(config, {**config, **config_must_contain})
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -4,26 +4,35 @@ from posixpath import join as posix_join
|
|||
from unittest.mock import patch
|
||||
|
||||
from pelican.settings import DEFAULT_CONFIG
|
||||
from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder,
|
||||
unittest, TestCaseWithCLocale)
|
||||
from pelican.tools.pelican_import import (blogger2fields, build_header,
|
||||
build_markdown_header,
|
||||
decode_wp_content,
|
||||
download_attachments, fields2pelican,
|
||||
get_attachments, tumblr2fields,
|
||||
wp2fields,
|
||||
)
|
||||
from pelican.tests.support import (
|
||||
mute,
|
||||
skipIfNoExecutable,
|
||||
temporary_folder,
|
||||
unittest,
|
||||
TestCaseWithCLocale,
|
||||
)
|
||||
from pelican.tools.pelican_import import (
|
||||
blogger2fields,
|
||||
build_header,
|
||||
build_markdown_header,
|
||||
decode_wp_content,
|
||||
download_attachments,
|
||||
fields2pelican,
|
||||
get_attachments,
|
||||
tumblr2fields,
|
||||
wp2fields,
|
||||
)
|
||||
from pelican.utils import path_to_file_url, slugify
|
||||
|
||||
CUR_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
BLOGGER_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'bloggerexport.xml')
|
||||
WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml')
|
||||
WORDPRESS_ENCODED_CONTENT_SAMPLE = os.path.join(CUR_DIR,
|
||||
'content',
|
||||
'wordpress_content_encoded')
|
||||
WORDPRESS_DECODED_CONTENT_SAMPLE = os.path.join(CUR_DIR,
|
||||
'content',
|
||||
'wordpress_content_decoded')
|
||||
BLOGGER_XML_SAMPLE = os.path.join(CUR_DIR, "content", "bloggerexport.xml")
|
||||
WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, "content", "wordpressexport.xml")
|
||||
WORDPRESS_ENCODED_CONTENT_SAMPLE = os.path.join(
|
||||
CUR_DIR, "content", "wordpress_content_encoded"
|
||||
)
|
||||
WORDPRESS_DECODED_CONTENT_SAMPLE = os.path.join(
|
||||
CUR_DIR, "content", "wordpress_content_decoded"
|
||||
)
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
|
|
@ -36,10 +45,9 @@ except ImportError:
|
|||
LXML = False
|
||||
|
||||
|
||||
@skipIfNoExecutable(['pandoc', '--version'])
|
||||
@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
|
||||
@skipIfNoExecutable(["pandoc", "--version"])
|
||||
@unittest.skipUnless(BeautifulSoup, "Needs BeautifulSoup module")
|
||||
class TestBloggerXmlImporter(TestCaseWithCLocale):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.posts = blogger2fields(BLOGGER_XML_SAMPLE)
|
||||
|
|
@ -50,16 +58,17 @@ class TestBloggerXmlImporter(TestCaseWithCLocale):
|
|||
"""
|
||||
test_posts = list(self.posts)
|
||||
kinds = {x[8] for x in test_posts}
|
||||
self.assertEqual({'page', 'article', 'comment'}, kinds)
|
||||
page_titles = {x[0] for x in test_posts if x[8] == 'page'}
|
||||
self.assertEqual({'Test page', 'Test page 2'}, page_titles)
|
||||
article_titles = {x[0] for x in test_posts if x[8] == 'article'}
|
||||
self.assertEqual({'Black as Egypt\'s Night', 'The Steel Windpipe'},
|
||||
article_titles)
|
||||
comment_titles = {x[0] for x in test_posts if x[8] == 'comment'}
|
||||
self.assertEqual({'Mishka, always a pleasure to read your '
|
||||
'adventures!...'},
|
||||
comment_titles)
|
||||
self.assertEqual({"page", "article", "comment"}, kinds)
|
||||
page_titles = {x[0] for x in test_posts if x[8] == "page"}
|
||||
self.assertEqual({"Test page", "Test page 2"}, page_titles)
|
||||
article_titles = {x[0] for x in test_posts if x[8] == "article"}
|
||||
self.assertEqual(
|
||||
{"Black as Egypt's Night", "The Steel Windpipe"}, article_titles
|
||||
)
|
||||
comment_titles = {x[0] for x in test_posts if x[8] == "comment"}
|
||||
self.assertEqual(
|
||||
{"Mishka, always a pleasure to read your " "adventures!..."}, comment_titles
|
||||
)
|
||||
|
||||
def test_recognise_status_with_correct_filename(self):
|
||||
"""Check that importerer outputs only statuses 'published' and 'draft',
|
||||
|
|
@ -67,24 +76,25 @@ class TestBloggerXmlImporter(TestCaseWithCLocale):
|
|||
"""
|
||||
test_posts = list(self.posts)
|
||||
statuses = {x[7] for x in test_posts}
|
||||
self.assertEqual({'published', 'draft'}, statuses)
|
||||
self.assertEqual({"published", "draft"}, statuses)
|
||||
|
||||
draft_filenames = {x[2] for x in test_posts if x[7] == 'draft'}
|
||||
draft_filenames = {x[2] for x in test_posts if x[7] == "draft"}
|
||||
# draft filenames are id-based
|
||||
self.assertEqual({'page-4386962582497458967',
|
||||
'post-1276418104709695660'}, draft_filenames)
|
||||
self.assertEqual(
|
||||
{"page-4386962582497458967", "post-1276418104709695660"}, draft_filenames
|
||||
)
|
||||
|
||||
published_filenames = {x[2] for x in test_posts if x[7] == 'published'}
|
||||
published_filenames = {x[2] for x in test_posts if x[7] == "published"}
|
||||
# published filenames are url-based, except comments
|
||||
self.assertEqual({'the-steel-windpipe',
|
||||
'test-page',
|
||||
'post-5590533389087749201'}, published_filenames)
|
||||
self.assertEqual(
|
||||
{"the-steel-windpipe", "test-page", "post-5590533389087749201"},
|
||||
published_filenames,
|
||||
)
|
||||
|
||||
|
||||
@skipIfNoExecutable(['pandoc', '--version'])
|
||||
@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
|
||||
@skipIfNoExecutable(["pandoc", "--version"])
|
||||
@unittest.skipUnless(BeautifulSoup, "Needs BeautifulSoup module")
|
||||
class TestWordpressXmlImporter(TestCaseWithCLocale):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.posts = wp2fields(WORDPRESS_XML_SAMPLE)
|
||||
|
|
@ -92,30 +102,49 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
|
||||
def test_ignore_empty_posts(self):
|
||||
self.assertTrue(self.posts)
|
||||
for (title, content, fname, date, author,
|
||||
categ, tags, status, kind, format) in self.posts:
|
||||
for (
|
||||
title,
|
||||
content,
|
||||
fname,
|
||||
date,
|
||||
author,
|
||||
categ,
|
||||
tags,
|
||||
status,
|
||||
kind,
|
||||
format,
|
||||
) in self.posts:
|
||||
self.assertTrue(title.strip())
|
||||
|
||||
def test_recognise_page_kind(self):
|
||||
""" Check that we recognise pages in wordpress, as opposed to posts """
|
||||
"""Check that we recognise pages in wordpress, as opposed to posts"""
|
||||
self.assertTrue(self.posts)
|
||||
# Collect (title, filename, kind) of non-empty posts recognised as page
|
||||
pages_data = []
|
||||
for (title, content, fname, date, author,
|
||||
categ, tags, status, kind, format) in self.posts:
|
||||
if kind == 'page':
|
||||
for (
|
||||
title,
|
||||
content,
|
||||
fname,
|
||||
date,
|
||||
author,
|
||||
categ,
|
||||
tags,
|
||||
status,
|
||||
kind,
|
||||
format,
|
||||
) in self.posts:
|
||||
if kind == "page":
|
||||
pages_data.append((title, fname))
|
||||
self.assertEqual(2, len(pages_data))
|
||||
self.assertEqual(('Page', 'contact'), pages_data[0])
|
||||
self.assertEqual(('Empty Page', 'empty'), pages_data[1])
|
||||
self.assertEqual(("Page", "contact"), pages_data[0])
|
||||
self.assertEqual(("Empty Page", "empty"), pages_data[1])
|
||||
|
||||
def test_dirpage_directive_for_page_kind(self):
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(lambda p: p[0].startswith("Empty Page"), self.posts)
|
||||
with temporary_folder() as temp:
|
||||
fname = list(silent_f2p(test_post, 'markdown',
|
||||
temp, dirpage=True))[0]
|
||||
self.assertTrue(fname.endswith('pages%sempty.md' % os.path.sep))
|
||||
fname = list(silent_f2p(test_post, "markdown", temp, dirpage=True))[0]
|
||||
self.assertTrue(fname.endswith("pages%sempty.md" % os.path.sep))
|
||||
|
||||
def test_dircat(self):
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
|
|
@ -125,14 +154,13 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
if len(post[5]) > 0: # Has a category
|
||||
test_posts.append(post)
|
||||
with temporary_folder() as temp:
|
||||
fnames = list(silent_f2p(test_posts, 'markdown',
|
||||
temp, dircat=True))
|
||||
subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS']
|
||||
fnames = list(silent_f2p(test_posts, "markdown", temp, dircat=True))
|
||||
subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"]
|
||||
index = 0
|
||||
for post in test_posts:
|
||||
name = post[2]
|
||||
category = slugify(post[5][0], regex_subs=subs, preserve_case=True)
|
||||
name += '.md'
|
||||
name += ".md"
|
||||
filename = os.path.join(category, name)
|
||||
out_name = fnames[index]
|
||||
self.assertTrue(out_name.endswith(filename))
|
||||
|
|
@ -141,9 +169,19 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
def test_unless_custom_post_all_items_should_be_pages_or_posts(self):
|
||||
self.assertTrue(self.posts)
|
||||
pages_data = []
|
||||
for (title, content, fname, date, author, categ,
|
||||
tags, status, kind, format) in self.posts:
|
||||
if kind == 'page' or kind == 'article':
|
||||
for (
|
||||
title,
|
||||
content,
|
||||
fname,
|
||||
date,
|
||||
author,
|
||||
categ,
|
||||
tags,
|
||||
status,
|
||||
kind,
|
||||
format,
|
||||
) in self.posts:
|
||||
if kind == "page" or kind == "article":
|
||||
pass
|
||||
else:
|
||||
pages_data.append((title, fname))
|
||||
|
|
@ -152,40 +190,45 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
def test_recognise_custom_post_type(self):
|
||||
self.assertTrue(self.custposts)
|
||||
cust_data = []
|
||||
for (title, content, fname, date, author, categ,
|
||||
tags, status, kind, format) in self.custposts:
|
||||
if kind == 'article' or kind == 'page':
|
||||
for (
|
||||
title,
|
||||
content,
|
||||
fname,
|
||||
date,
|
||||
author,
|
||||
categ,
|
||||
tags,
|
||||
status,
|
||||
kind,
|
||||
format,
|
||||
) in self.custposts:
|
||||
if kind == "article" or kind == "page":
|
||||
pass
|
||||
else:
|
||||
cust_data.append((title, kind))
|
||||
self.assertEqual(3, len(cust_data))
|
||||
self.assertEqual(("A custom post in category 4", "custom1"), cust_data[0])
|
||||
self.assertEqual(("A custom post in category 5", "custom1"), cust_data[1])
|
||||
self.assertEqual(
|
||||
('A custom post in category 4', 'custom1'),
|
||||
cust_data[0])
|
||||
self.assertEqual(
|
||||
('A custom post in category 5', 'custom1'),
|
||||
cust_data[1])
|
||||
self.assertEqual(
|
||||
('A 2nd custom post type also in category 5', 'custom2'),
|
||||
cust_data[2])
|
||||
("A 2nd custom post type also in category 5", "custom2"), cust_data[2]
|
||||
)
|
||||
|
||||
def test_custom_posts_put_in_own_dir(self):
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_posts = []
|
||||
for post in self.custposts:
|
||||
# check post kind
|
||||
if post[8] == 'article' or post[8] == 'page':
|
||||
if post[8] == "article" or post[8] == "page":
|
||||
pass
|
||||
else:
|
||||
test_posts.append(post)
|
||||
with temporary_folder() as temp:
|
||||
fnames = list(silent_f2p(test_posts, 'markdown',
|
||||
temp, wp_custpost=True))
|
||||
fnames = list(silent_f2p(test_posts, "markdown", temp, wp_custpost=True))
|
||||
index = 0
|
||||
for post in test_posts:
|
||||
name = post[2]
|
||||
kind = post[8]
|
||||
name += '.md'
|
||||
name += ".md"
|
||||
filename = os.path.join(kind, name)
|
||||
out_name = fnames[index]
|
||||
self.assertTrue(out_name.endswith(filename))
|
||||
|
|
@ -196,20 +239,21 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
test_posts = []
|
||||
for post in self.custposts:
|
||||
# check post kind
|
||||
if post[8] == 'article' or post[8] == 'page':
|
||||
if post[8] == "article" or post[8] == "page":
|
||||
pass
|
||||
else:
|
||||
test_posts.append(post)
|
||||
with temporary_folder() as temp:
|
||||
fnames = list(silent_f2p(test_posts, 'markdown', temp,
|
||||
wp_custpost=True, dircat=True))
|
||||
subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS']
|
||||
fnames = list(
|
||||
silent_f2p(test_posts, "markdown", temp, wp_custpost=True, dircat=True)
|
||||
)
|
||||
subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"]
|
||||
index = 0
|
||||
for post in test_posts:
|
||||
name = post[2]
|
||||
kind = post[8]
|
||||
category = slugify(post[5][0], regex_subs=subs, preserve_case=True)
|
||||
name += '.md'
|
||||
name += ".md"
|
||||
filename = os.path.join(kind, category, name)
|
||||
out_name = fnames[index]
|
||||
self.assertTrue(out_name.endswith(filename))
|
||||
|
|
@ -221,16 +265,19 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
test_posts = []
|
||||
for post in self.custposts:
|
||||
# check post kind
|
||||
if post[8] == 'page':
|
||||
if post[8] == "page":
|
||||
test_posts.append(post)
|
||||
with temporary_folder() as temp:
|
||||
fnames = list(silent_f2p(test_posts, 'markdown', temp,
|
||||
wp_custpost=True, dirpage=False))
|
||||
fnames = list(
|
||||
silent_f2p(
|
||||
test_posts, "markdown", temp, wp_custpost=True, dirpage=False
|
||||
)
|
||||
)
|
||||
index = 0
|
||||
for post in test_posts:
|
||||
name = post[2]
|
||||
name += '.md'
|
||||
filename = os.path.join('pages', name)
|
||||
name += ".md"
|
||||
filename = os.path.join("pages", name)
|
||||
out_name = fnames[index]
|
||||
self.assertFalse(out_name.endswith(filename))
|
||||
|
||||
|
|
@ -238,117 +285,114 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
test_posts = list(self.posts)
|
||||
|
||||
def r(f):
|
||||
with open(f, encoding='utf-8') as infile:
|
||||
with open(f, encoding="utf-8") as infile:
|
||||
return infile.read()
|
||||
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
|
||||
with temporary_folder() as temp:
|
||||
|
||||
rst_files = (r(f) for f
|
||||
in silent_f2p(test_posts, 'markdown', temp))
|
||||
self.assertTrue(any('<iframe' in rst for rst in rst_files))
|
||||
rst_files = (r(f) for f
|
||||
in silent_f2p(test_posts, 'markdown',
|
||||
temp, strip_raw=True))
|
||||
self.assertFalse(any('<iframe' in rst for rst in rst_files))
|
||||
rst_files = (r(f) for f in silent_f2p(test_posts, "markdown", temp))
|
||||
self.assertTrue(any("<iframe" in rst for rst in rst_files))
|
||||
rst_files = (
|
||||
r(f) for f in silent_f2p(test_posts, "markdown", temp, strip_raw=True)
|
||||
)
|
||||
self.assertFalse(any("<iframe" in rst for rst in rst_files))
|
||||
# no effect in rst
|
||||
rst_files = (r(f) for f in silent_f2p(test_posts, 'rst', temp))
|
||||
self.assertFalse(any('<iframe' in rst for rst in rst_files))
|
||||
rst_files = (r(f) for f in silent_f2p(test_posts, 'rst', temp,
|
||||
strip_raw=True))
|
||||
self.assertFalse(any('<iframe' in rst for rst in rst_files))
|
||||
rst_files = (r(f) for f in silent_f2p(test_posts, "rst", temp))
|
||||
self.assertFalse(any("<iframe" in rst for rst in rst_files))
|
||||
rst_files = (
|
||||
r(f) for f in silent_f2p(test_posts, "rst", temp, strip_raw=True)
|
||||
)
|
||||
self.assertFalse(any("<iframe" in rst for rst in rst_files))
|
||||
|
||||
def test_decode_html_entities_in_titles(self):
|
||||
test_posts = [post for post
|
||||
in self.posts if post[2] == 'html-entity-test']
|
||||
test_posts = [post for post in self.posts if post[2] == "html-entity-test"]
|
||||
self.assertEqual(len(test_posts), 1)
|
||||
|
||||
post = test_posts[0]
|
||||
title = post[0]
|
||||
self.assertTrue(title, "A normal post with some <html> entities in "
|
||||
"the title. You can't miss them.")
|
||||
self.assertNotIn('&', title)
|
||||
self.assertTrue(
|
||||
title,
|
||||
"A normal post with some <html> entities in "
|
||||
"the title. You can't miss them.",
|
||||
)
|
||||
self.assertNotIn("&", title)
|
||||
|
||||
def test_decode_wp_content_returns_empty(self):
|
||||
""" Check that given an empty string we return an empty string."""
|
||||
"""Check that given an empty string we return an empty string."""
|
||||
self.assertEqual(decode_wp_content(""), "")
|
||||
|
||||
def test_decode_wp_content(self):
|
||||
""" Check that we can decode a wordpress content string."""
|
||||
"""Check that we can decode a wordpress content string."""
|
||||
with open(WORDPRESS_ENCODED_CONTENT_SAMPLE) as encoded_file:
|
||||
encoded_content = encoded_file.read()
|
||||
with open(WORDPRESS_DECODED_CONTENT_SAMPLE) as decoded_file:
|
||||
decoded_content = decoded_file.read()
|
||||
self.assertEqual(
|
||||
decode_wp_content(encoded_content, br=False),
|
||||
decoded_content)
|
||||
decode_wp_content(encoded_content, br=False), decoded_content
|
||||
)
|
||||
|
||||
def test_preserve_verbatim_formatting(self):
|
||||
def r(f):
|
||||
with open(f, encoding='utf-8') as infile:
|
||||
with open(f, encoding="utf-8") as infile:
|
||||
return infile.read()
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(
|
||||
lambda p: p[0].startswith("Code in List"),
|
||||
self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, 'markdown', temp)][0]
|
||||
self.assertTrue(re.search(r'\s+a = \[1, 2, 3\]', md))
|
||||
self.assertTrue(re.search(r'\s+b = \[4, 5, 6\]', md))
|
||||
|
||||
for_line = re.search(r'\s+for i in zip\(a, b\):', md).group(0)
|
||||
print_line = re.search(r'\s+print i', md).group(0)
|
||||
self.assertTrue(
|
||||
for_line.rindex('for') < print_line.rindex('print'))
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(lambda p: p[0].startswith("Code in List"), self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, "markdown", temp)][0]
|
||||
self.assertTrue(re.search(r"\s+a = \[1, 2, 3\]", md))
|
||||
self.assertTrue(re.search(r"\s+b = \[4, 5, 6\]", md))
|
||||
|
||||
for_line = re.search(r"\s+for i in zip\(a, b\):", md).group(0)
|
||||
print_line = re.search(r"\s+print i", md).group(0)
|
||||
self.assertTrue(for_line.rindex("for") < print_line.rindex("print"))
|
||||
|
||||
def test_code_in_list(self):
|
||||
def r(f):
|
||||
with open(f, encoding='utf-8') as infile:
|
||||
with open(f, encoding="utf-8") as infile:
|
||||
return infile.read()
|
||||
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(
|
||||
lambda p: p[0].startswith("Code in List"),
|
||||
self.posts)
|
||||
test_post = filter(lambda p: p[0].startswith("Code in List"), self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, 'markdown', temp)][0]
|
||||
sample_line = re.search(r'- This is a code sample', md).group(0)
|
||||
code_line = re.search(r'\s+a = \[1, 2, 3\]', md).group(0)
|
||||
self.assertTrue(sample_line.rindex('This') < code_line.rindex('a'))
|
||||
md = [r(f) for f in silent_f2p(test_post, "markdown", temp)][0]
|
||||
sample_line = re.search(r"- This is a code sample", md).group(0)
|
||||
code_line = re.search(r"\s+a = \[1, 2, 3\]", md).group(0)
|
||||
self.assertTrue(sample_line.rindex("This") < code_line.rindex("a"))
|
||||
|
||||
def test_dont_use_smart_quotes(self):
|
||||
def r(f):
|
||||
with open(f, encoding='utf-8') as infile:
|
||||
with open(f, encoding="utf-8") as infile:
|
||||
return infile.read()
|
||||
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(
|
||||
lambda p: p[0].startswith("Post with raw data"),
|
||||
self.posts)
|
||||
test_post = filter(lambda p: p[0].startswith("Post with raw data"), self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, 'markdown', temp)][0]
|
||||
md = [r(f) for f in silent_f2p(test_post, "markdown", temp)][0]
|
||||
escaped_quotes = re.search(r'\\[\'"“”‘’]', md)
|
||||
self.assertFalse(escaped_quotes)
|
||||
|
||||
def test_convert_caption_to_figure(self):
|
||||
def r(f):
|
||||
with open(f, encoding='utf-8') as infile:
|
||||
with open(f, encoding="utf-8") as infile:
|
||||
return infile.read()
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(
|
||||
lambda p: p[0].startswith("Caption on image"),
|
||||
self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, 'markdown', temp)][0]
|
||||
|
||||
caption = re.search(r'\[caption', md)
|
||||
silent_f2p = mute(True)(fields2pelican)
|
||||
test_post = filter(lambda p: p[0].startswith("Caption on image"), self.posts)
|
||||
with temporary_folder() as temp:
|
||||
md = [r(f) for f in silent_f2p(test_post, "markdown", temp)][0]
|
||||
|
||||
caption = re.search(r"\[caption", md)
|
||||
self.assertFalse(caption)
|
||||
|
||||
for occurence in [
|
||||
'/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png',
|
||||
'/theme/img/xpelican-3.png.pagespeed.ic.m-NAIdRCOM.png',
|
||||
'/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png',
|
||||
'This is a pelican',
|
||||
'This also a pelican',
|
||||
'Yet another pelican',
|
||||
"/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png",
|
||||
"/theme/img/xpelican-3.png.pagespeed.ic.m-NAIdRCOM.png",
|
||||
"/theme/img/xpelican.png.pagespeed.ic.Rjep0025-y.png",
|
||||
"This is a pelican",
|
||||
"This also a pelican",
|
||||
"Yet another pelican",
|
||||
]:
|
||||
# pandoc 2.x converts into 
|
||||
# pandoc 3.x converts into <figure>src<figcaption>text</figcaption></figure>
|
||||
|
|
@ -357,70 +401,97 @@ class TestWordpressXmlImporter(TestCaseWithCLocale):
|
|||
|
||||
class TestBuildHeader(unittest.TestCase):
|
||||
def test_build_header(self):
|
||||
header = build_header('test', None, None, None, None, None)
|
||||
self.assertEqual(header, 'test\n####\n\n')
|
||||
header = build_header("test", None, None, None, None, None)
|
||||
self.assertEqual(header, "test\n####\n\n")
|
||||
|
||||
def test_build_header_with_fields(self):
|
||||
header_data = [
|
||||
'Test Post',
|
||||
'2014-11-04',
|
||||
'Alexis Métaireau',
|
||||
['Programming'],
|
||||
['Pelican', 'Python'],
|
||||
'test-post',
|
||||
"Test Post",
|
||||
"2014-11-04",
|
||||
"Alexis Métaireau",
|
||||
["Programming"],
|
||||
["Pelican", "Python"],
|
||||
"test-post",
|
||||
]
|
||||
|
||||
expected_docutils = '\n'.join([
|
||||
'Test Post',
|
||||
'#########',
|
||||
':date: 2014-11-04',
|
||||
':author: Alexis Métaireau',
|
||||
':category: Programming',
|
||||
':tags: Pelican, Python',
|
||||
':slug: test-post',
|
||||
'\n',
|
||||
])
|
||||
expected_docutils = "\n".join(
|
||||
[
|
||||
"Test Post",
|
||||
"#########",
|
||||
":date: 2014-11-04",
|
||||
":author: Alexis Métaireau",
|
||||
":category: Programming",
|
||||
":tags: Pelican, Python",
|
||||
":slug: test-post",
|
||||
"\n",
|
||||
]
|
||||
)
|
||||
|
||||
expected_md = '\n'.join([
|
||||
'Title: Test Post',
|
||||
'Date: 2014-11-04',
|
||||
'Author: Alexis Métaireau',
|
||||
'Category: Programming',
|
||||
'Tags: Pelican, Python',
|
||||
'Slug: test-post',
|
||||
'\n',
|
||||
])
|
||||
expected_md = "\n".join(
|
||||
[
|
||||
"Title: Test Post",
|
||||
"Date: 2014-11-04",
|
||||
"Author: Alexis Métaireau",
|
||||
"Category: Programming",
|
||||
"Tags: Pelican, Python",
|
||||
"Slug: test-post",
|
||||
"\n",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(build_header(*header_data), expected_docutils)
|
||||
self.assertEqual(build_markdown_header(*header_data), expected_md)
|
||||
|
||||
def test_build_header_with_east_asian_characters(self):
|
||||
header = build_header('これは広い幅の文字だけで構成されたタイトルです',
|
||||
None, None, None, None, None)
|
||||
header = build_header(
|
||||
"これは広い幅の文字だけで構成されたタイトルです",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(header,
|
||||
('これは広い幅の文字だけで構成されたタイトルです\n'
|
||||
'##############################################'
|
||||
'\n\n'))
|
||||
|
||||
def test_galleries_added_to_header(self):
|
||||
header = build_header('test', None, None, None, None, None,
|
||||
attachments=['output/test1', 'output/test2'])
|
||||
self.assertEqual(header, ('test\n####\n'
|
||||
':attachments: output/test1, '
|
||||
'output/test2\n\n'))
|
||||
|
||||
def test_galleries_added_to_markdown_header(self):
|
||||
header = build_markdown_header('test', None, None, None, None, None,
|
||||
attachments=['output/test1',
|
||||
'output/test2'])
|
||||
self.assertEqual(
|
||||
header,
|
||||
'Title: test\nAttachments: output/test1, output/test2\n\n')
|
||||
(
|
||||
"これは広い幅の文字だけで構成されたタイトルです\n"
|
||||
"##############################################"
|
||||
"\n\n"
|
||||
),
|
||||
)
|
||||
|
||||
def test_galleries_added_to_header(self):
|
||||
header = build_header(
|
||||
"test",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
attachments=["output/test1", "output/test2"],
|
||||
)
|
||||
self.assertEqual(
|
||||
header, ("test\n####\n" ":attachments: output/test1, " "output/test2\n\n")
|
||||
)
|
||||
|
||||
def test_galleries_added_to_markdown_header(self):
|
||||
header = build_markdown_header(
|
||||
"test",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
attachments=["output/test1", "output/test2"],
|
||||
)
|
||||
self.assertEqual(
|
||||
header, "Title: test\nAttachments: output/test1, output/test2\n\n"
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
|
||||
@unittest.skipUnless(LXML, 'Needs lxml module')
|
||||
@unittest.skipUnless(BeautifulSoup, "Needs BeautifulSoup module")
|
||||
@unittest.skipUnless(LXML, "Needs lxml module")
|
||||
class TestWordpressXMLAttachements(TestCaseWithCLocale):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -435,38 +506,45 @@ class TestWordpressXMLAttachements(TestCaseWithCLocale):
|
|||
for post in self.attachments.keys():
|
||||
if post is None:
|
||||
expected = {
|
||||
('https://upload.wikimedia.org/wikipedia/commons/'
|
||||
'thumb/2/2c/Pelican_lakes_entrance02.jpg/'
|
||||
'240px-Pelican_lakes_entrance02.jpg')
|
||||
(
|
||||
"https://upload.wikimedia.org/wikipedia/commons/"
|
||||
"thumb/2/2c/Pelican_lakes_entrance02.jpg/"
|
||||
"240px-Pelican_lakes_entrance02.jpg"
|
||||
)
|
||||
}
|
||||
self.assertEqual(self.attachments[post], expected)
|
||||
elif post == 'with-excerpt':
|
||||
expected_invalid = ('http://thisurlisinvalid.notarealdomain/'
|
||||
'not_an_image.jpg')
|
||||
expected_pelikan = ('http://en.wikipedia.org/wiki/'
|
||||
'File:Pelikan_Walvis_Bay.jpg')
|
||||
self.assertEqual(self.attachments[post],
|
||||
{expected_invalid, expected_pelikan})
|
||||
elif post == 'with-tags':
|
||||
expected_invalid = ('http://thisurlisinvalid.notarealdomain')
|
||||
elif post == "with-excerpt":
|
||||
expected_invalid = (
|
||||
"http://thisurlisinvalid.notarealdomain/" "not_an_image.jpg"
|
||||
)
|
||||
expected_pelikan = (
|
||||
"http://en.wikipedia.org/wiki/" "File:Pelikan_Walvis_Bay.jpg"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.attachments[post], {expected_invalid, expected_pelikan}
|
||||
)
|
||||
elif post == "with-tags":
|
||||
expected_invalid = "http://thisurlisinvalid.notarealdomain"
|
||||
self.assertEqual(self.attachments[post], {expected_invalid})
|
||||
else:
|
||||
self.fail('all attachments should match to a '
|
||||
'filename or None, {}'
|
||||
.format(post))
|
||||
self.fail(
|
||||
"all attachments should match to a " "filename or None, {}".format(
|
||||
post
|
||||
)
|
||||
)
|
||||
|
||||
def test_download_attachments(self):
|
||||
real_file = os.path.join(CUR_DIR, 'content/article.rst')
|
||||
real_file = os.path.join(CUR_DIR, "content/article.rst")
|
||||
good_url = path_to_file_url(real_file)
|
||||
bad_url = 'http://localhost:1/not_a_file.txt'
|
||||
bad_url = "http://localhost:1/not_a_file.txt"
|
||||
silent_da = mute()(download_attachments)
|
||||
with temporary_folder() as temp:
|
||||
locations = list(silent_da(temp, [good_url, bad_url]))
|
||||
self.assertEqual(1, len(locations))
|
||||
directory = locations[0]
|
||||
self.assertTrue(
|
||||
directory.endswith(posix_join('content', 'article.rst')),
|
||||
directory)
|
||||
directory.endswith(posix_join("content", "article.rst")), directory
|
||||
)
|
||||
|
||||
|
||||
class TestTumblrImporter(TestCaseWithCLocale):
|
||||
|
|
@ -484,32 +562,42 @@ class TestTumblrImporter(TestCaseWithCLocale):
|
|||
"timestamp": 1573162000,
|
||||
"format": "html",
|
||||
"slug": "a-slug",
|
||||
"tags": [
|
||||
"economics"
|
||||
],
|
||||
"tags": ["economics"],
|
||||
"state": "published",
|
||||
|
||||
"photos": [
|
||||
{
|
||||
"caption": "",
|
||||
"original_size": {
|
||||
"url": "https://..fccdc2360ba7182a.jpg",
|
||||
"width": 634,
|
||||
"height": 789
|
||||
"height": 789,
|
||||
},
|
||||
}]
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
get.side_effect = get_posts
|
||||
|
||||
posts = list(tumblr2fields("api_key", "blogname"))
|
||||
self.assertEqual(
|
||||
[('Photo',
|
||||
'<img alt="" src="https://..fccdc2360ba7182a.jpg" />\n',
|
||||
'2019-11-07-a-slug', '2019-11-07 21:26:40+0000', 'testy', ['photo'],
|
||||
['economics'], 'published', 'article', 'html')],
|
||||
[
|
||||
(
|
||||
"Photo",
|
||||
'<img alt="" src="https://..fccdc2360ba7182a.jpg" />\n',
|
||||
"2019-11-07-a-slug",
|
||||
"2019-11-07 21:26:40+0000",
|
||||
"testy",
|
||||
["photo"],
|
||||
["economics"],
|
||||
"published",
|
||||
"article",
|
||||
"html",
|
||||
)
|
||||
],
|
||||
posts,
|
||||
posts)
|
||||
posts,
|
||||
)
|
||||
|
||||
@patch("pelican.tools.pelican_import._get_tumblr_posts")
|
||||
def test_video_embed(self, get):
|
||||
|
|
@ -531,40 +619,39 @@ class TestTumblrImporter(TestCaseWithCLocale):
|
|||
"source_title": "youtube.com",
|
||||
"caption": "<p>Caption</p>",
|
||||
"player": [
|
||||
{
|
||||
"width": 250,
|
||||
"embed_code":
|
||||
"<iframe>1</iframe>"
|
||||
},
|
||||
{
|
||||
"width": 400,
|
||||
"embed_code":
|
||||
"<iframe>2</iframe>"
|
||||
},
|
||||
{
|
||||
"width": 500,
|
||||
"embed_code":
|
||||
"<iframe>3</iframe>"
|
||||
}
|
||||
{"width": 250, "embed_code": "<iframe>1</iframe>"},
|
||||
{"width": 400, "embed_code": "<iframe>2</iframe>"},
|
||||
{"width": 500, "embed_code": "<iframe>3</iframe>"},
|
||||
],
|
||||
"video_type": "youtube",
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
get.side_effect = get_posts
|
||||
|
||||
posts = list(tumblr2fields("api_key", "blogname"))
|
||||
self.assertEqual(
|
||||
[('youtube.com',
|
||||
'<p><a href="https://href.li/?'
|
||||
'https://www.youtube.com/a">via</a></p>\n<p>Caption</p>'
|
||||
'<iframe>1</iframe>\n'
|
||||
'<iframe>2</iframe>\n'
|
||||
'<iframe>3</iframe>\n',
|
||||
'2017-07-07-the-slug',
|
||||
'2017-07-07 20:31:41+0000', 'testy', ['video'], [], 'published',
|
||||
'article', 'html')],
|
||||
[
|
||||
(
|
||||
"youtube.com",
|
||||
'<p><a href="https://href.li/?'
|
||||
'https://www.youtube.com/a">via</a></p>\n<p>Caption</p>'
|
||||
"<iframe>1</iframe>\n"
|
||||
"<iframe>2</iframe>\n"
|
||||
"<iframe>3</iframe>\n",
|
||||
"2017-07-07-the-slug",
|
||||
"2017-07-07 20:31:41+0000",
|
||||
"testy",
|
||||
["video"],
|
||||
[],
|
||||
"published",
|
||||
"article",
|
||||
"html",
|
||||
)
|
||||
],
|
||||
posts,
|
||||
posts)
|
||||
posts,
|
||||
)
|
||||
|
||||
@patch("pelican.tools.pelican_import._get_tumblr_posts")
|
||||
def test_broken_video_embed(self, get):
|
||||
|
|
@ -581,42 +668,43 @@ class TestTumblrImporter(TestCaseWithCLocale):
|
|||
"timestamp": 1471192655,
|
||||
"state": "published",
|
||||
"format": "html",
|
||||
"tags": [
|
||||
"interviews"
|
||||
],
|
||||
"source_url":
|
||||
"https://href.li/?https://www.youtube.com/watch?v=b",
|
||||
"tags": ["interviews"],
|
||||
"source_url": "https://href.li/?https://www.youtube.com/watch?v=b",
|
||||
"source_title": "youtube.com",
|
||||
"caption":
|
||||
"<p>Caption</p>",
|
||||
"caption": "<p>Caption</p>",
|
||||
"player": [
|
||||
{
|
||||
"width": 250,
|
||||
# If video is gone, embed_code is False
|
||||
"embed_code": False
|
||||
"embed_code": False,
|
||||
},
|
||||
{
|
||||
"width": 400,
|
||||
"embed_code": False
|
||||
},
|
||||
{
|
||||
"width": 500,
|
||||
"embed_code": False
|
||||
}
|
||||
{"width": 400, "embed_code": False},
|
||||
{"width": 500, "embed_code": False},
|
||||
],
|
||||
"video_type": "youtube",
|
||||
}
|
||||
]
|
||||
|
||||
get.side_effect = get_posts
|
||||
|
||||
posts = list(tumblr2fields("api_key", "blogname"))
|
||||
self.assertEqual(
|
||||
[('youtube.com',
|
||||
'<p><a href="https://href.li/?https://www.youtube.com/watch?'
|
||||
'v=b">via</a></p>\n<p>Caption</p>'
|
||||
'<p>(This video isn\'t available anymore.)</p>\n',
|
||||
'2016-08-14-the-slug',
|
||||
'2016-08-14 16:37:35+0000', 'testy', ['video'], ['interviews'],
|
||||
'published', 'article', 'html')],
|
||||
[
|
||||
(
|
||||
"youtube.com",
|
||||
'<p><a href="https://href.li/?https://www.youtube.com/watch?'
|
||||
'v=b">via</a></p>\n<p>Caption</p>'
|
||||
"<p>(This video isn't available anymore.)</p>\n",
|
||||
"2016-08-14-the-slug",
|
||||
"2016-08-14 16:37:35+0000",
|
||||
"testy",
|
||||
["video"],
|
||||
["interviews"],
|
||||
"published",
|
||||
"article",
|
||||
"html",
|
||||
)
|
||||
],
|
||||
posts,
|
||||
posts)
|
||||
posts,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,48 +35,41 @@ class TestLog(unittest.TestCase):
|
|||
def test_log_filter(self):
|
||||
def do_logging():
|
||||
for i in range(5):
|
||||
self.logger.warning('Log %s', i)
|
||||
self.logger.warning('Another log %s', i)
|
||||
self.logger.warning("Log %s", i)
|
||||
self.logger.warning("Another log %s", i)
|
||||
|
||||
# no filter
|
||||
with self.reset_logger():
|
||||
do_logging()
|
||||
self.assertEqual(self.handler.count_logs("Log \\d", logging.WARNING), 5)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Log \\d', logging.WARNING),
|
||||
5)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Another log \\d', logging.WARNING),
|
||||
5)
|
||||
self.handler.count_logs("Another log \\d", logging.WARNING), 5
|
||||
)
|
||||
|
||||
# filter by template
|
||||
with self.reset_logger():
|
||||
log.LimitFilter._ignore.add((logging.WARNING, 'Log %s'))
|
||||
log.LimitFilter._ignore.add((logging.WARNING, "Log %s"))
|
||||
do_logging()
|
||||
self.assertEqual(self.handler.count_logs("Log \\d", logging.WARNING), 0)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Log \\d', logging.WARNING),
|
||||
0)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Another log \\d', logging.WARNING),
|
||||
5)
|
||||
self.handler.count_logs("Another log \\d", logging.WARNING), 5
|
||||
)
|
||||
|
||||
# filter by exact message
|
||||
with self.reset_logger():
|
||||
log.LimitFilter._ignore.add((logging.WARNING, 'Log 3'))
|
||||
log.LimitFilter._ignore.add((logging.WARNING, "Log 3"))
|
||||
do_logging()
|
||||
self.assertEqual(self.handler.count_logs("Log \\d", logging.WARNING), 4)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Log \\d', logging.WARNING),
|
||||
4)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Another log \\d', logging.WARNING),
|
||||
5)
|
||||
self.handler.count_logs("Another log \\d", logging.WARNING), 5
|
||||
)
|
||||
|
||||
# filter by both
|
||||
with self.reset_logger():
|
||||
log.LimitFilter._ignore.add((logging.WARNING, 'Log 3'))
|
||||
log.LimitFilter._ignore.add((logging.WARNING, 'Another log %s'))
|
||||
log.LimitFilter._ignore.add((logging.WARNING, "Log 3"))
|
||||
log.LimitFilter._ignore.add((logging.WARNING, "Another log %s"))
|
||||
do_logging()
|
||||
self.assertEqual(self.handler.count_logs("Log \\d", logging.WARNING), 4)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Log \\d', logging.WARNING),
|
||||
4)
|
||||
self.assertEqual(
|
||||
self.handler.count_logs('Another log \\d', logging.WARNING),
|
||||
0)
|
||||
self.handler.count_logs("Another log \\d", logging.WARNING), 0
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,17 +17,17 @@ class TestPage(unittest.TestCase):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
self.page_kwargs = {
|
||||
'content': TEST_CONTENT,
|
||||
'context': {
|
||||
'localsiteurl': '',
|
||||
"content": TEST_CONTENT,
|
||||
"context": {
|
||||
"localsiteurl": "",
|
||||
},
|
||||
'metadata': {
|
||||
'summary': TEST_SUMMARY,
|
||||
'title': 'foo bar',
|
||||
"metadata": {
|
||||
"summary": TEST_SUMMARY,
|
||||
"title": "foo bar",
|
||||
},
|
||||
'source_path': '/path/to/file/foo.ext'
|
||||
"source_path": "/path/to/file/foo.ext",
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
|
|
@ -37,68 +37,79 @@ class TestPage(unittest.TestCase):
|
|||
settings = get_settings()
|
||||
# fix up pagination rules
|
||||
from pelican.paginator import PaginationRule
|
||||
|
||||
pagination_rules = [
|
||||
PaginationRule(*r) for r in settings.get(
|
||||
'PAGINATION_PATTERNS',
|
||||
DEFAULT_CONFIG['PAGINATION_PATTERNS'],
|
||||
PaginationRule(*r)
|
||||
for r in settings.get(
|
||||
"PAGINATION_PATTERNS",
|
||||
DEFAULT_CONFIG["PAGINATION_PATTERNS"],
|
||||
)
|
||||
]
|
||||
settings['PAGINATION_PATTERNS'] = sorted(
|
||||
settings["PAGINATION_PATTERNS"] = sorted(
|
||||
pagination_rules,
|
||||
key=lambda r: r[0],
|
||||
)
|
||||
|
||||
self.page_kwargs['metadata']['author'] = Author('Blogger', settings)
|
||||
object_list = [Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs)]
|
||||
paginator = Paginator('foobar.foo', 'foobar/foo', object_list,
|
||||
settings)
|
||||
self.page_kwargs["metadata"]["author"] = Author("Blogger", settings)
|
||||
object_list = [Article(**self.page_kwargs), Article(**self.page_kwargs)]
|
||||
paginator = Paginator("foobar.foo", "foobar/foo", object_list, settings)
|
||||
page = paginator.page(1)
|
||||
self.assertEqual(page.save_as, 'foobar.foo')
|
||||
self.assertEqual(page.save_as, "foobar.foo")
|
||||
|
||||
def test_custom_pagination_pattern(self):
|
||||
from pelican.paginator import PaginationRule
|
||||
settings = get_settings()
|
||||
settings['PAGINATION_PATTERNS'] = [PaginationRule(*r) for r in [
|
||||
(1, '/{url}', '{base_name}/index.html'),
|
||||
(2, '/{url}{number}/', '{base_name}/{number}/index.html')
|
||||
]]
|
||||
|
||||
self.page_kwargs['metadata']['author'] = Author('Blogger', settings)
|
||||
object_list = [Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs)]
|
||||
paginator = Paginator('blog/index.html', '//blog.my.site/',
|
||||
object_list, settings, 1)
|
||||
settings = get_settings()
|
||||
settings["PAGINATION_PATTERNS"] = [
|
||||
PaginationRule(*r)
|
||||
for r in [
|
||||
(1, "/{url}", "{base_name}/index.html"),
|
||||
(2, "/{url}{number}/", "{base_name}/{number}/index.html"),
|
||||
]
|
||||
]
|
||||
|
||||
self.page_kwargs["metadata"]["author"] = Author("Blogger", settings)
|
||||
object_list = [Article(**self.page_kwargs), Article(**self.page_kwargs)]
|
||||
paginator = Paginator(
|
||||
"blog/index.html", "//blog.my.site/", object_list, settings, 1
|
||||
)
|
||||
# The URL *has to* stay absolute (with // in the front), so verify that
|
||||
page1 = paginator.page(1)
|
||||
self.assertEqual(page1.save_as, 'blog/index.html')
|
||||
self.assertEqual(page1.url, '//blog.my.site/')
|
||||
self.assertEqual(page1.save_as, "blog/index.html")
|
||||
self.assertEqual(page1.url, "//blog.my.site/")
|
||||
page2 = paginator.page(2)
|
||||
self.assertEqual(page2.save_as, 'blog/2/index.html')
|
||||
self.assertEqual(page2.url, '//blog.my.site/2/')
|
||||
self.assertEqual(page2.save_as, "blog/2/index.html")
|
||||
self.assertEqual(page2.url, "//blog.my.site/2/")
|
||||
|
||||
def test_custom_pagination_pattern_last_page(self):
|
||||
from pelican.paginator import PaginationRule
|
||||
settings = get_settings()
|
||||
settings['PAGINATION_PATTERNS'] = [PaginationRule(*r) for r in [
|
||||
(1, '/{url}1/', '{base_name}/1/index.html'),
|
||||
(2, '/{url}{number}/', '{base_name}/{number}/index.html'),
|
||||
(-1, '/{url}', '{base_name}/index.html'),
|
||||
]]
|
||||
|
||||
self.page_kwargs['metadata']['author'] = Author('Blogger', settings)
|
||||
object_list = [Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs)]
|
||||
paginator = Paginator('blog/index.html', '//blog.my.site/',
|
||||
object_list, settings, 1)
|
||||
settings = get_settings()
|
||||
settings["PAGINATION_PATTERNS"] = [
|
||||
PaginationRule(*r)
|
||||
for r in [
|
||||
(1, "/{url}1/", "{base_name}/1/index.html"),
|
||||
(2, "/{url}{number}/", "{base_name}/{number}/index.html"),
|
||||
(-1, "/{url}", "{base_name}/index.html"),
|
||||
]
|
||||
]
|
||||
|
||||
self.page_kwargs["metadata"]["author"] = Author("Blogger", settings)
|
||||
object_list = [
|
||||
Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs),
|
||||
Article(**self.page_kwargs),
|
||||
]
|
||||
paginator = Paginator(
|
||||
"blog/index.html", "//blog.my.site/", object_list, settings, 1
|
||||
)
|
||||
# The URL *has to* stay absolute (with // in the front), so verify that
|
||||
page1 = paginator.page(1)
|
||||
self.assertEqual(page1.save_as, 'blog/1/index.html')
|
||||
self.assertEqual(page1.url, '//blog.my.site/1/')
|
||||
self.assertEqual(page1.save_as, "blog/1/index.html")
|
||||
self.assertEqual(page1.url, "//blog.my.site/1/")
|
||||
page2 = paginator.page(2)
|
||||
self.assertEqual(page2.save_as, 'blog/2/index.html')
|
||||
self.assertEqual(page2.url, '//blog.my.site/2/')
|
||||
self.assertEqual(page2.save_as, "blog/2/index.html")
|
||||
self.assertEqual(page2.url, "//blog.my.site/2/")
|
||||
page3 = paginator.page(3)
|
||||
self.assertEqual(page3.save_as, 'blog/index.html')
|
||||
self.assertEqual(page3.url, '//blog.my.site/')
|
||||
self.assertEqual(page3.save_as, "blog/index.html")
|
||||
self.assertEqual(page3.url, "//blog.my.site/")
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ from pelican.tests.support import (
|
|||
)
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SAMPLES_PATH = os.path.abspath(os.path.join(
|
||||
CURRENT_DIR, os.pardir, os.pardir, 'samples'))
|
||||
OUTPUT_PATH = os.path.abspath(os.path.join(CURRENT_DIR, 'output'))
|
||||
SAMPLES_PATH = os.path.abspath(
|
||||
os.path.join(CURRENT_DIR, os.pardir, os.pardir, "samples")
|
||||
)
|
||||
OUTPUT_PATH = os.path.abspath(os.path.join(CURRENT_DIR, "output"))
|
||||
|
||||
INPUT_PATH = os.path.join(SAMPLES_PATH, "content")
|
||||
SAMPLE_CONFIG = os.path.join(SAMPLES_PATH, "pelican.conf.py")
|
||||
|
|
@ -31,9 +32,9 @@ SAMPLE_FR_CONFIG = os.path.join(SAMPLES_PATH, "pelican.conf_FR.py")
|
|||
|
||||
def recursiveDiff(dcmp):
|
||||
diff = {
|
||||
'diff_files': [os.path.join(dcmp.right, f) for f in dcmp.diff_files],
|
||||
'left_only': [os.path.join(dcmp.right, f) for f in dcmp.left_only],
|
||||
'right_only': [os.path.join(dcmp.right, f) for f in dcmp.right_only],
|
||||
"diff_files": [os.path.join(dcmp.right, f) for f in dcmp.diff_files],
|
||||
"left_only": [os.path.join(dcmp.right, f) for f in dcmp.left_only],
|
||||
"right_only": [os.path.join(dcmp.right, f) for f in dcmp.right_only],
|
||||
}
|
||||
for sub_dcmp in dcmp.subdirs.values():
|
||||
for k, v in recursiveDiff(sub_dcmp).items():
|
||||
|
|
@ -47,11 +48,11 @@ class TestPelican(LoggedTestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.temp_path = mkdtemp(prefix='pelicantests.')
|
||||
self.temp_cache = mkdtemp(prefix='pelican_cache.')
|
||||
self.temp_path = mkdtemp(prefix="pelicantests.")
|
||||
self.temp_cache = mkdtemp(prefix="pelican_cache.")
|
||||
self.maxDiff = None
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
|
||||
def tearDown(self):
|
||||
read_settings() # cleanup PYGMENTS_RST_OPTIONS
|
||||
|
|
@ -70,8 +71,8 @@ class TestPelican(LoggedTestCase):
|
|||
if proc.returncode != 0:
|
||||
msg = self._formatMessage(
|
||||
msg,
|
||||
"%s and %s differ:\nstdout:\n%s\nstderr\n%s" %
|
||||
(left_path, right_path, out, err)
|
||||
"%s and %s differ:\nstdout:\n%s\nstderr\n%s"
|
||||
% (left_path, right_path, out, err),
|
||||
)
|
||||
raise self.failureException(msg)
|
||||
|
||||
|
|
@ -85,136 +86,154 @@ class TestPelican(LoggedTestCase):
|
|||
|
||||
self.assertTrue(
|
||||
generator_classes[-1] is StaticGenerator,
|
||||
"StaticGenerator must be the last generator, but it isn't!")
|
||||
"StaticGenerator must be the last generator, but it isn't!",
|
||||
)
|
||||
self.assertIsInstance(
|
||||
generator_classes, Sequence,
|
||||
"_get_generator_classes() must return a Sequence to preserve order")
|
||||
generator_classes,
|
||||
Sequence,
|
||||
"_get_generator_classes() must return a Sequence to preserve order",
|
||||
)
|
||||
|
||||
@skipIfNoExecutable(['git', '--version'])
|
||||
@skipIfNoExecutable(["git", "--version"])
|
||||
def test_basic_generation_works(self):
|
||||
# when running pelican without settings, it should pick up the default
|
||||
# ones and generate correct output without raising any exception
|
||||
settings = read_settings(path=None, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'LOCALE': locale.normalize('en_US'),
|
||||
})
|
||||
settings = read_settings(
|
||||
path=None,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"LOCALE": locale.normalize("en_US"),
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
self.assertDirsEqual(
|
||||
self.temp_path, os.path.join(OUTPUT_PATH, 'basic')
|
||||
)
|
||||
self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, "basic"))
|
||||
self.assertLogCountEqual(
|
||||
count=1,
|
||||
msg="Unable to find.*skipping url replacement",
|
||||
level=logging.WARNING)
|
||||
level=logging.WARNING,
|
||||
)
|
||||
|
||||
@skipIfNoExecutable(['git', '--version'])
|
||||
@skipIfNoExecutable(["git", "--version"])
|
||||
def test_custom_generation_works(self):
|
||||
# the same thing with a specified set of settings should work
|
||||
settings = read_settings(path=SAMPLE_CONFIG, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'LOCALE': locale.normalize('en_US.UTF-8'),
|
||||
})
|
||||
settings = read_settings(
|
||||
path=SAMPLE_CONFIG,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"LOCALE": locale.normalize("en_US.UTF-8"),
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
self.assertDirsEqual(
|
||||
self.temp_path, os.path.join(OUTPUT_PATH, 'custom')
|
||||
)
|
||||
self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, "custom"))
|
||||
|
||||
@skipIfNoExecutable(['git', '--version'])
|
||||
@unittest.skipUnless(locale_available('fr_FR.UTF-8') or
|
||||
locale_available('French'), 'French locale needed')
|
||||
@skipIfNoExecutable(["git", "--version"])
|
||||
@unittest.skipUnless(
|
||||
locale_available("fr_FR.UTF-8") or locale_available("French"),
|
||||
"French locale needed",
|
||||
)
|
||||
def test_custom_locale_generation_works(self):
|
||||
'''Test that generation with fr_FR.UTF-8 locale works'''
|
||||
if sys.platform == 'win32':
|
||||
our_locale = 'French'
|
||||
"""Test that generation with fr_FR.UTF-8 locale works"""
|
||||
if sys.platform == "win32":
|
||||
our_locale = "French"
|
||||
else:
|
||||
our_locale = 'fr_FR.UTF-8'
|
||||
our_locale = "fr_FR.UTF-8"
|
||||
|
||||
settings = read_settings(path=SAMPLE_FR_CONFIG, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'LOCALE': our_locale,
|
||||
})
|
||||
settings = read_settings(
|
||||
path=SAMPLE_FR_CONFIG,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"LOCALE": our_locale,
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
self.assertDirsEqual(
|
||||
self.temp_path, os.path.join(OUTPUT_PATH, 'custom_locale')
|
||||
)
|
||||
self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, "custom_locale"))
|
||||
|
||||
def test_theme_static_paths_copy(self):
|
||||
# the same thing with a specified set of settings should work
|
||||
settings = read_settings(path=SAMPLE_CONFIG, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'THEME_STATIC_PATHS': [os.path.join(SAMPLES_PATH, 'very'),
|
||||
os.path.join(SAMPLES_PATH, 'kinda'),
|
||||
os.path.join(SAMPLES_PATH,
|
||||
'theme_standard')]
|
||||
})
|
||||
settings = read_settings(
|
||||
path=SAMPLE_CONFIG,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"THEME_STATIC_PATHS": [
|
||||
os.path.join(SAMPLES_PATH, "very"),
|
||||
os.path.join(SAMPLES_PATH, "kinda"),
|
||||
os.path.join(SAMPLES_PATH, "theme_standard"),
|
||||
],
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
theme_output = os.path.join(self.temp_path, 'theme')
|
||||
extra_path = os.path.join(theme_output, 'exciting', 'new', 'files')
|
||||
theme_output = os.path.join(self.temp_path, "theme")
|
||||
extra_path = os.path.join(theme_output, "exciting", "new", "files")
|
||||
|
||||
for file in ['a_stylesheet', 'a_template']:
|
||||
for file in ["a_stylesheet", "a_template"]:
|
||||
self.assertTrue(os.path.exists(os.path.join(theme_output, file)))
|
||||
|
||||
for file in ['wow!', 'boom!', 'bap!', 'zap!']:
|
||||
for file in ["wow!", "boom!", "bap!", "zap!"]:
|
||||
self.assertTrue(os.path.exists(os.path.join(extra_path, file)))
|
||||
|
||||
def test_theme_static_paths_copy_single_file(self):
|
||||
# the same thing with a specified set of settings should work
|
||||
settings = read_settings(path=SAMPLE_CONFIG, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'THEME_STATIC_PATHS': [os.path.join(SAMPLES_PATH,
|
||||
'theme_standard')]
|
||||
})
|
||||
settings = read_settings(
|
||||
path=SAMPLE_CONFIG,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"THEME_STATIC_PATHS": [os.path.join(SAMPLES_PATH, "theme_standard")],
|
||||
},
|
||||
)
|
||||
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
theme_output = os.path.join(self.temp_path, 'theme')
|
||||
theme_output = os.path.join(self.temp_path, "theme")
|
||||
|
||||
for file in ['a_stylesheet', 'a_template']:
|
||||
for file in ["a_stylesheet", "a_template"]:
|
||||
self.assertTrue(os.path.exists(os.path.join(theme_output, file)))
|
||||
|
||||
def test_write_only_selected(self):
|
||||
"""Test that only the selected files are written"""
|
||||
settings = read_settings(path=None, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'WRITE_SELECTED': [
|
||||
os.path.join(self.temp_path, 'oh-yeah.html'),
|
||||
os.path.join(self.temp_path, 'categories.html'),
|
||||
],
|
||||
'LOCALE': locale.normalize('en_US'),
|
||||
})
|
||||
settings = read_settings(
|
||||
path=None,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"WRITE_SELECTED": [
|
||||
os.path.join(self.temp_path, "oh-yeah.html"),
|
||||
os.path.join(self.temp_path, "categories.html"),
|
||||
],
|
||||
"LOCALE": locale.normalize("en_US"),
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
logger = logging.getLogger()
|
||||
orig_level = logger.getEffectiveLevel()
|
||||
logger.setLevel(logging.INFO)
|
||||
mute(True)(pelican.run)()
|
||||
logger.setLevel(orig_level)
|
||||
self.assertLogCountEqual(
|
||||
count=2,
|
||||
msg="Writing .*",
|
||||
level=logging.INFO)
|
||||
self.assertLogCountEqual(count=2, msg="Writing .*", level=logging.INFO)
|
||||
|
||||
def test_cyclic_intersite_links_no_warnings(self):
|
||||
settings = read_settings(path=None, override={
|
||||
'PATH': os.path.join(CURRENT_DIR, 'cyclic_intersite_links'),
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
})
|
||||
settings = read_settings(
|
||||
path=None,
|
||||
override={
|
||||
"PATH": os.path.join(CURRENT_DIR, "cyclic_intersite_links"),
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
# There are four different intersite links:
|
||||
|
|
@ -230,41 +249,48 @@ class TestPelican(LoggedTestCase):
|
|||
self.assertLogCountEqual(
|
||||
count=1,
|
||||
msg="Unable to find '.*\\.rst', skipping url replacement.",
|
||||
level=logging.WARNING)
|
||||
level=logging.WARNING,
|
||||
)
|
||||
|
||||
def test_md_extensions_deprecation(self):
|
||||
"""Test that a warning is issued if MD_EXTENSIONS is used"""
|
||||
settings = read_settings(path=None, override={
|
||||
'PATH': INPUT_PATH,
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
'MD_EXTENSIONS': {},
|
||||
})
|
||||
settings = read_settings(
|
||||
path=None,
|
||||
override={
|
||||
"PATH": INPUT_PATH,
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
"MD_EXTENSIONS": {},
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
self.assertLogCountEqual(
|
||||
count=1,
|
||||
msg="MD_EXTENSIONS is deprecated use MARKDOWN instead.",
|
||||
level=logging.WARNING)
|
||||
level=logging.WARNING,
|
||||
)
|
||||
|
||||
def test_parse_errors(self):
|
||||
# Verify that just an error is printed and the application doesn't
|
||||
# abort, exit or something.
|
||||
settings = read_settings(path=None, override={
|
||||
'PATH': os.path.abspath(os.path.join(CURRENT_DIR, 'parse_error')),
|
||||
'OUTPUT_PATH': self.temp_path,
|
||||
'CACHE_PATH': self.temp_cache,
|
||||
})
|
||||
settings = read_settings(
|
||||
path=None,
|
||||
override={
|
||||
"PATH": os.path.abspath(os.path.join(CURRENT_DIR, "parse_error")),
|
||||
"OUTPUT_PATH": self.temp_path,
|
||||
"CACHE_PATH": self.temp_cache,
|
||||
},
|
||||
)
|
||||
pelican = Pelican(settings=settings)
|
||||
mute(True)(pelican.run)()
|
||||
self.assertLogCountEqual(
|
||||
count=1,
|
||||
msg="Could not process .*parse_error.rst",
|
||||
level=logging.ERROR)
|
||||
count=1, msg="Could not process .*parse_error.rst", level=logging.ERROR
|
||||
)
|
||||
|
||||
def test_module_load(self):
|
||||
"""Test loading via python -m pelican --help displays the help"""
|
||||
output = subprocess.check_output([
|
||||
sys.executable, '-m', 'pelican', '--help'
|
||||
]).decode('ascii', 'replace')
|
||||
assert 'usage:' in output
|
||||
output = subprocess.check_output(
|
||||
[sys.executable, "-m", "pelican", "--help"]
|
||||
).decode("ascii", "replace")
|
||||
assert "usage:" in output
|
||||
|
|
|
|||
|
|
@ -2,27 +2,26 @@ import os
|
|||
from contextlib import contextmanager
|
||||
|
||||
import pelican.tests.dummy_plugins.normal_plugin.normal_plugin as normal_plugin
|
||||
from pelican.plugins._utils import (get_namespace_plugins, get_plugin_name,
|
||||
load_plugins)
|
||||
from pelican.plugins._utils import get_namespace_plugins, get_plugin_name, load_plugins
|
||||
from pelican.tests.support import unittest
|
||||
|
||||
|
||||
@contextmanager
|
||||
def tmp_namespace_path(path):
|
||||
'''Context manager for temporarily appending namespace plugin packages
|
||||
"""Context manager for temporarily appending namespace plugin packages
|
||||
|
||||
path: path containing the `pelican` folder
|
||||
|
||||
This modifies the `pelican.__path__` and lets the `pelican.plugins`
|
||||
namespace package resolve it from that.
|
||||
'''
|
||||
"""
|
||||
# This avoids calls to internal `pelican.plugins.__path__._recalculate()`
|
||||
# as it should not be necessary
|
||||
import pelican
|
||||
|
||||
old_path = pelican.__path__[:]
|
||||
try:
|
||||
pelican.__path__.append(os.path.join(path, 'pelican'))
|
||||
pelican.__path__.append(os.path.join(path, "pelican"))
|
||||
yield
|
||||
finally:
|
||||
pelican.__path__ = old_path
|
||||
|
|
@ -30,38 +29,38 @@ def tmp_namespace_path(path):
|
|||
|
||||
class PluginTest(unittest.TestCase):
|
||||
_PLUGIN_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'dummy_plugins')
|
||||
_NS_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, 'namespace_plugin')
|
||||
_NORMAL_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, 'normal_plugin')
|
||||
os.path.abspath(os.path.dirname(__file__)), "dummy_plugins"
|
||||
)
|
||||
_NS_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, "namespace_plugin")
|
||||
_NORMAL_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, "normal_plugin")
|
||||
|
||||
def test_namespace_path_modification(self):
|
||||
import pelican
|
||||
import pelican.plugins
|
||||
|
||||
old_path = pelican.__path__[:]
|
||||
|
||||
# not existing path
|
||||
path = os.path.join(self._PLUGIN_FOLDER, 'foo')
|
||||
path = os.path.join(self._PLUGIN_FOLDER, "foo")
|
||||
with tmp_namespace_path(path):
|
||||
self.assertIn(
|
||||
os.path.join(path, 'pelican'),
|
||||
pelican.__path__)
|
||||
self.assertIn(os.path.join(path, "pelican"), pelican.__path__)
|
||||
# foo/pelican does not exist, so it won't propagate
|
||||
self.assertNotIn(
|
||||
os.path.join(path, 'pelican', 'plugins'),
|
||||
pelican.plugins.__path__)
|
||||
os.path.join(path, "pelican", "plugins"), pelican.plugins.__path__
|
||||
)
|
||||
# verify that we restored path back
|
||||
self.assertEqual(pelican.__path__, old_path)
|
||||
|
||||
# existing path
|
||||
with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
|
||||
self.assertIn(
|
||||
os.path.join(self._NS_PLUGIN_FOLDER, 'pelican'),
|
||||
pelican.__path__)
|
||||
os.path.join(self._NS_PLUGIN_FOLDER, "pelican"), pelican.__path__
|
||||
)
|
||||
# /namespace_plugin/pelican exists, so it should be in
|
||||
self.assertIn(
|
||||
os.path.join(self._NS_PLUGIN_FOLDER, 'pelican', 'plugins'),
|
||||
pelican.plugins.__path__)
|
||||
os.path.join(self._NS_PLUGIN_FOLDER, "pelican", "plugins"),
|
||||
pelican.plugins.__path__,
|
||||
)
|
||||
self.assertEqual(pelican.__path__, old_path)
|
||||
|
||||
def test_get_namespace_plugins(self):
|
||||
|
|
@ -71,11 +70,11 @@ class PluginTest(unittest.TestCase):
|
|||
# with plugin
|
||||
with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
|
||||
ns_plugins = get_namespace_plugins()
|
||||
self.assertEqual(len(ns_plugins), len(existing_ns_plugins)+1)
|
||||
self.assertIn('pelican.plugins.ns_plugin', ns_plugins)
|
||||
self.assertEqual(len(ns_plugins), len(existing_ns_plugins) + 1)
|
||||
self.assertIn("pelican.plugins.ns_plugin", ns_plugins)
|
||||
self.assertEqual(
|
||||
ns_plugins['pelican.plugins.ns_plugin'].NAME,
|
||||
'namespace plugin')
|
||||
ns_plugins["pelican.plugins.ns_plugin"].NAME, "namespace plugin"
|
||||
)
|
||||
|
||||
# should be back to existing namespace plugins outside `with`
|
||||
ns_plugins = get_namespace_plugins()
|
||||
|
|
@ -91,15 +90,14 @@ class PluginTest(unittest.TestCase):
|
|||
with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
|
||||
# with no `PLUGINS` setting, load namespace plugins
|
||||
plugins = load_plugins({})
|
||||
self.assertEqual(len(plugins), len(existing_ns_plugins)+1, plugins)
|
||||
self.assertEqual(len(plugins), len(existing_ns_plugins) + 1, plugins)
|
||||
self.assertEqual(
|
||||
{'pelican.plugins.ns_plugin'} | get_plugin_names(existing_ns_plugins),
|
||||
get_plugin_names(plugins))
|
||||
{"pelican.plugins.ns_plugin"} | get_plugin_names(existing_ns_plugins),
|
||||
get_plugin_names(plugins),
|
||||
)
|
||||
|
||||
# disable namespace plugins with `PLUGINS = []`
|
||||
SETTINGS = {
|
||||
'PLUGINS': []
|
||||
}
|
||||
SETTINGS = {"PLUGINS": []}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 0, plugins)
|
||||
|
||||
|
|
@ -107,34 +105,35 @@ class PluginTest(unittest.TestCase):
|
|||
|
||||
# normal plugin
|
||||
SETTINGS = {
|
||||
'PLUGINS': ['normal_plugin'],
|
||||
'PLUGIN_PATHS': [self._NORMAL_PLUGIN_FOLDER]
|
||||
"PLUGINS": ["normal_plugin"],
|
||||
"PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
|
||||
}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 1, plugins)
|
||||
self.assertEqual(
|
||||
{'normal_plugin'},
|
||||
get_plugin_names(plugins))
|
||||
self.assertEqual({"normal_plugin"}, get_plugin_names(plugins))
|
||||
|
||||
# normal submodule/subpackage plugins
|
||||
SETTINGS = {
|
||||
'PLUGINS': [
|
||||
'normal_submodule_plugin.subplugin',
|
||||
'normal_submodule_plugin.subpackage.subpackage',
|
||||
"PLUGINS": [
|
||||
"normal_submodule_plugin.subplugin",
|
||||
"normal_submodule_plugin.subpackage.subpackage",
|
||||
],
|
||||
'PLUGIN_PATHS': [self._NORMAL_PLUGIN_FOLDER]
|
||||
"PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
|
||||
}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 2, plugins)
|
||||
self.assertEqual(
|
||||
{'normal_submodule_plugin.subplugin',
|
||||
'normal_submodule_plugin.subpackage.subpackage'},
|
||||
get_plugin_names(plugins))
|
||||
{
|
||||
"normal_submodule_plugin.subplugin",
|
||||
"normal_submodule_plugin.subpackage.subpackage",
|
||||
},
|
||||
get_plugin_names(plugins),
|
||||
)
|
||||
|
||||
# ensure normal plugins are loaded only once
|
||||
SETTINGS = {
|
||||
'PLUGINS': ['normal_plugin'],
|
||||
'PLUGIN_PATHS': [self._NORMAL_PLUGIN_FOLDER],
|
||||
"PLUGINS": ["normal_plugin"],
|
||||
"PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
|
||||
}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
for plugin in load_plugins(SETTINGS):
|
||||
|
|
@ -143,40 +142,33 @@ class PluginTest(unittest.TestCase):
|
|||
self.assertIn(plugin, plugins)
|
||||
|
||||
# namespace plugin short
|
||||
SETTINGS = {
|
||||
'PLUGINS': ['ns_plugin']
|
||||
}
|
||||
SETTINGS = {"PLUGINS": ["ns_plugin"]}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 1, plugins)
|
||||
self.assertEqual(
|
||||
{'pelican.plugins.ns_plugin'},
|
||||
get_plugin_names(plugins))
|
||||
self.assertEqual({"pelican.plugins.ns_plugin"}, get_plugin_names(plugins))
|
||||
|
||||
# namespace plugin long
|
||||
SETTINGS = {
|
||||
'PLUGINS': ['pelican.plugins.ns_plugin']
|
||||
}
|
||||
SETTINGS = {"PLUGINS": ["pelican.plugins.ns_plugin"]}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 1, plugins)
|
||||
self.assertEqual(
|
||||
{'pelican.plugins.ns_plugin'},
|
||||
get_plugin_names(plugins))
|
||||
self.assertEqual({"pelican.plugins.ns_plugin"}, get_plugin_names(plugins))
|
||||
|
||||
# normal and namespace plugin
|
||||
SETTINGS = {
|
||||
'PLUGINS': ['normal_plugin', 'ns_plugin'],
|
||||
'PLUGIN_PATHS': [self._NORMAL_PLUGIN_FOLDER]
|
||||
"PLUGINS": ["normal_plugin", "ns_plugin"],
|
||||
"PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
|
||||
}
|
||||
plugins = load_plugins(SETTINGS)
|
||||
self.assertEqual(len(plugins), 2, plugins)
|
||||
self.assertEqual(
|
||||
{'normal_plugin', 'pelican.plugins.ns_plugin'},
|
||||
get_plugin_names(plugins))
|
||||
{"normal_plugin", "pelican.plugins.ns_plugin"},
|
||||
get_plugin_names(plugins),
|
||||
)
|
||||
|
||||
def test_get_plugin_name(self):
|
||||
self.assertEqual(
|
||||
get_plugin_name(normal_plugin),
|
||||
'pelican.tests.dummy_plugins.normal_plugin.normal_plugin',
|
||||
"pelican.tests.dummy_plugins.normal_plugin.normal_plugin",
|
||||
)
|
||||
|
||||
class NoopPlugin:
|
||||
|
|
@ -185,7 +177,9 @@ class PluginTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(
|
||||
get_plugin_name(NoopPlugin),
|
||||
'PluginTest.test_get_plugin_name.<locals>.NoopPlugin')
|
||||
"PluginTest.test_get_plugin_name.<locals>.NoopPlugin",
|
||||
)
|
||||
self.assertEqual(
|
||||
get_plugin_name(NoopPlugin()),
|
||||
'PluginTest.test_get_plugin_name.<locals>.NoopPlugin')
|
||||
"PluginTest.test_get_plugin_name.<locals>.NoopPlugin",
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,11 +6,11 @@ from pelican.tests.support import unittest
|
|||
class Test_abbr_role(unittest.TestCase):
|
||||
def call_it(self, text):
|
||||
from pelican.rstdirectives import abbr_role
|
||||
|
||||
rawtext = text
|
||||
lineno = 42
|
||||
inliner = Mock(name='inliner')
|
||||
nodes, system_messages = abbr_role(
|
||||
'abbr', rawtext, text, lineno, inliner)
|
||||
inliner = Mock(name="inliner")
|
||||
nodes, system_messages = abbr_role("abbr", rawtext, text, lineno, inliner)
|
||||
self.assertEqual(system_messages, [])
|
||||
self.assertEqual(len(nodes), 1)
|
||||
return nodes[0]
|
||||
|
|
@ -18,14 +18,14 @@ class Test_abbr_role(unittest.TestCase):
|
|||
def test(self):
|
||||
node = self.call_it("Abbr (Abbreviation)")
|
||||
self.assertEqual(node.astext(), "Abbr")
|
||||
self.assertEqual(node['explanation'], "Abbreviation")
|
||||
self.assertEqual(node["explanation"], "Abbreviation")
|
||||
|
||||
def test_newlines_in_explanation(self):
|
||||
node = self.call_it("CUL (See you\nlater)")
|
||||
self.assertEqual(node.astext(), "CUL")
|
||||
self.assertEqual(node['explanation'], "See you\nlater")
|
||||
self.assertEqual(node["explanation"], "See you\nlater")
|
||||
|
||||
def test_newlines_in_abbr(self):
|
||||
node = self.call_it("US of\nA \n (USA)")
|
||||
self.assertEqual(node.astext(), "US of\nA")
|
||||
self.assertEqual(node['explanation'], "USA")
|
||||
self.assertEqual(node["explanation"], "USA")
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ class MockServer:
|
|||
|
||||
|
||||
class TestServer(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = MockServer()
|
||||
self.temp_output = mkdtemp(prefix='pelicantests.')
|
||||
self.temp_output = mkdtemp(prefix="pelicantests.")
|
||||
self.old_cwd = os.getcwd()
|
||||
os.chdir(self.temp_output)
|
||||
|
||||
|
|
@ -29,32 +28,33 @@ class TestServer(unittest.TestCase):
|
|||
rmtree(self.temp_output)
|
||||
|
||||
def test_get_path_that_exists(self):
|
||||
handler = ComplexHTTPRequestHandler(MockRequest(), ('0.0.0.0', 8888),
|
||||
self.server)
|
||||
handler = ComplexHTTPRequestHandler(
|
||||
MockRequest(), ("0.0.0.0", 8888), self.server
|
||||
)
|
||||
handler.base_path = self.temp_output
|
||||
|
||||
open(os.path.join(self.temp_output, 'foo.html'), 'a').close()
|
||||
os.mkdir(os.path.join(self.temp_output, 'foo'))
|
||||
open(os.path.join(self.temp_output, 'foo', 'index.html'), 'a').close()
|
||||
open(os.path.join(self.temp_output, "foo.html"), "a").close()
|
||||
os.mkdir(os.path.join(self.temp_output, "foo"))
|
||||
open(os.path.join(self.temp_output, "foo", "index.html"), "a").close()
|
||||
|
||||
os.mkdir(os.path.join(self.temp_output, 'bar'))
|
||||
open(os.path.join(self.temp_output, 'bar', 'index.html'), 'a').close()
|
||||
os.mkdir(os.path.join(self.temp_output, "bar"))
|
||||
open(os.path.join(self.temp_output, "bar", "index.html"), "a").close()
|
||||
|
||||
os.mkdir(os.path.join(self.temp_output, 'baz'))
|
||||
os.mkdir(os.path.join(self.temp_output, "baz"))
|
||||
|
||||
for suffix in ['', '/']:
|
||||
for suffix in ["", "/"]:
|
||||
# foo.html has precedence over foo/index.html
|
||||
path = handler.get_path_that_exists('foo' + suffix)
|
||||
self.assertEqual(path, 'foo.html')
|
||||
path = handler.get_path_that_exists("foo" + suffix)
|
||||
self.assertEqual(path, "foo.html")
|
||||
|
||||
# folder with index.html should return folder/index.html
|
||||
path = handler.get_path_that_exists('bar' + suffix)
|
||||
self.assertEqual(path, 'bar/index.html')
|
||||
path = handler.get_path_that_exists("bar" + suffix)
|
||||
self.assertEqual(path, "bar/index.html")
|
||||
|
||||
# folder without index.html should return same as input
|
||||
path = handler.get_path_that_exists('baz' + suffix)
|
||||
self.assertEqual(path, 'baz' + suffix)
|
||||
path = handler.get_path_that_exists("baz" + suffix)
|
||||
self.assertEqual(path, "baz" + suffix)
|
||||
|
||||
# not existing path should return None
|
||||
path = handler.get_path_that_exists('quux' + suffix)
|
||||
path = handler.get_path_that_exists("quux" + suffix)
|
||||
self.assertIsNone(path)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ import os
|
|||
from os.path import abspath, dirname, join
|
||||
|
||||
|
||||
from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME,
|
||||
_printf_s_to_format_field,
|
||||
configure_settings,
|
||||
handle_deprecated_settings, read_settings)
|
||||
from pelican.settings import (
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_THEME,
|
||||
_printf_s_to_format_field,
|
||||
configure_settings,
|
||||
handle_deprecated_settings,
|
||||
read_settings,
|
||||
)
|
||||
from pelican.tests.support import unittest
|
||||
|
||||
|
||||
|
|
@ -16,40 +20,39 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
append new values to the settings (if any), and apply basic settings
|
||||
optimizations.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, 'default_conf.py')
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
self.settings = read_settings(default_conf)
|
||||
|
||||
def tearDown(self):
|
||||
locale.setlocale(locale.LC_ALL, self.old_locale)
|
||||
|
||||
def test_overwrite_existing_settings(self):
|
||||
self.assertEqual(self.settings.get('SITENAME'), "Alexis' log")
|
||||
self.assertEqual(
|
||||
self.settings.get('SITEURL'),
|
||||
'http://blog.notmyidea.org')
|
||||
self.assertEqual(self.settings.get("SITENAME"), "Alexis' log")
|
||||
self.assertEqual(self.settings.get("SITEURL"), "http://blog.notmyidea.org")
|
||||
|
||||
def test_keep_default_settings(self):
|
||||
# Keep default settings if not defined.
|
||||
self.assertEqual(
|
||||
self.settings.get('DEFAULT_CATEGORY'),
|
||||
DEFAULT_CONFIG['DEFAULT_CATEGORY'])
|
||||
self.settings.get("DEFAULT_CATEGORY"), DEFAULT_CONFIG["DEFAULT_CATEGORY"]
|
||||
)
|
||||
|
||||
def test_dont_copy_small_keys(self):
|
||||
# Do not copy keys not in caps.
|
||||
self.assertNotIn('foobar', self.settings)
|
||||
self.assertNotIn("foobar", self.settings)
|
||||
|
||||
def test_read_empty_settings(self):
|
||||
# Ensure an empty settings file results in default settings.
|
||||
settings = read_settings(None)
|
||||
expected = copy.deepcopy(DEFAULT_CONFIG)
|
||||
# Added by configure settings
|
||||
expected['FEED_DOMAIN'] = ''
|
||||
expected['ARTICLE_EXCLUDES'] = ['pages']
|
||||
expected['PAGE_EXCLUDES'] = ['']
|
||||
expected["FEED_DOMAIN"] = ""
|
||||
expected["ARTICLE_EXCLUDES"] = ["pages"]
|
||||
expected["PAGE_EXCLUDES"] = [""]
|
||||
self.maxDiff = None
|
||||
self.assertDictEqual(settings, expected)
|
||||
|
||||
|
|
@ -57,250 +60,265 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
# Make sure that the results from one settings call doesn't
|
||||
# effect past or future instances.
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, 'default_conf.py')
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
settings = read_settings(default_conf)
|
||||
settings['SITEURL'] = 'new-value'
|
||||
settings["SITEURL"] = "new-value"
|
||||
new_settings = read_settings(default_conf)
|
||||
self.assertNotEqual(new_settings['SITEURL'], settings['SITEURL'])
|
||||
self.assertNotEqual(new_settings["SITEURL"], settings["SITEURL"])
|
||||
|
||||
def test_defaults_not_overwritten(self):
|
||||
# This assumes 'SITENAME': 'A Pelican Blog'
|
||||
settings = read_settings(None)
|
||||
settings['SITENAME'] = 'Not a Pelican Blog'
|
||||
self.assertNotEqual(settings['SITENAME'], DEFAULT_CONFIG['SITENAME'])
|
||||
settings["SITENAME"] = "Not a Pelican Blog"
|
||||
self.assertNotEqual(settings["SITENAME"], DEFAULT_CONFIG["SITENAME"])
|
||||
|
||||
def test_static_path_settings_safety(self):
|
||||
# Disallow static paths from being strings
|
||||
settings = {
|
||||
'STATIC_PATHS': 'foo/bar',
|
||||
'THEME_STATIC_PATHS': 'bar/baz',
|
||||
"STATIC_PATHS": "foo/bar",
|
||||
"THEME_STATIC_PATHS": "bar/baz",
|
||||
# These 4 settings are required to run configure_settings
|
||||
'PATH': '.',
|
||||
'THEME': DEFAULT_THEME,
|
||||
'SITEURL': 'http://blog.notmyidea.org/',
|
||||
'LOCALE': '',
|
||||
"PATH": ".",
|
||||
"THEME": DEFAULT_THEME,
|
||||
"SITEURL": "http://blog.notmyidea.org/",
|
||||
"LOCALE": "",
|
||||
}
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings["STATIC_PATHS"], DEFAULT_CONFIG["STATIC_PATHS"])
|
||||
self.assertEqual(
|
||||
settings['STATIC_PATHS'],
|
||||
DEFAULT_CONFIG['STATIC_PATHS'])
|
||||
self.assertEqual(
|
||||
settings['THEME_STATIC_PATHS'],
|
||||
DEFAULT_CONFIG['THEME_STATIC_PATHS'])
|
||||
settings["THEME_STATIC_PATHS"], DEFAULT_CONFIG["THEME_STATIC_PATHS"]
|
||||
)
|
||||
|
||||
def test_configure_settings(self):
|
||||
# Manipulations to settings should be applied correctly.
|
||||
settings = {
|
||||
'SITEURL': 'http://blog.notmyidea.org/',
|
||||
'LOCALE': '',
|
||||
'PATH': os.curdir,
|
||||
'THEME': DEFAULT_THEME,
|
||||
"SITEURL": "http://blog.notmyidea.org/",
|
||||
"LOCALE": "",
|
||||
"PATH": os.curdir,
|
||||
"THEME": DEFAULT_THEME,
|
||||
}
|
||||
configure_settings(settings)
|
||||
|
||||
# SITEURL should not have a trailing slash
|
||||
self.assertEqual(settings['SITEURL'], 'http://blog.notmyidea.org')
|
||||
self.assertEqual(settings["SITEURL"], "http://blog.notmyidea.org")
|
||||
|
||||
# FEED_DOMAIN, if undefined, should default to SITEURL
|
||||
self.assertEqual(settings['FEED_DOMAIN'], 'http://blog.notmyidea.org')
|
||||
self.assertEqual(settings["FEED_DOMAIN"], "http://blog.notmyidea.org")
|
||||
|
||||
settings['FEED_DOMAIN'] = 'http://feeds.example.com'
|
||||
settings["FEED_DOMAIN"] = "http://feeds.example.com"
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings['FEED_DOMAIN'], 'http://feeds.example.com')
|
||||
self.assertEqual(settings["FEED_DOMAIN"], "http://feeds.example.com")
|
||||
|
||||
def test_theme_settings_exceptions(self):
|
||||
settings = self.settings
|
||||
|
||||
# Check that theme lookup in "pelican/themes" functions as expected
|
||||
settings['THEME'] = os.path.split(settings['THEME'])[1]
|
||||
settings["THEME"] = os.path.split(settings["THEME"])[1]
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings['THEME'], DEFAULT_THEME)
|
||||
self.assertEqual(settings["THEME"], DEFAULT_THEME)
|
||||
|
||||
# Check that non-existent theme raises exception
|
||||
settings['THEME'] = 'foo'
|
||||
settings["THEME"] = "foo"
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
def test_deprecated_dir_setting(self):
|
||||
settings = self.settings
|
||||
|
||||
settings['ARTICLE_DIR'] = 'foo'
|
||||
settings['PAGE_DIR'] = 'bar'
|
||||
settings["ARTICLE_DIR"] = "foo"
|
||||
settings["PAGE_DIR"] = "bar"
|
||||
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
||||
self.assertEqual(settings['ARTICLE_PATHS'], ['foo'])
|
||||
self.assertEqual(settings['PAGE_PATHS'], ['bar'])
|
||||
self.assertEqual(settings["ARTICLE_PATHS"], ["foo"])
|
||||
self.assertEqual(settings["PAGE_PATHS"], ["bar"])
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
settings['ARTICLE_DIR']
|
||||
settings['PAGE_DIR']
|
||||
settings["ARTICLE_DIR"]
|
||||
settings["PAGE_DIR"]
|
||||
|
||||
def test_default_encoding(self):
|
||||
# Test that the user locale is set if not specified in settings
|
||||
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
# empty string = user system locale
|
||||
self.assertEqual(self.settings['LOCALE'], [''])
|
||||
self.assertEqual(self.settings["LOCALE"], [""])
|
||||
|
||||
configure_settings(self.settings)
|
||||
lc_time = locale.getlocale(locale.LC_TIME) # should be set to user locale
|
||||
|
||||
# explicitly set locale to user pref and test
|
||||
locale.setlocale(locale.LC_TIME, '')
|
||||
locale.setlocale(locale.LC_TIME, "")
|
||||
self.assertEqual(lc_time, locale.getlocale(locale.LC_TIME))
|
||||
|
||||
def test_invalid_settings_throw_exception(self):
|
||||
# Test that the path name is valid
|
||||
|
||||
# test that 'PATH' is set
|
||||
settings = {
|
||||
}
|
||||
settings = {}
|
||||
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
# Test that 'PATH' is valid
|
||||
settings['PATH'] = ''
|
||||
settings["PATH"] = ""
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
# Test nonexistent THEME
|
||||
settings['PATH'] = os.curdir
|
||||
settings['THEME'] = 'foo'
|
||||
settings["PATH"] = os.curdir
|
||||
settings["THEME"] = "foo"
|
||||
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
def test__printf_s_to_format_field(self):
|
||||
for s in ('%s', '{%s}', '{%s'):
|
||||
option = 'foo/{}/bar.baz'.format(s)
|
||||
result = _printf_s_to_format_field(option, 'slug')
|
||||
expected = option % 'qux'
|
||||
found = result.format(slug='qux')
|
||||
for s in ("%s", "{%s}", "{%s"):
|
||||
option = "foo/{}/bar.baz".format(s)
|
||||
result = _printf_s_to_format_field(option, "slug")
|
||||
expected = option % "qux"
|
||||
found = result.format(slug="qux")
|
||||
self.assertEqual(expected, found)
|
||||
|
||||
def test_deprecated_extra_templates_paths(self):
|
||||
settings = self.settings
|
||||
settings['EXTRA_TEMPLATES_PATHS'] = ['/foo/bar', '/ha']
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/foo/bar", "/ha"]
|
||||
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
||||
self.assertEqual(settings['THEME_TEMPLATES_OVERRIDES'],
|
||||
['/foo/bar', '/ha'])
|
||||
self.assertNotIn('EXTRA_TEMPLATES_PATHS', settings)
|
||||
self.assertEqual(settings["THEME_TEMPLATES_OVERRIDES"], ["/foo/bar", "/ha"])
|
||||
self.assertNotIn("EXTRA_TEMPLATES_PATHS", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates(self):
|
||||
settings = self.settings
|
||||
settings['PAGINATED_DIRECT_TEMPLATES'] = ['index', 'archives']
|
||||
settings['PAGINATED_TEMPLATES'] = {'index': 10, 'category': None}
|
||||
settings["PAGINATED_DIRECT_TEMPLATES"] = ["index", "archives"]
|
||||
settings["PAGINATED_TEMPLATES"] = {"index": 10, "category": None}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(settings['PAGINATED_TEMPLATES'],
|
||||
{'index': 10, 'category': None, 'archives': None})
|
||||
self.assertNotIn('PAGINATED_DIRECT_TEMPLATES', settings)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{"index": 10, "category": None, "archives": None},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# PAGINATED_DIRECT_TEMPLATES defined but no PAGINATED_TEMPLATES.
|
||||
settings = read_settings(None, override={
|
||||
'PAGINATED_DIRECT_TEMPLATES': ['index', 'archives']
|
||||
})
|
||||
self.assertEqual(settings['PAGINATED_TEMPLATES'], {
|
||||
'archives': None,
|
||||
'author': None,
|
||||
'index': None,
|
||||
'category': None,
|
||||
'tag': None})
|
||||
self.assertNotIn('PAGINATED_DIRECT_TEMPLATES', settings)
|
||||
settings = read_settings(
|
||||
None, override={"PAGINATED_DIRECT_TEMPLATES": ["index", "archives"]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{
|
||||
"archives": None,
|
||||
"author": None,
|
||||
"index": None,
|
||||
"category": None,
|
||||
"tag": None,
|
||||
},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_theme_and_extra_templates_exception(self):
|
||||
settings = self.settings
|
||||
settings['EXTRA_TEMPLATES_PATHS'] = ['/ha']
|
||||
settings['THEME_TEMPLATES_OVERRIDES'] = ['/foo/bar']
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/ha"]
|
||||
settings["THEME_TEMPLATES_OVERRIDES"] = ["/foo/bar"]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_slug_and_slug_regex_substitutions_exception(self):
|
||||
settings = {}
|
||||
settings['SLUG_REGEX_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
||||
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_deprecated_slug_substitutions(self):
|
||||
default_slug_regex_subs = self.settings['SLUG_REGEX_SUBSTITUTIONS']
|
||||
default_slug_regex_subs = self.settings["SLUG_REGEX_SUBSTITUTIONS"]
|
||||
|
||||
# If no deprecated setting is set, don't set new ones
|
||||
settings = {}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn('SLUG_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertNotIn('TAG_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertNotIn('CATEGORY_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertNotIn('AUTHOR_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("AUTHOR_REGEX_SUBSTITUTIONS", settings)
|
||||
|
||||
# If SLUG_SUBSTITUTIONS is set, set {SLUG, AUTHOR}_REGEX_SUBSTITUTIONS
|
||||
# correctly, don't set {CATEGORY, TAG}_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(settings.get('SLUG_REGEX_SUBSTITUTIONS'),
|
||||
[(r'C\+\+', 'cpp')] + default_slug_regex_subs)
|
||||
self.assertNotIn('TAG_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertNotIn('CATEGORY_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertEqual(settings.get('AUTHOR_REGEX_SUBSTITUTIONS'),
|
||||
default_slug_regex_subs)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings.get("AUTHOR_REGEX_SUBSTITUTIONS"), default_slug_regex_subs
|
||||
)
|
||||
|
||||
# If {CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly, don't set
|
||||
# SLUG_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
||||
settings['CATEGORY_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
||||
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov')]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn('SLUG_REGEX_SUBSTITUTIONS', settings)
|
||||
self.assertEqual(settings['TAG_REGEX_SUBSTITUTIONS'],
|
||||
[(r'C\#', 'csharp')] + default_slug_regex_subs)
|
||||
self.assertEqual(settings['CATEGORY_REGEX_SUBSTITUTIONS'],
|
||||
[(r'C\#', 'csharp')] + default_slug_regex_subs)
|
||||
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
||||
[(r'Alexander\ Todorov', 'atodorov')] +
|
||||
default_slug_regex_subs)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# If {SLUG, CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {SLUG, CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly
|
||||
settings = {}
|
||||
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
||||
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
||||
settings['CATEGORY_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
||||
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov')]
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(settings['TAG_REGEX_SUBSTITUTIONS'],
|
||||
[(r'C\+\+', 'cpp')] + [(r'C\#', 'csharp')] +
|
||||
default_slug_regex_subs)
|
||||
self.assertEqual(settings['CATEGORY_REGEX_SUBSTITUTIONS'],
|
||||
[(r'C\+\+', 'cpp')] + [(r'C\#', 'csharp')] +
|
||||
default_slug_regex_subs)
|
||||
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
||||
[(r'Alexander\ Todorov', 'atodorov')] +
|
||||
default_slug_regex_subs)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# Handle old 'skip' flags correctly
|
||||
settings = {}
|
||||
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp', True)]
|
||||
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov',
|
||||
False)]
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp", True)]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov", False)]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(settings.get('SLUG_REGEX_SUBSTITUTIONS'),
|
||||
[(r'C\+\+', 'cpp')] +
|
||||
[(r'(?u)\A\s*', ''), (r'(?u)\s*\Z', '')])
|
||||
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
||||
[(r'Alexander\ Todorov', 'atodorov')] +
|
||||
default_slug_regex_subs)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + [(r"(?u)\A\s*", ""), (r"(?u)\s*\Z", "")],
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
def test_deprecated_slug_substitutions_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# SLUG_SUBSTITUTIONS defined but no SLUG_REGEX_SUBSTITUTIONS.
|
||||
settings = read_settings(None, override={
|
||||
'SLUG_SUBSTITUTIONS': [('C++', 'cpp')]
|
||||
})
|
||||
self.assertEqual(settings['SLUG_REGEX_SUBSTITUTIONS'],
|
||||
[(r'C\+\+', 'cpp')] +
|
||||
self.settings['SLUG_REGEX_SUBSTITUTIONS'])
|
||||
self.assertNotIn('SLUG_SUBSTITUTIONS', settings)
|
||||
settings = read_settings(
|
||||
None, override={"SLUG_SUBSTITUTIONS": [("C++", "cpp")]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + self.settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
)
|
||||
self.assertNotIn("SLUG_SUBSTITUTIONS", settings)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from pelican.tests.support import unittest
|
|||
|
||||
|
||||
class TestSuiteTest(unittest.TestCase):
|
||||
|
||||
def test_error_on_warning(self):
|
||||
with self.assertRaises(UserWarning):
|
||||
warnings.warn('test warning')
|
||||
warnings.warn("test warning")
|
||||
|
|
|
|||
|
|
@ -5,22 +5,22 @@ from pelican.urlwrappers import Author, Category, Tag, URLWrapper
|
|||
class TestURLWrapper(unittest.TestCase):
|
||||
def test_ordering(self):
|
||||
# URLWrappers are sorted by name
|
||||
wrapper_a = URLWrapper(name='first', settings={})
|
||||
wrapper_b = URLWrapper(name='last', settings={})
|
||||
wrapper_a = URLWrapper(name="first", settings={})
|
||||
wrapper_b = URLWrapper(name="last", settings={})
|
||||
self.assertFalse(wrapper_a > wrapper_b)
|
||||
self.assertFalse(wrapper_a >= wrapper_b)
|
||||
self.assertFalse(wrapper_a == wrapper_b)
|
||||
self.assertTrue(wrapper_a != wrapper_b)
|
||||
self.assertTrue(wrapper_a <= wrapper_b)
|
||||
self.assertTrue(wrapper_a < wrapper_b)
|
||||
wrapper_b.name = 'first'
|
||||
wrapper_b.name = "first"
|
||||
self.assertFalse(wrapper_a > wrapper_b)
|
||||
self.assertTrue(wrapper_a >= wrapper_b)
|
||||
self.assertTrue(wrapper_a == wrapper_b)
|
||||
self.assertFalse(wrapper_a != wrapper_b)
|
||||
self.assertTrue(wrapper_a <= wrapper_b)
|
||||
self.assertFalse(wrapper_a < wrapper_b)
|
||||
wrapper_a.name = 'last'
|
||||
wrapper_a.name = "last"
|
||||
self.assertTrue(wrapper_a > wrapper_b)
|
||||
self.assertTrue(wrapper_a >= wrapper_b)
|
||||
self.assertFalse(wrapper_a == wrapper_b)
|
||||
|
|
@ -29,57 +29,68 @@ class TestURLWrapper(unittest.TestCase):
|
|||
self.assertFalse(wrapper_a < wrapper_b)
|
||||
|
||||
def test_equality(self):
|
||||
tag = Tag('test', settings={})
|
||||
cat = Category('test', settings={})
|
||||
author = Author('test', settings={})
|
||||
tag = Tag("test", settings={})
|
||||
cat = Category("test", settings={})
|
||||
author = Author("test", settings={})
|
||||
|
||||
# same name, but different class
|
||||
self.assertNotEqual(tag, cat)
|
||||
self.assertNotEqual(tag, author)
|
||||
|
||||
# should be equal vs text representing the same name
|
||||
self.assertEqual(tag, 'test')
|
||||
self.assertEqual(tag, "test")
|
||||
|
||||
# should not be equal vs binary
|
||||
self.assertNotEqual(tag, b'test')
|
||||
self.assertNotEqual(tag, b"test")
|
||||
|
||||
# Tags describing the same should be equal
|
||||
tag_equal = Tag('Test', settings={})
|
||||
tag_equal = Tag("Test", settings={})
|
||||
self.assertEqual(tag, tag_equal)
|
||||
|
||||
# Author describing the same should be equal
|
||||
author_equal = Author('Test', settings={})
|
||||
author_equal = Author("Test", settings={})
|
||||
self.assertEqual(author, author_equal)
|
||||
|
||||
cat_ascii = Category('指導書', settings={})
|
||||
self.assertEqual(cat_ascii, 'zhi dao shu')
|
||||
cat_ascii = Category("指導書", settings={})
|
||||
self.assertEqual(cat_ascii, "zhi dao shu")
|
||||
|
||||
def test_slugify_with_substitutions_and_dots(self):
|
||||
tag = Tag('Tag Dot', settings={'TAG_REGEX_SUBSTITUTIONS': [
|
||||
('Tag Dot', 'tag.dot'),
|
||||
]})
|
||||
cat = Category('Category Dot',
|
||||
settings={'CATEGORY_REGEX_SUBSTITUTIONS': [
|
||||
('Category Dot', 'cat.dot'),
|
||||
]})
|
||||
tag = Tag(
|
||||
"Tag Dot",
|
||||
settings={
|
||||
"TAG_REGEX_SUBSTITUTIONS": [
|
||||
("Tag Dot", "tag.dot"),
|
||||
]
|
||||
},
|
||||
)
|
||||
cat = Category(
|
||||
"Category Dot",
|
||||
settings={
|
||||
"CATEGORY_REGEX_SUBSTITUTIONS": [
|
||||
("Category Dot", "cat.dot"),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(tag.slug, 'tag.dot')
|
||||
self.assertEqual(cat.slug, 'cat.dot')
|
||||
self.assertEqual(tag.slug, "tag.dot")
|
||||
self.assertEqual(cat.slug, "cat.dot")
|
||||
|
||||
def test_author_slug_substitutions(self):
|
||||
settings = {'AUTHOR_REGEX_SUBSTITUTIONS': [
|
||||
('Alexander Todorov', 'atodorov'),
|
||||
('Krasimir Tsonev', 'krasimir'),
|
||||
(r'[^\w\s-]', ''),
|
||||
(r'(?u)\A\s*', ''),
|
||||
(r'(?u)\s*\Z', ''),
|
||||
(r'[-\s]+', '-'),
|
||||
]}
|
||||
settings = {
|
||||
"AUTHOR_REGEX_SUBSTITUTIONS": [
|
||||
("Alexander Todorov", "atodorov"),
|
||||
("Krasimir Tsonev", "krasimir"),
|
||||
(r"[^\w\s-]", ""),
|
||||
(r"(?u)\A\s*", ""),
|
||||
(r"(?u)\s*\Z", ""),
|
||||
(r"[-\s]+", "-"),
|
||||
]
|
||||
}
|
||||
|
||||
author1 = Author('Mr. Senko', settings=settings)
|
||||
author2 = Author('Alexander Todorov', settings=settings)
|
||||
author3 = Author('Krasimir Tsonev', settings=settings)
|
||||
author1 = Author("Mr. Senko", settings=settings)
|
||||
author2 = Author("Alexander Todorov", settings=settings)
|
||||
author3 = Author("Krasimir Tsonev", settings=settings)
|
||||
|
||||
self.assertEqual(author1.slug, 'mr-senko')
|
||||
self.assertEqual(author2.slug, 'atodorov')
|
||||
self.assertEqual(author3.slug, 'krasimir')
|
||||
self.assertEqual(author1.slug, "mr-senko")
|
||||
self.assertEqual(author2.slug, "atodorov")
|
||||
self.assertEqual(author3.slug, "krasimir")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,7 @@ except ImportError:
|
|||
|
||||
try:
|
||||
import tzlocal
|
||||
|
||||
if hasattr(tzlocal.get_localzone(), "zone"):
|
||||
_DEFAULT_TIMEZONE = tzlocal.get_localzone().zone
|
||||
else:
|
||||
|
|
@ -28,55 +29,51 @@ except ModuleNotFoundError:
|
|||
|
||||
from pelican import __version__
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
try:
|
||||
_DEFAULT_LANGUAGE = locale.getlocale()[0]
|
||||
except ValueError:
|
||||
# Don't fail on macosx: "unknown locale: UTF-8"
|
||||
_DEFAULT_LANGUAGE = None
|
||||
if _DEFAULT_LANGUAGE is None:
|
||||
_DEFAULT_LANGUAGE = 'en'
|
||||
_DEFAULT_LANGUAGE = "en"
|
||||
else:
|
||||
_DEFAULT_LANGUAGE = _DEFAULT_LANGUAGE.split('_')[0]
|
||||
_DEFAULT_LANGUAGE = _DEFAULT_LANGUAGE.split("_")[0]
|
||||
|
||||
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"templates")
|
||||
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
||||
_jinja_env = Environment(
|
||||
loader=FileSystemLoader(_TEMPLATES_DIR),
|
||||
trim_blocks=True,
|
||||
)
|
||||
|
||||
|
||||
_GITHUB_PAGES_BRANCHES = {
|
||||
'personal': 'main',
|
||||
'project': 'gh-pages'
|
||||
}
|
||||
_GITHUB_PAGES_BRANCHES = {"personal": "main", "project": "gh-pages"}
|
||||
|
||||
CONF = {
|
||||
'pelican': 'pelican',
|
||||
'pelicanopts': '',
|
||||
'basedir': os.curdir,
|
||||
'ftp_host': 'localhost',
|
||||
'ftp_user': 'anonymous',
|
||||
'ftp_target_dir': '/',
|
||||
'ssh_host': 'localhost',
|
||||
'ssh_port': 22,
|
||||
'ssh_user': 'root',
|
||||
'ssh_target_dir': '/var/www',
|
||||
's3_bucket': 'my_s3_bucket',
|
||||
'cloudfiles_username': 'my_rackspace_username',
|
||||
'cloudfiles_api_key': 'my_rackspace_api_key',
|
||||
'cloudfiles_container': 'my_cloudfiles_container',
|
||||
'dropbox_dir': '~/Dropbox/Public/',
|
||||
'github_pages_branch': _GITHUB_PAGES_BRANCHES['project'],
|
||||
'default_pagination': 10,
|
||||
'siteurl': '',
|
||||
'lang': _DEFAULT_LANGUAGE,
|
||||
'timezone': _DEFAULT_TIMEZONE
|
||||
"pelican": "pelican",
|
||||
"pelicanopts": "",
|
||||
"basedir": os.curdir,
|
||||
"ftp_host": "localhost",
|
||||
"ftp_user": "anonymous",
|
||||
"ftp_target_dir": "/",
|
||||
"ssh_host": "localhost",
|
||||
"ssh_port": 22,
|
||||
"ssh_user": "root",
|
||||
"ssh_target_dir": "/var/www",
|
||||
"s3_bucket": "my_s3_bucket",
|
||||
"cloudfiles_username": "my_rackspace_username",
|
||||
"cloudfiles_api_key": "my_rackspace_api_key",
|
||||
"cloudfiles_container": "my_cloudfiles_container",
|
||||
"dropbox_dir": "~/Dropbox/Public/",
|
||||
"github_pages_branch": _GITHUB_PAGES_BRANCHES["project"],
|
||||
"default_pagination": 10,
|
||||
"siteurl": "",
|
||||
"lang": _DEFAULT_LANGUAGE,
|
||||
"timezone": _DEFAULT_TIMEZONE,
|
||||
}
|
||||
|
||||
# url for list of valid timezones
|
||||
_TZ_URL = 'https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'
|
||||
_TZ_URL = "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
|
||||
|
||||
|
||||
# Create a 'marked' default path, to determine if someone has supplied
|
||||
|
|
@ -90,12 +87,12 @@ _DEFAULT_PATH = _DEFAULT_PATH_TYPE(os.curdir)
|
|||
|
||||
def ask(question, answer=str, default=None, length=None):
|
||||
if answer == str:
|
||||
r = ''
|
||||
r = ""
|
||||
while True:
|
||||
if default:
|
||||
r = input('> {} [{}] '.format(question, default))
|
||||
r = input("> {} [{}] ".format(question, default))
|
||||
else:
|
||||
r = input('> {} '.format(question))
|
||||
r = input("> {} ".format(question))
|
||||
|
||||
r = r.strip()
|
||||
|
||||
|
|
@ -104,10 +101,10 @@ def ask(question, answer=str, default=None, length=None):
|
|||
r = default
|
||||
break
|
||||
else:
|
||||
print('You must enter something')
|
||||
print("You must enter something")
|
||||
else:
|
||||
if length and len(r) != length:
|
||||
print('Entry must be {} characters long'.format(length))
|
||||
print("Entry must be {} characters long".format(length))
|
||||
else:
|
||||
break
|
||||
|
||||
|
|
@ -117,18 +114,18 @@ def ask(question, answer=str, default=None, length=None):
|
|||
r = None
|
||||
while True:
|
||||
if default is True:
|
||||
r = input('> {} (Y/n) '.format(question))
|
||||
r = input("> {} (Y/n) ".format(question))
|
||||
elif default is False:
|
||||
r = input('> {} (y/N) '.format(question))
|
||||
r = input("> {} (y/N) ".format(question))
|
||||
else:
|
||||
r = input('> {} (y/n) '.format(question))
|
||||
r = input("> {} (y/n) ".format(question))
|
||||
|
||||
r = r.strip().lower()
|
||||
|
||||
if r in ('y', 'yes'):
|
||||
if r in ("y", "yes"):
|
||||
r = True
|
||||
break
|
||||
elif r in ('n', 'no'):
|
||||
elif r in ("n", "no"):
|
||||
r = False
|
||||
break
|
||||
elif not r:
|
||||
|
|
@ -141,9 +138,9 @@ def ask(question, answer=str, default=None, length=None):
|
|||
r = None
|
||||
while True:
|
||||
if default:
|
||||
r = input('> {} [{}] '.format(question, default))
|
||||
r = input("> {} [{}] ".format(question, default))
|
||||
else:
|
||||
r = input('> {} '.format(question))
|
||||
r = input("> {} ".format(question))
|
||||
|
||||
r = r.strip()
|
||||
|
||||
|
|
@ -155,11 +152,10 @@ def ask(question, answer=str, default=None, length=None):
|
|||
r = int(r)
|
||||
break
|
||||
except ValueError:
|
||||
print('You must enter an integer')
|
||||
print("You must enter an integer")
|
||||
return r
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Argument `answer` must be str, bool, or integer')
|
||||
raise NotImplementedError("Argument `answer` must be str, bool, or integer")
|
||||
|
||||
|
||||
def ask_timezone(question, default, tzurl):
|
||||
|
|
@ -178,162 +174,227 @@ def ask_timezone(question, default, tzurl):
|
|||
|
||||
def render_jinja_template(tmpl_name: str, tmpl_vars: Mapping, target_path: str):
|
||||
try:
|
||||
with open(os.path.join(CONF['basedir'], target_path),
|
||||
'w', encoding='utf-8') as fd:
|
||||
with open(
|
||||
os.path.join(CONF["basedir"], target_path), "w", encoding="utf-8"
|
||||
) as fd:
|
||||
_template = _jinja_env.get_template(tmpl_name)
|
||||
fd.write(_template.render(**tmpl_vars))
|
||||
except OSError as e:
|
||||
print('Error: {}'.format(e))
|
||||
print("Error: {}".format(e))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="A kickstarter for Pelican",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('-p', '--path', default=_DEFAULT_PATH,
|
||||
help="The path to generate the blog into")
|
||||
parser.add_argument('-t', '--title', metavar="title",
|
||||
help='Set the title of the website')
|
||||
parser.add_argument('-a', '--author', metavar="author",
|
||||
help='Set the author name of the website')
|
||||
parser.add_argument('-l', '--lang', metavar="lang",
|
||||
help='Set the default web site language')
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--path", default=_DEFAULT_PATH, help="The path to generate the blog into"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", "--title", metavar="title", help="Set the title of the website"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a", "--author", metavar="author", help="Set the author name of the website"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", "--lang", metavar="lang", help="Set the default web site language"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print('''Welcome to pelican-quickstart v{v}.
|
||||
print(
|
||||
"""Welcome to pelican-quickstart v{v}.
|
||||
|
||||
This script will help you create a new Pelican-based website.
|
||||
|
||||
Please answer the following questions so this script can generate the files
|
||||
needed by Pelican.
|
||||
|
||||
'''.format(v=__version__))
|
||||
""".format(v=__version__)
|
||||
)
|
||||
|
||||
project = os.path.join(
|
||||
os.environ.get('VIRTUAL_ENV', os.curdir), '.project')
|
||||
no_path_was_specified = hasattr(args.path, 'is_default_path')
|
||||
project = os.path.join(os.environ.get("VIRTUAL_ENV", os.curdir), ".project")
|
||||
no_path_was_specified = hasattr(args.path, "is_default_path")
|
||||
if os.path.isfile(project) and no_path_was_specified:
|
||||
CONF['basedir'] = open(project).read().rstrip("\n")
|
||||
print('Using project associated with current virtual environment. '
|
||||
'Will save to:\n%s\n' % CONF['basedir'])
|
||||
CONF["basedir"] = open(project).read().rstrip("\n")
|
||||
print(
|
||||
"Using project associated with current virtual environment. "
|
||||
"Will save to:\n%s\n" % CONF["basedir"]
|
||||
)
|
||||
else:
|
||||
CONF['basedir'] = os.path.abspath(os.path.expanduser(
|
||||
ask('Where do you want to create your new web site?',
|
||||
answer=str, default=args.path)))
|
||||
CONF["basedir"] = os.path.abspath(
|
||||
os.path.expanduser(
|
||||
ask(
|
||||
"Where do you want to create your new web site?",
|
||||
answer=str,
|
||||
default=args.path,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
CONF['sitename'] = ask('What will be the title of this web site?',
|
||||
answer=str, default=args.title)
|
||||
CONF['author'] = ask('Who will be the author of this web site?',
|
||||
answer=str, default=args.author)
|
||||
CONF['lang'] = ask('What will be the default language of this web site?',
|
||||
str, args.lang or CONF['lang'], 2)
|
||||
CONF["sitename"] = ask(
|
||||
"What will be the title of this web site?", answer=str, default=args.title
|
||||
)
|
||||
CONF["author"] = ask(
|
||||
"Who will be the author of this web site?", answer=str, default=args.author
|
||||
)
|
||||
CONF["lang"] = ask(
|
||||
"What will be the default language of this web site?",
|
||||
str,
|
||||
args.lang or CONF["lang"],
|
||||
2,
|
||||
)
|
||||
|
||||
if ask('Do you want to specify a URL prefix? e.g., https://example.com ',
|
||||
answer=bool, default=True):
|
||||
CONF['siteurl'] = ask('What is your URL prefix? (see '
|
||||
'above example; no trailing slash)',
|
||||
str, CONF['siteurl'])
|
||||
if ask(
|
||||
"Do you want to specify a URL prefix? e.g., https://example.com ",
|
||||
answer=bool,
|
||||
default=True,
|
||||
):
|
||||
CONF["siteurl"] = ask(
|
||||
"What is your URL prefix? (see " "above example; no trailing slash)",
|
||||
str,
|
||||
CONF["siteurl"],
|
||||
)
|
||||
|
||||
CONF['with_pagination'] = ask('Do you want to enable article pagination?',
|
||||
bool, bool(CONF['default_pagination']))
|
||||
CONF["with_pagination"] = ask(
|
||||
"Do you want to enable article pagination?",
|
||||
bool,
|
||||
bool(CONF["default_pagination"]),
|
||||
)
|
||||
|
||||
if CONF['with_pagination']:
|
||||
CONF['default_pagination'] = ask('How many articles per page '
|
||||
'do you want?',
|
||||
int, CONF['default_pagination'])
|
||||
if CONF["with_pagination"]:
|
||||
CONF["default_pagination"] = ask(
|
||||
"How many articles per page " "do you want?",
|
||||
int,
|
||||
CONF["default_pagination"],
|
||||
)
|
||||
else:
|
||||
CONF['default_pagination'] = False
|
||||
CONF["default_pagination"] = False
|
||||
|
||||
CONF['timezone'] = ask_timezone('What is your time zone?',
|
||||
CONF['timezone'], _TZ_URL)
|
||||
CONF["timezone"] = ask_timezone(
|
||||
"What is your time zone?", CONF["timezone"], _TZ_URL
|
||||
)
|
||||
|
||||
automation = ask('Do you want to generate a tasks.py/Makefile '
|
||||
'to automate generation and publishing?', bool, True)
|
||||
automation = ask(
|
||||
"Do you want to generate a tasks.py/Makefile "
|
||||
"to automate generation and publishing?",
|
||||
bool,
|
||||
True,
|
||||
)
|
||||
|
||||
if automation:
|
||||
if ask('Do you want to upload your website using FTP?',
|
||||
answer=bool, default=False):
|
||||
CONF['ftp'] = True,
|
||||
CONF['ftp_host'] = ask('What is the hostname of your FTP server?',
|
||||
str, CONF['ftp_host'])
|
||||
CONF['ftp_user'] = ask('What is your username on that server?',
|
||||
str, CONF['ftp_user'])
|
||||
CONF['ftp_target_dir'] = ask('Where do you want to put your '
|
||||
'web site on that server?',
|
||||
str, CONF['ftp_target_dir'])
|
||||
if ask('Do you want to upload your website using SSH?',
|
||||
answer=bool, default=False):
|
||||
CONF['ssh'] = True,
|
||||
CONF['ssh_host'] = ask('What is the hostname of your SSH server?',
|
||||
str, CONF['ssh_host'])
|
||||
CONF['ssh_port'] = ask('What is the port of your SSH server?',
|
||||
int, CONF['ssh_port'])
|
||||
CONF['ssh_user'] = ask('What is your username on that server?',
|
||||
str, CONF['ssh_user'])
|
||||
CONF['ssh_target_dir'] = ask('Where do you want to put your '
|
||||
'web site on that server?',
|
||||
str, CONF['ssh_target_dir'])
|
||||
if ask(
|
||||
"Do you want to upload your website using FTP?", answer=bool, default=False
|
||||
):
|
||||
CONF["ftp"] = (True,)
|
||||
CONF["ftp_host"] = ask(
|
||||
"What is the hostname of your FTP server?", str, CONF["ftp_host"]
|
||||
)
|
||||
CONF["ftp_user"] = ask(
|
||||
"What is your username on that server?", str, CONF["ftp_user"]
|
||||
)
|
||||
CONF["ftp_target_dir"] = ask(
|
||||
"Where do you want to put your " "web site on that server?",
|
||||
str,
|
||||
CONF["ftp_target_dir"],
|
||||
)
|
||||
if ask(
|
||||
"Do you want to upload your website using SSH?", answer=bool, default=False
|
||||
):
|
||||
CONF["ssh"] = (True,)
|
||||
CONF["ssh_host"] = ask(
|
||||
"What is the hostname of your SSH server?", str, CONF["ssh_host"]
|
||||
)
|
||||
CONF["ssh_port"] = ask(
|
||||
"What is the port of your SSH server?", int, CONF["ssh_port"]
|
||||
)
|
||||
CONF["ssh_user"] = ask(
|
||||
"What is your username on that server?", str, CONF["ssh_user"]
|
||||
)
|
||||
CONF["ssh_target_dir"] = ask(
|
||||
"Where do you want to put your " "web site on that server?",
|
||||
str,
|
||||
CONF["ssh_target_dir"],
|
||||
)
|
||||
|
||||
if ask('Do you want to upload your website using Dropbox?',
|
||||
answer=bool, default=False):
|
||||
CONF['dropbox'] = True,
|
||||
CONF['dropbox_dir'] = ask('Where is your Dropbox directory?',
|
||||
str, CONF['dropbox_dir'])
|
||||
if ask(
|
||||
"Do you want to upload your website using Dropbox?",
|
||||
answer=bool,
|
||||
default=False,
|
||||
):
|
||||
CONF["dropbox"] = (True,)
|
||||
CONF["dropbox_dir"] = ask(
|
||||
"Where is your Dropbox directory?", str, CONF["dropbox_dir"]
|
||||
)
|
||||
|
||||
if ask('Do you want to upload your website using S3?',
|
||||
answer=bool, default=False):
|
||||
CONF['s3'] = True,
|
||||
CONF['s3_bucket'] = ask('What is the name of your S3 bucket?',
|
||||
str, CONF['s3_bucket'])
|
||||
if ask(
|
||||
"Do you want to upload your website using S3?", answer=bool, default=False
|
||||
):
|
||||
CONF["s3"] = (True,)
|
||||
CONF["s3_bucket"] = ask(
|
||||
"What is the name of your S3 bucket?", str, CONF["s3_bucket"]
|
||||
)
|
||||
|
||||
if ask('Do you want to upload your website using '
|
||||
'Rackspace Cloud Files?', answer=bool, default=False):
|
||||
CONF['cloudfiles'] = True,
|
||||
CONF['cloudfiles_username'] = ask('What is your Rackspace '
|
||||
'Cloud username?', str,
|
||||
CONF['cloudfiles_username'])
|
||||
CONF['cloudfiles_api_key'] = ask('What is your Rackspace '
|
||||
'Cloud API key?', str,
|
||||
CONF['cloudfiles_api_key'])
|
||||
CONF['cloudfiles_container'] = ask('What is the name of your '
|
||||
'Cloud Files container?',
|
||||
str,
|
||||
CONF['cloudfiles_container'])
|
||||
if ask(
|
||||
"Do you want to upload your website using " "Rackspace Cloud Files?",
|
||||
answer=bool,
|
||||
default=False,
|
||||
):
|
||||
CONF["cloudfiles"] = (True,)
|
||||
CONF["cloudfiles_username"] = ask(
|
||||
"What is your Rackspace " "Cloud username?",
|
||||
str,
|
||||
CONF["cloudfiles_username"],
|
||||
)
|
||||
CONF["cloudfiles_api_key"] = ask(
|
||||
"What is your Rackspace " "Cloud API key?",
|
||||
str,
|
||||
CONF["cloudfiles_api_key"],
|
||||
)
|
||||
CONF["cloudfiles_container"] = ask(
|
||||
"What is the name of your " "Cloud Files container?",
|
||||
str,
|
||||
CONF["cloudfiles_container"],
|
||||
)
|
||||
|
||||
if ask('Do you want to upload your website using GitHub Pages?',
|
||||
answer=bool, default=False):
|
||||
CONF['github'] = True,
|
||||
if ask('Is this your personal page (username.github.io)?',
|
||||
answer=bool, default=False):
|
||||
CONF['github_pages_branch'] = \
|
||||
_GITHUB_PAGES_BRANCHES['personal']
|
||||
if ask(
|
||||
"Do you want to upload your website using GitHub Pages?",
|
||||
answer=bool,
|
||||
default=False,
|
||||
):
|
||||
CONF["github"] = (True,)
|
||||
if ask(
|
||||
"Is this your personal page (username.github.io)?",
|
||||
answer=bool,
|
||||
default=False,
|
||||
):
|
||||
CONF["github_pages_branch"] = _GITHUB_PAGES_BRANCHES["personal"]
|
||||
else:
|
||||
CONF['github_pages_branch'] = \
|
||||
_GITHUB_PAGES_BRANCHES['project']
|
||||
CONF["github_pages_branch"] = _GITHUB_PAGES_BRANCHES["project"]
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.join(CONF['basedir'], 'content'))
|
||||
os.makedirs(os.path.join(CONF["basedir"], "content"))
|
||||
except OSError as e:
|
||||
print('Error: {}'.format(e))
|
||||
print("Error: {}".format(e))
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.join(CONF['basedir'], 'output'))
|
||||
os.makedirs(os.path.join(CONF["basedir"], "output"))
|
||||
except OSError as e:
|
||||
print('Error: {}'.format(e))
|
||||
print("Error: {}".format(e))
|
||||
|
||||
conf_python = dict()
|
||||
for key, value in CONF.items():
|
||||
conf_python[key] = repr(value)
|
||||
render_jinja_template('pelicanconf.py.jinja2', conf_python, 'pelicanconf.py')
|
||||
render_jinja_template("pelicanconf.py.jinja2", conf_python, "pelicanconf.py")
|
||||
|
||||
render_jinja_template('publishconf.py.jinja2', CONF, 'publishconf.py')
|
||||
render_jinja_template("publishconf.py.jinja2", CONF, "publishconf.py")
|
||||
|
||||
if automation:
|
||||
render_jinja_template('tasks.py.jinja2', CONF, 'tasks.py')
|
||||
render_jinja_template('Makefile.jinja2', CONF, 'Makefile')
|
||||
render_jinja_template("tasks.py.jinja2", CONF, "tasks.py")
|
||||
render_jinja_template("Makefile.jinja2", CONF, "Makefile")
|
||||
|
||||
print('Done. Your new project is available at %s' % CONF['basedir'])
|
||||
print("Done. Your new project is available at %s" % CONF["basedir"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import sys
|
|||
|
||||
def err(msg, die=None):
|
||||
"""Print an error message and exits if an exit code is given"""
|
||||
sys.stderr.write(msg + '\n')
|
||||
sys.stderr.write(msg + "\n")
|
||||
if die:
|
||||
sys.exit(die if isinstance(die, int) else 1)
|
||||
|
||||
|
|
@ -16,62 +16,96 @@ def err(msg, die=None):
|
|||
try:
|
||||
import pelican
|
||||
except ImportError:
|
||||
err('Cannot import pelican.\nYou must '
|
||||
'install Pelican in order to run this script.',
|
||||
-1)
|
||||
err(
|
||||
"Cannot import pelican.\nYou must "
|
||||
"install Pelican in order to run this script.",
|
||||
-1,
|
||||
)
|
||||
|
||||
|
||||
global _THEMES_PATH
|
||||
_THEMES_PATH = os.path.join(
|
||||
os.path.dirname(
|
||||
os.path.abspath(pelican.__file__)
|
||||
),
|
||||
'themes'
|
||||
os.path.dirname(os.path.abspath(pelican.__file__)), "themes"
|
||||
)
|
||||
|
||||
__version__ = '0.2'
|
||||
_BUILTIN_THEMES = ['simple', 'notmyidea']
|
||||
__version__ = "0.2"
|
||||
_BUILTIN_THEMES = ["simple", "notmyidea"]
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""Install themes for Pelican""")
|
||||
parser = argparse.ArgumentParser(description="""Install themes for Pelican""")
|
||||
|
||||
excl = parser.add_mutually_exclusive_group()
|
||||
excl.add_argument(
|
||||
'-l', '--list', dest='action', action="store_const", const='list',
|
||||
help="Show the themes already installed and exit")
|
||||
"-l",
|
||||
"--list",
|
||||
dest="action",
|
||||
action="store_const",
|
||||
const="list",
|
||||
help="Show the themes already installed and exit",
|
||||
)
|
||||
excl.add_argument(
|
||||
'-p', '--path', dest='action', action="store_const", const='path',
|
||||
help="Show the themes path and exit")
|
||||
"-p",
|
||||
"--path",
|
||||
dest="action",
|
||||
action="store_const",
|
||||
const="path",
|
||||
help="Show the themes path and exit",
|
||||
)
|
||||
excl.add_argument(
|
||||
'-V', '--version', action='version',
|
||||
version='pelican-themes v{}'.format(__version__),
|
||||
help='Print the version of this script')
|
||||
"-V",
|
||||
"--version",
|
||||
action="version",
|
||||
version="pelican-themes v{}".format(__version__),
|
||||
help="Print the version of this script",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-i', '--install', dest='to_install', nargs='+', metavar="theme path",
|
||||
help='The themes to install')
|
||||
"-i",
|
||||
"--install",
|
||||
dest="to_install",
|
||||
nargs="+",
|
||||
metavar="theme path",
|
||||
help="The themes to install",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--remove', dest='to_remove', nargs='+', metavar="theme name",
|
||||
help='The themes to remove')
|
||||
"-r",
|
||||
"--remove",
|
||||
dest="to_remove",
|
||||
nargs="+",
|
||||
metavar="theme name",
|
||||
help="The themes to remove",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-U', '--upgrade', dest='to_upgrade', nargs='+',
|
||||
metavar="theme path", help='The themes to upgrade')
|
||||
"-U",
|
||||
"--upgrade",
|
||||
dest="to_upgrade",
|
||||
nargs="+",
|
||||
metavar="theme path",
|
||||
help="The themes to upgrade",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--symlink', dest='to_symlink', nargs='+', metavar="theme path",
|
||||
"-s",
|
||||
"--symlink",
|
||||
dest="to_symlink",
|
||||
nargs="+",
|
||||
metavar="theme path",
|
||||
help="Same as `--install', but create a symbolic link instead of "
|
||||
"copying the theme. Useful for theme development")
|
||||
"copying the theme. Useful for theme development",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--clean', dest='clean', action="store_true",
|
||||
help="Remove the broken symbolic links of the theme path")
|
||||
"-c",
|
||||
"--clean",
|
||||
dest="clean",
|
||||
action="store_true",
|
||||
help="Remove the broken symbolic links of the theme path",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose', dest='verbose',
|
||||
action="store_true",
|
||||
help="Verbose output")
|
||||
"-v", "--verbose", dest="verbose", action="store_true", help="Verbose output"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -79,46 +113,46 @@ def main():
|
|||
to_sym = args.to_symlink or args.clean
|
||||
|
||||
if args.action:
|
||||
if args.action == 'list':
|
||||
if args.action == "list":
|
||||
list_themes(args.verbose)
|
||||
elif args.action == 'path':
|
||||
elif args.action == "path":
|
||||
print(_THEMES_PATH)
|
||||
elif to_install or args.to_remove or to_sym:
|
||||
if args.to_remove:
|
||||
if args.verbose:
|
||||
print('Removing themes...')
|
||||
print("Removing themes...")
|
||||
|
||||
for i in args.to_remove:
|
||||
remove(i, v=args.verbose)
|
||||
|
||||
if args.to_install:
|
||||
if args.verbose:
|
||||
print('Installing themes...')
|
||||
print("Installing themes...")
|
||||
|
||||
for i in args.to_install:
|
||||
install(i, v=args.verbose)
|
||||
|
||||
if args.to_upgrade:
|
||||
if args.verbose:
|
||||
print('Upgrading themes...')
|
||||
print("Upgrading themes...")
|
||||
|
||||
for i in args.to_upgrade:
|
||||
install(i, v=args.verbose, u=True)
|
||||
|
||||
if args.to_symlink:
|
||||
if args.verbose:
|
||||
print('Linking themes...')
|
||||
print("Linking themes...")
|
||||
|
||||
for i in args.to_symlink:
|
||||
symlink(i, v=args.verbose)
|
||||
|
||||
if args.clean:
|
||||
if args.verbose:
|
||||
print('Cleaning the themes directory...')
|
||||
print("Cleaning the themes directory...")
|
||||
|
||||
clean(v=args.verbose)
|
||||
else:
|
||||
print('No argument given... exiting.')
|
||||
print("No argument given... exiting.")
|
||||
|
||||
|
||||
def themes():
|
||||
|
|
@ -142,7 +176,7 @@ def list_themes(v=False):
|
|||
if v:
|
||||
print(theme_path + (" (symbolic link to `" + link_target + "')"))
|
||||
else:
|
||||
print(theme_path + '@')
|
||||
print(theme_path + "@")
|
||||
else:
|
||||
print(theme_path)
|
||||
|
||||
|
|
@ -150,51 +184,52 @@ def list_themes(v=False):
|
|||
def remove(theme_name, v=False):
|
||||
"""Removes a theme"""
|
||||
|
||||
theme_name = theme_name.replace('/', '')
|
||||
theme_name = theme_name.replace("/", "")
|
||||
target = os.path.join(_THEMES_PATH, theme_name)
|
||||
|
||||
if theme_name in _BUILTIN_THEMES:
|
||||
err(theme_name + ' is a builtin theme.\n'
|
||||
'You cannot remove a builtin theme with this script, '
|
||||
'remove it by hand if you want.')
|
||||
err(
|
||||
theme_name + " is a builtin theme.\n"
|
||||
"You cannot remove a builtin theme with this script, "
|
||||
"remove it by hand if you want."
|
||||
)
|
||||
elif os.path.islink(target):
|
||||
if v:
|
||||
print('Removing link `' + target + "'")
|
||||
print("Removing link `" + target + "'")
|
||||
os.remove(target)
|
||||
elif os.path.isdir(target):
|
||||
if v:
|
||||
print('Removing directory `' + target + "'")
|
||||
print("Removing directory `" + target + "'")
|
||||
shutil.rmtree(target)
|
||||
elif os.path.exists(target):
|
||||
err(target + ' : not a valid theme')
|
||||
err(target + " : not a valid theme")
|
||||
else:
|
||||
err(target + ' : no such file or directory')
|
||||
err(target + " : no such file or directory")
|
||||
|
||||
|
||||
def install(path, v=False, u=False):
|
||||
"""Installs a theme"""
|
||||
if not os.path.exists(path):
|
||||
err(path + ' : no such file or directory')
|
||||
err(path + " : no such file or directory")
|
||||
elif not os.path.isdir(path):
|
||||
err(path + ' : not a directory')
|
||||
err(path + " : not a directory")
|
||||
else:
|
||||
theme_name = os.path.basename(os.path.normpath(path))
|
||||
theme_path = os.path.join(_THEMES_PATH, theme_name)
|
||||
exists = os.path.exists(theme_path)
|
||||
if exists and not u:
|
||||
err(path + ' : already exists')
|
||||
err(path + " : already exists")
|
||||
elif exists:
|
||||
remove(theme_name, v)
|
||||
install(path, v)
|
||||
else:
|
||||
if v:
|
||||
print("Copying '{p}' to '{t}' ...".format(p=path,
|
||||
t=theme_path))
|
||||
print("Copying '{p}' to '{t}' ...".format(p=path, t=theme_path))
|
||||
try:
|
||||
shutil.copytree(path, theme_path)
|
||||
|
||||
try:
|
||||
if os.name == 'posix':
|
||||
if os.name == "posix":
|
||||
for root, dirs, files in os.walk(theme_path):
|
||||
for d in dirs:
|
||||
dname = os.path.join(root, d)
|
||||
|
|
@ -203,35 +238,41 @@ def install(path, v=False, u=False):
|
|||
fname = os.path.join(root, f)
|
||||
os.chmod(fname, 420) # 0o644
|
||||
except OSError as e:
|
||||
err("Cannot change permissions of files "
|
||||
"or directory in `{r}':\n{e}".format(r=theme_path,
|
||||
e=str(e)),
|
||||
die=False)
|
||||
err(
|
||||
"Cannot change permissions of files "
|
||||
"or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)),
|
||||
die=False,
|
||||
)
|
||||
except Exception as e:
|
||||
err("Cannot copy `{p}' to `{t}':\n{e}".format(
|
||||
p=path, t=theme_path, e=str(e)))
|
||||
err(
|
||||
"Cannot copy `{p}' to `{t}':\n{e}".format(
|
||||
p=path, t=theme_path, e=str(e)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def symlink(path, v=False):
|
||||
"""Symbolically link a theme"""
|
||||
if not os.path.exists(path):
|
||||
err(path + ' : no such file or directory')
|
||||
err(path + " : no such file or directory")
|
||||
elif not os.path.isdir(path):
|
||||
err(path + ' : not a directory')
|
||||
err(path + " : not a directory")
|
||||
else:
|
||||
theme_name = os.path.basename(os.path.normpath(path))
|
||||
theme_path = os.path.join(_THEMES_PATH, theme_name)
|
||||
if os.path.exists(theme_path):
|
||||
err(path + ' : already exists')
|
||||
err(path + " : already exists")
|
||||
else:
|
||||
if v:
|
||||
print("Linking `{p}' to `{t}' ...".format(
|
||||
p=path, t=theme_path))
|
||||
print("Linking `{p}' to `{t}' ...".format(p=path, t=theme_path))
|
||||
try:
|
||||
os.symlink(path, theme_path)
|
||||
except Exception as e:
|
||||
err("Cannot link `{p}' to `{t}':\n{e}".format(
|
||||
p=path, t=theme_path, e=str(e)))
|
||||
err(
|
||||
"Cannot link `{p}' to `{t}':\n{e}".format(
|
||||
p=path, t=theme_path, e=str(e)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_broken_link(path):
|
||||
|
|
@ -247,11 +288,11 @@ def clean(v=False):
|
|||
path = os.path.join(_THEMES_PATH, path)
|
||||
if os.path.islink(path) and is_broken_link(path):
|
||||
if v:
|
||||
print('Removing {}'.format(path))
|
||||
print("Removing {}".format(path))
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
print('Error: cannot remove {}'.format(path))
|
||||
print("Error: cannot remove {}".format(path))
|
||||
else:
|
||||
c += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -31,17 +31,16 @@ class URLWrapper:
|
|||
@property
|
||||
def slug(self):
|
||||
if self._slug is None:
|
||||
class_key = '{}_REGEX_SUBSTITUTIONS'.format(
|
||||
self.__class__.__name__.upper())
|
||||
class_key = "{}_REGEX_SUBSTITUTIONS".format(self.__class__.__name__.upper())
|
||||
regex_subs = self.settings.get(
|
||||
class_key,
|
||||
self.settings.get('SLUG_REGEX_SUBSTITUTIONS', []))
|
||||
preserve_case = self.settings.get('SLUGIFY_PRESERVE_CASE', False)
|
||||
class_key, self.settings.get("SLUG_REGEX_SUBSTITUTIONS", [])
|
||||
)
|
||||
preserve_case = self.settings.get("SLUGIFY_PRESERVE_CASE", False)
|
||||
self._slug = slugify(
|
||||
self.name,
|
||||
regex_subs=regex_subs,
|
||||
preserve_case=preserve_case,
|
||||
use_unicode=self.settings.get('SLUGIFY_USE_UNICODE', False)
|
||||
use_unicode=self.settings.get("SLUGIFY_USE_UNICODE", False),
|
||||
)
|
||||
return self._slug
|
||||
|
||||
|
|
@ -53,26 +52,26 @@ class URLWrapper:
|
|||
|
||||
def as_dict(self):
|
||||
d = self.__dict__
|
||||
d['name'] = self.name
|
||||
d['slug'] = self.slug
|
||||
d["name"] = self.name
|
||||
d["slug"] = self.slug
|
||||
return d
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.slug)
|
||||
|
||||
def _normalize_key(self, key):
|
||||
class_key = '{}_REGEX_SUBSTITUTIONS'.format(
|
||||
self.__class__.__name__.upper())
|
||||
class_key = "{}_REGEX_SUBSTITUTIONS".format(self.__class__.__name__.upper())
|
||||
regex_subs = self.settings.get(
|
||||
class_key,
|
||||
self.settings.get('SLUG_REGEX_SUBSTITUTIONS', []))
|
||||
use_unicode = self.settings.get('SLUGIFY_USE_UNICODE', False)
|
||||
preserve_case = self.settings.get('SLUGIFY_PRESERVE_CASE', False)
|
||||
class_key, self.settings.get("SLUG_REGEX_SUBSTITUTIONS", [])
|
||||
)
|
||||
use_unicode = self.settings.get("SLUGIFY_USE_UNICODE", False)
|
||||
preserve_case = self.settings.get("SLUGIFY_PRESERVE_CASE", False)
|
||||
return slugify(
|
||||
key,
|
||||
regex_subs=regex_subs,
|
||||
preserve_case=preserve_case,
|
||||
use_unicode=use_unicode)
|
||||
use_unicode=use_unicode,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
|
|
@ -99,7 +98,7 @@ class URLWrapper:
|
|||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{} {}>'.format(type(self).__name__, repr(self._name))
|
||||
return "<{} {}>".format(type(self).__name__, repr(self._name))
|
||||
|
||||
def _from_settings(self, key, get_page_name=False):
|
||||
"""Returns URL information as defined in settings.
|
||||
|
|
@ -114,7 +113,7 @@ class URLWrapper:
|
|||
if isinstance(value, pathlib.Path):
|
||||
value = str(value)
|
||||
if not isinstance(value, str):
|
||||
logger.warning('%s is set to %s', setting, value)
|
||||
logger.warning("%s is set to %s", setting, value)
|
||||
return value
|
||||
else:
|
||||
if get_page_name:
|
||||
|
|
@ -122,10 +121,11 @@ class URLWrapper:
|
|||
else:
|
||||
return value.format(**self.as_dict())
|
||||
|
||||
page_name = property(functools.partial(_from_settings, key='URL',
|
||||
get_page_name=True))
|
||||
url = property(functools.partial(_from_settings, key='URL'))
|
||||
save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
|
||||
page_name = property(
|
||||
functools.partial(_from_settings, key="URL", get_page_name=True)
|
||||
)
|
||||
url = property(functools.partial(_from_settings, key="URL"))
|
||||
save_as = property(functools.partial(_from_settings, key="SAVE_AS"))
|
||||
|
||||
|
||||
class Category(URLWrapper):
|
||||
|
|
|
|||
323
pelican/utils.py
323
pelican/utils.py
|
|
@ -32,38 +32,37 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def sanitised_join(base_directory, *parts):
|
||||
joined = posixize_path(
|
||||
os.path.abspath(os.path.join(base_directory, *parts)))
|
||||
joined = posixize_path(os.path.abspath(os.path.join(base_directory, *parts)))
|
||||
base = posixize_path(os.path.abspath(base_directory))
|
||||
if not joined.startswith(base):
|
||||
raise RuntimeError(
|
||||
"Attempted to break out of output directory to {}".format(
|
||||
joined
|
||||
)
|
||||
"Attempted to break out of output directory to {}".format(joined)
|
||||
)
|
||||
|
||||
return joined
|
||||
|
||||
|
||||
def strftime(date, date_format):
|
||||
'''
|
||||
"""
|
||||
Enhanced replacement for built-in strftime with zero stripping
|
||||
|
||||
This works by 'grabbing' possible format strings (those starting with %),
|
||||
formatting them with the date, stripping any leading zeros if - prefix is
|
||||
used and replacing formatted output back.
|
||||
'''
|
||||
"""
|
||||
|
||||
def strip_zeros(x):
|
||||
return x.lstrip('0') or '0'
|
||||
return x.lstrip("0") or "0"
|
||||
|
||||
# includes ISO date parameters added by Python 3.6
|
||||
c89_directives = 'aAbBcdfGHIjmMpSUuVwWxXyYzZ%'
|
||||
c89_directives = "aAbBcdfGHIjmMpSUuVwWxXyYzZ%"
|
||||
|
||||
# grab candidate format options
|
||||
format_options = '%[-]?.'
|
||||
format_options = "%[-]?."
|
||||
candidates = re.findall(format_options, date_format)
|
||||
|
||||
# replace candidates with placeholders for later % formatting
|
||||
template = re.sub(format_options, '%s', date_format)
|
||||
template = re.sub(format_options, "%s", date_format)
|
||||
|
||||
formatted_candidates = []
|
||||
for candidate in candidates:
|
||||
|
|
@ -72,7 +71,7 @@ def strftime(date, date_format):
|
|||
# check for '-' prefix
|
||||
if len(candidate) == 3:
|
||||
# '-' prefix
|
||||
candidate = '%{}'.format(candidate[-1])
|
||||
candidate = "%{}".format(candidate[-1])
|
||||
conversion = strip_zeros
|
||||
else:
|
||||
conversion = None
|
||||
|
|
@ -95,10 +94,10 @@ def strftime(date, date_format):
|
|||
|
||||
|
||||
class SafeDatetime(datetime.datetime):
|
||||
'''Subclass of datetime that works with utf-8 format strings on PY2'''
|
||||
"""Subclass of datetime that works with utf-8 format strings on PY2"""
|
||||
|
||||
def strftime(self, fmt, safe=True):
|
||||
'''Uses our custom strftime if supposed to be *safe*'''
|
||||
"""Uses our custom strftime if supposed to be *safe*"""
|
||||
if safe:
|
||||
return strftime(self, fmt)
|
||||
else:
|
||||
|
|
@ -106,22 +105,21 @@ class SafeDatetime(datetime.datetime):
|
|||
|
||||
|
||||
class DateFormatter:
|
||||
'''A date formatter object used as a jinja filter
|
||||
"""A date formatter object used as a jinja filter
|
||||
|
||||
Uses the `strftime` implementation and makes sure jinja uses the locale
|
||||
defined in LOCALE setting
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.locale = locale.setlocale(locale.LC_TIME)
|
||||
|
||||
def __call__(self, date, date_format):
|
||||
|
||||
# on OSX, encoding from LC_CTYPE determines the unicode output in PY3
|
||||
# make sure it's same as LC_TIME
|
||||
with temporary_locale(self.locale, locale.LC_TIME), \
|
||||
temporary_locale(self.locale, locale.LC_CTYPE):
|
||||
|
||||
with temporary_locale(self.locale, locale.LC_TIME), temporary_locale(
|
||||
self.locale, locale.LC_CTYPE
|
||||
):
|
||||
formatted = strftime(date, date_format)
|
||||
|
||||
return formatted
|
||||
|
|
@ -155,7 +153,7 @@ class memoized:
|
|||
return self.func.__doc__
|
||||
|
||||
def __get__(self, obj, objtype):
|
||||
'''Support instance methods.'''
|
||||
"""Support instance methods."""
|
||||
fn = partial(self.__call__, obj)
|
||||
fn.cache = self.cache
|
||||
return fn
|
||||
|
|
@ -177,17 +175,16 @@ def deprecated_attribute(old, new, since=None, remove=None, doc=None):
|
|||
Note that the decorator needs a dummy method to attach to, but the
|
||||
content of the dummy method is ignored.
|
||||
"""
|
||||
|
||||
def _warn():
|
||||
version = '.'.join(str(x) for x in since)
|
||||
message = ['{} has been deprecated since {}'.format(old, version)]
|
||||
version = ".".join(str(x) for x in since)
|
||||
message = ["{} has been deprecated since {}".format(old, version)]
|
||||
if remove:
|
||||
version = '.'.join(str(x) for x in remove)
|
||||
message.append(
|
||||
' and will be removed by version {}'.format(version))
|
||||
message.append('. Use {} instead.'.format(new))
|
||||
logger.warning(''.join(message))
|
||||
logger.debug(''.join(str(x) for x
|
||||
in traceback.format_stack()))
|
||||
version = ".".join(str(x) for x in remove)
|
||||
message.append(" and will be removed by version {}".format(version))
|
||||
message.append(". Use {} instead.".format(new))
|
||||
logger.warning("".join(message))
|
||||
logger.debug("".join(str(x) for x in traceback.format_stack()))
|
||||
|
||||
def fget(self):
|
||||
_warn()
|
||||
|
|
@ -208,21 +205,20 @@ def get_date(string):
|
|||
|
||||
If no format matches the given date, raise a ValueError.
|
||||
"""
|
||||
string = re.sub(' +', ' ', string)
|
||||
default = SafeDatetime.now().replace(hour=0, minute=0,
|
||||
second=0, microsecond=0)
|
||||
string = re.sub(" +", " ", string)
|
||||
default = SafeDatetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
try:
|
||||
return dateutil.parser.parse(string, default=default)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('{!r} is not a valid date'.format(string))
|
||||
raise ValueError("{!r} is not a valid date".format(string))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pelican_open(filename, mode='r', strip_crs=(sys.platform == 'win32')):
|
||||
def pelican_open(filename, mode="r", strip_crs=(sys.platform == "win32")):
|
||||
"""Open a file and return its content"""
|
||||
|
||||
# utf-8-sig will clear any BOM if present
|
||||
with open(filename, mode, encoding='utf-8-sig') as infile:
|
||||
with open(filename, mode, encoding="utf-8-sig") as infile:
|
||||
content = infile.read()
|
||||
yield content
|
||||
|
||||
|
|
@ -244,7 +240,7 @@ def slugify(value, regex_subs=(), preserve_case=False, use_unicode=False):
|
|||
def normalize_unicode(text):
|
||||
# normalize text by compatibility composition
|
||||
# see: https://en.wikipedia.org/wiki/Unicode_equivalence
|
||||
return unicodedata.normalize('NFKC', text)
|
||||
return unicodedata.normalize("NFKC", text)
|
||||
|
||||
# strip tags from value
|
||||
value = Markup(value).striptags()
|
||||
|
|
@ -259,10 +255,8 @@ def slugify(value, regex_subs=(), preserve_case=False, use_unicode=False):
|
|||
# perform regex substitutions
|
||||
for src, dst in regex_subs:
|
||||
value = re.sub(
|
||||
normalize_unicode(src),
|
||||
normalize_unicode(dst),
|
||||
value,
|
||||
flags=re.IGNORECASE)
|
||||
normalize_unicode(src), normalize_unicode(dst), value, flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
if not preserve_case:
|
||||
value = value.lower()
|
||||
|
|
@ -283,8 +277,7 @@ def copy(source, destination, ignores=None):
|
|||
"""
|
||||
|
||||
def walk_error(err):
|
||||
logger.warning("While copying %s: %s: %s",
|
||||
source_, err.filename, err.strerror)
|
||||
logger.warning("While copying %s: %s: %s", source_, err.filename, err.strerror)
|
||||
|
||||
source_ = os.path.abspath(os.path.expanduser(source))
|
||||
destination_ = os.path.abspath(os.path.expanduser(destination))
|
||||
|
|
@ -292,39 +285,40 @@ def copy(source, destination, ignores=None):
|
|||
if ignores is None:
|
||||
ignores = []
|
||||
|
||||
if any(fnmatch.fnmatch(os.path.basename(source), ignore)
|
||||
for ignore in ignores):
|
||||
logger.info('Not copying %s due to ignores', source_)
|
||||
if any(fnmatch.fnmatch(os.path.basename(source), ignore) for ignore in ignores):
|
||||
logger.info("Not copying %s due to ignores", source_)
|
||||
return
|
||||
|
||||
if os.path.isfile(source_):
|
||||
dst_dir = os.path.dirname(destination_)
|
||||
if not os.path.exists(dst_dir):
|
||||
logger.info('Creating directory %s', dst_dir)
|
||||
logger.info("Creating directory %s", dst_dir)
|
||||
os.makedirs(dst_dir)
|
||||
logger.info('Copying %s to %s', source_, destination_)
|
||||
logger.info("Copying %s to %s", source_, destination_)
|
||||
copy_file(source_, destination_)
|
||||
|
||||
elif os.path.isdir(source_):
|
||||
if not os.path.exists(destination_):
|
||||
logger.info('Creating directory %s', destination_)
|
||||
logger.info("Creating directory %s", destination_)
|
||||
os.makedirs(destination_)
|
||||
if not os.path.isdir(destination_):
|
||||
logger.warning('Cannot copy %s (a directory) to %s (a file)',
|
||||
source_, destination_)
|
||||
logger.warning(
|
||||
"Cannot copy %s (a directory) to %s (a file)", source_, destination_
|
||||
)
|
||||
return
|
||||
|
||||
for src_dir, subdirs, others in os.walk(source_, followlinks=True):
|
||||
dst_dir = os.path.join(destination_,
|
||||
os.path.relpath(src_dir, source_))
|
||||
dst_dir = os.path.join(destination_, os.path.relpath(src_dir, source_))
|
||||
|
||||
subdirs[:] = (s for s in subdirs if not any(fnmatch.fnmatch(s, i)
|
||||
for i in ignores))
|
||||
others[:] = (o for o in others if not any(fnmatch.fnmatch(o, i)
|
||||
for i in ignores))
|
||||
subdirs[:] = (
|
||||
s for s in subdirs if not any(fnmatch.fnmatch(s, i) for i in ignores)
|
||||
)
|
||||
others[:] = (
|
||||
o for o in others if not any(fnmatch.fnmatch(o, i) for i in ignores)
|
||||
)
|
||||
|
||||
if not os.path.isdir(dst_dir):
|
||||
logger.info('Creating directory %s', dst_dir)
|
||||
logger.info("Creating directory %s", dst_dir)
|
||||
# Parent directories are known to exist, so 'mkdir' suffices.
|
||||
os.mkdir(dst_dir)
|
||||
|
||||
|
|
@ -332,21 +326,24 @@ def copy(source, destination, ignores=None):
|
|||
src_path = os.path.join(src_dir, o)
|
||||
dst_path = os.path.join(dst_dir, o)
|
||||
if os.path.isfile(src_path):
|
||||
logger.info('Copying %s to %s', src_path, dst_path)
|
||||
logger.info("Copying %s to %s", src_path, dst_path)
|
||||
copy_file(src_path, dst_path)
|
||||
else:
|
||||
logger.warning('Skipped copy %s (not a file or '
|
||||
'directory) to %s',
|
||||
src_path, dst_path)
|
||||
logger.warning(
|
||||
"Skipped copy %s (not a file or " "directory) to %s",
|
||||
src_path,
|
||||
dst_path,
|
||||
)
|
||||
|
||||
|
||||
def copy_file(source, destination):
|
||||
'''Copy a file'''
|
||||
"""Copy a file"""
|
||||
try:
|
||||
shutil.copyfile(source, destination)
|
||||
except OSError as e:
|
||||
logger.warning("A problem occurred copying file %s to %s; %s",
|
||||
source, destination, e)
|
||||
logger.warning(
|
||||
"A problem occurred copying file %s to %s; %s", source, destination, e
|
||||
)
|
||||
|
||||
|
||||
def clean_output_dir(path, retention):
|
||||
|
|
@ -367,15 +364,15 @@ def clean_output_dir(path, retention):
|
|||
for filename in os.listdir(path):
|
||||
file = os.path.join(path, filename)
|
||||
if any(filename == retain for retain in retention):
|
||||
logger.debug("Skipping deletion; %s is on retention list: %s",
|
||||
filename, file)
|
||||
logger.debug(
|
||||
"Skipping deletion; %s is on retention list: %s", filename, file
|
||||
)
|
||||
elif os.path.isdir(file):
|
||||
try:
|
||||
shutil.rmtree(file)
|
||||
logger.debug("Deleted directory %s", file)
|
||||
except Exception as e:
|
||||
logger.error("Unable to delete directory %s; %s",
|
||||
file, e)
|
||||
logger.error("Unable to delete directory %s; %s", file, e)
|
||||
elif os.path.isfile(file) or os.path.islink(file):
|
||||
try:
|
||||
os.remove(file)
|
||||
|
|
@ -407,29 +404,31 @@ def posixize_path(rel_path):
|
|||
"""Use '/' as path separator, so that source references,
|
||||
like '{static}/foo/bar.jpg' or 'extras/favicon.ico',
|
||||
will work on Windows as well as on Mac and Linux."""
|
||||
return rel_path.replace(os.sep, '/')
|
||||
return rel_path.replace(os.sep, "/")
|
||||
|
||||
|
||||
class _HTMLWordTruncator(HTMLParser):
|
||||
|
||||
_word_regex = re.compile(r"{DBC}|(\w[\w'-]*)".format(
|
||||
# DBC means CJK-like characters. An character can stand for a word.
|
||||
DBC=("([\u4E00-\u9FFF])|" # CJK Unified Ideographs
|
||||
"([\u3400-\u4DBF])|" # CJK Unified Ideographs Extension A
|
||||
"([\uF900-\uFAFF])|" # CJK Compatibility Ideographs
|
||||
"([\U00020000-\U0002A6DF])|" # CJK Unified Ideographs Extension B
|
||||
"([\U0002F800-\U0002FA1F])|" # CJK Compatibility Ideographs Supplement
|
||||
"([\u3040-\u30FF])|" # Hiragana and Katakana
|
||||
"([\u1100-\u11FF])|" # Hangul Jamo
|
||||
"([\uAC00-\uD7FF])|" # Hangul Compatibility Jamo
|
||||
"([\u3130-\u318F])" # Hangul Syllables
|
||||
)), re.UNICODE)
|
||||
_word_prefix_regex = re.compile(r'\w', re.U)
|
||||
_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area',
|
||||
'hr', 'input')
|
||||
_word_regex = re.compile(
|
||||
r"{DBC}|(\w[\w'-]*)".format(
|
||||
# DBC means CJK-like characters. An character can stand for a word.
|
||||
DBC=(
|
||||
"([\u4E00-\u9FFF])|" # CJK Unified Ideographs
|
||||
"([\u3400-\u4DBF])|" # CJK Unified Ideographs Extension A
|
||||
"([\uF900-\uFAFF])|" # CJK Compatibility Ideographs
|
||||
"([\U00020000-\U0002A6DF])|" # CJK Unified Ideographs Extension B
|
||||
"([\U0002F800-\U0002FA1F])|" # CJK Compatibility Ideographs Supplement
|
||||
"([\u3040-\u30FF])|" # Hiragana and Katakana
|
||||
"([\u1100-\u11FF])|" # Hangul Jamo
|
||||
"([\uAC00-\uD7FF])|" # Hangul Compatibility Jamo
|
||||
"([\u3130-\u318F])" # Hangul Syllables
|
||||
)
|
||||
),
|
||||
re.UNICODE,
|
||||
)
|
||||
_word_prefix_regex = re.compile(r"\w", re.U)
|
||||
_singlets = ("br", "col", "link", "base", "img", "param", "area", "hr", "input")
|
||||
|
||||
class TruncationCompleted(Exception):
|
||||
|
||||
def __init__(self, truncate_at):
|
||||
super().__init__(truncate_at)
|
||||
self.truncate_at = truncate_at
|
||||
|
|
@ -455,7 +454,7 @@ class _HTMLWordTruncator(HTMLParser):
|
|||
line_start = 0
|
||||
lineno, line_offset = self.getpos()
|
||||
for i in range(lineno - 1):
|
||||
line_start = self.rawdata.index('\n', line_start) + 1
|
||||
line_start = self.rawdata.index("\n", line_start) + 1
|
||||
return line_start + line_offset
|
||||
|
||||
def add_word(self, word_end):
|
||||
|
|
@ -482,7 +481,7 @@ class _HTMLWordTruncator(HTMLParser):
|
|||
else:
|
||||
# SGML: An end tag closes, back to the matching start tag,
|
||||
# all unclosed intervening start tags with omitted end tags
|
||||
del self.open_tags[:i + 1]
|
||||
del self.open_tags[: i + 1]
|
||||
|
||||
def handle_data(self, data):
|
||||
word_end = 0
|
||||
|
|
@ -531,7 +530,7 @@ class _HTMLWordTruncator(HTMLParser):
|
|||
ref_end = offset + len(name) + 1
|
||||
|
||||
try:
|
||||
if self.rawdata[ref_end] == ';':
|
||||
if self.rawdata[ref_end] == ";":
|
||||
ref_end += 1
|
||||
except IndexError:
|
||||
# We are at the end of the string and there's no ';'
|
||||
|
|
@ -556,7 +555,7 @@ class _HTMLWordTruncator(HTMLParser):
|
|||
codepoint = entities.name2codepoint[name]
|
||||
char = chr(codepoint)
|
||||
except KeyError:
|
||||
char = ''
|
||||
char = ""
|
||||
self._handle_ref(name, char)
|
||||
|
||||
def handle_charref(self, name):
|
||||
|
|
@ -567,17 +566,17 @@ class _HTMLWordTruncator(HTMLParser):
|
|||
`#x2014`)
|
||||
"""
|
||||
try:
|
||||
if name.startswith('x'):
|
||||
if name.startswith("x"):
|
||||
codepoint = int(name[1:], 16)
|
||||
else:
|
||||
codepoint = int(name)
|
||||
char = chr(codepoint)
|
||||
except (ValueError, OverflowError):
|
||||
char = ''
|
||||
self._handle_ref('#' + name, char)
|
||||
char = ""
|
||||
self._handle_ref("#" + name, char)
|
||||
|
||||
|
||||
def truncate_html_words(s, num, end_text='…'):
|
||||
def truncate_html_words(s, num, end_text="…"):
|
||||
"""Truncates HTML to a certain number of words.
|
||||
|
||||
(not counting tags and comments). Closes opened tags if they were correctly
|
||||
|
|
@ -588,23 +587,23 @@ def truncate_html_words(s, num, end_text='…'):
|
|||
"""
|
||||
length = int(num)
|
||||
if length <= 0:
|
||||
return ''
|
||||
return ""
|
||||
truncator = _HTMLWordTruncator(length)
|
||||
truncator.feed(s)
|
||||
if truncator.truncate_at is None:
|
||||
return s
|
||||
out = s[:truncator.truncate_at]
|
||||
out = s[: truncator.truncate_at]
|
||||
if end_text:
|
||||
out += ' ' + end_text
|
||||
out += " " + end_text
|
||||
# Close any tags still open
|
||||
for tag in truncator.open_tags:
|
||||
out += '</%s>' % tag
|
||||
out += "</%s>" % tag
|
||||
# Return string
|
||||
return out
|
||||
|
||||
|
||||
def process_translations(content_list, translation_id=None):
|
||||
""" Finds translations and returns them.
|
||||
"""Finds translations and returns them.
|
||||
|
||||
For each content_list item, populates the 'translations' attribute, and
|
||||
returns a tuple with two lists (index, translations). Index list includes
|
||||
|
|
@ -632,19 +631,23 @@ def process_translations(content_list, translation_id=None):
|
|||
try:
|
||||
content_list.sort(key=attrgetter(*translation_id))
|
||||
except TypeError:
|
||||
raise TypeError('Cannot unpack {}, \'translation_id\' must be falsy, a'
|
||||
' string or a collection of strings'
|
||||
.format(translation_id))
|
||||
raise TypeError(
|
||||
"Cannot unpack {}, 'translation_id' must be falsy, a"
|
||||
" string or a collection of strings".format(translation_id)
|
||||
)
|
||||
except AttributeError:
|
||||
raise AttributeError('Cannot use {} as \'translation_id\', there '
|
||||
'appear to be items without these metadata '
|
||||
'attributes'.format(translation_id))
|
||||
raise AttributeError(
|
||||
"Cannot use {} as 'translation_id', there "
|
||||
"appear to be items without these metadata "
|
||||
"attributes".format(translation_id)
|
||||
)
|
||||
|
||||
for id_vals, items in groupby(content_list, attrgetter(*translation_id)):
|
||||
# prepare warning string
|
||||
id_vals = (id_vals,) if len(translation_id) == 1 else id_vals
|
||||
with_str = 'with' + ', '.join([' {} "{{}}"'] * len(translation_id))\
|
||||
.format(*translation_id).format(*id_vals)
|
||||
with_str = "with" + ", ".join([' {} "{{}}"'] * len(translation_id)).format(
|
||||
*translation_id
|
||||
).format(*id_vals)
|
||||
|
||||
items = list(items)
|
||||
original_items = get_original_items(items, with_str)
|
||||
|
|
@ -662,24 +665,24 @@ def get_original_items(items, with_str):
|
|||
args = [len(items)]
|
||||
args.extend(extra)
|
||||
args.extend(x.source_path for x in items)
|
||||
logger.warning('{}: {}'.format(msg, '\n%s' * len(items)), *args)
|
||||
logger.warning("{}: {}".format(msg, "\n%s" * len(items)), *args)
|
||||
|
||||
# warn if several items have the same lang
|
||||
for lang, lang_items in groupby(items, attrgetter('lang')):
|
||||
for lang, lang_items in groupby(items, attrgetter("lang")):
|
||||
lang_items = list(lang_items)
|
||||
if len(lang_items) > 1:
|
||||
_warn_source_paths('There are %s items "%s" with lang %s',
|
||||
lang_items, with_str, lang)
|
||||
_warn_source_paths(
|
||||
'There are %s items "%s" with lang %s', lang_items, with_str, lang
|
||||
)
|
||||
|
||||
# items with `translation` metadata will be used as translations...
|
||||
candidate_items = [
|
||||
i for i in items
|
||||
if i.metadata.get('translation', 'false').lower() == 'false']
|
||||
i for i in items if i.metadata.get("translation", "false").lower() == "false"
|
||||
]
|
||||
|
||||
# ...unless all items with that slug are translations
|
||||
if not candidate_items:
|
||||
_warn_source_paths('All items ("%s") "%s" are translations',
|
||||
items, with_str)
|
||||
_warn_source_paths('All items ("%s") "%s" are translations', items, with_str)
|
||||
candidate_items = items
|
||||
|
||||
# find items with default language
|
||||
|
|
@ -691,13 +694,14 @@ def get_original_items(items, with_str):
|
|||
|
||||
# warn if there are several original items
|
||||
if len(original_items) > 1:
|
||||
_warn_source_paths('There are %s original (not translated) items %s',
|
||||
original_items, with_str)
|
||||
_warn_source_paths(
|
||||
"There are %s original (not translated) items %s", original_items, with_str
|
||||
)
|
||||
return original_items
|
||||
|
||||
|
||||
def order_content(content_list, order_by='slug'):
|
||||
""" Sorts content.
|
||||
def order_content(content_list, order_by="slug"):
|
||||
"""Sorts content.
|
||||
|
||||
order_by can be a string of an attribute or sorting function. If order_by
|
||||
is defined, content will be ordered by that attribute or sorting function.
|
||||
|
|
@ -713,22 +717,22 @@ def order_content(content_list, order_by='slug'):
|
|||
try:
|
||||
content_list.sort(key=order_by)
|
||||
except Exception:
|
||||
logger.error('Error sorting with function %s', order_by)
|
||||
logger.error("Error sorting with function %s", order_by)
|
||||
elif isinstance(order_by, str):
|
||||
if order_by.startswith('reversed-'):
|
||||
if order_by.startswith("reversed-"):
|
||||
order_reversed = True
|
||||
order_by = order_by.replace('reversed-', '', 1)
|
||||
order_by = order_by.replace("reversed-", "", 1)
|
||||
else:
|
||||
order_reversed = False
|
||||
|
||||
if order_by == 'basename':
|
||||
if order_by == "basename":
|
||||
content_list.sort(
|
||||
key=lambda x: os.path.basename(x.source_path or ''),
|
||||
reverse=order_reversed)
|
||||
key=lambda x: os.path.basename(x.source_path or ""),
|
||||
reverse=order_reversed,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
content_list.sort(key=attrgetter(order_by),
|
||||
reverse=order_reversed)
|
||||
content_list.sort(key=attrgetter(order_by), reverse=order_reversed)
|
||||
except AttributeError:
|
||||
for content in content_list:
|
||||
try:
|
||||
|
|
@ -736,26 +740,31 @@ def order_content(content_list, order_by='slug'):
|
|||
except AttributeError:
|
||||
logger.warning(
|
||||
'There is no "%s" attribute in "%s". '
|
||||
'Defaulting to slug order.',
|
||||
"Defaulting to slug order.",
|
||||
order_by,
|
||||
content.get_relative_source_path(),
|
||||
extra={
|
||||
'limit_msg': ('More files are missing '
|
||||
'the needed attribute.')
|
||||
})
|
||||
"limit_msg": (
|
||||
"More files are missing "
|
||||
"the needed attribute."
|
||||
)
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
'Invalid *_ORDER_BY setting (%s). '
|
||||
'Valid options are strings and functions.', order_by)
|
||||
"Invalid *_ORDER_BY setting (%s). "
|
||||
"Valid options are strings and functions.",
|
||||
order_by,
|
||||
)
|
||||
|
||||
return content_list
|
||||
|
||||
|
||||
def wait_for_changes(settings_file, reader_class, settings):
|
||||
content_path = settings.get('PATH', '')
|
||||
theme_path = settings.get('THEME', '')
|
||||
content_path = settings.get("PATH", "")
|
||||
theme_path = settings.get("THEME", "")
|
||||
ignore_files = set(
|
||||
fnmatch.translate(pattern) for pattern in settings.get('IGNORE_FILES', [])
|
||||
fnmatch.translate(pattern) for pattern in settings.get("IGNORE_FILES", [])
|
||||
)
|
||||
|
||||
candidate_paths = [
|
||||
|
|
@ -765,7 +774,7 @@ def wait_for_changes(settings_file, reader_class, settings):
|
|||
]
|
||||
|
||||
candidate_paths.extend(
|
||||
os.path.join(content_path, path) for path in settings.get('STATIC_PATHS', [])
|
||||
os.path.join(content_path, path) for path in settings.get("STATIC_PATHS", [])
|
||||
)
|
||||
|
||||
watching_paths = []
|
||||
|
|
@ -778,11 +787,13 @@ def wait_for_changes(settings_file, reader_class, settings):
|
|||
else:
|
||||
watching_paths.append(path)
|
||||
|
||||
return next(watchfiles.watch(
|
||||
*watching_paths,
|
||||
watch_filter=watchfiles.DefaultFilter(ignore_entity_patterns=ignore_files),
|
||||
rust_timeout=0
|
||||
))
|
||||
return next(
|
||||
watchfiles.watch(
|
||||
*watching_paths,
|
||||
watch_filter=watchfiles.DefaultFilter(ignore_entity_patterns=ignore_files),
|
||||
rust_timeout=0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def set_date_tzinfo(d, tz_name=None):
|
||||
|
|
@ -811,7 +822,7 @@ def split_all(path):
|
|||
"""
|
||||
if isinstance(path, str):
|
||||
components = []
|
||||
path = path.lstrip('/')
|
||||
path = path.lstrip("/")
|
||||
while path:
|
||||
head, tail = os.path.split(path)
|
||||
if tail:
|
||||
|
|
@ -827,32 +838,30 @@ def split_all(path):
|
|||
return None
|
||||
else:
|
||||
raise TypeError(
|
||||
'"path" was {}, must be string, None, or pathlib.Path'.format(
|
||||
type(path)
|
||||
)
|
||||
'"path" was {}, must be string, None, or pathlib.Path'.format(type(path))
|
||||
)
|
||||
|
||||
|
||||
def is_selected_for_writing(settings, path):
|
||||
'''Check whether path is selected for writing
|
||||
"""Check whether path is selected for writing
|
||||
according to the WRITE_SELECTED list
|
||||
|
||||
If WRITE_SELECTED is an empty list (default),
|
||||
any path is selected for writing.
|
||||
'''
|
||||
if settings['WRITE_SELECTED']:
|
||||
return path in settings['WRITE_SELECTED']
|
||||
"""
|
||||
if settings["WRITE_SELECTED"]:
|
||||
return path in settings["WRITE_SELECTED"]
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def path_to_file_url(path):
|
||||
'''Convert file-system path to file:// URL'''
|
||||
"""Convert file-system path to file:// URL"""
|
||||
return urllib.parse.urljoin("file://", urllib.request.pathname2url(path))
|
||||
|
||||
|
||||
def maybe_pluralize(count, singular, plural):
|
||||
'''
|
||||
"""
|
||||
Returns a formatted string containing count and plural if count is not 1
|
||||
Returns count and singular if count is 1
|
||||
|
||||
|
|
@ -860,22 +869,22 @@ def maybe_pluralize(count, singular, plural):
|
|||
maybe_pluralize(1, 'Article', 'Articles') -> '1 Article'
|
||||
maybe_pluralize(2, 'Article', 'Articles') -> '2 Articles'
|
||||
|
||||
'''
|
||||
"""
|
||||
selection = plural
|
||||
if count == 1:
|
||||
selection = singular
|
||||
return '{} {}'.format(count, selection)
|
||||
return "{} {}".format(count, selection)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_locale(temp_locale=None, lc_category=locale.LC_ALL):
|
||||
'''
|
||||
"""
|
||||
Enable code to run in a context with a temporary locale
|
||||
Resets the locale back when exiting context.
|
||||
|
||||
Use tests.support.TestCaseWithCLocale if you want every unit test in a
|
||||
class to use the C locale.
|
||||
'''
|
||||
"""
|
||||
orig_locale = locale.setlocale(lc_category)
|
||||
if temp_locale:
|
||||
locale.setlocale(lc_category, temp_locale)
|
||||
|
|
|
|||
|
|
@ -9,14 +9,18 @@ from markupsafe import Markup
|
|||
|
||||
from pelican.paginator import Paginator
|
||||
from pelican.plugins import signals
|
||||
from pelican.utils import (get_relative_path, is_selected_for_writing,
|
||||
path_to_url, sanitised_join, set_date_tzinfo)
|
||||
from pelican.utils import (
|
||||
get_relative_path,
|
||||
is_selected_for_writing,
|
||||
path_to_url,
|
||||
sanitised_join,
|
||||
set_date_tzinfo,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Writer:
|
||||
|
||||
def __init__(self, output_path, settings=None):
|
||||
self.output_path = output_path
|
||||
self.reminder = dict()
|
||||
|
|
@ -25,24 +29,26 @@ class Writer:
|
|||
self._overridden_files = set()
|
||||
|
||||
# See Content._link_replacer for details
|
||||
if "RELATIVE_URLS" in self.settings and self.settings['RELATIVE_URLS']:
|
||||
if "RELATIVE_URLS" in self.settings and self.settings["RELATIVE_URLS"]:
|
||||
self.urljoiner = posix_join
|
||||
else:
|
||||
self.urljoiner = lambda base, url: urljoin(
|
||||
base if base.endswith('/') else base + '/', str(url))
|
||||
base if base.endswith("/") else base + "/", str(url)
|
||||
)
|
||||
|
||||
def _create_new_feed(self, feed_type, feed_title, context):
|
||||
feed_class = Rss201rev2Feed if feed_type == 'rss' else Atom1Feed
|
||||
feed_class = Rss201rev2Feed if feed_type == "rss" else Atom1Feed
|
||||
if feed_title:
|
||||
feed_title = context['SITENAME'] + ' - ' + feed_title
|
||||
feed_title = context["SITENAME"] + " - " + feed_title
|
||||
else:
|
||||
feed_title = context['SITENAME']
|
||||
feed_title = context["SITENAME"]
|
||||
return feed_class(
|
||||
title=Markup(feed_title).striptags(),
|
||||
link=(self.site_url + '/'),
|
||||
link=(self.site_url + "/"),
|
||||
feed_url=self.feed_url,
|
||||
description=context.get('SITESUBTITLE', ''),
|
||||
subtitle=context.get('SITESUBTITLE', None))
|
||||
description=context.get("SITESUBTITLE", ""),
|
||||
subtitle=context.get("SITESUBTITLE", None),
|
||||
)
|
||||
|
||||
def _add_item_to_the_feed(self, feed, item):
|
||||
title = Markup(item.title).striptags()
|
||||
|
|
@ -52,7 +58,7 @@ class Writer:
|
|||
# RSS feeds use a single tag called 'description' for both the full
|
||||
# content and the summary
|
||||
content = None
|
||||
if self.settings.get('RSS_FEED_SUMMARY_ONLY'):
|
||||
if self.settings.get("RSS_FEED_SUMMARY_ONLY"):
|
||||
description = item.summary
|
||||
else:
|
||||
description = item.get_content(self.site_url)
|
||||
|
|
@ -71,9 +77,9 @@ class Writer:
|
|||
description = None
|
||||
|
||||
categories = []
|
||||
if hasattr(item, 'category'):
|
||||
if hasattr(item, "category"):
|
||||
categories.append(item.category)
|
||||
if hasattr(item, 'tags'):
|
||||
if hasattr(item, "tags"):
|
||||
categories.extend(item.tags)
|
||||
|
||||
feed.add_item(
|
||||
|
|
@ -83,14 +89,12 @@ class Writer:
|
|||
description=description,
|
||||
content=content,
|
||||
categories=categories or None,
|
||||
author_name=getattr(item, 'author', ''),
|
||||
pubdate=set_date_tzinfo(
|
||||
item.date, self.settings.get('TIMEZONE', None)
|
||||
),
|
||||
author_name=getattr(item, "author", ""),
|
||||
pubdate=set_date_tzinfo(item.date, self.settings.get("TIMEZONE", None)),
|
||||
updateddate=set_date_tzinfo(
|
||||
item.modified, self.settings.get('TIMEZONE', None)
|
||||
item.modified, self.settings.get("TIMEZONE", None)
|
||||
)
|
||||
if hasattr(item, 'modified')
|
||||
if hasattr(item, "modified")
|
||||
else None,
|
||||
)
|
||||
|
||||
|
|
@ -102,22 +106,29 @@ class Writer:
|
|||
"""
|
||||
if filename in self._overridden_files:
|
||||
if override:
|
||||
raise RuntimeError('File %s is set to be overridden twice'
|
||||
% filename)
|
||||
logger.info('Skipping %s', filename)
|
||||
raise RuntimeError("File %s is set to be overridden twice" % filename)
|
||||
logger.info("Skipping %s", filename)
|
||||
filename = os.devnull
|
||||
elif filename in self._written_files:
|
||||
if override:
|
||||
logger.info('Overwriting %s', filename)
|
||||
logger.info("Overwriting %s", filename)
|
||||
else:
|
||||
raise RuntimeError('File %s is to be overwritten' % filename)
|
||||
raise RuntimeError("File %s is to be overwritten" % filename)
|
||||
if override:
|
||||
self._overridden_files.add(filename)
|
||||
self._written_files.add(filename)
|
||||
return open(filename, 'w', encoding=encoding)
|
||||
return open(filename, "w", encoding=encoding)
|
||||
|
||||
def write_feed(self, elements, context, path=None, url=None,
|
||||
feed_type='atom', override_output=False, feed_title=None):
|
||||
def write_feed(
|
||||
self,
|
||||
elements,
|
||||
context,
|
||||
path=None,
|
||||
url=None,
|
||||
feed_type="atom",
|
||||
override_output=False,
|
||||
feed_title=None,
|
||||
):
|
||||
"""Generate a feed with the list of articles provided
|
||||
|
||||
Return the feed. If no path or output_path is specified, just
|
||||
|
|
@ -137,16 +148,15 @@ class Writer:
|
|||
if not is_selected_for_writing(self.settings, path):
|
||||
return
|
||||
|
||||
self.site_url = context.get(
|
||||
'SITEURL', path_to_url(get_relative_path(path)))
|
||||
self.site_url = context.get("SITEURL", path_to_url(get_relative_path(path)))
|
||||
|
||||
self.feed_domain = context.get('FEED_DOMAIN')
|
||||
self.feed_domain = context.get("FEED_DOMAIN")
|
||||
self.feed_url = self.urljoiner(self.feed_domain, url or path)
|
||||
|
||||
feed = self._create_new_feed(feed_type, feed_title, context)
|
||||
|
||||
# FEED_MAX_ITEMS = None means [:None] to get every element
|
||||
for element in elements[:self.settings['FEED_MAX_ITEMS']]:
|
||||
for element in elements[: self.settings["FEED_MAX_ITEMS"]]:
|
||||
self._add_item_to_the_feed(feed, element)
|
||||
|
||||
signals.feed_generated.send(context, feed=feed)
|
||||
|
|
@ -158,17 +168,25 @@ class Writer:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
with self._open_w(complete_path, 'utf-8', override_output) as fp:
|
||||
feed.write(fp, 'utf-8')
|
||||
logger.info('Writing %s', complete_path)
|
||||
with self._open_w(complete_path, "utf-8", override_output) as fp:
|
||||
feed.write(fp, "utf-8")
|
||||
logger.info("Writing %s", complete_path)
|
||||
|
||||
signals.feed_written.send(
|
||||
complete_path, context=context, feed=feed)
|
||||
signals.feed_written.send(complete_path, context=context, feed=feed)
|
||||
return feed
|
||||
|
||||
def write_file(self, name, template, context, relative_urls=False,
|
||||
paginated=None, template_name=None, override_output=False,
|
||||
url=None, **kwargs):
|
||||
def write_file(
|
||||
self,
|
||||
name,
|
||||
template,
|
||||
context,
|
||||
relative_urls=False,
|
||||
paginated=None,
|
||||
template_name=None,
|
||||
override_output=False,
|
||||
url=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Render the template and write the file.
|
||||
|
||||
:param name: name of the file to output
|
||||
|
|
@ -185,10 +203,13 @@ class Writer:
|
|||
:param **kwargs: additional variables to pass to the templates
|
||||
"""
|
||||
|
||||
if name is False or \
|
||||
name == "" or \
|
||||
not is_selected_for_writing(self.settings,
|
||||
os.path.join(self.output_path, name)):
|
||||
if (
|
||||
name is False
|
||||
or name == ""
|
||||
or not is_selected_for_writing(
|
||||
self.settings, os.path.join(self.output_path, name)
|
||||
)
|
||||
):
|
||||
return
|
||||
elif not name:
|
||||
# other stuff, just return for now
|
||||
|
|
@ -197,8 +218,8 @@ class Writer:
|
|||
def _write_file(template, localcontext, output_path, name, override):
|
||||
"""Render the template write the file."""
|
||||
# set localsiteurl for context so that Contents can adjust links
|
||||
if localcontext['localsiteurl']:
|
||||
context['localsiteurl'] = localcontext['localsiteurl']
|
||||
if localcontext["localsiteurl"]:
|
||||
context["localsiteurl"] = localcontext["localsiteurl"]
|
||||
output = template.render(localcontext)
|
||||
path = sanitised_join(output_path, name)
|
||||
|
||||
|
|
@ -207,9 +228,9 @@ class Writer:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
with self._open_w(path, 'utf-8', override=override) as f:
|
||||
with self._open_w(path, "utf-8", override=override) as f:
|
||||
f.write(output)
|
||||
logger.info('Writing %s', path)
|
||||
logger.info("Writing %s", path)
|
||||
|
||||
# Send a signal to say we're writing a file with some specific
|
||||
# local context.
|
||||
|
|
@ -217,54 +238,66 @@ class Writer:
|
|||
|
||||
def _get_localcontext(context, name, kwargs, relative_urls):
|
||||
localcontext = context.copy()
|
||||
localcontext['localsiteurl'] = localcontext.get(
|
||||
'localsiteurl', None)
|
||||
localcontext["localsiteurl"] = localcontext.get("localsiteurl", None)
|
||||
if relative_urls:
|
||||
relative_url = path_to_url(get_relative_path(name))
|
||||
localcontext['SITEURL'] = relative_url
|
||||
localcontext['localsiteurl'] = relative_url
|
||||
localcontext['output_file'] = name
|
||||
localcontext["SITEURL"] = relative_url
|
||||
localcontext["localsiteurl"] = relative_url
|
||||
localcontext["output_file"] = name
|
||||
localcontext.update(kwargs)
|
||||
return localcontext
|
||||
|
||||
if paginated is None:
|
||||
paginated = {key: val for key, val in kwargs.items()
|
||||
if key in {'articles', 'dates'}}
|
||||
paginated = {
|
||||
key: val for key, val in kwargs.items() if key in {"articles", "dates"}
|
||||
}
|
||||
|
||||
# pagination
|
||||
if paginated and template_name in self.settings['PAGINATED_TEMPLATES']:
|
||||
if paginated and template_name in self.settings["PAGINATED_TEMPLATES"]:
|
||||
# pagination needed
|
||||
per_page = self.settings['PAGINATED_TEMPLATES'][template_name] \
|
||||
or self.settings['DEFAULT_PAGINATION']
|
||||
per_page = (
|
||||
self.settings["PAGINATED_TEMPLATES"][template_name]
|
||||
or self.settings["DEFAULT_PAGINATION"]
|
||||
)
|
||||
|
||||
# init paginators
|
||||
paginators = {key: Paginator(name, url, val, self.settings,
|
||||
per_page)
|
||||
for key, val in paginated.items()}
|
||||
paginators = {
|
||||
key: Paginator(name, url, val, self.settings, per_page)
|
||||
for key, val in paginated.items()
|
||||
}
|
||||
|
||||
# generated pages, and write
|
||||
for page_num in range(list(paginators.values())[0].num_pages):
|
||||
paginated_kwargs = kwargs.copy()
|
||||
for key in paginators.keys():
|
||||
paginator = paginators[key]
|
||||
previous_page = paginator.page(page_num) \
|
||||
if page_num > 0 else None
|
||||
previous_page = paginator.page(page_num) if page_num > 0 else None
|
||||
page = paginator.page(page_num + 1)
|
||||
next_page = paginator.page(page_num + 2) \
|
||||
if page_num + 1 < paginator.num_pages else None
|
||||
next_page = (
|
||||
paginator.page(page_num + 2)
|
||||
if page_num + 1 < paginator.num_pages
|
||||
else None
|
||||
)
|
||||
paginated_kwargs.update(
|
||||
{'%s_paginator' % key: paginator,
|
||||
'%s_page' % key: page,
|
||||
'%s_previous_page' % key: previous_page,
|
||||
'%s_next_page' % key: next_page})
|
||||
{
|
||||
"%s_paginator" % key: paginator,
|
||||
"%s_page" % key: page,
|
||||
"%s_previous_page" % key: previous_page,
|
||||
"%s_next_page" % key: next_page,
|
||||
}
|
||||
)
|
||||
|
||||
localcontext = _get_localcontext(
|
||||
context, page.save_as, paginated_kwargs, relative_urls)
|
||||
_write_file(template, localcontext, self.output_path,
|
||||
page.save_as, override_output)
|
||||
context, page.save_as, paginated_kwargs, relative_urls
|
||||
)
|
||||
_write_file(
|
||||
template,
|
||||
localcontext,
|
||||
self.output_path,
|
||||
page.save_as,
|
||||
override_output,
|
||||
)
|
||||
else:
|
||||
# no pagination
|
||||
localcontext = _get_localcontext(
|
||||
context, name, kwargs, relative_urls)
|
||||
_write_file(template, localcontext, self.output_path, name,
|
||||
override_output)
|
||||
localcontext = _get_localcontext(context, name, kwargs, relative_urls)
|
||||
_write_file(template, localcontext, self.output_path, name, override_output)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue