diff options
Diffstat (limited to 'nikola/nikola.py')
| -rw-r--r-- | nikola/nikola.py | 172 |
1 files changed, 83 insertions, 89 deletions
diff --git a/nikola/nikola.py b/nikola/nikola.py index 41eeac6..916ca69 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2022 Roberto Alsina and others. +# Copyright © 2012-2024 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -33,7 +33,9 @@ import functools import logging import operator import os +import pathlib import sys +import typing import mimetypes from collections import defaultdict from copy import copy @@ -46,27 +48,14 @@ import natsort import PyRSS2Gen as rss from pkg_resources import resource_filename from blinker import signal -from yapsy.PluginManager import PluginManager from . import DEBUG, SHOW_TRACEBACKS, filters, utils, hierarchy_utils, shortcodes from . import metadata_extractors from .metadata_extractors import default_metadata_extractors_by from .post import Post # NOQA +from .plugin_manager import PluginCandidate, PluginInfo, PluginManager from .plugin_categories import ( - Command, - LateTask, - PageCompiler, - CompilerExtension, - MarkdownExtension, - RestExtension, - MetadataExtractor, - ShortcodePlugin, - Task, - TaskMultiplier, TemplateSystem, - SignalHandler, - ConfigPlugin, - CommentSystem, PostScanner, Taxonomy, ) @@ -96,6 +85,7 @@ LEGAL_VALUES = { 'DEFAULT_THEME': 'bootblog4', 'COMMENT_SYSTEM': [ 'disqus', + 'discourse', 'facebook', 'intensedebate', 'isso', @@ -381,6 +371,9 @@ class Nikola(object): Takes a site config as argument on creation. """ + plugin_manager: PluginManager + _template_system: TemplateSystem + def __init__(self, **config): """Initialize proper environment for running tasks.""" # Register our own path handlers @@ -891,6 +884,7 @@ class Nikola(object): utils.LOGGER.warning('You are currently disabling "{}", but not the following new taxonomy plugins: {}'.format(old_plugin_name, ', '.join(missing_plugins))) utils.LOGGER.warning('Please also disable these new plugins or remove "{}" from the DISABLED_PLUGINS list.'.format(old_plugin_name)) self.config['DISABLED_PLUGINS'].extend(missing_plugins) + # Special-case logic for "render_indexes" to fix #2591 if 'render_indexes' in self.config['DISABLED_PLUGINS']: if 'generate_rss' in self.config['DISABLED_PLUGINS'] or self.config['GENERATE_RSS'] is False: @@ -1000,57 +994,50 @@ class Nikola(object): self.state._set_site(self) self.cache._set_site(self) - def _filter_duplicate_plugins(self, plugin_list): + # WebP files have no official MIME type yet, but we need to recognize them (Issue #3671) + mimetypes.add_type('image/webp', '.webp') + + def _filter_duplicate_plugins(self, plugin_list: typing.Iterable[PluginCandidate]): """Find repeated plugins and discard the less local copy.""" - def plugin_position_in_places(plugin): + def plugin_position_in_places(plugin: PluginInfo): # plugin here is a tuple: # (path to the .plugin file, path to plugin module w/o .py, plugin metadata) for i, place in enumerate(self._plugin_places): - if plugin[0].startswith(place): + place: pathlib.Path + try: + # Path.is_relative_to backport + plugin.source_dir.relative_to(place) return i - utils.LOGGER.warn("Duplicate plugin found in unexpected location: {}".format(plugin[0])) + except ValueError: + pass + utils.LOGGER.warning("Duplicate plugin found in unexpected location: {}".format(plugin.source_dir)) return len(self._plugin_places) plugin_dict = defaultdict(list) - for data in plugin_list: - plugin_dict[data[2].name].append(data) + for plugin in plugin_list: + plugin_dict[plugin.name].append(plugin) result = [] - for _, plugins in plugin_dict.items(): + for name, plugins in plugin_dict.items(): if len(plugins) > 1: # Sort by locality plugins.sort(key=plugin_position_in_places) utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format( - plugins[-1][2].name, plugins[-1][0])) + name, plugins[-1].source_dir)) result.append(plugins[-1]) return result def init_plugins(self, commands_only=False, load_all=False): """Load plugins as needed.""" - self.plugin_manager = PluginManager(categories_filter={ - "Command": Command, - "Task": Task, - "LateTask": LateTask, - "TemplateSystem": TemplateSystem, - "PageCompiler": PageCompiler, - "TaskMultiplier": TaskMultiplier, - "CompilerExtension": CompilerExtension, - "MarkdownExtension": MarkdownExtension, - "RestExtension": RestExtension, - "MetadataExtractor": MetadataExtractor, - "ShortcodePlugin": ShortcodePlugin, - "SignalHandler": SignalHandler, - "ConfigPlugin": ConfigPlugin, - "CommentSystem": CommentSystem, - "PostScanner": PostScanner, - "Taxonomy": Taxonomy, - }) - self.plugin_manager.getPluginLocator().setPluginInfoExtension('plugin') extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS'] + self._loading_commands_only = commands_only self._plugin_places = [ resource_filename('nikola', 'plugins'), os.path.expanduser(os.path.join('~', '.nikola', 'plugins')), os.path.join(os.getcwd(), 'plugins'), ] + [path for path in extra_plugins_dirs if path] + self._plugin_places = [pathlib.Path(p) for p in self._plugin_places] + + self.plugin_manager = PluginManager(plugin_places=self._plugin_places) compilers = defaultdict(set) # Also add aliases for combinations with TRANSLATIONS_PATTERN @@ -1069,41 +1056,37 @@ class Nikola(object): self.disabled_compilers = {} self.disabled_compiler_extensions = defaultdict(list) - self.plugin_manager.getPluginLocator().setPluginPlaces(self._plugin_places) - self.plugin_manager.locatePlugins() - bad_candidates = set([]) + candidates = self.plugin_manager.locate_plugins() + good_candidates = set() if not load_all: - for p in self.plugin_manager._candidates: + for p in candidates: if commands_only: - if p[-1].details.has_option('Nikola', 'PluginCategory'): - # FIXME TemplateSystem should not be needed - if p[-1].details.get('Nikola', 'PluginCategory') not in {'Command', 'Template'}: - bad_candidates.add(p) - else: - bad_candidates.add(p) + if p.category != 'Command': + continue elif self.configured: # Not commands-only, and configured # Remove blacklisted plugins - if p[-1].name in self.config['DISABLED_PLUGINS']: - bad_candidates.add(p) - utils.LOGGER.debug('Not loading disabled plugin {}', p[-1].name) - # Remove compilers we don't use - if p[-1].details.has_option('Nikola', 'PluginCategory') and p[-1].details.get('Nikola', 'PluginCategory') in ('Compiler', 'PageCompiler'): - bad_candidates.add(p) - self.disabled_compilers[p[-1].name] = p + if p.name in self.config['DISABLED_PLUGINS']: + utils.LOGGER.debug('Not loading disabled plugin {}', p.name) + continue + # Remove compilers - will be loaded later based on usage + if p.category == "PageCompiler": + self.disabled_compilers[p.name] = p + continue # Remove compiler extensions we don't need - if p[-1].details.has_option('Nikola', 'compiler') and p[-1].details.get('Nikola', 'compiler') in self.disabled_compilers: - bad_candidates.add(p) - self.disabled_compiler_extensions[p[-1].details.get('Nikola', 'compiler')].append(p) - self.plugin_manager._candidates = list(set(self.plugin_manager._candidates) - bad_candidates) + if p.compiler and p.compiler in self.disabled_compilers: + self.disabled_compiler_extensions[p.compiler].append(p) + continue + good_candidates.add(p) - self.plugin_manager._candidates = self._filter_duplicate_plugins(self.plugin_manager._candidates) - self.plugin_manager.loadPlugins() + good_candidates = self._filter_duplicate_plugins(good_candidates) + self.plugin_manager.load_plugins(good_candidates) # Search for compiler plugins which we disabled but shouldn't have self._activate_plugins_of_category("PostScanner") if not load_all: file_extensions = set() - for post_scanner in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('PostScanner')]: + for post_scanner in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('PostScanner')]: + post_scanner: PostScanner exts = post_scanner.supported_extensions() if exts is not None: file_extensions.update(exts) @@ -1122,13 +1105,13 @@ class Nikola(object): for p in self.disabled_compiler_extensions.pop(k, []): to_add.append(p) for _, p in self.disabled_compilers.items(): - utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name) + utils.LOGGER.debug('Not loading unneeded compiler %s', p.name) for _, plugins in self.disabled_compiler_extensions.items(): for p in plugins: - utils.LOGGER.debug('Not loading compiler extension {}', p[-1].name) + utils.LOGGER.debug('Not loading compiler extension %s', p.name) if to_add: - self.plugin_manager._candidates = self._filter_duplicate_plugins(to_add) - self.plugin_manager.loadPlugins() + extra_candidates = self._filter_duplicate_plugins(to_add) + self.plugin_manager.load_plugins(extra_candidates) # Jupyter theme configuration. If a website has ipynb enabled in post_pages # we should enable the Jupyter CSS (leaving that up to the theme itself). @@ -1141,7 +1124,8 @@ class Nikola(object): self._activate_plugins_of_category("Taxonomy") self.taxonomy_plugins = {} - for taxonomy in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('Taxonomy')]: + for taxonomy in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('Taxonomy')]: + taxonomy: Taxonomy if not taxonomy.is_enabled(): continue if taxonomy.classification_name in self.taxonomy_plugins: @@ -1167,10 +1151,9 @@ class Nikola(object): # Activate all required compiler plugins self.compiler_extensions = self._activate_plugins_of_category("CompilerExtension") - for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"): + for plugin_info in self.plugin_manager.get_plugins_of_category("PageCompiler"): if plugin_info.name in self.config["COMPILERS"].keys(): - self.plugin_manager.activatePluginByName(plugin_info.name) - plugin_info.plugin_object.set_site(self) + self._activate_plugin(plugin_info) # Activate shortcode plugins self._activate_plugins_of_category("ShortcodePlugin") @@ -1179,10 +1162,8 @@ class Nikola(object): self.compilers = {} self.inverse_compilers = {} - for plugin_info in self.plugin_manager.getPluginsOfCategory( - "PageCompiler"): - self.compilers[plugin_info.name] = \ - plugin_info.plugin_object + for plugin_info in self.plugin_manager.get_plugins_of_category("PageCompiler"): + self.compilers[plugin_info.name] = plugin_info.plugin_object # Load comment systems, config plugins and register templated shortcodes self._activate_plugins_of_category("CommentSystem") @@ -1325,13 +1306,26 @@ class Nikola(object): self.ALL_PAGE_DEPS['index_read_more_link'] = self.config.get('INDEX_READ_MORE_LINK') self.ALL_PAGE_DEPS['feed_read_more_link'] = self.config.get('FEED_READ_MORE_LINK') - def _activate_plugins_of_category(self, category): + def _activate_plugin(self, plugin_info: PluginInfo) -> None: + plugin_info.plugin_object.set_site(self) + + if plugin_info.category == "TemplateSystem" or self._loading_commands_only: + return + + templates_directory_candidates = [ + plugin_info.source_dir / "templates" / self.template_system.name, + plugin_info.source_dir / plugin_info.module_name / "templates" / self.template_system.name + ] + for candidate in templates_directory_candidates: + if candidate.exists() and candidate.is_dir(): + self.template_system.inject_directory(str(candidate)) + + def _activate_plugins_of_category(self, category) -> typing.List[PluginInfo]: """Activate all the plugins of a given category and return them.""" # this code duplicated in tests/base.py plugins = [] - for plugin_info in self.plugin_manager.getPluginsOfCategory(category): - self.plugin_manager.activatePluginByName(plugin_info.name) - plugin_info.plugin_object.set_site(self) + for plugin_info in self.plugin_manager.get_plugins_of_category(category): + self._activate_plugin(plugin_info) plugins.append(plugin_info) return plugins @@ -1395,13 +1389,12 @@ class Nikola(object): if self._template_system is None: # Load template plugin template_sys_name = utils.get_template_engine(self.THEMES) - pi = self.plugin_manager.getPluginByName( - template_sys_name, "TemplateSystem") + pi = self.plugin_manager.get_plugin_by_name(template_sys_name, "TemplateSystem") if pi is None: sys.stderr.write("Error loading {0} template system " "plugin\n".format(template_sys_name)) sys.exit(1) - self._template_system = pi.plugin_object + self._template_system = typing.cast(TemplateSystem, pi.plugin_object) lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name), "templates") for name in self.THEMES] self._template_system.set_directories(lookup_dirs, @@ -1921,7 +1914,8 @@ class Nikola(object): else: return link else: - return os.path.join(*path) + # URLs should always use forward slash separators, even on Windows + return str(pathlib.PurePosixPath(*path)) def post_path(self, name, lang): """Link to the destination of an element in the POSTS/PAGES settings. @@ -2069,7 +2063,7 @@ class Nikola(object): yield ft task_dep = [] - for pluginInfo in self.plugin_manager.getPluginsOfCategory(plugin_category): + for pluginInfo in self.plugin_manager.get_plugins_of_category(plugin_category): for task in flatten(pluginInfo.plugin_object.gen_tasks()): if 'basename' not in task: raise ValueError("Task {0} does not have a basename".format(task)) @@ -2078,7 +2072,7 @@ class Nikola(object): task['task_dep'] = [] task['task_dep'].extend(self.injected_deps[task['basename']]) yield task - for multi in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"): + for multi in self.plugin_manager.get_plugins_of_category("TaskMultiplier"): flag = False for task in multi.plugin_object.process(task, name): flag = True @@ -2187,7 +2181,7 @@ class Nikola(object): self.timeline = [] self.pages = [] - for p in sorted(self.plugin_manager.getPluginsOfCategory('PostScanner'), key=operator.attrgetter('name')): + for p in sorted(self.plugin_manager.get_plugins_of_category('PostScanner'), key=operator.attrgetter('name')): try: timeline = p.plugin_object.scan() except Exception: |
