From d8172318366f8147c1bee07f0f1cabf3d7256d5d Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 23 Apr 2020 12:49:44 -0600 Subject: [PATCH 01/88] Allow generators to deal with settings that are `pathlib.Path`s --- pelican/generators.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pelican/generators.py b/pelican/generators.py index 63e20a0a..424e9c22 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -18,7 +18,6 @@ from pelican.readers import Readers from pelican.utils import (DateFormatter, copy, mkdir_p, order_content, posixize_path, process_translations) - logger = logging.getLogger(__name__) @@ -322,8 +321,9 @@ class ArticlesGenerator(CachingGenerator): all_articles = list(self.articles) for article in self.articles: all_articles.extend(article.translations) - order_content(all_articles, - order_by=self.settings['ARTICLE_ORDER_BY']) + order_content( + all_articles, order_by=self.settings['ARTICLE_ORDER_BY'] + ) if self.settings.get('FEED_ALL_ATOM'): writer.write_feed( @@ -352,7 +352,7 @@ class ArticlesGenerator(CachingGenerator): self.settings['CATEGORY_FEED_ATOM'].format(slug=cat.slug), self.settings.get( 'CATEGORY_FEED_ATOM_URL', - self.settings['CATEGORY_FEED_ATOM']).format( + str(self.settings['CATEGORY_FEED_ATOM'])).format( slug=cat.slug ), feed_title=cat.name @@ -365,7 +365,7 @@ class ArticlesGenerator(CachingGenerator): self.settings['CATEGORY_FEED_RSS'].format(slug=cat.slug), self.settings.get( 'CATEGORY_FEED_RSS_URL', - self.settings['CATEGORY_FEED_RSS']).format( + str(self.settings['CATEGORY_FEED_RSS'])).format( slug=cat.slug ), feed_title=cat.name, @@ -380,8 +380,9 @@ class ArticlesGenerator(CachingGenerator): self.settings['AUTHOR_FEED_ATOM'].format(slug=auth.slug), self.settings.get( 'AUTHOR_FEED_ATOM_URL', - self.settings['AUTHOR_FEED_ATOM'] - ).format(slug=auth.slug), + str(self.settings['AUTHOR_FEED_ATOM'])).format( + slug=auth.slug + ), feed_title=auth.name ) @@ -392,8 +393,9 @@ class ArticlesGenerator(CachingGenerator): self.settings['AUTHOR_FEED_RSS'].format(slug=auth.slug), self.settings.get( 'AUTHOR_FEED_RSS_URL', - self.settings['AUTHOR_FEED_RSS'] - ).format(slug=auth.slug), + str(self.settings['AUTHOR_FEED_RSS'])).format( + slug=auth.slug + ), feed_title=auth.name, feed_type='rss' ) @@ -408,8 +410,9 @@ class ArticlesGenerator(CachingGenerator): self.settings['TAG_FEED_ATOM'].format(slug=tag.slug), self.settings.get( 'TAG_FEED_ATOM_URL', - self.settings['TAG_FEED_ATOM'] - ).format(slug=tag.slug), + str(self.settings['TAG_FEED_ATOM'])).format( + slug=tag.slug + ), feed_title=tag.name ) @@ -420,8 +423,9 @@ class ArticlesGenerator(CachingGenerator): self.settings['TAG_FEED_RSS'].format(slug=tag.slug), self.settings.get( 'TAG_FEED_RSS_URL', - self.settings['TAG_FEED_RSS'] - ).format(slug=tag.slug), + str(self.settings['TAG_FEED_RSS'])).format( + slug=tag.slug + ), feed_title=tag.name, feed_type='rss' ) @@ -443,7 +447,8 @@ class ArticlesGenerator(CachingGenerator): .format(lang=lang), self.settings.get( 'TRANSLATION_FEED_ATOM_URL', - self.settings['TRANSLATION_FEED_ATOM'] + str( + self.settings['TRANSLATION_FEED_ATOM']) ).format(lang=lang), ) if self.settings.get('TRANSLATION_FEED_RSS'): @@ -454,8 +459,9 @@ class ArticlesGenerator(CachingGenerator): .format(lang=lang), self.settings.get( 'TRANSLATION_FEED_RSS_URL', - self.settings['TRANSLATION_FEED_RSS'] - ).format(lang=lang), + str(self.settings['TRANSLATION_FEED_RSS'])).format( + lang=lang + ), feed_type='rss' ) From cfba3d72beef60de10311a75f83ebb259269fed8 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 23 Apr 2020 13:47:10 -0600 Subject: [PATCH 02/88] fix testing failures when settings could be pathlib.Path --- pelican/contents.py | 2 +- pelican/generators.py | 52 +++++++++++++++++++++++------------------- pelican/settings.py | 4 ++-- pelican/urlwrappers.py | 3 +++ pelican/utils.py | 34 ++++++++++++++++++--------- pelican/writers.py | 2 +- 6 files changed, 58 insertions(+), 39 deletions(-) diff --git a/pelican/contents.py b/pelican/contents.py index 2bb2e3a0..2e29d84e 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -215,7 +215,7 @@ class Content: if not klass: klass = self.__class__.__name__ fq_key = ('{}_{}'.format(klass, key)).upper() - return self.settings[fq_key].format(**self.url_format) + return str(self.settings[fq_key]).format(**self.url_format) def get_url_setting(self, key): if hasattr(self, 'override_' + key): diff --git a/pelican/generators.py b/pelican/generators.py index 424e9c22..d92e8ff8 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -349,12 +349,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['CATEGORY_FEED_ATOM'].format(slug=cat.slug), + str(self.settings['CATEGORY_FEED_ATOM']).format(slug=cat.slug), self.settings.get( 'CATEGORY_FEED_ATOM_URL', - str(self.settings['CATEGORY_FEED_ATOM'])).format( + str(self.settings['CATEGORY_FEED_ATOM']).format( slug=cat.slug - ), + )), feed_title=cat.name ) @@ -362,12 +362,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['CATEGORY_FEED_RSS'].format(slug=cat.slug), + str(self.settings['CATEGORY_FEED_RSS']).format(slug=cat.slug), self.settings.get( 'CATEGORY_FEED_RSS_URL', - str(self.settings['CATEGORY_FEED_RSS'])).format( + str(self.settings['CATEGORY_FEED_RSS']).format( slug=cat.slug - ), + )), feed_title=cat.name, feed_type='rss' ) @@ -377,12 +377,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['AUTHOR_FEED_ATOM'].format(slug=auth.slug), + str(self.settings['AUTHOR_FEED_ATOM']).format(slug=auth.slug), self.settings.get( 'AUTHOR_FEED_ATOM_URL', - str(self.settings['AUTHOR_FEED_ATOM'])).format( + str(self.settings['AUTHOR_FEED_ATOM']).format( slug=auth.slug - ), + )), feed_title=auth.name ) @@ -390,12 +390,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['AUTHOR_FEED_RSS'].format(slug=auth.slug), + str(self.settings['AUTHOR_FEED_RSS']).format(slug=auth.slug), self.settings.get( 'AUTHOR_FEED_RSS_URL', - str(self.settings['AUTHOR_FEED_RSS'])).format( + str(self.settings['AUTHOR_FEED_RSS']).format( slug=auth.slug - ), + )), feed_title=auth.name, feed_type='rss' ) @@ -407,12 +407,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['TAG_FEED_ATOM'].format(slug=tag.slug), + str(self.settings['TAG_FEED_ATOM']).format(slug=tag.slug), self.settings.get( 'TAG_FEED_ATOM_URL', - str(self.settings['TAG_FEED_ATOM'])).format( + str(self.settings['TAG_FEED_ATOM']).format( slug=tag.slug - ), + )), feed_title=tag.name ) @@ -420,12 +420,12 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( arts, self.context, - self.settings['TAG_FEED_RSS'].format(slug=tag.slug), + str(self.settings['TAG_FEED_RSS']).format(slug=tag.slug), self.settings.get( 'TAG_FEED_RSS_URL', - str(self.settings['TAG_FEED_RSS'])).format( + str(self.settings['TAG_FEED_RSS']).format( slug=tag.slug - ), + )), feed_title=tag.name, feed_type='rss' ) @@ -443,27 +443,31 @@ class ArticlesGenerator(CachingGenerator): writer.write_feed( items, self.context, - self.settings['TRANSLATION_FEED_ATOM'] - .format(lang=lang), + str( + self.settings['TRANSLATION_FEED_ATOM'] + ).format(lang=lang), self.settings.get( 'TRANSLATION_FEED_ATOM_URL', str( - self.settings['TRANSLATION_FEED_ATOM']) + self.settings['TRANSLATION_FEED_ATOM'] ).format(lang=lang), ) + ) if self.settings.get('TRANSLATION_FEED_RSS'): writer.write_feed( items, self.context, - self.settings['TRANSLATION_FEED_RSS'] - .format(lang=lang), + str( + self.settings['TRANSLATION_FEED_RSS'] + ).format(lang=lang), self.settings.get( 'TRANSLATION_FEED_RSS_URL', - str(self.settings['TRANSLATION_FEED_RSS'])).format( + str(self.settings['TRANSLATION_FEED_RSS']).format( lang=lang ), feed_type='rss' ) + ) def generate_articles(self, write): """Generate the articles.""" diff --git a/pelican/settings.py b/pelican/settings.py index 7b333de8..ddb6748d 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -406,7 +406,7 @@ def handle_deprecated_settings(settings): for key in ['TRANSLATION_FEED_ATOM', 'TRANSLATION_FEED_RSS' ]: - if settings.get(key) and '%s' in settings[key]: + if settings.get(key) and not isinstance(settings[key], Path) and '%s' in settings[key]: logger.warning('%%s usage in %s is deprecated, use {lang} ' 'instead.', key) try: @@ -423,7 +423,7 @@ def handle_deprecated_settings(settings): 'TAG_FEED_ATOM', 'TAG_FEED_RSS', ]: - if settings.get(key) and '%s' in settings[key]: + if settings.get(key) and not isinstance(settings[key], Path) and '%s' in settings[key]: logger.warning('%%s usage in %s is deprecated, use {slug} ' 'instead.', key) try: diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py index efe09fbc..e00b914c 100644 --- a/pelican/urlwrappers.py +++ b/pelican/urlwrappers.py @@ -1,6 +1,7 @@ import functools import logging import os +import pathlib from pelican.utils import slugify @@ -110,6 +111,8 @@ class URLWrapper: """ setting = "{}_{}".format(self.__class__.__name__.upper(), key) value = self.settings[setting] + if isinstance(value, pathlib.Path): + value = str(value) if not isinstance(value, str): logger.warning('%s is set to %s', setting, value) return value diff --git a/pelican/utils.py b/pelican/utils.py index e82117d3..a3ece8ce 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -3,6 +3,7 @@ import fnmatch import locale import logging import os +import pathlib import re import shutil import sys @@ -921,17 +922,28 @@ def split_all(path): >>> split_all(os.path.join('a', 'b', 'c')) ['a', 'b', 'c'] """ - components = [] - path = path.lstrip('/') - while path: - head, tail = os.path.split(path) - if tail: - components.insert(0, tail) - elif head == path: - components.insert(0, head) - break - path = head - return components + if isinstance(path, str): + components = [] + path = path.lstrip('/') + while path: + head, tail = os.path.split(path) + if tail: + components.insert(0, tail) + elif head == path: + components.insert(0, head) + break + path = head + return components + elif isinstance(path, pathlib.Path): + return path.parts + elif path is None: + return None + else: + raise TypeError( + '"path" was {}, must be string, None, or pathlib.Path'.format( + type(path) + ) + ) def is_selected_for_writing(settings, path): diff --git a/pelican/writers.py b/pelican/writers.py index 9b27a748..379af1f4 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -29,7 +29,7 @@ class Writer: self.urljoiner = posix_join else: self.urljoiner = lambda base, url: urljoin( - base if base.endswith('/') else base + '/', 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 From a13371670970661c7d3f673e9c634df83f7a95bf Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 21 May 2020 21:43:06 -0600 Subject: [PATCH 03/88] flake8 fixes --- pelican/generators.py | 3 +-- pelican/settings.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pelican/generators.py b/pelican/generators.py index d92e8ff8..ecc06851 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -462,11 +462,10 @@ class ArticlesGenerator(CachingGenerator): ).format(lang=lang), self.settings.get( 'TRANSLATION_FEED_RSS_URL', - str(self.settings['TRANSLATION_FEED_RSS']).format( + str(self.settings['TRANSLATION_FEED_RSS'])).format( lang=lang ), feed_type='rss' - ) ) def generate_articles(self, write): diff --git a/pelican/settings.py b/pelican/settings.py index ddb6748d..a5e39161 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -6,6 +6,7 @@ import logging import os import re from os.path import isabs +from pathlib import Path from pelican.log import LimitFilter @@ -406,7 +407,10 @@ def handle_deprecated_settings(settings): for key in ['TRANSLATION_FEED_ATOM', 'TRANSLATION_FEED_RSS' ]: - if settings.get(key) and not isinstance(settings[key], Path) and '%s' in settings[key]: + if ( + settings.get(key) and not isinstance(settings[key], Path) + and '%s' in settings[key] + ): logger.warning('%%s usage in %s is deprecated, use {lang} ' 'instead.', key) try: @@ -423,7 +427,10 @@ def handle_deprecated_settings(settings): 'TAG_FEED_ATOM', 'TAG_FEED_RSS', ]: - if settings.get(key) and not isinstance(settings[key], Path) and '%s' in settings[key]: + if ( + settings.get(key) and not isinstance(settings[key], Path) + and '%s' in settings[key] + ): logger.warning('%%s usage in %s is deprecated, use {slug} ' 'instead.', key) try: From b10c7c699b49b1701692c0d0edc7691ac71c3c8f Mon Sep 17 00:00:00 2001 From: Ryan de Kleer Date: Thu, 15 Sep 2022 16:51:34 -0700 Subject: [PATCH 04/88] Fix false-positive in content gen. test failures Assert equal dirs by return value of diff subprocess, rather than its output. This prevents tests from failing when file contents are the same but the file modes are different. Fix #3042 --- pelican/tests/support.py | 17 ++++++++++++ pelican/tests/test_pelican.py | 51 +++++++++++++++++------------------ 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 55ddf625..8a394395 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -218,6 +218,23 @@ class LogCountHandler(BufferingHandler): ]) +def diff_subproc(first, second): + """ + Return a subprocess that runs a diff on the two paths. + + Check results with:: + + >>> out_stream, err_stream = proc.communicate() + >>> didCheckFail = proc.returnCode != 0 + """ + return subprocess.Popen( + ['git', '--no-pager', 'diff', '--no-ext-diff', '--exit-code', + '-w', first, second], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + class LoggedTestCase(unittest.TestCase): """A test case that captures log messages.""" diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index 389dbb3d..adba32e0 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -3,6 +3,7 @@ import logging import os import subprocess import sys +import unittest from collections.abc import Sequence from shutil import rmtree from tempfile import mkdtemp @@ -10,8 +11,12 @@ from tempfile import mkdtemp from pelican import Pelican from pelican.generators import StaticGenerator from pelican.settings import read_settings -from pelican.tests.support import (LoggedTestCase, locale_available, - mute, unittest) +from pelican.tests.support import ( + LoggedTestCase, + diff_subproc, + locale_available, + mute +) CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) SAMPLES_PATH = os.path.abspath(os.path.join( @@ -54,28 +59,19 @@ class TestPelican(LoggedTestCase): locale.setlocale(locale.LC_ALL, self.old_locale) super().tearDown() - def assertDirsEqual(self, left_path, right_path): - out, err = subprocess.Popen( - ['git', '--no-pager', 'diff', '--no-ext-diff', '--exit-code', - '-w', left_path, right_path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ).communicate() + def assertDirsEqual(self, left_path, right_path, msg=None): + """ + Check if the files are the same (ignoring whitespace) below both paths. + """ + proc = diff_subproc(left_path, right_path) - def ignorable_git_crlf_errors(line): - # Work around for running tests on Windows - for msg in [ - "LF will be replaced by CRLF", - "CRLF will be replaced by LF", - "The file will have its original line endings"]: - if msg in line: - return True - return False - if err: - err = '\n'.join([line for line in err.decode('utf8').splitlines() - if not ignorable_git_crlf_errors(line)]) - assert not out, out - assert not err, err + out, err = proc.communicate() + if proc.returncode != 0: + msg = self._formatMessage( + msg, + "%s and %s differ:\n%s" % (left_path, right_path, err) + ) + raise self.failureException(msg) def test_order_of_generators(self): # StaticGenerator must run last, so it can identify files that @@ -104,7 +100,8 @@ class TestPelican(LoggedTestCase): pelican = Pelican(settings=settings) mute(True)(pelican.run)() self.assertDirsEqual( - self.temp_path, os.path.join(OUTPUT_PATH, 'basic')) + self.temp_path, os.path.join(OUTPUT_PATH, 'basic') + ) self.assertLogCountEqual( count=1, msg="Unable to find.*skipping url replacement", @@ -121,7 +118,8 @@ class TestPelican(LoggedTestCase): pelican = Pelican(settings=settings) mute(True)(pelican.run)() self.assertDirsEqual( - self.temp_path, os.path.join(OUTPUT_PATH, 'custom')) + self.temp_path, os.path.join(OUTPUT_PATH, 'custom') + ) @unittest.skipUnless(locale_available('fr_FR.UTF-8') or locale_available('French'), 'French locale needed') @@ -141,7 +139,8 @@ class TestPelican(LoggedTestCase): pelican = Pelican(settings=settings) mute(True)(pelican.run)() self.assertDirsEqual( - self.temp_path, os.path.join(OUTPUT_PATH, 'custom_locale')) + 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 From bb682973fb9e71e378bc74b9e7130508721f3a44 Mon Sep 17 00:00:00 2001 From: "Martin (mart-e)" Date: Sat, 6 May 2023 08:40:29 +0200 Subject: [PATCH 05/88] Don't specify unlimited feed size by default Having a feed with hundreds of articles, making a very large file, is rarely expected. Set a high fallback value of 100 so it does not change for small sites. Still allow to have infinite feed by setting FEED_MAX_ITEM = None --- docs/settings.rst | 6 +++--- pelican/settings.py | 2 +- pelican/writers.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index e51c6a12..0c0353a3 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -998,10 +998,10 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings: placeholder. If not set, ``TAG_FEED_RSS`` is used both for save location and URL. -.. data:: FEED_MAX_ITEMS +.. data:: FEED_MAX_ITEMS = 100 - Maximum number of items allowed in a feed. Feed item quantity is - unrestricted by default. + Maximum number of items allowed in a feed. Setting to ``None`` will cause the + feed to contains every article. 100 if not specified. .. data:: RSS_FEED_SUMMARY_ONLY = True diff --git a/pelican/settings.py b/pelican/settings.py index 5b495e86..f38b46f0 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -40,7 +40,7 @@ DEFAULT_CONFIG = { 'AUTHOR_FEED_ATOM': 'feeds/{slug}.atom.xml', 'AUTHOR_FEED_RSS': 'feeds/{slug}.rss.xml', 'TRANSLATION_FEED_ATOM': 'feeds/all-{lang}.atom.xml', - 'FEED_MAX_ITEMS': '', + 'FEED_MAX_ITEMS': 100, 'RSS_FEED_SUMMARY_ONLY': True, 'SITEURL': '', 'SITENAME': 'A Pelican Blog', diff --git a/pelican/writers.py b/pelican/writers.py index 73ee4b33..f0280269 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -143,11 +143,9 @@ class Writer: feed = self._create_new_feed(feed_type, feed_title, context) - max_items = len(elements) - if self.settings['FEED_MAX_ITEMS']: - max_items = min(self.settings['FEED_MAX_ITEMS'], max_items) - for i in range(max_items): - self._add_item_to_the_feed(feed, elements[i]) + # FEED_MAX_ITEMS = None means [:None] to get every element + for element in elements[:self.settings['FEED_MAX_ITEMS']]: + self._add_item_to_the_feed(feed, element) signals.feed_generated.send(context, feed=feed) if path: From 5214248344ba75ec1e0ea804bcfcc25bc4b533e2 Mon Sep 17 00:00:00 2001 From: DJ Ramones <50655786+djramones@users.noreply.github.com> Date: Sun, 18 Jun 2023 11:07:39 +0800 Subject: [PATCH 06/88] Implement period_archives common context variable Also, set default patterns for time-period *_ARCHIVE_URL settings. --- docs/settings.rst | 25 +++--- docs/themes.rst | 61 ++++++++++++++ pelican/generators.py | 135 ++++++++++++++++++------------- pelican/settings.py | 6 +- pelican/tests/test_generators.py | 97 ++++++++++++++++++++++ 5 files changed, 255 insertions(+), 69 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index e51c6a12..259b53f5 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -572,33 +572,36 @@ posts for the month at ``posts/2011/Aug/index.html``. This way a reader can remove a portion of your URL and automatically arrive at an appropriate archive of posts, without having to specify a page name. -.. data:: YEAR_ARCHIVE_URL = '' - - The URL to use for per-year archives of your posts. Used only if you have - the ``{url}`` placeholder in ``PAGINATION_PATTERNS``. - .. data:: YEAR_ARCHIVE_SAVE_AS = '' The location to save per-year archives of your posts. -.. data:: MONTH_ARCHIVE_URL = '' +.. data:: YEAR_ARCHIVE_URL = 'posts/{date:%Y}/' - The URL to use for per-month archives of your posts. Used only if you have - the ``{url}`` placeholder in ``PAGINATION_PATTERNS``. + The URL to use for per-year archives of your posts. This default value + matches a ``YEAR_ARCHIVE_SAVE_AS`` setting of + ``posts/{date:%Y}/index.html``. .. data:: MONTH_ARCHIVE_SAVE_AS = '' The location to save per-month archives of your posts. -.. data:: DAY_ARCHIVE_URL = '' +.. data:: MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/' - The URL to use for per-day archives of your posts. Used only if you have the - ``{url}`` placeholder in ``PAGINATION_PATTERNS``. + The URL to use for per-month archives of your posts. This default value + matches a ``MONTH_ARCHIVE_SAVE_AS`` setting of + ``posts/{date:%Y}/{date:%b}/index.html``. .. data:: DAY_ARCHIVE_SAVE_AS = '' The location to save per-day archives of your posts. +.. data:: DAY_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/' + + The URL to use for per-day archives of your posts. This default value + matches a ``DAY_ARCHIVE_SAVE_AS`` setting of + ``posts/{date:%Y}/{date:%b}/{date:%d}/index.html``. + ``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the ``_SAVE_AS`` settings are available, but it is available for any direct template. diff --git a/docs/themes.rst b/docs/themes.rst index fe6337d6..8e46b716 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -71,6 +71,8 @@ All templates will receive the variables defined in your settings file, as long as they are in all-caps. You can access them directly. +.. _common_variables: + Common Variables ---------------- @@ -92,6 +94,10 @@ dates The same list of articles, but ordered by date, ascending. hidden_articles The list of hidden articles drafts The list of draft articles +period_archives A dictionary containing elements related to + time-period archives (if enabled). See the section + :ref:`Listing and Linking to Period Archives + ` for details. authors A list of (author, articles) tuples, containing all the authors and corresponding articles (values) categories A list of (category, articles) tuples, containing @@ -348,6 +354,61 @@ period_archives.html template `_. +.. _period_archives_variable: + +Listing and Linking to Period Archives +"""""""""""""""""""""""""""""""""""""" + +The ``period_archives`` variable can be used to generate a list of links to +the set of period archives that Pelican generates. As a :ref:`common variable +`, it is available for use in any template, so you +can implement such an index in a custom direct template, or in a sidebar +visible across different site pages. + +``period_archives`` is a dict that may contain ``year``, ``month``, and/or +``day`` keys, depending on which ``*_ARCHIVE_SAVE_AS`` settings are enabled. +The corresponding value is a list of dicts, where each dict in turn represents +a time period, with the following keys and values: + +=================== =================================================== +Key Value +=================== =================================================== +period The same tuple as described in + ``period_archives.html``, e.g. + ``(2023, 'June', 18)``. +period_num The same tuple as described in + ``period_archives.html``, e.g. ``(2023, 6, 18)``. +url The URL to the period archive page, e.g. + ``posts/2023/06/18/``. This is controlled by the + corresponding ``*_ARCHIVE_URL`` setting. +save_as The path to the save location of the period archive + page file, e.g. ``posts/2023/06/18/index.html``. + This is used internally by Pelican and is usually + not relevant to themes. +articles A list of :ref:`Article ` objects + that fall under the time period. +dates Same list as ``articles``, but ordered by date. +=================== =================================================== + +Here is an example of how ``period_archives`` can be used in a template: + +.. code-block:: html+jinja + + + +You can change ``period_archives.month`` in the ``for`` statement to +``period_archives.year`` or ``period_archives.day`` as appropriate, depending +on the time period granularity desired. + + Objects ======= diff --git a/pelican/generators.py b/pelican/generators.py index e18531be..ad0f84c4 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -295,6 +295,7 @@ class ArticlesGenerator(CachingGenerator): self.drafts = [] # only drafts in default language self.drafts_translations = [] self.dates = {} + self.period_archives = defaultdict(list) self.tags = defaultdict(list) self.categories = defaultdict(list) self.related_posts = [] @@ -483,64 +484,17 @@ class ArticlesGenerator(CachingGenerator): except PelicanTemplateNotFound: template = self.get_template('archives') - period_save_as = { - 'year': self.settings['YEAR_ARCHIVE_SAVE_AS'], - 'month': self.settings['MONTH_ARCHIVE_SAVE_AS'], - 'day': self.settings['DAY_ARCHIVE_SAVE_AS'], - } + for granularity in list(self.period_archives.keys()): + for period in self.period_archives[granularity]: - period_url = { - 'year': self.settings['YEAR_ARCHIVE_URL'], - 'month': self.settings['MONTH_ARCHIVE_URL'], - 'day': self.settings['DAY_ARCHIVE_URL'], - } - - period_date_key = { - 'year': attrgetter('date.year'), - 'month': attrgetter('date.year', 'date.month'), - 'day': attrgetter('date.year', 'date.month', 'date.day') - } - - def _generate_period_archives(dates, key, save_as_fmt, url_fmt): - """Generate period archives from `dates`, grouped by - `key` and written to `save_as`. - """ - # `dates` is already sorted by date - for _period, group in groupby(dates, key=key): - archive = list(group) - articles = [a for a in self.articles if a in archive] - # arbitrarily grab the first date so that the usual - # format string syntax can be used for specifying the - # period archive dates - date = archive[0].date - save_as = save_as_fmt.format(date=date) - url = url_fmt.format(date=date) context = self.context.copy() + context['period'] = period['period'] + context['period_num'] = period['period_num'] - if key == period_date_key['year']: - context["period"] = (_period,) - context["period_num"] = (_period,) - else: - month_name = calendar.month_name[_period[1]] - if key == period_date_key['month']: - context["period"] = (_period[0], - month_name) - else: - context["period"] = (_period[0], - month_name, - _period[2]) - context["period_num"] = tuple(_period) - - write(save_as, template, context, articles=articles, - dates=archive, template_name='period_archives', - blog=True, url=url, all_articles=self.articles) - - for period in 'year', 'month', 'day': - save_as = period_save_as[period] - url = period_url[period] - if save_as: - key = period_date_key[period] - _generate_period_archives(self.dates, key, save_as, url) + write(period['save_as'], template, context, + articles=period['articles'], dates=period['dates'], + template_name='period_archives', blog=True, + url=period['url'], all_articles=self.articles) def generate_direct_templates(self, write): """Generate direct templates pages""" @@ -680,6 +634,74 @@ class ArticlesGenerator(CachingGenerator): self.dates.sort(key=attrgetter('date'), reverse=self.context['NEWEST_FIRST_ARCHIVES']) + def _build_period_archives(sorted_articles): + period_archives = defaultdict(list) + + period_archives_settings = { + 'year': { + 'save_as': self.settings['YEAR_ARCHIVE_SAVE_AS'], + 'url': self.settings['YEAR_ARCHIVE_URL'], + }, + 'month': { + 'save_as': self.settings['MONTH_ARCHIVE_SAVE_AS'], + 'url': self.settings['MONTH_ARCHIVE_URL'], + }, + 'day': { + 'save_as': self.settings['DAY_ARCHIVE_SAVE_AS'], + 'url': self.settings['DAY_ARCHIVE_URL'], + }, + } + + granularity_key_func = { + 'year': attrgetter('date.year'), + 'month': attrgetter('date.year', 'date.month'), + 'day': attrgetter('date.year', 'date.month', 'date.day'), + } + + for granularity in 'year', 'month', 'day': + save_as_fmt = period_archives_settings[granularity]['save_as'] + url_fmt = period_archives_settings[granularity]['url'] + key_func = granularity_key_func[granularity] + + if not save_as_fmt: + # the archives for this period granularity are not needed + continue + + for period, group in groupby(sorted_articles, key=key_func): + period_archive = {} + + dates = list(group) + period_archive['dates'] = dates + period_archive['articles'] = [ + a for a in self.articles if a in dates + ] + + # use the first date to specify the period archive URL + # and save_as; the specific date used does not matter as + # they all belong to the same period + d = dates[0].date + period_archive['save_as'] = save_as_fmt.format(date=d) + period_archive['url'] = url_fmt.format(date=d) + + if granularity == 'year': + period_archive['period'] = (period,) + period_archive['period_num'] = (period,) + else: + month_name = calendar.month_name[period[1]] + if granularity == 'month': + period_archive['period'] = (period[0], month_name) + else: + period_archive['period'] = (period[0], + month_name, + period[2]) + period_archive['period_num'] = tuple(period) + + period_archives[granularity].append(period_archive) + + return period_archives + + self.period_archives = _build_period_archives(self.dates) + # and generate the output :) # order the categories per name @@ -694,6 +716,9 @@ class ArticlesGenerator(CachingGenerator): 'articles', 'drafts', 'hidden_articles', 'dates', 'tags', 'categories', 'authors', 'related_posts')) + # _update_context flattens dicts, which should not happen to + # period_archives, so we update the context directly for it: + self.context['period_archives'] = self.period_archives self.save_cache() self.readers.save_cache() signals.article_generator_finalized.send(self) diff --git a/pelican/settings.py b/pelican/settings.py index 5b495e86..2405902f 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -90,11 +90,11 @@ DEFAULT_CONFIG = { (1, '{name}{extension}', '{name}{extension}'), (2, '{name}{number}{extension}', '{name}{number}{extension}'), ], - 'YEAR_ARCHIVE_URL': '', + 'YEAR_ARCHIVE_URL': 'posts/{date:%Y}/', 'YEAR_ARCHIVE_SAVE_AS': '', - 'MONTH_ARCHIVE_URL': '', + 'MONTH_ARCHIVE_URL': 'posts/{date:%Y}/{date:%b}/', 'MONTH_ARCHIVE_SAVE_AS': '', - 'DAY_ARCHIVE_URL': '', + 'DAY_ARCHIVE_URL': 'posts/{date:%Y}/{date:%b}/{date:%d}/', 'DAY_ARCHIVE_SAVE_AS': '', 'RELATIVE_URLS': False, 'DEFAULT_LANG': 'en', diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 1bc8aff0..a6fe9731 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -405,6 +405,103 @@ class TestArticlesGenerator(unittest.TestCase): self.assertIn(custom_template, self.articles) self.assertIn(standard_template, self.articles) + def test_period_archives_context(self): + """Test correctness of the period_archives context values.""" + + old_locale = locale.setlocale(locale.LC_ALL) + locale.setlocale(locale.LC_ALL, 'C') + settings = get_settings() + settings['CACHE_PATH'] = self.temp_cache + + # No period archives enabled: + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + period_archives = generator.context['period_archives'] + self.assertEqual(len(period_archives.items()), 0) + + # Year archives enabled: + settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' + settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/' + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + period_archives = generator.context['period_archives'] + self.assertEqual(len(period_archives.items()), 1) + self.assertIn('year', period_archives.keys()) + archive_years = [p['period'][0] for p in period_archives['year']] + self.assertIn(1970, archive_years) + self.assertIn(2014, archive_years) + + # Month archives enabled: + settings['MONTH_ARCHIVE_SAVE_AS'] = \ + 'posts/{date:%Y}/{date:%b}/index.html' + settings['MONTH_ARCHIVE_URL'] = \ + 'posts/{date:%Y}/{date:%b}/' + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + period_archives = generator.context['period_archives'] + self.assertEqual(len(period_archives.items()), 2) + self.assertIn('month', period_archives.keys()) + month_archives_tuples = [p['period'] for p in period_archives['month']] + self.assertIn((1970, 'January'), month_archives_tuples) + self.assertIn((2014, 'February'), month_archives_tuples) + + # Day archives enabled: + settings['DAY_ARCHIVE_SAVE_AS'] = \ + 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' + settings['DAY_ARCHIVE_URL'] = \ + 'posts/{date:%Y}/{date:%b}/{date:%d}/' + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + period_archives = generator.context['period_archives'] + self.assertEqual(len(period_archives.items()), 3) + self.assertIn('day', period_archives.keys()) + day_archives_tuples = [p['period'] for p in period_archives['day']] + self.assertIn((1970, 'January', 1), day_archives_tuples) + self.assertIn((2014, 'February', 9), day_archives_tuples) + + # Further item values tests + filtered_archives = [ + p for p in period_archives['day'] + if p['period'] == (2014, 'February', 9) + ] + self.assertEqual(len(filtered_archives), 1) + sample_archive = filtered_archives[0] + self.assertEqual(sample_archive['period_num'], (2014, 2, 9)) + self.assertEqual( + sample_archive['save_as'], 'posts/2014/Feb/09/index.html') + self.assertEqual( + sample_archive['url'], 'posts/2014/Feb/09/') + articles = [ + d for d in generator.articles if + d.date.year == 2014 and + d.date.month == 2 and + d.date.day == 9 + ] + self.assertEqual(len(sample_archive['articles']), len(articles)) + dates = [ + d for d in generator.dates if + d.date.year == 2014 and + d.date.month == 2 and + d.date.day == 9 + ] + self.assertEqual(len(sample_archive['dates']), len(dates)) + self.assertEqual(sample_archive['dates'][0].title, dates[0].title) + self.assertEqual(sample_archive['dates'][0].date, dates[0].date) + + locale.setlocale(locale.LC_ALL, old_locale) + def test_period_in_timeperiod_archive(self): """ Test that the context of a generated period_archive is passed From 6ba7a0926d506ae3c6ce8e085eb7a7a64c9946f2 Mon Sep 17 00:00:00 2001 From: DJ Ramones <50655786+djramones@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:25:32 +0800 Subject: [PATCH 07/88] Clarify docs on ordering in period_archives var --- docs/themes.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/themes.rst b/docs/themes.rst index 8e46b716..f68f50c0 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -368,7 +368,8 @@ visible across different site pages. ``period_archives`` is a dict that may contain ``year``, ``month``, and/or ``day`` keys, depending on which ``*_ARCHIVE_SAVE_AS`` settings are enabled. The corresponding value is a list of dicts, where each dict in turn represents -a time period, with the following keys and values: +a time period (ordered according to the ``NEWEST_FIRST_ARCHIVES`` setting) +with the following keys and values: =================== =================================================== Key Value @@ -387,7 +388,8 @@ save_as The path to the save location of the period archive not relevant to themes. articles A list of :ref:`Article ` objects that fall under the time period. -dates Same list as ``articles``, but ordered by date. +dates Same list as ``articles``, but ordered according + to the ``NEWEST_FIRST_ARCHIVES`` setting. =================== =================================================== Here is an example of how ``period_archives`` can be used in a template: From 8a5f02ac6072e1fe7c406a7177a5745fc6e05112 Mon Sep 17 00:00:00 2001 From: DJ Ramones <50655786+djramones@users.noreply.github.com> Date: Thu, 17 Aug 2023 02:04:06 +0800 Subject: [PATCH 08/88] Move _build_period_archives out of generate_context So that we don't contribute to further clutter in generate_context. --- pelican/generators.py | 136 +++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/pelican/generators.py b/pelican/generators.py index ad0f84c4..7ab99263 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -634,73 +634,8 @@ class ArticlesGenerator(CachingGenerator): self.dates.sort(key=attrgetter('date'), reverse=self.context['NEWEST_FIRST_ARCHIVES']) - def _build_period_archives(sorted_articles): - period_archives = defaultdict(list) - - period_archives_settings = { - 'year': { - 'save_as': self.settings['YEAR_ARCHIVE_SAVE_AS'], - 'url': self.settings['YEAR_ARCHIVE_URL'], - }, - 'month': { - 'save_as': self.settings['MONTH_ARCHIVE_SAVE_AS'], - 'url': self.settings['MONTH_ARCHIVE_URL'], - }, - 'day': { - 'save_as': self.settings['DAY_ARCHIVE_SAVE_AS'], - 'url': self.settings['DAY_ARCHIVE_URL'], - }, - } - - granularity_key_func = { - 'year': attrgetter('date.year'), - 'month': attrgetter('date.year', 'date.month'), - 'day': attrgetter('date.year', 'date.month', 'date.day'), - } - - for granularity in 'year', 'month', 'day': - save_as_fmt = period_archives_settings[granularity]['save_as'] - url_fmt = period_archives_settings[granularity]['url'] - key_func = granularity_key_func[granularity] - - if not save_as_fmt: - # the archives for this period granularity are not needed - continue - - for period, group in groupby(sorted_articles, key=key_func): - period_archive = {} - - dates = list(group) - period_archive['dates'] = dates - period_archive['articles'] = [ - a for a in self.articles if a in dates - ] - - # use the first date to specify the period archive URL - # and save_as; the specific date used does not matter as - # they all belong to the same period - d = dates[0].date - period_archive['save_as'] = save_as_fmt.format(date=d) - period_archive['url'] = url_fmt.format(date=d) - - if granularity == 'year': - period_archive['period'] = (period,) - period_archive['period_num'] = (period,) - else: - month_name = calendar.month_name[period[1]] - if granularity == 'month': - period_archive['period'] = (period[0], month_name) - else: - period_archive['period'] = (period[0], - month_name, - period[2]) - period_archive['period_num'] = tuple(period) - - period_archives[granularity].append(period_archive) - - return period_archives - - self.period_archives = _build_period_archives(self.dates) + self.period_archives = self._build_period_archives( + self.dates, self.articles, self.settings) # and generate the output :) @@ -723,6 +658,73 @@ class ArticlesGenerator(CachingGenerator): self.readers.save_cache() signals.article_generator_finalized.send(self) + def _build_period_archives(self, sorted_articles, articles, settings): + """ + Compute the groupings of articles, with related attributes, for + per-year, per-month, and per-day archives. + """ + + period_archives = defaultdict(list) + + period_archives_settings = { + 'year': { + 'save_as': settings['YEAR_ARCHIVE_SAVE_AS'], + 'url': settings['YEAR_ARCHIVE_URL'], + }, + 'month': { + 'save_as': settings['MONTH_ARCHIVE_SAVE_AS'], + 'url': settings['MONTH_ARCHIVE_URL'], + }, + 'day': { + 'save_as': settings['DAY_ARCHIVE_SAVE_AS'], + 'url': settings['DAY_ARCHIVE_URL'], + }, + } + + granularity_key_func = { + 'year': attrgetter('date.year'), + 'month': attrgetter('date.year', 'date.month'), + 'day': attrgetter('date.year', 'date.month', 'date.day'), + } + + for granularity in 'year', 'month', 'day': + save_as_fmt = period_archives_settings[granularity]['save_as'] + url_fmt = period_archives_settings[granularity]['url'] + key_func = granularity_key_func[granularity] + + if not save_as_fmt: + # the archives for this period granularity are not needed + continue + + for period, group in groupby(sorted_articles, key=key_func): + archive = {} + + dates = list(group) + archive['dates'] = dates + archive['articles'] = [a for a in articles if a in dates] + + # use the first date to specify the period archive URL + # and save_as; the specific date used does not matter as + # they all belong to the same period + d = dates[0].date + archive['save_as'] = save_as_fmt.format(date=d) + archive['url'] = url_fmt.format(date=d) + + if granularity == 'year': + archive['period'] = (period,) + archive['period_num'] = (period,) + else: + month_name = calendar.month_name[period[1]] + if granularity == 'month': + archive['period'] = (period[0], month_name) + else: + archive['period'] = (period[0], month_name, period[2]) + archive['period_num'] = tuple(period) + + period_archives[granularity].append(archive) + + return period_archives + def generate_output(self, writer): self.generate_feeds(writer) self.generate_pages(writer) From 30adfba1cafb046a77ee52463d430941bd75dc3e Mon Sep 17 00:00:00 2001 From: DJ Ramones <50655786+djramones@users.noreply.github.com> Date: Thu, 17 Aug 2023 02:35:39 +0800 Subject: [PATCH 09/88] Revert *_ARCHIVE_URL default settings to blank --- docs/settings.rst | 26 +++++++++++++------------- pelican/settings.py | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 259b53f5..2950ea82 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -561,11 +561,14 @@ written over time. Example usage:: YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html' + YEAR_ARCHIVE_URL = 'posts/{date:%Y}/' MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html' + MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/' With these settings, Pelican will create an archive of all your posts for the year at (for instance) ``posts/2011/index.html`` and an archive of all your -posts for the month at ``posts/2011/Aug/index.html``. +posts for the month at ``posts/2011/Aug/index.html``. These can be accessed +through the URLs ``posts/2011/`` and ``posts/2011/Aug/``, respectively. .. note:: Period archives work best when the final path segment is ``index.html``. @@ -576,31 +579,28 @@ posts for the month at ``posts/2011/Aug/index.html``. The location to save per-year archives of your posts. -.. data:: YEAR_ARCHIVE_URL = 'posts/{date:%Y}/' +.. data:: YEAR_ARCHIVE_URL = '' - The URL to use for per-year archives of your posts. This default value - matches a ``YEAR_ARCHIVE_SAVE_AS`` setting of - ``posts/{date:%Y}/index.html``. + The URL to use for per-year archives of your posts. You should set this if + you enable per-year archives. .. data:: MONTH_ARCHIVE_SAVE_AS = '' The location to save per-month archives of your posts. -.. data:: MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/' +.. data:: MONTH_ARCHIVE_URL = '' - The URL to use for per-month archives of your posts. This default value - matches a ``MONTH_ARCHIVE_SAVE_AS`` setting of - ``posts/{date:%Y}/{date:%b}/index.html``. + The URL to use for per-month archives of your posts. You should set this if + you enable per-month archives. .. data:: DAY_ARCHIVE_SAVE_AS = '' The location to save per-day archives of your posts. -.. data:: DAY_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/' +.. data:: DAY_ARCHIVE_URL = '' - The URL to use for per-day archives of your posts. This default value - matches a ``DAY_ARCHIVE_SAVE_AS`` setting of - ``posts/{date:%Y}/{date:%b}/{date:%d}/index.html``. + The URL to use for per-day archives of your posts. You should set this if + you enable per-day archives. ``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the ``_SAVE_AS`` settings are available, but it is available for any direct diff --git a/pelican/settings.py b/pelican/settings.py index 2405902f..5b495e86 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -90,11 +90,11 @@ DEFAULT_CONFIG = { (1, '{name}{extension}', '{name}{extension}'), (2, '{name}{number}{extension}', '{name}{number}{extension}'), ], - 'YEAR_ARCHIVE_URL': 'posts/{date:%Y}/', + 'YEAR_ARCHIVE_URL': '', 'YEAR_ARCHIVE_SAVE_AS': '', - 'MONTH_ARCHIVE_URL': 'posts/{date:%Y}/{date:%b}/', + 'MONTH_ARCHIVE_URL': '', 'MONTH_ARCHIVE_SAVE_AS': '', - 'DAY_ARCHIVE_URL': 'posts/{date:%Y}/{date:%b}/{date:%d}/', + 'DAY_ARCHIVE_URL': '', 'DAY_ARCHIVE_SAVE_AS': '', 'RELATIVE_URLS': False, 'DEFAULT_LANG': 'en', From 29185e4ad7fbd6ec900657cc13c529e68454e8af Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 28 Aug 2023 19:21:03 +0100 Subject: [PATCH 10/88] Add GitHub Actions workflow for GitHub Pages Add a GitHub Actions workflow that users can use to publish their Pelican sites to GitHub Pages by running `pelican` on GitHub Actions, without having to run `pelican` locally and push the output directory to a branch. See: https://github.com/getpelican/pelican/discussions/3174 --- .github/workflows/github_pages.yml | 61 +++++++++++++++ docs/tips.rst | 114 ++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/github_pages.yml diff --git a/.github/workflows/github_pages.yml b/.github/workflows/github_pages.yml new file mode 100644 index 00000000..481dd118 --- /dev/null +++ b/.github/workflows/github_pages.yml @@ -0,0 +1,61 @@ +name: Deploy to GitHub Pages +on: + workflow_call: + inputs: + settings: + required: true + description: "The path to your Pelican settings file (`pelican`'s `--settings` option), for example: 'publishconf.py'" + type: string + requirements: + required: false + default: "pelican" + description: "The Python requirements to install, for example to enable markdown and typogrify use: 'pelican[markdown] typogrify'" + type: string + output-path: + required: false + default: "output/" + description: "Where to output the generated files (`pelican`'s `--output` option)" + type: string +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: "pages" + cancel-in-progress: false +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Configure GitHub Pages + id: pages + uses: actions/configure-pages@v3 + - name: Install requirements + run: pip install ${{ inputs.requirements }} + - name: Build Pelican site + run: | + pelican \ + --settings "${{ inputs.settings }}" \ + --extra-settings SITEURL='"${{ steps.pages.outputs.base_url }}"' \ + --output "${{ inputs.output-path }}" + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: ${{ inputs.output-path }} + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 + diff --git a/docs/tips.rst b/docs/tips.rst index 8b9dda15..abd46c8a 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -34,17 +34,30 @@ settings on your AWS console. From there:: Error Document: 404.html -Publishing to GitHub -==================== +Publishing to GitHub Pages +========================== -`GitHub Pages `_ offer an easy -and convenient way to publish Pelican sites. There are `two types of GitHub -Pages `_: -*Project Pages* and *User Pages*. Pelican sites can be published as both -Project Pages and User Pages. +If you use `GitHub `_ for your Pelican site you can +publish your site to `GitHub Pages `_ for free. +Your site will be published to ``https://.github.io`` if it's a user or +organization site or to ``https://.github.io/`` if it's a +project site. It's also possible to `use a custom domain with GitHub Pages `_. -Project Pages -------------- +There are `two ways to publish a site to GitHub Pages `_: + +1. **Publishing from a branch:** run ``pelican`` locally and push the output + directory to a special branch of your GitHub repo. GitHub will then publish + the contents of this branch to your GitHub Pages site. +2. **Publishing with a custom GitHub Actions workflow:** just push the source + files of your Pelican site to your GitHub repo's default branch and have a + custom GitHub Actions workflow run ``pelican`` for you to generate the + output directory and publish it to your GitHub Pages site. This way you + don't need to run ``pelican`` locally. You can even edit your site's source + files using GitHub's web interface and any changes that you commit will be + published. + +Publishing a Project Site to GitHub Pages from a Branch +------------------------------------------------------- To publish a Pelican site as a Project Page you need to *push* the content of the ``output`` dir generated by Pelican to a repository's ``gh-pages`` branch @@ -72,8 +85,8 @@ already exist). The ``git push origin gh-pages`` command updates the remote ``tasks.py``) created by the ``pelican-quickstart`` command publishes the Pelican site as Project Pages, as described above. -User Pages ----------- +Publishing a User Site to GitHub Pages from a Branch +---------------------------------------------------- To publish a Pelican site in the form of User Pages, you need to *push* the content of the ``output`` dir generated by Pelican to the ``master`` branch of @@ -110,6 +123,85 @@ branch of your GitHub repository:: (assuming origin is set to your remote repository). +Publishing to GitHub Pages Using a Custom GitHub Actions Workflow +----------------------------------------------------------------- + +Pelican comes with a `custom workflow `_ +for publishing a Pelican site. To use it: + +1. Enable GitHub Pages in your repo: go to **Settings → Pages** and choose + **GitHub Actions** for the **Source** setting. + +2. Commit a ``.github/workflows/pelican.yml`` file to your repo with these contents: + + .. code-block:: yaml + + name: Deploy to GitHub Pages + on: + push: + branches: ["main"] + workflow_dispatch: + jobs: + deploy: + uses: "getpelican/pelican/.github/workflows/github_pages.yml@master" + permissions: + contents: "read" + pages: "write" + id-token: "write" + with: + settings: "publishconf.py" + +3. Go to the **Actions** tab in your repo + (``https://github.com///actions``) and you should see a + **Deploy to GitHub Pages** action running. + +4. Once the action completes you should see your Pelican site deployed at your + repo's GitHub Pages URL: ``https://.github.io`` for a user or + organization site or ``https://.github.io/>`` for a + project site. + +Notes: + +* You don't need to set ``SITEURL`` in your Pelican settings: the workflow will + set it for you + +* You don't need to commit your ``--output`` / ``OUTPUT_PATH`` directory + (``output/``) to git: the workflow will run ``pelican`` to build the output + directory for you on GitHub Actions + +See `GitHub's docs about reusable workflows `_ +for more information. + +A number of optional inputs can be added to the ``with:`` block when calling +the workflow: + ++--------------+----------+-----------------------------------+--------+---------------+ +| Name | Required | Description | Type | Default | ++==============+==========+===================================+========+===============+ +| settings | Yes | The path to your Pelican settings | string | | +| | | file (``pelican``'s | | | +| | | ``--settings`` option), | | | +| | | for example: ``"publishconf.py"`` | | | ++--------------+----------+-----------------------------------+--------+---------------+ +| requirements | No | The Python requirements to | string | ``"pelican"`` | +| | | install, for example to enable | | | +| | | markdown and typogrify use: | | | +| | | ``"pelican[markdown] typogrify"`` | | | ++--------------+----------+-----------------------------------+--------+---------------+ +| output-path | No | Where to output the generated | string | ``"output/"`` | +| | | files (``pelican``'s ``--output`` | | | +| | | option) | | | ++--------------+----------+-----------------------------------+--------+---------------+ + +For example: + +.. code-block:: yaml + + with: + settings: "publishconf.py" + requirements: "pelican[markdown] typogrify" + output-path: "__output__/" + Custom 404 Pages ---------------- From 48166bd6878352f0289b2761ed117b94b01ded03 Mon Sep 17 00:00:00 2001 From: "Martin (mart-e)" Date: Sun, 4 Jun 2023 12:34:53 +0200 Subject: [PATCH 11/88] Convert Wordpress caption to figure In Wordpress, inserting image with a caption can look like: [caption id="attachment_42" caption="Image Description"][/caption] [caption id="attachment_42"] Image Description[/caption] [caption id="attachment_42"] Image Description[/caption] Replace by an HTML figure tag --- pelican/tests/content/wordpressexport.xml | 47 ++++++++++++++++++++++- pelican/tests/test_importer.py | 26 +++++++++++++ pelican/tools/pelican_import.py | 7 ++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/pelican/tests/content/wordpressexport.xml b/pelican/tests/content/wordpressexport.xml index 9b194e8f..4f5b3651 100644 --- a/pelican/tests/content/wordpressexport.xml +++ b/pelican/tests/content/wordpressexport.xml @@ -685,7 +685,52 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]>_edit_last - + + + Caption on image + http://thisisa.test/?p=176 + Thu, 01 Jan 1970 00:00:00 +0000 + bob + http://thisisa.test/?p=176 + + [/caption] + +[caption attachment_id="43" align="aligncenter" width="300"] This also a pelican[/caption] + +[caption attachment_id="44" align="aligncenter" width="300"] Yet another pelican[/caption] + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]> + + 176 + 2012-02-16 15:52:55 + 0000-00-00 00:00:00 + open + open + caption-on-image + publish + 0 + 0 + post + + 0 + + + _edit_last + + + A custom post in category 4 http://thisisa.test/?p=175 diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 198ee0fe..743cea8c 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -334,6 +334,32 @@ class TestWordpressXmlImporter(unittest.TestCase): 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: + 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) + 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', + ]: + # pandoc 2.x converts into ![text](src) + # pandoc 3.x converts into
src
text
+ self.assertIn(occurence, md) + class TestBuildHeader(unittest.TestCase): def test_build_header(self): diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 7833ebbe..b426de9c 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -107,6 +107,13 @@ def decode_wp_content(content, br=True): return re.sub(pattern, lambda m: dic[m.group()], string) content = _multi_replace(pre_tags, content) + # convert [caption] tags into
+ content = re.sub( + r'\[caption(?:.*?)(?:caption=\"(.*?)\")?\]' + r'((?:\)?(?:\)(?:\<\/a\>)?)\s?(.*?)\[\/caption\]', + r'
\n\2\n
\1\3
\n
', + content) + return content From 5c36cfbb9b878ed7b2183dca6c6708bfb9371c3a Mon Sep 17 00:00:00 2001 From: Lioman Date: Wed, 4 Oct 2023 10:58:18 +0200 Subject: [PATCH 12/88] Only run 'Deploy' action on main repository Deploy action will always fail on forks as the token is not there. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 58333075..f5a709b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,7 +97,7 @@ jobs: environment: Deployment needs: [test, lint, docs] runs-on: ubuntu-latest - if: github.ref=='refs/heads/master' && github.event_name!='pull_request' + if: github.ref=='refs/heads/master' && github.event_name!='pull_request' && github.repository == 'getpelican/pelican' permissions: contents: write From 5d8c03108b376299d614d386abfcb259de7f584a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 3 Oct 2023 11:32:35 +0200 Subject: [PATCH 13/88] Remove Posterous integration Posterous closed down in 2013. The API is no longer accessible and the code did not work in python 3 (base64.encodestring was expecting bytes, not string) --- docs/importer.rst | 19 +++------- pelican/tools/pelican_import.py | 62 ++------------------------------- 2 files changed, 6 insertions(+), 75 deletions(-) diff --git a/docs/importer.rst b/docs/importer.rst index 7b839d30..997a4632 100644 --- a/docs/importer.rst +++ b/docs/importer.rst @@ -11,7 +11,6 @@ software to reStructuredText or Markdown. The supported import formats are: - Blogger XML export - Dotclear export -- Posterous API - Tumblr API - WordPress XML export - RSS/Atom feed @@ -48,16 +47,15 @@ Usage :: - pelican-import [-h] [--blogger] [--dotclear] [--posterous] [--tumblr] [--wpfile] [--feed] + pelican-import [-h] [--blogger] [--dotclear] [--tumblr] [--wpfile] [--feed] [-o OUTPUT] [-m MARKUP] [--dir-cat] [--dir-page] [--strip-raw] [--wp-custpost] - [--wp-attach] [--disable-slugs] [-e EMAIL] [-p PASSWORD] [-b BLOGNAME] - input|api_token|api_key + [--wp-attach] [--disable-slugs] [-b BLOGNAME] + input|api_key Positional arguments -------------------- ============= ============================================================================ ``input`` The input file to read - ``api_token`` (Posterous only) api_token can be obtained from http://posterous.com/api/ ``api_key`` (Tumblr only) api_key can be obtained from https://www.tumblr.com/oauth/apps ============= ============================================================================ @@ -67,7 +65,6 @@ Optional arguments -h, --help Show this help message and exit --blogger Blogger XML export (default: False) --dotclear Dotclear export (default: False) - --posterous Posterous API (default: False) --tumblr Tumblr API (default: False) --wpfile WordPress XML export (default: False) --feed Feed to parse (default: False) @@ -101,10 +98,6 @@ Optional arguments output. With this disabled, your Pelican URLs may not be consistent with your original posts. (default: False) - -e EMAIL, --email=EMAIL - Email used to authenticate Posterous API - -p PASSWORD, --password=PASSWORD - Password used to authenticate Posterous API -b BLOGNAME, --blogname=BLOGNAME Blog name used in Tumblr API @@ -120,13 +113,9 @@ For Dotclear:: $ pelican-import --dotclear -o ~/output ~/backup.txt -for Posterous:: - - $ pelican-import --posterous -o ~/output --email= --password= - For Tumblr:: - $ pelican-import --tumblr -o ~/output --blogname= + $ pelican-import --tumblr -o ~/output --blogname= For WordPress:: diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 7833ebbe..b5108aa4 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -383,51 +383,6 @@ def dc2fields(file): post_format) -def posterous2fields(api_token, email, password): - """Imports posterous posts""" - import base64 - from datetime import timedelta - import json - import urllib.request as urllib_request - - def get_posterous_posts(api_token, email, password, page=1): - base64string = base64.encodestring( - ("{}:{}".format(email, password)).encode('utf-8')).replace('\n', '') - url = ("http://posterous.com/api/v2/users/me/sites/primary/" - "posts?api_token=%s&page=%d") % (api_token, page) - request = urllib_request.Request(url) - request.add_header('Authorization', 'Basic %s' % base64string.decode()) - handle = urllib_request.urlopen(request) - posts = json.loads(handle.read().decode('utf-8')) - return posts - - page = 1 - posts = get_posterous_posts(api_token, email, password, page) - subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] - while len(posts) > 0: - posts = get_posterous_posts(api_token, email, password, page) - page += 1 - - for post in posts: - slug = post.get('slug') - if not slug: - slug = slugify(post.get('title'), regex_subs=subs) - tags = [tag.get('name') for tag in post.get('tags')] - raw_date = post.get('display_date') - date_object = SafeDatetime.strptime( - raw_date[:-6], '%Y/%m/%d %H:%M:%S') - offset = int(raw_date[-5:]) - delta = timedelta(hours=(offset / 100)) - date_object -= delta - date = date_object.strftime('%Y-%m-%d %H:%M') - kind = 'article' # TODO: Recognise pages - status = 'published' # TODO: Find a way for draft posts - - yield (post.get('title'), post.get('body_cleaned'), - slug, date, post.get('user').get('display_name'), - [], tags, status, kind, 'html') - - def tumblr2fields(api_key, blogname): """ Imports Tumblr posts (API v2)""" import json @@ -886,7 +841,7 @@ def fields2pelican( def main(): parser = argparse.ArgumentParser( - description="Transform feed, Blogger, Dotclear, Posterous, Tumblr, or " + description="Transform feed, Blogger, Dotclear, Tumblr, or " "WordPress files into reST (rst) or Markdown (md) files. " "Be sure to have pandoc installed.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -899,9 +854,6 @@ def main(): parser.add_argument( '--dotclear', action='store_true', dest='dotclear', help='Dotclear export') - parser.add_argument( - '--posterous', action='store_true', dest='posterous', - help='Posterous export') parser.add_argument( '--tumblr', action='store_true', dest='tumblr', help='Tumblr export') @@ -952,12 +904,6 @@ def main(): help='Disable storing slugs from imported posts within output. ' 'With this disabled, your Pelican URLs may not be consistent ' 'with your original posts.') - parser.add_argument( - '-e', '--email', dest='email', - help="Email address (posterous import only)") - parser.add_argument( - '-p', '--password', dest='password', - help="Password (posterous import only)") parser.add_argument( '-b', '--blogname', dest='blogname', help="Blog name (Tumblr import only)") @@ -969,8 +915,6 @@ def main(): input_type = 'blogger' elif args.dotclear: input_type = 'dotclear' - elif args.posterous: - input_type = 'posterous' elif args.tumblr: input_type = 'tumblr' elif args.wpfile: @@ -979,7 +923,7 @@ def main(): input_type = 'feed' else: error = ('You must provide either --blogger, --dotclear, ' - '--posterous, --tumblr, --wpfile or --feed options') + '--tumblr, --wpfile or --feed options') exit(error) if not os.path.exists(args.output): @@ -998,8 +942,6 @@ def main(): fields = blogger2fields(args.input) elif input_type == 'dotclear': fields = dc2fields(args.input) - elif input_type == 'posterous': - fields = posterous2fields(args.input, args.email, args.password) elif input_type == 'tumblr': fields = tumblr2fields(args.input, args.blogname) elif input_type == 'wordpress': From ab9e55b398be3861526cda0456f0d9c1e9140700 Mon Sep 17 00:00:00 2001 From: FriedrichFroebel Date: Wed, 11 Oct 2023 19:29:17 +0200 Subject: [PATCH 14/88] Allow dataclasses in settings --- pelican/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pelican/settings.py b/pelican/settings.py index 5b495e86..6196d62a 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -5,6 +5,7 @@ import locale import logging import os import re +import sys from os.path import isabs from pelican.log import LimitFilter @@ -13,6 +14,7 @@ from pelican.log import LimitFilter def load_source(name, path): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod spec.loader.exec_module(mod) return mod From a8fefad33148883d54c26ea341beae4d6d79ad5c Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Fri, 13 Oct 2023 08:01:29 +0200 Subject: [PATCH 15/88] Add OpenGraph metadata to docs via Sphinx extension --- docs/conf.py | 9 ++++++--- requirements/docs.pip | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8f80ba63..f00ed3c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,9 +8,12 @@ sys.path.append(os.path.abspath(os.pardir)) # -- General configuration ---------------------------------------------------- templates_path = ['_templates'] -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.ifconfig', - 'sphinx.ext.extlinks'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.ifconfig", + "sphinx.ext.extlinks", + "sphinxext.opengraph", +] source_suffix = '.rst' master_doc = 'index' project = 'Pelican' diff --git a/requirements/docs.pip b/requirements/docs.pip index dda53a56..6db7c6c8 100644 --- a/requirements/docs.pip +++ b/requirements/docs.pip @@ -1,3 +1,4 @@ sphinx<6.0 +sphinxext-opengraph furo livereload From fab6e1a2c51317b1ff3523755cd82f7b3ea3d433 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Tue, 24 Oct 2023 11:07:25 +0200 Subject: [PATCH 16/88] Fix warning re: future dates setting. Fixes #3184 --- pelican/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pelican/settings.py b/pelican/settings.py index 6196d62a..6680ea96 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -584,7 +584,7 @@ def configure_settings(settings): # check content caching layer and warn of incompatibilities if settings.get('CACHE_CONTENT', False) and \ settings.get('CONTENT_CACHING_LAYER', '') == 'generator' and \ - settings.get('WITH_FUTURE_DATES', False): + not settings.get('WITH_FUTURE_DATES', True): logger.warning( "WITH_FUTURE_DATES conflicts with CONTENT_CACHING_LAYER " "set to 'generator', use 'reader' layer instead") From 1404a2dbc32296b6f2d8ceb46287a63e5bb37724 Mon Sep 17 00:00:00 2001 From: boxydog <93335439+boxydog@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:56:34 -0500 Subject: [PATCH 17/88] Remove newline when importing Tumblr post photos (#3215) Co-authored-by: Dan Frankowski --- pelican/tests/test_importer.py | 79 ++++++++++++++++++++++++++++----- pelican/tools/pelican_import.py | 34 +++++++------- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 743cea8c..3855e382 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -1,7 +1,11 @@ +import datetime import locale import os import re from posixpath import join as posix_join +from unittest.mock import patch + +import dateutil.tz from pelican.settings import DEFAULT_CONFIG from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder, @@ -10,9 +14,12 @@ from pelican.tools.pelican_import import (blogger2fields, build_header, build_markdown_header, decode_wp_content, download_attachments, fields2pelican, - get_attachments, wp2fields) + 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') @@ -34,17 +41,26 @@ except ImportError: LXML = False -@skipIfNoExecutable(['pandoc', '--version']) -@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestBloggerXmlImporter(unittest.TestCase): - +class TestWithOsDefaults(unittest.TestCase): + """Set locale to C and timezone to UTC for tests, then restore.""" def setUp(self): self.old_locale = locale.setlocale(locale.LC_ALL) locale.setlocale(locale.LC_ALL, 'C') - self.posts = blogger2fields(BLOGGER_XML_SAMPLE) + self.old_timezone = datetime.datetime.now(dateutil.tz.tzlocal()).tzname() + os.environ['TZ'] = 'UTC' def tearDown(self): locale.setlocale(locale.LC_ALL, self.old_locale) + os.environ['TZ'] = self.old_timezone + + +@skipIfNoExecutable(['pandoc', '--version']) +@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') +class TestBloggerXmlImporter(TestWithOsDefaults): + + def setUp(self): + super().setUp() + self.posts = blogger2fields(BLOGGER_XML_SAMPLE) def test_recognise_kind_and_title(self): """Check that importer only outputs pages, articles and comments, @@ -85,17 +101,13 @@ class TestBloggerXmlImporter(unittest.TestCase): @skipIfNoExecutable(['pandoc', '--version']) @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestWordpressXmlImporter(unittest.TestCase): +class TestWordpressXmlImporter(TestWithOsDefaults): def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + super().setUp() self.posts = wp2fields(WORDPRESS_XML_SAMPLE) self.custposts = wp2fields(WORDPRESS_XML_SAMPLE, True) - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - def test_ignore_empty_posts(self): self.assertTrue(self.posts) for (title, content, fname, date, author, @@ -477,3 +489,46 @@ class TestWordpressXMLAttachements(unittest.TestCase): self.assertTrue( directory.endswith(posix_join('content', 'article.rst')), directory) + + +class TestTumblrImporter(TestWithOsDefaults): + @patch("pelican.tools.pelican_import._get_tumblr_posts") + def test_posts(self, get): + def get_posts(api_key, blogname, offset=0): + if offset > 0: + return [] + + return [ + { + "type": "photo", + "blog_name": "testy", + "date": "2019-11-07 21:26:40 GMT", + "timestamp": 1573162000, + "format": "html", + "slug": "a-slug", + "tags": [ + "economics" + ], + "state": "published", + + "photos": [ + { + "caption": "", + "original_size": { + "url": "https://..fccdc2360ba7182a.jpg", + "width": 634, + "height": 789 + }, + }] + } + ] + get.side_effect = get_posts + + posts = list(tumblr2fields("api_key", "blogname")) + self.assertEqual( + [('Photo', + '\n', + '2019-11-07-a-slug', '2019-11-07 21:26:40', 'testy', ['photo'], + ['economics'], 'published', 'article', 'html')], + posts, + posts) diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index cd643ec6..474b5cba 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -390,22 +390,22 @@ def dc2fields(file): post_format) -def tumblr2fields(api_key, blogname): - """ Imports Tumblr posts (API v2)""" +def _get_tumblr_posts(api_key, blogname, offset=0): import json import urllib.request as urllib_request + url = ("https://api.tumblr.com/v2/blog/%s.tumblr.com/" + "posts?api_key=%s&offset=%d&filter=raw") % ( + blogname, api_key, offset) + request = urllib_request.Request(url) + handle = urllib_request.urlopen(request) + posts = json.loads(handle.read().decode('utf-8')) + return posts.get('response').get('posts') - def get_tumblr_posts(api_key, blogname, offset=0): - url = ("https://api.tumblr.com/v2/blog/%s.tumblr.com/" - "posts?api_key=%s&offset=%d&filter=raw") % ( - blogname, api_key, offset) - request = urllib_request.Request(url) - handle = urllib_request.urlopen(request) - posts = json.loads(handle.read().decode('utf-8')) - return posts.get('response').get('posts') +def tumblr2fields(api_key, blogname): + """ Imports Tumblr posts (API v2)""" offset = 0 - posts = get_tumblr_posts(api_key, blogname, offset) + posts = _get_tumblr_posts(api_key, blogname, offset) subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] while len(posts) > 0: for post in posts: @@ -428,12 +428,10 @@ def tumblr2fields(api_key, blogname): fmtstr = '![%s](%s)' else: fmtstr = '%s' - content = '' - for photo in post.get('photos'): - content += '\n'.join( - fmtstr % (photo.get('caption'), - photo.get('original_size').get('url'))) - content += '\n\n' + post.get('caption') + content = '\n'.join( + fmtstr % (photo.get('caption'), + photo.get('original_size').get('url')) + for photo in post.get('photos')) elif type == 'quote': if format == 'markdown': fmtstr = '\n\n— %s' @@ -483,7 +481,7 @@ def tumblr2fields(api_key, blogname): tags, status, kind, format) offset += len(posts) - posts = get_tumblr_posts(api_key, blogname, offset) + posts = _get_tumblr_posts(api_key, blogname, offset) def feed2fields(file): From 8fd5d6f51b20b5a842d4dac5304f181a6aa74cd7 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sat, 28 Oct 2023 09:55:16 +0200 Subject: [PATCH 18/88] Fix IRC server in new GitHub issue template --- .github/ISSUE_TEMPLATE/config.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d080329a..9e240bd9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,8 @@ +--- # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true contact_links: -- name: '💬 Pelican IRC Channel on Freenode' - url: https://kiwiirc.com/client/irc.freenode.net/?#pelican +- name: '💬 Pelican IRC Channel' + url: https://web.libera.chat/?#pelican about: | Chat with the community, ask questions, and learn about best practices. From 58e70082e0dc290e2c2d56079e020434e1ee5280 Mon Sep 17 00:00:00 2001 From: Lioman Date: Sat, 28 Oct 2023 10:53:33 +0200 Subject: [PATCH 19/88] Remove python 3.7 build configuration --- .github/workflows/main.yml | 6 ++---- tox.ini | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5a709b6..c0ffd9c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,8 +15,6 @@ jobs: strategy: matrix: config: - - os: ubuntu - python: "3.7" - os: ubuntu python: "3.8" - os: ubuntu @@ -28,9 +26,9 @@ jobs: - os: ubuntu python: "3.12" - os: macos - python: "3.7" + python: "3.10" - os: windows - python: "3.7" + python: "3.10" steps: - uses: actions/checkout@v3 diff --git a/tox.ini b/tox.ini index 8f43fbc5..c31044ca 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,8 @@ [tox] -envlist = py{3.7,3.8,3.9,3.10,3.11.3.12},docs,flake8 +envlist = py{3.8,3.9,3.10,3.11.3.12},docs,flake8 [testenv] basepython = - py3.7: python3.7 py3.8: python3.8 py3.9: python3.9 py3.10: python3.10 From 91d9ef7a7085bf9cf8aa341f23236970e9c6c2b6 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sat, 28 Oct 2023 11:17:48 +0200 Subject: [PATCH 20/88] Add tzdata as dependency in test requirements Otherwise yields the following error with Python 3.10 on Windows: zoneinfo._common.ZoneInfoNotFoundError: 'No time zone found with key UTC' --- requirements/test.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/test.pip b/requirements/test.pip index a7d566f5..2cf1ea1f 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -3,6 +3,7 @@ Pygments==2.14.0 pytest pytest-cov pytest-xdist[psutil] +tzdata # Optional Packages Markdown==3.4.3 From 865f7b10dd22899202c2874bd4232bffc2c36272 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 16 Apr 2023 09:22:27 +0200 Subject: [PATCH 21/88] Replace deprecated pkg_resources importlib.metadata.version() appears to be the anointed replacement. --- pelican/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index bd867988..f0af3429 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -1,4 +1,5 @@ import argparse +import importlib.metadata import json import logging import multiprocessing @@ -30,8 +31,7 @@ from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize) from pelican.writers import Writer try: - __version__ = __import__('pkg_resources') \ - .get_distribution('pelican').version + __version__ = importlib.metadata.version("pelican") except Exception: __version__ = "unknown" From 9c87d8f3a36aab2462cc6e064abb7ed9fe5abca0 Mon Sep 17 00:00:00 2001 From: boxydog <93335439+boxydog@users.noreply.github.com> Date: Sat, 28 Oct 2023 05:56:00 -0500 Subject: [PATCH 22/88] Deal with broken embedded video links when importing from Tumblr (#3218) Co-authored-by: boxydog Co-authored-by: Will Thong --- pelican/tests/test_importer.py | 110 ++++++++++++++++++++++++++++++++ pelican/tools/pelican_import.py | 12 +++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 3855e382..f45f885c 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -532,3 +532,113 @@ class TestTumblrImporter(TestWithOsDefaults): ['economics'], 'published', 'article', 'html')], posts, posts) + + @patch("pelican.tools.pelican_import._get_tumblr_posts") + def test_video_embed(self, get): + def get_posts(api_key, blogname, offset=0): + if offset > 0: + return [] + + return [ + { + "type": "video", + "blog_name": "testy", + "slug": "the-slug", + "date": "2017-07-07 20:31:41 GMT", + "timestamp": 1499459501, + "state": "published", + "format": "html", + "tags": [], + "source_url": "https://href.li/?https://www.youtube.com/a", + "source_title": "youtube.com", + "caption": "

Caption

", + "player": [ + { + "width": 250, + "embed_code": + "" + }, + { + "width": 400, + "embed_code": + "" + }, + { + "width": 500, + "embed_code": + "" + } + ], + "video_type": "youtube", + } + ] + get.side_effect = get_posts + + posts = list(tumblr2fields("api_key", "blogname")) + self.assertEqual( + [('youtube.com', + '

via

\n

Caption

' + '\n' + '\n' + '\n', + '2017-07-07-the-slug', + '2017-07-07 20:31:41', 'testy', ['video'], [], 'published', + 'article', 'html')], + posts, + posts) + + @patch("pelican.tools.pelican_import._get_tumblr_posts") + def test_broken_video_embed(self, get): + def get_posts(api_key, blogname, offset=0): + if offset > 0: + return [] + + return [ + { + "type": "video", + "blog_name": "testy", + "slug": "the-slug", + "date": "2016-08-14 16:37:35 GMT", + "timestamp": 1471192655, + "state": "published", + "format": "html", + "tags": [ + "interviews" + ], + "source_url": + "https://href.li/?https://www.youtube.com/watch?v=b", + "source_title": "youtube.com", + "caption": + "

Caption

", + "player": [ + { + "width": 250, + # If video is gone, embed_code is False + "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', + '

via

\n

Caption

' + '

(This video isn\'t available anymore.)

\n', + '2016-08-14-the-slug', + '2016-08-14 16:37:35', 'testy', ['video'], ['interviews'], + 'published', 'article', 'html')], + posts, + posts) diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 474b5cba..16ce6305 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -459,8 +459,16 @@ def tumblr2fields(api_key, blogname): fmtstr = '

via

\n' source = fmtstr % post.get('source_url') caption = post.get('caption') - players = '\n'.join(player.get('embed_code') - for player in post.get('player')) + players = [ + # If embed_code is False, couldn't get the video + player.get('embed_code') or None + for player in post.get('player')] + # If there are no embeddable players, say so, once + if len(players) > 0 and all( + player is None for player in players): + players = "

(This video isn't available anymore.)

\n" + else: + players = '\n'.join(players) content = source + caption + players elif type == 'answer': title = post.get('question') From b6a9a8333b285e9781d290187c69b5667f428579 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 14:49:55 +0300 Subject: [PATCH 23/88] skip tests that require git if git is not installed and minor tweaks to subprocess handling --- pelican/tests/support.py | 3 ++- pelican/tests/test_pelican.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 8a394395..720e4d0e 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -231,7 +231,8 @@ def diff_subproc(first, second): ['git', '--no-pager', 'diff', '--no-ext-diff', '--exit-code', '-w', first, second], stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, + text=True, ) diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index adba32e0..885c2138 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -15,7 +15,8 @@ from pelican.tests.support import ( LoggedTestCase, diff_subproc, locale_available, - mute + mute, + skipIfNoExecutable, ) CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -69,7 +70,8 @@ class TestPelican(LoggedTestCase): if proc.returncode != 0: msg = self._formatMessage( msg, - "%s and %s differ:\n%s" % (left_path, right_path, err) + "%s and %s differ:\nstdout:\n%s\nstderr\n%s" % + (left_path, right_path, out, err) ) raise self.failureException(msg) @@ -88,6 +90,7 @@ class TestPelican(LoggedTestCase): generator_classes, Sequence, "_get_generator_classes() must return a Sequence to preserve order") + @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 @@ -107,6 +110,7 @@ class TestPelican(LoggedTestCase): msg="Unable to find.*skipping url replacement", level=logging.WARNING) + @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={ @@ -121,6 +125,7 @@ class TestPelican(LoggedTestCase): 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') def test_custom_locale_generation_works(self): From dc427ad9d6e460debe0a667cfd41d6d9ca4133d1 Mon Sep 17 00:00:00 2001 From: Gullumluvl <7593801+Gullumluvl@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:24:16 +0200 Subject: [PATCH 24/88] Strip HTML tags from SITENAME inside title tags. Fixes #3147 (#3149) --- pelican/themes/notmyidea/templates/author.html | 2 +- .../themes/notmyidea/templates/authors.html | 2 +- pelican/themes/notmyidea/templates/base.html | 6 +++--- .../themes/notmyidea/templates/categories.html | 2 +- .../themes/notmyidea/templates/category.html | 2 +- pelican/themes/notmyidea/templates/tag.html | 2 +- pelican/themes/notmyidea/templates/tags.html | 2 +- pelican/themes/simple/templates/archives.html | 2 +- pelican/themes/simple/templates/article.html | 2 +- pelican/themes/simple/templates/author.html | 2 +- pelican/themes/simple/templates/authors.html | 2 +- pelican/themes/simple/templates/base.html | 18 +++++++++--------- .../themes/simple/templates/categories.html | 2 +- pelican/themes/simple/templates/category.html | 2 +- pelican/themes/simple/templates/page.html | 2 +- .../simple/templates/period_archives.html | 2 +- pelican/themes/simple/templates/tag.html | 2 +- pelican/themes/simple/templates/tags.html | 2 +- 18 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pelican/themes/notmyidea/templates/author.html b/pelican/themes/notmyidea/templates/author.html index 0b372902..536ac50d 100644 --- a/pelican/themes/notmyidea/templates/author.html +++ b/pelican/themes/notmyidea/templates/author.html @@ -1,2 +1,2 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - {{ author }}{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ author }}{% endblock %} diff --git a/pelican/themes/notmyidea/templates/authors.html b/pelican/themes/notmyidea/templates/authors.html index e61a332f..b9f87e22 100644 --- a/pelican/themes/notmyidea/templates/authors.html +++ b/pelican/themes/notmyidea/templates/authors.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Authors{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Authors{% endblock %} {% block content %} diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html index 2b302899..8483f268 100644 --- a/pelican/themes/notmyidea/templates/base.html +++ b/pelican/themes/notmyidea/templates/base.html @@ -5,13 +5,13 @@ - {% block title %}{{ SITENAME }}{%endblock%} + {% block title %}{{ SITENAME|striptags }}{%endblock%} {% if FEED_ALL_ATOM %} - + {% endif %} {% if FEED_ALL_RSS %} - + {% endif %} {% block extra_head %}{% endblock extra_head %} {% endblock head %} diff --git a/pelican/themes/notmyidea/templates/categories.html b/pelican/themes/notmyidea/templates/categories.html index 07f6290a..7c5951c7 100644 --- a/pelican/themes/notmyidea/templates/categories.html +++ b/pelican/themes/notmyidea/templates/categories.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Categories{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Categories{% endblock %} {% block content %} diff --git a/pelican/themes/notmyidea/templates/category.html b/pelican/themes/notmyidea/templates/category.html index 56f8e93e..ff14ed76 100644 --- a/pelican/themes/notmyidea/templates/category.html +++ b/pelican/themes/notmyidea/templates/category.html @@ -1,2 +1,2 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - {{ category }}{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ category }}{% endblock %} diff --git a/pelican/themes/notmyidea/templates/tag.html b/pelican/themes/notmyidea/templates/tag.html index 68cdcba6..1e32857b 100644 --- a/pelican/themes/notmyidea/templates/tag.html +++ b/pelican/themes/notmyidea/templates/tag.html @@ -1,2 +1,2 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - {{ tag }}{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ tag }}{% endblock %} diff --git a/pelican/themes/notmyidea/templates/tags.html b/pelican/themes/notmyidea/templates/tags.html index fb099557..a1729321 100644 --- a/pelican/themes/notmyidea/templates/tags.html +++ b/pelican/themes/notmyidea/templates/tags.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Tags{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Tags{% endblock %} {% block content %} diff --git a/pelican/themes/simple/templates/archives.html b/pelican/themes/simple/templates/archives.html index cd129507..b7754c45 100644 --- a/pelican/themes/simple/templates/archives.html +++ b/pelican/themes/simple/templates/archives.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Archives{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Archives{% endblock %} {% block content %}

Archives for {{ SITENAME }}

diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index 6dd0d967..a17f2759 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block html_lang %}{{ article.lang }}{% endblock %} -{% block title %}{{ SITENAME }} - {{ article.title|striptags }}{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ article.title|striptags }}{% endblock %} {% block head %} {{ super() }} diff --git a/pelican/themes/simple/templates/author.html b/pelican/themes/simple/templates/author.html index 79d22c7d..64aadffb 100644 --- a/pelican/themes/simple/templates/author.html +++ b/pelican/themes/simple/templates/author.html @@ -1,6 +1,6 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - Articles by {{ author }}{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Articles by {{ author }}{% endblock %} {% block content_title %}

Articles by {{ author }}

diff --git a/pelican/themes/simple/templates/authors.html b/pelican/themes/simple/templates/authors.html index 9aee5db4..9b80b499 100644 --- a/pelican/themes/simple/templates/authors.html +++ b/pelican/themes/simple/templates/authors.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Authors{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Authors{% endblock %} {% block content %}

Authors on {{ SITENAME }}

diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index 1d8ae843..3125e5aa 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -2,33 +2,33 @@ {% block head %} - {% block title %}{{ SITENAME }}{% endblock title %} + {% block title %}{{ SITENAME|striptags }}{% endblock title %} {% if FEED_ALL_ATOM %} - + {% endif %} {% if FEED_ALL_RSS %} - + {% endif %} {% if FEED_ATOM %} - + {% endif %} {% if FEED_RSS %} - + {% endif %} {% if CATEGORY_FEED_ATOM and category %} - + {% endif %} {% if CATEGORY_FEED_RSS and category %} - + {% endif %} {% if TAG_FEED_ATOM and tag %} - + {% endif %} {% if TAG_FEED_RSS and tag %} - + {% endif %} {% endblock head %} diff --git a/pelican/themes/simple/templates/categories.html b/pelican/themes/simple/templates/categories.html index 7999de43..f099e88f 100644 --- a/pelican/themes/simple/templates/categories.html +++ b/pelican/themes/simple/templates/categories.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Categories{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Categories{% endblock %} {% block content %}

Categories on {{ SITENAME }}

diff --git a/pelican/themes/simple/templates/category.html b/pelican/themes/simple/templates/category.html index d73f6e31..f7889d00 100644 --- a/pelican/themes/simple/templates/category.html +++ b/pelican/themes/simple/templates/category.html @@ -1,6 +1,6 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - {{ category }} category{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ category }} category{% endblock %} {% block content_title %}

Articles in the {{ category }} category

diff --git a/pelican/themes/simple/templates/page.html b/pelican/themes/simple/templates/page.html index 33344eac..eea816a9 100644 --- a/pelican/themes/simple/templates/page.html +++ b/pelican/themes/simple/templates/page.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block html_lang %}{{ page.lang }}{% endblock %} -{% block title %}{{ SITENAME }} - {{ page.title|striptags }}{%endblock%} +{% block title %}{{ SITENAME|striptags }} - {{ page.title|striptags }}{%endblock%} {% block head %} {{ super() }} diff --git a/pelican/themes/simple/templates/period_archives.html b/pelican/themes/simple/templates/period_archives.html index e1ddf626..9cdc354d 100644 --- a/pelican/themes/simple/templates/period_archives.html +++ b/pelican/themes/simple/templates/period_archives.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - {{ period | reverse | join(' ') }} archives{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ period | reverse | join(' ') }} archives{% endblock %} {% block content %}

Archives for {{ period | reverse | join(' ') }}

diff --git a/pelican/themes/simple/templates/tag.html b/pelican/themes/simple/templates/tag.html index 93878134..59725a05 100644 --- a/pelican/themes/simple/templates/tag.html +++ b/pelican/themes/simple/templates/tag.html @@ -1,6 +1,6 @@ {% extends "index.html" %} -{% block title %}{{ SITENAME }} - {{ tag }} tag{% endblock %} +{% block title %}{{ SITENAME|striptags }} - {{ tag }} tag{% endblock %} {% block content_title %}

Articles tagged with {{ tag }}

diff --git a/pelican/themes/simple/templates/tags.html b/pelican/themes/simple/templates/tags.html index b90b0ac3..92c142d2 100644 --- a/pelican/themes/simple/templates/tags.html +++ b/pelican/themes/simple/templates/tags.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ SITENAME }} - Tags{% endblock %} +{% block title %}{{ SITENAME|striptags }} - Tags{% endblock %} {% block content %}

Tags for {{ SITENAME }}

From 83a8059d02af772e1e094e36d8a22fa66b5030db Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 15:55:02 +0300 Subject: [PATCH 25/88] force timestamp conversion in tumblr importer to be UTC with offset and adjust tests --- pelican/tests/test_importer.py | 18 ++++++------------ pelican/tools/pelican_import.py | 11 +++++++---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index f45f885c..0d9586f0 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -1,12 +1,9 @@ -import datetime import locale import os import re from posixpath import join as posix_join from unittest.mock import patch -import dateutil.tz - from pelican.settings import DEFAULT_CONFIG from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder, unittest) @@ -46,12 +43,9 @@ class TestWithOsDefaults(unittest.TestCase): def setUp(self): self.old_locale = locale.setlocale(locale.LC_ALL) locale.setlocale(locale.LC_ALL, 'C') - self.old_timezone = datetime.datetime.now(dateutil.tz.tzlocal()).tzname() - os.environ['TZ'] = 'UTC' def tearDown(self): locale.setlocale(locale.LC_ALL, self.old_locale) - os.environ['TZ'] = self.old_timezone @skipIfNoExecutable(['pandoc', '--version']) @@ -502,7 +496,7 @@ class TestTumblrImporter(TestWithOsDefaults): { "type": "photo", "blog_name": "testy", - "date": "2019-11-07 21:26:40 GMT", + "date": "2019-11-07 21:26:40 UTC", "timestamp": 1573162000, "format": "html", "slug": "a-slug", @@ -528,7 +522,7 @@ class TestTumblrImporter(TestWithOsDefaults): self.assertEqual( [('Photo', '\n', - '2019-11-07-a-slug', '2019-11-07 21:26:40', 'testy', ['photo'], + '2019-11-07-a-slug', '2019-11-07 21:26:40+0000', 'testy', ['photo'], ['economics'], 'published', 'article', 'html')], posts, posts) @@ -544,7 +538,7 @@ class TestTumblrImporter(TestWithOsDefaults): "type": "video", "blog_name": "testy", "slug": "the-slug", - "date": "2017-07-07 20:31:41 GMT", + "date": "2017-07-07 20:31:41 UTC", "timestamp": 1499459501, "state": "published", "format": "html", @@ -583,7 +577,7 @@ class TestTumblrImporter(TestWithOsDefaults): '\n' '\n', '2017-07-07-the-slug', - '2017-07-07 20:31:41', 'testy', ['video'], [], 'published', + '2017-07-07 20:31:41+0000', 'testy', ['video'], [], 'published', 'article', 'html')], posts, posts) @@ -599,7 +593,7 @@ class TestTumblrImporter(TestWithOsDefaults): "type": "video", "blog_name": "testy", "slug": "the-slug", - "date": "2016-08-14 16:37:35 GMT", + "date": "2016-08-14 16:37:35 UTC", "timestamp": 1471192655, "state": "published", "format": "html", @@ -638,7 +632,7 @@ class TestTumblrImporter(TestWithOsDefaults): 'v=b">via

\n

Caption

' '

(This video isn\'t available anymore.)

\n', '2016-08-14-the-slug', - '2016-08-14 16:37:35', 'testy', ['video'], ['interviews'], + '2016-08-14 16:37:35+0000', 'testy', ['video'], ['interviews'], 'published', 'article', 'html')], posts, posts) diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 16ce6305..44568161 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import argparse +import datetime import logging import os import re @@ -416,10 +417,12 @@ def tumblr2fields(api_key, blogname): slug = post.get('slug') or slugify(title, regex_subs=subs) tags = post.get('tags') timestamp = post.get('timestamp') - date = SafeDatetime.fromtimestamp(int(timestamp)).strftime( - "%Y-%m-%d %H:%M:%S") - slug = SafeDatetime.fromtimestamp(int(timestamp)).strftime( - "%Y-%m-%d-") + slug + date = SafeDatetime.fromtimestamp( + int(timestamp), tz=datetime.timezone.utc + ).strftime("%Y-%m-%d %H:%M:%S%z") + slug = SafeDatetime.fromtimestamp( + int(timestamp), tz=datetime.timezone.utc + ).strftime("%Y-%m-%d-") + slug format = post.get('format') content = post.get('body') type = post.get('type') From 11c13ceae1c72bd786a1b09657de2926eb6ae267 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 16:31:05 +0300 Subject: [PATCH 26/88] use a tempfile for intermediate html file for pandoc in importer --- pelican/tools/pelican_import.py | 64 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 44568161..95e196ba 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -7,6 +7,7 @@ import os import re import subprocess import sys +import tempfile import time from collections import defaultdict from html import unescape @@ -785,9 +786,8 @@ def fields2pelican( print(out_filename) if in_markup in ('html', 'wp-html'): - html_filename = os.path.join(output_path, filename + '.html') - - with open(html_filename, 'w', encoding='utf-8') as fp: + with tempfile.TemporaryDirectory() as tmpdir: + html_filename = os.path.join(tmpdir, 'pandoc-input.html') # Replace newlines with paragraphs wrapped with

so # HTML is valid before conversion if in_markup == 'wp-html': @@ -796,41 +796,39 @@ def fields2pelican( paragraphs = content.splitlines() paragraphs = ['

{}

'.format(p) for p in paragraphs] new_content = ''.join(paragraphs) + with open(html_filename, 'w', encoding='utf-8') as fp: + fp.write(new_content) - fp.write(new_content) + if pandoc_version < (2,): + parse_raw = '--parse-raw' if not strip_raw else '' + wrap_none = '--wrap=none' \ + if pandoc_version >= (1, 16) else '--no-wrap' + cmd = ('pandoc --normalize {0} --from=html' + ' --to={1} {2} -o "{3}" "{4}"') + cmd = cmd.format(parse_raw, + out_markup if out_markup != 'markdown' else "gfm", + wrap_none, + out_filename, html_filename) + else: + from_arg = '-f html+raw_html' if not strip_raw else '-f html' + cmd = ('pandoc {0} --to={1}-smart --wrap=none -o "{2}" "{3}"') + cmd = cmd.format(from_arg, + out_markup if out_markup != 'markdown' else "gfm", + out_filename, html_filename) - if pandoc_version < (2,): - parse_raw = '--parse-raw' if not strip_raw else '' - wrap_none = '--wrap=none' \ - if pandoc_version >= (1, 16) else '--no-wrap' - cmd = ('pandoc --normalize {0} --from=html' - ' --to={1} {2} -o "{3}" "{4}"') - cmd = cmd.format(parse_raw, - out_markup if out_markup != 'markdown' else "gfm", - wrap_none, - out_filename, html_filename) - else: - from_arg = '-f html+raw_html' if not strip_raw else '-f html' - cmd = ('pandoc {0} --to={1}-smart --wrap=none -o "{2}" "{3}"') - cmd = cmd.format(from_arg, - out_markup if out_markup != 'markdown' else "gfm", - out_filename, html_filename) + try: + rc = subprocess.call(cmd, shell=True) + if rc < 0: + error = 'Child was terminated by signal %d' % -rc + exit(error) - try: - rc = subprocess.call(cmd, shell=True) - if rc < 0: - error = 'Child was terminated by signal %d' % -rc + elif rc > 0: + error = 'Please, check your Pandoc installation.' + exit(error) + except OSError as e: + error = 'Pandoc execution failed: %s' % e exit(error) - elif rc > 0: - error = 'Please, check your Pandoc installation.' - exit(error) - except OSError as e: - error = 'Pandoc execution failed: %s' % e - exit(error) - - os.remove(html_filename) - with open(out_filename, encoding='utf-8') as fs: content = fs.read() if out_markup == 'markdown': From 61ca47c5194f33ad28e96cf965ba9f582d6d7909 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Wed, 21 Jun 2023 22:01:38 +0100 Subject: [PATCH 27/88] Use watchfiles as a file watching backend This doesn't use polling unless absolutely necessarily, making it more efficient. It also reduces the amount of first-party code required, and simplifies working out which files are being watched. --- pelican/__init__.py | 25 ++--- pelican/tests/test_utils.py | 86 ----------------- pelican/utils.py | 180 +++++------------------------------- pyproject.toml | 1 + 4 files changed, 32 insertions(+), 260 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index f0af3429..a4f5b38e 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -27,7 +27,7 @@ from pelican.plugins._utils import get_plugin_name, load_plugins from pelican.readers import Readers from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer from pelican.settings import read_settings -from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize) +from pelican.utils import (wait_for_changes, clean_output_dir, maybe_pluralize) from pelican.writers import Writer try: @@ -452,26 +452,19 @@ def autoreload(args, excqueue=None): console.print(' --- AutoReload Mode: Monitoring `content`, `theme` and' ' `settings` for changes. ---') pelican, settings = get_instance(args) - watcher = FileSystemWatcher(args.settings, Readers, settings) - sleep = False + settings_file = os.path.abspath(args.settings) while True: try: - # Don't sleep first time, but sleep afterwards to reduce cpu load - if sleep: - time.sleep(0.5) - else: - sleep = True + changed_files = wait_for_changes(args.settings, Readers, settings) - modified = watcher.check() + changed_files = {c[1] for c in changed_files} - if modified['settings']: + if settings_file in changed_files: pelican, settings = get_instance(args) - watcher.update_watchers(settings) - if any(modified.values()): - console.print('\n-> Modified: {}. re-generating...'.format( - ', '.join(k for k, v in modified.items() if v))) - pelican.run() + console.print('\n-> Modified: {}. re-generating...'.format( + ', '.join(changed_files))) + pelican.run() except KeyboardInterrupt: if excqueue is not None: @@ -558,8 +551,6 @@ def main(argv=None): listen(settings.get('BIND'), settings.get('PORT'), settings.get("OUTPUT_PATH")) else: - watcher = FileSystemWatcher(args.settings, Readers, settings) - watcher.check() with console.status("Generating..."): pelican.run() except KeyboardInterrupt: diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index e1758726..7ff1af7a 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -412,92 +412,6 @@ class TestUtils(LoggedTestCase): self.assertNotIn(a_arts[4], b_arts[5].translations) self.assertNotIn(a_arts[5], b_arts[4].translations) - def test_filesystemwatcher(self): - def create_file(name, content): - with open(name, 'w') as f: - f.write(content) - - # disable logger filter - from pelican.utils import logger - logger.disable_filter() - - # create a temp "project" dir - root = mkdtemp() - content_path = os.path.join(root, 'content') - static_path = os.path.join(root, 'content', 'static') - config_file = os.path.join(root, 'config.py') - theme_path = os.path.join(root, 'mytheme') - - # populate - os.mkdir(content_path) - os.mkdir(theme_path) - create_file(config_file, - 'PATH = "content"\n' - 'THEME = "mytheme"\n' - 'STATIC_PATHS = ["static"]') - - t = time.time() - 1000 # make sure it's in the "past" - os.utime(config_file, (t, t)) - settings = read_settings(config_file) - - watcher = utils.FileSystemWatcher(config_file, Readers, settings) - # should get a warning for static not not existing - self.assertLogCountEqual(1, 'Watched path does not exist: .*static') - - # create it and update config - os.mkdir(static_path) - watcher.update_watchers(settings) - # no new warning - self.assertLogCountEqual(1, 'Watched path does not exist: .*static') - - # get modified values - modified = watcher.check() - # empty theme and content should raise warnings - self.assertLogCountEqual(1, 'No valid files found in content') - self.assertLogCountEqual(1, 'Empty theme folder. Using `basic` theme') - - self.assertIsNone(modified['content']) # empty - self.assertIsNone(modified['theme']) # empty - self.assertIsNone(modified['[static]static']) # empty - self.assertTrue(modified['settings']) # modified, first time - - # add a content, add file to theme and check again - create_file(os.path.join(content_path, 'article.md'), - 'Title: test\n' - 'Date: 01-01-2020') - - create_file(os.path.join(theme_path, 'dummy'), - 'test') - - modified = watcher.check() - # no new warning - self.assertLogCountEqual(1, 'No valid files found in content') - self.assertLogCountEqual(1, 'Empty theme folder. Using `basic` theme') - - self.assertIsNone(modified['[static]static']) # empty - self.assertFalse(modified['settings']) # not modified - self.assertTrue(modified['theme']) # modified - self.assertTrue(modified['content']) # modified - - # change config, remove static path - create_file(config_file, - 'PATH = "content"\n' - 'THEME = "mytheme"\n' - 'STATIC_PATHS = []') - - settings = read_settings(config_file) - watcher.update_watchers(settings) - - modified = watcher.check() - self.assertNotIn('[static]static', modified) # should be gone - self.assertTrue(modified['settings']) # modified - self.assertFalse(modified['content']) # not modified - self.assertFalse(modified['theme']) # not modified - - # cleanup - logger.enable_filter() - shutil.rmtree(root) - def test_clean_output_dir(self): retention = () test_directory = os.path.join(self.temp_output, diff --git a/pelican/utils.py b/pelican/utils.py index d8cf15b4..4832e0c1 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -24,6 +24,8 @@ except ModuleNotFoundError: from backports.zoneinfo import ZoneInfo from markupsafe import Markup +import watchfiles + logger = logging.getLogger(__name__) @@ -755,167 +757,31 @@ def order_content(content_list, order_by='slug'): return content_list -class FileSystemWatcher: - def __init__(self, settings_file, reader_class, settings=None): - self.watchers = { - 'settings': FileSystemWatcher.file_watcher(settings_file) - } +def wait_for_changes(settings_file, reader_class, settings): + new_extensions = set(reader_class(settings).extensions) + content_path = settings.get('PATH', '') + theme_path = settings.get('THEME', '') + ignore_files = set(settings.get('IGNORE_FILES', [])) - self.settings = None - self.reader_class = reader_class - self._extensions = None - self._content_path = None - self._theme_path = None - self._ignore_files = None + watching_paths = [ + settings_file, + theme_path, + content_path, + ] - if settings is not None: - self.update_watchers(settings) + watching_paths.extend( + os.path.join(content_path, path) for path in settings.get('STATIC_PATHS', []) + ) - def update_watchers(self, settings): - new_extensions = set(self.reader_class(settings).extensions) - new_content_path = settings.get('PATH', '') - new_theme_path = settings.get('THEME', '') - new_ignore_files = set(settings.get('IGNORE_FILES', [])) + watching_paths = [os.path.abspath(p) for p in watching_paths if p and os.path.exists(p)] - extensions_changed = new_extensions != self._extensions - content_changed = new_content_path != self._content_path - theme_changed = new_theme_path != self._theme_path - ignore_changed = new_ignore_files != self._ignore_files - - # Refresh content watcher if related settings changed - if extensions_changed or content_changed or ignore_changed: - self.add_watcher('content', - new_content_path, - new_extensions, - new_ignore_files) - - # Refresh theme watcher if related settings changed - if theme_changed or ignore_changed: - self.add_watcher('theme', - new_theme_path, - [''], - new_ignore_files) - - # Watch STATIC_PATHS - old_static_watchers = set(key - for key in self.watchers - if key.startswith('[static]')) - - for path in settings.get('STATIC_PATHS', []): - key = '[static]{}'.format(path) - if ignore_changed or (key not in self.watchers): - self.add_watcher( - key, - os.path.join(new_content_path, path), - [''], - new_ignore_files) - if key in old_static_watchers: - old_static_watchers.remove(key) - - # cleanup removed static watchers - for key in old_static_watchers: - del self.watchers[key] - - # update values - self.settings = settings - self._extensions = new_extensions - self._content_path = new_content_path - self._theme_path = new_theme_path - self._ignore_files = new_ignore_files - - def check(self): - '''return a key:watcher_status dict for all watchers''' - result = {key: next(watcher) for key, watcher in self.watchers.items()} - - # Various warnings - if result.get('content') is None: - reader_descs = sorted( - { - ' | %s (%s)' % (type(r).__name__, ', '.join(r.file_extensions)) - for r in self.reader_class(self.settings).readers.values() - if r.enabled - } - ) - logger.warning( - 'No valid files found in content for the active readers:\n' - + '\n'.join(reader_descs)) - - if result.get('theme') is None: - logger.warning('Empty theme folder. Using `basic` theme.') - - return result - - def add_watcher(self, key, path, extensions=[''], ignores=[]): - watcher = self.get_watcher(path, extensions, ignores) - if watcher is not None: - self.watchers[key] = watcher - - def get_watcher(self, path, extensions=[''], ignores=[]): - '''return a watcher depending on path type (file or folder)''' - if not os.path.exists(path): - logger.warning("Watched path does not exist: %s", path) - return None - - if os.path.isdir(path): - return self.folder_watcher(path, extensions, ignores) - else: - return self.file_watcher(path) - - @staticmethod - def folder_watcher(path, extensions, ignores=[]): - '''Generator for monitoring a folder for modifications. - - Returns a boolean indicating if files are changed since last check. - Returns None if there are no matching files in the folder''' - - def file_times(path): - '''Return `mtime` for each file in path''' - - for root, dirs, files in os.walk(path, followlinks=True): - dirs[:] = [x for x in dirs if not x.startswith(os.curdir)] - - for f in files: - valid_extension = f.endswith(tuple(extensions)) - file_ignored = any( - fnmatch.fnmatch(f, ignore) for ignore in ignores - ) - if valid_extension and not file_ignored: - try: - yield os.stat(os.path.join(root, f)).st_mtime - except OSError as e: - logger.warning('Caught Exception: %s', e) - - LAST_MTIME = 0 - while True: - try: - mtime = max(file_times(path)) - if mtime > LAST_MTIME: - LAST_MTIME = mtime - yield True - except ValueError: - yield None - else: - yield False - - @staticmethod - def file_watcher(path): - '''Generator for monitoring a file for modifications''' - LAST_MTIME = 0 - while True: - if path: - try: - mtime = os.stat(path).st_mtime - except OSError as e: - logger.warning('Caught Exception: %s', e) - continue - - if mtime > LAST_MTIME: - LAST_MTIME = mtime - yield True - else: - yield False - else: - yield None + return next(watchfiles.watch( + *watching_paths, + watch_filter=watchfiles.DefaultFilter( + ignore_entity_patterns=[fnmatch.translate(pattern) for pattern in ignore_files] + ), + rust_timeout=0 + )) def set_date_tzinfo(d, tz_name=None): diff --git a/pyproject.toml b/pyproject.toml index 826c1179..788b8dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ rich = ">=10.1" unidecode = ">=1.1" markdown = {version = ">=3.1", optional = true} backports-zoneinfo = {version = "^0.2.1", python = "<3.9"} +watchfiles = "^0.19.0" [tool.poetry.dev-dependencies] BeautifulSoup4 = "^4.9" From 7643e0e92b901d71ee1f4996e019682982abb3df Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Fri, 14 Jul 2023 16:50:43 +0100 Subject: [PATCH 28/88] Make sure the package depends on `watchfiles` --- setup.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 18eedb00..4ffee0cb 100755 --- a/setup.py +++ b/setup.py @@ -8,9 +8,18 @@ from setuptools import find_packages, setup version = "4.8.0" -requires = ['feedgenerator >= 1.9', 'jinja2 >= 2.7', 'pygments', - 'docutils>=0.15', 'blinker', 'unidecode', 'python-dateutil', - 'rich', 'backports-zoneinfo[tzdata] >= 0.2; python_version<"3.9"'] +requires = [ + 'feedgenerator >= 1.9', + 'jinja2 >= 2.7', + 'pygments', + 'docutils>=0.15', + 'blinker', + 'unidecode', + 'python-dateutil', + 'rich', + 'backports-zoneinfo[tzdata] >= 0.2; python_version<"3.9"', + 'watchfiles' +] entry_points = { 'console_scripts': [ From 5519efef2e24a3fef506a1e19222fc49053e39a6 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Tue, 15 Aug 2023 17:45:50 +0100 Subject: [PATCH 29/88] Log watching files which don't exist --- pelican/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pelican/utils.py b/pelican/utils.py index 4832e0c1..d8a19e4b 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -773,7 +773,11 @@ def wait_for_changes(settings_file, reader_class, settings): os.path.join(content_path, path) for path in settings.get('STATIC_PATHS', []) ) - watching_paths = [os.path.abspath(p) for p in watching_paths if p and os.path.exists(p)] + watching_paths = [os.path.abspath(p) for p in watching_paths if p] + + for path in watching_paths: + if not os.path.exists(path): + logger.warning("Unable to watch path '%s' as it does not exist.", path) return next(watchfiles.watch( *watching_paths, From b388057d664daacb7a318e3334fdb975841b0962 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Tue, 15 Aug 2023 17:47:04 +0100 Subject: [PATCH 30/88] Remove unused extensions list --- pelican/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pelican/utils.py b/pelican/utils.py index d8a19e4b..23c6fe1c 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -758,7 +758,6 @@ def order_content(content_list, order_by='slug'): def wait_for_changes(settings_file, reader_class, settings): - new_extensions = set(reader_class(settings).extensions) content_path = settings.get('PATH', '') theme_path = settings.get('THEME', '') ignore_files = set(settings.get('IGNORE_FILES', [])) From 631ac1bdb39d3ebe3d7dd94aa4e33a187ddb52c5 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Tue, 15 Aug 2023 17:49:58 +0100 Subject: [PATCH 31/88] Cleanup imports --- pelican/__init__.py | 2 +- pelican/tests/test_utils.py | 2 -- pelican/utils.py | 8 ++++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index a4f5b38e..e0526b1f 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -27,7 +27,7 @@ from pelican.plugins._utils import get_plugin_name, load_plugins from pelican.readers import Readers from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer from pelican.settings import read_settings -from pelican.utils import (wait_for_changes, clean_output_dir, maybe_pluralize) +from pelican.utils import clean_output_dir, maybe_pluralize, wait_for_changes from pelican.writers import Writer try: diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 7ff1af7a..d8296285 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -2,7 +2,6 @@ import locale import logging import os import shutil -import time from datetime import timezone from sys import platform from tempfile import mkdtemp @@ -14,7 +13,6 @@ except ModuleNotFoundError: from pelican import utils from pelican.generators import TemplatePagesGenerator -from pelican.readers import Readers from pelican.settings import read_settings from pelican.tests.support import (LoggedTestCase, get_article, locale_available, unittest) diff --git a/pelican/utils.py b/pelican/utils.py index 23c6fe1c..1225e479 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -760,7 +760,9 @@ def order_content(content_list, order_by='slug'): def wait_for_changes(settings_file, reader_class, settings): content_path = settings.get('PATH', '') theme_path = settings.get('THEME', '') - ignore_files = set(settings.get('IGNORE_FILES', [])) + ignore_files = set( + fnmatch.translate(pattern) for pattern in settings.get('IGNORE_FILES', []) + ) watching_paths = [ settings_file, @@ -780,9 +782,7 @@ def wait_for_changes(settings_file, reader_class, settings): return next(watchfiles.watch( *watching_paths, - watch_filter=watchfiles.DefaultFilter( - ignore_entity_patterns=[fnmatch.translate(pattern) for pattern in ignore_files] - ), + watch_filter=watchfiles.DefaultFilter(ignore_entity_patterns=ignore_files), rust_timeout=0 )) From b289dcea82bac461cf879be5935e64a1ff192622 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 17:30:45 +0300 Subject: [PATCH 32/88] don't watch not existing paths --- pelican/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pelican/utils.py b/pelican/utils.py index 1225e479..84a18deb 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -764,21 +764,25 @@ def wait_for_changes(settings_file, reader_class, settings): fnmatch.translate(pattern) for pattern in settings.get('IGNORE_FILES', []) ) - watching_paths = [ + candidate_paths = [ settings_file, theme_path, content_path, ] - watching_paths.extend( + candidate_paths.extend( os.path.join(content_path, path) for path in settings.get('STATIC_PATHS', []) ) - watching_paths = [os.path.abspath(p) for p in watching_paths if p] - - for path in watching_paths: + watching_paths = [] + for path in candidate_paths: + if not path: + continue + path = os.path.abspath(path) if not os.path.exists(path): logger.warning("Unable to watch path '%s' as it does not exist.", path) + else: + watching_paths.append(path) return next(watchfiles.watch( *watching_paths, From 43e513f218e66ccdf128e5de5a4db279f47ff063 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 17:37:56 +0300 Subject: [PATCH 33/88] run pelican first before waiting for changes --- pelican/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index e0526b1f..fcdda8a4 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -455,8 +455,9 @@ def autoreload(args, excqueue=None): settings_file = os.path.abspath(args.settings) while True: try: - changed_files = wait_for_changes(args.settings, Readers, settings) + pelican.run() + changed_files = wait_for_changes(args.settings, Readers, settings) changed_files = {c[1] for c in changed_files} if settings_file in changed_files: @@ -464,7 +465,6 @@ def autoreload(args, excqueue=None): console.print('\n-> Modified: {}. re-generating...'.format( ', '.join(changed_files))) - pelican.run() except KeyboardInterrupt: if excqueue is not None: From f342dc309758a7f1197f2797b8965653f538b68a Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 08:00:27 -0700 Subject: [PATCH 34/88] Add macOS testing for 3.11/3.12 --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0ffd9c6..6f146631 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,10 @@ jobs: python: "3.12" - os: macos python: "3.10" + - os: macos + python: "3.11" + - os: macos + python: "3.12" - os: windows python: "3.10" From 7dfc799f255815de1665e257a93987598aab95f4 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 10:44:39 -0700 Subject: [PATCH 35/88] Use ruff in pre-commit --- .pre-commit-config.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c0c85b0..f68521e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,13 +13,11 @@ repos: - id: end-of-file-fixer - id: forbid-new-submodules - id: trailing-whitespace - - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.0 hooks: - - id: flake8 - name: Flake8 on commit diff - description: This hook limits Flake8 checks to changed lines of code. - entry: bash - args: [-c, 'git diff HEAD | flake8 --diff --max-line-length=88'] + - id: ruff + - id: ruff-format + args: ["--check"] exclude: ^pelican/tests/output/ From 19c797af5e08dd7ccc38f939c5fc9c9bbe862e54 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 10:50:17 -0700 Subject: [PATCH 36/88] Add support to verify windows, too --- .github/workflows/main.yml | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6f146631..ba3aef55 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,37 +9,25 @@ env: jobs: test: - name: Test - ${{ matrix.config.python }} - ${{ matrix.config.os }} - runs-on: ${{ matrix.config.os }}-latest + name: Test - ${{ matrix.python }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest strategy: matrix: - config: + os: [ubuntu, macos, windows] + python: ["3.10", "3.11", "3.12"] + include: - os: ubuntu python: "3.8" - os: ubuntu python: "3.9" - - os: ubuntu - python: "3.10" - - os: ubuntu - python: "3.11" - - os: ubuntu - python: "3.12" - - os: macos - python: "3.10" - - os: macos - python: "3.11" - - os: macos - python: "3.12" - - os: windows - python: "3.10" steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.config.python }} + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: - python-version: ${{ matrix.config.python }} + python-version: ${{ matrix.python }} cache: "pip" cache-dependency-path: "**/requirements/*" - name: Install locale (Linux) @@ -58,7 +46,7 @@ jobs: echo "===== PANDOC =====" pandoc --version | head -2 - name: Run tests - run: tox -e py${{ matrix.config.python }} + run: tox -e py${{ matrix.python }} lint: name: Lint From 58fd8553850a161f1656599f4d0d40f43eefbf82 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 10:54:09 -0700 Subject: [PATCH 37/88] inv task now uses ruff --- pyproject.toml | 1 + tasks.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 826c1179..b3eaa0d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ pytest = "^7.1" pytest-cov = "^4.0" pytest-sugar = "^0.9.5" pytest-xdist = "^2.0" +ruff = "^0.1.3" tox = {version = "^3.13", optional = true} flake8 = "^3.8" flake8-import-order = "^0.18.1" diff --git a/tasks.py b/tasks.py index 148899c7..d41e2955 100644 --- a/tasks.py +++ b/tasks.py @@ -66,13 +66,20 @@ def isort(c, check=False, diff=False): @task -def flake8(c): - c.run(f"git diff HEAD | {VENV_BIN}/flake8 --diff --max-line-length=88", pty=PTY) +def ruff(c, fix=False, diff=False): + """Run Ruff to ensure code meets project standards.""" + diff_flag, fix_flag = "", "" + if fix: + fix_flag = "--fix" + if diff: + diff_flag = "--diff" + c.run(f"{VENV_BIN}/ruff check {diff_flag} {fix_flag} .", pty=PTY) @task -def lint(c): - flake8(c) +def lint(c, fix=False, diff=False): + """Check code style via linting tools.""" + ruff(c, fix=fix, diff=diff) @task From 6cf6a1ffe97b23074cf4e8c223fb6174d4092ae4 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 10:58:41 -0700 Subject: [PATCH 38/88] Ruff lint fixes --- pelican/tools/pelican_themes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py index 96d07c1f..b8bf1be2 100755 --- a/pelican/tools/pelican_themes.py +++ b/pelican/tools/pelican_themes.py @@ -10,7 +10,7 @@ def err(msg, die=None): """Print an error message and exits if an exit code is given""" sys.stderr.write(msg + '\n') if die: - sys.exit(die if type(die) is int else 1) + sys.exit(die if isinstance(die, int) else 1) try: @@ -135,16 +135,16 @@ def themes(): def list_themes(v=False): """Display the list of the themes""" - for t, l in themes(): + for theme_path, link_target in themes(): if not v: - t = os.path.basename(t) - if l: + theme_path = os.path.basename(theme_path) + if link_target: if v: - print(t + (" (symbolic link to `" + l + "')")) + print(theme_path + (" (symbolic link to `" + link_target + "')")) else: - print(t + '@') + print(theme_path + '@') else: - print(t) + print(theme_path) def remove(theme_name, v=False): From 29b10ef6e640f017757fd0afeb67dcd607cf2e75 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 11:02:06 -0700 Subject: [PATCH 39/88] Use poetry directly in lints --- .github/workflows/main.yml | 16 ++++++++++------ tox.ini | 14 -------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0ffd9c6..b59c5316 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,16 +62,20 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install Poetry + run: pipx install poetry - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.9" - cache: "pip" - cache-dependency-path: "**/requirements/*" - - name: Install tox - run: python -m pip install -U pip tox - - name: Check - run: tox -e flake8 + cache: "poetry" + cache-dependency-path: "pyproject.toml" + - name: Install dependencies + run: | + poetry env use "3.9" + poetry install --no-interaction + - name: Run linters + run: poetry run invoke lint --diff docs: name: Build docs diff --git a/tox.ini b/tox.ini index c31044ca..361c52dd 100644 --- a/tox.ini +++ b/tox.ini @@ -30,17 +30,3 @@ filterwarnings = default::DeprecationWarning error:.*:Warning:pelican addopts = -n auto -r a - -[flake8] -application-import-names = pelican -import-order-style = cryptography -max-line-length = 88 - -[testenv:flake8] -basepython = python3.9 -skip_install = true -deps = - -rrequirements/style.pip -commands = - flake8 --version - flake8 pelican From 33d6712e8b1283354b305ea73ac0ee3331092dfc Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sat, 28 Oct 2023 11:18:24 -0700 Subject: [PATCH 40/88] Don't install pelican's dependencies to lint --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b59c5316..b477cecb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,7 +73,7 @@ jobs: - name: Install dependencies run: | poetry env use "3.9" - poetry install --no-interaction + poetry install --no-interaction --no-root - name: Run linters run: poetry run invoke lint --diff From b8d5919cd24edc0aeb322f5c1eb036810ae6b38e Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 28 Oct 2023 22:11:11 +0300 Subject: [PATCH 41/88] expand period tests to be more specific --- pelican/generators.py | 2 +- pelican/tests/test_generators.py | 62 ++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/pelican/generators.py b/pelican/generators.py index 7ab99263..d874d97c 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -484,7 +484,7 @@ class ArticlesGenerator(CachingGenerator): except PelicanTemplateNotFound: template = self.get_template('archives') - for granularity in list(self.period_archives.keys()): + for granularity in self.period_archives: for period in self.period_archives[granularity]: context = self.context.copy() diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index a6fe9731..ac271c1c 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -431,11 +431,12 @@ class TestArticlesGenerator(unittest.TestCase): path=CONTENT_DIR, theme=settings['THEME'], output_path=None) generator.generate_context() period_archives = generator.context['period_archives'] - self.assertEqual(len(period_archives.items()), 1) - self.assertIn('year', period_archives.keys()) - archive_years = [p['period'][0] for p in period_archives['year']] - self.assertIn(1970, archive_years) - self.assertIn(2014, archive_years) + abbreviated_archives = { + granularity: {period['period'] for period in periods} + for granularity, periods in period_archives.items() + } + expected = {'year': {(1970,), (2010,), (2012,), (2014,)}} + self.assertEqual(expected, abbreviated_archives) # Month archives enabled: settings['MONTH_ARCHIVE_SAVE_AS'] = \ @@ -448,11 +449,22 @@ class TestArticlesGenerator(unittest.TestCase): path=CONTENT_DIR, theme=settings['THEME'], output_path=None) generator.generate_context() period_archives = generator.context['period_archives'] - self.assertEqual(len(period_archives.items()), 2) - self.assertIn('month', period_archives.keys()) - month_archives_tuples = [p['period'] for p in period_archives['month']] - self.assertIn((1970, 'January'), month_archives_tuples) - self.assertIn((2014, 'February'), month_archives_tuples) + abbreviated_archives = { + granularity: {period['period'] for period in periods} + for granularity, periods in period_archives.items() + } + expected = { + 'year': {(1970,), (2010,), (2012,), (2014,)}, + 'month': { + (1970, 'January'), + (2010, 'December'), + (2012, 'December'), + (2012, 'November'), + (2012, 'October'), + (2014, 'February'), + }, + } + self.assertEqual(expected, abbreviated_archives) # Day archives enabled: settings['DAY_ARCHIVE_SAVE_AS'] = \ @@ -465,11 +477,31 @@ class TestArticlesGenerator(unittest.TestCase): path=CONTENT_DIR, theme=settings['THEME'], output_path=None) generator.generate_context() period_archives = generator.context['period_archives'] - self.assertEqual(len(period_archives.items()), 3) - self.assertIn('day', period_archives.keys()) - day_archives_tuples = [p['period'] for p in period_archives['day']] - self.assertIn((1970, 'January', 1), day_archives_tuples) - self.assertIn((2014, 'February', 9), day_archives_tuples) + abbreviated_archives = { + granularity: {period['period'] for period in periods} + for granularity, periods in period_archives.items() + } + expected = { + 'year': {(1970,), (2010,), (2012,), (2014,)}, + 'month': { + (1970, 'January'), + (2010, 'December'), + (2012, 'December'), + (2012, 'November'), + (2012, 'October'), + (2014, 'February'), + }, + 'day': { + (1970, 'January', 1), + (2010, 'December', 2), + (2012, 'December', 20), + (2012, 'November', 29), + (2012, 'October', 30), + (2012, 'October', 31), + (2014, 'February', 9), + }, + } + self.assertEqual(expected, abbreviated_archives) # Further item values tests filtered_archives = [ From b812f2ad1c5feb010d5d33bda247c126dadd1b4b Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Sat, 28 Oct 2023 21:06:24 +0100 Subject: [PATCH 42/88] =?UTF-8?q?chore:=20Simplify=20boolean=20`if`=20expr?= =?UTF-8?q?ession=20=E2=9C=A8=20(#2944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pelican/tools/pelican_themes.py | 21 ++++++++++----------- pelican/writers.py | 22 ++++++++++++---------- tasks.py | 6 +++--- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py index 96d07c1f..47f1a625 100755 --- a/pelican/tools/pelican_themes.py +++ b/pelican/tools/pelican_themes.py @@ -183,7 +183,7 @@ def install(path, v=False, u=False): exists = os.path.exists(theme_path) if exists and not u: err(path + ' : already exists') - elif exists and u: + elif exists: remove(theme_name, v) install(path, v) else: @@ -245,15 +245,14 @@ def clean(v=False): c = 0 for path in os.listdir(_THEMES_PATH): path = os.path.join(_THEMES_PATH, path) - if os.path.islink(path): - if is_broken_link(path): - if v: - print('Removing {}'.format(path)) - try: - os.remove(path) - except OSError: - print('Error: cannot remove {}'.format(path)) - else: - c += 1 + if os.path.islink(path) and is_broken_link(path): + if v: + print('Removing {}'.format(path)) + try: + os.remove(path) + except OSError: + print('Error: cannot remove {}'.format(path)) + else: + c += 1 print("\nRemoved {} broken links".format(c)) diff --git a/pelican/writers.py b/pelican/writers.py index b08da1f3..afc8e4b7 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -37,13 +37,12 @@ class Writer: feed_title = context['SITENAME'] + ' - ' + feed_title else: feed_title = context['SITENAME'] - feed = feed_class( + return feed_class( title=Markup(feed_title).striptags(), link=(self.site_url + '/'), feed_url=self.feed_url, description=context.get('SITESUBTITLE', ''), subtitle=context.get('SITESUBTITLE', None)) - return feed def _add_item_to_the_feed(self, feed, item): title = Markup(item.title).striptags() @@ -71,7 +70,7 @@ class Writer: if description == content: description = None - categories = list() + categories = [] if hasattr(item, 'category'): categories.append(item.category) if hasattr(item, 'tags'): @@ -83,13 +82,17 @@ class Writer: unique_id=get_tag_uri(link, item.date), description=description, content=content, - categories=categories if categories else None, + categories=categories or None, author_name=getattr(item, 'author', ''), pubdate=set_date_tzinfo( - item.date, self.settings.get('TIMEZONE', None)), + item.date, self.settings.get('TIMEZONE', None) + ), updateddate=set_date_tzinfo( item.modified, self.settings.get('TIMEZONE', None) - ) if hasattr(item, 'modified') else None) + ) + if hasattr(item, 'modified') + else None, + ) def _open_w(self, filename, encoding, override=False): """Open a file to write some content to it. @@ -101,9 +104,8 @@ class Writer: if override: raise RuntimeError('File %s is set to be overridden twice' % filename) - else: - logger.info('Skipping %s', filename) - filename = os.devnull + logger.info('Skipping %s', filename) + filename = os.devnull elif filename in self._written_files: if override: logger.info('Overwriting %s', filename) @@ -139,7 +141,7 @@ class Writer: 'SITEURL', path_to_url(get_relative_path(path))) self.feed_domain = context.get('FEED_DOMAIN') - self.feed_url = self.urljoiner(self.feed_domain, url if url else path) + self.feed_url = self.urljoiner(self.feed_domain, url or path) feed = self._create_new_feed(feed_type, feed_title, context) diff --git a/tasks.py b/tasks.py index 148899c7..e4268ec6 100644 --- a/tasks.py +++ b/tasks.py @@ -8,7 +8,7 @@ PKG_NAME = "pelican" PKG_PATH = Path(PKG_NAME) DOCS_PORT = os.environ.get("DOCS_PORT", 8000) BIN_DIR = "bin" if os.name != "nt" else "Scripts" -PTY = True if os.name != "nt" else False +PTY = os.name != "nt" ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None) VENV_HOME = Path(os.environ.get("WORKON_HOME", "~/virtualenvs")) VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME) @@ -16,8 +16,8 @@ VENV = str(VENV_PATH.expanduser()) VENV_BIN = Path(VENV) / Path(BIN_DIR) TOOLS = ["poetry", "pre-commit", "psutil"] -POETRY = which("poetry") if which("poetry") else (VENV_BIN / "poetry") -PRECOMMIT = which("pre-commit") if which("pre-commit") else (VENV_BIN / "pre-commit") +POETRY = which("poetry") or VENV_BIN / "poetry" +PRECOMMIT = which("pre-commit") or VENV_BIN / "pre-commit" @task From 8a7e01646b4fa962f6225fa5d4cac4693876d7bb Mon Sep 17 00:00:00 2001 From: Will Thong Date: Sat, 28 Oct 2023 21:11:44 +0100 Subject: [PATCH 43/88] Add rel='nofollow' to all external hardcoded links in templates (#3162) --- pelican/tests/output/basic/a-markdown-powered-article.html | 4 ++-- pelican/tests/output/basic/archives.html | 4 ++-- pelican/tests/output/basic/article-1.html | 4 ++-- pelican/tests/output/basic/article-2.html | 4 ++-- pelican/tests/output/basic/article-3.html | 4 ++-- pelican/tests/output/basic/author/alexis-metaireau.html | 4 ++-- pelican/tests/output/basic/authors.html | 4 ++-- pelican/tests/output/basic/categories.html | 4 ++-- pelican/tests/output/basic/category/bar.html | 4 ++-- pelican/tests/output/basic/category/cat1.html | 4 ++-- pelican/tests/output/basic/category/misc.html | 4 ++-- pelican/tests/output/basic/category/yeah.html | 4 ++-- .../output/basic/drafts/a-draft-article-without-date.html | 4 ++-- pelican/tests/output/basic/drafts/a-draft-article.html | 4 ++-- pelican/tests/output/basic/filename_metadata-example.html | 4 ++-- pelican/tests/output/basic/index.html | 4 ++-- pelican/tests/output/basic/oh-yeah-fr.html | 4 ++-- pelican/tests/output/basic/oh-yeah.html | 4 ++-- pelican/tests/output/basic/override/index.html | 4 ++-- .../tests/output/basic/pages/this-is-a-test-hidden-page.html | 4 ++-- pelican/tests/output/basic/pages/this-is-a-test-page.html | 4 ++-- pelican/tests/output/basic/second-article-fr.html | 4 ++-- pelican/tests/output/basic/second-article.html | 4 ++-- pelican/tests/output/basic/tag/bar.html | 4 ++-- pelican/tests/output/basic/tag/baz.html | 4 ++-- pelican/tests/output/basic/tag/foo.html | 4 ++-- pelican/tests/output/basic/tag/foobar.html | 4 ++-- pelican/tests/output/basic/tag/oh.html | 4 ++-- pelican/tests/output/basic/tag/yeah.html | 4 ++-- pelican/tests/output/basic/tags.html | 4 ++-- pelican/tests/output/basic/this-is-a-super-article.html | 4 ++-- pelican/tests/output/basic/unbelievable.html | 4 ++-- pelican/tests/output/custom/a-markdown-powered-article.html | 4 ++-- pelican/tests/output/custom/archives.html | 4 ++-- pelican/tests/output/custom/article-1.html | 4 ++-- pelican/tests/output/custom/article-2.html | 4 ++-- pelican/tests/output/custom/article-3.html | 4 ++-- pelican/tests/output/custom/author/alexis-metaireau.html | 4 ++-- pelican/tests/output/custom/author/alexis-metaireau2.html | 4 ++-- pelican/tests/output/custom/author/alexis-metaireau3.html | 4 ++-- pelican/tests/output/custom/authors.html | 4 ++-- pelican/tests/output/custom/categories.html | 4 ++-- pelican/tests/output/custom/category/bar.html | 4 ++-- pelican/tests/output/custom/category/cat1.html | 4 ++-- pelican/tests/output/custom/category/misc.html | 4 ++-- pelican/tests/output/custom/category/yeah.html | 4 ++-- .../output/custom/drafts/a-draft-article-without-date.html | 4 ++-- pelican/tests/output/custom/drafts/a-draft-article.html | 4 ++-- pelican/tests/output/custom/filename_metadata-example.html | 4 ++-- pelican/tests/output/custom/index.html | 4 ++-- pelican/tests/output/custom/index2.html | 4 ++-- pelican/tests/output/custom/index3.html | 4 ++-- pelican/tests/output/custom/jinja2_template.html | 4 ++-- pelican/tests/output/custom/oh-yeah-fr.html | 4 ++-- pelican/tests/output/custom/oh-yeah.html | 4 ++-- pelican/tests/output/custom/override/index.html | 4 ++-- .../tests/output/custom/pages/this-is-a-test-hidden-page.html | 4 ++-- pelican/tests/output/custom/pages/this-is-a-test-page.html | 4 ++-- pelican/tests/output/custom/second-article-fr.html | 4 ++-- pelican/tests/output/custom/second-article.html | 4 ++-- pelican/tests/output/custom/tag/bar.html | 4 ++-- pelican/tests/output/custom/tag/baz.html | 4 ++-- pelican/tests/output/custom/tag/foo.html | 4 ++-- pelican/tests/output/custom/tag/foobar.html | 4 ++-- pelican/tests/output/custom/tag/oh.html | 4 ++-- pelican/tests/output/custom/tag/yeah.html | 4 ++-- pelican/tests/output/custom/tags.html | 4 ++-- pelican/tests/output/custom/this-is-a-super-article.html | 4 ++-- pelican/tests/output/custom/unbelievable.html | 4 ++-- pelican/tests/output/custom_locale/archives.html | 4 ++-- .../tests/output/custom_locale/author/alexis-metaireau.html | 4 ++-- .../tests/output/custom_locale/author/alexis-metaireau2.html | 4 ++-- .../tests/output/custom_locale/author/alexis-metaireau3.html | 4 ++-- pelican/tests/output/custom_locale/authors.html | 4 ++-- pelican/tests/output/custom_locale/categories.html | 4 ++-- pelican/tests/output/custom_locale/category/bar.html | 4 ++-- pelican/tests/output/custom_locale/category/cat1.html | 4 ++-- pelican/tests/output/custom_locale/category/misc.html | 4 ++-- pelican/tests/output/custom_locale/category/yeah.html | 4 ++-- .../custom_locale/drafts/a-draft-article-without-date.html | 4 ++-- .../tests/output/custom_locale/drafts/a-draft-article.html | 4 ++-- pelican/tests/output/custom_locale/index.html | 4 ++-- pelican/tests/output/custom_locale/index2.html | 4 ++-- pelican/tests/output/custom_locale/index3.html | 4 ++-- pelican/tests/output/custom_locale/jinja2_template.html | 4 ++-- pelican/tests/output/custom_locale/oh-yeah-fr.html | 4 ++-- pelican/tests/output/custom_locale/override/index.html | 4 ++-- .../custom_locale/pages/this-is-a-test-hidden-page.html | 4 ++-- .../tests/output/custom_locale/pages/this-is-a-test-page.html | 4 ++-- .../posts/2010/décembre/02/this-is-a-super-article/index.html | 4 ++-- .../posts/2010/octobre/15/unbelievable/index.html | 4 ++-- .../custom_locale/posts/2010/octobre/20/oh-yeah/index.html | 4 ++-- .../posts/2011/avril/20/a-markdown-powered-article/index.html | 4 ++-- .../custom_locale/posts/2011/février/17/article-1/index.html | 4 ++-- .../custom_locale/posts/2011/février/17/article-2/index.html | 4 ++-- .../custom_locale/posts/2011/février/17/article-3/index.html | 4 ++-- .../posts/2012/février/29/second-article/index.html | 4 ++-- .../2012/novembre/30/filename_metadata-example/index.html | 4 ++-- pelican/tests/output/custom_locale/second-article-fr.html | 4 ++-- pelican/tests/output/custom_locale/tag/bar.html | 4 ++-- pelican/tests/output/custom_locale/tag/baz.html | 4 ++-- pelican/tests/output/custom_locale/tag/foo.html | 4 ++-- pelican/tests/output/custom_locale/tag/foobar.html | 4 ++-- pelican/tests/output/custom_locale/tag/oh.html | 4 ++-- pelican/tests/output/custom_locale/tag/yeah.html | 4 ++-- pelican/tests/output/custom_locale/tags.html | 4 ++-- pelican/themes/notmyidea/templates/base.html | 4 ++-- pelican/themes/notmyidea/templates/twitter.html | 2 +- pelican/themes/simple/templates/base.html | 4 ++-- 109 files changed, 217 insertions(+), 217 deletions(-) diff --git a/pelican/tests/output/basic/a-markdown-powered-article.html b/pelican/tests/output/basic/a-markdown-powered-article.html index ca9b62eb..0098ccac 100644 --- a/pelican/tests/output/basic/a-markdown-powered-article.html +++ b/pelican/tests/output/basic/a-markdown-powered-article.html @@ -58,10 +58,10 @@ diff --git a/pelican/tests/output/basic/archives.html b/pelican/tests/output/basic/archives.html index 93c1d5be..e3a6c7df 100644 --- a/pelican/tests/output/basic/archives.html +++ b/pelican/tests/output/basic/archives.html @@ -60,10 +60,10 @@ diff --git a/pelican/tests/output/basic/article-1.html b/pelican/tests/output/basic/article-1.html index 4dee2eb6..961ad390 100644 --- a/pelican/tests/output/basic/article-1.html +++ b/pelican/tests/output/basic/article-1.html @@ -57,10 +57,10 @@ diff --git a/pelican/tests/output/basic/article-2.html b/pelican/tests/output/basic/article-2.html index 7e5995c0..e5389d35 100644 --- a/pelican/tests/output/basic/article-2.html +++ b/pelican/tests/output/basic/article-2.html @@ -57,10 +57,10 @@ diff --git a/pelican/tests/output/basic/article-3.html b/pelican/tests/output/basic/article-3.html index b0b8ed1b..d23e5da2 100644 --- a/pelican/tests/output/basic/article-3.html +++ b/pelican/tests/output/basic/article-3.html @@ -57,10 +57,10 @@ diff --git a/pelican/tests/output/basic/author/alexis-metaireau.html b/pelican/tests/output/basic/author/alexis-metaireau.html index 87eaa461..12e05ec8 100644 --- a/pelican/tests/output/basic/author/alexis-metaireau.html +++ b/pelican/tests/output/basic/author/alexis-metaireau.html @@ -102,10 +102,10 @@ YEAH !

diff --git a/pelican/tests/output/basic/authors.html b/pelican/tests/output/basic/authors.html index 937a3085..cff1360b 100644 --- a/pelican/tests/output/basic/authors.html +++ b/pelican/tests/output/basic/authors.html @@ -42,10 +42,10 @@ diff --git a/pelican/tests/output/basic/categories.html b/pelican/tests/output/basic/categories.html index 96a61c73..cc54f4a3 100644 --- a/pelican/tests/output/basic/categories.html +++ b/pelican/tests/output/basic/categories.html @@ -45,10 +45,10 @@ diff --git a/pelican/tests/output/basic/category/bar.html b/pelican/tests/output/basic/category/bar.html index 65b7b04e..1f9c0d8d 100644 --- a/pelican/tests/output/basic/category/bar.html +++ b/pelican/tests/output/basic/category/bar.html @@ -58,10 +58,10 @@ YEAH !

diff --git a/pelican/tests/output/basic/category/cat1.html b/pelican/tests/output/basic/category/cat1.html index 62fb034c..ca47821b 100644 --- a/pelican/tests/output/basic/category/cat1.html +++ b/pelican/tests/output/basic/category/cat1.html @@ -115,10 +115,10 @@ diff --git a/pelican/tests/output/basic/category/misc.html b/pelican/tests/output/basic/category/misc.html index ed06ebc0..58490001 100644 --- a/pelican/tests/output/basic/category/misc.html +++ b/pelican/tests/output/basic/category/misc.html @@ -126,10 +126,10 @@ pelican.conf, it will …

diff --git a/pelican/tests/output/basic/category/yeah.html b/pelican/tests/output/basic/category/yeah.html index 9d2f30a2..815d42e4 100644 --- a/pelican/tests/output/basic/category/yeah.html +++ b/pelican/tests/output/basic/category/yeah.html @@ -66,10 +66,10 @@ diff --git a/pelican/tests/output/basic/drafts/a-draft-article-without-date.html b/pelican/tests/output/basic/drafts/a-draft-article-without-date.html index bc30dabd..5a2d367b 100644 --- a/pelican/tests/output/basic/drafts/a-draft-article-without-date.html +++ b/pelican/tests/output/basic/drafts/a-draft-article-without-date.html @@ -58,10 +58,10 @@ listed anywhere else.

diff --git a/pelican/tests/output/basic/drafts/a-draft-article.html b/pelican/tests/output/basic/drafts/a-draft-article.html index 4abd4946..a0bed241 100644 --- a/pelican/tests/output/basic/drafts/a-draft-article.html +++ b/pelican/tests/output/basic/drafts/a-draft-article.html @@ -58,10 +58,10 @@ listed anywhere else.

diff --git a/pelican/tests/output/basic/filename_metadata-example.html b/pelican/tests/output/basic/filename_metadata-example.html index ae19ceb1..f3ae9cea 100644 --- a/pelican/tests/output/basic/filename_metadata-example.html +++ b/pelican/tests/output/basic/filename_metadata-example.html @@ -57,10 +57,10 @@ diff --git a/pelican/tests/output/basic/index.html b/pelican/tests/output/basic/index.html index 711dd2bd..fd334d38 100644 --- a/pelican/tests/output/basic/index.html +++ b/pelican/tests/output/basic/index.html @@ -265,10 +265,10 @@ pelican.conf, it will …

diff --git a/pelican/tests/output/basic/oh-yeah-fr.html b/pelican/tests/output/basic/oh-yeah-fr.html index 183754ff..3fdfeae9 100644 --- a/pelican/tests/output/basic/oh-yeah-fr.html +++ b/pelican/tests/output/basic/oh-yeah-fr.html @@ -61,10 +61,10 @@ Translations: diff --git a/pelican/tests/output/basic/oh-yeah.html b/pelican/tests/output/basic/oh-yeah.html index f63302d2..f8d3d227 100644 --- a/pelican/tests/output/basic/oh-yeah.html +++ b/pelican/tests/output/basic/oh-yeah.html @@ -69,10 +69,10 @@ YEAH !

diff --git a/pelican/tests/output/basic/override/index.html b/pelican/tests/output/basic/override/index.html index 1d5531d7..a74d802f 100644 --- a/pelican/tests/output/basic/override/index.html +++ b/pelican/tests/output/basic/override/index.html @@ -41,10 +41,10 @@ at a custom location.

diff --git a/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html b/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html index 8c14e354..1c836201 100644 --- a/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html +++ b/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html @@ -41,10 +41,10 @@ Anyone can see this page but it's not linked to anywhere!

diff --git a/pelican/tests/output/basic/pages/this-is-a-test-page.html b/pelican/tests/output/basic/pages/this-is-a-test-page.html index 1fdb846f..372eff76 100644 --- a/pelican/tests/output/basic/pages/this-is-a-test-page.html +++ b/pelican/tests/output/basic/pages/this-is-a-test-page.html @@ -42,10 +42,10 @@ diff --git a/pelican/tests/output/basic/second-article-fr.html b/pelican/tests/output/basic/second-article-fr.html index b49c4517..514c5585 100644 --- a/pelican/tests/output/basic/second-article-fr.html +++ b/pelican/tests/output/basic/second-article-fr.html @@ -61,10 +61,10 @@ diff --git a/pelican/tests/output/basic/second-article.html b/pelican/tests/output/basic/second-article.html index 2b3bca88..365f99cd 100644 --- a/pelican/tests/output/basic/second-article.html +++ b/pelican/tests/output/basic/second-article.html @@ -61,10 +61,10 @@ diff --git a/pelican/tests/output/basic/tag/bar.html b/pelican/tests/output/basic/tag/bar.html index a78c08f9..047d772a 100644 --- a/pelican/tests/output/basic/tag/bar.html +++ b/pelican/tests/output/basic/tag/bar.html @@ -114,10 +114,10 @@ YEAH !

diff --git a/pelican/tests/output/basic/tag/baz.html b/pelican/tests/output/basic/tag/baz.html index 0b7d04ab..51518620 100644 --- a/pelican/tests/output/basic/tag/baz.html +++ b/pelican/tests/output/basic/tag/baz.html @@ -57,10 +57,10 @@ diff --git a/pelican/tests/output/basic/tag/foo.html b/pelican/tests/output/basic/tag/foo.html index 6ec55bee..0214cf26 100644 --- a/pelican/tests/output/basic/tag/foo.html +++ b/pelican/tests/output/basic/tag/foo.html @@ -84,10 +84,10 @@ as well as inline markup.

diff --git a/pelican/tests/output/basic/tag/foobar.html b/pelican/tests/output/basic/tag/foobar.html index def1d0c6..ab07e87f 100644 --- a/pelican/tests/output/basic/tag/foobar.html +++ b/pelican/tests/output/basic/tag/foobar.html @@ -66,10 +66,10 @@ diff --git a/pelican/tests/output/basic/tag/oh.html b/pelican/tests/output/basic/tag/oh.html index 11b06e61..f3af2a2f 100644 --- a/pelican/tests/output/basic/tag/oh.html +++ b/pelican/tests/output/basic/tag/oh.html @@ -40,10 +40,10 @@ diff --git a/pelican/tests/output/basic/tag/yeah.html b/pelican/tests/output/basic/tag/yeah.html index 565a3a25..f04affa8 100644 --- a/pelican/tests/output/basic/tag/yeah.html +++ b/pelican/tests/output/basic/tag/yeah.html @@ -58,10 +58,10 @@ YEAH !

diff --git a/pelican/tests/output/basic/tags.html b/pelican/tests/output/basic/tags.html index 6dce2632..db5d6634 100644 --- a/pelican/tests/output/basic/tags.html +++ b/pelican/tests/output/basic/tags.html @@ -47,10 +47,10 @@ diff --git a/pelican/tests/output/basic/this-is-a-super-article.html b/pelican/tests/output/basic/this-is-a-super-article.html index 8681bb1b..6ac68664 100644 --- a/pelican/tests/output/basic/this-is-a-super-article.html +++ b/pelican/tests/output/basic/this-is-a-super-article.html @@ -75,10 +75,10 @@ diff --git a/pelican/tests/output/basic/unbelievable.html b/pelican/tests/output/basic/unbelievable.html index 8a8b4f5a..d87c31ea 100644 --- a/pelican/tests/output/basic/unbelievable.html +++ b/pelican/tests/output/basic/unbelievable.html @@ -89,10 +89,10 @@ pelican.conf, it will have nothing in default.

diff --git a/pelican/tests/output/custom/a-markdown-powered-article.html b/pelican/tests/output/custom/a-markdown-powered-article.html index 36fb242f..422421d8 100644 --- a/pelican/tests/output/custom/a-markdown-powered-article.html +++ b/pelican/tests/output/custom/a-markdown-powered-article.html @@ -95,10 +95,10 @@ + {% endif %} diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index 3125e5aa..94a16930 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -58,8 +58,8 @@
- Proudly powered by Pelican, - which takes great advantage of Python. + Proudly powered by Pelican, + which takes great advantage of Python.
From 3a6ae72333f447dececb8552800e507a619444c6 Mon Sep 17 00:00:00 2001 From: boxydog Date: Sat, 28 Oct 2023 17:01:33 -0500 Subject: [PATCH 44/88] Add a "coverage" task to generate a coverage report Add a one-liner about "invoke" in docs. --- docs/contribute.rst | 3 +++ tasks.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/docs/contribute.rst b/docs/contribute.rst index 64c144d6..cfbfe351 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -75,6 +75,9 @@ via:: invoke tests +(For more on Invoke, see ``invoke -l`` to list tasks, or +https://pyinvoke.org for documentation.) + In addition to running the test suite, it is important to also ensure that any lines you changed conform to code style guidelines. You can check that via:: diff --git a/tasks.py b/tasks.py index efdef8a4..e9f65db3 100644 --- a/tasks.py +++ b/tasks.py @@ -44,6 +44,13 @@ def tests(c): c.run(f"{VENV_BIN}/pytest", pty=PTY) +@task +def coverage(c): + """Generate code coverage of running the test suite.""" + c.run(f"{VENV_BIN}/pytest --cov=pelican", pty=PTY) + c.run(f"{VENV_BIN}/coverage html", pty=PTY) + + @task def black(c, check=False, diff=False): """Run Black auto-formatter, optionally with --check or --diff""" From fad2ff7ae3cd0ea8b974db5fe42de7383da679c1 Mon Sep 17 00:00:00 2001 From: boxydog <93335439+boxydog@users.noreply.github.com> Date: Sat, 28 Oct 2023 17:40:40 -0500 Subject: [PATCH 45/88] Add unit test utilities temporary_locale and TestCaseWithCLocale (#3224) --- pelican/tests/support.py | 13 +++ pelican/tests/test_generators.py | 30 ++----- pelican/tests/test_importer.py | 28 ++----- pelican/tests/test_utils.py | 133 ++++++++++++++++--------------- pelican/utils.py | 26 ++++-- 5 files changed, 111 insertions(+), 119 deletions(-) diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 720e4d0e..3e4da785 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -254,3 +254,16 @@ class LoggedTestCase(unittest.TestCase): actual, count, msg='expected {} occurrences of {!r}, but found {}'.format( count, msg, actual)) + + +class TestCaseWithCLocale(unittest.TestCase): + """Set locale to C for each test case, then restore afterward. + + 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') + + def tearDown(self): + locale.setlocale(locale.LC_ALL, self.old_locale) diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index ac271c1c..05c37269 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -1,4 +1,3 @@ -import locale import os import sys from shutil import copy, rmtree @@ -9,26 +8,21 @@ from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator, PelicanTemplateNotFound, StaticGenerator, TemplatePagesGenerator) from pelican.tests.support import (can_symlink, get_context, get_settings, - unittest) + unittest, TestCaseWithCLocale) from pelican.writers import Writer - CUR_DIR = os.path.dirname(__file__) CONTENT_DIR = os.path.join(CUR_DIR, 'content') -class TestGenerator(unittest.TestCase): +class TestGenerator(TestCaseWithCLocale): def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + super().setUp() self.settings = get_settings() self.settings['READERS'] = {'asc': None} self.generator = Generator(self.settings.copy(), self.settings, CUR_DIR, self.settings['THEME'], None) - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - def test_include_path(self): self.settings['IGNORE_FILES'] = {'ignored1.rst', 'ignored2.rst'} @@ -408,8 +402,6 @@ class TestArticlesGenerator(unittest.TestCase): def test_period_archives_context(self): """Test correctness of the period_archives context values.""" - old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') settings = get_settings() settings['CACHE_PATH'] = self.temp_cache @@ -532,15 +524,11 @@ class TestArticlesGenerator(unittest.TestCase): self.assertEqual(sample_archive['dates'][0].title, dates[0].title) self.assertEqual(sample_archive['dates'][0].date, dates[0].date) - locale.setlocale(locale.LC_ALL, old_locale) - def test_period_in_timeperiod_archive(self): """ Test that the context of a generated period_archive is passed 'period' : a tuple of year, month, day according to the time period """ - old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') settings = get_settings() settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' @@ -625,7 +613,6 @@ class TestArticlesGenerator(unittest.TestCase): dates=dates, template_name='period_archives', url="posts/1970/Jan/01/", all_articles=generator.articles) - locale.setlocale(locale.LC_ALL, old_locale) def test_nonexistent_template(self): """Attempt to load a non-existent template""" @@ -926,20 +913,18 @@ class TestPageGenerator(unittest.TestCase): context['static_links']) -class TestTemplatePagesGenerator(unittest.TestCase): +class TestTemplatePagesGenerator(TestCaseWithCLocale): TEMPLATE_CONTENT = "foo: {{ foo }}" def setUp(self): + super().setUp() self.temp_content = mkdtemp(prefix='pelicantests.') self.temp_output = mkdtemp(prefix='pelicantests.') - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') def tearDown(self): rmtree(self.temp_content) rmtree(self.temp_output) - locale.setlocale(locale.LC_ALL, self.old_locale) def test_generate_output(self): @@ -1299,18 +1284,15 @@ class TestStaticGenerator(unittest.TestCase): self.assertTrue(os.path.isfile(self.endfile)) -class TestJinja2Environment(unittest.TestCase): +class TestJinja2Environment(TestCaseWithCLocale): def setUp(self): self.temp_content = mkdtemp(prefix='pelicantests.') self.temp_output = mkdtemp(prefix='pelicantests.') - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') def tearDown(self): rmtree(self.temp_content) rmtree(self.temp_output) - locale.setlocale(locale.LC_ALL, self.old_locale) def _test_jinja2_helper(self, additional_settings, content, expected): settings = get_settings() diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 0d9586f0..870d3001 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -1,4 +1,3 @@ -import locale import os import re from posixpath import join as posix_join @@ -6,7 +5,7 @@ from unittest.mock import patch from pelican.settings import DEFAULT_CONFIG from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder, - unittest) + unittest, TestCaseWithCLocale) from pelican.tools.pelican_import import (blogger2fields, build_header, build_markdown_header, decode_wp_content, @@ -16,7 +15,6 @@ from pelican.tools.pelican_import import (blogger2fields, build_header, ) 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') @@ -38,19 +36,9 @@ except ImportError: LXML = False -class TestWithOsDefaults(unittest.TestCase): - """Set locale to C and timezone to UTC for tests, then restore.""" - def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') - - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - - @skipIfNoExecutable(['pandoc', '--version']) @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestBloggerXmlImporter(TestWithOsDefaults): +class TestBloggerXmlImporter(TestCaseWithCLocale): def setUp(self): super().setUp() @@ -95,7 +83,7 @@ class TestBloggerXmlImporter(TestWithOsDefaults): @skipIfNoExecutable(['pandoc', '--version']) @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestWordpressXmlImporter(TestWithOsDefaults): +class TestWordpressXmlImporter(TestCaseWithCLocale): def setUp(self): super().setUp() @@ -433,15 +421,11 @@ class TestBuildHeader(unittest.TestCase): @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') @unittest.skipUnless(LXML, 'Needs lxml module') -class TestWordpressXMLAttachements(unittest.TestCase): +class TestWordpressXMLAttachements(TestCaseWithCLocale): def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + super().setUp() self.attachments = get_attachments(WORDPRESS_XML_SAMPLE) - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - def test_recognise_attachments(self): self.assertTrue(self.attachments) self.assertTrue(len(self.attachments.keys()) == 3) @@ -485,7 +469,7 @@ class TestWordpressXMLAttachements(unittest.TestCase): directory) -class TestTumblrImporter(TestWithOsDefaults): +class TestTumblrImporter(TestCaseWithCLocale): @patch("pelican.tools.pelican_import._get_tumblr_posts") def test_posts(self, get): def get_posts(api_key, blogname, offset=0): diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index d8296285..40aff005 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -484,33 +484,25 @@ class TestUtils(LoggedTestCase): locale_available('Turkish'), 'Turkish locale needed') def test_strftime_locale_dependent_turkish(self): - # store current locale - old_locale = locale.setlocale(locale.LC_ALL) + temp_locale = 'Turkish' if platform == 'win32' else 'tr_TR.UTF-8' - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'Turkish') - else: - locale.setlocale(locale.LC_ALL, 'tr_TR.UTF-8') + with utils.temporary_locale(temp_locale): + d = utils.SafeDatetime(2012, 8, 29) - d = utils.SafeDatetime(2012, 8, 29) + # simple + self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') + self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), + 'Çarşamba, 29 Ağustos 2012') - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') - self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), - 'Çarşamba, 29 Ağustos 2012') + # with text + self.assertEqual( + utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), + 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') - # with text - self.assertEqual( - utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), - 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') - - # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%Y yılında %üretim artışı'), - '2012 yılında %üretim artışı') - - # restore locale back - locale.setlocale(locale.LC_ALL, old_locale) + # non-ascii format candidate (someone might pass it… for some reason) + self.assertEqual( + utils.strftime(d, '%Y yılında %üretim artışı'), + '2012 yılında %üretim artışı') # test the output of utils.strftime in a different locale # French locale @@ -518,34 +510,26 @@ class TestUtils(LoggedTestCase): locale_available('French'), 'French locale needed') def test_strftime_locale_dependent_french(self): - # store current locale - old_locale = locale.setlocale(locale.LC_ALL) + temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'French') - else: - locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') + with utils.temporary_locale(temp_locale): + d = utils.SafeDatetime(2012, 8, 29) - d = utils.SafeDatetime(2012, 8, 29) + # simple + self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') + # depending on OS, the first letter is m or M + self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) - # depending on OS, the first letter is m or M - self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) + # with text + self.assertEqual( + utils.strftime(d, 'Écrit le %d %B %Y'), + 'Écrit le 29 août 2012') - # with text - self.assertEqual( - utils.strftime(d, 'Écrit le %d %B %Y'), - 'Écrit le 29 août 2012') - - # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%écrits en %Y'), - '%écrits en 2012') - - # restore locale back - locale.setlocale(locale.LC_ALL, old_locale) + # non-ascii format candidate (someone might pass it… for some reason) + self.assertEqual( + utils.strftime(d, '%écrits en %Y'), + '%écrits en 2012') def test_maybe_pluralize(self): self.assertEqual( @@ -558,6 +542,23 @@ class TestUtils(LoggedTestCase): utils.maybe_pluralize(2, 'Article', 'Articles'), '2 Articles') + def test_temporary_locale(self): + # test with default LC category + orig_locale = locale.setlocale(locale.LC_ALL) + + with utils.temporary_locale('C'): + self.assertEqual(locale.setlocale(locale.LC_ALL), 'C') + + self.assertEqual(locale.setlocale(locale.LC_ALL), orig_locale) + + # test with custom LC category + orig_locale = locale.setlocale(locale.LC_TIME) + + with utils.temporary_locale('C', locale.LC_TIME): + self.assertEqual(locale.setlocale(locale.LC_TIME), 'C') + + self.assertEqual(locale.setlocale(locale.LC_TIME), orig_locale) + class TestCopy(unittest.TestCase): '''Tests the copy utility''' @@ -673,27 +674,27 @@ class TestDateFormatter(unittest.TestCase): def test_french_strftime(self): # This test tries to reproduce an issue that # occurred with python3.3 under macos10 only - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'French') - else: - locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') - date = utils.SafeDatetime(2014, 8, 14) - # we compare the lower() dates since macos10 returns - # "Jeudi" for %A whereas linux reports "jeudi" - self.assertEqual( - 'jeudi, 14 août 2014', - utils.strftime(date, date_format="%A, %d %B %Y").lower()) - df = utils.DateFormatter() - self.assertEqual( - 'jeudi, 14 août 2014', - df(date, date_format="%A, %d %B %Y").lower()) + temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' + + with utils.temporary_locale(temp_locale): + date = utils.SafeDatetime(2014, 8, 14) + # we compare the lower() dates since macos10 returns + # "Jeudi" for %A whereas linux reports "jeudi" + self.assertEqual( + 'jeudi, 14 août 2014', + utils.strftime(date, date_format="%A, %d %B %Y").lower()) + df = utils.DateFormatter() + self.assertEqual( + 'jeudi, 14 août 2014', + df(date, date_format="%A, %d %B %Y").lower()) + # Let us now set the global locale to C: - locale.setlocale(locale.LC_ALL, 'C') - # DateFormatter should still work as expected - # since it is the whole point of DateFormatter - # (This is where pre-2014/4/15 code fails on macos10) - df_date = df(date, date_format="%A, %d %B %Y").lower() - self.assertEqual('jeudi, 14 août 2014', df_date) + with utils.temporary_locale('C'): + # DateFormatter should still work as expected + # since it is the whole point of DateFormatter + # (This is where pre-2014/4/15 code fails on macos10) + df_date = df(date, date_format="%A, %d %B %Y").lower() + self.assertEqual('jeudi, 14 août 2014', df_date) @unittest.skipUnless(locale_available('fr_FR.UTF-8') or locale_available('French'), diff --git a/pelican/utils.py b/pelican/utils.py index 3c67369b..e1bed154 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -116,18 +116,14 @@ class DateFormatter: self.locale = locale.setlocale(locale.LC_TIME) def __call__(self, date, date_format): - old_lc_time = locale.setlocale(locale.LC_TIME) - old_lc_ctype = locale.setlocale(locale.LC_CTYPE) - locale.setlocale(locale.LC_TIME, self.locale) # on OSX, encoding from LC_CTYPE determines the unicode output in PY3 # make sure it's same as LC_TIME - locale.setlocale(locale.LC_CTYPE, self.locale) + with temporary_locale(self.locale, locale.LC_TIME), \ + temporary_locale(self.locale, locale.LC_CTYPE): - formatted = strftime(date, date_format) + formatted = strftime(date, date_format) - locale.setlocale(locale.LC_TIME, old_lc_time) - locale.setlocale(locale.LC_CTYPE, old_lc_ctype) return formatted @@ -872,3 +868,19 @@ def maybe_pluralize(count, singular, plural): if count == 1: selection = singular 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) + yield + locale.setlocale(lc_category, orig_locale) From a76a4195856f6dde3e00345999727d59bb201f07 Mon Sep 17 00:00:00 2001 From: Lioman Date: Sat, 28 Oct 2023 08:38:29 +0200 Subject: [PATCH 46/88] migrate configuration to PEP621 compatible one --- .pdm-python | 1 + pdm.lock | 1431 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 123 +++-- 3 files changed, 1500 insertions(+), 55 deletions(-) create mode 100644 .pdm-python create mode 100644 pdm.lock diff --git a/.pdm-python b/.pdm-python new file mode 100644 index 00000000..c740e1bd --- /dev/null +++ b/.pdm-python @@ -0,0 +1 @@ +/Users/eliaskirchgaessner/Development/FOSS/pelican/.venv/bin/python \ No newline at end of file diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 00000000..0bfddb5e --- /dev/null +++ b/pdm.lock @@ -0,0 +1,1431 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +cross_platform = true +static_urls = false +lock_version = "4.3" +content_hash = "sha256:0774056f38e53e29569c2888786ef845063ad0abcdaa8910c7795619996ef224" + +[[package]] +name = "alabaster" +version = "0.7.13" +requires_python = ">=3.6" +summary = "A configurable sidebar-enabled Sphinx theme" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[[package]] +name = "babel" +version = "2.13.0" +requires_python = ">=3.7" +summary = "Internationalization utilities" +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] +files = [ + {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, + {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +files = [ + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +requires_python = ">=3.6.0" +summary = "Screen-scraping library" +dependencies = [ + "soupsieve>1.2", +] +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[[package]] +name = "black" +version = "19.10b0" +requires_python = ">=3.6" +summary = "The uncompromising code formatter." +dependencies = [ + "appdirs", + "attrs>=18.1.0", + "click>=6.5", + "pathspec<1,>=0.6", + "regex", + "toml>=0.9.4", + "typed-ast>=1.4.0", +] +files = [ + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] + +[[package]] +name = "blinker" +version = "1.6.3" +requires_python = ">=3.7" +summary = "Fast, simple object-to-object and broadcast signaling" +files = [ + {file = "blinker-1.6.3-py3-none-any.whl", hash = "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa"}, + {file = "blinker-1.6.3.tar.gz", hash = "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.1" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, + {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +extras = ["toml"] +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +dependencies = [ + "coverage==7.2.7", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "distlib" +version = "0.3.7" +summary = "Distribution utilities" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "docutils" +version = "0.19" +requires_python = ">=3.7" +summary = "Docutils -- Python Documentation Utilities" +files = [ + {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, + {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[[package]] +name = "execnet" +version = "2.0.2" +requires_python = ">=3.7" +summary = "execnet: rapid multi-Python deployment" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[[package]] +name = "feedgenerator" +version = "2.1.0" +requires_python = ">=3.7" +summary = "Standalone version of django.utils.feedgenerator" +dependencies = [ + "pytz>=0a", +] +files = [ + {file = "feedgenerator-2.1.0-py3-none-any.whl", hash = "sha256:93b7ce1c5a86195cafd6a8e9baf6a2a863ebd6d9905e840ce5778f73efd9a8d5"}, + {file = "feedgenerator-2.1.0.tar.gz", hash = "sha256:f075f23f28fd227f097c36b212161c6cf012e1c6caaf7ff53d5d6bb02cd42b9d"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +requires_python = ">=3.7" +summary = "A platform independent file lock." +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[[package]] +name = "flake8" +version = "3.9.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +summary = "the modular source code checker: pep8 pyflakes and co" +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", + "mccabe<0.7.0,>=0.6.0", + "pycodestyle<2.8.0,>=2.7.0", + "pyflakes<2.4.0,>=2.3.0", +] +files = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] + +[[package]] +name = "flake8-import-order" +version = "0.18.2" +summary = "Flake8 and pylama plugin that checks the ordering of import statements." +dependencies = [ + "pycodestyle", + "setuptools", +] +files = [ + {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, + {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, +] + +[[package]] +name = "furo" +version = "2023.3.27" +requires_python = ">=3.7" +summary = "A clean customisable Sphinx documentation theme." +dependencies = [ + "beautifulsoup4", + "pygments>=2.7", + "sphinx-basic-ng", + "sphinx<7.0,>=5.0", +] +files = [ + {file = "furo-2023.3.27-py3-none-any.whl", hash = "sha256:4ab2be254a2d5e52792d0ca793a12c35582dd09897228a6dd47885dabd5c9521"}, + {file = "furo-2023.3.27.tar.gz", hash = "sha256:b99e7867a5cc833b2b34d7230631dd6558c7a29f93071fdbb5709634bb33c5a5"}, +] + +[[package]] +name = "idna" +version = "3.4" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Getting image size from png/jpeg/jpeg2000/gif file" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +requires_python = ">=3.7" +summary = "Read metadata from Python packages" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "invoke" +version = "2.2.0" +requires_python = ">=3.6" +summary = "Pythonic task execution" +files = [ + {file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"}, + {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"}, +] + +[[package]] +name = "isort" +version = "5.11.5" +requires_python = ">=3.7.0" +summary = "A Python utility / library to sort Python imports." +files = [ + {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, + {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[[package]] +name = "livereload" +version = "2.6.3" +summary = "Python LiveReload is an awesome tool for web developers" +dependencies = [ + "six", + "tornado; python_version > \"2.7\"", +] +files = [ + {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, + {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, +] + +[[package]] +name = "lxml" +version = "4.9.3" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +files = [ + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +] + +[[package]] +name = "markdown" +version = "3.4.4" +requires_python = ">=3.7" +summary = "Python implementation of John Gruber's Markdown." +dependencies = [ + "importlib-metadata>=4.4; python_version < \"3.10\"", +] +files = [ + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, +] + +[[package]] +name = "markdown-it-py" +version = "2.2.0" +requires_python = ">=3.7" +summary = "Python port of markdown-it. Markdown parsing, done right!" +dependencies = [ + "mdurl~=0.1", + "typing-extensions>=3.7.4; python_version < \"3.8\"", +] +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.3" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.6.1" +summary = "McCabe checker, plugin for flake8" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "packaging" +version = "23.2" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +requires_python = ">=3.7" +summary = "Utility library for gitignore style pattern matching of file paths." +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +dependencies = [ + "typing-extensions>=4.7.1; python_version < \"3.8\"", +] +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +requires_python = ">=3.7" +summary = "plugin and hook calling mechanisms for python" +dependencies = [ + "importlib-metadata>=0.12; python_version < \"3.8\"", +] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[[package]] +name = "psutil" +version = "5.9.6" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +summary = "Cross-platform lib for process and system monitoring in Python." +files = [ + {file = "psutil-5.9.6-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c69596f9fc2f8acd574a12d5f8b7b1ba3765a641ea5d60fb4736bf3c08a8214a"}, + {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92e0cc43c524834af53e9d3369245e6cc3b130e78e26100d1f63cdb0abeb3d3c"}, + {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748c9dd2583ed86347ed65d0035f45fa8c851e8d90354c122ab72319b5f366f4"}, + {file = "psutil-5.9.6-cp37-abi3-win32.whl", hash = "sha256:a6f01f03bf1843280f4ad16f4bde26b817847b4c1a0db59bf6419807bc5ce05c"}, + {file = "psutil-5.9.6-cp37-abi3-win_amd64.whl", hash = "sha256:6e5fb8dc711a514da83098bc5234264e551ad980cec5f85dabf4d38ed6f15e9a"}, + {file = "psutil-5.9.6-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:daecbcbd29b289aac14ece28eca6a3e60aa361754cf6da3dfb20d4d32b6c7f57"}, + {file = "psutil-5.9.6.tar.gz", hash = "sha256:e4b92ddcd7dd4cdd3f900180ea1e104932c7bce234fb88976e2a3b296441225a"}, +] + +[[package]] +name = "py" +version = "1.11.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "library with cross-python path, ini-parsing, io, code, log facilities" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pycodestyle" +version = "2.7.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Python style guide checker" +files = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] + +[[package]] +name = "pyflakes" +version = "2.3.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "passive checker of Python programs" +files = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] + +[[package]] +name = "pygments" +version = "2.16.1" +requires_python = ">=3.7" +summary = "Pygments is a syntax highlighting package written in Python." +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[[package]] +name = "pytest" +version = "7.4.2" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Pytest plugin for measuring coverage." +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[[package]] +name = "pytest-forked" +version = "1.6.0" +requires_python = ">=3.7" +summary = "run tests in isolated forked subprocesses" +dependencies = [ + "py", + "pytest>=3.10", +] +files = [ + {file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"}, + {file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"}, +] + +[[package]] +name = "pytest-sugar" +version = "0.9.7" +summary = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +dependencies = [ + "packaging>=21.3", + "pytest>=6.2.0", + "termcolor>=2.1.0", +] +files = [ + {file = "pytest-sugar-0.9.7.tar.gz", hash = "sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46"}, + {file = "pytest_sugar-0.9.7-py2.py3-none-any.whl", hash = "sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062"}, +] + +[[package]] +name = "pytest-xdist" +version = "2.5.0" +requires_python = ">=3.6" +summary = "pytest xdist plugin for distributed testing and loop-on-failing modes" +dependencies = [ + "execnet>=1.1", + "pytest-forked", + "pytest>=6.2.0", +] +files = [ + {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, + {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[[package]] +name = "pytz" +version = "2023.3.post1" +summary = "World timezone definitions, modern and historical" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "regex" +version = "2023.10.3" +requires_python = ">=3.7" +summary = "Alternative regular expression module, to replace re." +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[[package]] +name = "rich" +version = "13.6.0" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +requires_python = ">=3.7" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smartypants" +version = "2.0.1" +summary = "Python with the SmartyPants" +files = [ + {file = "smartypants-2.0.1-py2.py3-none-any.whl", hash = "sha256:8db97f7cbdf08d15b158a86037cd9e116b4cf37703d24e0419a0d64ca5808f0d"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.4.1" +requires_python = ">=3.7" +summary = "A modern CSS selector implementation for Beautiful Soup." +files = [ + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, +] + +[[package]] +name = "sphinx" +version = "5.3.0" +requires_python = ">=3.6" +summary = "Python documentation generator" +dependencies = [ + "Jinja2>=3.0", + "Pygments>=2.12", + "alabaster<0.8,>=0.7", + "babel>=2.9", + "colorama>=0.4.5; sys_platform == \"win32\"", + "docutils<0.20,>=0.14", + "imagesize>=1.3", + "importlib-metadata>=4.8; python_version < \"3.10\"", + "packaging>=21.0", + "requests>=2.5.0", + "snowballstemmer>=2.0", + "sphinxcontrib-applehelp", + "sphinxcontrib-devhelp", + "sphinxcontrib-htmlhelp>=2.0.0", + "sphinxcontrib-jsmath", + "sphinxcontrib-qthelp", + "sphinxcontrib-serializinghtml>=1.1.5", +] +files = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +requires_python = ">=3.7" +summary = "A modern skeleton for Sphinx themes." +dependencies = [ + "sphinx>=4.0", +] +files = [ + {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, + {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +requires_python = ">=3.5" +summary = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +files = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +requires_python = ">=3.5" +summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +requires_python = ">=3.6" +summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +requires_python = ">=3.5" +summary = "A sphinx extension which renders display math in HTML via JavaScript" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +requires_python = ">=3.5" +summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +requires_python = ">=3.5" +summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[[package]] +name = "termcolor" +version = "2.3.0" +requires_python = ">=3.7" +summary = "ANSI color formatting for output in terminal" +files = [ + {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, + {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tornado" +version = "6.2" +requires_python = ">= 3.7" +summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +files = [ + {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, + {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, + {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, + {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, + {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, +] + +[[package]] +name = "tox" +version = "3.28.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +summary = "tox is a generic virtualenv management and test command line tool" +dependencies = [ + "colorama>=0.4.1; platform_system == \"Windows\"", + "filelock>=3.0.0", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "packaging>=14", + "pluggy>=0.12.0", + "py>=1.4.17", + "six>=1.14.0", + "tomli>=2.0.1; python_version >= \"3.7\" and python_version < \"3.11\"", + "virtualenv!=20.0.0,!=20.0.1,!=20.0.2,!=20.0.3,!=20.0.4,!=20.0.5,!=20.0.6,!=20.0.7,>=16.0.0", +] +files = [ + {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, + {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +requires_python = ">=3.6" +summary = "a fork of Python 2 and 3 ast modules with type comment support" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +requires_python = ">=3.7" +summary = "Backported and Experimental Type Hints for Python 3.7+" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "typogrify" +version = "2.0.7" +summary = "Filters to enhance web typography, including support for Django & Jinja templates" +dependencies = [ + "smartypants>=1.8.3", +] +files = [ + {file = "typogrify-2.0.7.tar.gz", hash = "sha256:8be4668cda434163ce229d87ca273a11922cb1614cb359970b7dc96eed13cb38"}, +] + +[[package]] +name = "unidecode" +version = "1.3.7" +requires_python = ">=3.5" +summary = "ASCII transliterations of Unicode text" +files = [ + {file = "Unidecode-1.3.7-py3-none-any.whl", hash = "sha256:663a537f506834ed836af26a81b210d90cbde044c47bfbdc0fbbc9f94c86a6e4"}, + {file = "Unidecode-1.3.7.tar.gz", hash = "sha256:3c90b4662aa0de0cb591884b934ead8d2225f1800d8da675a7750cbc3bd94610"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +requires_python = ">=3.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[[package]] +name = "virtualenv" +version = "20.24.6" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<4,>=3.9.1", +] +files = [ + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, +] + +[[package]] +name = "zipp" +version = "3.15.0" +requires_python = ">=3.7" +summary = "Backport of pathlib-compatible object wrapper for zip files" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] diff --git a/pyproject.toml b/pyproject.toml index 02d1160e..9383029b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,30 @@ -[tool.poetry] +[project] +authors = [{ name = "Justin Mayer", email = "authors@getpelican.com" }] +license = { text = "AGPLv3" } +requires-python = ">=3.8,<4.0" +dependencies = [ + "blinker>=1.4", + "docutils>=0.16", + "feedgenerator>=1.9", + "jinja2>=2.7", + "pygments>=2.6", + "python-dateutil>=2.8", + "rich>=10.1", + "unidecode>=1.1", + "backports-zoneinfo<1.0.0,>=0.2.1; python_version < \"3.9\"", +] name = "pelican" version = "4.8.0" description = "Static site generator supporting Markdown and reStructuredText" -authors = ["Justin Mayer "] -license = "AGPLv3" readme = "README.rst" keywords = ["static site generator", "static sites", "ssg"] - -homepage = "https://getpelican.com" -repository = "https://github.com/getpelican/pelican" -documentation = "https://docs.getpelican.com" - classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: Pelican", @@ -25,56 +38,12 @@ classifiers = [ "Topic :: Text Processing :: Markup :: reStructuredText", ] -[tool.poetry.urls] -"Funding" = "https://donate.getpelican.com/" -"Tracker" = "https://github.com/getpelican/pelican/issues" - -[tool.poetry.dependencies] -python = ">=3.7,<4.0" -blinker = ">=1.4" -docutils = ">=0.16" -feedgenerator = ">=1.9" -jinja2 = ">=2.7" -pygments = ">=2.6" -python-dateutil = ">=2.8" -rich = ">=10.1" -unidecode = ">=1.1" -markdown = {version = ">=3.1", optional = true} -backports-zoneinfo = {version = "^0.2.1", python = "<3.9"} -watchfiles = "^0.19.0" - -[tool.poetry.dev-dependencies] -BeautifulSoup4 = "^4.9" -jinja2 = "~3.1.2" -lxml = "^4.3" -markdown = "~3.4.3" -typogrify = "^2.0" -sphinx = "^5.1" -furo = "2023.03.27" -livereload = "^2.6" -psutil = {version = "^5.7", optional = true} -pygments = "~2.15" -pytest = "^7.1" -pytest-cov = "^4.0" -pytest-sugar = "^0.9.5" -pytest-xdist = "^2.0" -ruff = "^0.1.3" -tox = {version = "^3.13", optional = true} -flake8 = "^3.8" -flake8-import-order = "^0.18.1" -invoke = "^2.0" -isort = "^5.2" -black = {version = "^19.10b0", allow-prereleases = true} - -[tool.poetry.extras] -markdown = ["markdown"] - -[tool.poetry.scripts] +[project.scripts] pelican = "pelican.__main__:main" pelican-import = "pelican.tools.pelican_import:main" -pelican-plugins = "pelican.plugins._utils:list_plugins" pelican-quickstart = "pelican.tools.pelican_quickstart:main" pelican-themes = "pelican.tools.pelican_themes:main" +pelican-plugins = "pelican.plugins._utils:list_plugins" [tool.autopub] project-name = "Pelican" @@ -86,5 +55,49 @@ version-header = "=" version-strings = ["setup.py"] build-system = "setuptools" +[tool.pdm] + +[tool.pdm.scripts] +docbuild = "invoke docbuild" +docserve = "invoke docserve" + +[tool.pdm.dev-dependencies] +dev = [ + "BeautifulSoup4<5.0,>=4.9", + "jinja2~=3.1.2", + "lxml<5.0,>=4.3", + "markdown~=3.4.3", + "typogrify<3.0,>=2.0", + "sphinx<6.0,>=5.1", + "furo==2023.03.27", + "livereload<3.0,>=2.6", + "psutil<6.0,>=5.7", + "pygments~=2.15", + "pytest<8.0,>=7.1", + "pytest-cov<5.0,>=4.0", + "pytest-sugar<1.0.0,>=0.9.5", + "pytest-xdist<3.0,>=2.0", + "tox<4.0,>=3.13", + "flake8<4.0,>=3.8", + "flake8-import-order<1.0.0,>=0.18.1", + "invoke<3.0,>=2.0", + "isort<6.0,>=5.2", + "black<20.0,>=19.10b0", +] + +[tool.pdm.build] +includes = [] + +[project.urls] +Funding = "https://donate.getpelican.com/" +Tracker = "https://github.com/getpelican/pelican/issues" +Homepage = "https://getpelican.com" +Repository = "https://github.com/getpelican/pelican" +Documentation = "https://docs.getpelican.com" + +[project.optional-dependencies] +markdown = ["markdown>=3.1"] + [build-system] -requires = ["setuptools >= 40.6.0", "wheel"] +requires = ["pdm-backend"] +build-backend = "pdm.backend" From c18f1a7308d528743746986d9aaebf29b04af645 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sat, 28 Oct 2023 12:17:57 +0200 Subject: [PATCH 47/88] Re-order pyproject items, with other small fixes --- pyproject.toml | 74 ++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9383029b..da9deec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,33 @@ [project] +name = "pelican" authors = [{ name = "Justin Mayer", email = "authors@getpelican.com" }] +description = "Static site generator supporting Markdown and reStructuredText" +version = "4.8.0" license = { text = "AGPLv3" } -requires-python = ">=3.8,<4.0" +readme = "README.rst" +keywords = ["static site generator", "static sites", "ssg"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Framework :: Pelican", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System", + "Topic :: Internet :: WWW/HTTP :: Site Management", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Markup :: Markdown", + "Topic :: Text Processing :: Markup :: HTML", + "Topic :: Text Processing :: Markup :: reStructuredText", +] +requires-python = ">=3.8.1,<4.0" dependencies = [ "blinker>=1.4", "docutils>=0.16", @@ -11,33 +37,19 @@ dependencies = [ "python-dateutil>=2.8", "rich>=10.1", "unidecode>=1.1", - "backports-zoneinfo<1.0.0,>=0.2.1; python_version < \"3.9\"", -] -name = "pelican" -version = "4.8.0" -description = "Static site generator supporting Markdown and reStructuredText" -readme = "README.rst" -keywords = ["static site generator", "static sites", "ssg"] -classifiers = [ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Framework :: Pelican", - "Operating System :: OS Independent", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System", - "Topic :: Internet :: WWW/HTTP :: Site Management", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Text Processing :: Markup :: Markdown", - "Topic :: Text Processing :: Markup :: HTML", - "Topic :: Text Processing :: Markup :: reStructuredText", + "backports-zoneinfo<1.0.0,>=0.2.1; python_version<3.9", ] +[project.optional-dependencies] +markdown = ["markdown>=3.1"] + +[project.urls] +Homepage = "https://getpelican.com" +Funding = "https://donate.getpelican.com/" +"Issue Tracker" = "https://github.com/getpelican/pelican/issues" +Repository = "https://github.com/getpelican/pelican" +Documentation = "https://docs.getpelican.com" + [project.scripts] pelican = "pelican.__main__:main" pelican-import = "pelican.tools.pelican_import:main" @@ -88,16 +100,6 @@ dev = [ [tool.pdm.build] includes = [] -[project.urls] -Funding = "https://donate.getpelican.com/" -Tracker = "https://github.com/getpelican/pelican/issues" -Homepage = "https://getpelican.com" -Repository = "https://github.com/getpelican/pelican" -Documentation = "https://docs.getpelican.com" - -[project.optional-dependencies] -markdown = ["markdown>=3.1"] - [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" From 8b6d2159345ee9260a2fda0911decab3248f112f Mon Sep 17 00:00:00 2001 From: Lioman Date: Sat, 28 Oct 2023 17:43:16 +0200 Subject: [PATCH 48/88] migrate configuration to PEP621 compatible config - adapt documentation - add wheel tests to check wheel contents. - adapt pipeline to use pdm - adapt autopub config - add scripts as shortcuts to invoke tasks --- .github/workflows/main.yml | 17 +- .gitignore | 3 +- .pdm-python | 1 - docs/conf.py | 82 +- docs/contribute.rst | 12 +- docs/install.rst | 2 +- docs/quickstart.rst | 2 +- pdm.lock | 1431 ------------------------ pelican/tests/build_test/conftest.py | 7 + pelican/tests/build_test/test_wheel.py | 28 + pyproject.toml | 14 +- requirements/docs.pip | 1 + tasks.py | 6 +- 13 files changed, 112 insertions(+), 1494 deletions(-) delete mode 100644 .pdm-python delete mode 100644 pdm.lock create mode 100644 pelican/tests/build_test/conftest.py create mode 100644 pelican/tests/build_test/test_wheel.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d867122f..ff1c15b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,23 +51,18 @@ jobs: lint: name: Lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - - name: Install Poetry - run: pipx install poetry - - name: Set up Python - uses: actions/setup-python@v4 + - uses: pdm-project/setup-pdm@v3 with: - python-version: "3.9" - cache: "poetry" - cache-dependency-path: "pyproject.toml" + python-version: 3.9 + cache: true + cache-dependency-path: ./pyproject.toml - name: Install dependencies run: | - poetry env use "3.9" - poetry install --no-interaction --no-root + pdm install - name: Run linters - run: poetry run invoke lint --diff + run: pdm lint --diff docs: name: Build docs diff --git a/.gitignore b/.gitignore index b94526d6..b27f3eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ htmlcov venv samples/output *.pem -poetry.lock +*.lock +.pdm-python diff --git a/.pdm-python b/.pdm-python deleted file mode 100644 index c740e1bd..00000000 --- a/.pdm-python +++ /dev/null @@ -1 +0,0 @@ -/Users/eliaskirchgaessner/Development/FOSS/pelican/.venv/bin/python \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index f00ed3c2..8d8078a2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,48 +2,58 @@ import datetime import os import sys -from pelican import __version__ +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + sys.path.append(os.path.abspath(os.pardir)) + +with open("../pyproject.toml", "rb") as f: + project_data = tomllib.load(f).get("project") + if project_data is None: + raise KeyError("project data is not found") + + # -- General configuration ---------------------------------------------------- -templates_path = ['_templates'] +templates_path = ["_templates"] extensions = [ "sphinx.ext.autodoc", "sphinx.ext.ifconfig", "sphinx.ext.extlinks", "sphinxext.opengraph", ] -source_suffix = '.rst' -master_doc = 'index' -project = 'Pelican' +source_suffix = ".rst" +master_doc = "index" +project = project_data.get("name").upper() year = datetime.datetime.now().date().year -copyright = f'2010–{year}' -exclude_patterns = ['_build'] -release = __version__ -version = '.'.join(release.split('.')[:1]) -last_stable = __version__ -rst_prolog = ''' -.. |last_stable| replace:: :pelican-doc:`{}` -'''.format(last_stable) +copyright = f"2010–{year}" +exclude_patterns = ["_build"] +release = project_data.get("version") +version = ".".join(release.split(".")[:1]) +last_stable = project_data.get("version") +rst_prolog = f""" +.. |last_stable| replace:: :pelican-doc:`{last_stable}` +.. |min_python| replace:: {project_data.get('requires-python').split(",")[0]} +""" -extlinks = { - 'pelican-doc': ('https://docs.getpelican.com/en/latest/%s.html', '%s') -} +extlinks = {"pelican-doc": ("https://docs.getpelican.com/en/latest/%s.html", "%s")} # -- Options for HTML output -------------------------------------------------- -html_theme = 'furo' -html_title = f'{project} {release}' -html_static_path = ['_static'] +html_theme = "furo" +html_title = f"{project} {release}" +html_static_path = ["_static"] html_theme_options = { - 'light_logo': 'pelican-logo.svg', - 'dark_logo': 'pelican-logo.svg', - 'navigation_with_keys': True, + "light_logo": "pelican-logo.svg", + "dark_logo": "pelican-logo.svg", + "navigation_with_keys": True, } # Output file base name for HTML help builder. -htmlhelp_basename = 'Pelicandoc' +htmlhelp_basename = "Pelicandoc" html_use_smartypants = True @@ -59,21 +69,29 @@ html_show_sourcelink = False def setup(app): # overrides for wide tables in RTD theme - app.add_css_file('theme_overrides.css') # path relative to _static + app.add_css_file("theme_overrides.css") # path relative to _static # -- Options for LaTeX output ------------------------------------------------- latex_documents = [ - ('index', 'Pelican.tex', 'Pelican Documentation', 'Justin Mayer', - 'manual'), + ("index", "Pelican.tex", "Pelican Documentation", "Justin Mayer", "manual"), ] # -- Options for manual page output ------------------------------------------- man_pages = [ - ('index', 'pelican', 'pelican documentation', - ['Justin Mayer'], 1), - ('pelican-themes', 'pelican-themes', 'A theme manager for Pelican', - ['Mickaël Raybaud'], 1), - ('themes', 'pelican-theming', 'How to create themes for Pelican', - ['The Pelican contributors'], 1) + ("index", "pelican", "pelican documentation", ["Justin Mayer"], 1), + ( + "pelican-themes", + "pelican-themes", + "A theme manager for Pelican", + ["Mickaël Raybaud"], + 1, + ), + ( + "themes", + "pelican-theming", + "How to create themes for Pelican", + ["The Pelican contributors"], + 1, + ), ] diff --git a/docs/contribute.rst b/docs/contribute.rst index cfbfe351..a5292dd5 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -15,16 +15,16 @@ Setting up the development environment ====================================== While there are many ways to set up one's development environment, the following -instructions will utilize Pip_ and Poetry_. These tools facilitate managing +instructions will utilize Pip_ and pdm_. These tools facilitate managing virtual environments for separate Python projects that are isolated from one another, so you can use different packages (and package versions) for each. -Please note that Python 3.7+ is required for Pelican development. +Please note that Python |min_python| is required for Pelican development. -*(Optional)* If you prefer to `install Poetry `_ once for use with multiple projects, +*(Optional)* If you prefer to `install pdm `_ once for use with multiple projects, you can install it via:: - curl -sSL https://install.python-poetry.org | python3 - + curl -sSL https://pdm.fming.dev/install-pdm.py | python3 - Point your web browser to the `Pelican repository`_ and tap the **Fork** button at top-right. Then clone the source for your fork and add the upstream project @@ -35,7 +35,7 @@ as a Git remote:: cd ~/projects/pelican git remote add upstream https://github.com/getpelican/pelican.git -While Poetry can dynamically create and manage virtual environments, we're going +While pdm can dynamically create and manage virtual environments, we're going to manually create and activate a virtual environment:: mkdir ~/virtualenvs && cd ~/virtualenvs @@ -51,7 +51,7 @@ Install the needed dependencies and set up the project:: Your local environment should now be ready to go! .. _Pip: https://pip.pypa.io/ -.. _Poetry: https://python-poetry.org/ +.. _pdm: https://pdm.fming.dev/latest/ .. _Pelican repository: https://github.com/getpelican/pelican Development diff --git a/docs/install.rst b/docs/install.rst index ea47311f..aa3c92d0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,7 +1,7 @@ Installing Pelican ################## -Pelican currently runs best on 3.7+; earlier versions of Python are not supported. +Pelican currently runs best on |min_python|; earlier versions of Python are not supported. You can install Pelican via several different methods. The simplest is via Pip_:: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f1198b94..686b822f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -8,7 +8,7 @@ Installation ------------ Install Pelican (and optionally Markdown if you intend to use it) on Python -3.7+ by running the following command in your preferred terminal, prefixing +|min_python| by running the following command in your preferred terminal, prefixing with ``sudo`` if permissions warrant:: python -m pip install "pelican[markdown]" diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index 0bfddb5e..00000000 --- a/pdm.lock +++ /dev/null @@ -1,1431 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev"] -cross_platform = true -static_urls = false -lock_version = "4.3" -content_hash = "sha256:0774056f38e53e29569c2888786ef845063ad0abcdaa8910c7795619996ef224" - -[[package]] -name = "alabaster" -version = "0.7.13" -requires_python = ">=3.6" -summary = "A configurable sidebar-enabled Sphinx theme" -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - -[[package]] -name = "appdirs" -version = "1.4.4" -summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - -[[package]] -name = "attrs" -version = "23.1.0" -requires_python = ">=3.7" -summary = "Classes Without Boilerplate" -dependencies = [ - "importlib-metadata; python_version < \"3.8\"", -] -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] - -[[package]] -name = "babel" -version = "2.13.0" -requires_python = ">=3.7" -summary = "Internationalization utilities" -dependencies = [ - "pytz>=2015.7; python_version < \"3.9\"", -] -files = [ - {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, - {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, -] - -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -requires_python = ">=3.6" -summary = "Backport of the standard library zoneinfo module" -files = [ - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -requires_python = ">=3.6.0" -summary = "Screen-scraping library" -dependencies = [ - "soupsieve>1.2", -] -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[[package]] -name = "black" -version = "19.10b0" -requires_python = ">=3.6" -summary = "The uncompromising code formatter." -dependencies = [ - "appdirs", - "attrs>=18.1.0", - "click>=6.5", - "pathspec<1,>=0.6", - "regex", - "toml>=0.9.4", - "typed-ast>=1.4.0", -] -files = [ - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] - -[[package]] -name = "blinker" -version = "1.6.3" -requires_python = ">=3.7" -summary = "Fast, simple object-to-object and broadcast signaling" -files = [ - {file = "blinker-1.6.3-py3-none-any.whl", hash = "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa"}, - {file = "blinker-1.6.3.tar.gz", hash = "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d"}, -] - -[[package]] -name = "certifi" -version = "2023.7.22" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.1" -requires_python = ">=3.7.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -files = [ - {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, - {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, -] - -[[package]] -name = "click" -version = "8.1.7" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.2.7" -requires_python = ">=3.7" -summary = "Code coverage measurement for Python" -files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, -] - -[[package]] -name = "coverage" -version = "7.2.7" -extras = ["toml"] -requires_python = ">=3.7" -summary = "Code coverage measurement for Python" -dependencies = [ - "coverage==7.2.7", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, -] - -[[package]] -name = "distlib" -version = "0.3.7" -summary = "Distribution utilities" -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "docutils" -version = "0.19" -requires_python = ">=3.7" -summary = "Docutils -- Python Documentation Utilities" -files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.1.3" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, -] - -[[package]] -name = "execnet" -version = "2.0.2" -requires_python = ">=3.7" -summary = "execnet: rapid multi-Python deployment" -files = [ - {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, - {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, -] - -[[package]] -name = "feedgenerator" -version = "2.1.0" -requires_python = ">=3.7" -summary = "Standalone version of django.utils.feedgenerator" -dependencies = [ - "pytz>=0a", -] -files = [ - {file = "feedgenerator-2.1.0-py3-none-any.whl", hash = "sha256:93b7ce1c5a86195cafd6a8e9baf6a2a863ebd6d9905e840ce5778f73efd9a8d5"}, - {file = "feedgenerator-2.1.0.tar.gz", hash = "sha256:f075f23f28fd227f097c36b212161c6cf012e1c6caaf7ff53d5d6bb02cd42b9d"}, -] - -[[package]] -name = "filelock" -version = "3.12.2" -requires_python = ">=3.7" -summary = "A platform independent file lock." -files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, -] - -[[package]] -name = "flake8" -version = "3.9.2" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -summary = "the modular source code checker: pep8 pyflakes and co" -dependencies = [ - "importlib-metadata; python_version < \"3.8\"", - "mccabe<0.7.0,>=0.6.0", - "pycodestyle<2.8.0,>=2.7.0", - "pyflakes<2.4.0,>=2.3.0", -] -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[[package]] -name = "flake8-import-order" -version = "0.18.2" -summary = "Flake8 and pylama plugin that checks the ordering of import statements." -dependencies = [ - "pycodestyle", - "setuptools", -] -files = [ - {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, - {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, -] - -[[package]] -name = "furo" -version = "2023.3.27" -requires_python = ">=3.7" -summary = "A clean customisable Sphinx documentation theme." -dependencies = [ - "beautifulsoup4", - "pygments>=2.7", - "sphinx-basic-ng", - "sphinx<7.0,>=5.0", -] -files = [ - {file = "furo-2023.3.27-py3-none-any.whl", hash = "sha256:4ab2be254a2d5e52792d0ca793a12c35582dd09897228a6dd47885dabd5c9521"}, - {file = "furo-2023.3.27.tar.gz", hash = "sha256:b99e7867a5cc833b2b34d7230631dd6558c7a29f93071fdbb5709634bb33c5a5"}, -] - -[[package]] -name = "idna" -version = "3.4" -requires_python = ">=3.5" -summary = "Internationalized Domain Names in Applications (IDNA)" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "imagesize" -version = "1.4.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "Getting image size from png/jpeg/jpeg2000/gif file" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.7.0" -requires_python = ">=3.7" -summary = "Read metadata from Python packages" -dependencies = [ - "typing-extensions>=3.6.4; python_version < \"3.8\"", - "zipp>=0.5", -] -files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "invoke" -version = "2.2.0" -requires_python = ">=3.6" -summary = "Pythonic task execution" -files = [ - {file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"}, - {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"}, -] - -[[package]] -name = "isort" -version = "5.11.5" -requires_python = ">=3.7.0" -summary = "A Python utility / library to sort Python imports." -files = [ - {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, - {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, -] - -[[package]] -name = "jinja2" -version = "3.1.2" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[[package]] -name = "livereload" -version = "2.6.3" -summary = "Python LiveReload is an awesome tool for web developers" -dependencies = [ - "six", - "tornado; python_version > \"2.7\"", -] -files = [ - {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] - -[[package]] -name = "lxml" -version = "4.9.3" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -files = [ - {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, - {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, - {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, - {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, - {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, - {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, - {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, - {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, - {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, - {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, - {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, - {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, - {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, - {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, - {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, - {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, - {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, -] - -[[package]] -name = "markdown" -version = "3.4.4" -requires_python = ">=3.7" -summary = "Python implementation of John Gruber's Markdown." -dependencies = [ - "importlib-metadata>=4.4; python_version < \"3.10\"", -] -files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, -] - -[[package]] -name = "markdown-it-py" -version = "2.2.0" -requires_python = ">=3.7" -summary = "Python port of markdown-it. Markdown parsing, done right!" -dependencies = [ - "mdurl~=0.1", - "typing-extensions>=3.7.4; python_version < \"3.8\"", -] -files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, -] - -[[package]] -name = "markupsafe" -version = "2.1.3" -requires_python = ">=3.7" -summary = "Safely add untrusted strings to HTML/XML markup." -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "mccabe" -version = "0.6.1" -summary = "McCabe checker, plugin for flake8" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "packaging" -version = "23.2" -requires_python = ">=3.7" -summary = "Core utilities for Python packages" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "pathspec" -version = "0.11.2" -requires_python = ">=3.7" -summary = "Utility library for gitignore style pattern matching of file paths." -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - -[[package]] -name = "platformdirs" -version = "3.11.0" -requires_python = ">=3.7" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -dependencies = [ - "typing-extensions>=4.7.1; python_version < \"3.8\"", -] -files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, -] - -[[package]] -name = "pluggy" -version = "1.2.0" -requires_python = ">=3.7" -summary = "plugin and hook calling mechanisms for python" -dependencies = [ - "importlib-metadata>=0.12; python_version < \"3.8\"", -] -files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, -] - -[[package]] -name = "psutil" -version = "5.9.6" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -summary = "Cross-platform lib for process and system monitoring in Python." -files = [ - {file = "psutil-5.9.6-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c69596f9fc2f8acd574a12d5f8b7b1ba3765a641ea5d60fb4736bf3c08a8214a"}, - {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92e0cc43c524834af53e9d3369245e6cc3b130e78e26100d1f63cdb0abeb3d3c"}, - {file = "psutil-5.9.6-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748c9dd2583ed86347ed65d0035f45fa8c851e8d90354c122ab72319b5f366f4"}, - {file = "psutil-5.9.6-cp37-abi3-win32.whl", hash = "sha256:a6f01f03bf1843280f4ad16f4bde26b817847b4c1a0db59bf6419807bc5ce05c"}, - {file = "psutil-5.9.6-cp37-abi3-win_amd64.whl", hash = "sha256:6e5fb8dc711a514da83098bc5234264e551ad980cec5f85dabf4d38ed6f15e9a"}, - {file = "psutil-5.9.6-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:daecbcbd29b289aac14ece28eca6a3e60aa361754cf6da3dfb20d4d32b6c7f57"}, - {file = "psutil-5.9.6.tar.gz", hash = "sha256:e4b92ddcd7dd4cdd3f900180ea1e104932c7bce234fb88976e2a3b296441225a"}, -] - -[[package]] -name = "py" -version = "1.11.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "library with cross-python path, ini-parsing, io, code, log facilities" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - -[[package]] -name = "pycodestyle" -version = "2.7.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "Python style guide checker" -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] - -[[package]] -name = "pyflakes" -version = "2.3.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "passive checker of Python programs" -files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] - -[[package]] -name = "pygments" -version = "2.16.1" -requires_python = ">=3.7" -summary = "Pygments is a syntax highlighting package written in Python." -files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, -] - -[[package]] -name = "pytest" -version = "7.4.2" -requires_python = ">=3.7" -summary = "pytest: simple powerful testing with Python" -dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "importlib-metadata>=0.12; python_version < \"3.8\"", - "iniconfig", - "packaging", - "pluggy<2.0,>=0.12", - "tomli>=1.0.0; python_version < \"3.11\"", -] -files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, -] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -requires_python = ">=3.7" -summary = "Pytest plugin for measuring coverage." -dependencies = [ - "coverage[toml]>=5.2.1", - "pytest>=4.6", -] -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[[package]] -name = "pytest-forked" -version = "1.6.0" -requires_python = ">=3.7" -summary = "run tests in isolated forked subprocesses" -dependencies = [ - "py", - "pytest>=3.10", -] -files = [ - {file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"}, - {file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"}, -] - -[[package]] -name = "pytest-sugar" -version = "0.9.7" -summary = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." -dependencies = [ - "packaging>=21.3", - "pytest>=6.2.0", - "termcolor>=2.1.0", -] -files = [ - {file = "pytest-sugar-0.9.7.tar.gz", hash = "sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46"}, - {file = "pytest_sugar-0.9.7-py2.py3-none-any.whl", hash = "sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062"}, -] - -[[package]] -name = "pytest-xdist" -version = "2.5.0" -requires_python = ">=3.6" -summary = "pytest xdist plugin for distributed testing and loop-on-failing modes" -dependencies = [ - "execnet>=1.1", - "pytest-forked", - "pytest>=6.2.0", -] -files = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[[package]] -name = "pytz" -version = "2023.3.post1" -summary = "World timezone definitions, modern and historical" -files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, -] - -[[package]] -name = "regex" -version = "2023.10.3" -requires_python = ">=3.7" -summary = "Alternative regular expression module, to replace re." -files = [ - {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, - {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, - {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, - {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, - {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, - {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, - {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, - {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, - {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, - {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, - {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, - {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, - {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, - {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, - {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, - {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -requires_python = ">=3.7" -summary = "Python HTTP for Humans." -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[[package]] -name = "rich" -version = "13.6.0" -requires_python = ">=3.7.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, -] - -[[package]] -name = "setuptools" -version = "68.0.0" -requires_python = ">=3.7" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, -] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "smartypants" -version = "2.0.1" -summary = "Python with the SmartyPants" -files = [ - {file = "smartypants-2.0.1-py2.py3-none-any.whl", hash = "sha256:8db97f7cbdf08d15b158a86037cd9e116b4cf37703d24e0419a0d64ca5808f0d"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.4.1" -requires_python = ">=3.7" -summary = "A modern CSS selector implementation for Beautiful Soup." -files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, -] - -[[package]] -name = "sphinx" -version = "5.3.0" -requires_python = ">=3.6" -summary = "Python documentation generator" -dependencies = [ - "Jinja2>=3.0", - "Pygments>=2.12", - "alabaster<0.8,>=0.7", - "babel>=2.9", - "colorama>=0.4.5; sys_platform == \"win32\"", - "docutils<0.20,>=0.14", - "imagesize>=1.3", - "importlib-metadata>=4.8; python_version < \"3.10\"", - "packaging>=21.0", - "requests>=2.5.0", - "snowballstemmer>=2.0", - "sphinxcontrib-applehelp", - "sphinxcontrib-devhelp", - "sphinxcontrib-htmlhelp>=2.0.0", - "sphinxcontrib-jsmath", - "sphinxcontrib-qthelp", - "sphinxcontrib-serializinghtml>=1.1.5", -] -files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, -] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -requires_python = ">=3.7" -summary = "A modern skeleton for Sphinx themes." -dependencies = [ - "sphinx>=4.0", -] -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.2" -requires_python = ">=3.5" -summary = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -files = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -requires_python = ">=3.5" -summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.0" -requires_python = ">=3.6" -summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -requires_python = ">=3.5" -summary = "A sphinx extension which renders display math in HTML via JavaScript" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -requires_python = ">=3.5" -summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -requires_python = ">=3.5" -summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[[package]] -name = "termcolor" -version = "2.3.0" -requires_python = ">=3.7" -summary = "ANSI color formatting for output in terminal" -files = [ - {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, - {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python Library for Tom's Obvious, Minimal Language" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -requires_python = ">=3.7" -summary = "A lil' TOML parser" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.2" -requires_python = ">= 3.7" -summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -files = [ - {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, - {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, - {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, - {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, - {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, -] - -[[package]] -name = "tox" -version = "3.28.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -summary = "tox is a generic virtualenv management and test command line tool" -dependencies = [ - "colorama>=0.4.1; platform_system == \"Windows\"", - "filelock>=3.0.0", - "importlib-metadata>=0.12; python_version < \"3.8\"", - "packaging>=14", - "pluggy>=0.12.0", - "py>=1.4.17", - "six>=1.14.0", - "tomli>=2.0.1; python_version >= \"3.7\" and python_version < \"3.11\"", - "virtualenv!=20.0.0,!=20.0.1,!=20.0.2,!=20.0.3,!=20.0.4,!=20.0.5,!=20.0.6,!=20.0.7,>=16.0.0", -] -files = [ - {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, - {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, -] - -[[package]] -name = "typed-ast" -version = "1.5.5" -requires_python = ">=3.6" -summary = "a fork of Python 2 and 3 ast modules with type comment support" -files = [ - {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, - {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, - {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, - {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, - {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, - {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, - {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, - {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, - {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, -] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -requires_python = ">=3.7" -summary = "Backported and Experimental Type Hints for Python 3.7+" -files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, -] - -[[package]] -name = "typogrify" -version = "2.0.7" -summary = "Filters to enhance web typography, including support for Django & Jinja templates" -dependencies = [ - "smartypants>=1.8.3", -] -files = [ - {file = "typogrify-2.0.7.tar.gz", hash = "sha256:8be4668cda434163ce229d87ca273a11922cb1614cb359970b7dc96eed13cb38"}, -] - -[[package]] -name = "unidecode" -version = "1.3.7" -requires_python = ">=3.5" -summary = "ASCII transliterations of Unicode text" -files = [ - {file = "Unidecode-1.3.7-py3-none-any.whl", hash = "sha256:663a537f506834ed836af26a81b210d90cbde044c47bfbdc0fbbc9f94c86a6e4"}, - {file = "Unidecode-1.3.7.tar.gz", hash = "sha256:3c90b4662aa0de0cb591884b934ead8d2225f1800d8da675a7750cbc3bd94610"}, -] - -[[package]] -name = "urllib3" -version = "2.0.7" -requires_python = ">=3.7" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, -] - -[[package]] -name = "virtualenv" -version = "20.24.6" -requires_python = ">=3.7" -summary = "Virtual Python Environment builder" -dependencies = [ - "distlib<1,>=0.3.7", - "filelock<4,>=3.12.2", - "importlib-metadata>=6.6; python_version < \"3.8\"", - "platformdirs<4,>=3.9.1", -] -files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, -] - -[[package]] -name = "zipp" -version = "3.15.0" -requires_python = ">=3.7" -summary = "Backport of pathlib-compatible object wrapper for zip files" -files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] diff --git a/pelican/tests/build_test/conftest.py b/pelican/tests/build_test/conftest.py new file mode 100644 index 00000000..548f7970 --- /dev/null +++ b/pelican/tests/build_test/conftest.py @@ -0,0 +1,7 @@ +def pytest_addoption(parser): + parser.addoption( + "--check-wheel", + action="store", + default=False, + help="Check wheel contents.", + ) diff --git a/pelican/tests/build_test/test_wheel.py b/pelican/tests/build_test/test_wheel.py new file mode 100644 index 00000000..a4635481 --- /dev/null +++ b/pelican/tests/build_test/test_wheel.py @@ -0,0 +1,28 @@ +from pathlib import Path +import pytest +from zipfile import ZipFile + + +@pytest.mark.skipif( + "not config.getoption('--check-wheel')", + reason="Only run when --check-wheel is given", +) +def test_wheel_contents(pytestconfig): + """ + This test, should test the contents of the wheel to make sure, + that everything that is needed is included in the final build + """ + wheel_file = pytestconfig.getoption("--check-wheel") + assert wheel_file.endswith(".whl") + files_list = ZipFile(wheel_file).namelist() + ## Check is theme files are copiedto wheel + simple_theme = Path("./pelican/themes/simple/templates") + for x in simple_theme.iterdir(): + assert str(x) in files_list + + ## Check is tool templatesare copiedto wheel + tools = Path("./pelican/tools/templates") + for x in tools.iterdir(): + assert str(x) in files_list + + assert "pelican/tools/templates/tasks.py.jinja2" in files_list diff --git a/pyproject.toml b/pyproject.toml index da9deec1..9d439680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ dependencies = [ "python-dateutil>=2.8", "rich>=10.1", "unidecode>=1.1", - "backports-zoneinfo<1.0.0,>=0.2.1; python_version<3.9", + "backports-zoneinfo<1.0.0,>=0.2.1;python_version<'3.9'", + "watchfiles", ] [project.optional-dependencies] @@ -53,9 +54,9 @@ Documentation = "https://docs.getpelican.com" [project.scripts] pelican = "pelican.__main__:main" pelican-import = "pelican.tools.pelican_import:main" +pelican-plugins = "pelican.plugins._utils:list_plugins" pelican-quickstart = "pelican.tools.pelican_quickstart:main" pelican-themes = "pelican.tools.pelican_themes:main" -pelican-plugins = "pelican.plugins._utils:list_plugins" [tool.autopub] project-name = "Pelican" @@ -64,14 +65,14 @@ git-email = "52496925+botpub@users.noreply.github.com" changelog-file = "docs/changelog.rst" changelog-header = "###############" version-header = "=" -version-strings = ["setup.py"] -build-system = "setuptools" [tool.pdm] [tool.pdm.scripts] docbuild = "invoke docbuild" docserve = "invoke docserve" +lint = "invoke lint" +test = "invoke tests" [tool.pdm.dev-dependencies] dev = [ @@ -95,11 +96,10 @@ dev = [ "invoke<3.0,>=2.0", "isort<6.0,>=5.2", "black<20.0,>=19.10b0", + "ruff>=0.1.3,<1.0.0", + "tomli;python_version<'3.11'", ] -[tool.pdm.build] -includes = [] - [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" diff --git a/requirements/docs.pip b/requirements/docs.pip index 6db7c6c8..961a6473 100644 --- a/requirements/docs.pip +++ b/requirements/docs.pip @@ -2,3 +2,4 @@ sphinx<6.0 sphinxext-opengraph furo livereload +tomli;python_version<"3.11" diff --git a/tasks.py b/tasks.py index e9f65db3..56461679 100644 --- a/tasks.py +++ b/tasks.py @@ -15,8 +15,8 @@ VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME) VENV = str(VENV_PATH.expanduser()) VENV_BIN = Path(VENV) / Path(BIN_DIR) -TOOLS = ["poetry", "pre-commit", "psutil"] -POETRY = which("poetry") or VENV_BIN / "poetry" +TOOLS = ["pdm", "pre-commit", "psutil"] +PDM = which("pdm") or VENV_BIN / "pdm" PRECOMMIT = which("pre-commit") or VENV_BIN / "pre-commit" @@ -107,7 +107,7 @@ def precommit(c): def setup(c): c.run(f"{VENV_BIN}/python -m pip install -U pip", pty=PTY) tools(c) - c.run(f"{POETRY} install", pty=PTY) + c.run(f"{PDM} install", pty=PTY) precommit(c) From 00d26fc0686acc6b8405cbc9c9e014b9671eaccd Mon Sep 17 00:00:00 2001 From: Lioman Date: Sun, 29 Oct 2023 11:56:28 +0100 Subject: [PATCH 49/88] remove old setup files --- MANIFEST.in | 6 ---- setup.cfg | 2 -- setup.py | 96 ----------------------------------------------------- 3 files changed, 104 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 87d433a8..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include *.rst -recursive-include pelican *.html *.css *png *.rst *.markdown *.md *.mkd *.xml *.py *.jinja2 -include LICENSE THANKS docs/changelog.rst pyproject.toml -graft samples -global-exclude __pycache__ -global-exclude *.py[co] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf13..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100755 index 4ffee0cb..00000000 --- a/setup.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python - -from os import walk -from os.path import join, relpath - -from setuptools import find_packages, setup - - -version = "4.8.0" - -requires = [ - 'feedgenerator >= 1.9', - 'jinja2 >= 2.7', - 'pygments', - 'docutils>=0.15', - 'blinker', - 'unidecode', - 'python-dateutil', - 'rich', - 'backports-zoneinfo[tzdata] >= 0.2; python_version<"3.9"', - 'watchfiles' -] - -entry_points = { - 'console_scripts': [ - 'pelican = pelican.__main__:main', - 'pelican-import = pelican.tools.pelican_import:main', - 'pelican-quickstart = pelican.tools.pelican_quickstart:main', - 'pelican-themes = pelican.tools.pelican_themes:main', - 'pelican-plugins = pelican.plugins._utils:list_plugins' - ] -} - -README = open('README.rst', encoding='utf-8').read() -CHANGELOG = open('docs/changelog.rst', encoding='utf-8').read() - -# Relative links in the README must be converted to absolute URL's -# so that they render correctly on PyPI. -README = README.replace( - "", - "", -) - -description = '\n'.join([README, CHANGELOG]) - -setup( - name='pelican', - version=version, - url='https://getpelican.com/', - author='Justin Mayer', - author_email='authors@getpelican.com', - description="Static site generator supporting reStructuredText and " - "Markdown source content.", - project_urls={ - 'Documentation': 'https://docs.getpelican.com/', - 'Funding': 'https://donate.getpelican.com/', - 'Source': 'https://github.com/getpelican/pelican', - 'Tracker': 'https://github.com/getpelican/pelican/issues', - }, - keywords='static web site generator SSG reStructuredText Markdown', - license='AGPLv3', - long_description=description, - long_description_content_type='text/x-rst', - packages=find_packages(), - include_package_data=True, # includes all in MANIFEST.in if in package - # NOTE : This will collect any files that happen to be in the themes - # directory, even though they may not be checked into version control. - package_data={ # pelican/themes is not a package, so include manually - 'pelican': [relpath(join(root, name), 'pelican') - for root, _, names in walk(join('pelican', 'themes')) - for name in names], - }, - install_requires=requires, - extras_require={ - 'Markdown': ['markdown~=3.1.1'] - }, - entry_points=entry_points, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Framework :: Pelican', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - test_suite='pelican.tests', -) From eb052cae096c27dba40ccf3a467b197a7bc91e1b Mon Sep 17 00:00:00 2001 From: Lioman Date: Sun, 29 Oct 2023 12:41:55 +0100 Subject: [PATCH 50/88] Capitalize PDM in docs --- docs/contribute.rst | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/contribute.rst b/docs/contribute.rst index a5292dd5..33a62064 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -15,13 +15,13 @@ Setting up the development environment ====================================== While there are many ways to set up one's development environment, the following -instructions will utilize Pip_ and pdm_. These tools facilitate managing +instructions will utilize Pip_ and PDM_. These tools facilitate managing virtual environments for separate Python projects that are isolated from one another, so you can use different packages (and package versions) for each. Please note that Python |min_python| is required for Pelican development. -*(Optional)* If you prefer to `install pdm `_ once for use with multiple projects, +*(Optional)* If you prefer to `install PDM `_ once for use with multiple projects, you can install it via:: curl -sSL https://pdm.fming.dev/install-pdm.py | python3 - @@ -35,7 +35,7 @@ as a Git remote:: cd ~/projects/pelican git remote add upstream https://github.com/getpelican/pelican.git -While pdm can dynamically create and manage virtual environments, we're going +While PDM can dynamically create and manage virtual environments, we're going to manually create and activate a virtual environment:: mkdir ~/virtualenvs && cd ~/virtualenvs @@ -51,7 +51,7 @@ Install the needed dependencies and set up the project:: Your local environment should now be ready to go! .. _Pip: https://pip.pypa.io/ -.. _pdm: https://pdm.fming.dev/latest/ +.. _PDM: https://pdm.fming.dev/latest/ .. _Pelican repository: https://github.com/getpelican/pelican Development diff --git a/pyproject.toml b/pyproject.toml index 9d439680..ac4a73df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "rich>=10.1", "unidecode>=1.1", "backports-zoneinfo<1.0.0,>=0.2.1;python_version<'3.9'", - "watchfiles", + "watchfiles>=0.21.0", ] [project.optional-dependencies] From 8a0f335e2bfb8cb3bba0a1e6b9ec0468e5bcd8c9 Mon Sep 17 00:00:00 2001 From: Lioman Date: Sun, 29 Oct 2023 15:23:14 +0100 Subject: [PATCH 51/88] only install dev dependencies during lint step --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ff1c15b5..0127982e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,7 +60,7 @@ jobs: cache-dependency-path: ./pyproject.toml - name: Install dependencies run: | - pdm install + pdm install --no-default --dev - name: Run linters run: pdm lint --diff From cce15701359f028bddb3f4115c614b006efc560b Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 29 Oct 2023 15:53:11 +0100 Subject: [PATCH 52/88] Fix some comments in wheel-related test --- pelican/tests/build_test/test_wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pelican/tests/build_test/test_wheel.py b/pelican/tests/build_test/test_wheel.py index a4635481..8e643981 100644 --- a/pelican/tests/build_test/test_wheel.py +++ b/pelican/tests/build_test/test_wheel.py @@ -15,12 +15,12 @@ def test_wheel_contents(pytestconfig): wheel_file = pytestconfig.getoption("--check-wheel") assert wheel_file.endswith(".whl") files_list = ZipFile(wheel_file).namelist() - ## Check is theme files are copiedto wheel + # Check if theme files are copied to wheel simple_theme = Path("./pelican/themes/simple/templates") for x in simple_theme.iterdir(): assert str(x) in files_list - ## Check is tool templatesare copiedto wheel + # Check if tool templates are copied to wheel tools = Path("./pelican/tools/templates") for x in tools.iterdir(): assert str(x) in files_list From 9437de63419080a61733b26444517ac81a5e6b65 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 29 Oct 2023 15:51:24 +0100 Subject: [PATCH 53/88] Include more files in PDM sdist builds This was previously the job of directives in MANIFEST.in, which should be covered by this PDM-specific configuration. --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ac4a73df..58fda86b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,14 @@ dev = [ "tomli;python_version<'3.11'", ] +[tool.pdm.build] +source-includes = [ + "CONTRIBUTING.rst", + "THANKS", + "docs/changelog.rst", + "samples/", +] + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" From 6f1605edf9b434fd311a4e801b5d441a5f04985d Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sun, 29 Oct 2023 18:30:25 +0300 Subject: [PATCH 54/88] Extend GHA documentation to specify requirements file --- .github/workflows/github_pages.yml | 3 +-- docs/tips.rst | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_pages.yml b/.github/workflows/github_pages.yml index 481dd118..ccf172b4 100644 --- a/.github/workflows/github_pages.yml +++ b/.github/workflows/github_pages.yml @@ -9,7 +9,7 @@ on: requirements: required: false default: "pelican" - description: "The Python requirements to install, for example to enable markdown and typogrify use: 'pelican[markdown] typogrify'" + description: "The Python requirements to install, for example to enable markdown and typogrify use: 'pelican[markdown] typogrify' or if you have a requirements file use: '-r requirements.txt'" type: string output-path: required: false @@ -58,4 +58,3 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 - diff --git a/docs/tips.rst b/docs/tips.rst index abd46c8a..904e5ee7 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -187,6 +187,8 @@ the workflow: | | | install, for example to enable | | | | | | markdown and typogrify use: | | | | | | ``"pelican[markdown] typogrify"`` | | | +| | | or if you have a requirements | | | +| | | file: ``"-r requirements.txt"`` | | | +--------------+----------+-----------------------------------+--------+---------------+ | output-path | No | Where to output the generated | string | ``"output/"`` | | | | files (``pelican``'s ``--output`` | | | From dbe0b1125f573721dd25211bc19523baf1082425 Mon Sep 17 00:00:00 2001 From: boxydog Date: Sun, 29 Oct 2023 12:55:37 -0500 Subject: [PATCH 55/88] Don't copy file ownership, permissions and metadata --- pelican/utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pelican/utils.py b/pelican/utils.py index e1bed154..09ffcfe6 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -303,7 +303,7 @@ def copy(source, destination, ignores=None): logger.info('Creating directory %s', dst_dir) os.makedirs(dst_dir) logger.info('Copying %s to %s', source_, destination_) - copy_file_metadata(source_, destination_) + copy_file(source_, destination_) elif os.path.isdir(source_): if not os.path.exists(destination_): @@ -333,20 +333,17 @@ def copy(source, destination, ignores=None): dst_path = os.path.join(dst_dir, o) if os.path.isfile(src_path): logger.info('Copying %s to %s', src_path, dst_path) - copy_file_metadata(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) -def copy_file_metadata(source, destination): - '''Copy a file and its metadata (perm bits, access times, ...)''' - - # This function is a workaround for Android python copystat - # bug ([issue28141]) https://bugs.python.org/issue28141 +def copy_file(source, destination): + '''Copy a file''' try: - shutil.copy2(source, destination) + shutil.copyfile(source, destination) except OSError as e: logger.warning("A problem occurred copying file %s to %s; %s", source, destination, e) From 8ea27b82f6c0c6fccd4362246ae1d2ff76b46d05 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sun, 29 Oct 2023 09:50:01 -0700 Subject: [PATCH 56/88] Bump all of the dev dependencies - remove upper version caps - updated the minimum version of most of Pelican's runtime deps - replaced black with ruff as a formatter for pelican - added a cache step to the docs CI task so that the docs can be downloaded and inspected. --- .github/workflows/main.yml | 5 +++ pyproject.toml | 62 +++++++++++++++++++------------------- tasks.py | 19 ++++-------- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0127982e..59a22862 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,6 +80,11 @@ jobs: run: python -m pip install -U pip tox - name: Check run: tox -e docs + - name: cache the docs for inspection + uses: actions/upload-artifact@v3 + with: + name: docs + path: docs/_build/html/ deploy: name: Deploy diff --git a/pyproject.toml b/pyproject.toml index 58fda86b..4e3e712a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,15 +29,15 @@ classifiers = [ ] requires-python = ">=3.8.1,<4.0" dependencies = [ - "blinker>=1.4", - "docutils>=0.16", - "feedgenerator>=1.9", - "jinja2>=2.7", - "pygments>=2.6", - "python-dateutil>=2.8", - "rich>=10.1", - "unidecode>=1.1", - "backports-zoneinfo<1.0.0,>=0.2.1;python_version<'3.9'", + "blinker>=1.6.3", + "docutils>=0.20.1", + "feedgenerator>=2.1.0", + "jinja2>=3.1.2", + "pygments>=2.16.1", + "python-dateutil>=2.8.2", + "rich>=13.6.0", + "unidecode>=1.3.7", + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", "watchfiles>=0.21.0", ] @@ -76,28 +76,28 @@ test = "invoke tests" [tool.pdm.dev-dependencies] dev = [ - "BeautifulSoup4<5.0,>=4.9", - "jinja2~=3.1.2", - "lxml<5.0,>=4.3", - "markdown~=3.4.3", - "typogrify<3.0,>=2.0", - "sphinx<6.0,>=5.1", - "furo==2023.03.27", - "livereload<3.0,>=2.6", - "psutil<6.0,>=5.7", - "pygments~=2.15", - "pytest<8.0,>=7.1", - "pytest-cov<5.0,>=4.0", - "pytest-sugar<1.0.0,>=0.9.5", - "pytest-xdist<3.0,>=2.0", - "tox<4.0,>=3.13", - "flake8<4.0,>=3.8", - "flake8-import-order<1.0.0,>=0.18.1", - "invoke<3.0,>=2.0", - "isort<6.0,>=5.2", - "black<20.0,>=19.10b0", - "ruff>=0.1.3,<1.0.0", - "tomli;python_version<'3.11'", + "BeautifulSoup4>=4.12.2", + "jinja2>=3.1.2", + "lxml>=4.9.3", + "markdown>=3.5", + "typogrify>=2.0.7", + "sphinx>=7.1.2", + "furo>=2023.9.10", + "livereload>=2.6.3", + "psutil>=5.9.6", + "pygments>=2.16.1", + "pytest>=7.4.3", + "pytest-cov>=4.1.0", + "pytest-sugar>=0.9.7", + "pytest-xdist>=3.3.1", + "tox>=4.11.3", + "flake8>=6.1.0", + "flake8-import-order>=0.18.2", + "invoke>=2.2.0", + "isort>=5.12.0", + "black>=23.10.1", + "ruff>=0.1.3", + "tomli>=2.0.1; python_version < \"3.11\"", ] [tool.pdm.build] diff --git a/tasks.py b/tasks.py index 56461679..64409e20 100644 --- a/tasks.py +++ b/tasks.py @@ -52,24 +52,16 @@ def coverage(c): @task -def black(c, check=False, diff=False): - """Run Black auto-formatter, optionally with --check or --diff""" +def format(c, check=False, diff=False): + """Run Ruff's auto-formatter, optionally with --check or --diff""" check_flag, diff_flag = "", "" if check: check_flag = "--check" if diff: diff_flag = "--diff" - c.run(f"{VENV_BIN}/black {check_flag} {diff_flag} {PKG_PATH} tasks.py", pty=PTY) - - -@task -def isort(c, check=False, diff=False): - check_flag, diff_flag = "", "" - if check: - check_flag = "-c" - if diff: - diff_flag = "--diff" - c.run(f"{VENV_BIN}/isort {check_flag} {diff_flag} .", pty=PTY) + c.run( + f"{VENV_BIN}/ruff format {check_flag} {diff_flag} {PKG_PATH} tasks.py", pty=PTY + ) @task @@ -87,6 +79,7 @@ def ruff(c, fix=False, diff=False): def lint(c, fix=False, diff=False): """Check code style via linting tools.""" ruff(c, fix=fix, diff=diff) + format(c, check=not fix, diff=diff) @task From cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sun, 29 Oct 2023 22:18:29 +0100 Subject: [PATCH 57/88] Apply code style to project via: ruff format . --- pelican/__init__.py | 530 +++--- pelican/__main__.py | 2 +- pelican/cache.py | 52 +- pelican/contents.py | 323 ++-- pelican/generators.py | 796 +++++---- pelican/log.py | 45 +- pelican/paginator.py | 47 +- pelican/plugins/_utils.py | 36 +- pelican/plugins/signals.py | 56 +- pelican/readers.py | 416 ++--- pelican/rstdirectives.py | 60 +- pelican/server.py | 110 +- pelican/settings.py | 819 +++++---- pelican/signals.py | 4 +- pelican/tests/default_conf.py | 44 +- .../pelican/plugins/ns_plugin/__init__.py | 2 +- pelican/tests/support.py | 80 +- pelican/tests/test_cache.py | 235 ++- pelican/tests/test_cli.py | 77 +- pelican/tests/test_contents.py | 870 +++++----- pelican/tests/test_generators.py | 1500 ++++++++++------- pelican/tests/test_importer.py | 638 ++++--- pelican/tests/test_log.py | 45 +- pelican/tests/test_paginator.py | 111 +- pelican/tests/test_pelican.py | 250 +-- pelican/tests/test_plugins.py | 118 +- pelican/tests/test_readers.py | 853 +++++----- pelican/tests/test_rstdirectives.py | 12 +- pelican/tests/test_server.py | 36 +- pelican/tests/test_settings.py | 292 ++-- pelican/tests/test_testsuite.py | 3 +- pelican/tests/test_urlwrappers.py | 83 +- pelican/tests/test_utils.py | 920 +++++----- pelican/tools/pelican_import.py | 963 ++++++----- pelican/tools/pelican_quickstart.py | 367 ++-- pelican/tools/pelican_themes.py | 181 +- pelican/urlwrappers.py | 42 +- pelican/utils.py | 323 ++-- pelican/writers.py | 185 +- samples/pelican.conf.py | 52 +- samples/pelican.conf_FR.py | 54 +- 41 files changed, 6487 insertions(+), 5145 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index fcdda8a4..a0ff4989 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -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)) diff --git a/pelican/__main__.py b/pelican/__main__.py index 69a5b95d..17aead3b 100644 --- a/pelican/__main__.py +++ b/pelican/__main__.py @@ -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() diff --git a/pelican/cache.py b/pelican/cache.py index d2665691..d1f8550e 100644 --- a/pelican/cache.py +++ b/pelican/cache.py @@ -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 diff --git a/pelican/contents.py b/pelican/contents.py index c347a999..f99e6426 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -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<[^\>]+ # 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 diff --git a/pelican/generators.py b/pelican/generators.py index b9063304..0bbb7268 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -8,15 +8,27 @@ from functools import partial from itertools import chain, groupby from operator import attrgetter -from jinja2 import (BaseLoader, ChoiceLoader, Environment, FileSystemLoader, - PrefixLoader, TemplateNotFound) +from jinja2 import ( + BaseLoader, + ChoiceLoader, + Environment, + FileSystemLoader, + PrefixLoader, + TemplateNotFound, +) from pelican.cache import FileStampDataCacher from pelican.contents import Article, Page, Static from pelican.plugins import signals from pelican.readers import Readers -from pelican.utils import (DateFormatter, copy, mkdir_p, order_content, - posixize_path, process_translations) +from pelican.utils import ( + DateFormatter, + copy, + mkdir_p, + order_content, + posixize_path, + process_translations, +) logger = logging.getLogger(__name__) @@ -28,8 +40,16 @@ class PelicanTemplateNotFound(Exception): class Generator: """Baseclass generator""" - def __init__(self, context, settings, path, theme, output_path, - readers_cache_name='', **kwargs): + def __init__( + self, + context, + settings, + path, + theme, + output_path, + readers_cache_name="", + **kwargs, + ): self.context = context self.settings = settings self.path = path @@ -43,44 +63,45 @@ class Generator: # templates cache self._templates = {} - self._templates_path = list(self.settings['THEME_TEMPLATES_OVERRIDES']) + self._templates_path = list(self.settings["THEME_TEMPLATES_OVERRIDES"]) - theme_templates_path = os.path.expanduser( - os.path.join(self.theme, 'templates')) + theme_templates_path = os.path.expanduser(os.path.join(self.theme, "templates")) self._templates_path.append(theme_templates_path) theme_loader = FileSystemLoader(theme_templates_path) simple_theme_path = os.path.dirname(os.path.abspath(__file__)) simple_loader = FileSystemLoader( - os.path.join(simple_theme_path, "themes", "simple", "templates")) - - self.env = Environment( - loader=ChoiceLoader([ - FileSystemLoader(self._templates_path), - simple_loader, # implicit inheritance - PrefixLoader({ - '!simple': simple_loader, - '!theme': theme_loader - }) # explicit ones - ]), - **self.settings['JINJA_ENVIRONMENT'] + os.path.join(simple_theme_path, "themes", "simple", "templates") ) - logger.debug('Template list: %s', self.env.list_templates()) + self.env = Environment( + loader=ChoiceLoader( + [ + FileSystemLoader(self._templates_path), + simple_loader, # implicit inheritance + PrefixLoader( + {"!simple": simple_loader, "!theme": theme_loader} + ), # explicit ones + ] + ), + **self.settings["JINJA_ENVIRONMENT"], + ) + + logger.debug("Template list: %s", self.env.list_templates()) # provide utils.strftime as a jinja filter - self.env.filters.update({'strftime': DateFormatter()}) + self.env.filters.update({"strftime": DateFormatter()}) # get custom Jinja filters from user settings - custom_filters = self.settings['JINJA_FILTERS'] + custom_filters = self.settings["JINJA_FILTERS"] self.env.filters.update(custom_filters) # get custom Jinja globals from user settings - custom_globals = self.settings['JINJA_GLOBALS'] + custom_globals = self.settings["JINJA_GLOBALS"] self.env.globals.update(custom_globals) # get custom Jinja tests from user settings - custom_tests = self.settings['JINJA_TESTS'] + custom_tests = self.settings["JINJA_TESTS"] self.env.tests.update(custom_tests) signals.generator_init.send(self) @@ -91,7 +112,7 @@ class Generator: templates ready to use with Jinja2. """ if name not in self._templates: - for ext in self.settings['TEMPLATE_EXTENSIONS']: + for ext in self.settings["TEMPLATE_EXTENSIONS"]: try: self._templates[name] = self.env.get_template(name + ext) break @@ -100,9 +121,12 @@ class Generator: if name not in self._templates: raise PelicanTemplateNotFound( - '[templates] unable to load {}[{}] from {}'.format( - name, ', '.join(self.settings['TEMPLATE_EXTENSIONS']), - self._templates_path)) + "[templates] unable to load {}[{}] from {}".format( + name, + ", ".join(self.settings["TEMPLATE_EXTENSIONS"]), + self._templates_path, + ) + ) return self._templates[name] @@ -118,7 +142,7 @@ class Generator: basename = os.path.basename(path) # check IGNORE_FILES - ignores = self.settings['IGNORE_FILES'] + ignores = self.settings["IGNORE_FILES"] if any(fnmatch.fnmatch(basename, ignore) for ignore in ignores): return False @@ -147,20 +171,21 @@ class Generator: exclusions_by_dirpath.setdefault(parent_path, set()).add(subdir) files = set() - ignores = self.settings['IGNORE_FILES'] + ignores = self.settings["IGNORE_FILES"] for path in paths: # careful: os.path.join() will add a slash when path == ''. root = os.path.join(self.path, path) if path else self.path if os.path.isdir(root): for dirpath, dirs, temp_files in os.walk( - root, topdown=True, followlinks=True): + root, topdown=True, followlinks=True + ): excl = exclusions_by_dirpath.get(dirpath, ()) # We copy the `dirs` list as we will modify it in the loop: for d in list(dirs): - if (d in excl or - any(fnmatch.fnmatch(d, ignore) - for ignore in ignores)): + if d in excl or any( + fnmatch.fnmatch(d, ignore) for ignore in ignores + ): if d in dirs: dirs.remove(d) @@ -178,7 +203,7 @@ class Generator: Store a reference to its Content object, for url lookups later. """ location = content.get_relative_source_path() - key = 'static_content' if static else 'generated_content' + key = "static_content" if static else "generated_content" self.context[key][location] = content def _add_failed_source_path(self, path, static=False): @@ -186,7 +211,7 @@ class Generator: (For example, one that was missing mandatory metadata.) The path argument is expected to be relative to self.path. """ - key = 'static_content' if static else 'generated_content' + key = "static_content" if static else "generated_content" self.context[key][posixize_path(os.path.normpath(path))] = None def _is_potential_source_path(self, path, static=False): @@ -195,14 +220,14 @@ class Generator: before this method is called, even if they failed to process.) The path argument is expected to be relative to self.path. """ - key = 'static_content' if static else 'generated_content' - return (posixize_path(os.path.normpath(path)) in self.context[key]) + key = "static_content" if static else "generated_content" + return posixize_path(os.path.normpath(path)) in self.context[key] def add_static_links(self, content): """Add file links in content to context to be processed as Static content. """ - self.context['static_links'] |= content.get_static_links() + self.context["static_links"] |= content.get_static_links() def _update_context(self, items): """Update the context with the given items from the current processor. @@ -211,7 +236,7 @@ class Generator: """ for item in items: value = getattr(self, item) - if hasattr(value, 'items'): + if hasattr(value, "items"): value = list(value.items()) # py3k safeguard for iterators self.context[item] = value @@ -221,37 +246,35 @@ class Generator: class CachingGenerator(Generator, FileStampDataCacher): - '''Subclass of Generator and FileStampDataCacher classes + """Subclass of Generator and FileStampDataCacher classes enables content caching, either at the generator or reader level - ''' + """ def __init__(self, *args, **kwargs): - '''Initialize the generator, then set up caching + """Initialize the generator, then set up caching note the multiple inheritance structure - ''' + """ cls_name = self.__class__.__name__ - Generator.__init__(self, *args, - readers_cache_name=(cls_name + '-Readers'), - **kwargs) + Generator.__init__( + self, *args, readers_cache_name=(cls_name + "-Readers"), **kwargs + ) - cache_this_level = \ - self.settings['CONTENT_CACHING_LAYER'] == 'generator' - caching_policy = cache_this_level and self.settings['CACHE_CONTENT'] - load_policy = cache_this_level and self.settings['LOAD_CONTENT_CACHE'] - FileStampDataCacher.__init__(self, self.settings, cls_name, - caching_policy, load_policy - ) + cache_this_level = self.settings["CONTENT_CACHING_LAYER"] == "generator" + caching_policy = cache_this_level and self.settings["CACHE_CONTENT"] + load_policy = cache_this_level and self.settings["LOAD_CONTENT_CACHE"] + FileStampDataCacher.__init__( + self, self.settings, cls_name, caching_policy, load_policy + ) def _get_file_stamp(self, filename): - '''Get filestamp for path relative to generator.path''' + """Get filestamp for path relative to generator.path""" filename = os.path.join(self.path, filename) return super()._get_file_stamp(filename) class _FileLoader(BaseLoader): - def __init__(self, path, basedir): self.path = path self.fullpath = os.path.join(basedir, path) @@ -260,22 +283,21 @@ class _FileLoader(BaseLoader): if template != self.path or not os.path.exists(self.fullpath): raise TemplateNotFound(template) mtime = os.path.getmtime(self.fullpath) - with open(self.fullpath, encoding='utf-8') as f: + with open(self.fullpath, encoding="utf-8") as f: source = f.read() - return (source, self.fullpath, - lambda: mtime == os.path.getmtime(self.fullpath)) + return (source, self.fullpath, lambda: mtime == os.path.getmtime(self.fullpath)) class TemplatePagesGenerator(Generator): - def generate_output(self, writer): - for source, dest in self.settings['TEMPLATE_PAGES'].items(): + for source, dest in self.settings["TEMPLATE_PAGES"].items(): self.env.loader.loaders.insert(0, _FileLoader(source, self.path)) try: template = self.env.get_template(source) - rurls = self.settings['RELATIVE_URLS'] - writer.write_file(dest, template, self.context, rurls, - override_output=True, url='') + rurls = self.settings["RELATIVE_URLS"] + writer.write_file( + dest, template, self.context, rurls, override_output=True, url="" + ) finally: del self.env.loader.loaders[0] @@ -286,13 +308,13 @@ class ArticlesGenerator(CachingGenerator): def __init__(self, *args, **kwargs): """initialize properties""" # Published, listed articles - self.articles = [] # only articles in default language + self.articles = [] # only articles in default language self.translations = [] # Published, unlisted articles self.hidden_articles = [] self.hidden_translations = [] # Draft articles - self.drafts = [] # only drafts in default language + self.drafts = [] # only drafts in default language self.drafts_translations = [] self.dates = {} self.period_archives = defaultdict(list) @@ -306,263 +328,304 @@ class ArticlesGenerator(CachingGenerator): def generate_feeds(self, writer): """Generate the feeds from the current context, and output files.""" - if self.settings.get('FEED_ATOM'): + if self.settings.get("FEED_ATOM"): writer.write_feed( self.articles, self.context, - self.settings['FEED_ATOM'], - self.settings.get('FEED_ATOM_URL', self.settings['FEED_ATOM']) - ) + self.settings["FEED_ATOM"], + self.settings.get("FEED_ATOM_URL", self.settings["FEED_ATOM"]), + ) - if self.settings.get('FEED_RSS'): + if self.settings.get("FEED_RSS"): writer.write_feed( self.articles, self.context, - self.settings['FEED_RSS'], - self.settings.get('FEED_RSS_URL', self.settings['FEED_RSS']), - feed_type='rss' - ) + self.settings["FEED_RSS"], + self.settings.get("FEED_RSS_URL", self.settings["FEED_RSS"]), + feed_type="rss", + ) - if (self.settings.get('FEED_ALL_ATOM') or - self.settings.get('FEED_ALL_RSS')): + if self.settings.get("FEED_ALL_ATOM") or self.settings.get("FEED_ALL_RSS"): all_articles = list(self.articles) for article in self.articles: all_articles.extend(article.translations) - order_content( - all_articles, order_by=self.settings['ARTICLE_ORDER_BY'] - ) + order_content(all_articles, order_by=self.settings["ARTICLE_ORDER_BY"]) - if self.settings.get('FEED_ALL_ATOM'): + if self.settings.get("FEED_ALL_ATOM"): writer.write_feed( all_articles, self.context, - self.settings['FEED_ALL_ATOM'], - self.settings.get('FEED_ALL_ATOM_URL', - self.settings['FEED_ALL_ATOM']) - ) + self.settings["FEED_ALL_ATOM"], + self.settings.get( + "FEED_ALL_ATOM_URL", self.settings["FEED_ALL_ATOM"] + ), + ) - if self.settings.get('FEED_ALL_RSS'): + if self.settings.get("FEED_ALL_RSS"): writer.write_feed( all_articles, self.context, - self.settings['FEED_ALL_RSS'], - self.settings.get('FEED_ALL_RSS_URL', - self.settings['FEED_ALL_RSS']), - feed_type='rss' - ) + self.settings["FEED_ALL_RSS"], + self.settings.get( + "FEED_ALL_RSS_URL", self.settings["FEED_ALL_RSS"] + ), + feed_type="rss", + ) for cat, arts in self.categories: - if self.settings.get('CATEGORY_FEED_ATOM'): + if self.settings.get("CATEGORY_FEED_ATOM"): writer.write_feed( arts, self.context, - str(self.settings['CATEGORY_FEED_ATOM']).format(slug=cat.slug), + str(self.settings["CATEGORY_FEED_ATOM"]).format(slug=cat.slug), self.settings.get( - 'CATEGORY_FEED_ATOM_URL', - str(self.settings['CATEGORY_FEED_ATOM']).format( - slug=cat.slug - )), - feed_title=cat.name - ) - - if self.settings.get('CATEGORY_FEED_RSS'): - writer.write_feed( - arts, - self.context, - str(self.settings['CATEGORY_FEED_RSS']).format(slug=cat.slug), - self.settings.get( - 'CATEGORY_FEED_RSS_URL', - str(self.settings['CATEGORY_FEED_RSS']).format( - slug=cat.slug - )), + "CATEGORY_FEED_ATOM_URL", + str(self.settings["CATEGORY_FEED_ATOM"]).format(slug=cat.slug), + ), feed_title=cat.name, - feed_type='rss' - ) + ) + + if self.settings.get("CATEGORY_FEED_RSS"): + writer.write_feed( + arts, + self.context, + str(self.settings["CATEGORY_FEED_RSS"]).format(slug=cat.slug), + self.settings.get( + "CATEGORY_FEED_RSS_URL", + str(self.settings["CATEGORY_FEED_RSS"]).format(slug=cat.slug), + ), + feed_title=cat.name, + feed_type="rss", + ) for auth, arts in self.authors: - if self.settings.get('AUTHOR_FEED_ATOM'): + if self.settings.get("AUTHOR_FEED_ATOM"): writer.write_feed( arts, self.context, - str(self.settings['AUTHOR_FEED_ATOM']).format(slug=auth.slug), + str(self.settings["AUTHOR_FEED_ATOM"]).format(slug=auth.slug), self.settings.get( - 'AUTHOR_FEED_ATOM_URL', - str(self.settings['AUTHOR_FEED_ATOM']).format( - slug=auth.slug - )), - feed_title=auth.name - ) - - if self.settings.get('AUTHOR_FEED_RSS'): - writer.write_feed( - arts, - self.context, - str(self.settings['AUTHOR_FEED_RSS']).format(slug=auth.slug), - self.settings.get( - 'AUTHOR_FEED_RSS_URL', - str(self.settings['AUTHOR_FEED_RSS']).format( - slug=auth.slug - )), + "AUTHOR_FEED_ATOM_URL", + str(self.settings["AUTHOR_FEED_ATOM"]).format(slug=auth.slug), + ), feed_title=auth.name, - feed_type='rss' + ) + + if self.settings.get("AUTHOR_FEED_RSS"): + writer.write_feed( + arts, + self.context, + str(self.settings["AUTHOR_FEED_RSS"]).format(slug=auth.slug), + self.settings.get( + "AUTHOR_FEED_RSS_URL", + str(self.settings["AUTHOR_FEED_RSS"]).format(slug=auth.slug), + ), + feed_title=auth.name, + feed_type="rss", + ) + + if self.settings.get("TAG_FEED_ATOM") or self.settings.get("TAG_FEED_RSS"): + for tag, arts in self.tags.items(): + if self.settings.get("TAG_FEED_ATOM"): + writer.write_feed( + arts, + self.context, + str(self.settings["TAG_FEED_ATOM"]).format(slug=tag.slug), + self.settings.get( + "TAG_FEED_ATOM_URL", + str(self.settings["TAG_FEED_ATOM"]).format(slug=tag.slug), + ), + feed_title=tag.name, ) - if (self.settings.get('TAG_FEED_ATOM') or - self.settings.get('TAG_FEED_RSS')): - for tag, arts in self.tags.items(): - if self.settings.get('TAG_FEED_ATOM'): + if self.settings.get("TAG_FEED_RSS"): writer.write_feed( arts, self.context, - str(self.settings['TAG_FEED_ATOM']).format(slug=tag.slug), + str(self.settings["TAG_FEED_RSS"]).format(slug=tag.slug), self.settings.get( - 'TAG_FEED_ATOM_URL', - str(self.settings['TAG_FEED_ATOM']).format( - slug=tag.slug - )), - feed_title=tag.name - ) - - if self.settings.get('TAG_FEED_RSS'): - writer.write_feed( - arts, - self.context, - str(self.settings['TAG_FEED_RSS']).format(slug=tag.slug), - self.settings.get( - 'TAG_FEED_RSS_URL', - str(self.settings['TAG_FEED_RSS']).format( - slug=tag.slug - )), + "TAG_FEED_RSS_URL", + str(self.settings["TAG_FEED_RSS"]).format(slug=tag.slug), + ), feed_title=tag.name, - feed_type='rss' - ) + feed_type="rss", + ) - if (self.settings.get('TRANSLATION_FEED_ATOM') or - self.settings.get('TRANSLATION_FEED_RSS')): + if self.settings.get("TRANSLATION_FEED_ATOM") or self.settings.get( + "TRANSLATION_FEED_RSS" + ): translations_feeds = defaultdict(list) for article in chain(self.articles, self.translations): translations_feeds[article.lang].append(article) for lang, items in translations_feeds.items(): - items = order_content( - items, order_by=self.settings['ARTICLE_ORDER_BY']) - if self.settings.get('TRANSLATION_FEED_ATOM'): + items = order_content(items, order_by=self.settings["ARTICLE_ORDER_BY"]) + if self.settings.get("TRANSLATION_FEED_ATOM"): writer.write_feed( items, self.context, - str( - self.settings['TRANSLATION_FEED_ATOM'] - ).format(lang=lang), + str(self.settings["TRANSLATION_FEED_ATOM"]).format(lang=lang), self.settings.get( - 'TRANSLATION_FEED_ATOM_URL', - str( - self.settings['TRANSLATION_FEED_ATOM'] - ).format(lang=lang), - ) - ) - if self.settings.get('TRANSLATION_FEED_RSS'): - writer.write_feed( - items, - self.context, - str( - self.settings['TRANSLATION_FEED_RSS'] - ).format(lang=lang), - self.settings.get( - 'TRANSLATION_FEED_RSS_URL', - str(self.settings['TRANSLATION_FEED_RSS'])).format( + "TRANSLATION_FEED_ATOM_URL", + str(self.settings["TRANSLATION_FEED_ATOM"]).format( lang=lang ), - feed_type='rss' + ), + ) + if self.settings.get("TRANSLATION_FEED_RSS"): + writer.write_feed( + items, + self.context, + str(self.settings["TRANSLATION_FEED_RSS"]).format(lang=lang), + self.settings.get( + "TRANSLATION_FEED_RSS_URL", + str(self.settings["TRANSLATION_FEED_RSS"]), + ).format(lang=lang), + feed_type="rss", ) def generate_articles(self, write): """Generate the articles.""" for article in chain( - self.translations, self.articles, - self.hidden_translations, self.hidden_articles + self.translations, + self.articles, + self.hidden_translations, + self.hidden_articles, ): signals.article_generator_write_article.send(self, content=article) - write(article.save_as, self.get_template(article.template), - self.context, article=article, category=article.category, - override_output=hasattr(article, 'override_save_as'), - url=article.url, blog=True) + write( + article.save_as, + self.get_template(article.template), + self.context, + article=article, + category=article.category, + override_output=hasattr(article, "override_save_as"), + url=article.url, + blog=True, + ) def generate_period_archives(self, write): """Generate per-year, per-month, and per-day archives.""" try: - template = self.get_template('period_archives') + template = self.get_template("period_archives") except PelicanTemplateNotFound: - template = self.get_template('archives') + template = self.get_template("archives") for granularity in self.period_archives: for period in self.period_archives[granularity]: - context = self.context.copy() - context['period'] = period['period'] - context['period_num'] = period['period_num'] + context["period"] = period["period"] + context["period_num"] = period["period_num"] - write(period['save_as'], template, context, - articles=period['articles'], dates=period['dates'], - template_name='period_archives', blog=True, - url=period['url'], all_articles=self.articles) + write( + period["save_as"], + template, + context, + articles=period["articles"], + dates=period["dates"], + template_name="period_archives", + blog=True, + url=period["url"], + all_articles=self.articles, + ) def generate_direct_templates(self, write): """Generate direct templates pages""" - for template in self.settings['DIRECT_TEMPLATES']: - save_as = self.settings.get("%s_SAVE_AS" % template.upper(), - '%s.html' % template) - url = self.settings.get("%s_URL" % template.upper(), - '%s.html' % template) + for template in self.settings["DIRECT_TEMPLATES"]: + save_as = self.settings.get( + "%s_SAVE_AS" % template.upper(), "%s.html" % template + ) + url = self.settings.get("%s_URL" % template.upper(), "%s.html" % template) if not save_as: continue - write(save_as, self.get_template(template), self.context, - articles=self.articles, dates=self.dates, blog=True, - template_name=template, - page_name=os.path.splitext(save_as)[0], url=url) + write( + save_as, + self.get_template(template), + self.context, + articles=self.articles, + dates=self.dates, + blog=True, + template_name=template, + page_name=os.path.splitext(save_as)[0], + url=url, + ) def generate_tags(self, write): """Generate Tags pages.""" - tag_template = self.get_template('tag') + tag_template = self.get_template("tag") for tag, articles in self.tags.items(): dates = [article for article in self.dates if article in articles] - write(tag.save_as, tag_template, self.context, tag=tag, - url=tag.url, articles=articles, dates=dates, - template_name='tag', blog=True, page_name=tag.page_name, - all_articles=self.articles) + write( + tag.save_as, + tag_template, + self.context, + tag=tag, + url=tag.url, + articles=articles, + dates=dates, + template_name="tag", + blog=True, + page_name=tag.page_name, + all_articles=self.articles, + ) def generate_categories(self, write): """Generate category pages.""" - category_template = self.get_template('category') + category_template = self.get_template("category") for cat, articles in self.categories: dates = [article for article in self.dates if article in articles] - write(cat.save_as, category_template, self.context, url=cat.url, - category=cat, articles=articles, dates=dates, - template_name='category', blog=True, page_name=cat.page_name, - all_articles=self.articles) + write( + cat.save_as, + category_template, + self.context, + url=cat.url, + category=cat, + articles=articles, + dates=dates, + template_name="category", + blog=True, + page_name=cat.page_name, + all_articles=self.articles, + ) def generate_authors(self, write): """Generate Author pages.""" - author_template = self.get_template('author') + author_template = self.get_template("author") for aut, articles in self.authors: dates = [article for article in self.dates if article in articles] - write(aut.save_as, author_template, self.context, - url=aut.url, author=aut, articles=articles, dates=dates, - template_name='author', blog=True, - page_name=aut.page_name, all_articles=self.articles) + write( + aut.save_as, + author_template, + self.context, + url=aut.url, + author=aut, + articles=articles, + dates=dates, + template_name="author", + blog=True, + page_name=aut.page_name, + all_articles=self.articles, + ) def generate_drafts(self, write): """Generate drafts pages.""" for draft in chain(self.drafts_translations, self.drafts): - write(draft.save_as, self.get_template(draft.template), - self.context, article=draft, category=draft.category, - override_output=hasattr(draft, 'override_save_as'), - blog=True, all_articles=self.articles, url=draft.url) + write( + draft.save_as, + self.get_template(draft.template), + self.context, + article=draft, + category=draft.category, + override_output=hasattr(draft, "override_save_as"), + blog=True, + all_articles=self.articles, + url=draft.url, + ) def generate_pages(self, writer): """Generate the pages on the disk""" - write = partial(writer.write_file, - relative_urls=self.settings['RELATIVE_URLS']) + write = partial(writer.write_file, relative_urls=self.settings["RELATIVE_URLS"]) # to minimize the number of relative path stuff modification # in writer, articles pass first @@ -583,22 +646,28 @@ class ArticlesGenerator(CachingGenerator): all_drafts = [] hidden_articles = [] for f in self.get_files( - self.settings['ARTICLE_PATHS'], - exclude=self.settings['ARTICLE_EXCLUDES']): + self.settings["ARTICLE_PATHS"], exclude=self.settings["ARTICLE_EXCLUDES"] + ): article = self.get_cached_data(f, None) if article is None: try: article = self.readers.read_file( - base_path=self.path, path=f, content_class=Article, + base_path=self.path, + path=f, + content_class=Article, context=self.context, preread_signal=signals.article_generator_preread, preread_sender=self, context_signal=signals.article_generator_context, - context_sender=self) + context_sender=self, + ) except Exception as e: logger.error( - 'Could not process %s\n%s', f, e, - exc_info=self.settings.get('DEBUG', False)) + "Could not process %s\n%s", + f, + e, + exc_info=self.settings.get("DEBUG", False), + ) self._add_failed_source_path(f) continue @@ -620,8 +689,9 @@ class ArticlesGenerator(CachingGenerator): def _process(arts): origs, translations = process_translations( - arts, translation_id=self.settings['ARTICLE_TRANSLATION_ID']) - origs = order_content(origs, self.settings['ARTICLE_ORDER_BY']) + arts, translation_id=self.settings["ARTICLE_TRANSLATION_ID"] + ) + origs = order_content(origs, self.settings["ARTICLE_ORDER_BY"]) return origs, translations self.articles, self.translations = _process(all_articles) @@ -634,36 +704,45 @@ class ArticlesGenerator(CachingGenerator): # only main articles are listed in categories and tags # not translations or hidden articles self.categories[article.category].append(article) - if hasattr(article, 'tags'): + if hasattr(article, "tags"): for tag in article.tags: self.tags[tag].append(article) - for author in getattr(article, 'authors', []): + for author in getattr(article, "authors", []): self.authors[author].append(article) self.dates = list(self.articles) - self.dates.sort(key=attrgetter('date'), - reverse=self.context['NEWEST_FIRST_ARCHIVES']) + self.dates.sort( + key=attrgetter("date"), reverse=self.context["NEWEST_FIRST_ARCHIVES"] + ) self.period_archives = self._build_period_archives( - self.dates, self.articles, self.settings) + self.dates, self.articles, self.settings + ) # and generate the output :) # order the categories per name self.categories = list(self.categories.items()) - self.categories.sort( - reverse=self.settings['REVERSE_CATEGORY_ORDER']) + self.categories.sort(reverse=self.settings["REVERSE_CATEGORY_ORDER"]) self.authors = list(self.authors.items()) self.authors.sort() - self._update_context(( - 'articles', 'drafts', 'hidden_articles', - 'dates', 'tags', 'categories', - 'authors', 'related_posts')) + self._update_context( + ( + "articles", + "drafts", + "hidden_articles", + "dates", + "tags", + "categories", + "authors", + "related_posts", + ) + ) # _update_context flattens dicts, which should not happen to # period_archives, so we update the context directly for it: - self.context['period_archives'] = self.period_archives + self.context["period_archives"] = self.period_archives self.save_cache() self.readers.save_cache() signals.article_generator_finalized.send(self) @@ -677,29 +756,29 @@ class ArticlesGenerator(CachingGenerator): period_archives = defaultdict(list) period_archives_settings = { - 'year': { - 'save_as': settings['YEAR_ARCHIVE_SAVE_AS'], - 'url': settings['YEAR_ARCHIVE_URL'], + "year": { + "save_as": settings["YEAR_ARCHIVE_SAVE_AS"], + "url": settings["YEAR_ARCHIVE_URL"], }, - 'month': { - 'save_as': settings['MONTH_ARCHIVE_SAVE_AS'], - 'url': settings['MONTH_ARCHIVE_URL'], + "month": { + "save_as": settings["MONTH_ARCHIVE_SAVE_AS"], + "url": settings["MONTH_ARCHIVE_URL"], }, - 'day': { - 'save_as': settings['DAY_ARCHIVE_SAVE_AS'], - 'url': settings['DAY_ARCHIVE_URL'], + "day": { + "save_as": settings["DAY_ARCHIVE_SAVE_AS"], + "url": settings["DAY_ARCHIVE_URL"], }, } granularity_key_func = { - 'year': attrgetter('date.year'), - 'month': attrgetter('date.year', 'date.month'), - 'day': attrgetter('date.year', 'date.month', 'date.day'), + "year": attrgetter("date.year"), + "month": attrgetter("date.year", "date.month"), + "day": attrgetter("date.year", "date.month", "date.day"), } - for granularity in 'year', 'month', 'day': - save_as_fmt = period_archives_settings[granularity]['save_as'] - url_fmt = period_archives_settings[granularity]['url'] + for granularity in "year", "month", "day": + save_as_fmt = period_archives_settings[granularity]["save_as"] + url_fmt = period_archives_settings[granularity]["url"] key_func = granularity_key_func[granularity] if not save_as_fmt: @@ -710,26 +789,26 @@ class ArticlesGenerator(CachingGenerator): archive = {} dates = list(group) - archive['dates'] = dates - archive['articles'] = [a for a in articles if a in dates] + archive["dates"] = dates + archive["articles"] = [a for a in articles if a in dates] # use the first date to specify the period archive URL # and save_as; the specific date used does not matter as # they all belong to the same period d = dates[0].date - archive['save_as'] = save_as_fmt.format(date=d) - archive['url'] = url_fmt.format(date=d) + archive["save_as"] = save_as_fmt.format(date=d) + archive["url"] = url_fmt.format(date=d) - if granularity == 'year': - archive['period'] = (period,) - archive['period_num'] = (period,) + if granularity == "year": + archive["period"] = (period,) + archive["period_num"] = (period,) else: month_name = calendar.month_name[period[1]] - if granularity == 'month': - archive['period'] = (period[0], month_name) + if granularity == "month": + archive["period"] = (period[0], month_name) else: - archive['period'] = (period[0], month_name, period[2]) - archive['period_num'] = tuple(period) + archive["period"] = (period[0], month_name, period[2]) + archive["period_num"] = tuple(period) period_archives[granularity].append(archive) @@ -741,13 +820,15 @@ class ArticlesGenerator(CachingGenerator): signals.article_writer_finalized.send(self, writer=writer) def refresh_metadata_intersite_links(self): - for e in chain(self.articles, - self.translations, - self.drafts, - self.drafts_translations, - self.hidden_articles, - self.hidden_translations): - if hasattr(e, 'refresh_metadata_intersite_links'): + for e in chain( + self.articles, + self.translations, + self.drafts, + self.drafts_translations, + self.hidden_articles, + self.hidden_translations, + ): + if hasattr(e, "refresh_metadata_intersite_links"): e.refresh_metadata_intersite_links() @@ -769,22 +850,28 @@ class PagesGenerator(CachingGenerator): hidden_pages = [] draft_pages = [] for f in self.get_files( - self.settings['PAGE_PATHS'], - exclude=self.settings['PAGE_EXCLUDES']): + self.settings["PAGE_PATHS"], exclude=self.settings["PAGE_EXCLUDES"] + ): page = self.get_cached_data(f, None) if page is None: try: page = self.readers.read_file( - base_path=self.path, path=f, content_class=Page, + base_path=self.path, + path=f, + content_class=Page, context=self.context, preread_signal=signals.page_generator_preread, preread_sender=self, context_signal=signals.page_generator_context, - context_sender=self) + context_sender=self, + ) except Exception as e: logger.error( - 'Could not process %s\n%s', f, e, - exc_info=self.settings.get('DEBUG', False)) + "Could not process %s\n%s", + f, + e, + exc_info=self.settings.get("DEBUG", False), + ) self._add_failed_source_path(f) continue @@ -805,40 +892,51 @@ class PagesGenerator(CachingGenerator): def _process(pages): origs, translations = process_translations( - pages, translation_id=self.settings['PAGE_TRANSLATION_ID']) - origs = order_content(origs, self.settings['PAGE_ORDER_BY']) + pages, translation_id=self.settings["PAGE_TRANSLATION_ID"] + ) + origs = order_content(origs, self.settings["PAGE_ORDER_BY"]) return origs, translations self.pages, self.translations = _process(all_pages) self.hidden_pages, self.hidden_translations = _process(hidden_pages) self.draft_pages, self.draft_translations = _process(draft_pages) - self._update_context(('pages', 'hidden_pages', 'draft_pages')) + self._update_context(("pages", "hidden_pages", "draft_pages")) self.save_cache() self.readers.save_cache() signals.page_generator_finalized.send(self) def generate_output(self, writer): - for page in chain(self.translations, self.pages, - self.hidden_translations, self.hidden_pages, - self.draft_translations, self.draft_pages): + for page in chain( + self.translations, + self.pages, + self.hidden_translations, + self.hidden_pages, + self.draft_translations, + self.draft_pages, + ): signals.page_generator_write_page.send(self, content=page) writer.write_file( - page.save_as, self.get_template(page.template), - self.context, page=page, - relative_urls=self.settings['RELATIVE_URLS'], - override_output=hasattr(page, 'override_save_as'), - url=page.url) + page.save_as, + self.get_template(page.template), + self.context, + page=page, + relative_urls=self.settings["RELATIVE_URLS"], + override_output=hasattr(page, "override_save_as"), + url=page.url, + ) signals.page_writer_finalized.send(self, writer=writer) def refresh_metadata_intersite_links(self): - for e in chain(self.pages, - self.hidden_pages, - self.hidden_translations, - self.draft_pages, - self.draft_translations): - if hasattr(e, 'refresh_metadata_intersite_links'): + for e in chain( + self.pages, + self.hidden_pages, + self.hidden_translations, + self.draft_pages, + self.draft_translations, + ): + if hasattr(e, "refresh_metadata_intersite_links"): e.refresh_metadata_intersite_links() @@ -853,71 +951,82 @@ class StaticGenerator(Generator): def generate_context(self): self.staticfiles = [] - linked_files = set(self.context['static_links']) - found_files = self.get_files(self.settings['STATIC_PATHS'], - exclude=self.settings['STATIC_EXCLUDES'], - extensions=False) + linked_files = set(self.context["static_links"]) + found_files = self.get_files( + self.settings["STATIC_PATHS"], + exclude=self.settings["STATIC_EXCLUDES"], + extensions=False, + ) for f in linked_files | found_files: - # skip content source files unless the user explicitly wants them - if self.settings['STATIC_EXCLUDE_SOURCES']: + if self.settings["STATIC_EXCLUDE_SOURCES"]: if self._is_potential_source_path(f): continue static = self.readers.read_file( - base_path=self.path, path=f, content_class=Static, - fmt='static', context=self.context, + base_path=self.path, + path=f, + content_class=Static, + fmt="static", + context=self.context, preread_signal=signals.static_generator_preread, preread_sender=self, context_signal=signals.static_generator_context, - context_sender=self) + context_sender=self, + ) self.staticfiles.append(static) self.add_source_path(static, static=True) - self._update_context(('staticfiles',)) + self._update_context(("staticfiles",)) signals.static_generator_finalized.send(self) def generate_output(self, writer): - self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme, - self.settings['THEME_STATIC_DIR'], self.output_path, - os.curdir) - for sc in self.context['staticfiles']: + self._copy_paths( + self.settings["THEME_STATIC_PATHS"], + self.theme, + self.settings["THEME_STATIC_DIR"], + self.output_path, + os.curdir, + ) + for sc in self.context["staticfiles"]: if self._file_update_required(sc): self._link_or_copy_staticfile(sc) else: - logger.debug('%s is up to date, not copying', sc.source_path) + logger.debug("%s is up to date, not copying", sc.source_path) - def _copy_paths(self, paths, source, destination, output_path, - final_path=None): + def _copy_paths(self, paths, source, destination, output_path, final_path=None): """Copy all the paths from source to destination""" for path in paths: source_path = os.path.join(source, path) if final_path: if os.path.isfile(source_path): - destination_path = os.path.join(output_path, destination, - final_path, - os.path.basename(path)) + destination_path = os.path.join( + output_path, destination, final_path, os.path.basename(path) + ) else: - destination_path = os.path.join(output_path, destination, - final_path) + destination_path = os.path.join( + output_path, destination, final_path + ) else: destination_path = os.path.join(output_path, destination, path) - copy(source_path, destination_path, - self.settings['IGNORE_FILES']) + copy(source_path, destination_path, self.settings["IGNORE_FILES"]) def _file_update_required(self, staticfile): source_path = os.path.join(self.path, staticfile.source_path) save_as = os.path.join(self.output_path, staticfile.save_as) if not os.path.exists(save_as): return True - elif (self.settings['STATIC_CREATE_LINKS'] and - os.path.samefile(source_path, save_as)): + elif self.settings["STATIC_CREATE_LINKS"] and os.path.samefile( + source_path, save_as + ): return False - elif (self.settings['STATIC_CREATE_LINKS'] and - os.path.realpath(save_as) == source_path): + elif ( + self.settings["STATIC_CREATE_LINKS"] + and os.path.realpath(save_as) == source_path + ): return False - elif not self.settings['STATIC_CHECK_IF_MODIFIED']: + elif not self.settings["STATIC_CHECK_IF_MODIFIED"]: return True else: return self._source_is_newer(staticfile) @@ -930,7 +1039,7 @@ class StaticGenerator(Generator): return s_mtime - d_mtime > 0.000001 def _link_or_copy_staticfile(self, sc): - if self.settings['STATIC_CREATE_LINKS']: + if self.settings["STATIC_CREATE_LINKS"]: self._link_staticfile(sc) else: self._copy_staticfile(sc) @@ -940,7 +1049,7 @@ class StaticGenerator(Generator): save_as = os.path.join(self.output_path, sc.save_as) self._mkdir(os.path.dirname(save_as)) copy(source_path, save_as) - logger.info('Copying %s to %s', sc.source_path, sc.save_as) + logger.info("Copying %s to %s", sc.source_path, sc.save_as) def _link_staticfile(self, sc): source_path = os.path.join(self.path, sc.source_path) @@ -949,7 +1058,7 @@ class StaticGenerator(Generator): try: if os.path.lexists(save_as): os.unlink(save_as) - logger.info('Linking %s and %s', sc.source_path, sc.save_as) + logger.info("Linking %s and %s", sc.source_path, sc.save_as) if self.fallback_to_symlinks: os.symlink(source_path, save_as) else: @@ -957,9 +1066,8 @@ class StaticGenerator(Generator): except OSError as err: if err.errno == errno.EXDEV: # 18: Invalid cross-device link logger.debug( - "Cross-device links not valid. " - "Creating symbolic links instead." - ) + "Cross-device links not valid. " "Creating symbolic links instead." + ) self.fallback_to_symlinks = True self._link_staticfile(sc) else: @@ -972,19 +1080,17 @@ class StaticGenerator(Generator): class SourceFileGenerator(Generator): - def generate_context(self): - self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION'] + self.output_extension = self.settings["OUTPUT_SOURCES_EXTENSION"] def _create_source(self, obj): output_path, _ = os.path.splitext(obj.save_as) - dest = os.path.join(self.output_path, - output_path + self.output_extension) + dest = os.path.join(self.output_path, output_path + self.output_extension) copy(obj.source_path, dest) def generate_output(self, writer=None): - logger.info('Generating source files...') - for obj in chain(self.context['articles'], self.context['pages']): + logger.info("Generating source files...") + for obj in chain(self.context["articles"], self.context["pages"]): self._create_source(obj) for obj_trans in obj.translations: self._create_source(obj_trans) diff --git a/pelican/log.py b/pelican/log.py index be176ea8..0d2b6a3f 100644 --- a/pelican/log.py +++ b/pelican/log.py @@ -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") diff --git a/pelican/paginator.py b/pelican/paginator.py index 4231e67b..930c915b 100644 --- a/pelican/paginator.py +++ b/pelican/paginator.py @@ -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 ''.format(self.number, self.paginator.num_pages) + return "".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")) diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py index 87877b08..f0c18f5c 100644 --- a/pelican/plugins/_utils.py +++ b/pelican/plugins/_utils.py @@ -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: diff --git a/pelican/plugins/signals.py b/pelican/plugins/signals.py index 4013360f..ff129cb4 100644 --- a/pelican/plugins/signals.py +++ b/pelican/plugins/signals.py @@ -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") diff --git a/pelican/readers.py b/pelican/readers.py index 03c3cc20..5033c0bd 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -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('') + self.body.append("") 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, )', - context_signal.name, - context_sender) + "Signal %s.send(%s, )", 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 ]* 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.*)', subdir)) + if settings.get("USE_FOLDER_AS_CATEGORY", None): + checks.append(("(?P.*)", subdir)) for regexp, data in checks: if regexp and data: match = re.match(regexp, data) diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py index 500c8578..0a549424 100644 --- a/pelican/rstdirectives.py +++ b/pelican/rstdirectives.py @@ -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) diff --git a/pelican/server.py b/pelican/server.py index 913c3761..61729bf1 100644 --- a/pelican/server.py +++ b/pelican/server.py @@ -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: diff --git a/pelican/settings.py b/pelican/settings.py index 9a54b2a6..2c84b6f0 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -22,150 +22,157 @@ def load_source(name, path): logger = logging.getLogger(__name__) -DEFAULT_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)), - 'themes', 'notmyidea') +DEFAULT_THEME = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "themes", "notmyidea" +) DEFAULT_CONFIG = { - 'PATH': os.curdir, - 'ARTICLE_PATHS': [''], - 'ARTICLE_EXCLUDES': [], - 'PAGE_PATHS': ['pages'], - 'PAGE_EXCLUDES': [], - 'THEME': DEFAULT_THEME, - 'OUTPUT_PATH': 'output', - 'READERS': {}, - 'STATIC_PATHS': ['images'], - 'STATIC_EXCLUDES': [], - 'STATIC_EXCLUDE_SOURCES': True, - 'THEME_STATIC_DIR': 'theme', - 'THEME_STATIC_PATHS': ['static', ], - 'FEED_ALL_ATOM': 'feeds/all.atom.xml', - 'CATEGORY_FEED_ATOM': 'feeds/{slug}.atom.xml', - 'AUTHOR_FEED_ATOM': 'feeds/{slug}.atom.xml', - 'AUTHOR_FEED_RSS': 'feeds/{slug}.rss.xml', - 'TRANSLATION_FEED_ATOM': 'feeds/all-{lang}.atom.xml', - 'FEED_MAX_ITEMS': 100, - 'RSS_FEED_SUMMARY_ONLY': True, - 'SITEURL': '', - 'SITENAME': 'A Pelican Blog', - 'DISPLAY_PAGES_ON_MENU': True, - 'DISPLAY_CATEGORIES_ON_MENU': True, - 'DOCUTILS_SETTINGS': {}, - 'OUTPUT_SOURCES': False, - 'OUTPUT_SOURCES_EXTENSION': '.text', - 'USE_FOLDER_AS_CATEGORY': True, - 'DEFAULT_CATEGORY': 'misc', - 'WITH_FUTURE_DATES': True, - 'CSS_FILE': 'main.css', - 'NEWEST_FIRST_ARCHIVES': True, - 'REVERSE_CATEGORY_ORDER': False, - 'DELETE_OUTPUT_DIRECTORY': False, - 'OUTPUT_RETENTION': [], - 'INDEX_SAVE_AS': 'index.html', - 'ARTICLE_URL': '{slug}.html', - 'ARTICLE_SAVE_AS': '{slug}.html', - 'ARTICLE_ORDER_BY': 'reversed-date', - 'ARTICLE_LANG_URL': '{slug}-{lang}.html', - 'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html', - 'DRAFT_URL': 'drafts/{slug}.html', - 'DRAFT_SAVE_AS': 'drafts/{slug}.html', - 'DRAFT_LANG_URL': 'drafts/{slug}-{lang}.html', - 'DRAFT_LANG_SAVE_AS': 'drafts/{slug}-{lang}.html', - 'PAGE_URL': 'pages/{slug}.html', - 'PAGE_SAVE_AS': 'pages/{slug}.html', - 'PAGE_ORDER_BY': 'basename', - 'PAGE_LANG_URL': 'pages/{slug}-{lang}.html', - 'PAGE_LANG_SAVE_AS': 'pages/{slug}-{lang}.html', - 'DRAFT_PAGE_URL': 'drafts/pages/{slug}.html', - 'DRAFT_PAGE_SAVE_AS': 'drafts/pages/{slug}.html', - 'DRAFT_PAGE_LANG_URL': 'drafts/pages/{slug}-{lang}.html', - 'DRAFT_PAGE_LANG_SAVE_AS': 'drafts/pages/{slug}-{lang}.html', - 'STATIC_URL': '{path}', - 'STATIC_SAVE_AS': '{path}', - 'STATIC_CREATE_LINKS': False, - 'STATIC_CHECK_IF_MODIFIED': False, - 'CATEGORY_URL': 'category/{slug}.html', - 'CATEGORY_SAVE_AS': 'category/{slug}.html', - 'TAG_URL': 'tag/{slug}.html', - 'TAG_SAVE_AS': 'tag/{slug}.html', - 'AUTHOR_URL': 'author/{slug}.html', - 'AUTHOR_SAVE_AS': 'author/{slug}.html', - 'PAGINATION_PATTERNS': [ - (1, '{name}{extension}', '{name}{extension}'), - (2, '{name}{number}{extension}', '{name}{number}{extension}'), + "PATH": os.curdir, + "ARTICLE_PATHS": [""], + "ARTICLE_EXCLUDES": [], + "PAGE_PATHS": ["pages"], + "PAGE_EXCLUDES": [], + "THEME": DEFAULT_THEME, + "OUTPUT_PATH": "output", + "READERS": {}, + "STATIC_PATHS": ["images"], + "STATIC_EXCLUDES": [], + "STATIC_EXCLUDE_SOURCES": True, + "THEME_STATIC_DIR": "theme", + "THEME_STATIC_PATHS": [ + "static", ], - 'YEAR_ARCHIVE_URL': '', - 'YEAR_ARCHIVE_SAVE_AS': '', - 'MONTH_ARCHIVE_URL': '', - 'MONTH_ARCHIVE_SAVE_AS': '', - 'DAY_ARCHIVE_URL': '', - 'DAY_ARCHIVE_SAVE_AS': '', - 'RELATIVE_URLS': False, - 'DEFAULT_LANG': 'en', - 'ARTICLE_TRANSLATION_ID': 'slug', - 'PAGE_TRANSLATION_ID': 'slug', - 'DIRECT_TEMPLATES': ['index', 'tags', 'categories', 'authors', 'archives'], - 'THEME_TEMPLATES_OVERRIDES': [], - 'PAGINATED_TEMPLATES': {'index': None, 'tag': None, 'category': None, - 'author': None}, - 'PELICAN_CLASS': 'pelican.Pelican', - 'DEFAULT_DATE_FORMAT': '%a %d %B %Y', - 'DATE_FORMATS': {}, - 'MARKDOWN': { - 'extension_configs': { - 'markdown.extensions.codehilite': {'css_class': 'highlight'}, - 'markdown.extensions.extra': {}, - 'markdown.extensions.meta': {}, + "FEED_ALL_ATOM": "feeds/all.atom.xml", + "CATEGORY_FEED_ATOM": "feeds/{slug}.atom.xml", + "AUTHOR_FEED_ATOM": "feeds/{slug}.atom.xml", + "AUTHOR_FEED_RSS": "feeds/{slug}.rss.xml", + "TRANSLATION_FEED_ATOM": "feeds/all-{lang}.atom.xml", + "FEED_MAX_ITEMS": 100, + "RSS_FEED_SUMMARY_ONLY": True, + "SITEURL": "", + "SITENAME": "A Pelican Blog", + "DISPLAY_PAGES_ON_MENU": True, + "DISPLAY_CATEGORIES_ON_MENU": True, + "DOCUTILS_SETTINGS": {}, + "OUTPUT_SOURCES": False, + "OUTPUT_SOURCES_EXTENSION": ".text", + "USE_FOLDER_AS_CATEGORY": True, + "DEFAULT_CATEGORY": "misc", + "WITH_FUTURE_DATES": True, + "CSS_FILE": "main.css", + "NEWEST_FIRST_ARCHIVES": True, + "REVERSE_CATEGORY_ORDER": False, + "DELETE_OUTPUT_DIRECTORY": False, + "OUTPUT_RETENTION": [], + "INDEX_SAVE_AS": "index.html", + "ARTICLE_URL": "{slug}.html", + "ARTICLE_SAVE_AS": "{slug}.html", + "ARTICLE_ORDER_BY": "reversed-date", + "ARTICLE_LANG_URL": "{slug}-{lang}.html", + "ARTICLE_LANG_SAVE_AS": "{slug}-{lang}.html", + "DRAFT_URL": "drafts/{slug}.html", + "DRAFT_SAVE_AS": "drafts/{slug}.html", + "DRAFT_LANG_URL": "drafts/{slug}-{lang}.html", + "DRAFT_LANG_SAVE_AS": "drafts/{slug}-{lang}.html", + "PAGE_URL": "pages/{slug}.html", + "PAGE_SAVE_AS": "pages/{slug}.html", + "PAGE_ORDER_BY": "basename", + "PAGE_LANG_URL": "pages/{slug}-{lang}.html", + "PAGE_LANG_SAVE_AS": "pages/{slug}-{lang}.html", + "DRAFT_PAGE_URL": "drafts/pages/{slug}.html", + "DRAFT_PAGE_SAVE_AS": "drafts/pages/{slug}.html", + "DRAFT_PAGE_LANG_URL": "drafts/pages/{slug}-{lang}.html", + "DRAFT_PAGE_LANG_SAVE_AS": "drafts/pages/{slug}-{lang}.html", + "STATIC_URL": "{path}", + "STATIC_SAVE_AS": "{path}", + "STATIC_CREATE_LINKS": False, + "STATIC_CHECK_IF_MODIFIED": False, + "CATEGORY_URL": "category/{slug}.html", + "CATEGORY_SAVE_AS": "category/{slug}.html", + "TAG_URL": "tag/{slug}.html", + "TAG_SAVE_AS": "tag/{slug}.html", + "AUTHOR_URL": "author/{slug}.html", + "AUTHOR_SAVE_AS": "author/{slug}.html", + "PAGINATION_PATTERNS": [ + (1, "{name}{extension}", "{name}{extension}"), + (2, "{name}{number}{extension}", "{name}{number}{extension}"), + ], + "YEAR_ARCHIVE_URL": "", + "YEAR_ARCHIVE_SAVE_AS": "", + "MONTH_ARCHIVE_URL": "", + "MONTH_ARCHIVE_SAVE_AS": "", + "DAY_ARCHIVE_URL": "", + "DAY_ARCHIVE_SAVE_AS": "", + "RELATIVE_URLS": False, + "DEFAULT_LANG": "en", + "ARTICLE_TRANSLATION_ID": "slug", + "PAGE_TRANSLATION_ID": "slug", + "DIRECT_TEMPLATES": ["index", "tags", "categories", "authors", "archives"], + "THEME_TEMPLATES_OVERRIDES": [], + "PAGINATED_TEMPLATES": { + "index": None, + "tag": None, + "category": None, + "author": None, + }, + "PELICAN_CLASS": "pelican.Pelican", + "DEFAULT_DATE_FORMAT": "%a %d %B %Y", + "DATE_FORMATS": {}, + "MARKDOWN": { + "extension_configs": { + "markdown.extensions.codehilite": {"css_class": "highlight"}, + "markdown.extensions.extra": {}, + "markdown.extensions.meta": {}, }, - 'output_format': 'html5', + "output_format": "html5", }, - 'JINJA_FILTERS': {}, - 'JINJA_GLOBALS': {}, - 'JINJA_TESTS': {}, - 'JINJA_ENVIRONMENT': { - 'trim_blocks': True, - 'lstrip_blocks': True, - 'extensions': [], + "JINJA_FILTERS": {}, + "JINJA_GLOBALS": {}, + "JINJA_TESTS": {}, + "JINJA_ENVIRONMENT": { + "trim_blocks": True, + "lstrip_blocks": True, + "extensions": [], }, - 'LOG_FILTER': [], - 'LOCALE': [''], # defaults to user locale - 'DEFAULT_PAGINATION': False, - 'DEFAULT_ORPHANS': 0, - 'DEFAULT_METADATA': {}, - 'FILENAME_METADATA': r'(?P\d{4}-\d{2}-\d{2}).*', - 'PATH_METADATA': '', - 'EXTRA_PATH_METADATA': {}, - 'ARTICLE_PERMALINK_STRUCTURE': '', - 'TYPOGRIFY': False, - 'TYPOGRIFY_IGNORE_TAGS': [], - 'TYPOGRIFY_DASHES': 'default', - 'SUMMARY_END_SUFFIX': '…', - 'SUMMARY_MAX_LENGTH': 50, - 'PLUGIN_PATHS': [], - 'PLUGINS': None, - 'PYGMENTS_RST_OPTIONS': {}, - 'TEMPLATE_PAGES': {}, - 'TEMPLATE_EXTENSIONS': ['.html'], - 'IGNORE_FILES': ['.#*'], - 'SLUG_REGEX_SUBSTITUTIONS': [ - (r'[^\w\s-]', ''), # remove non-alphabetical/whitespace/'-' chars - (r'(?u)\A\s*', ''), # strip leading whitespace - (r'(?u)\s*\Z', ''), # strip trailing whitespace - (r'[-\s]+', '-'), # reduce multiple whitespace or '-' to single '-' + "LOG_FILTER": [], + "LOCALE": [""], # defaults to user locale + "DEFAULT_PAGINATION": False, + "DEFAULT_ORPHANS": 0, + "DEFAULT_METADATA": {}, + "FILENAME_METADATA": r"(?P\d{4}-\d{2}-\d{2}).*", + "PATH_METADATA": "", + "EXTRA_PATH_METADATA": {}, + "ARTICLE_PERMALINK_STRUCTURE": "", + "TYPOGRIFY": False, + "TYPOGRIFY_IGNORE_TAGS": [], + "TYPOGRIFY_DASHES": "default", + "SUMMARY_END_SUFFIX": "…", + "SUMMARY_MAX_LENGTH": 50, + "PLUGIN_PATHS": [], + "PLUGINS": None, + "PYGMENTS_RST_OPTIONS": {}, + "TEMPLATE_PAGES": {}, + "TEMPLATE_EXTENSIONS": [".html"], + "IGNORE_FILES": [".#*"], + "SLUG_REGEX_SUBSTITUTIONS": [ + (r"[^\w\s-]", ""), # remove non-alphabetical/whitespace/'-' chars + (r"(?u)\A\s*", ""), # strip leading whitespace + (r"(?u)\s*\Z", ""), # strip trailing whitespace + (r"[-\s]+", "-"), # reduce multiple whitespace or '-' to single '-' ], - 'INTRASITE_LINK_REGEX': '[{|](?P.*?)[|}]', - 'SLUGIFY_SOURCE': 'title', - 'SLUGIFY_USE_UNICODE': False, - 'SLUGIFY_PRESERVE_CASE': False, - 'CACHE_CONTENT': False, - 'CONTENT_CACHING_LAYER': 'reader', - 'CACHE_PATH': 'cache', - 'GZIP_CACHE': True, - 'CHECK_MODIFIED_METHOD': 'mtime', - 'LOAD_CONTENT_CACHE': False, - 'WRITE_SELECTED': [], - 'FORMATTED_FIELDS': ['summary'], - 'PORT': 8000, - 'BIND': '127.0.0.1', + "INTRASITE_LINK_REGEX": "[{|](?P.*?)[|}]", + "SLUGIFY_SOURCE": "title", + "SLUGIFY_USE_UNICODE": False, + "SLUGIFY_PRESERVE_CASE": False, + "CACHE_CONTENT": False, + "CONTENT_CACHING_LAYER": "reader", + "CACHE_PATH": "cache", + "GZIP_CACHE": True, + "CHECK_MODIFIED_METHOD": "mtime", + "LOAD_CONTENT_CACHE": False, + "WRITE_SELECTED": [], + "FORMATTED_FIELDS": ["summary"], + "PORT": 8000, + "BIND": "127.0.0.1", } PYGMENTS_RST_OPTIONS = None @@ -185,20 +192,23 @@ def read_settings(path=None, override=None): def getabs(maybe_relative, base_path=path): if isabs(maybe_relative): return maybe_relative - return os.path.abspath(os.path.normpath(os.path.join( - os.path.dirname(base_path), maybe_relative))) + return os.path.abspath( + os.path.normpath( + os.path.join(os.path.dirname(base_path), maybe_relative) + ) + ) - for p in ['PATH', 'OUTPUT_PATH', 'THEME', 'CACHE_PATH']: + for p in ["PATH", "OUTPUT_PATH", "THEME", "CACHE_PATH"]: if settings.get(p) is not None: absp = getabs(settings[p]) # THEME may be a name rather than a path - if p != 'THEME' or os.path.exists(absp): + if p != "THEME" or os.path.exists(absp): settings[p] = absp - if settings.get('PLUGIN_PATHS') is not None: - settings['PLUGIN_PATHS'] = [getabs(pluginpath) - for pluginpath - in settings['PLUGIN_PATHS']] + if settings.get("PLUGIN_PATHS") is not None: + settings["PLUGIN_PATHS"] = [ + getabs(pluginpath) for pluginpath in settings["PLUGIN_PATHS"] + ] settings = dict(copy.deepcopy(DEFAULT_CONFIG), **settings) settings = configure_settings(settings) @@ -208,7 +218,7 @@ def read_settings(path=None, override=None): # variable here that we'll import from within Pygments.run (see # rstdirectives.py) to see what the user defaults were. global PYGMENTS_RST_OPTIONS - PYGMENTS_RST_OPTIONS = settings.get('PYGMENTS_RST_OPTIONS', None) + PYGMENTS_RST_OPTIONS = settings.get("PYGMENTS_RST_OPTIONS", None) return settings @@ -217,8 +227,7 @@ def get_settings_from_module(module=None): context = {} if module is not None: - context.update( - (k, v) for k, v in inspect.getmembers(module) if k.isupper()) + context.update((k, v) for k, v in inspect.getmembers(module) if k.isupper()) return context @@ -233,11 +242,12 @@ def get_settings_from_file(path): def get_jinja_environment(settings): """Sets the environment for Jinja""" - jinja_env = settings.setdefault('JINJA_ENVIRONMENT', - DEFAULT_CONFIG['JINJA_ENVIRONMENT']) + jinja_env = settings.setdefault( + "JINJA_ENVIRONMENT", DEFAULT_CONFIG["JINJA_ENVIRONMENT"] + ) # Make sure we include the defaults if the user has set env variables - for key, value in DEFAULT_CONFIG['JINJA_ENVIRONMENT'].items(): + for key, value in DEFAULT_CONFIG["JINJA_ENVIRONMENT"].items(): if key not in jinja_env: jinja_env[key] = value @@ -248,14 +258,14 @@ def _printf_s_to_format_field(printf_string, format_field): """Tries to replace %s with {format_field} in the provided printf_string. Raises ValueError in case of failure. """ - TEST_STRING = 'PELICAN_PRINTF_S_DEPRECATION' + TEST_STRING = "PELICAN_PRINTF_S_DEPRECATION" expected = printf_string % TEST_STRING - result = printf_string.replace('{', '{{').replace('}', '}}') \ - % '{{{}}}'.format(format_field) + result = printf_string.replace("{", "{{").replace("}", "}}") % "{{{}}}".format( + format_field + ) if result.format(**{format_field: TEST_STRING}) != expected: - raise ValueError('Failed to safely replace %s with {{{}}}'.format( - format_field)) + raise ValueError("Failed to safely replace %s with {{{}}}".format(format_field)) return result @@ -266,115 +276,140 @@ def handle_deprecated_settings(settings): """ # PLUGIN_PATH -> PLUGIN_PATHS - if 'PLUGIN_PATH' in settings: - logger.warning('PLUGIN_PATH setting has been replaced by ' - 'PLUGIN_PATHS, moving it to the new setting name.') - settings['PLUGIN_PATHS'] = settings['PLUGIN_PATH'] - del settings['PLUGIN_PATH'] + if "PLUGIN_PATH" in settings: + logger.warning( + "PLUGIN_PATH setting has been replaced by " + "PLUGIN_PATHS, moving it to the new setting name." + ) + settings["PLUGIN_PATHS"] = settings["PLUGIN_PATH"] + del settings["PLUGIN_PATH"] # PLUGIN_PATHS: str -> [str] - if isinstance(settings.get('PLUGIN_PATHS'), str): - logger.warning("Defining PLUGIN_PATHS setting as string " - "has been deprecated (should be a list)") - settings['PLUGIN_PATHS'] = [settings['PLUGIN_PATHS']] + if isinstance(settings.get("PLUGIN_PATHS"), str): + logger.warning( + "Defining PLUGIN_PATHS setting as string " + "has been deprecated (should be a list)" + ) + settings["PLUGIN_PATHS"] = [settings["PLUGIN_PATHS"]] # JINJA_EXTENSIONS -> JINJA_ENVIRONMENT > extensions - if 'JINJA_EXTENSIONS' in settings: - logger.warning('JINJA_EXTENSIONS setting has been deprecated, ' - 'moving it to JINJA_ENVIRONMENT setting.') - settings['JINJA_ENVIRONMENT']['extensions'] = \ - settings['JINJA_EXTENSIONS'] - del settings['JINJA_EXTENSIONS'] + if "JINJA_EXTENSIONS" in settings: + logger.warning( + "JINJA_EXTENSIONS setting has been deprecated, " + "moving it to JINJA_ENVIRONMENT setting." + ) + settings["JINJA_ENVIRONMENT"]["extensions"] = settings["JINJA_EXTENSIONS"] + del settings["JINJA_EXTENSIONS"] # {ARTICLE,PAGE}_DIR -> {ARTICLE,PAGE}_PATHS - for key in ['ARTICLE', 'PAGE']: - old_key = key + '_DIR' - new_key = key + '_PATHS' + for key in ["ARTICLE", "PAGE"]: + old_key = key + "_DIR" + new_key = key + "_PATHS" if old_key in settings: logger.warning( - 'Deprecated setting %s, moving it to %s list', - old_key, new_key) - settings[new_key] = [settings[old_key]] # also make a list + "Deprecated setting %s, moving it to %s list", old_key, new_key + ) + settings[new_key] = [settings[old_key]] # also make a list del settings[old_key] # EXTRA_TEMPLATES_PATHS -> THEME_TEMPLATES_OVERRIDES - if 'EXTRA_TEMPLATES_PATHS' in settings: - logger.warning('EXTRA_TEMPLATES_PATHS is deprecated use ' - 'THEME_TEMPLATES_OVERRIDES instead.') - if ('THEME_TEMPLATES_OVERRIDES' in settings and - settings['THEME_TEMPLATES_OVERRIDES']): + if "EXTRA_TEMPLATES_PATHS" in settings: + logger.warning( + "EXTRA_TEMPLATES_PATHS is deprecated use " + "THEME_TEMPLATES_OVERRIDES instead." + ) + if ( + "THEME_TEMPLATES_OVERRIDES" in settings + and settings["THEME_TEMPLATES_OVERRIDES"] + ): raise Exception( - 'Setting both EXTRA_TEMPLATES_PATHS and ' - 'THEME_TEMPLATES_OVERRIDES is not permitted. Please move to ' - 'only setting THEME_TEMPLATES_OVERRIDES.') - settings['THEME_TEMPLATES_OVERRIDES'] = \ - settings['EXTRA_TEMPLATES_PATHS'] - del settings['EXTRA_TEMPLATES_PATHS'] + "Setting both EXTRA_TEMPLATES_PATHS and " + "THEME_TEMPLATES_OVERRIDES is not permitted. Please move to " + "only setting THEME_TEMPLATES_OVERRIDES." + ) + settings["THEME_TEMPLATES_OVERRIDES"] = settings["EXTRA_TEMPLATES_PATHS"] + del settings["EXTRA_TEMPLATES_PATHS"] # MD_EXTENSIONS -> MARKDOWN - if 'MD_EXTENSIONS' in settings: - logger.warning('MD_EXTENSIONS is deprecated use MARKDOWN ' - 'instead. Falling back to the default.') - settings['MARKDOWN'] = DEFAULT_CONFIG['MARKDOWN'] + if "MD_EXTENSIONS" in settings: + logger.warning( + "MD_EXTENSIONS is deprecated use MARKDOWN " + "instead. Falling back to the default." + ) + settings["MARKDOWN"] = DEFAULT_CONFIG["MARKDOWN"] # LESS_GENERATOR -> Webassets plugin # FILES_TO_COPY -> STATIC_PATHS, EXTRA_PATH_METADATA for old, new, doc in [ - ('LESS_GENERATOR', 'the Webassets plugin', None), - ('FILES_TO_COPY', 'STATIC_PATHS and EXTRA_PATH_METADATA', - 'https://github.com/getpelican/pelican/' - 'blob/master/docs/settings.rst#path-metadata'), + ("LESS_GENERATOR", "the Webassets plugin", None), + ( + "FILES_TO_COPY", + "STATIC_PATHS and EXTRA_PATH_METADATA", + "https://github.com/getpelican/pelican/" + "blob/master/docs/settings.rst#path-metadata", + ), ]: if old in settings: - message = 'The {} setting has been removed in favor of {}'.format( - old, new) + message = "The {} setting has been removed in favor of {}".format(old, new) if doc: - message += ', see {} for details'.format(doc) + message += ", see {} for details".format(doc) logger.warning(message) # PAGINATED_DIRECT_TEMPLATES -> PAGINATED_TEMPLATES - if 'PAGINATED_DIRECT_TEMPLATES' in settings: - message = 'The {} setting has been removed in favor of {}'.format( - 'PAGINATED_DIRECT_TEMPLATES', 'PAGINATED_TEMPLATES') + if "PAGINATED_DIRECT_TEMPLATES" in settings: + message = "The {} setting has been removed in favor of {}".format( + "PAGINATED_DIRECT_TEMPLATES", "PAGINATED_TEMPLATES" + ) logger.warning(message) # set PAGINATED_TEMPLATES - if 'PAGINATED_TEMPLATES' not in settings: - settings['PAGINATED_TEMPLATES'] = { - 'tag': None, 'category': None, 'author': None} + if "PAGINATED_TEMPLATES" not in settings: + settings["PAGINATED_TEMPLATES"] = { + "tag": None, + "category": None, + "author": None, + } - for t in settings['PAGINATED_DIRECT_TEMPLATES']: - if t not in settings['PAGINATED_TEMPLATES']: - settings['PAGINATED_TEMPLATES'][t] = None - del settings['PAGINATED_DIRECT_TEMPLATES'] + for t in settings["PAGINATED_DIRECT_TEMPLATES"]: + if t not in settings["PAGINATED_TEMPLATES"]: + settings["PAGINATED_TEMPLATES"][t] = None + del settings["PAGINATED_DIRECT_TEMPLATES"] # {SLUG,CATEGORY,TAG,AUTHOR}_SUBSTITUTIONS -> # {SLUG,CATEGORY,TAG,AUTHOR}_REGEX_SUBSTITUTIONS - url_settings_url = \ - 'http://docs.getpelican.com/en/latest/settings.html#url-settings' - flavours = {'SLUG', 'CATEGORY', 'TAG', 'AUTHOR'} - old_values = {f: settings[f + '_SUBSTITUTIONS'] - for f in flavours if f + '_SUBSTITUTIONS' in settings} - new_values = {f: settings[f + '_REGEX_SUBSTITUTIONS'] - for f in flavours if f + '_REGEX_SUBSTITUTIONS' in settings} + url_settings_url = "http://docs.getpelican.com/en/latest/settings.html#url-settings" + flavours = {"SLUG", "CATEGORY", "TAG", "AUTHOR"} + old_values = { + f: settings[f + "_SUBSTITUTIONS"] + for f in flavours + if f + "_SUBSTITUTIONS" in settings + } + new_values = { + f: settings[f + "_REGEX_SUBSTITUTIONS"] + for f in flavours + if f + "_REGEX_SUBSTITUTIONS" in settings + } if old_values and new_values: raise Exception( - 'Setting both {new_key} and {old_key} (or variants thereof) is ' - 'not permitted. Please move to only setting {new_key}.' - .format(old_key='SLUG_SUBSTITUTIONS', - new_key='SLUG_REGEX_SUBSTITUTIONS')) + "Setting both {new_key} and {old_key} (or variants thereof) is " + "not permitted. Please move to only setting {new_key}.".format( + old_key="SLUG_SUBSTITUTIONS", new_key="SLUG_REGEX_SUBSTITUTIONS" + ) + ) if old_values: - message = ('{} and variants thereof are deprecated and will be ' - 'removed in the future. Please use {} and variants thereof ' - 'instead. Check {}.' - .format('SLUG_SUBSTITUTIONS', 'SLUG_REGEX_SUBSTITUTIONS', - url_settings_url)) + message = ( + "{} and variants thereof are deprecated and will be " + "removed in the future. Please use {} and variants thereof " + "instead. Check {}.".format( + "SLUG_SUBSTITUTIONS", "SLUG_REGEX_SUBSTITUTIONS", url_settings_url + ) + ) logger.warning(message) - if old_values.get('SLUG'): - for f in {'CATEGORY', 'TAG'}: + if old_values.get("SLUG"): + for f in {"CATEGORY", "TAG"}: if old_values.get(f): - old_values[f] = old_values['SLUG'] + old_values[f] - old_values['AUTHOR'] = old_values.get('AUTHOR', []) + old_values[f] = old_values["SLUG"] + old_values[f] + old_values["AUTHOR"] = old_values.get("AUTHOR", []) for f in flavours: if old_values.get(f) is not None: regex_subs = [] @@ -387,120 +422,138 @@ def handle_deprecated_settings(settings): replace = False except ValueError: src, dst = tpl - regex_subs.append( - (re.escape(src), dst.replace('\\', r'\\'))) + regex_subs.append((re.escape(src), dst.replace("\\", r"\\"))) if replace: regex_subs += [ - (r'[^\w\s-]', ''), - (r'(?u)\A\s*', ''), - (r'(?u)\s*\Z', ''), - (r'[-\s]+', '-'), + (r"[^\w\s-]", ""), + (r"(?u)\A\s*", ""), + (r"(?u)\s*\Z", ""), + (r"[-\s]+", "-"), ] else: regex_subs += [ - (r'(?u)\A\s*', ''), - (r'(?u)\s*\Z', ''), + (r"(?u)\A\s*", ""), + (r"(?u)\s*\Z", ""), ] - settings[f + '_REGEX_SUBSTITUTIONS'] = regex_subs - settings.pop(f + '_SUBSTITUTIONS', None) + settings[f + "_REGEX_SUBSTITUTIONS"] = regex_subs + settings.pop(f + "_SUBSTITUTIONS", None) # `%s` -> '{slug}` or `{lang}` in FEED settings - for key in ['TRANSLATION_FEED_ATOM', - 'TRANSLATION_FEED_RSS' - ]: + for key in ["TRANSLATION_FEED_ATOM", "TRANSLATION_FEED_RSS"]: if ( - settings.get(key) and not isinstance(settings[key], Path) - and '%s' in settings[key] + settings.get(key) + and not isinstance(settings[key], Path) + and "%s" in settings[key] ): - logger.warning('%%s usage in %s is deprecated, use {lang} ' - 'instead.', key) + logger.warning("%%s usage in %s is deprecated, use {lang} " "instead.", key) try: - settings[key] = _printf_s_to_format_field( - settings[key], 'lang') + settings[key] = _printf_s_to_format_field(settings[key], "lang") except ValueError: - logger.warning('Failed to convert %%s to {lang} for %s. ' - 'Falling back to default.', key) + logger.warning( + "Failed to convert %%s to {lang} for %s. " + "Falling back to default.", + key, + ) settings[key] = DEFAULT_CONFIG[key] - for key in ['AUTHOR_FEED_ATOM', - 'AUTHOR_FEED_RSS', - 'CATEGORY_FEED_ATOM', - 'CATEGORY_FEED_RSS', - 'TAG_FEED_ATOM', - 'TAG_FEED_RSS', - ]: + for key in [ + "AUTHOR_FEED_ATOM", + "AUTHOR_FEED_RSS", + "CATEGORY_FEED_ATOM", + "CATEGORY_FEED_RSS", + "TAG_FEED_ATOM", + "TAG_FEED_RSS", + ]: if ( - settings.get(key) and not isinstance(settings[key], Path) - and '%s' in settings[key] + settings.get(key) + and not isinstance(settings[key], Path) + and "%s" in settings[key] ): - logger.warning('%%s usage in %s is deprecated, use {slug} ' - 'instead.', key) + logger.warning("%%s usage in %s is deprecated, use {slug} " "instead.", key) try: - settings[key] = _printf_s_to_format_field( - settings[key], 'slug') + settings[key] = _printf_s_to_format_field(settings[key], "slug") except ValueError: - logger.warning('Failed to convert %%s to {slug} for %s. ' - 'Falling back to default.', key) + logger.warning( + "Failed to convert %%s to {slug} for %s. " + "Falling back to default.", + key, + ) settings[key] = DEFAULT_CONFIG[key] # CLEAN_URLS - if settings.get('CLEAN_URLS', False): - logger.warning('Found deprecated `CLEAN_URLS` in settings.' - ' Modifying the following settings for the' - ' same behaviour.') + if settings.get("CLEAN_URLS", False): + logger.warning( + "Found deprecated `CLEAN_URLS` in settings." + " Modifying the following settings for the" + " same behaviour." + ) - settings['ARTICLE_URL'] = '{slug}/' - settings['ARTICLE_LANG_URL'] = '{slug}-{lang}/' - settings['PAGE_URL'] = 'pages/{slug}/' - settings['PAGE_LANG_URL'] = 'pages/{slug}-{lang}/' + settings["ARTICLE_URL"] = "{slug}/" + settings["ARTICLE_LANG_URL"] = "{slug}-{lang}/" + settings["PAGE_URL"] = "pages/{slug}/" + settings["PAGE_LANG_URL"] = "pages/{slug}-{lang}/" - for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', - 'PAGE_LANG_URL'): + for setting in ("ARTICLE_URL", "ARTICLE_LANG_URL", "PAGE_URL", "PAGE_LANG_URL"): logger.warning("%s = '%s'", setting, settings[setting]) # AUTORELOAD_IGNORE_CACHE -> --ignore-cache - if settings.get('AUTORELOAD_IGNORE_CACHE'): - logger.warning('Found deprecated `AUTORELOAD_IGNORE_CACHE` in ' - 'settings. Use --ignore-cache instead.') - settings.pop('AUTORELOAD_IGNORE_CACHE') + if settings.get("AUTORELOAD_IGNORE_CACHE"): + logger.warning( + "Found deprecated `AUTORELOAD_IGNORE_CACHE` in " + "settings. Use --ignore-cache instead." + ) + settings.pop("AUTORELOAD_IGNORE_CACHE") # ARTICLE_PERMALINK_STRUCTURE - if settings.get('ARTICLE_PERMALINK_STRUCTURE', False): - logger.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in' - ' settings. Modifying the following settings for' - ' the same behaviour.') + if settings.get("ARTICLE_PERMALINK_STRUCTURE", False): + logger.warning( + "Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in" + " settings. Modifying the following settings for" + " the same behaviour." + ) - structure = settings['ARTICLE_PERMALINK_STRUCTURE'] + structure = settings["ARTICLE_PERMALINK_STRUCTURE"] # Convert %(variable) into {variable}. - structure = re.sub(r'%\((\w+)\)s', r'{\g<1>}', structure) + structure = re.sub(r"%\((\w+)\)s", r"{\g<1>}", structure) # Convert %x into {date:%x} for strftime - structure = re.sub(r'(%[A-z])', r'{date:\g<1>}', structure) + structure = re.sub(r"(%[A-z])", r"{date:\g<1>}", structure) # Strip a / prefix - structure = re.sub('^/', '', structure) + structure = re.sub("^/", "", structure) - for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', - 'PAGE_LANG_URL', 'DRAFT_URL', 'DRAFT_LANG_URL', - 'ARTICLE_SAVE_AS', 'ARTICLE_LANG_SAVE_AS', - 'DRAFT_SAVE_AS', 'DRAFT_LANG_SAVE_AS', - 'PAGE_SAVE_AS', 'PAGE_LANG_SAVE_AS'): - settings[setting] = os.path.join(structure, - settings[setting]) + for setting in ( + "ARTICLE_URL", + "ARTICLE_LANG_URL", + "PAGE_URL", + "PAGE_LANG_URL", + "DRAFT_URL", + "DRAFT_LANG_URL", + "ARTICLE_SAVE_AS", + "ARTICLE_LANG_SAVE_AS", + "DRAFT_SAVE_AS", + "DRAFT_LANG_SAVE_AS", + "PAGE_SAVE_AS", + "PAGE_LANG_SAVE_AS", + ): + settings[setting] = os.path.join(structure, settings[setting]) logger.warning("%s = '%s'", setting, settings[setting]) # {,TAG,CATEGORY,TRANSLATION}_FEED -> {,TAG,CATEGORY,TRANSLATION}_FEED_ATOM - for new, old in [('FEED', 'FEED_ATOM'), ('TAG_FEED', 'TAG_FEED_ATOM'), - ('CATEGORY_FEED', 'CATEGORY_FEED_ATOM'), - ('TRANSLATION_FEED', 'TRANSLATION_FEED_ATOM')]: + for new, old in [ + ("FEED", "FEED_ATOM"), + ("TAG_FEED", "TAG_FEED_ATOM"), + ("CATEGORY_FEED", "CATEGORY_FEED_ATOM"), + ("TRANSLATION_FEED", "TRANSLATION_FEED_ATOM"), + ]: if settings.get(new, False): logger.warning( - 'Found deprecated `%(new)s` in settings. Modify %(new)s ' - 'to %(old)s in your settings and theme for the same ' - 'behavior. Temporarily setting %(old)s for backwards ' - 'compatibility.', - {'new': new, 'old': old} + "Found deprecated `%(new)s` in settings. Modify %(new)s " + "to %(old)s in your settings and theme for the same " + "behavior. Temporarily setting %(old)s for backwards " + "compatibility.", + {"new": new, "old": old}, ) settings[old] = settings[new] @@ -512,34 +565,34 @@ def configure_settings(settings): settings. Also, specify the log messages to be ignored. """ - if 'PATH' not in settings or not os.path.isdir(settings['PATH']): - raise Exception('You need to specify a path containing the content' - ' (see pelican --help for more information)') + if "PATH" not in settings or not os.path.isdir(settings["PATH"]): + raise Exception( + "You need to specify a path containing the content" + " (see pelican --help for more information)" + ) # specify the log messages to be ignored - log_filter = settings.get('LOG_FILTER', DEFAULT_CONFIG['LOG_FILTER']) + log_filter = settings.get("LOG_FILTER", DEFAULT_CONFIG["LOG_FILTER"]) LimitFilter._ignore.update(set(log_filter)) # lookup the theme in "pelican/themes" if the given one doesn't exist - if not os.path.isdir(settings['THEME']): + if not os.path.isdir(settings["THEME"]): theme_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - 'themes', - settings['THEME']) + os.path.dirname(os.path.abspath(__file__)), "themes", settings["THEME"] + ) if os.path.exists(theme_path): - settings['THEME'] = theme_path + settings["THEME"] = theme_path else: - raise Exception("Could not find the theme %s" - % settings['THEME']) + raise Exception("Could not find the theme %s" % settings["THEME"]) # make paths selected for writing absolute if necessary - settings['WRITE_SELECTED'] = [ - os.path.abspath(path) for path in - settings.get('WRITE_SELECTED', DEFAULT_CONFIG['WRITE_SELECTED']) + settings["WRITE_SELECTED"] = [ + os.path.abspath(path) + for path in settings.get("WRITE_SELECTED", DEFAULT_CONFIG["WRITE_SELECTED"]) ] # standardize strings to lowercase strings - for key in ['DEFAULT_LANG']: + for key in ["DEFAULT_LANG"]: if key in settings: settings[key] = settings[key].lower() @@ -547,24 +600,26 @@ def configure_settings(settings): settings = get_jinja_environment(settings) # standardize strings to lists - for key in ['LOCALE']: + for key in ["LOCALE"]: if key in settings and isinstance(settings[key], str): settings[key] = [settings[key]] # check settings that must be a particular type for key, types in [ - ('OUTPUT_SOURCES_EXTENSION', str), - ('FILENAME_METADATA', str), + ("OUTPUT_SOURCES_EXTENSION", str), + ("FILENAME_METADATA", str), ]: if key in settings and not isinstance(settings[key], types): value = settings.pop(key) logger.warn( - 'Detected misconfigured %s (%s), ' - 'falling back to the default (%s)', - key, value, DEFAULT_CONFIG[key]) + "Detected misconfigured %s (%s), " "falling back to the default (%s)", + key, + value, + DEFAULT_CONFIG[key], + ) # try to set the different locales, fallback on the default. - locales = settings.get('LOCALE', DEFAULT_CONFIG['LOCALE']) + locales = settings.get("LOCALE", DEFAULT_CONFIG["LOCALE"]) for locale_ in locales: try: @@ -575,95 +630,111 @@ def configure_settings(settings): else: logger.warning( "Locale could not be set. Check the LOCALE setting, ensuring it " - "is valid and available on your system.") + "is valid and available on your system." + ) - if ('SITEURL' in settings): + if "SITEURL" in settings: # If SITEURL has a trailing slash, remove it and provide a warning - siteurl = settings['SITEURL'] - if (siteurl.endswith('/')): - settings['SITEURL'] = siteurl[:-1] + siteurl = settings["SITEURL"] + if siteurl.endswith("/"): + settings["SITEURL"] = siteurl[:-1] logger.warning("Removed extraneous trailing slash from SITEURL.") # If SITEURL is defined but FEED_DOMAIN isn't, # set FEED_DOMAIN to SITEURL - if 'FEED_DOMAIN' not in settings: - settings['FEED_DOMAIN'] = settings['SITEURL'] + if "FEED_DOMAIN" not in settings: + settings["FEED_DOMAIN"] = settings["SITEURL"] # check content caching layer and warn of incompatibilities - if settings.get('CACHE_CONTENT', False) and \ - settings.get('CONTENT_CACHING_LAYER', '') == 'generator' and \ - not settings.get('WITH_FUTURE_DATES', True): + if ( + settings.get("CACHE_CONTENT", False) + and settings.get("CONTENT_CACHING_LAYER", "") == "generator" + and not settings.get("WITH_FUTURE_DATES", True) + ): logger.warning( "WITH_FUTURE_DATES conflicts with CONTENT_CACHING_LAYER " - "set to 'generator', use 'reader' layer instead") + "set to 'generator', use 'reader' layer instead" + ) # Warn if feeds are generated with both SITEURL & FEED_DOMAIN undefined feed_keys = [ - 'FEED_ATOM', 'FEED_RSS', - 'FEED_ALL_ATOM', 'FEED_ALL_RSS', - 'CATEGORY_FEED_ATOM', 'CATEGORY_FEED_RSS', - 'AUTHOR_FEED_ATOM', 'AUTHOR_FEED_RSS', - 'TAG_FEED_ATOM', 'TAG_FEED_RSS', - 'TRANSLATION_FEED_ATOM', 'TRANSLATION_FEED_RSS', + "FEED_ATOM", + "FEED_RSS", + "FEED_ALL_ATOM", + "FEED_ALL_RSS", + "CATEGORY_FEED_ATOM", + "CATEGORY_FEED_RSS", + "AUTHOR_FEED_ATOM", + "AUTHOR_FEED_RSS", + "TAG_FEED_ATOM", + "TAG_FEED_RSS", + "TRANSLATION_FEED_ATOM", + "TRANSLATION_FEED_RSS", ] if any(settings.get(k) for k in feed_keys): - if not settings.get('SITEURL'): - logger.warning('Feeds generated without SITEURL set properly may' - ' not be valid') + if not settings.get("SITEURL"): + logger.warning( + "Feeds generated without SITEURL set properly may" " not be valid" + ) - if 'TIMEZONE' not in settings: + if "TIMEZONE" not in settings: logger.warning( - 'No timezone information specified in the settings. Assuming' - ' your timezone is UTC for feed generation. Check ' - 'https://docs.getpelican.com/en/latest/settings.html#TIMEZONE ' - 'for more information') + "No timezone information specified in the settings. Assuming" + " your timezone is UTC for feed generation. Check " + "https://docs.getpelican.com/en/latest/settings.html#TIMEZONE " + "for more information" + ) # 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], ) # Save people from accidentally setting a string rather than a list path_keys = ( - 'ARTICLE_EXCLUDES', - 'DEFAULT_METADATA', - 'DIRECT_TEMPLATES', - 'THEME_TEMPLATES_OVERRIDES', - 'FILES_TO_COPY', - 'IGNORE_FILES', - 'PAGINATED_DIRECT_TEMPLATES', - 'PLUGINS', - 'STATIC_EXCLUDES', - 'STATIC_PATHS', - 'THEME_STATIC_PATHS', - 'ARTICLE_PATHS', - 'PAGE_PATHS', + "ARTICLE_EXCLUDES", + "DEFAULT_METADATA", + "DIRECT_TEMPLATES", + "THEME_TEMPLATES_OVERRIDES", + "FILES_TO_COPY", + "IGNORE_FILES", + "PAGINATED_DIRECT_TEMPLATES", + "PLUGINS", + "STATIC_EXCLUDES", + "STATIC_PATHS", + "THEME_STATIC_PATHS", + "ARTICLE_PATHS", + "PAGE_PATHS", ) for PATH_KEY in filter(lambda k: k in settings, path_keys): if isinstance(settings[PATH_KEY], str): - logger.warning("Detected misconfiguration with %s setting " - "(must be a list), falling back to the default", - PATH_KEY) + logger.warning( + "Detected misconfiguration with %s setting " + "(must be a list), falling back to the default", + PATH_KEY, + ) settings[PATH_KEY] = DEFAULT_CONFIG[PATH_KEY] # Add {PAGE,ARTICLE}_PATHS to {ARTICLE,PAGE}_EXCLUDES - mutually_exclusive = ('ARTICLE', 'PAGE') + mutually_exclusive = ("ARTICLE", "PAGE") for type_1, type_2 in [mutually_exclusive, mutually_exclusive[::-1]]: try: - includes = settings[type_1 + '_PATHS'] - excludes = settings[type_2 + '_EXCLUDES'] + includes = settings[type_1 + "_PATHS"] + excludes = settings[type_2 + "_EXCLUDES"] for path in includes: if path not in excludes: excludes.append(path) except KeyError: - continue # setting not specified, nothing to do + continue # setting not specified, nothing to do return settings diff --git a/pelican/signals.py b/pelican/signals.py index 9b84a92a..4d232e34 100644 --- a/pelican/signals.py +++ b/pelican/signals.py @@ -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." ) diff --git a/pelican/tests/default_conf.py b/pelican/tests/default_conf.py index 99f3b6cf..583c3253 100644 --- a/pelican/tests/default_conf.py +++ b/pelican/tests/default_conf.py @@ -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 diff --git a/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py index c514861d..1979cf09 100644 --- a/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py +++ b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py @@ -1,4 +1,4 @@ -NAME = 'namespace plugin' +NAME = "namespace plugin" def register(): diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 3e4da785..31b12ce8 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -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) diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py index 564f1d31..6dc91b2c 100644 --- a/pelican/tests/test_cache.py +++ b/pelican/tests/test_cache.py @@ -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) diff --git a/pelican/tests/test_cli.py b/pelican/tests/test_cli.py index 13b307e7..0b9656be 100644 --- a/pelican/tests/test_cli.py +++ b/pelican/tests/test_cli.py @@ -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}) diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py index 3a223b5a..9dc7b70d 100644 --- a/pelican/tests/test_contents.py +++ b/pelican/tests/test_contents.py @@ -10,9 +10,8 @@ from jinja2.utils import generate_lorem_ipsum from pelican.contents import Article, Author, Category, Page, Static from pelican.plugins.signals import content_object_init from pelican.settings import DEFAULT_CONFIG -from pelican.tests.support import (LoggedTestCase, get_context, get_settings, - unittest) -from pelican.utils import (path_to_url, posixize_path, truncate_html_words) +from pelican.tests.support import LoggedTestCase, get_context, get_settings, unittest +from pelican.utils import path_to_url, posixize_path, truncate_html_words # generate one paragraph, enclosed with

@@ -21,25 +20,24 @@ TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False) class TestBase(LoggedTestCase): - 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': '', - 'generated_content': {}, - 'static_content': {}, - 'static_links': set() + "content": TEST_CONTENT, + "context": { + "localsiteurl": "", + "generated_content": {}, + "static_content": {}, + "static_links": set(), }, - 'metadata': { - 'summary': TEST_SUMMARY, - 'title': 'foo bar', - 'author': Author('Blogger', DEFAULT_CONFIG), + "metadata": { + "summary": TEST_SUMMARY, + "title": "foo bar", + "author": Author("Blogger", DEFAULT_CONFIG), }, - 'source_path': '/path/to/file/foo.ext' + "source_path": "/path/to/file/foo.ext", } self._disable_limit_filter() @@ -49,10 +47,12 @@ class TestBase(LoggedTestCase): def _disable_limit_filter(self): from pelican.contents import logger + logger.disable_filter() def _enable_limit_filter(self): from pelican.contents import logger + logger.enable_filter() def _copy_page_kwargs(self): @@ -72,9 +72,12 @@ class TestPage(TestBase): def test_use_args(self): # Creating a page with arguments passed to the constructor should use # them to initialise object's attributes. - metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', } - page = Page(TEST_CONTENT, metadata=metadata, - context={'localsiteurl': ''}) + metadata = { + "foo": "bar", + "foobar": "baz", + "title": "foobar", + } + page = Page(TEST_CONTENT, metadata=metadata, context={"localsiteurl": ""}) for key, value in metadata.items(): self.assertTrue(hasattr(page, key)) self.assertEqual(value, getattr(page, key)) @@ -82,13 +85,14 @@ class TestPage(TestBase): def test_mandatory_properties(self): # If the title is not set, must throw an exception. - page = Page('content') + page = Page("content") self.assertFalse(page._has_valid_mandatory_properties()) self.assertLogCountEqual( - count=1, - msg="Skipping .*: could not find information about 'title'", - level=logging.ERROR) - page = Page('content', metadata={'title': 'foobar'}) + count=1, + msg="Skipping .*: could not find information about 'title'", + level=logging.ERROR, + ) + page = Page("content", metadata={"title": "foobar"}) self.assertTrue(page._has_valid_mandatory_properties()) def test_summary_from_metadata(self): @@ -101,31 +105,32 @@ class TestPage(TestBase): # generated summary should not exceed the given length. page_kwargs = self._copy_page_kwargs() settings = get_settings() - page_kwargs['settings'] = settings - del page_kwargs['metadata']['summary'] - settings['SUMMARY_MAX_LENGTH'] = None + page_kwargs["settings"] = settings + del page_kwargs["metadata"]["summary"] + settings["SUMMARY_MAX_LENGTH"] = None page = Page(**page_kwargs) self.assertEqual(page.summary, TEST_CONTENT) - settings['SUMMARY_MAX_LENGTH'] = 10 + settings["SUMMARY_MAX_LENGTH"] = 10 page = Page(**page_kwargs) self.assertEqual(page.summary, truncate_html_words(TEST_CONTENT, 10)) - settings['SUMMARY_MAX_LENGTH'] = 0 + settings["SUMMARY_MAX_LENGTH"] = 0 page = Page(**page_kwargs) - self.assertEqual(page.summary, '') + self.assertEqual(page.summary, "") def test_summary_end_suffix(self): # If a :SUMMARY_END_SUFFIX: is set, and there is no other summary, # generated summary should contain the specified marker at the end. page_kwargs = self._copy_page_kwargs() settings = get_settings() - page_kwargs['settings'] = settings - del page_kwargs['metadata']['summary'] - settings['SUMMARY_END_SUFFIX'] = 'test_marker' - settings['SUMMARY_MAX_LENGTH'] = 10 + page_kwargs["settings"] = settings + del page_kwargs["metadata"]["summary"] + settings["SUMMARY_END_SUFFIX"] = "test_marker" + settings["SUMMARY_MAX_LENGTH"] = 10 page = Page(**page_kwargs) - self.assertEqual(page.summary, truncate_html_words(TEST_CONTENT, 10, - 'test_marker')) - self.assertIn('test_marker', page.summary) + self.assertEqual( + page.summary, truncate_html_words(TEST_CONTENT, 10, "test_marker") + ) + self.assertIn("test_marker", page.summary) def test_summary_get_summary_warning(self): """calling ._get_summary() should issue a warning""" @@ -134,57 +139,61 @@ class TestPage(TestBase): self.assertEqual(page.summary, TEST_SUMMARY) self.assertEqual(page._get_summary(), TEST_SUMMARY) self.assertLogCountEqual( - count=1, - msg=r"_get_summary\(\) has been deprecated since 3\.6\.4\. " - "Use the summary decorator instead", - level=logging.WARNING) + count=1, + msg=r"_get_summary\(\) has been deprecated since 3\.6\.4\. " + "Use the summary decorator instead", + level=logging.WARNING, + ) def test_slug(self): page_kwargs = self._copy_page_kwargs() settings = get_settings() - page_kwargs['settings'] = settings - settings['SLUGIFY_SOURCE'] = "title" + page_kwargs["settings"] = settings + settings["SLUGIFY_SOURCE"] = "title" page = Page(**page_kwargs) - self.assertEqual(page.slug, 'foo-bar') - settings['SLUGIFY_SOURCE'] = "basename" + self.assertEqual(page.slug, "foo-bar") + settings["SLUGIFY_SOURCE"] = "basename" page = Page(**page_kwargs) - self.assertEqual(page.slug, 'foo') + self.assertEqual(page.slug, "foo") # test slug from title with unicode and case inputs = ( # (title, expected, preserve_case, use_unicode) - ('指導書', 'zhi-dao-shu', False, False), - ('指導書', 'Zhi-Dao-Shu', True, False), - ('指導書', '指導書', False, True), - ('指導書', '指導書', True, True), - ('Çığ', 'cig', False, False), - ('Çığ', 'Cig', True, False), - ('Çığ', 'çığ', False, True), - ('Çığ', 'Çığ', True, True), + ("指導書", "zhi-dao-shu", False, False), + ("指導書", "Zhi-Dao-Shu", True, False), + ("指導書", "指導書", False, True), + ("指導書", "指導書", True, True), + ("Çığ", "cig", False, False), + ("Çığ", "Cig", True, False), + ("Çığ", "çığ", False, True), + ("Çığ", "Çığ", True, True), ) settings = get_settings() page_kwargs = self._copy_page_kwargs() - page_kwargs['settings'] = settings + page_kwargs["settings"] = settings for title, expected, preserve_case, use_unicode in inputs: - settings['SLUGIFY_PRESERVE_CASE'] = preserve_case - settings['SLUGIFY_USE_UNICODE'] = use_unicode - page_kwargs['metadata']['title'] = title + settings["SLUGIFY_PRESERVE_CASE"] = preserve_case + settings["SLUGIFY_USE_UNICODE"] = use_unicode + page_kwargs["metadata"]["title"] = title page = Page(**page_kwargs) - self.assertEqual(page.slug, expected, - (title, preserve_case, use_unicode)) + self.assertEqual(page.slug, expected, (title, preserve_case, use_unicode)) def test_defaultlang(self): # If no lang is given, default to the default one. page = Page(**self.page_kwargs) - self.assertEqual(page.lang, DEFAULT_CONFIG['DEFAULT_LANG']) + self.assertEqual(page.lang, DEFAULT_CONFIG["DEFAULT_LANG"]) # it is possible to specify the lang in the metadata infos - self.page_kwargs['metadata'].update({'lang': 'fr', }) + self.page_kwargs["metadata"].update( + { + "lang": "fr", + } + ) page = Page(**self.page_kwargs) - self.assertEqual(page.lang, 'fr') + self.assertEqual(page.lang, "fr") def test_save_as(self): # If a lang is not the default lang, save_as should be set @@ -195,7 +204,11 @@ class TestPage(TestBase): self.assertEqual(page.save_as, "pages/foo-bar.html") # if a language is defined, save_as should include it accordingly - self.page_kwargs['metadata'].update({'lang': 'fr', }) + self.page_kwargs["metadata"].update( + { + "lang": "fr", + } + ) page = Page(**self.page_kwargs) self.assertEqual(page.save_as, "pages/foo-bar-fr.html") @@ -206,34 +219,32 @@ class TestPage(TestBase): # If 'source_path' is None, 'relative_source_path' should # also return None - page_kwargs['source_path'] = None + page_kwargs["source_path"] = None page = Page(**page_kwargs) self.assertIsNone(page.relative_source_path) page_kwargs = self._copy_page_kwargs() settings = get_settings() - full_path = page_kwargs['source_path'] + full_path = page_kwargs["source_path"] - settings['PATH'] = os.path.dirname(full_path) - page_kwargs['settings'] = settings + settings["PATH"] = os.path.dirname(full_path) + page_kwargs["settings"] = settings page = Page(**page_kwargs) # if 'source_path' is set, 'relative_source_path' should # return the relative path from 'PATH' to 'source_path' self.assertEqual( page.relative_source_path, - os.path.relpath( - full_path, - os.path.dirname(full_path) - )) + os.path.relpath(full_path, os.path.dirname(full_path)), + ) def test_metadata_url_format(self): # Arbitrary metadata should be passed through url_format() page = Page(**self.page_kwargs) - self.assertIn('summary', page.url_format.keys()) - page.metadata['directory'] = 'test-dir' - page.settings = get_settings(PAGE_SAVE_AS='{directory}/{slug}') - self.assertEqual(page.save_as, 'test-dir/foo-bar') + self.assertIn("summary", page.url_format.keys()) + page.metadata["directory"] = "test-dir" + page.settings = get_settings(PAGE_SAVE_AS="{directory}/{slug}") + self.assertEqual(page.save_as, "test-dir/foo-bar") def test_datetime(self): # If DATETIME is set to a tuple, it should be used to override LOCALE @@ -242,28 +253,28 @@ class TestPage(TestBase): page_kwargs = self._copy_page_kwargs() # set its date to dt - page_kwargs['metadata']['date'] = dt + page_kwargs["metadata"]["date"] = dt page = Page(**page_kwargs) # page.locale_date is a unicode string in both python2 and python3 - dt_date = dt.strftime(DEFAULT_CONFIG['DEFAULT_DATE_FORMAT']) + dt_date = dt.strftime(DEFAULT_CONFIG["DEFAULT_DATE_FORMAT"]) self.assertEqual(page.locale_date, dt_date) - page_kwargs['settings'] = get_settings() + page_kwargs["settings"] = get_settings() # I doubt this can work on all platforms ... if platform == "win32": - locale = 'jpn' + locale = "jpn" else: - locale = 'ja_JP.utf8' - page_kwargs['settings']['DATE_FORMATS'] = {'jp': (locale, - '%Y-%m-%d(%a)')} - page_kwargs['metadata']['lang'] = 'jp' + locale = "ja_JP.utf8" + page_kwargs["settings"]["DATE_FORMATS"] = {"jp": (locale, "%Y-%m-%d(%a)")} + page_kwargs["metadata"]["lang"] = "jp" import locale as locale_module + try: page = Page(**page_kwargs) - self.assertEqual(page.locale_date, '2015-09-13(\u65e5)') + self.assertEqual(page.locale_date, "2015-09-13(\u65e5)") except locale_module.Error: # The constructor of ``Page`` will try to set the locale to # ``ja_JP.utf8``. But this attempt will failed when there is no @@ -277,22 +288,21 @@ class TestPage(TestBase): def test_template(self): # Pages default to page, metadata overwrites default_page = Page(**self.page_kwargs) - self.assertEqual('page', default_page.template) + self.assertEqual("page", default_page.template) page_kwargs = self._copy_page_kwargs() - page_kwargs['metadata']['template'] = 'custom' + page_kwargs["metadata"]["template"] = "custom" custom_page = Page(**page_kwargs) - self.assertEqual('custom', custom_page.template) + self.assertEqual("custom", custom_page.template) def test_signal(self): def receiver_test_function(sender): receiver_test_function.has_been_called = True pass + receiver_test_function.has_been_called = False content_object_init.connect(receiver_test_function) - self.assertIn( - receiver_test_function, - content_object_init.receivers_for(Page)) + self.assertIn(receiver_test_function, content_object_init.receivers_for(Page)) self.assertFalse(receiver_test_function.has_been_called) Page(**self.page_kwargs) @@ -303,102 +313,106 @@ class TestPage(TestBase): # filenames, tags and categories. settings = get_settings() args = self.page_kwargs.copy() - args['settings'] = settings + args["settings"] = settings # Tag - args['content'] = ('A simple test, with a ' - 'link') + args["content"] = "A simple test, with a " 'link' page = Page(**args) - content = page.get_content('http://notmyidea.org') + content = page.get_content("http://notmyidea.org") self.assertEqual( content, - ('A simple test, with a ' - 'link')) + ( + "A simple test, with a " + 'link' + ), + ) # Category - args['content'] = ('A simple test, with a ' - 'link') + args["content"] = ( + "A simple test, with a " 'link' + ) page = Page(**args) - content = page.get_content('http://notmyidea.org') + content = page.get_content("http://notmyidea.org") self.assertEqual( content, - ('A simple test, with a ' - 'link')) + ( + "A simple test, with a " + 'link' + ), + ) def test_intrasite_link(self): - cls_name = '_DummyArticle' - article = type(cls_name, (object,), {'url': 'article.html'}) + cls_name = "_DummyArticle" + article = type(cls_name, (object,), {"url": "article.html"}) args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['generated_content'] = {'article.rst': article} + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["generated_content"] = {"article.rst": article} # Classic intrasite link via filename - args['content'] = ( - 'A simple test, with a ' - 'link' + args["content"] = ( + "A simple test, with a " 'link' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a ' - 'link' + "A simple test, with a " + 'link', ) # fragment - args['content'] = ( - 'A simple test, with a ' + args["content"] = ( + "A simple test, with a " 'link' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a ' - 'link' + "A simple test, with a " + 'link', ) # query - args['content'] = ( - 'A simple test, with a ' + args["content"] = ( + "A simple test, with a " 'link' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a ' + "A simple test, with a " 'link' + '?utm_whatever=234&highlight=word">link', ) # combination - args['content'] = ( - 'A simple test, with a ' + args["content"] = ( + "A simple test, with a " 'link' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a ' + "A simple test, with a " 'link' + '?utm_whatever=234&highlight=word#section-2">link', ) # also test for summary in metadata parsed = ( - 'A simple summary test, with a ' - 'link' + "A simple summary test, with a " 'link' ) linked = ( - 'A simple summary test, with a ' + "A simple summary test, with a " 'link' ) - args['settings']['FORMATTED_FIELDS'] = ['summary', 'custom'] - args['metadata']['summary'] = parsed - args['metadata']['custom'] = parsed - args['context']['localsiteurl'] = 'http://notmyidea.org' + args["settings"]["FORMATTED_FIELDS"] = ["summary", "custom"] + args["metadata"]["summary"] = parsed + args["metadata"]["custom"] = parsed + args["context"]["localsiteurl"] = "http://notmyidea.org" p = Page(**args) # This is called implicitly from all generators and Pelican.run() once # all files are processed. Here we process just one page so it needs @@ -408,252 +422,236 @@ class TestPage(TestBase): self.assertEqual(p.custom, linked) def test_intrasite_link_more(self): - cls_name = '_DummyAsset' + cls_name = "_DummyAsset" args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['static_content'] = { - 'images/poster.jpg': - type(cls_name, (object,), {'url': 'images/poster.jpg'}), - 'assets/video.mp4': - type(cls_name, (object,), {'url': 'assets/video.mp4'}), - 'images/graph.svg': - type(cls_name, (object,), {'url': 'images/graph.svg'}), + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["static_content"] = { + "images/poster.jpg": type( + cls_name, (object,), {"url": "images/poster.jpg"} + ), + "assets/video.mp4": type(cls_name, (object,), {"url": "assets/video.mp4"}), + "images/graph.svg": type(cls_name, (object,), {"url": "images/graph.svg"}), } - args['context']['generated_content'] = { - 'reference.rst': - type(cls_name, (object,), {'url': 'reference.html'}), + args["context"]["generated_content"] = { + "reference.rst": type(cls_name, (object,), {"url": "reference.html"}), } # video.poster - args['content'] = ( - 'There is a video with poster ' + args["content"] = ( + "There is a video with poster " '' + "" ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'There is a video with poster ' + "There is a video with poster " '' + "", ) # object.data - args['content'] = ( - 'There is a svg object ' + args["content"] = ( + "There is a svg object " '' - '' + "" ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'There is a svg object ' + "There is a svg object " '' - '' + "", ) # blockquote.cite - args['content'] = ( - 'There is a blockquote with cite attribute ' + args["content"] = ( + "There is a blockquote with cite attribute " '

blah blah
' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'There is a blockquote with cite attribute ' + "There is a blockquote with cite attribute " '
' - 'blah blah' - '
' + "blah blah" + "", ) def test_intrasite_link_absolute(self): """Test that absolute URLs are merged properly.""" args = self.page_kwargs.copy() - args['settings'] = get_settings( - STATIC_URL='http://static.cool.site/{path}', - ARTICLE_URL='http://blog.cool.site/{slug}.html') - args['source_path'] = 'content' - args['context']['static_content'] = { - 'images/poster.jpg': - Static('', settings=args['settings'], - source_path='images/poster.jpg'), + args["settings"] = get_settings( + STATIC_URL="http://static.cool.site/{path}", + ARTICLE_URL="http://blog.cool.site/{slug}.html", + ) + args["source_path"] = "content" + args["context"]["static_content"] = { + "images/poster.jpg": Static( + "", settings=args["settings"], source_path="images/poster.jpg" + ), } - args['context']['generated_content'] = { - 'article.rst': - Article('', settings=args['settings'], metadata={ - 'slug': 'article', 'title': 'Article'}) + args["context"]["generated_content"] = { + "article.rst": Article( + "", + settings=args["settings"], + metadata={"slug": "article", "title": "Article"}, + ) } # Article link will go to blog - args['content'] = ( - 'Article' - ) - content = Page(**args).get_content('http://cool.site') + args["content"] = 'Article' + content = Page(**args).get_content("http://cool.site") self.assertEqual( - content, - 'Article' + content, 'Article' ) # Page link will go to the main site - args['content'] = ( - 'Index' - ) - content = Page(**args).get_content('http://cool.site') + args["content"] = 'Index' + content = Page(**args).get_content("http://cool.site") + self.assertEqual(content, 'Index') + + # Image link will go to static + args["content"] = '' + content = Page(**args).get_content("http://cool.site") self.assertEqual( - content, - 'Index' + content, '' ) # Image link will go to static - args['content'] = ( - '' - ) - content = Page(**args).get_content('http://cool.site') + args["content"] = '' + content = Page(**args).get_content("http://cool.site") self.assertEqual( - content, - '' - ) - - # Image link will go to static - args['content'] = ( - '' - ) - content = Page(**args).get_content('http://cool.site') - self.assertEqual( - content, - '' + content, '' ) def test_intrasite_link_escape(self): - article = type( - '_DummyArticle', (object,), {'url': 'article-spaces.html'}) - asset = type( - '_DummyAsset', (object,), {'url': 'name@example.com'}) + article = type("_DummyArticle", (object,), {"url": "article-spaces.html"}) + asset = type("_DummyAsset", (object,), {"url": "name@example.com"}) args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['generated_content'] = {'article spaces.rst': article} - args['context']['static_content'] = {'name@example.com': asset} + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["generated_content"] = {"article spaces.rst": article} + args["context"]["static_content"] = {"name@example.com": asset} expected_output = ( - 'A simple test with a ' + "A simple test with a " 'link ' 'file' ) # not escaped - args['content'] = ( - 'A simple test with a ' + args["content"] = ( + "A simple test with a " 'link ' 'file' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual(content, expected_output) # html escaped - args['content'] = ( - 'A simple test with a ' + args["content"] = ( + "A simple test with a " 'link ' 'file' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual(content, expected_output) # url escaped - args['content'] = ( - 'A simple test with a ' + args["content"] = ( + "A simple test with a " 'link ' 'file' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual(content, expected_output) # html and url escaped - args['content'] = ( - 'A simple test with a ' + args["content"] = ( + "A simple test with a " 'link ' 'file' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual(content, expected_output) def test_intrasite_link_markdown_spaces(self): - cls_name = '_DummyArticle' - article = type(cls_name, (object,), {'url': 'article-spaces.html'}) + cls_name = "_DummyArticle" + article = type(cls_name, (object,), {"url": "article-spaces.html"}) args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['generated_content'] = {'article spaces.rst': article} + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["generated_content"] = {"article spaces.rst": article} # An intrasite link via filename with %20 as a space - args['content'] = ( - 'A simple test, with a ' - 'link' + args["content"] = ( + "A simple test, with a " 'link' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a ' - 'link' + "A simple test, with a " + 'link', ) def test_intrasite_link_source_and_generated(self): - """Test linking both to the source and the generated article - """ - cls_name = '_DummyAsset' + """Test linking both to the source and the generated article""" + cls_name = "_DummyAsset" args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['generated_content'] = { - 'article.rst': type(cls_name, (object,), {'url': 'article.html'})} - args['context']['static_content'] = { - 'article.rst': type(cls_name, (object,), {'url': 'article.rst'})} + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["generated_content"] = { + "article.rst": type(cls_name, (object,), {"url": "article.html"}) + } + args["context"]["static_content"] = { + "article.rst": type(cls_name, (object,), {"url": "article.rst"}) + } - args['content'] = ( - 'A simple test, with a link to an' + args["content"] = ( + "A simple test, with a link to an" 'article and its' 'source' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a link to an' + "A simple test, with a link to an" 'article and its' - 'source' + 'source', ) def test_intrasite_link_to_static_content_with_filename(self): - """Test linking to a static resource with deprecated {filename} - """ - cls_name = '_DummyAsset' + """Test linking to a static resource with deprecated {filename}""" + cls_name = "_DummyAsset" args = self.page_kwargs.copy() - args['settings'] = get_settings() - args['source_path'] = 'content' - args['context']['static_content'] = { - 'poster.jpg': - type(cls_name, (object,), {'url': 'images/poster.jpg'})} + args["settings"] = get_settings() + args["source_path"] = "content" + args["context"]["static_content"] = { + "poster.jpg": type(cls_name, (object,), {"url": "images/poster.jpg"}) + } - args['content'] = ( - 'A simple test, with a link to a' + args["content"] = ( + "A simple test, with a link to a" 'poster' ) - content = Page(**args).get_content('http://notmyidea.org') + content = Page(**args).get_content("http://notmyidea.org") self.assertEqual( content, - 'A simple test, with a link to a' - 'poster' + "A simple test, with a link to a" + 'poster', ) def test_multiple_authors(self): @@ -661,9 +659,11 @@ class TestPage(TestBase): args = self.page_kwargs.copy() content = Page(**args) assert content.authors == [content.author] - args['metadata'].pop('author') - args['metadata']['authors'] = [Author('First Author', DEFAULT_CONFIG), - Author('Second Author', DEFAULT_CONFIG)] + args["metadata"].pop("author") + args["metadata"]["authors"] = [ + Author("First Author", DEFAULT_CONFIG), + Author("Second Author", DEFAULT_CONFIG), + ] content = Page(**args) assert content.authors assert content.author == content.authors[0] @@ -673,173 +673,184 @@ class TestArticle(TestBase): def test_template(self): # Articles default to article, metadata overwrites default_article = Article(**self.page_kwargs) - self.assertEqual('article', default_article.template) + self.assertEqual("article", default_article.template) article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['template'] = 'custom' + article_kwargs["metadata"]["template"] = "custom" custom_article = Article(**article_kwargs) - self.assertEqual('custom', custom_article.template) + self.assertEqual("custom", custom_article.template) def test_slugify_category_author(self): settings = get_settings() - settings['SLUG_REGEX_SUBSTITUTIONS'] = [ - (r'C#', 'csharp'), - (r'[^\w\s-]', ''), - (r'(?u)\A\s*', ''), - (r'(?u)\s*\Z', ''), - (r'[-\s]+', '-'), + settings["SLUG_REGEX_SUBSTITUTIONS"] = [ + (r"C#", "csharp"), + (r"[^\w\s-]", ""), + (r"(?u)\A\s*", ""), + (r"(?u)\s*\Z", ""), + (r"[-\s]+", "-"), ] - settings['ARTICLE_URL'] = '{author}/{category}/{slug}/' - settings['ARTICLE_SAVE_AS'] = '{author}/{category}/{slug}/index.html' + settings["ARTICLE_URL"] = "{author}/{category}/{slug}/" + settings["ARTICLE_SAVE_AS"] = "{author}/{category}/{slug}/index.html" article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['author'] = Author("O'Brien", settings) - article_kwargs['metadata']['category'] = Category( - 'C# & stuff', settings) - article_kwargs['metadata']['title'] = 'fnord' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["author"] = Author("O'Brien", settings) + article_kwargs["metadata"]["category"] = Category("C# & stuff", settings) + article_kwargs["metadata"]["title"] = "fnord" + article_kwargs["settings"] = settings article = Article(**article_kwargs) - self.assertEqual(article.url, 'obrien/csharp-stuff/fnord/') - self.assertEqual( - article.save_as, 'obrien/csharp-stuff/fnord/index.html') + self.assertEqual(article.url, "obrien/csharp-stuff/fnord/") + self.assertEqual(article.save_as, "obrien/csharp-stuff/fnord/index.html") def test_slugify_with_author_substitutions(self): settings = get_settings() - 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]+", "-"), ] - settings['ARTICLE_URL'] = 'blog/{author}/{slug}/' - settings['ARTICLE_SAVE_AS'] = 'blog/{author}/{slug}/index.html' + settings["ARTICLE_URL"] = "blog/{author}/{slug}/" + settings["ARTICLE_SAVE_AS"] = "blog/{author}/{slug}/index.html" article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['author'] = Author('Alexander Todorov', - settings) - article_kwargs['metadata']['title'] = 'fnord' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["author"] = Author("Alexander Todorov", settings) + article_kwargs["metadata"]["title"] = "fnord" + article_kwargs["settings"] = settings article = Article(**article_kwargs) - self.assertEqual(article.url, 'blog/atodorov/fnord/') - self.assertEqual(article.save_as, 'blog/atodorov/fnord/index.html') + self.assertEqual(article.url, "blog/atodorov/fnord/") + self.assertEqual(article.save_as, "blog/atodorov/fnord/index.html") def test_slugify_category_with_dots(self): settings = get_settings() - settings['CATEGORY_REGEX_SUBSTITUTIONS'] = [ - ('Fedora QA', 'fedora.qa'), + settings["CATEGORY_REGEX_SUBSTITUTIONS"] = [ + ("Fedora QA", "fedora.qa"), ] - settings['ARTICLE_URL'] = '{category}/{slug}/' + settings["ARTICLE_URL"] = "{category}/{slug}/" article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['category'] = Category('Fedora QA', - settings) - article_kwargs['metadata']['title'] = 'This Week in Fedora QA' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["category"] = Category("Fedora QA", settings) + article_kwargs["metadata"]["title"] = "This Week in Fedora QA" + article_kwargs["settings"] = settings article = Article(**article_kwargs) - self.assertEqual(article.url, 'fedora.qa/this-week-in-fedora-qa/') + self.assertEqual(article.url, "fedora.qa/this-week-in-fedora-qa/") def test_valid_save_as_detects_breakout(self): settings = get_settings() article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['slug'] = '../foo' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["slug"] = "../foo" + article_kwargs["settings"] = settings article = Article(**article_kwargs) self.assertFalse(article._has_valid_save_as()) def test_valid_save_as_detects_breakout_to_root(self): settings = get_settings() article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['slug'] = '/foo' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["slug"] = "/foo" + article_kwargs["settings"] = settings article = Article(**article_kwargs) self.assertFalse(article._has_valid_save_as()) def test_valid_save_as_passes_valid(self): settings = get_settings() article_kwargs = self._copy_page_kwargs() - article_kwargs['metadata']['slug'] = 'foo' - article_kwargs['settings'] = settings + article_kwargs["metadata"]["slug"] = "foo" + article_kwargs["settings"] = settings article = Article(**article_kwargs) self.assertTrue(article._has_valid_save_as()) class TestStatic(LoggedTestCase): - def setUp(self): super().setUp() self.settings = get_settings( - STATIC_SAVE_AS='{path}', - STATIC_URL='{path}', - PAGE_SAVE_AS=os.path.join('outpages', '{slug}.html'), - PAGE_URL='outpages/{slug}.html') + STATIC_SAVE_AS="{path}", + STATIC_URL="{path}", + PAGE_SAVE_AS=os.path.join("outpages", "{slug}.html"), + PAGE_URL="outpages/{slug}.html", + ) self.context = get_context(self.settings) - self.static = Static(content=None, metadata={}, settings=self.settings, - source_path=posix_join('dir', 'foo.jpg'), - context=self.context) + self.static = Static( + content=None, + metadata={}, + settings=self.settings, + source_path=posix_join("dir", "foo.jpg"), + context=self.context, + ) - self.context['static_content'][self.static.source_path] = self.static + self.context["static_content"][self.static.source_path] = self.static def tearDown(self): pass def test_attach_to_same_dir(self): - """attach_to() overrides a static file's save_as and url. - """ + """attach_to() overrides a static file's save_as and url.""" page = Page( content="fake page", - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'fakepage.md')) + source_path=os.path.join("dir", "fakepage.md"), + ) self.static.attach_to(page) - expected_save_as = os.path.join('outpages', 'foo.jpg') + expected_save_as = os.path.join("outpages", "foo.jpg") self.assertEqual(self.static.save_as, expected_save_as) self.assertEqual(self.static.url, path_to_url(expected_save_as)) def test_attach_to_parent_dir(self): - """attach_to() preserves dirs inside the linking document dir. - """ - page = Page(content="fake page", metadata={'title': 'fakepage'}, - settings=self.settings, source_path='fakepage.md') + """attach_to() preserves dirs inside the linking document dir.""" + page = Page( + content="fake page", + metadata={"title": "fakepage"}, + settings=self.settings, + source_path="fakepage.md", + ) self.static.attach_to(page) - expected_save_as = os.path.join('outpages', 'dir', 'foo.jpg') + expected_save_as = os.path.join("outpages", "dir", "foo.jpg") self.assertEqual(self.static.save_as, expected_save_as) self.assertEqual(self.static.url, path_to_url(expected_save_as)) def test_attach_to_other_dir(self): - """attach_to() ignores dirs outside the linking document dir. - """ - page = Page(content="fake page", - metadata={'title': 'fakepage'}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md')) + """attach_to() ignores dirs outside the linking document dir.""" + page = Page( + content="fake page", + metadata={"title": "fakepage"}, + settings=self.settings, + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + ) self.static.attach_to(page) - expected_save_as = os.path.join('outpages', 'foo.jpg') + expected_save_as = os.path.join("outpages", "foo.jpg") self.assertEqual(self.static.save_as, expected_save_as) self.assertEqual(self.static.url, path_to_url(expected_save_as)) def test_attach_to_ignores_subsequent_calls(self): - """attach_to() does nothing when called a second time. - """ - page = Page(content="fake page", - metadata={'title': 'fakepage'}, settings=self.settings, - source_path=os.path.join('dir', 'fakepage.md')) + """attach_to() does nothing when called a second time.""" + page = Page( + content="fake page", + metadata={"title": "fakepage"}, + settings=self.settings, + source_path=os.path.join("dir", "fakepage.md"), + ) self.static.attach_to(page) otherdir_settings = self.settings.copy() - otherdir_settings.update(dict( - PAGE_SAVE_AS=os.path.join('otherpages', '{slug}.html'), - PAGE_URL='otherpages/{slug}.html')) + otherdir_settings.update( + dict( + PAGE_SAVE_AS=os.path.join("otherpages", "{slug}.html"), + PAGE_URL="otherpages/{slug}.html", + ) + ) otherdir_page = Page( content="other page", - metadata={'title': 'otherpage'}, + metadata={"title": "otherpage"}, settings=otherdir_settings, - source_path=os.path.join('dir', 'otherpage.md')) + source_path=os.path.join("dir", "otherpage.md"), + ) self.static.attach_to(otherdir_page) - otherdir_save_as = os.path.join('otherpages', 'foo.jpg') + otherdir_save_as = os.path.join("otherpages", "foo.jpg") self.assertNotEqual(self.static.save_as, otherdir_save_as) self.assertNotEqual(self.static.url, path_to_url(otherdir_save_as)) @@ -851,9 +862,10 @@ class TestStatic(LoggedTestCase): page = Page( content="fake page", - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'fakepage.md')) + source_path=os.path.join("dir", "fakepage.md"), + ) self.static.attach_to(page) self.assertEqual(self.static.save_as, original_save_as) @@ -867,9 +879,10 @@ class TestStatic(LoggedTestCase): page = Page( content="fake page", - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'fakepage.md')) + source_path=os.path.join("dir", "fakepage.md"), + ) self.static.attach_to(page) self.assertEqual(self.static.save_as, self.static.source_path) @@ -881,38 +894,41 @@ class TestStatic(LoggedTestCase): """ customstatic = Static( content=None, - metadata=dict(save_as='customfoo.jpg', url='customfoo.jpg'), + metadata=dict(save_as="customfoo.jpg", url="customfoo.jpg"), settings=self.settings, - source_path=os.path.join('dir', 'foo.jpg'), - context=self.settings.copy()) + source_path=os.path.join("dir", "foo.jpg"), + context=self.settings.copy(), + ) page = Page( content="fake page", - metadata={'title': 'fakepage'}, settings=self.settings, - source_path=os.path.join('dir', 'fakepage.md')) + metadata={"title": "fakepage"}, + settings=self.settings, + source_path=os.path.join("dir", "fakepage.md"), + ) customstatic.attach_to(page) - self.assertEqual(customstatic.save_as, 'customfoo.jpg') - self.assertEqual(customstatic.url, 'customfoo.jpg') + self.assertEqual(customstatic.save_as, "customfoo.jpg") + self.assertEqual(customstatic.url, "customfoo.jpg") def test_attach_link_syntax(self): - """{attach} link syntax triggers output path override & url replacement. - """ + """{attach} link syntax triggers output path override & url replacement.""" html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual( - content, html, - "{attach} link syntax did not trigger URL replacement.") + content, html, "{attach} link syntax did not trigger URL replacement." + ) - expected_save_as = os.path.join('outpages', 'foo.jpg') + expected_save_as = os.path.join("outpages", "foo.jpg") self.assertEqual(self.static.save_as, expected_save_as) self.assertEqual(self.static.url, path_to_url(expected_save_as)) @@ -922,11 +938,12 @@ class TestStatic(LoggedTestCase): html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual(content, html) @@ -936,11 +953,12 @@ class TestStatic(LoggedTestCase): html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual(content, html) @@ -950,11 +968,12 @@ class TestStatic(LoggedTestCase): html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual(content, html) @@ -964,52 +983,62 @@ class TestStatic(LoggedTestCase): html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual(content, html) - expected_html = ('link') + expected_html = ( + 'link' + ) self.assertEqual(content, expected_html) def test_unknown_link_syntax(self): "{unknown} link syntax should trigger warning." html = 'link' - page = Page(content=html, - metadata={'title': 'fakepage'}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + page = Page( + content=html, + metadata={"title": "fakepage"}, + settings=self.settings, + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertEqual(content, html) self.assertLogCountEqual( count=1, msg="Replacement Indicator 'unknown' not recognized, " - "skipping replacement", - level=logging.WARNING) + "skipping replacement", + level=logging.WARNING, + ) def test_link_to_unknown_file(self): "{filename} link to unknown file should trigger warning." html = 'link' - page = Page(content=html, - metadata={'title': 'fakepage'}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + page = Page( + content=html, + metadata={"title": "fakepage"}, + settings=self.settings, + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertEqual(content, html) self.assertLogCountEqual( count=1, msg="Unable to find 'foo', skipping url replacement.", - level=logging.WARNING) + level=logging.WARNING, + ) def test_index_link_syntax_with_spaces(self): """{index} link syntax triggers url replacement @@ -1018,18 +1047,20 @@ class TestStatic(LoggedTestCase): html = 'link' page = Page( content=html, - metadata={'title': 'fakepage'}, + metadata={"title": "fakepage"}, settings=self.settings, - source_path=os.path.join('dir', 'otherdir', 'fakepage.md'), - context=self.context) - content = page.get_content('') + source_path=os.path.join("dir", "otherdir", "fakepage.md"), + context=self.context, + ) + content = page.get_content("") self.assertNotEqual(content, html) - expected_html = ('link') + expected_html = ( + 'link' + ) self.assertEqual(content, expected_html) def test_not_save_as_draft(self): @@ -1037,12 +1068,15 @@ class TestStatic(LoggedTestCase): static = Static( content=None, - metadata=dict(status='draft',), + metadata=dict( + status="draft", + ), settings=self.settings, - source_path=os.path.join('dir', 'foo.jpg'), - context=self.settings.copy()) + source_path=os.path.join("dir", "foo.jpg"), + context=self.settings.copy(), + ) - expected_save_as = posixize_path(os.path.join('dir', 'foo.jpg')) - self.assertEqual(static.status, 'draft') + expected_save_as = posixize_path(os.path.join("dir", "foo.jpg")) + self.assertEqual(static.status, "draft") self.assertEqual(static.save_as, expected_save_as) self.assertEqual(static.url, path_to_url(expected_save_as)) diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 05c37269..52adb2c9 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -4,293 +4,383 @@ from shutil import copy, rmtree from tempfile import mkdtemp from unittest.mock import MagicMock -from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator, - PelicanTemplateNotFound, StaticGenerator, - TemplatePagesGenerator) -from pelican.tests.support import (can_symlink, get_context, get_settings, - unittest, TestCaseWithCLocale) +from pelican.generators import ( + ArticlesGenerator, + Generator, + PagesGenerator, + PelicanTemplateNotFound, + StaticGenerator, + TemplatePagesGenerator, +) +from pelican.tests.support import ( + can_symlink, + get_context, + get_settings, + unittest, + TestCaseWithCLocale, +) from pelican.writers import Writer CUR_DIR = os.path.dirname(__file__) -CONTENT_DIR = os.path.join(CUR_DIR, 'content') +CONTENT_DIR = os.path.join(CUR_DIR, "content") class TestGenerator(TestCaseWithCLocale): def setUp(self): super().setUp() self.settings = get_settings() - self.settings['READERS'] = {'asc': None} - self.generator = Generator(self.settings.copy(), self.settings, - CUR_DIR, self.settings['THEME'], None) + self.settings["READERS"] = {"asc": None} + self.generator = Generator( + self.settings.copy(), self.settings, CUR_DIR, self.settings["THEME"], None + ) def test_include_path(self): - self.settings['IGNORE_FILES'] = {'ignored1.rst', 'ignored2.rst'} + self.settings["IGNORE_FILES"] = {"ignored1.rst", "ignored2.rst"} - filename = os.path.join(CUR_DIR, 'content', 'article.rst') + filename = os.path.join(CUR_DIR, "content", "article.rst") include_path = self.generator._include_path self.assertTrue(include_path(filename)) - self.assertTrue(include_path(filename, extensions=('rst',))) - self.assertFalse(include_path(filename, extensions=('md',))) + self.assertTrue(include_path(filename, extensions=("rst",))) + self.assertFalse(include_path(filename, extensions=("md",))) - ignored_file = os.path.join(CUR_DIR, 'content', 'ignored1.rst') + ignored_file = os.path.join(CUR_DIR, "content", "ignored1.rst") self.assertFalse(include_path(ignored_file)) def test_get_files_exclude(self): - """Test that Generator.get_files() properly excludes directories. - """ + """Test that Generator.get_files() properly excludes directories.""" # We use our own Generator so we can give it our own content path generator = Generator( context=self.settings.copy(), settings=self.settings, - path=os.path.join(CUR_DIR, 'nested_content'), - theme=self.settings['THEME'], output_path=None) + path=os.path.join(CUR_DIR, "nested_content"), + theme=self.settings["THEME"], + output_path=None, + ) - filepaths = generator.get_files(paths=['maindir']) + filepaths = generator.get_files(paths=["maindir"]) found_files = {os.path.basename(f) for f in filepaths} - expected_files = {'maindir.md', 'subdir.md'} + expected_files = {"maindir.md", "subdir.md"} self.assertFalse( - expected_files - found_files, - "get_files() failed to find one or more files") + expected_files - found_files, "get_files() failed to find one or more files" + ) # Test string as `paths` argument rather than list - filepaths = generator.get_files(paths='maindir') + filepaths = generator.get_files(paths="maindir") found_files = {os.path.basename(f) for f in filepaths} - expected_files = {'maindir.md', 'subdir.md'} + expected_files = {"maindir.md", "subdir.md"} self.assertFalse( - expected_files - found_files, - "get_files() failed to find one or more files") + expected_files - found_files, "get_files() failed to find one or more files" + ) - filepaths = generator.get_files(paths=[''], exclude=['maindir']) + filepaths = generator.get_files(paths=[""], exclude=["maindir"]) found_files = {os.path.basename(f) for f in filepaths} self.assertNotIn( - 'maindir.md', found_files, - "get_files() failed to exclude a top-level directory") + "maindir.md", + found_files, + "get_files() failed to exclude a top-level directory", + ) self.assertNotIn( - 'subdir.md', found_files, - "get_files() failed to exclude a subdir of an excluded directory") + "subdir.md", + found_files, + "get_files() failed to exclude a subdir of an excluded directory", + ) filepaths = generator.get_files( - paths=[''], - exclude=[os.path.join('maindir', 'subdir')]) + paths=[""], exclude=[os.path.join("maindir", "subdir")] + ) found_files = {os.path.basename(f) for f in filepaths} self.assertNotIn( - 'subdir.md', found_files, - "get_files() failed to exclude a subdirectory") + "subdir.md", found_files, "get_files() failed to exclude a subdirectory" + ) - filepaths = generator.get_files(paths=[''], exclude=['subdir']) + filepaths = generator.get_files(paths=[""], exclude=["subdir"]) found_files = {os.path.basename(f) for f in filepaths} self.assertIn( - 'subdir.md', found_files, - "get_files() excluded a subdirectory by name, ignoring its path") + "subdir.md", + found_files, + "get_files() excluded a subdirectory by name, ignoring its path", + ) def test_custom_jinja_environment(self): """ - Test that setting the JINJA_ENVIRONMENT - properly gets set from the settings config + Test that setting the JINJA_ENVIRONMENT + properly gets set from the settings config """ settings = get_settings() - comment_start_string = 'abc' - comment_end_string = '/abc' - settings['JINJA_ENVIRONMENT'] = { - 'comment_start_string': comment_start_string, - 'comment_end_string': comment_end_string + comment_start_string = "abc" + comment_end_string = "/abc" + settings["JINJA_ENVIRONMENT"] = { + "comment_start_string": comment_start_string, + "comment_end_string": comment_end_string, } - generator = Generator(settings.copy(), settings, - CUR_DIR, settings['THEME'], None) - self.assertEqual(comment_start_string, - generator.env.comment_start_string) - self.assertEqual(comment_end_string, - generator.env.comment_end_string) + generator = Generator( + settings.copy(), settings, CUR_DIR, settings["THEME"], None + ) + self.assertEqual(comment_start_string, generator.env.comment_start_string) + self.assertEqual(comment_end_string, generator.env.comment_end_string) def test_theme_overrides(self): """ - Test that the THEME_TEMPLATES_OVERRIDES configuration setting is - utilized correctly in the Generator. + Test that the THEME_TEMPLATES_OVERRIDES configuration setting is + utilized correctly in the Generator. """ - override_dirs = (os.path.join(CUR_DIR, 'theme_overrides', 'level1'), - os.path.join(CUR_DIR, 'theme_overrides', 'level2')) - self.settings['THEME_TEMPLATES_OVERRIDES'] = override_dirs + override_dirs = ( + os.path.join(CUR_DIR, "theme_overrides", "level1"), + os.path.join(CUR_DIR, "theme_overrides", "level2"), + ) + self.settings["THEME_TEMPLATES_OVERRIDES"] = override_dirs generator = Generator( context=self.settings.copy(), settings=self.settings, path=CUR_DIR, - theme=self.settings['THEME'], - output_path=None) + theme=self.settings["THEME"], + output_path=None, + ) - filename = generator.get_template('article').filename + filename = generator.get_template("article").filename self.assertEqual(override_dirs[0], os.path.dirname(filename)) - self.assertEqual('article.html', os.path.basename(filename)) + self.assertEqual("article.html", os.path.basename(filename)) - filename = generator.get_template('authors').filename + filename = generator.get_template("authors").filename self.assertEqual(override_dirs[1], os.path.dirname(filename)) - self.assertEqual('authors.html', os.path.basename(filename)) + self.assertEqual("authors.html", os.path.basename(filename)) - filename = generator.get_template('taglist').filename - self.assertEqual(os.path.join(self.settings['THEME'], 'templates'), - os.path.dirname(filename)) + filename = generator.get_template("taglist").filename + self.assertEqual( + os.path.join(self.settings["THEME"], "templates"), os.path.dirname(filename) + ) self.assertNotIn(os.path.dirname(filename), override_dirs) - self.assertEqual('taglist.html', os.path.basename(filename)) + self.assertEqual("taglist.html", os.path.basename(filename)) def test_simple_prefix(self): """ - Test `!simple` theme prefix. + Test `!simple` theme prefix. """ - filename = self.generator.get_template('!simple/authors').filename + filename = self.generator.get_template("!simple/authors").filename expected_path = os.path.join( - os.path.dirname(CUR_DIR), 'themes', 'simple', 'templates') + os.path.dirname(CUR_DIR), "themes", "simple", "templates" + ) self.assertEqual(expected_path, os.path.dirname(filename)) - self.assertEqual('authors.html', os.path.basename(filename)) + self.assertEqual("authors.html", os.path.basename(filename)) def test_theme_prefix(self): """ - Test `!theme` theme prefix. + Test `!theme` theme prefix. """ - filename = self.generator.get_template('!theme/authors').filename + filename = self.generator.get_template("!theme/authors").filename expected_path = os.path.join( - os.path.dirname(CUR_DIR), 'themes', 'notmyidea', 'templates') + os.path.dirname(CUR_DIR), "themes", "notmyidea", "templates" + ) self.assertEqual(expected_path, os.path.dirname(filename)) - self.assertEqual('authors.html', os.path.basename(filename)) + self.assertEqual("authors.html", os.path.basename(filename)) def test_bad_prefix(self): """ - Test unknown/bad theme prefix throws exception. + Test unknown/bad theme prefix throws exception. """ - self.assertRaises(PelicanTemplateNotFound, self.generator.get_template, - '!UNKNOWN/authors') + self.assertRaises( + PelicanTemplateNotFound, self.generator.get_template, "!UNKNOWN/authors" + ) class TestArticlesGenerator(unittest.TestCase): - @classmethod def setUpClass(cls): settings = get_settings() - settings['DEFAULT_CATEGORY'] = 'Default' - settings['DEFAULT_DATE'] = (1970, 1, 1) - settings['READERS'] = {'asc': None} - settings['CACHE_CONTENT'] = False + settings["DEFAULT_CATEGORY"] = "Default" + settings["DEFAULT_DATE"] = (1970, 1, 1) + settings["READERS"] = {"asc": None} + settings["CACHE_CONTENT"] = False context = get_context(settings) cls.generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) cls.generator.generate_context() cls.articles = cls.distill_articles(cls.generator.articles) cls.drafts = cls.distill_articles(cls.generator.drafts) cls.hidden_articles = cls.distill_articles(cls.generator.hidden_articles) def setUp(self): - self.temp_cache = mkdtemp(prefix='pelican_cache.') + self.temp_cache = mkdtemp(prefix="pelican_cache.") def tearDown(self): rmtree(self.temp_cache) @staticmethod def distill_articles(articles): - return [[article.title, article.status, article.category.name, - article.template] for article in articles] + return [ + [article.title, article.status, article.category.name, article.template] + for article in articles + ] def test_generate_feeds(self): settings = get_settings() - settings['CACHE_PATH'] = self.temp_cache + settings["CACHE_PATH"] = self.temp_cache generator = ArticlesGenerator( - context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=settings, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) writer = MagicMock() generator.generate_feeds(writer) - writer.write_feed.assert_called_with([], settings, - 'feeds/all.atom.xml', - 'feeds/all.atom.xml') + writer.write_feed.assert_called_with( + [], settings, "feeds/all.atom.xml", "feeds/all.atom.xml" + ) generator = ArticlesGenerator( - context=settings, settings=get_settings(FEED_ALL_ATOM=None), - path=None, theme=settings['THEME'], output_path=None) + context=settings, + settings=get_settings(FEED_ALL_ATOM=None), + path=None, + theme=settings["THEME"], + output_path=None, + ) writer = MagicMock() generator.generate_feeds(writer) self.assertFalse(writer.write_feed.called) def test_generate_feeds_override_url(self): settings = get_settings() - settings['CACHE_PATH'] = self.temp_cache - settings['FEED_ALL_ATOM_URL'] = 'feeds/atom/all/' + settings["CACHE_PATH"] = self.temp_cache + settings["FEED_ALL_ATOM_URL"] = "feeds/atom/all/" generator = ArticlesGenerator( - context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=settings, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) writer = MagicMock() generator.generate_feeds(writer) - writer.write_feed.assert_called_with([], settings, - 'feeds/all.atom.xml', - 'feeds/atom/all/') + writer.write_feed.assert_called_with( + [], settings, "feeds/all.atom.xml", "feeds/atom/all/" + ) def test_generate_context(self): articles_expected = [ - ['Article title', 'published', 'Default', 'article'], - ['Article with markdown and summary metadata multi', 'published', - 'Default', 'article'], - ['Article with markdown and nested summary metadata', 'published', - 'Default', 'article'], - ['Article with markdown and summary metadata single', 'published', - 'Default', 'article'], - ['Article with markdown containing footnotes', 'published', - 'Default', 'article'], - ['Article with template', 'published', 'Default', 'custom'], - ['Metadata tags as list!', 'published', 'Default', 'article'], - ['Rst with filename metadata', 'published', 'yeah', 'article'], - ['One -, two --, three --- dashes!', 'published', 'Default', - 'article'], - ['One -, two --, three --- dashes!', 'published', 'Default', - 'article'], - ['Test Markdown extensions', 'published', 'Default', 'article'], - ['Test markdown File', 'published', 'test', 'article'], - ['Test md File', 'published', 'test', 'article'], - ['Test mdown File', 'published', 'test', 'article'], - ['Test metadata duplicates', 'published', 'test', 'article'], - ['Test mkd File', 'published', 'test', 'article'], - ['This is a super article !', 'published', 'Yeah', 'article'], - ['This is a super article !', 'published', 'Yeah', 'article'], - ['Article with Nonconformant HTML meta tags', 'published', - 'Default', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'yeah', 'article'], - ['This is a super article !', 'published', 'Default', 'article'], - ['Article with an inline SVG', 'published', 'Default', 'article'], - ['Article with markdown and empty tags', 'published', 'Default', - 'article'], - ['This is an article with category !', 'published', 'yeah', - 'article'], - ['This is an article with multiple authors!', 'published', - 'Default', 'article'], - ['This is an article with multiple authors!', 'published', - 'Default', 'article'], - ['This is an article with multiple authors in list format!', - 'published', 'Default', 'article'], - ['This is an article with multiple authors in lastname, ' - 'firstname format!', 'published', 'Default', 'article'], - ['This is an article without category !', 'published', 'Default', - 'article'], - ['This is an article without category !', 'published', - 'TestCategory', 'article'], - ['An Article With Code Block To Test Typogrify Ignore', - 'published', 'Default', 'article'], - ['マックOS X 10.8でパイソンとVirtualenvをインストールと設定', - 'published', '指導書', 'article'], + ["Article title", "published", "Default", "article"], + [ + "Article with markdown and summary metadata multi", + "published", + "Default", + "article", + ], + [ + "Article with markdown and nested summary metadata", + "published", + "Default", + "article", + ], + [ + "Article with markdown and summary metadata single", + "published", + "Default", + "article", + ], + [ + "Article with markdown containing footnotes", + "published", + "Default", + "article", + ], + ["Article with template", "published", "Default", "custom"], + ["Metadata tags as list!", "published", "Default", "article"], + ["Rst with filename metadata", "published", "yeah", "article"], + ["One -, two --, three --- dashes!", "published", "Default", "article"], + ["One -, two --, three --- dashes!", "published", "Default", "article"], + ["Test Markdown extensions", "published", "Default", "article"], + ["Test markdown File", "published", "test", "article"], + ["Test md File", "published", "test", "article"], + ["Test mdown File", "published", "test", "article"], + ["Test metadata duplicates", "published", "test", "article"], + ["Test mkd File", "published", "test", "article"], + ["This is a super article !", "published", "Yeah", "article"], + ["This is a super article !", "published", "Yeah", "article"], + [ + "Article with Nonconformant HTML meta tags", + "published", + "Default", + "article", + ], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "yeah", "article"], + ["This is a super article !", "published", "Default", "article"], + ["Article with an inline SVG", "published", "Default", "article"], + ["Article with markdown and empty tags", "published", "Default", "article"], + ["This is an article with category !", "published", "yeah", "article"], + [ + "This is an article with multiple authors!", + "published", + "Default", + "article", + ], + [ + "This is an article with multiple authors!", + "published", + "Default", + "article", + ], + [ + "This is an article with multiple authors in list format!", + "published", + "Default", + "article", + ], + [ + "This is an article with multiple authors in lastname, " + "firstname format!", + "published", + "Default", + "article", + ], + [ + "This is an article without category !", + "published", + "Default", + "article", + ], + [ + "This is an article without category !", + "published", + "TestCategory", + "article", + ], + [ + "An Article With Code Block To Test Typogrify Ignore", + "published", + "Default", + "article", + ], + [ + "マックOS X 10.8でパイソンとVirtualenvをインストールと設定", + "published", + "指導書", + "article", + ], ] self.assertEqual(sorted(articles_expected), sorted(self.articles)) def test_articles_draft(self): draft_articles_expected = [ - ['Draft article', 'draft', 'Default', 'article'], + ["Draft article", "draft", "Default", "article"], ] self.assertEqual(sorted(draft_articles_expected), sorted(self.drafts)) def test_articles_hidden(self): hidden_articles_expected = [ - ['Hidden article', 'hidden', 'Default', 'article'], + ["Hidden article", "hidden", "Default", "article"], ] self.assertEqual(sorted(hidden_articles_expected), sorted(self.hidden_articles)) @@ -301,27 +391,30 @@ class TestArticlesGenerator(unittest.TestCase): # terms of process order will define the name for that category categories = [cat.name for cat, _ in self.generator.categories] categories_alternatives = ( - sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書']), - sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']), + sorted(["Default", "TestCategory", "Yeah", "test", "指導書"]), + sorted(["Default", "TestCategory", "yeah", "test", "指導書"]), ) self.assertIn(sorted(categories), categories_alternatives) # test for slug categories = [cat.slug for cat, _ in self.generator.categories] - categories_expected = ['default', 'testcategory', 'yeah', 'test', - 'zhi-dao-shu'] + categories_expected = ["default", "testcategory", "yeah", "test", "zhi-dao-shu"] self.assertEqual(sorted(categories), sorted(categories_expected)) def test_do_not_use_folder_as_category(self): settings = get_settings() - settings['DEFAULT_CATEGORY'] = 'Default' - settings['DEFAULT_DATE'] = (1970, 1, 1) - settings['USE_FOLDER_AS_CATEGORY'] = False - settings['CACHE_PATH'] = self.temp_cache - settings['READERS'] = {'asc': None} + settings["DEFAULT_CATEGORY"] = "Default" + settings["DEFAULT_DATE"] = (1970, 1, 1) + settings["USE_FOLDER_AS_CATEGORY"] = False + settings["CACHE_PATH"] = self.temp_cache + settings["READERS"] = {"asc": None} context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() # test for name # categories are grouped by slug; if two categories have the same slug @@ -329,61 +422,79 @@ class TestArticlesGenerator(unittest.TestCase): # terms of process order will define the name for that category categories = [cat.name for cat, _ in generator.categories] categories_alternatives = ( - sorted(['Default', 'Yeah', 'test', '指導書']), - sorted(['Default', 'yeah', 'test', '指導書']), + sorted(["Default", "Yeah", "test", "指導書"]), + sorted(["Default", "yeah", "test", "指導書"]), ) self.assertIn(sorted(categories), categories_alternatives) # test for slug categories = [cat.slug for cat, _ in generator.categories] - categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu'] + categories_expected = ["default", "yeah", "test", "zhi-dao-shu"] self.assertEqual(sorted(categories), sorted(categories_expected)) def test_direct_templates_save_as_url_default(self): - settings = get_settings() - settings['CACHE_PATH'] = self.temp_cache + settings["CACHE_PATH"] = self.temp_cache context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) write = MagicMock() generator.generate_direct_templates(write) - write.assert_called_with("archives.html", - generator.get_template("archives"), context, - articles=generator.articles, - dates=generator.dates, blog=True, - template_name='archives', - page_name='archives', url="archives.html") + write.assert_called_with( + "archives.html", + generator.get_template("archives"), + context, + articles=generator.articles, + dates=generator.dates, + blog=True, + template_name="archives", + page_name="archives", + url="archives.html", + ) def test_direct_templates_save_as_url_modified(self): - settings = get_settings() - settings['DIRECT_TEMPLATES'] = ['archives'] - settings['ARCHIVES_SAVE_AS'] = 'archives/index.html' - settings['ARCHIVES_URL'] = 'archives/' - settings['CACHE_PATH'] = self.temp_cache + settings["DIRECT_TEMPLATES"] = ["archives"] + settings["ARCHIVES_SAVE_AS"] = "archives/index.html" + settings["ARCHIVES_URL"] = "archives/" + settings["CACHE_PATH"] = self.temp_cache generator = ArticlesGenerator( - context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=settings, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) write = MagicMock() generator.generate_direct_templates(write) - write.assert_called_with("archives/index.html", - generator.get_template("archives"), settings, - articles=generator.articles, - dates=generator.dates, blog=True, - template_name='archives', - page_name='archives/index', - url="archives/") + write.assert_called_with( + "archives/index.html", + generator.get_template("archives"), + settings, + articles=generator.articles, + dates=generator.dates, + blog=True, + template_name="archives", + page_name="archives/index", + url="archives/", + ) def test_direct_templates_save_as_false(self): - settings = get_settings() - settings['DIRECT_TEMPLATES'] = ['archives'] - settings['ARCHIVES_SAVE_AS'] = False - settings['CACHE_PATH'] = self.temp_cache + settings["DIRECT_TEMPLATES"] = ["archives"] + settings["ARCHIVES_SAVE_AS"] = False + settings["CACHE_PATH"] = self.temp_cache generator = ArticlesGenerator( - context=settings, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=settings, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) write = MagicMock() generator.generate_direct_templates(write) self.assertEqual(write.call_count, 0) @@ -392,10 +503,13 @@ class TestArticlesGenerator(unittest.TestCase): """ Custom template articles get the field but standard/unset are None """ - custom_template = ['Article with template', 'published', 'Default', - 'custom'] - standard_template = ['This is a super article !', 'published', 'Yeah', - 'article'] + custom_template = ["Article with template", "published", "Default", "custom"] + standard_template = [ + "This is a super article !", + "published", + "Yeah", + "article", + ] self.assertIn(custom_template, self.articles) self.assertIn(standard_template, self.articles) @@ -403,126 +517,135 @@ class TestArticlesGenerator(unittest.TestCase): """Test correctness of the period_archives context values.""" settings = get_settings() - settings['CACHE_PATH'] = self.temp_cache + settings["CACHE_PATH"] = self.temp_cache # No period archives enabled: context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() - period_archives = generator.context['period_archives'] + period_archives = generator.context["period_archives"] self.assertEqual(len(period_archives.items()), 0) # Year archives enabled: - settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' - settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/' + settings["YEAR_ARCHIVE_SAVE_AS"] = "posts/{date:%Y}/index.html" + settings["YEAR_ARCHIVE_URL"] = "posts/{date:%Y}/" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() - period_archives = generator.context['period_archives'] + period_archives = generator.context["period_archives"] abbreviated_archives = { - granularity: {period['period'] for period in periods} + granularity: {period["period"] for period in periods} for granularity, periods in period_archives.items() } - expected = {'year': {(1970,), (2010,), (2012,), (2014,)}} + expected = {"year": {(1970,), (2010,), (2012,), (2014,)}} self.assertEqual(expected, abbreviated_archives) # Month archives enabled: - settings['MONTH_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/index.html' - settings['MONTH_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/' + settings["MONTH_ARCHIVE_SAVE_AS"] = "posts/{date:%Y}/{date:%b}/index.html" + settings["MONTH_ARCHIVE_URL"] = "posts/{date:%Y}/{date:%b}/" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() - period_archives = generator.context['period_archives'] + period_archives = generator.context["period_archives"] abbreviated_archives = { - granularity: {period['period'] for period in periods} + granularity: {period["period"] for period in periods} for granularity, periods in period_archives.items() } expected = { - 'year': {(1970,), (2010,), (2012,), (2014,)}, - 'month': { - (1970, 'January'), - (2010, 'December'), - (2012, 'December'), - (2012, 'November'), - (2012, 'October'), - (2014, 'February'), + "year": {(1970,), (2010,), (2012,), (2014,)}, + "month": { + (1970, "January"), + (2010, "December"), + (2012, "December"), + (2012, "November"), + (2012, "October"), + (2014, "February"), }, } self.assertEqual(expected, abbreviated_archives) # Day archives enabled: - settings['DAY_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' - settings['DAY_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/' + settings[ + "DAY_ARCHIVE_SAVE_AS" + ] = "posts/{date:%Y}/{date:%b}/{date:%d}/index.html" + settings["DAY_ARCHIVE_URL"] = "posts/{date:%Y}/{date:%b}/{date:%d}/" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() - period_archives = generator.context['period_archives'] + period_archives = generator.context["period_archives"] abbreviated_archives = { - granularity: {period['period'] for period in periods} + granularity: {period["period"] for period in periods} for granularity, periods in period_archives.items() } expected = { - 'year': {(1970,), (2010,), (2012,), (2014,)}, - 'month': { - (1970, 'January'), - (2010, 'December'), - (2012, 'December'), - (2012, 'November'), - (2012, 'October'), - (2014, 'February'), + "year": {(1970,), (2010,), (2012,), (2014,)}, + "month": { + (1970, "January"), + (2010, "December"), + (2012, "December"), + (2012, "November"), + (2012, "October"), + (2014, "February"), }, - 'day': { - (1970, 'January', 1), - (2010, 'December', 2), - (2012, 'December', 20), - (2012, 'November', 29), - (2012, 'October', 30), - (2012, 'October', 31), - (2014, 'February', 9), + "day": { + (1970, "January", 1), + (2010, "December", 2), + (2012, "December", 20), + (2012, "November", 29), + (2012, "October", 30), + (2012, "October", 31), + (2014, "February", 9), }, } self.assertEqual(expected, abbreviated_archives) # Further item values tests filtered_archives = [ - p for p in period_archives['day'] - if p['period'] == (2014, 'February', 9) + p for p in period_archives["day"] if p["period"] == (2014, "February", 9) ] self.assertEqual(len(filtered_archives), 1) sample_archive = filtered_archives[0] - self.assertEqual(sample_archive['period_num'], (2014, 2, 9)) - self.assertEqual( - sample_archive['save_as'], 'posts/2014/Feb/09/index.html') - self.assertEqual( - sample_archive['url'], 'posts/2014/Feb/09/') + self.assertEqual(sample_archive["period_num"], (2014, 2, 9)) + self.assertEqual(sample_archive["save_as"], "posts/2014/Feb/09/index.html") + self.assertEqual(sample_archive["url"], "posts/2014/Feb/09/") articles = [ - d for d in generator.articles if - d.date.year == 2014 and - d.date.month == 2 and - d.date.day == 9 + d + for d in generator.articles + if d.date.year == 2014 and d.date.month == 2 and d.date.day == 9 ] - self.assertEqual(len(sample_archive['articles']), len(articles)) + self.assertEqual(len(sample_archive["articles"]), len(articles)) dates = [ - d for d in generator.dates if - d.date.year == 2014 and - d.date.month == 2 and - d.date.day == 9 + d + for d in generator.dates + if d.date.year == 2014 and d.date.month == 2 and d.date.day == 9 ] - self.assertEqual(len(sample_archive['dates']), len(dates)) - self.assertEqual(sample_archive['dates'][0].title, dates[0].title) - self.assertEqual(sample_archive['dates'][0].date, dates[0].date) + self.assertEqual(len(sample_archive["dates"]), len(dates)) + self.assertEqual(sample_archive["dates"][0].title, dates[0].title) + self.assertEqual(sample_archive["dates"][0].date, dates[0].date) def test_period_in_timeperiod_archive(self): """ @@ -531,13 +654,17 @@ class TestArticlesGenerator(unittest.TestCase): """ settings = get_settings() - settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' - settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/' - settings['CACHE_PATH'] = self.temp_cache + settings["YEAR_ARCHIVE_SAVE_AS"] = "posts/{date:%Y}/index.html" + settings["YEAR_ARCHIVE_URL"] = "posts/{date:%Y}/" + settings["CACHE_PATH"] = self.temp_cache context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) @@ -547,196 +674,257 @@ class TestArticlesGenerator(unittest.TestCase): # among other things it must have at least been called with this context["period"] = (1970,) context["period_num"] = (1970,) - write.assert_called_with("posts/1970/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/", - all_articles=generator.articles) + write.assert_called_with( + "posts/1970/index.html", + generator.get_template("period_archives"), + context, + blog=True, + articles=articles, + dates=dates, + template_name="period_archives", + url="posts/1970/", + all_articles=generator.articles, + ) - settings['MONTH_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/index.html' - settings['MONTH_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/' + settings["MONTH_ARCHIVE_SAVE_AS"] = "posts/{date:%Y}/{date:%b}/index.html" + settings["MONTH_ARCHIVE_URL"] = "posts/{date:%Y}/{date:%b}/" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) - generator.generate_context() - write = MagicMock() - generator.generate_period_archives(write) - dates = [d for d in generator.dates - if d.date.year == 1970 and d.date.month == 1] - articles = [d for d in generator.articles - if d.date.year == 1970 and d.date.month == 1] - self.assertEqual(len(dates), 1) - context["period"] = (1970, "January") - context["period_num"] = (1970, 1) - # among other things it must have at least been called with this - write.assert_called_with("posts/1970/Jan/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/Jan/", - all_articles=generator.articles) - - settings['DAY_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' - settings['DAY_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/' - context = get_context(settings) - generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() write = MagicMock() generator.generate_period_archives(write) dates = [ - d for d in generator.dates if - d.date.year == 1970 and - d.date.month == 1 and - d.date.day == 1 + d for d in generator.dates if d.date.year == 1970 and d.date.month == 1 ] articles = [ - d for d in generator.articles if - d.date.year == 1970 and - d.date.month == 1 and - d.date.day == 1 + d for d in generator.articles if d.date.year == 1970 and d.date.month == 1 + ] + self.assertEqual(len(dates), 1) + context["period"] = (1970, "January") + context["period_num"] = (1970, 1) + # among other things it must have at least been called with this + write.assert_called_with( + "posts/1970/Jan/index.html", + generator.get_template("period_archives"), + context, + blog=True, + articles=articles, + dates=dates, + template_name="period_archives", + url="posts/1970/Jan/", + all_articles=generator.articles, + ) + + settings[ + "DAY_ARCHIVE_SAVE_AS" + ] = "posts/{date:%Y}/{date:%b}/{date:%d}/index.html" + settings["DAY_ARCHIVE_URL"] = "posts/{date:%Y}/{date:%b}/{date:%d}/" + context = get_context(settings) + generator = ArticlesGenerator( + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) + generator.generate_context() + write = MagicMock() + generator.generate_period_archives(write) + dates = [ + d + for d in generator.dates + if d.date.year == 1970 and d.date.month == 1 and d.date.day == 1 + ] + articles = [ + d + for d in generator.articles + if d.date.year == 1970 and d.date.month == 1 and d.date.day == 1 ] self.assertEqual(len(dates), 1) context["period"] = (1970, "January", 1) context["period_num"] = (1970, 1, 1) # among other things it must have at least been called with this - write.assert_called_with("posts/1970/Jan/01/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/Jan/01/", - all_articles=generator.articles) + write.assert_called_with( + "posts/1970/Jan/01/index.html", + generator.get_template("period_archives"), + context, + blog=True, + articles=articles, + dates=dates, + template_name="period_archives", + url="posts/1970/Jan/01/", + all_articles=generator.articles, + ) def test_nonexistent_template(self): """Attempt to load a non-existent template""" settings = get_settings() context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=None, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=None, + theme=settings["THEME"], + output_path=None, + ) self.assertRaises(Exception, generator.get_template, "not_a_template") def test_generate_authors(self): """Check authors generation.""" authors = [author.name for author, _ in self.generator.authors] authors_expected = sorted( - ['Alexis Métaireau', 'Author, First', 'Author, Second', - 'First Author', 'Second Author']) + [ + "Alexis Métaireau", + "Author, First", + "Author, Second", + "First Author", + "Second Author", + ] + ) self.assertEqual(sorted(authors), authors_expected) # test for slug authors = [author.slug for author, _ in self.generator.authors] - authors_expected = ['alexis-metaireau', 'author-first', - 'author-second', 'first-author', 'second-author'] + authors_expected = [ + "alexis-metaireau", + "author-first", + "author-second", + "first-author", + "second-author", + ] self.assertEqual(sorted(authors), sorted(authors_expected)) def test_standard_metadata_in_default_metadata(self): settings = get_settings() - settings['CACHE_CONTENT'] = False - settings['DEFAULT_CATEGORY'] = 'Default' - settings['DEFAULT_DATE'] = (1970, 1, 1) - settings['DEFAULT_METADATA'] = (('author', 'Blogger'), - # category will be ignored in favor of - # DEFAULT_CATEGORY - ('category', 'Random'), - ('tags', 'general, untagged')) + settings["CACHE_CONTENT"] = False + settings["DEFAULT_CATEGORY"] = "Default" + settings["DEFAULT_DATE"] = (1970, 1, 1) + settings["DEFAULT_METADATA"] = ( + ("author", "Blogger"), + # category will be ignored in favor of + # DEFAULT_CATEGORY + ("category", "Random"), + ("tags", "general, untagged"), + ) context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() authors = sorted([author.name for author, _ in generator.authors]) - authors_expected = sorted(['Alexis Métaireau', 'Blogger', - 'Author, First', 'Author, Second', - 'First Author', 'Second Author']) + authors_expected = sorted( + [ + "Alexis Métaireau", + "Blogger", + "Author, First", + "Author, Second", + "First Author", + "Second Author", + ] + ) self.assertEqual(authors, authors_expected) - categories = sorted([category.name - for category, _ in generator.categories]) + categories = sorted([category.name for category, _ in generator.categories]) categories_expected = [ - sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']), - sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書'])] + sorted(["Default", "TestCategory", "yeah", "test", "指導書"]), + sorted(["Default", "TestCategory", "Yeah", "test", "指導書"]), + ] self.assertIn(categories, categories_expected) tags = sorted([tag.name for tag in generator.tags]) - tags_expected = sorted(['bar', 'foo', 'foobar', 'general', 'untagged', - 'パイソン', 'マック']) + tags_expected = sorted( + ["bar", "foo", "foobar", "general", "untagged", "パイソン", "マック"] + ) self.assertEqual(tags, tags_expected) def test_article_order_by(self): settings = get_settings() - settings['DEFAULT_CATEGORY'] = 'Default' - settings['DEFAULT_DATE'] = (1970, 1, 1) - settings['ARTICLE_ORDER_BY'] = 'title' + settings["DEFAULT_CATEGORY"] = "Default" + settings["DEFAULT_DATE"] = (1970, 1, 1) + settings["ARTICLE_ORDER_BY"] = "title" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() expected = [ - 'An Article With Code Block To Test Typogrify Ignore', - 'Article title', - 'Article with Nonconformant HTML meta tags', - 'Article with an inline SVG', - 'Article with markdown and empty tags', - 'Article with markdown and nested summary metadata', - 'Article with markdown and summary metadata multi', - 'Article with markdown and summary metadata single', - 'Article with markdown containing footnotes', - 'Article with template', - 'Metadata tags as list!', - 'One -, two --, three --- dashes!', - 'One -, two --, three --- dashes!', - 'Rst with filename metadata', - 'Test Markdown extensions', - 'Test markdown File', - 'Test md File', - 'Test mdown File', - 'Test metadata duplicates', - 'Test mkd File', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is a super article !', - 'This is an article with category !', - ('This is an article with multiple authors in lastname, ' - 'firstname format!'), - 'This is an article with multiple authors in list format!', - 'This is an article with multiple authors!', - 'This is an article with multiple authors!', - 'This is an article without category !', - 'This is an article without category !', - 'マックOS X 10.8でパイソンとVirtualenvをインストールと設定'] + "An Article With Code Block To Test Typogrify Ignore", + "Article title", + "Article with Nonconformant HTML meta tags", + "Article with an inline SVG", + "Article with markdown and empty tags", + "Article with markdown and nested summary metadata", + "Article with markdown and summary metadata multi", + "Article with markdown and summary metadata single", + "Article with markdown containing footnotes", + "Article with template", + "Metadata tags as list!", + "One -, two --, three --- dashes!", + "One -, two --, three --- dashes!", + "Rst with filename metadata", + "Test Markdown extensions", + "Test markdown File", + "Test md File", + "Test mdown File", + "Test metadata duplicates", + "Test mkd File", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is a super article !", + "This is an article with category !", + ( + "This is an article with multiple authors in lastname, " + "firstname format!" + ), + "This is an article with multiple authors in list format!", + "This is an article with multiple authors!", + "This is an article with multiple authors!", + "This is an article without category !", + "This is an article without category !", + "マックOS X 10.8でパイソンとVirtualenvをインストールと設定", + ] articles = [article.title for article in generator.articles] self.assertEqual(articles, expected) # reversed title settings = get_settings() - settings['DEFAULT_CATEGORY'] = 'Default' - settings['DEFAULT_DATE'] = (1970, 1, 1) - settings['ARTICLE_ORDER_BY'] = 'reversed-title' + settings["DEFAULT_CATEGORY"] = "Default" + settings["DEFAULT_DATE"] = (1970, 1, 1) + settings["ARTICLE_ORDER_BY"] = "reversed-title" context = get_context(settings) generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CONTENT_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() articles = [article.title for article in generator.articles] @@ -750,7 +938,7 @@ class TestPageGenerator(unittest.TestCase): # to match expected def setUp(self): - self.temp_cache = mkdtemp(prefix='pelican_cache.') + self.temp_cache = mkdtemp(prefix="pelican_cache.") def tearDown(self): rmtree(self.temp_cache) @@ -760,112 +948,125 @@ class TestPageGenerator(unittest.TestCase): def test_generate_context(self): settings = get_settings() - settings['CACHE_PATH'] = self.temp_cache - settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR - settings['DEFAULT_DATE'] = (1970, 1, 1) + settings["CACHE_PATH"] = self.temp_cache + settings["PAGE_PATHS"] = ["TestPages"] # relative to CUR_DIR + settings["DEFAULT_DATE"] = (1970, 1, 1) context = get_context(settings) generator = PagesGenerator( - context=context, settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CUR_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() pages = self.distill_pages(generator.pages) hidden_pages = self.distill_pages(generator.hidden_pages) draft_pages = self.distill_pages(generator.draft_pages) pages_expected = [ - ['This is a test page', 'published', 'page'], - ['This is a markdown test page', 'published', 'page'], - ['This is a test page with a preset template', 'published', - 'custom'], - ['Page with a bunch of links', 'published', 'page'], - ['Page with static links', 'published', 'page'], - ['A Page (Test) for sorting', 'published', 'page'], + ["This is a test page", "published", "page"], + ["This is a markdown test page", "published", "page"], + ["This is a test page with a preset template", "published", "custom"], + ["Page with a bunch of links", "published", "page"], + ["Page with static links", "published", "page"], + ["A Page (Test) for sorting", "published", "page"], ] hidden_pages_expected = [ - ['This is a test hidden page', 'hidden', 'page'], - ['This is a markdown test hidden page', 'hidden', 'page'], - ['This is a test hidden page with a custom template', 'hidden', - 'custom'], + ["This is a test hidden page", "hidden", "page"], + ["This is a markdown test hidden page", "hidden", "page"], + ["This is a test hidden page with a custom template", "hidden", "custom"], ] draft_pages_expected = [ - ['This is a test draft page', 'draft', 'page'], - ['This is a markdown test draft page', 'draft', 'page'], - ['This is a test draft page with a custom template', 'draft', - 'custom'], + ["This is a test draft page", "draft", "page"], + ["This is a markdown test draft page", "draft", "page"], + ["This is a test draft page with a custom template", "draft", "custom"], ] self.assertEqual(sorted(pages_expected), sorted(pages)) self.assertEqual( sorted(pages_expected), - sorted(self.distill_pages(generator.context['pages']))) + sorted(self.distill_pages(generator.context["pages"])), + ) self.assertEqual(sorted(hidden_pages_expected), sorted(hidden_pages)) self.assertEqual(sorted(draft_pages_expected), sorted(draft_pages)) self.assertEqual( sorted(hidden_pages_expected), - sorted(self.distill_pages(generator.context['hidden_pages']))) + sorted(self.distill_pages(generator.context["hidden_pages"])), + ) self.assertEqual( sorted(draft_pages_expected), - sorted(self.distill_pages(generator.context['draft_pages']))) + sorted(self.distill_pages(generator.context["draft_pages"])), + ) def test_generate_sorted(self): settings = get_settings() - settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR - settings['CACHE_PATH'] = self.temp_cache - settings['DEFAULT_DATE'] = (1970, 1, 1) + settings["PAGE_PATHS"] = ["TestPages"] # relative to CUR_DIR + settings["CACHE_PATH"] = self.temp_cache + settings["DEFAULT_DATE"] = (1970, 1, 1) context = get_context(settings) # default sort (filename) pages_expected_sorted_by_filename = [ - ['This is a test page', 'published', 'page'], - ['This is a markdown test page', 'published', 'page'], - ['A Page (Test) for sorting', 'published', 'page'], - ['Page with a bunch of links', 'published', 'page'], - ['Page with static links', 'published', 'page'], - ['This is a test page with a preset template', 'published', - 'custom'], + ["This is a test page", "published", "page"], + ["This is a markdown test page", "published", "page"], + ["A Page (Test) for sorting", "published", "page"], + ["Page with a bunch of links", "published", "page"], + ["Page with static links", "published", "page"], + ["This is a test page with a preset template", "published", "custom"], ] generator = PagesGenerator( - context=context, settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CUR_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() pages = self.distill_pages(generator.pages) self.assertEqual(pages_expected_sorted_by_filename, pages) # sort by title pages_expected_sorted_by_title = [ - ['A Page (Test) for sorting', 'published', 'page'], - ['Page with a bunch of links', 'published', 'page'], - ['Page with static links', 'published', 'page'], - ['This is a markdown test page', 'published', 'page'], - ['This is a test page', 'published', 'page'], - ['This is a test page with a preset template', 'published', - 'custom'], + ["A Page (Test) for sorting", "published", "page"], + ["Page with a bunch of links", "published", "page"], + ["Page with static links", "published", "page"], + ["This is a markdown test page", "published", "page"], + ["This is a test page", "published", "page"], + ["This is a test page with a preset template", "published", "custom"], ] - settings['PAGE_ORDER_BY'] = 'title' + settings["PAGE_ORDER_BY"] = "title" 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() pages = self.distill_pages(generator.pages) self.assertEqual(pages_expected_sorted_by_title, pages) # sort by title reversed pages_expected_sorted_by_title = [ - ['This is a test page with a preset template', 'published', - 'custom'], - ['This is a test page', 'published', 'page'], - ['This is a markdown test page', 'published', 'page'], - ['Page with static links', 'published', 'page'], - ['Page with a bunch of links', 'published', 'page'], - ['A Page (Test) for sorting', 'published', 'page'], + ["This is a test page with a preset template", "published", "custom"], + ["This is a test page", "published", "page"], + ["This is a markdown test page", "published", "page"], + ["Page with static links", "published", "page"], + ["Page with a bunch of links", "published", "page"], + ["A Page (Test) for sorting", "published", "page"], ] - settings['PAGE_ORDER_BY'] = 'reversed-title' + settings["PAGE_ORDER_BY"] = "reversed-title" context = get_context(settings) generator = PagesGenerator( - context=context, settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CUR_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() pages = self.distill_pages(generator.pages) self.assertEqual(pages_expected_sorted_by_title, pages) @@ -876,18 +1077,22 @@ class TestPageGenerator(unittest.TestCase): are generated correctly on pages """ settings = get_settings() - settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR - settings['CACHE_PATH'] = self.temp_cache - settings['DEFAULT_DATE'] = (1970, 1, 1) + settings["PAGE_PATHS"] = ["TestPages"] # relative to CUR_DIR + settings["CACHE_PATH"] = self.temp_cache + settings["DEFAULT_DATE"] = (1970, 1, 1) context = get_context(settings) generator = PagesGenerator( - context=context, settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CUR_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() pages_by_title = {p.title: p for p in generator.pages} - test_content = pages_by_title['Page with a bunch of links'].content + test_content = pages_by_title["Page with a bunch of links"].content self.assertIn('', test_content) self.assertIn('', test_content) @@ -897,80 +1102,80 @@ class TestPageGenerator(unittest.TestCase): are included in context['static_links'] """ settings = get_settings() - settings['PAGE_PATHS'] = ['TestPages/page_with_static_links.md'] - settings['CACHE_PATH'] = self.temp_cache - settings['DEFAULT_DATE'] = (1970, 1, 1) + settings["PAGE_PATHS"] = ["TestPages/page_with_static_links.md"] + settings["CACHE_PATH"] = self.temp_cache + settings["DEFAULT_DATE"] = (1970, 1, 1) context = get_context(settings) generator = PagesGenerator( - context=context, settings=settings, - path=CUR_DIR, theme=settings['THEME'], output_path=None) + context=context, + settings=settings, + path=CUR_DIR, + theme=settings["THEME"], + output_path=None, + ) generator.generate_context() - self.assertIn('pelican/tests/TestPages/image0.jpg', - context['static_links']) - self.assertIn('pelican/tests/TestPages/image1.jpg', - context['static_links']) + self.assertIn("pelican/tests/TestPages/image0.jpg", context["static_links"]) + self.assertIn("pelican/tests/TestPages/image1.jpg", context["static_links"]) class TestTemplatePagesGenerator(TestCaseWithCLocale): - TEMPLATE_CONTENT = "foo: {{ foo }}" def setUp(self): super().setUp() - self.temp_content = mkdtemp(prefix='pelicantests.') - self.temp_output = mkdtemp(prefix='pelicantests.') + self.temp_content = mkdtemp(prefix="pelicantests.") + self.temp_output = mkdtemp(prefix="pelicantests.") def tearDown(self): rmtree(self.temp_content) rmtree(self.temp_output) def test_generate_output(self): - settings = get_settings() - settings['STATIC_PATHS'] = ['static'] - settings['TEMPLATE_PAGES'] = { - 'template/source.html': 'generated/file.html' - } + settings["STATIC_PATHS"] = ["static"] + settings["TEMPLATE_PAGES"] = {"template/source.html": "generated/file.html"} generator = TemplatePagesGenerator( - context={'foo': 'bar'}, settings=settings, - path=self.temp_content, theme='', output_path=self.temp_output) + context={"foo": "bar"}, + settings=settings, + path=self.temp_content, + theme="", + output_path=self.temp_output, + ) # create a dummy template file - template_dir = os.path.join(self.temp_content, 'template') - template_path = os.path.join(template_dir, 'source.html') + template_dir = os.path.join(self.temp_content, "template") + template_path = os.path.join(template_dir, "source.html") os.makedirs(template_dir) - with open(template_path, 'w') as template_file: + with open(template_path, "w") as template_file: template_file.write(self.TEMPLATE_CONTENT) writer = Writer(self.temp_output, settings=settings) generator.generate_output(writer) - output_path = os.path.join(self.temp_output, 'generated', 'file.html') + output_path = os.path.join(self.temp_output, "generated", "file.html") # output file has been generated self.assertTrue(os.path.exists(output_path)) # output content is correct with open(output_path) as output_file: - self.assertEqual(output_file.read(), 'foo: bar') + self.assertEqual(output_file.read(), "foo: bar") class TestStaticGenerator(unittest.TestCase): - def setUp(self): - self.content_path = os.path.join(CUR_DIR, 'mixed_content') - self.temp_content = mkdtemp(prefix='testcontent.') - self.temp_output = mkdtemp(prefix='testoutput.') + self.content_path = os.path.join(CUR_DIR, "mixed_content") + self.temp_content = mkdtemp(prefix="testcontent.") + self.temp_output = mkdtemp(prefix="testoutput.") self.settings = get_settings() - self.settings['PATH'] = self.temp_content - self.settings['STATIC_PATHS'] = ["static"] - self.settings['OUTPUT_PATH'] = self.temp_output + self.settings["PATH"] = self.temp_content + self.settings["STATIC_PATHS"] = ["static"] + self.settings["OUTPUT_PATH"] = self.temp_output os.mkdir(os.path.join(self.temp_content, "static")) - self.startfile = os.path.join(self.temp_content, - "static", "staticfile") + self.startfile = os.path.join(self.temp_content, "static", "staticfile") self.endfile = os.path.join(self.temp_output, "static", "staticfile") self.generator = StaticGenerator( context=get_context(), @@ -978,7 +1183,7 @@ class TestStaticGenerator(unittest.TestCase): path=self.temp_content, theme="", output_path=self.temp_output, - ) + ) def tearDown(self): rmtree(self.temp_content) @@ -989,155 +1194,198 @@ class TestStaticGenerator(unittest.TestCase): def test_theme_static_paths_dirs(self): """Test that StaticGenerator properly copies also files mentioned in - TEMPLATE_STATIC_PATHS, not just directories.""" + TEMPLATE_STATIC_PATHS, not just directories.""" settings = get_settings(PATH=self.content_path) context = get_context(settings, staticfiles=[]) StaticGenerator( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_output(None) + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_output(None) # The content of dirs listed in THEME_STATIC_PATHS (defaulting to # "static") is put into the output - self.assertTrue(os.path.isdir(os.path.join(self.temp_output, - "theme/css/"))) - self.assertTrue(os.path.isdir(os.path.join(self.temp_output, - "theme/fonts/"))) + self.assertTrue(os.path.isdir(os.path.join(self.temp_output, "theme/css/"))) + self.assertTrue(os.path.isdir(os.path.join(self.temp_output, "theme/fonts/"))) def test_theme_static_paths_files(self): """Test that StaticGenerator properly copies also files mentioned in - TEMPLATE_STATIC_PATHS, not just directories.""" + TEMPLATE_STATIC_PATHS, not just directories.""" settings = get_settings( PATH=self.content_path, - THEME_STATIC_PATHS=['static/css/fonts.css', 'static/fonts/'],) + THEME_STATIC_PATHS=["static/css/fonts.css", "static/fonts/"], + ) context = get_context(settings, staticfiles=[]) StaticGenerator( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_output(None) + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_output(None) # Only the content of dirs and files listed in THEME_STATIC_PATHS are # put into the output, not everything from static/ - self.assertFalse(os.path.isdir(os.path.join(self.temp_output, - "theme/css/"))) - self.assertFalse(os.path.isdir(os.path.join(self.temp_output, - "theme/fonts/"))) + self.assertFalse(os.path.isdir(os.path.join(self.temp_output, "theme/css/"))) + self.assertFalse(os.path.isdir(os.path.join(self.temp_output, "theme/fonts/"))) - self.assertTrue(os.path.isfile(os.path.join( - self.temp_output, "theme/Yanone_Kaffeesatz_400.eot"))) - self.assertTrue(os.path.isfile(os.path.join( - self.temp_output, "theme/Yanone_Kaffeesatz_400.svg"))) - self.assertTrue(os.path.isfile(os.path.join( - self.temp_output, "theme/Yanone_Kaffeesatz_400.ttf"))) - self.assertTrue(os.path.isfile(os.path.join( - self.temp_output, "theme/Yanone_Kaffeesatz_400.woff"))) - self.assertTrue(os.path.isfile(os.path.join( - self.temp_output, "theme/Yanone_Kaffeesatz_400.woff2"))) - self.assertTrue(os.path.isfile(os.path.join(self.temp_output, - "theme/font.css"))) - self.assertTrue(os.path.isfile(os.path.join(self.temp_output, - "theme/fonts.css"))) + self.assertTrue( + os.path.isfile( + os.path.join(self.temp_output, "theme/Yanone_Kaffeesatz_400.eot") + ) + ) + self.assertTrue( + os.path.isfile( + os.path.join(self.temp_output, "theme/Yanone_Kaffeesatz_400.svg") + ) + ) + self.assertTrue( + os.path.isfile( + os.path.join(self.temp_output, "theme/Yanone_Kaffeesatz_400.ttf") + ) + ) + self.assertTrue( + os.path.isfile( + os.path.join(self.temp_output, "theme/Yanone_Kaffeesatz_400.woff") + ) + ) + self.assertTrue( + os.path.isfile( + os.path.join(self.temp_output, "theme/Yanone_Kaffeesatz_400.woff2") + ) + ) + self.assertTrue( + os.path.isfile(os.path.join(self.temp_output, "theme/font.css")) + ) + self.assertTrue( + os.path.isfile(os.path.join(self.temp_output, "theme/fonts.css")) + ) def test_static_excludes(self): - """Test that StaticGenerator respects STATIC_EXCLUDES. - """ + """Test that StaticGenerator respects STATIC_EXCLUDES.""" settings = get_settings( - STATIC_EXCLUDES=['subdir'], + STATIC_EXCLUDES=["subdir"], PATH=self.content_path, - STATIC_PATHS=[''],) + STATIC_PATHS=[""], + ) context = get_context(settings) StaticGenerator( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_context() + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_context() - staticnames = [os.path.basename(c.source_path) - for c in context['staticfiles']] + staticnames = [os.path.basename(c.source_path) for c in context["staticfiles"]] self.assertNotIn( - 'subdir_fake_image.jpg', staticnames, - "StaticGenerator processed a file in a STATIC_EXCLUDES directory") + "subdir_fake_image.jpg", + staticnames, + "StaticGenerator processed a file in a STATIC_EXCLUDES directory", + ) self.assertIn( - 'fake_image.jpg', staticnames, - "StaticGenerator skipped a file that it should have included") + "fake_image.jpg", + staticnames, + "StaticGenerator skipped a file that it should have included", + ) def test_static_exclude_sources(self): - """Test that StaticGenerator respects STATIC_EXCLUDE_SOURCES. - """ + """Test that StaticGenerator respects STATIC_EXCLUDE_SOURCES.""" settings = get_settings( STATIC_EXCLUDE_SOURCES=True, PATH=self.content_path, - PAGE_PATHS=[''], - STATIC_PATHS=[''], - CACHE_CONTENT=False,) + PAGE_PATHS=[""], + STATIC_PATHS=[""], + CACHE_CONTENT=False, + ) context = get_context(settings) for generator_class in (PagesGenerator, StaticGenerator): generator_class( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_context() + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_context() - staticnames = [os.path.basename(c.source_path) - for c in context['staticfiles']] + staticnames = [os.path.basename(c.source_path) for c in context["staticfiles"]] self.assertFalse( any(name.endswith(".md") for name in staticnames), - "STATIC_EXCLUDE_SOURCES=True failed to exclude a markdown file") + "STATIC_EXCLUDE_SOURCES=True failed to exclude a markdown file", + ) settings.update(STATIC_EXCLUDE_SOURCES=False) context = get_context(settings) for generator_class in (PagesGenerator, StaticGenerator): generator_class( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_context() + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_context() - staticnames = [os.path.basename(c.source_path) - for c in context['staticfiles']] + staticnames = [os.path.basename(c.source_path) for c in context["staticfiles"]] self.assertTrue( any(name.endswith(".md") for name in staticnames), - "STATIC_EXCLUDE_SOURCES=False failed to include a markdown file") + "STATIC_EXCLUDE_SOURCES=False failed to include a markdown file", + ) def test_static_links(self): - """Test that StaticGenerator uses files in static_links - """ + """Test that StaticGenerator uses files in static_links""" settings = get_settings( - STATIC_EXCLUDES=['subdir'], + STATIC_EXCLUDES=["subdir"], PATH=self.content_path, - STATIC_PATHS=[],) + STATIC_PATHS=[], + ) context = get_context(settings) - context['static_links'] |= {'short_page.md', 'subdir_fake_image.jpg'} + context["static_links"] |= {"short_page.md", "subdir_fake_image.jpg"} StaticGenerator( - context=context, settings=settings, - path=settings['PATH'], output_path=self.temp_output, - theme=settings['THEME']).generate_context() + context=context, + settings=settings, + path=settings["PATH"], + output_path=self.temp_output, + theme=settings["THEME"], + ).generate_context() staticfiles_names = [ - os.path.basename(c.source_path) for c in context['staticfiles']] + os.path.basename(c.source_path) for c in context["staticfiles"] + ] - static_content_names = [ - os.path.basename(c) for c in context['static_content']] + static_content_names = [os.path.basename(c) for c in context["static_content"]] self.assertIn( - 'short_page.md', staticfiles_names, - "StaticGenerator skipped a file that it should have included") + "short_page.md", + staticfiles_names, + "StaticGenerator skipped a file that it should have included", + ) self.assertIn( - 'short_page.md', static_content_names, - "StaticGenerator skipped a file that it should have included") + "short_page.md", + static_content_names, + "StaticGenerator skipped a file that it should have included", + ) self.assertIn( - 'subdir_fake_image.jpg', staticfiles_names, - "StaticGenerator skipped a file that it should have included") + "subdir_fake_image.jpg", + staticfiles_names, + "StaticGenerator skipped a file that it should have included", + ) self.assertIn( - 'subdir_fake_image.jpg', static_content_names, - "StaticGenerator skipped a file that it should have included") + "subdir_fake_image.jpg", + static_content_names, + "StaticGenerator skipped a file that it should have included", + ) def test_copy_one_file(self): with open(self.startfile, "w") as f: @@ -1160,7 +1408,7 @@ class TestStaticGenerator(unittest.TestCase): staticfile = MagicMock() staticfile.source_path = self.startfile staticfile.save_as = self.endfile - self.settings['STATIC_CHECK_IF_MODIFIED'] = True + self.settings["STATIC_CHECK_IF_MODIFIED"] = True with open(staticfile.source_path, "w") as f: f.write("a") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1181,7 +1429,7 @@ class TestStaticGenerator(unittest.TestCase): self.assertTrue(isnewer) def test_skip_file_when_source_is_not_newer(self): - self.settings['STATIC_CHECK_IF_MODIFIED'] = True + self.settings["STATIC_CHECK_IF_MODIFIED"] = True with open(self.startfile, "w") as f: f.write("staticcontent") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1201,7 +1449,7 @@ class TestStaticGenerator(unittest.TestCase): self.assertFalse(os.path.samefile(self.startfile, self.endfile)) def test_output_file_is_linked_to_source(self): - self.settings['STATIC_CREATE_LINKS'] = True + self.settings["STATIC_CREATE_LINKS"] = True with open(self.startfile, "w") as f: f.write("staticcontent") self.generator.generate_context() @@ -1209,7 +1457,7 @@ class TestStaticGenerator(unittest.TestCase): self.assertTrue(os.path.samefile(self.startfile, self.endfile)) def test_output_file_exists_and_is_newer(self): - self.settings['STATIC_CREATE_LINKS'] = True + self.settings["STATIC_CREATE_LINKS"] = True with open(self.startfile, "w") as f: f.write("staticcontent") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1219,9 +1467,9 @@ class TestStaticGenerator(unittest.TestCase): self.generator.generate_output(None) self.assertTrue(os.path.samefile(self.startfile, self.endfile)) - @unittest.skipUnless(can_symlink(), 'No symlink privilege') + @unittest.skipUnless(can_symlink(), "No symlink privilege") def test_can_symlink_when_hardlink_not_possible(self): - self.settings['STATIC_CREATE_LINKS'] = True + self.settings["STATIC_CREATE_LINKS"] = True with open(self.startfile, "w") as f: f.write("staticcontent") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1230,9 +1478,9 @@ class TestStaticGenerator(unittest.TestCase): self.generator.generate_output(None) self.assertTrue(os.path.islink(self.endfile)) - @unittest.skipUnless(can_symlink(), 'No symlink privilege') + @unittest.skipUnless(can_symlink(), "No symlink privilege") def test_existing_symlink_is_considered_up_to_date(self): - self.settings['STATIC_CREATE_LINKS'] = True + self.settings["STATIC_CREATE_LINKS"] = True with open(self.startfile, "w") as f: f.write("staticcontent") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1243,9 +1491,9 @@ class TestStaticGenerator(unittest.TestCase): requires_update = self.generator._file_update_required(staticfile) self.assertFalse(requires_update) - @unittest.skipUnless(can_symlink(), 'No symlink privilege') + @unittest.skipUnless(can_symlink(), "No symlink privilege") def test_invalid_symlink_is_overwritten(self): - self.settings['STATIC_CREATE_LINKS'] = True + self.settings["STATIC_CREATE_LINKS"] = True with open(self.startfile, "w") as f: f.write("staticcontent") os.mkdir(os.path.join(self.temp_output, "static")) @@ -1263,14 +1511,14 @@ class TestStaticGenerator(unittest.TestCase): # os.path.realpath is broken on Windows before python3.8 for symlinks. # This is a (ugly) workaround. # see: https://bugs.python.org/issue9949 - if os.name == 'nt' and sys.version_info < (3, 8): + if os.name == "nt" and sys.version_info < (3, 8): + def get_real_path(path): return os.readlink(path) if os.path.islink(path) else path else: get_real_path = os.path.realpath - self.assertEqual(get_real_path(self.endfile), - get_real_path(self.startfile)) + self.assertEqual(get_real_path(self.endfile), get_real_path(self.startfile)) def test_delete_existing_file_before_mkdir(self): with open(self.startfile, "w") as f: @@ -1279,16 +1527,14 @@ class TestStaticGenerator(unittest.TestCase): f.write("This file should be a directory") self.generator.generate_context() self.generator.generate_output(None) - self.assertTrue( - os.path.isdir(os.path.join(self.temp_output, "static"))) + self.assertTrue(os.path.isdir(os.path.join(self.temp_output, "static"))) self.assertTrue(os.path.isfile(self.endfile)) class TestJinja2Environment(TestCaseWithCLocale): - def setUp(self): - self.temp_content = mkdtemp(prefix='pelicantests.') - self.temp_output = mkdtemp(prefix='pelicantests.') + self.temp_content = mkdtemp(prefix="pelicantests.") + self.temp_output = mkdtemp(prefix="pelicantests.") def tearDown(self): rmtree(self.temp_content) @@ -1296,27 +1542,29 @@ class TestJinja2Environment(TestCaseWithCLocale): def _test_jinja2_helper(self, additional_settings, content, expected): settings = get_settings() - settings['STATIC_PATHS'] = ['static'] - settings['TEMPLATE_PAGES'] = { - 'template/source.html': 'generated/file.html' - } + settings["STATIC_PATHS"] = ["static"] + settings["TEMPLATE_PAGES"] = {"template/source.html": "generated/file.html"} settings.update(additional_settings) generator = TemplatePagesGenerator( - context={'foo': 'foo', 'bar': 'bar'}, settings=settings, - path=self.temp_content, theme='', output_path=self.temp_output) + context={"foo": "foo", "bar": "bar"}, + settings=settings, + path=self.temp_content, + theme="", + output_path=self.temp_output, + ) # create a dummy template file - template_dir = os.path.join(self.temp_content, 'template') - template_path = os.path.join(template_dir, 'source.html') + template_dir = os.path.join(self.temp_content, "template") + template_path = os.path.join(template_dir, "source.html") os.makedirs(template_dir) - with open(template_path, 'w') as template_file: + with open(template_path, "w") as template_file: template_file.write(content) writer = Writer(self.temp_output, settings=settings) generator.generate_output(writer) - output_path = os.path.join(self.temp_output, 'generated', 'file.html') + output_path = os.path.join(self.temp_output, "generated", "file.html") # output file has been generated self.assertTrue(os.path.exists(output_path)) @@ -1327,32 +1575,32 @@ class TestJinja2Environment(TestCaseWithCLocale): def test_jinja2_filter(self): """JINJA_FILTERS adds custom filters to Jinja2 environment""" - content = 'foo: {{ foo|custom_filter }}, bar: {{ bar|custom_filter }}' - settings = {'JINJA_FILTERS': {'custom_filter': lambda x: x.upper()}} - expected = 'foo: FOO, bar: BAR' + content = "foo: {{ foo|custom_filter }}, bar: {{ bar|custom_filter }}" + settings = {"JINJA_FILTERS": {"custom_filter": lambda x: x.upper()}} + expected = "foo: FOO, bar: BAR" self._test_jinja2_helper(settings, content, expected) def test_jinja2_test(self): """JINJA_TESTS adds custom tests to Jinja2 environment""" - content = 'foo {{ foo is custom_test }}, bar {{ bar is custom_test }}' - settings = {'JINJA_TESTS': {'custom_test': lambda x: x == 'bar'}} - expected = 'foo False, bar True' + content = "foo {{ foo is custom_test }}, bar {{ bar is custom_test }}" + settings = {"JINJA_TESTS": {"custom_test": lambda x: x == "bar"}} + expected = "foo False, bar True" self._test_jinja2_helper(settings, content, expected) def test_jinja2_global(self): """JINJA_GLOBALS adds custom globals to Jinja2 environment""" - content = '{{ custom_global }}' - settings = {'JINJA_GLOBALS': {'custom_global': 'foobar'}} - expected = 'foobar' + content = "{{ custom_global }}" + settings = {"JINJA_GLOBALS": {"custom_global": "foobar"}} + expected = "foobar" self._test_jinja2_helper(settings, content, expected) def test_jinja2_extension(self): """JINJA_ENVIRONMENT adds extensions to Jinja2 environment""" - content = '{% set stuff = [] %}{% do stuff.append(1) %}{{ stuff }}' - settings = {'JINJA_ENVIRONMENT': {'extensions': ['jinja2.ext.do']}} - expected = '[1]' + content = "{% set stuff = [] %}{% do stuff.append(1) %}{{ stuff }}" + settings = {"JINJA_ENVIRONMENT": {"extensions": ["jinja2.ext.do"]}} + expected = "[1]" self._test_jinja2_helper(settings, content, expected) diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 870d3001..05ef5bbd 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -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(' entities in " - "the title. You can't miss them.") - self.assertNotIn('&', title) + self.assertTrue( + title, + "A normal post with some 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 ![text](src) # pandoc 3.x converts into
src
text
@@ -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', - '\n', - '2019-11-07-a-slug', '2019-11-07 21:26:40+0000', 'testy', ['photo'], - ['economics'], 'published', 'article', 'html')], + [ + ( + "Photo", + '\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": "

Caption

", "player": [ - { - "width": 250, - "embed_code": - "" - }, - { - "width": 400, - "embed_code": - "" - }, - { - "width": 500, - "embed_code": - "" - } + {"width": 250, "embed_code": ""}, + {"width": 400, "embed_code": ""}, + {"width": 500, "embed_code": ""}, ], "video_type": "youtube", } - ] + ] + get.side_effect = get_posts posts = list(tumblr2fields("api_key", "blogname")) self.assertEqual( - [('youtube.com', - '

via

\n

Caption

' - '\n' - '\n' - '\n', - '2017-07-07-the-slug', - '2017-07-07 20:31:41+0000', 'testy', ['video'], [], 'published', - 'article', 'html')], + [ + ( + "youtube.com", + '

via

\n

Caption

' + "\n" + "\n" + "\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": - "

Caption

", + "caption": "

Caption

", "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', - '

via

\n

Caption

' - '

(This video isn\'t available anymore.)

\n', - '2016-08-14-the-slug', - '2016-08-14 16:37:35+0000', 'testy', ['video'], ['interviews'], - 'published', 'article', 'html')], + [ + ( + "youtube.com", + '

via

\n

Caption

' + "

(This video isn't available anymore.)

\n", + "2016-08-14-the-slug", + "2016-08-14 16:37:35+0000", + "testy", + ["video"], + ["interviews"], + "published", + "article", + "html", + ) + ], posts, - posts) + posts, + ) diff --git a/pelican/tests/test_log.py b/pelican/tests/test_log.py index 1f2fb83a..8791fc7c 100644 --- a/pelican/tests/test_log.py +++ b/pelican/tests/test_log.py @@ -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 + ) diff --git a/pelican/tests/test_paginator.py b/pelican/tests/test_paginator.py index f8233eb4..2160421f 100644 --- a/pelican/tests/test_paginator.py +++ b/pelican/tests/test_paginator.py @@ -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/") diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index 885c2138..3c0c0572 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -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 diff --git a/pelican/tests/test_plugins.py b/pelican/tests/test_plugins.py index 348c3e94..4f02022c 100644 --- a/pelican/tests/test_plugins.py +++ b/pelican/tests/test_plugins.py @@ -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..NoopPlugin') + "PluginTest.test_get_plugin_name..NoopPlugin", + ) self.assertEqual( get_plugin_name(NoopPlugin()), - 'PluginTest.test_get_plugin_name..NoopPlugin') + "PluginTest.test_get_plugin_name..NoopPlugin", + ) diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py index 753a353d..cf0f39f1 100644 --- a/pelican/tests/test_readers.py +++ b/pelican/tests/test_readers.py @@ -7,7 +7,7 @@ from pelican.utils import SafeDatetime CUR_DIR = os.path.dirname(__file__) -CONTENT_PATH = os.path.join(CUR_DIR, 'content') +CONTENT_PATH = os.path.join(CUR_DIR, "content") def _path(*args): @@ -15,7 +15,6 @@ def _path(*args): class ReaderTest(unittest.TestCase): - def read_file(self, path, **kwargs): # Isolate from future API changes to readers.read_file @@ -29,26 +28,24 @@ class ReaderTest(unittest.TestCase): self.assertEqual( value, real_value, - 'Expected %s to have value %s, but was %s' % - (key, value, real_value)) + "Expected %s to have value %s, but was %s" + % (key, value, real_value), + ) else: self.fail( - 'Expected %s to have value %s, but was not in Dict' % - (key, value)) + "Expected %s to have value %s, but was not in Dict" % (key, value) + ) class TestAssertDictHasSubset(ReaderTest): def setUp(self): - self.dictionary = { - 'key-a': 'val-a', - 'key-b': 'val-b' - } + self.dictionary = {"key-a": "val-a", "key-b": "val-b"} def tearDown(self): self.dictionary = None def test_subset(self): - self.assertDictHasSubset(self.dictionary, {'key-a': 'val-a'}) + self.assertDictHasSubset(self.dictionary, {"key-a": "val-a"}) def test_equal(self): self.assertDictHasSubset(self.dictionary, self.dictionary) @@ -56,269 +53,260 @@ class TestAssertDictHasSubset(ReaderTest): def test_fail_not_set(self): self.assertRaisesRegex( AssertionError, - r'Expected.*key-c.*to have value.*val-c.*but was not in Dict', + r"Expected.*key-c.*to have value.*val-c.*but was not in Dict", self.assertDictHasSubset, self.dictionary, - {'key-c': 'val-c'}) + {"key-c": "val-c"}, + ) def test_fail_wrong_val(self): self.assertRaisesRegex( AssertionError, - r'Expected .*key-a.* to have value .*val-b.* but was .*val-a.*', + r"Expected .*key-a.* to have value .*val-b.* but was .*val-a.*", self.assertDictHasSubset, self.dictionary, - {'key-a': 'val-b'}) + {"key-a": "val-b"}, + ) class DefaultReaderTest(ReaderTest): - def test_readfile_unknown_extension(self): with self.assertRaises(TypeError): - self.read_file(path='article_with_metadata.unknownextension') + self.read_file(path="article_with_metadata.unknownextension") def test_readfile_path_metadata_implicit_dates(self): - test_file = 'article_with_metadata_implicit_dates.html' - page = self.read_file(path=test_file, DEFAULT_DATE='fs') + test_file = "article_with_metadata_implicit_dates.html" + page = self.read_file(path=test_file, DEFAULT_DATE="fs") expected = { - 'date': SafeDatetime.fromtimestamp( - os.stat(_path(test_file)).st_mtime), - 'modified': SafeDatetime.fromtimestamp( - os.stat(_path(test_file)).st_mtime) + "date": SafeDatetime.fromtimestamp(os.stat(_path(test_file)).st_mtime), + "modified": SafeDatetime.fromtimestamp(os.stat(_path(test_file)).st_mtime), } self.assertDictHasSubset(page.metadata, expected) def test_readfile_path_metadata_explicit_dates(self): - test_file = 'article_with_metadata_explicit_dates.html' - page = self.read_file(path=test_file, DEFAULT_DATE='fs') + test_file = "article_with_metadata_explicit_dates.html" + page = self.read_file(path=test_file, DEFAULT_DATE="fs") expected = { - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'modified': SafeDatetime(2010, 12, 31, 23, 59) + "date": SafeDatetime(2010, 12, 2, 10, 14), + "modified": SafeDatetime(2010, 12, 31, 23, 59), } self.assertDictHasSubset(page.metadata, expected) def test_readfile_path_metadata_implicit_date_explicit_modified(self): - test_file = 'article_with_metadata_implicit_date_explicit_modified.html' - page = self.read_file(path=test_file, DEFAULT_DATE='fs') + test_file = "article_with_metadata_implicit_date_explicit_modified.html" + page = self.read_file(path=test_file, DEFAULT_DATE="fs") expected = { - 'date': SafeDatetime.fromtimestamp( - os.stat(_path(test_file)).st_mtime), - 'modified': SafeDatetime(2010, 12, 2, 10, 14), + "date": SafeDatetime.fromtimestamp(os.stat(_path(test_file)).st_mtime), + "modified": SafeDatetime(2010, 12, 2, 10, 14), } self.assertDictHasSubset(page.metadata, expected) def test_readfile_path_metadata_explicit_date_implicit_modified(self): - test_file = 'article_with_metadata_explicit_date_implicit_modified.html' - page = self.read_file(path=test_file, DEFAULT_DATE='fs') + test_file = "article_with_metadata_explicit_date_implicit_modified.html" + page = self.read_file(path=test_file, DEFAULT_DATE="fs") expected = { - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'modified': SafeDatetime.fromtimestamp( - os.stat(_path(test_file)).st_mtime) + "date": SafeDatetime(2010, 12, 2, 10, 14), + "modified": SafeDatetime.fromtimestamp(os.stat(_path(test_file)).st_mtime), } self.assertDictHasSubset(page.metadata, expected) def test_find_empty_alt(self): - with patch('pelican.readers.logger') as log_mock: - content = ['', - ''] + with patch("pelican.readers.logger") as log_mock: + content = [ + '', + '', + ] for tag in content: - readers.find_empty_alt(tag, '/test/path') + readers.find_empty_alt(tag, "/test/path") log_mock.warning.assert_called_with( - 'Empty alt attribute for image %s in %s', - 'test-image.png', - '/test/path', - extra={'limit_msg': - 'Other images have empty alt attributes'} + "Empty alt attribute for image %s in %s", + "test-image.png", + "/test/path", + extra={"limit_msg": "Other images have empty alt attributes"}, ) class RstReaderTest(ReaderTest): - def test_article_with_metadata(self): - page = self.read_file(path='article_with_metadata.rst') + page = self.read_file(path="article_with_metadata.rst") expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'This is a super article !', - 'summary': '

Multi-line metadata should be' - ' supported\nas well as inline' - ' markup and stuff to "typogrify' - '"...

\n', - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'modified': SafeDatetime(2010, 12, 2, 10, 20), - 'tags': ['foo', 'bar', 'foobar'], - 'custom_field': 'http://notmyidea.org', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "This is a super article !", + "summary": '

Multi-line metadata should be' + " supported\nas well as inline" + " markup and stuff to "typogrify" + ""...

\n", + "date": SafeDatetime(2010, 12, 2, 10, 14), + "modified": SafeDatetime(2010, 12, 2, 10, 20), + "tags": ["foo", "bar", "foobar"], + "custom_field": "http://notmyidea.org", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_capitalized_metadata(self): - page = self.read_file(path='article_with_capitalized_metadata.rst') + page = self.read_file(path="article_with_capitalized_metadata.rst") expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'This is a super article !', - 'summary': '

Multi-line metadata should be' - ' supported\nas well as inline' - ' markup and stuff to "typogrify' - '"...

\n', - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'modified': SafeDatetime(2010, 12, 2, 10, 20), - 'tags': ['foo', 'bar', 'foobar'], - 'custom_field': 'http://notmyidea.org', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "This is a super article !", + "summary": '

Multi-line metadata should be' + " supported\nas well as inline" + " markup and stuff to "typogrify" + ""...

\n", + "date": SafeDatetime(2010, 12, 2, 10, 14), + "modified": SafeDatetime(2010, 12, 2, 10, 20), + "tags": ["foo", "bar", "foobar"], + "custom_field": "http://notmyidea.org", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_filename_metadata(self): page = self.read_file( - path='2012-11-29_rst_w_filename_meta#foo-bar.rst', - FILENAME_METADATA=None) + path="2012-11-29_rst_w_filename_meta#foo-bar.rst", FILENAME_METADATA=None + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'Rst with filename metadata', - 'reader': 'rst', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "Rst with filename metadata", + "reader": "rst", } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='2012-11-29_rst_w_filename_meta#foo-bar.rst', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2}).*') + path="2012-11-29_rst_w_filename_meta#foo-bar.rst", + FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2}).*", + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'Rst with filename metadata', - 'date': SafeDatetime(2012, 11, 29), - 'reader': 'rst', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "Rst with filename metadata", + "date": SafeDatetime(2012, 11, 29), + "reader": "rst", } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='2012-11-29_rst_w_filename_meta#foo-bar.rst', + path="2012-11-29_rst_w_filename_meta#foo-bar.rst", FILENAME_METADATA=( - r'(?P\d{4}-\d{2}-\d{2})' - r'_(?P.*)' - r'#(?P.*)-(?P.*)')) + r"(?P\d{4}-\d{2}-\d{2})" + r"_(?P.*)" + r"#(?P.*)-(?P.*)" + ), + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'Rst with filename metadata', - 'date': SafeDatetime(2012, 11, 29), - 'slug': 'rst_w_filename_meta', - 'mymeta': 'foo', - 'reader': 'rst', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "Rst with filename metadata", + "date": SafeDatetime(2012, 11, 29), + "slug": "rst_w_filename_meta", + "mymeta": "foo", + "reader": "rst", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_optional_filename_metadata(self): page = self.read_file( - path='2012-11-29_rst_w_filename_meta#foo-bar.rst', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?') + path="2012-11-29_rst_w_filename_meta#foo-bar.rst", + FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2})?", + ) expected = { - 'date': SafeDatetime(2012, 11, 29), - 'reader': 'rst', + "date": SafeDatetime(2012, 11, 29), + "reader": "rst", } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='article.rst', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?') + path="article.rst", FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2})?" + ) expected = { - 'reader': 'rst', + "reader": "rst", } self.assertDictHasSubset(page.metadata, expected) - self.assertNotIn('date', page.metadata, 'Date should not be set.') + self.assertNotIn("date", page.metadata, "Date should not be set.") def test_article_metadata_key_lowercase(self): # Keys of metadata should be lowercase. reader = readers.RstReader(settings=get_settings()) - content, metadata = reader.read( - _path('article_with_uppercase_metadata.rst')) + content, metadata = reader.read(_path("article_with_uppercase_metadata.rst")) - self.assertIn('category', metadata, 'Key should be lowercase.') - self.assertEqual('Yeah', metadata.get('category'), - 'Value keeps case.') + self.assertIn("category", metadata, "Key should be lowercase.") + self.assertEqual("Yeah", metadata.get("category"), "Value keeps case.") def test_article_extra_path_metadata(self): - input_with_metadata = '2012-11-29_rst_w_filename_meta#foo-bar.rst' + input_with_metadata = "2012-11-29_rst_w_filename_meta#foo-bar.rst" page_metadata = self.read_file( path=input_with_metadata, FILENAME_METADATA=( - r'(?P\d{4}-\d{2}-\d{2})' - r'_(?P.*)' - r'#(?P.*)-(?P.*)' + r"(?P\d{4}-\d{2}-\d{2})" + r"_(?P.*)" + r"#(?P.*)-(?P.*)" ), EXTRA_PATH_METADATA={ - input_with_metadata: { - 'key-1a': 'value-1a', - 'key-1b': 'value-1b' - } - } + input_with_metadata: {"key-1a": "value-1a", "key-1b": "value-1b"} + }, ) expected_metadata = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'Rst with filename metadata', - 'date': SafeDatetime(2012, 11, 29), - 'slug': 'rst_w_filename_meta', - 'mymeta': 'foo', - 'reader': 'rst', - 'key-1a': 'value-1a', - 'key-1b': 'value-1b' + "category": "yeah", + "author": "Alexis Métaireau", + "title": "Rst with filename metadata", + "date": SafeDatetime(2012, 11, 29), + "slug": "rst_w_filename_meta", + "mymeta": "foo", + "reader": "rst", + "key-1a": "value-1a", + "key-1b": "value-1b", } self.assertDictHasSubset(page_metadata.metadata, expected_metadata) - input_file_path_without_metadata = 'article.rst' + input_file_path_without_metadata = "article.rst" page_without_metadata = self.read_file( path=input_file_path_without_metadata, EXTRA_PATH_METADATA={ - input_file_path_without_metadata: { - 'author': 'Charlès Overwrite' - } - } + input_file_path_without_metadata: {"author": "Charlès Overwrite"} + }, ) expected_without_metadata = { - 'category': 'misc', - 'author': 'Charlès Overwrite', - 'title': 'Article title', - 'reader': 'rst', + "category": "misc", + "author": "Charlès Overwrite", + "title": "Article title", + "reader": "rst", } self.assertDictHasSubset( - page_without_metadata.metadata, - expected_without_metadata) + page_without_metadata.metadata, expected_without_metadata + ) def test_article_extra_path_metadata_dont_overwrite(self): # EXTRA_PATH_METADATA['author'] should get ignored # since we don't overwrite already set values - input_file_path = '2012-11-29_rst_w_filename_meta#foo-bar.rst' + input_file_path = "2012-11-29_rst_w_filename_meta#foo-bar.rst" page = self.read_file( path=input_file_path, FILENAME_METADATA=( - r'(?P\d{4}-\d{2}-\d{2})' - r'_(?P.*)' - r'#(?P.*)-(?P.*)' + r"(?P\d{4}-\d{2}-\d{2})" + r"_(?P.*)" + r"#(?P.*)-(?P.*)" ), EXTRA_PATH_METADATA={ - input_file_path: { - 'author': 'Charlès Overwrite', - 'key-1b': 'value-1b' - } - } + input_file_path: {"author": "Charlès Overwrite", "key-1b": "value-1b"} + }, ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'Rst with filename metadata', - 'date': SafeDatetime(2012, 11, 29), - 'slug': 'rst_w_filename_meta', - 'mymeta': 'foo', - 'reader': 'rst', - 'key-1b': 'value-1b' + "category": "yeah", + "author": "Alexis Métaireau", + "title": "Rst with filename metadata", + "date": SafeDatetime(2012, 11, 29), + "slug": "rst_w_filename_meta", + "mymeta": "foo", + "reader": "rst", + "key-1b": "value-1b", } self.assertDictHasSubset(page.metadata, expected) @@ -328,15 +316,19 @@ class RstReaderTest(ReaderTest): path = "TestCategory/article_without_category.rst" epm = { - parent: {'epmr_inherit': parent, - 'epmr_override': parent, }, - notparent: {'epmr_bogus': notparent}, - path: {'epmr_override': path, }, - } + parent: { + "epmr_inherit": parent, + "epmr_override": parent, + }, + notparent: {"epmr_bogus": notparent}, + path: { + "epmr_override": path, + }, + } expected_metadata = { - 'epmr_inherit': parent, - 'epmr_override': path, - } + "epmr_inherit": parent, + "epmr_override": path, + } page = self.read_file(path=path, EXTRA_PATH_METADATA=epm) self.assertDictHasSubset(page.metadata, expected_metadata) @@ -357,152 +349,157 @@ class RstReaderTest(ReaderTest): def test_typogrify(self): # if nothing is specified in the settings, the content should be # unmodified - page = self.read_file(path='article.rst') - expected = ('

THIS is some content. With some stuff to ' - '"typogrify"...

\n

Now with added ' - 'support for ' - 'TLA.

\n') + page = self.read_file(path="article.rst") + expected = ( + "

THIS is some content. With some stuff to " + ""typogrify"...

\n

Now with added " + 'support for ' + "TLA.

\n" + ) self.assertEqual(page.content, expected) try: # otherwise, typogrify should be applied - page = self.read_file(path='article.rst', TYPOGRIFY=True) + page = self.read_file(path="article.rst", TYPOGRIFY=True) expected = ( '

THIS is some content. ' - 'With some stuff to “typogrify”…

\n' + "With some stuff to “typogrify”…

\n" '

Now with added support for TLA.

\n') + 'acronym">TLA.

\n' + ) self.assertEqual(page.content, expected) except ImportError: - return unittest.skip('need the typogrify distribution') + return unittest.skip("need the typogrify distribution") def test_typogrify_summary(self): # if nothing is specified in the settings, the summary should be # unmodified - page = self.read_file(path='article_with_metadata.rst') - expected = ('

Multi-line metadata should be' - ' supported\nas well as inline' - ' markup and stuff to "typogrify' - '"...

\n') + page = self.read_file(path="article_with_metadata.rst") + expected = ( + '

Multi-line metadata should be' + " supported\nas well as inline" + " markup and stuff to "typogrify" + ""...

\n" + ) - self.assertEqual(page.metadata['summary'], expected) + self.assertEqual(page.metadata["summary"], expected) try: # otherwise, typogrify should be applied - page = self.read_file(path='article_with_metadata.rst', - TYPOGRIFY=True) - expected = ('

Multi-line metadata should be' - ' supported\nas well as inline' - ' markup and stuff to “typogrify' - '”…

\n') + page = self.read_file(path="article_with_metadata.rst", TYPOGRIFY=True) + expected = ( + '

Multi-line metadata should be' + " supported\nas well as inline" + " markup and stuff to “typogrify" + "”…

\n" + ) - self.assertEqual(page.metadata['summary'], expected) + self.assertEqual(page.metadata["summary"], expected) except ImportError: - return unittest.skip('need the typogrify distribution') + return unittest.skip("need the typogrify distribution") def test_typogrify_ignore_tags(self): try: # typogrify should be able to ignore user specified tags, # but tries to be clever with widont extension - page = self.read_file(path='article.rst', TYPOGRIFY=True, - TYPOGRIFY_IGNORE_TAGS=['p']) - expected = ('

THIS is some content. With some stuff to ' - '"typogrify"...

\n

Now with added ' - 'support for ' - 'TLA.

\n') + page = self.read_file( + path="article.rst", TYPOGRIFY=True, TYPOGRIFY_IGNORE_TAGS=["p"] + ) + expected = ( + "

THIS is some content. With some stuff to " + ""typogrify"...

\n

Now with added " + 'support for ' + "TLA.

\n" + ) self.assertEqual(page.content, expected) # typogrify should ignore code blocks by default because # code blocks are composed inside the pre tag - page = self.read_file(path='article_with_code_block.rst', - TYPOGRIFY=True) + page = self.read_file(path="article_with_code_block.rst", TYPOGRIFY=True) - expected = ('

An article with some code

\n' - '
'
-                        'x'
-                        ' &'
-                        ' y\n
\n' - '

A block quote:

\n
\nx ' - '& y
\n' - '

Normal:\nx' - ' &' - ' y' - '

\n') + expected = ( + "

An article with some code

\n" + '
'
+                'x'
+                ' &'
+                ' y\n
\n' + "

A block quote:

\n
\nx " + '& y
\n' + "

Normal:\nx" + ' &' + " y" + "

\n" + ) self.assertEqual(page.content, expected) # instruct typogrify to also ignore blockquotes - page = self.read_file(path='article_with_code_block.rst', - TYPOGRIFY=True, - TYPOGRIFY_IGNORE_TAGS=['blockquote']) + page = self.read_file( + path="article_with_code_block.rst", + TYPOGRIFY=True, + TYPOGRIFY_IGNORE_TAGS=["blockquote"], + ) - expected = ('

An article with some code

\n' - '
'
-                        'x'
-                        ' &'
-                        ' y\n
\n' - '

A block quote:

\n
\nx ' - '& y
\n' - '

Normal:\nx' - ' &' - ' y' - '

\n') + expected = ( + "

An article with some code

\n" + '
'
+                'x'
+                ' &'
+                ' y\n
\n' + "

A block quote:

\n
\nx " + "& y
\n" + "

Normal:\nx" + ' &' + " y" + "

\n" + ) self.assertEqual(page.content, expected) except ImportError: - return unittest.skip('need the typogrify distribution') + return unittest.skip("need the typogrify distribution") except TypeError: - return unittest.skip('need typogrify version 2.0.4 or later') + return unittest.skip("need typogrify version 2.0.4 or later") def test_article_with_multiple_authors(self): - page = self.read_file(path='article_with_multiple_authors.rst') - expected = { - 'authors': ['First Author', 'Second Author'] - } + page = self.read_file(path="article_with_multiple_authors.rst") + expected = {"authors": ["First Author", "Second Author"]} self.assertDictHasSubset(page.metadata, expected) def test_article_with_multiple_authors_semicolon(self): - page = self.read_file( - path='article_with_multiple_authors_semicolon.rst') - expected = { - 'authors': ['Author, First', 'Author, Second'] - } + page = self.read_file(path="article_with_multiple_authors_semicolon.rst") + expected = {"authors": ["Author, First", "Author, Second"]} self.assertDictHasSubset(page.metadata, expected) def test_article_with_multiple_authors_list(self): - page = self.read_file(path='article_with_multiple_authors_list.rst') - expected = { - 'authors': ['Author, First', 'Author, Second'] - } + page = self.read_file(path="article_with_multiple_authors_list.rst") + expected = {"authors": ["Author, First", "Author, Second"]} self.assertDictHasSubset(page.metadata, expected) def test_default_date_formats(self): - tuple_date = self.read_file(path='article.rst', - DEFAULT_DATE=(2012, 5, 1)) - string_date = self.read_file(path='article.rst', - DEFAULT_DATE='2012-05-01') + tuple_date = self.read_file(path="article.rst", DEFAULT_DATE=(2012, 5, 1)) + string_date = self.read_file(path="article.rst", DEFAULT_DATE="2012-05-01") - self.assertEqual(tuple_date.metadata['date'], - string_date.metadata['date']) + self.assertEqual(tuple_date.metadata["date"], string_date.metadata["date"]) def test_parse_error(self): # Verify that it raises an Exception, not nothing and not SystemExit or # some such with self.assertRaisesRegex(Exception, "underline too short"): - self.read_file(path='../parse_error/parse_error.rst') + self.read_file(path="../parse_error/parse_error.rst") def test_typogrify_dashes_config(self): # Test default config page = self.read_file( - path='article_with_typogrify_dashes.rst', + path="article_with_typogrify_dashes.rst", TYPOGRIFY=True, - TYPOGRIFY_DASHES='default') + TYPOGRIFY_DASHES="default", + ) expected = "

One: -; Two: —; Three: —-

\n" expected_title = "One -, two —, three —- dashes!" @@ -511,9 +508,10 @@ class RstReaderTest(ReaderTest): # Test 'oldschool' variant page = self.read_file( - path='article_with_typogrify_dashes.rst', + path="article_with_typogrify_dashes.rst", TYPOGRIFY=True, - TYPOGRIFY_DASHES='oldschool') + TYPOGRIFY_DASHES="oldschool", + ) expected = "

One: -; Two: –; Three: —

\n" expected_title = "One -, two –, three — dashes!" @@ -522,9 +520,10 @@ class RstReaderTest(ReaderTest): # Test 'oldschool_inverted' variant page = self.read_file( - path='article_with_typogrify_dashes.rst', + path="article_with_typogrify_dashes.rst", TYPOGRIFY=True, - TYPOGRIFY_DASHES='oldschool_inverted') + TYPOGRIFY_DASHES="oldschool_inverted", + ) expected = "

One: -; Two: —; Three: –

\n" expected_title = "One -, two —, three – dashes!" @@ -534,75 +533,73 @@ class RstReaderTest(ReaderTest): @unittest.skipUnless(readers.Markdown, "markdown isn't installed") class MdReaderTest(ReaderTest): - def test_article_with_metadata(self): reader = readers.MarkdownReader(settings=get_settings()) - content, metadata = reader.read( - _path('article_with_md_extension.md')) + content, metadata = reader.read(_path("article_with_md_extension.md")) expected = { - 'category': 'test', - 'title': 'Test md File', - 'summary': '

I have a lot to test

', - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'modified': SafeDatetime(2010, 12, 2, 10, 20), - 'tags': ['foo', 'bar', 'foobar'], + "category": "test", + "title": "Test md File", + "summary": "

I have a lot to test

", + "date": SafeDatetime(2010, 12, 2, 10, 14), + "modified": SafeDatetime(2010, 12, 2, 10, 20), + "tags": ["foo", "bar", "foobar"], } self.assertDictHasSubset(metadata, expected) content, metadata = reader.read( - _path('article_with_markdown_and_nonascii_summary.md')) + _path("article_with_markdown_and_nonascii_summary.md") + ) expected = { - 'title': 'マックOS X 10.8でパイソンとVirtualenvをインストールと設定', - 'summary': '

パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。

', - 'category': '指導書', - 'date': SafeDatetime(2012, 12, 20), - 'modified': SafeDatetime(2012, 12, 22), - 'tags': ['パイソン', 'マック'], - 'slug': 'python-virtualenv-on-mac-osx-mountain-lion-10.8', + "title": "マックOS X 10.8でパイソンとVirtualenvをインストールと設定", + "summary": "

パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。

", + "category": "指導書", + "date": SafeDatetime(2012, 12, 20), + "modified": SafeDatetime(2012, 12, 22), + "tags": ["パイソン", "マック"], + "slug": "python-virtualenv-on-mac-osx-mountain-lion-10.8", } self.assertDictHasSubset(metadata, expected) def test_article_with_footnote(self): settings = get_settings() - ec = settings['MARKDOWN']['extension_configs'] - ec['markdown.extensions.footnotes'] = {'SEPARATOR': '-'} + ec = settings["MARKDOWN"]["extension_configs"] + ec["markdown.extensions.footnotes"] = {"SEPARATOR": "-"} reader = readers.MarkdownReader(settings) - content, metadata = reader.read( - _path('article_with_markdown_and_footnote.md')) + content, metadata = reader.read(_path("article_with_markdown_and_footnote.md")) expected_content = ( - '

This is some content' + "

This is some content" '1' - ' with some footnotes' + ">1" + " with some footnotes" '2

\n' - '
\n' '
\n
    \n
  1. \n' - '

    Numbered footnote ' + "

    Numbered footnote " '

    \n' '
  2. \n
  3. \n' - '

    Named footnote ' + "

    Named footnote " '

    \n' - '
  4. \n
\n
') + "\n\n" + ) expected_metadata = { - 'title': 'Article with markdown containing footnotes', - 'summary': ( - '

Summary with inline markup ' - 'should be supported.

'), - 'date': SafeDatetime(2012, 10, 31), - 'modified': SafeDatetime(2012, 11, 1), - 'multiline': [ - 'Line Metadata should be handle properly.', - 'See syntax of Meta-Data extension of ' - 'Python Markdown package:', - 'If a line is indented by 4 or more spaces,', - 'that line is assumed to be an additional line of the value', - 'for the previous keyword.', - 'A keyword may have as many lines as desired.', - ] + "title": "Article with markdown containing footnotes", + "summary": ( + "

Summary with inline markup " + "should be supported.

" + ), + "date": SafeDatetime(2012, 10, 31), + "modified": SafeDatetime(2012, 11, 1), + "multiline": [ + "Line Metadata should be handle properly.", + "See syntax of Meta-Data extension of " "Python Markdown package:", + "If a line is indented by 4 or more spaces,", + "that line is assumed to be an additional line of the value", + "for the previous keyword.", + "A keyword may have as many lines as desired.", + ], } self.assertEqual(content, expected_content) self.assertDictHasSubset(metadata, expected_metadata) @@ -611,163 +608,173 @@ class MdReaderTest(ReaderTest): reader = readers.MarkdownReader(settings=get_settings()) # test to ensure the md file extension is being processed by the # correct reader - content, metadata = reader.read( - _path('article_with_md_extension.md')) + content, metadata = reader.read(_path("article_with_md_extension.md")) expected = ( "

Test Markdown File Header

\n" "

Used for pelican test

\n" - "

The quick brown fox jumped over the lazy dog's back.

") + "

The quick brown fox jumped over the lazy dog's back.

" + ) self.assertEqual(content, expected) # test to ensure the mkd file extension is being processed by the # correct reader - content, metadata = reader.read( - _path('article_with_mkd_extension.mkd')) - expected = ("

Test Markdown File Header

\n

Used for pelican" - " test

\n

This is another markdown test file. Uses" - " the mkd extension.

") + content, metadata = reader.read(_path("article_with_mkd_extension.mkd")) + expected = ( + "

Test Markdown File Header

\n

Used for pelican" + " test

\n

This is another markdown test file. Uses" + " the mkd extension.

" + ) self.assertEqual(content, expected) # test to ensure the markdown file extension is being processed by the # correct reader content, metadata = reader.read( - _path('article_with_markdown_extension.markdown')) - expected = ("

Test Markdown File Header

\n

Used for pelican" - " test

\n

This is another markdown test file. Uses" - " the markdown extension.

") + _path("article_with_markdown_extension.markdown") + ) + expected = ( + "

Test Markdown File Header

\n

Used for pelican" + " test

\n

This is another markdown test file. Uses" + " the markdown extension.

" + ) self.assertEqual(content, expected) # test to ensure the mdown file extension is being processed by the # correct reader - content, metadata = reader.read( - _path('article_with_mdown_extension.mdown')) - expected = ("

Test Markdown File Header

\n

Used for pelican" - " test

\n

This is another markdown test file. Uses" - " the mdown extension.

") + content, metadata = reader.read(_path("article_with_mdown_extension.mdown")) + expected = ( + "

Test Markdown File Header

\n

Used for pelican" + " test

\n

This is another markdown test file. Uses" + " the mdown extension.

" + ) self.assertEqual(content, expected) def test_article_with_markdown_markup_extension(self): # test to ensure the markdown markup extension is being processed as # expected page = self.read_file( - path='article_with_markdown_markup_extensions.md', + path="article_with_markdown_markup_extensions.md", MARKDOWN={ - 'extension_configs': { - 'markdown.extensions.toc': {}, - 'markdown.extensions.codehilite': {}, - 'markdown.extensions.extra': {} + "extension_configs": { + "markdown.extensions.toc": {}, + "markdown.extensions.codehilite": {}, + "markdown.extensions.extra": {}, } - } + }, + ) + expected = ( + '
\n' + "\n" + "
\n" + '

Level1

\n' + '

Level2

' ) - expected = ('
\n' - '\n' - '
\n' - '

Level1

\n' - '

Level2

') self.assertEqual(page.content, expected) def test_article_with_filename_metadata(self): page = self.read_file( - path='2012-11-30_md_w_filename_meta#foo-bar.md', - FILENAME_METADATA=None) + path="2012-11-30_md_w_filename_meta#foo-bar.md", FILENAME_METADATA=None + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', + "category": "yeah", + "author": "Alexis Métaireau", } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='2012-11-30_md_w_filename_meta#foo-bar.md', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2}).*') + path="2012-11-30_md_w_filename_meta#foo-bar.md", + FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2}).*", + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'date': SafeDatetime(2012, 11, 30), + "category": "yeah", + "author": "Alexis Métaireau", + "date": SafeDatetime(2012, 11, 30), } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='2012-11-30_md_w_filename_meta#foo-bar.md', + path="2012-11-30_md_w_filename_meta#foo-bar.md", FILENAME_METADATA=( - r'(?P\d{4}-\d{2}-\d{2})' - r'_(?P.*)' - r'#(?P.*)-(?P.*)')) + r"(?P\d{4}-\d{2}-\d{2})" + r"_(?P.*)" + r"#(?P.*)-(?P.*)" + ), + ) expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'date': SafeDatetime(2012, 11, 30), - 'slug': 'md_w_filename_meta', - 'mymeta': 'foo', + "category": "yeah", + "author": "Alexis Métaireau", + "date": SafeDatetime(2012, 11, 30), + "slug": "md_w_filename_meta", + "mymeta": "foo", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_optional_filename_metadata(self): page = self.read_file( - path='2012-11-30_md_w_filename_meta#foo-bar.md', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?') + path="2012-11-30_md_w_filename_meta#foo-bar.md", + FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2})?", + ) expected = { - 'date': SafeDatetime(2012, 11, 30), - 'reader': 'markdown', + "date": SafeDatetime(2012, 11, 30), + "reader": "markdown", } self.assertDictHasSubset(page.metadata, expected) page = self.read_file( - path='empty.md', - FILENAME_METADATA=r'(?P\d{4}-\d{2}-\d{2})?') + path="empty.md", FILENAME_METADATA=r"(?P\d{4}-\d{2}-\d{2})?" + ) expected = { - 'reader': 'markdown', + "reader": "markdown", } self.assertDictHasSubset(page.metadata, expected) - self.assertNotIn('date', page.metadata, 'Date should not be set.') + self.assertNotIn("date", page.metadata, "Date should not be set.") def test_duplicate_tags_or_authors_are_removed(self): reader = readers.MarkdownReader(settings=get_settings()) - content, metadata = reader.read( - _path('article_with_duplicate_tags_authors.md')) + content, metadata = reader.read(_path("article_with_duplicate_tags_authors.md")) expected = { - 'tags': ['foo', 'bar', 'foobar'], - 'authors': ['Author, First', 'Author, Second'], + "tags": ["foo", "bar", "foobar"], + "authors": ["Author, First", "Author, Second"], } self.assertDictHasSubset(metadata, expected) def test_metadata_not_parsed_for_metadata(self): settings = get_settings() - settings['FORMATTED_FIELDS'] = ['summary'] + settings["FORMATTED_FIELDS"] = ["summary"] reader = readers.MarkdownReader(settings=settings) content, metadata = reader.read( - _path('article_with_markdown_and_nested_metadata.md')) + _path("article_with_markdown_and_nested_metadata.md") + ) expected = { - 'title': 'Article with markdown and nested summary metadata', - 'summary': '

Test: This metadata value looks like metadata

', + "title": "Article with markdown and nested summary metadata", + "summary": "

Test: This metadata value looks like metadata

", } self.assertDictHasSubset(metadata, expected) def test_empty_file(self): reader = readers.MarkdownReader(settings=get_settings()) - content, metadata = reader.read( - _path('empty.md')) + content, metadata = reader.read(_path("empty.md")) self.assertEqual(metadata, {}) - self.assertEqual(content, '') + self.assertEqual(content, "") def test_empty_file_with_bom(self): reader = readers.MarkdownReader(settings=get_settings()) - content, metadata = reader.read( - _path('empty_with_bom.md')) + content, metadata = reader.read(_path("empty_with_bom.md")) self.assertEqual(metadata, {}) - self.assertEqual(content, '') + self.assertEqual(content, "") def test_typogrify_dashes_config(self): # Test default config page = self.read_file( - path='article_with_typogrify_dashes.md', + path="article_with_typogrify_dashes.md", TYPOGRIFY=True, - TYPOGRIFY_DASHES='default') + TYPOGRIFY_DASHES="default", + ) expected = "

One: -; Two: —; Three: —-

" expected_title = "One -, two —, three —- dashes!" @@ -776,9 +783,10 @@ class MdReaderTest(ReaderTest): # Test 'oldschool' variant page = self.read_file( - path='article_with_typogrify_dashes.md', + path="article_with_typogrify_dashes.md", TYPOGRIFY=True, - TYPOGRIFY_DASHES='oldschool') + TYPOGRIFY_DASHES="oldschool", + ) expected = "

One: -; Two: –; Three: —

" expected_title = "One -, two –, three — dashes!" @@ -787,9 +795,10 @@ class MdReaderTest(ReaderTest): # Test 'oldschool_inverted' variant page = self.read_file( - path='article_with_typogrify_dashes.md', + path="article_with_typogrify_dashes.md", TYPOGRIFY=True, - TYPOGRIFY_DASHES='oldschool_inverted') + TYPOGRIFY_DASHES="oldschool_inverted", + ) expected = "

One: -; Two: —; Three: –

" expected_title = "One -, two —, three – dashes!" @@ -797,124 +806,130 @@ class MdReaderTest(ReaderTest): self.assertEqual(page.title, expected_title) def test_metadata_has_no_discarded_data(self): - md_filename = 'article_with_markdown_and_empty_tags.md' + md_filename = "article_with_markdown_and_empty_tags.md" - r = readers.Readers(cache_name='cache', settings=get_settings( - CACHE_CONTENT=True)) + r = readers.Readers( + cache_name="cache", settings=get_settings(CACHE_CONTENT=True) + ) page = r.read_file(base_path=CONTENT_PATH, path=md_filename) - __, cached_metadata = r.get_cached_data( - _path(md_filename), (None, None)) + __, cached_metadata = r.get_cached_data(_path(md_filename), (None, None)) - expected = { - 'title': 'Article with markdown and empty tags' - } + expected = {"title": "Article with markdown and empty tags"} self.assertEqual(cached_metadata, expected) - self.assertNotIn('tags', page.metadata) + self.assertNotIn("tags", page.metadata) self.assertDictHasSubset(page.metadata, expected) class HTMLReaderTest(ReaderTest): def test_article_with_comments(self): - page = self.read_file(path='article_with_comments.html') + page = self.read_file(path="article_with_comments.html") - self.assertEqual(''' + self.assertEqual( + """ Body content - ''', page.content) + """, + page.content, + ) def test_article_with_keywords(self): - page = self.read_file(path='article_with_keywords.html') + page = self.read_file(path="article_with_keywords.html") expected = { - 'tags': ['foo', 'bar', 'foobar'], + "tags": ["foo", "bar", "foobar"], } self.assertDictHasSubset(page.metadata, expected) def test_article_with_metadata(self): - page = self.read_file(path='article_with_metadata.html') + page = self.read_file(path="article_with_metadata.html") expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'This is a super article !', - 'summary': 'Summary and stuff', - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'tags': ['foo', 'bar', 'foobar'], - 'custom_field': 'http://notmyidea.org', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "This is a super article !", + "summary": "Summary and stuff", + "date": SafeDatetime(2010, 12, 2, 10, 14), + "tags": ["foo", "bar", "foobar"], + "custom_field": "http://notmyidea.org", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_multiple_similar_metadata_tags(self): - page = self.read_file(path='article_with_multiple_metadata_tags.html') + page = self.read_file(path="article_with_multiple_metadata_tags.html") expected = { - 'custom_field': ['https://getpelican.com', 'https://www.eff.org'], + "custom_field": ["https://getpelican.com", "https://www.eff.org"], } self.assertDictHasSubset(page.metadata, expected) def test_article_with_multiple_authors(self): - page = self.read_file(path='article_with_multiple_authors.html') - expected = { - 'authors': ['First Author', 'Second Author'] - } + page = self.read_file(path="article_with_multiple_authors.html") + expected = {"authors": ["First Author", "Second Author"]} self.assertDictHasSubset(page.metadata, expected) def test_article_with_metadata_and_contents_attrib(self): - page = self.read_file(path='article_with_metadata_and_contents.html') + page = self.read_file(path="article_with_metadata_and_contents.html") expected = { - 'category': 'yeah', - 'author': 'Alexis Métaireau', - 'title': 'This is a super article !', - 'summary': 'Summary and stuff', - 'date': SafeDatetime(2010, 12, 2, 10, 14), - 'tags': ['foo', 'bar', 'foobar'], - 'custom_field': 'http://notmyidea.org', + "category": "yeah", + "author": "Alexis Métaireau", + "title": "This is a super article !", + "summary": "Summary and stuff", + "date": SafeDatetime(2010, 12, 2, 10, 14), + "tags": ["foo", "bar", "foobar"], + "custom_field": "http://notmyidea.org", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_null_attributes(self): - page = self.read_file(path='article_with_null_attributes.html') + page = self.read_file(path="article_with_null_attributes.html") - self.assertEqual(''' + self.assertEqual( + """ Ensure that empty attributes are copied properly. - ''', page.content) + """, + page.content, + ) def test_article_with_attributes_containing_double_quotes(self): - page = self.read_file(path='article_with_attributes_containing_' + - 'double_quotes.html') - self.assertEqual(''' + page = self.read_file( + path="article_with_attributes_containing_" + "double_quotes.html" + ) + self.assertEqual( + """ Ensure that if an attribute value contains a double quote, it is surrounded with single quotes, otherwise with double quotes. Span content Span content Span content - ''', page.content) + """, + page.content, + ) def test_article_metadata_key_lowercase(self): # Keys of metadata should be lowercase. - page = self.read_file(path='article_with_uppercase_metadata.html') + page = self.read_file(path="article_with_uppercase_metadata.html") # Key should be lowercase - self.assertIn('category', page.metadata, 'Key should be lowercase.') + self.assertIn("category", page.metadata, "Key should be lowercase.") # Value should keep cases - self.assertEqual('Yeah', page.metadata.get('category')) + self.assertEqual("Yeah", page.metadata.get("category")) def test_article_with_nonconformant_meta_tags(self): - page = self.read_file(path='article_with_nonconformant_meta_tags.html') + page = self.read_file(path="article_with_nonconformant_meta_tags.html") expected = { - 'summary': 'Summary and stuff', - 'title': 'Article with Nonconformant HTML meta tags', + "summary": "Summary and stuff", + "title": "Article with Nonconformant HTML meta tags", } self.assertDictHasSubset(page.metadata, expected) def test_article_with_inline_svg(self): - page = self.read_file(path='article_with_inline_svg.html') + page = self.read_file(path="article_with_inline_svg.html") expected = { - 'title': 'Article with an inline SVG', + "title": "Article with an inline SVG", } self.assertDictHasSubset(page.metadata, expected) diff --git a/pelican/tests/test_rstdirectives.py b/pelican/tests/test_rstdirectives.py index 6b733971..46ed6f49 100644 --- a/pelican/tests/test_rstdirectives.py +++ b/pelican/tests/test_rstdirectives.py @@ -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") diff --git a/pelican/tests/test_server.py b/pelican/tests/test_server.py index 9af030f8..fd616ef7 100644 --- a/pelican/tests/test_server.py +++ b/pelican/tests/test_server.py @@ -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) diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 0f630ad5..0e77674d 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -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) diff --git a/pelican/tests/test_testsuite.py b/pelican/tests/test_testsuite.py index fa930139..a9a0c200 100644 --- a/pelican/tests/test_testsuite.py +++ b/pelican/tests/test_testsuite.py @@ -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") diff --git a/pelican/tests/test_urlwrappers.py b/pelican/tests/test_urlwrappers.py index 66ae1524..13632e3a 100644 --- a/pelican/tests/test_urlwrappers.py +++ b/pelican/tests/test_urlwrappers.py @@ -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") diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 40aff005..22dd8e38 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -14,25 +14,29 @@ except ModuleNotFoundError: from pelican import utils from pelican.generators import TemplatePagesGenerator from pelican.settings import read_settings -from pelican.tests.support import (LoggedTestCase, get_article, - locale_available, unittest) +from pelican.tests.support import ( + LoggedTestCase, + get_article, + locale_available, + unittest, +) from pelican.writers import Writer class TestUtils(LoggedTestCase): - _new_attribute = 'new_value' + _new_attribute = "new_value" def setUp(self): super().setUp() - self.temp_output = mkdtemp(prefix='pelicantests.') + self.temp_output = mkdtemp(prefix="pelicantests.") def tearDown(self): super().tearDown() shutil.rmtree(self.temp_output) @utils.deprecated_attribute( - old='_old_attribute', new='_new_attribute', - since=(3, 1, 0), remove=(4, 1, 3)) + old="_old_attribute", new="_new_attribute", since=(3, 1, 0), remove=(4, 1, 3) + ) def _old_attribute(): return None @@ -41,69 +45,109 @@ class TestUtils(LoggedTestCase): self.assertEqual(value, self._new_attribute) self.assertLogCountEqual( count=1, - msg=('_old_attribute has been deprecated since 3.1.0 and will be ' - 'removed by version 4.1.3. Use _new_attribute instead'), - level=logging.WARNING) + msg=( + "_old_attribute has been deprecated since 3.1.0 and will be " + "removed by version 4.1.3. Use _new_attribute instead" + ), + level=logging.WARNING, + ) def test_get_date(self): # valid ones date = utils.SafeDatetime(year=2012, month=11, day=22) - date_hour = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11) + date_hour = utils.SafeDatetime(year=2012, month=11, day=22, hour=22, minute=11) date_hour_z = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, - tzinfo=timezone.utc) + year=2012, month=11, day=22, hour=22, minute=11, tzinfo=timezone.utc + ) date_hour_est = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, - tzinfo=ZoneInfo("EST")) + year=2012, month=11, day=22, hour=22, minute=11, tzinfo=ZoneInfo("EST") + ) date_hour_sec = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, second=10) + year=2012, month=11, day=22, hour=22, minute=11, second=10 + ) date_hour_sec_z = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, second=10, - tzinfo=timezone.utc) + year=2012, + month=11, + day=22, + hour=22, + minute=11, + second=10, + tzinfo=timezone.utc, + ) date_hour_sec_est = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, second=10, - tzinfo=ZoneInfo("EST")) + year=2012, + month=11, + day=22, + hour=22, + minute=11, + second=10, + tzinfo=ZoneInfo("EST"), + ) date_hour_sec_frac_z = utils.SafeDatetime( - year=2012, month=11, day=22, hour=22, minute=11, second=10, - microsecond=123000, tzinfo=timezone.utc) + year=2012, + month=11, + day=22, + hour=22, + minute=11, + second=10, + microsecond=123000, + tzinfo=timezone.utc, + ) dates = { - '2012-11-22': date, - '2012/11/22': date, - '2012-11-22 22:11': date_hour, - '2012/11/22 22:11': date_hour, - '22-11-2012': date, - '22/11/2012': date, - '22.11.2012': date, - '22.11.2012 22:11': date_hour, - '2012-11-22T22:11Z': date_hour_z, - '2012-11-22T22:11-0500': date_hour_est, - '2012-11-22 22:11:10': date_hour_sec, - '2012-11-22T22:11:10Z': date_hour_sec_z, - '2012-11-22T22:11:10-0500': date_hour_sec_est, - '2012-11-22T22:11:10.123Z': date_hour_sec_frac_z, + "2012-11-22": date, + "2012/11/22": date, + "2012-11-22 22:11": date_hour, + "2012/11/22 22:11": date_hour, + "22-11-2012": date, + "22/11/2012": date, + "22.11.2012": date, + "22.11.2012 22:11": date_hour, + "2012-11-22T22:11Z": date_hour_z, + "2012-11-22T22:11-0500": date_hour_est, + "2012-11-22 22:11:10": date_hour_sec, + "2012-11-22T22:11:10Z": date_hour_sec_z, + "2012-11-22T22:11:10-0500": date_hour_sec_est, + "2012-11-22T22:11:10.123Z": date_hour_sec_frac_z, } # examples from http://www.w3.org/TR/NOTE-datetime iso_8601_date = utils.SafeDatetime(year=1997, month=7, day=16) iso_8601_date_hour_tz = utils.SafeDatetime( - year=1997, month=7, day=16, hour=19, minute=20, - tzinfo=ZoneInfo("Europe/London")) + year=1997, + month=7, + day=16, + hour=19, + minute=20, + tzinfo=ZoneInfo("Europe/London"), + ) iso_8601_date_hour_sec_tz = utils.SafeDatetime( - year=1997, month=7, day=16, hour=19, minute=20, second=30, - tzinfo=ZoneInfo("Europe/London")) + year=1997, + month=7, + day=16, + hour=19, + minute=20, + second=30, + tzinfo=ZoneInfo("Europe/London"), + ) iso_8601_date_hour_sec_ms_tz = utils.SafeDatetime( - year=1997, month=7, day=16, hour=19, minute=20, second=30, - microsecond=450000, tzinfo=ZoneInfo("Europe/London")) + year=1997, + month=7, + day=16, + hour=19, + minute=20, + second=30, + microsecond=450000, + tzinfo=ZoneInfo("Europe/London"), + ) iso_8601 = { - '1997-07-16': iso_8601_date, - '1997-07-16T19:20+01:00': iso_8601_date_hour_tz, - '1997-07-16T19:20:30+01:00': iso_8601_date_hour_sec_tz, - '1997-07-16T19:20:30.45+01:00': iso_8601_date_hour_sec_ms_tz, + "1997-07-16": iso_8601_date, + "1997-07-16T19:20+01:00": iso_8601_date_hour_tz, + "1997-07-16T19:20:30+01:00": iso_8601_date_hour_sec_tz, + "1997-07-16T19:20:30.45+01:00": iso_8601_date_hour_sec_ms_tz, } # invalid ones - invalid_dates = ['2010-110-12', 'yay'] + invalid_dates = ["2010-110-12", "yay"] for value, expected in dates.items(): self.assertEqual(utils.get_date(value), expected, value) @@ -115,219 +159,247 @@ class TestUtils(LoggedTestCase): self.assertRaises(ValueError, utils.get_date, item) def test_slugify(self): - - samples = (('this is a test', 'this-is-a-test'), - ('this is a test', 'this-is-a-test'), - ('this → is ← a ↑ test', 'this-is-a-test'), - ('this--is---a test', 'this-is-a-test'), - ('unicode測試許功蓋,你看到了嗎?', - 'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'), - ('大飯原発4号機、18日夜起動へ', - 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),) + samples = ( + ("this is a test", "this-is-a-test"), + ("this is a test", "this-is-a-test"), + ("this → is ← a ↑ test", "this-is-a-test"), + ("this--is---a test", "this-is-a-test"), + ( + "unicode測試許功蓋,你看到了嗎?", + "unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma", + ), + ( + "大飯原発4号機、18日夜起動へ", + "da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he", + ), + ) settings = read_settings() - subs = settings['SLUG_REGEX_SUBSTITUTIONS'] + subs = settings["SLUG_REGEX_SUBSTITUTIONS"] for value, expected in samples: self.assertEqual(utils.slugify(value, regex_subs=subs), expected) - self.assertEqual(utils.slugify('Cat', regex_subs=subs), 'cat') + self.assertEqual(utils.slugify("Cat", regex_subs=subs), "cat") self.assertEqual( - utils.slugify('Cat', regex_subs=subs, preserve_case=False), 'cat') + utils.slugify("Cat", regex_subs=subs, preserve_case=False), "cat" + ) self.assertEqual( - utils.slugify('Cat', regex_subs=subs, preserve_case=True), 'Cat') + utils.slugify("Cat", regex_subs=subs, preserve_case=True), "Cat" + ) def test_slugify_use_unicode(self): - samples = ( - ('this is a test', 'this-is-a-test'), - ('this is a test', 'this-is-a-test'), - ('this → is ← a ↑ test', 'this-is-a-test'), - ('this--is---a test', 'this-is-a-test'), - ('unicode測試許功蓋,你看到了嗎?', 'unicode測試許功蓋你看到了嗎'), - ('Çığ', 'çığ') + ("this is a test", "this-is-a-test"), + ("this is a test", "this-is-a-test"), + ("this → is ← a ↑ test", "this-is-a-test"), + ("this--is---a test", "this-is-a-test"), + ("unicode測試許功蓋,你看到了嗎?", "unicode測試許功蓋你看到了嗎"), + ("Çığ", "çığ"), ) settings = read_settings() - subs = settings['SLUG_REGEX_SUBSTITUTIONS'] + subs = settings["SLUG_REGEX_SUBSTITUTIONS"] for value, expected in samples: self.assertEqual( - utils.slugify(value, regex_subs=subs, use_unicode=True), - expected) + utils.slugify(value, regex_subs=subs, use_unicode=True), expected + ) # check with preserve case for value, expected in samples: self.assertEqual( - utils.slugify('Çığ', regex_subs=subs, - preserve_case=True, use_unicode=True), - 'Çığ') + utils.slugify( + "Çığ", regex_subs=subs, preserve_case=True, use_unicode=True + ), + "Çığ", + ) # check normalization samples = ( - ('大飯原発4号機、18日夜起動へ', '大飯原発4号機18日夜起動へ'), + ("大飯原発4号機、18日夜起動へ", "大飯原発4号機18日夜起動へ"), ( - '\N{LATIN SMALL LETTER C}\N{COMBINING CEDILLA}', - '\N{LATIN SMALL LETTER C WITH CEDILLA}' - ) + "\N{LATIN SMALL LETTER C}\N{COMBINING CEDILLA}", + "\N{LATIN SMALL LETTER C WITH CEDILLA}", + ), ) for value, expected in samples: self.assertEqual( - utils.slugify(value, regex_subs=subs, use_unicode=True), - expected) + utils.slugify(value, regex_subs=subs, use_unicode=True), expected + ) def test_slugify_substitute(self): - - samples = (('C++ is based on C', 'cpp-is-based-on-c'), - ('C+++ test C+ test', 'cpp-test-c-test'), - ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'), - ('c++-streams', 'cpp-streams'),) + samples = ( + ("C++ is based on C", "cpp-is-based-on-c"), + ("C+++ test C+ test", "cpp-test-c-test"), + ("c++, c#, C#, C++", "cpp-c-sharp-c-sharp-cpp"), + ("c++-streams", "cpp-streams"), + ) settings = read_settings() subs = [ - (r'C\+\+', 'CPP'), - (r'C#', 'C-SHARP'), - ] + settings['SLUG_REGEX_SUBSTITUTIONS'] + (r"C\+\+", "CPP"), + (r"C#", "C-SHARP"), + ] + settings["SLUG_REGEX_SUBSTITUTIONS"] for value, expected in samples: self.assertEqual(utils.slugify(value, regex_subs=subs), expected) def test_slugify_substitute_and_keeping_non_alphanum(self): - - samples = (('Fedora QA', 'fedora.qa'), - ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'), - ('C++ is based on C', 'cpp is based on c'), - ('C+++ test C+ test', 'cpp+ test c+ test'),) + samples = ( + ("Fedora QA", "fedora.qa"), + ("C++ is used by Fedora QA", "cpp is used by fedora.qa"), + ("C++ is based on C", "cpp is based on c"), + ("C+++ test C+ test", "cpp+ test c+ test"), + ) subs = [ - (r'Fedora QA', 'fedora.qa'), - (r'c\+\+', 'cpp'), + (r"Fedora QA", "fedora.qa"), + (r"c\+\+", "cpp"), ] for value, expected in samples: self.assertEqual(utils.slugify(value, regex_subs=subs), expected) def test_get_relative_path(self): - - samples = ((os.path.join('test', 'test.html'), os.pardir), - (os.path.join('test', 'test', 'test.html'), - os.path.join(os.pardir, os.pardir)), - ('test.html', os.curdir), - (os.path.join('/test', 'test.html'), os.pardir), - (os.path.join('/test', 'test', 'test.html'), - os.path.join(os.pardir, os.pardir)), - ('/test.html', os.curdir),) + samples = ( + (os.path.join("test", "test.html"), os.pardir), + ( + os.path.join("test", "test", "test.html"), + os.path.join(os.pardir, os.pardir), + ), + ("test.html", os.curdir), + (os.path.join("/test", "test.html"), os.pardir), + ( + os.path.join("/test", "test", "test.html"), + os.path.join(os.pardir, os.pardir), + ), + ("/test.html", os.curdir), + ) for value, expected in samples: self.assertEqual(utils.get_relative_path(value), expected) def test_truncate_html_words(self): # Plain text. + self.assertEqual(utils.truncate_html_words("short string", 20), "short string") self.assertEqual( - utils.truncate_html_words('short string', 20), - 'short string') - self.assertEqual( - utils.truncate_html_words('word ' * 100, 20), - 'word ' * 20 + '…') + utils.truncate_html_words("word " * 100, 20), "word " * 20 + "…" + ) # Plain text with Unicode content. self.assertEqual( utils.truncate_html_words( - '我愿意这样,朋友——我独自远行,不但没有你,\ - 并且再没有别的影在黑暗里。', 12 + "我愿意这样,朋友——我独自远行,不但没有你,\ + 并且再没有别的影在黑暗里。", + 12, ), - '我愿意这样,朋友——我独自远行' + ' …') + "我愿意这样,朋友——我独自远行" + " …", + ) self.assertEqual( utils.truncate_html_words( - 'Ты мелькнула, ты предстала, Снова сердце задрожало,', 3 + "Ты мелькнула, ты предстала, Снова сердце задрожало,", 3 ), - 'Ты мелькнула, ты' + ' …') + "Ты мелькнула, ты" + " …", + ) self.assertEqual( - utils.truncate_html_words( - 'Trong đầm gì đẹp bằng sen', 4 - ), - 'Trong đầm gì đẹp' + ' …') + utils.truncate_html_words("Trong đầm gì đẹp bằng sen", 4), + "Trong đầm gì đẹp" + " …", + ) # Words enclosed or intervaled by HTML tags. self.assertEqual( - utils.truncate_html_words('

' + 'word ' * 100 + '

', 20), - '

' + 'word ' * 20 + '…

') + utils.truncate_html_words("

" + "word " * 100 + "

", 20), + "

" + "word " * 20 + "…

", + ) self.assertEqual( utils.truncate_html_words( - '' + 'word ' * 100 + '', 20), - '' + 'word ' * 20 + '…') + '' + "word " * 100 + "", 20 + ), + '' + "word " * 20 + "…", + ) self.assertEqual( - utils.truncate_html_words('
' + 'word ' * 100, 20), - '
' + 'word ' * 20 + '…') + utils.truncate_html_words("
" + "word " * 100, 20), + "
" + "word " * 20 + "…", + ) self.assertEqual( - utils.truncate_html_words('' + 'word ' * 100, 20), - '' + 'word ' * 20 + '…') + utils.truncate_html_words("" + "word " * 100, 20), + "" + "word " * 20 + "…", + ) # Words enclosed or intervaled by HTML tags with a custom end # marker containing HTML tags. self.assertEqual( - utils.truncate_html_words('

' + 'word ' * 100 + '

', 20, - 'marker'), - '

' + 'word ' * 20 + 'marker

') + utils.truncate_html_words( + "

" + "word " * 100 + "

", 20, "marker" + ), + "

" + "word " * 20 + "marker

", + ) self.assertEqual( utils.truncate_html_words( - '' + 'word ' * 100 + '', 20, - 'marker'), - '' + 'word ' * 20 + 'marker') + '' + "word " * 100 + "", + 20, + "marker", + ), + '' + "word " * 20 + "marker", + ) self.assertEqual( - utils.truncate_html_words('
' + 'word ' * 100, 20, - 'marker'), - '
' + 'word ' * 20 + 'marker') + utils.truncate_html_words( + "
" + "word " * 100, 20, "marker" + ), + "
" + "word " * 20 + "marker", + ) self.assertEqual( - utils.truncate_html_words('' + 'word ' * 100, 20, - 'marker'), - '' + 'word ' * 20 + 'marker') + utils.truncate_html_words( + "" + "word " * 100, 20, "marker" + ), + "" + "word " * 20 + "marker", + ) # Words with hypens and apostrophes. + self.assertEqual(utils.truncate_html_words("a-b " * 100, 20), "a-b " * 20 + "…") self.assertEqual( - utils.truncate_html_words("a-b " * 100, 20), - "a-b " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("it's " * 100, 20), - "it's " * 20 + '…') + utils.truncate_html_words("it's " * 100, 20), "it's " * 20 + "…" + ) # Words with HTML entity references. self.assertEqual( - utils.truncate_html_words("é " * 100, 20), - "é " * 20 + '…') + utils.truncate_html_words("é " * 100, 20), "é " * 20 + "…" + ) self.assertEqual( utils.truncate_html_words("café " * 100, 20), - "café " * 20 + '…') + "café " * 20 + "…", + ) self.assertEqual( utils.truncate_html_words("èlite " * 100, 20), - "èlite " * 20 + '…') + "èlite " * 20 + "…", + ) self.assertEqual( utils.truncate_html_words("cafetiére " * 100, 20), - "cafetiére " * 20 + '…') + "cafetiére " * 20 + "…", + ) self.assertEqual( - utils.truncate_html_words("∫dx " * 100, 20), - "∫dx " * 20 + '…') + utils.truncate_html_words("∫dx " * 100, 20), "∫dx " * 20 + "…" + ) # Words with HTML character references inside and outside # the ASCII range. self.assertEqual( - utils.truncate_html_words("é " * 100, 20), - "é " * 20 + '…') + utils.truncate_html_words("é " * 100, 20), "é " * 20 + "…" + ) self.assertEqual( - utils.truncate_html_words("∫dx " * 100, 20), - "∫dx " * 20 + '…') + utils.truncate_html_words("∫dx " * 100, 20), "∫dx " * 20 + "…" + ) # Words with invalid or broken HTML references. + self.assertEqual(utils.truncate_html_words("&invalid;", 20), "&invalid;") self.assertEqual( - utils.truncate_html_words('&invalid;', 20), '&invalid;') + utils.truncate_html_words("�", 20), "�" + ) self.assertEqual( - utils.truncate_html_words('�', 20), '�') - self.assertEqual( - utils.truncate_html_words('�', 20), '�') - self.assertEqual( - utils.truncate_html_words('&mdash text', 20), '&mdash text') - self.assertEqual( - utils.truncate_html_words('Ӓ text', 20), 'Ӓ text') - self.assertEqual( - utils.truncate_html_words('઼ text', 20), '઼ text') + utils.truncate_html_words("�", 20), "�" + ) + self.assertEqual(utils.truncate_html_words("&mdash text", 20), "&mdash text") + self.assertEqual(utils.truncate_html_words("Ӓ text", 20), "Ӓ text") + self.assertEqual(utils.truncate_html_words("઼ text", 20), "઼ text") def test_process_translations(self): fr_articles = [] @@ -335,65 +407,135 @@ class TestUtils(LoggedTestCase): # create a bunch of articles # 0: no translation metadata - fr_articles.append(get_article(lang='fr', slug='yay0', title='Titre', - content='en français')) - en_articles.append(get_article(lang='en', slug='yay0', title='Title', - content='in english')) + fr_articles.append( + get_article(lang="fr", slug="yay0", title="Titre", content="en français") + ) + en_articles.append( + get_article(lang="en", slug="yay0", title="Title", content="in english") + ) # 1: translation metadata on default lang - fr_articles.append(get_article(lang='fr', slug='yay1', title='Titre', - content='en français')) - en_articles.append(get_article(lang='en', slug='yay1', title='Title', - content='in english', - translation='true')) + fr_articles.append( + get_article(lang="fr", slug="yay1", title="Titre", content="en français") + ) + en_articles.append( + get_article( + lang="en", + slug="yay1", + title="Title", + content="in english", + translation="true", + ) + ) # 2: translation metadata not on default lang - fr_articles.append(get_article(lang='fr', slug='yay2', title='Titre', - content='en français', - translation='true')) - en_articles.append(get_article(lang='en', slug='yay2', title='Title', - content='in english')) + fr_articles.append( + get_article( + lang="fr", + slug="yay2", + title="Titre", + content="en français", + translation="true", + ) + ) + en_articles.append( + get_article(lang="en", slug="yay2", title="Title", content="in english") + ) # 3: back to default language detection if all items have the # translation metadata - fr_articles.append(get_article(lang='fr', slug='yay3', title='Titre', - content='en français', - translation='yep')) - en_articles.append(get_article(lang='en', slug='yay3', title='Title', - content='in english', - translation='yes')) + fr_articles.append( + get_article( + lang="fr", + slug="yay3", + title="Titre", + content="en français", + translation="yep", + ) + ) + en_articles.append( + get_article( + lang="en", + slug="yay3", + title="Title", + content="in english", + translation="yes", + ) + ) # 4-5: translation pairs with the same slug but different category - fr_articles.append(get_article(lang='fr', slug='yay4', title='Titre', - content='en français', category='foo')) - en_articles.append(get_article(lang='en', slug='yay4', title='Title', - content='in english', category='foo')) - fr_articles.append(get_article(lang='fr', slug='yay4', title='Titre', - content='en français', category='bar')) - en_articles.append(get_article(lang='en', slug='yay4', title='Title', - content='in english', category='bar')) + fr_articles.append( + get_article( + lang="fr", + slug="yay4", + title="Titre", + content="en français", + category="foo", + ) + ) + en_articles.append( + get_article( + lang="en", + slug="yay4", + title="Title", + content="in english", + category="foo", + ) + ) + fr_articles.append( + get_article( + lang="fr", + slug="yay4", + title="Titre", + content="en français", + category="bar", + ) + ) + en_articles.append( + get_article( + lang="en", + slug="yay4", + title="Title", + content="in english", + category="bar", + ) + ) # try adding articles in both orders - for lang0_articles, lang1_articles in ((fr_articles, en_articles), - (en_articles, fr_articles)): + for lang0_articles, lang1_articles in ( + (fr_articles, en_articles), + (en_articles, fr_articles), + ): articles = lang0_articles + lang1_articles # test process_translations with falsy translation_id - index, trans = utils.process_translations( - articles, translation_id=None) + index, trans = utils.process_translations(articles, translation_id=None) for i in range(6): for lang_articles in [en_articles, fr_articles]: self.assertIn(lang_articles[i], index) self.assertNotIn(lang_articles[i], trans) # test process_translations with simple and complex translation_id - for translation_id in ['slug', {'slug', 'category'}]: + for translation_id in ["slug", {"slug", "category"}]: index, trans = utils.process_translations( - articles, translation_id=translation_id) + articles, translation_id=translation_id + ) - for a in [en_articles[0], fr_articles[1], en_articles[2], - en_articles[3], en_articles[4], en_articles[5]]: + for a in [ + en_articles[0], + fr_articles[1], + en_articles[2], + en_articles[3], + en_articles[4], + en_articles[5], + ]: self.assertIn(a, index) self.assertNotIn(a, trans) - for a in [fr_articles[0], en_articles[1], fr_articles[2], - fr_articles[3], fr_articles[4], fr_articles[5]]: + for a in [ + fr_articles[0], + en_articles[1], + fr_articles[2], + fr_articles[3], + fr_articles[4], + fr_articles[5], + ]: self.assertIn(a, trans) self.assertNotIn(a, index) @@ -403,18 +545,17 @@ class TestUtils(LoggedTestCase): for a_arts in [en_articles, fr_articles]: for b_arts in [en_articles, fr_articles]: - if translation_id == 'slug': + if translation_id == "slug": self.assertIn(a_arts[4], b_arts[5].translations) self.assertIn(a_arts[5], b_arts[4].translations) - elif translation_id == {'slug', 'category'}: + elif translation_id == {"slug", "category"}: self.assertNotIn(a_arts[4], b_arts[5].translations) self.assertNotIn(a_arts[5], b_arts[4].translations) def test_clean_output_dir(self): retention = () - test_directory = os.path.join(self.temp_output, - 'clean_output') - content = os.path.join(os.path.dirname(__file__), 'content') + test_directory = os.path.join(self.temp_output, "clean_output") + content = os.path.join(os.path.dirname(__file__), "content") shutil.copytree(content, test_directory) utils.clean_output_dir(test_directory, retention) self.assertTrue(os.path.isdir(test_directory)) @@ -423,17 +564,15 @@ class TestUtils(LoggedTestCase): def test_clean_output_dir_not_there(self): retention = () - test_directory = os.path.join(self.temp_output, - 'does_not_exist') + test_directory = os.path.join(self.temp_output, "does_not_exist") utils.clean_output_dir(test_directory, retention) self.assertFalse(os.path.exists(test_directory)) def test_clean_output_dir_is_file(self): retention = () - test_directory = os.path.join(self.temp_output, - 'this_is_a_file') - f = open(test_directory, 'w') - f.write('') + test_directory = os.path.join(self.temp_output, "this_is_a_file") + f = open(test_directory, "w") + f.write("") f.close() utils.clean_output_dir(test_directory, retention) self.assertFalse(os.path.exists(test_directory)) @@ -442,223 +581,230 @@ class TestUtils(LoggedTestCase): d = utils.SafeDatetime(2012, 8, 29) # simple formatting - self.assertEqual(utils.strftime(d, '%d/%m/%y'), '29/08/12') - self.assertEqual(utils.strftime(d, '%d/%m/%Y'), '29/08/2012') + self.assertEqual(utils.strftime(d, "%d/%m/%y"), "29/08/12") + self.assertEqual(utils.strftime(d, "%d/%m/%Y"), "29/08/2012") # RFC 3339 self.assertEqual( - utils.strftime(d, '%Y-%m-%dT%H:%M:%SZ'), - '2012-08-29T00:00:00Z') + utils.strftime(d, "%Y-%m-%dT%H:%M:%SZ"), "2012-08-29T00:00:00Z" + ) # % escaped - self.assertEqual(utils.strftime(d, '%d%%%m%%%y'), '29%08%12') - self.assertEqual(utils.strftime(d, '%d %% %m %% %y'), '29 % 08 % 12') + self.assertEqual(utils.strftime(d, "%d%%%m%%%y"), "29%08%12") + self.assertEqual(utils.strftime(d, "%d %% %m %% %y"), "29 % 08 % 12") # not valid % formatter - self.assertEqual(utils.strftime(d, '10% reduction in %Y'), - '10% reduction in 2012') - self.assertEqual(utils.strftime(d, '%10 reduction in %Y'), - '%10 reduction in 2012') + self.assertEqual( + utils.strftime(d, "10% reduction in %Y"), "10% reduction in 2012" + ) + self.assertEqual( + utils.strftime(d, "%10 reduction in %Y"), "%10 reduction in 2012" + ) # with text - self.assertEqual(utils.strftime(d, 'Published in %d-%m-%Y'), - 'Published in 29-08-2012') + self.assertEqual( + utils.strftime(d, "Published in %d-%m-%Y"), "Published in 29-08-2012" + ) # with non-ascii text self.assertEqual( - utils.strftime(d, '%d/%m/%Y Øl trinken beim Besäufnis'), - '29/08/2012 Øl trinken beim Besäufnis') + utils.strftime(d, "%d/%m/%Y Øl trinken beim Besäufnis"), + "29/08/2012 Øl trinken beim Besäufnis", + ) # alternative formatting options - self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '29/8/12') - self.assertEqual(utils.strftime(d, '%-H:%-M:%-S'), '0:0:0') + self.assertEqual(utils.strftime(d, "%-d/%-m/%y"), "29/8/12") + self.assertEqual(utils.strftime(d, "%-H:%-M:%-S"), "0:0:0") d = utils.SafeDatetime(2012, 8, 9) - self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '9/8/12') + self.assertEqual(utils.strftime(d, "%-d/%-m/%y"), "9/8/12") d = utils.SafeDatetime(2021, 1, 8) - self.assertEqual(utils.strftime(d, '%G - %-V - %u'), '2021 - 1 - 5') + self.assertEqual(utils.strftime(d, "%G - %-V - %u"), "2021 - 1 - 5") # test the output of utils.strftime in a different locale # Turkish locale - @unittest.skipUnless(locale_available('tr_TR.UTF-8') or - locale_available('Turkish'), - 'Turkish locale needed') + @unittest.skipUnless( + locale_available("tr_TR.UTF-8") or locale_available("Turkish"), + "Turkish locale needed", + ) def test_strftime_locale_dependent_turkish(self): - temp_locale = 'Turkish' if platform == 'win32' else 'tr_TR.UTF-8' + temp_locale = "Turkish" if platform == "win32" else "tr_TR.UTF-8" with utils.temporary_locale(temp_locale): d = utils.SafeDatetime(2012, 8, 29) # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') - self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), - 'Çarşamba, 29 Ağustos 2012') + self.assertEqual(utils.strftime(d, "%d %B %Y"), "29 Ağustos 2012") + self.assertEqual( + utils.strftime(d, "%A, %d %B %Y"), "Çarşamba, 29 Ağustos 2012" + ) # with text self.assertEqual( - utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), - 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') + utils.strftime(d, "Yayınlanma tarihi: %A, %d %B %Y"), + "Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012", + ) # non-ascii format candidate (someone might pass it… for some reason) self.assertEqual( - utils.strftime(d, '%Y yılında %üretim artışı'), - '2012 yılında %üretim artışı') + utils.strftime(d, "%Y yılında %üretim artışı"), + "2012 yılında %üretim artışı", + ) # test the output of utils.strftime in a different locale # French locale - @unittest.skipUnless(locale_available('fr_FR.UTF-8') or - locale_available('French'), - 'French locale needed') + @unittest.skipUnless( + locale_available("fr_FR.UTF-8") or locale_available("French"), + "French locale needed", + ) def test_strftime_locale_dependent_french(self): - temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' + temp_locale = "French" if platform == "win32" else "fr_FR.UTF-8" with utils.temporary_locale(temp_locale): d = utils.SafeDatetime(2012, 8, 29) # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') + self.assertEqual(utils.strftime(d, "%d %B %Y"), "29 août 2012") # depending on OS, the first letter is m or M - self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) + self.assertTrue(utils.strftime(d, "%A") in ("mercredi", "Mercredi")) # with text self.assertEqual( - utils.strftime(d, 'Écrit le %d %B %Y'), - 'Écrit le 29 août 2012') + utils.strftime(d, "Écrit le %d %B %Y"), "Écrit le 29 août 2012" + ) # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%écrits en %Y'), - '%écrits en 2012') + self.assertEqual(utils.strftime(d, "%écrits en %Y"), "%écrits en 2012") def test_maybe_pluralize(self): - self.assertEqual( - utils.maybe_pluralize(0, 'Article', 'Articles'), - '0 Articles') - self.assertEqual( - utils.maybe_pluralize(1, 'Article', 'Articles'), - '1 Article') - self.assertEqual( - utils.maybe_pluralize(2, 'Article', 'Articles'), - '2 Articles') + self.assertEqual(utils.maybe_pluralize(0, "Article", "Articles"), "0 Articles") + self.assertEqual(utils.maybe_pluralize(1, "Article", "Articles"), "1 Article") + self.assertEqual(utils.maybe_pluralize(2, "Article", "Articles"), "2 Articles") def test_temporary_locale(self): # test with default LC category orig_locale = locale.setlocale(locale.LC_ALL) - with utils.temporary_locale('C'): - self.assertEqual(locale.setlocale(locale.LC_ALL), 'C') + with utils.temporary_locale("C"): + self.assertEqual(locale.setlocale(locale.LC_ALL), "C") self.assertEqual(locale.setlocale(locale.LC_ALL), orig_locale) # test with custom LC category orig_locale = locale.setlocale(locale.LC_TIME) - with utils.temporary_locale('C', locale.LC_TIME): - self.assertEqual(locale.setlocale(locale.LC_TIME), 'C') + with utils.temporary_locale("C", locale.LC_TIME): + self.assertEqual(locale.setlocale(locale.LC_TIME), "C") self.assertEqual(locale.setlocale(locale.LC_TIME), orig_locale) class TestCopy(unittest.TestCase): - '''Tests the copy utility''' + """Tests the copy utility""" def setUp(self): - self.root_dir = mkdtemp(prefix='pelicantests.') + self.root_dir = mkdtemp(prefix="pelicantests.") self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") def tearDown(self): shutil.rmtree(self.root_dir) locale.setlocale(locale.LC_ALL, self.old_locale) def _create_file(self, *path): - with open(os.path.join(self.root_dir, *path), 'w') as f: - f.write('42\n') + with open(os.path.join(self.root_dir, *path), "w") as f: + f.write("42\n") def _create_dir(self, *path): os.makedirs(os.path.join(self.root_dir, *path)) def _exist_file(self, *path): path = os.path.join(self.root_dir, *path) - self.assertTrue(os.path.isfile(path), 'File does not exist: %s' % path) + self.assertTrue(os.path.isfile(path), "File does not exist: %s" % path) def _exist_dir(self, *path): path = os.path.join(self.root_dir, *path) - self.assertTrue(os.path.exists(path), - 'Directory does not exist: %s' % path) + self.assertTrue(os.path.exists(path), "Directory does not exist: %s" % path) def test_copy_file_same_path(self): - self._create_file('a.txt') - utils.copy(os.path.join(self.root_dir, 'a.txt'), - os.path.join(self.root_dir, 'b.txt')) - self._exist_file('b.txt') + self._create_file("a.txt") + utils.copy( + os.path.join(self.root_dir, "a.txt"), os.path.join(self.root_dir, "b.txt") + ) + self._exist_file("b.txt") def test_copy_file_different_path(self): - self._create_dir('a') - self._create_dir('b') - self._create_file('a', 'a.txt') - utils.copy(os.path.join(self.root_dir, 'a', 'a.txt'), - os.path.join(self.root_dir, 'b', 'b.txt')) - self._exist_dir('b') - self._exist_file('b', 'b.txt') + self._create_dir("a") + self._create_dir("b") + self._create_file("a", "a.txt") + utils.copy( + os.path.join(self.root_dir, "a", "a.txt"), + os.path.join(self.root_dir, "b", "b.txt"), + ) + self._exist_dir("b") + self._exist_file("b", "b.txt") def test_copy_file_create_dirs(self): - self._create_file('a.txt') + self._create_file("a.txt") utils.copy( - os.path.join(self.root_dir, 'a.txt'), - os.path.join(self.root_dir, 'b0', 'b1', 'b2', 'b3', 'b.txt')) - self._exist_dir('b0') - self._exist_dir('b0', 'b1') - self._exist_dir('b0', 'b1', 'b2') - self._exist_dir('b0', 'b1', 'b2', 'b3') - self._exist_file('b0', 'b1', 'b2', 'b3', 'b.txt') + os.path.join(self.root_dir, "a.txt"), + os.path.join(self.root_dir, "b0", "b1", "b2", "b3", "b.txt"), + ) + self._exist_dir("b0") + self._exist_dir("b0", "b1") + self._exist_dir("b0", "b1", "b2") + self._exist_dir("b0", "b1", "b2", "b3") + self._exist_file("b0", "b1", "b2", "b3", "b.txt") def test_copy_dir_same_path(self): - self._create_dir('a') - self._create_file('a', 'a.txt') - utils.copy(os.path.join(self.root_dir, 'a'), - os.path.join(self.root_dir, 'b')) - self._exist_dir('b') - self._exist_file('b', 'a.txt') + self._create_dir("a") + self._create_file("a", "a.txt") + utils.copy(os.path.join(self.root_dir, "a"), os.path.join(self.root_dir, "b")) + self._exist_dir("b") + self._exist_file("b", "a.txt") def test_copy_dir_different_path(self): - self._create_dir('a0') - self._create_dir('a0', 'a1') - self._create_file('a0', 'a1', 'a.txt') - self._create_dir('b0') - utils.copy(os.path.join(self.root_dir, 'a0', 'a1'), - os.path.join(self.root_dir, 'b0', 'b1')) - self._exist_dir('b0', 'b1') - self._exist_file('b0', 'b1', 'a.txt') + self._create_dir("a0") + self._create_dir("a0", "a1") + self._create_file("a0", "a1", "a.txt") + self._create_dir("b0") + utils.copy( + os.path.join(self.root_dir, "a0", "a1"), + os.path.join(self.root_dir, "b0", "b1"), + ) + self._exist_dir("b0", "b1") + self._exist_file("b0", "b1", "a.txt") def test_copy_dir_create_dirs(self): - self._create_dir('a') - self._create_file('a', 'a.txt') - utils.copy(os.path.join(self.root_dir, 'a'), - os.path.join(self.root_dir, 'b0', 'b1', 'b2', 'b3', 'b')) - self._exist_dir('b0') - self._exist_dir('b0', 'b1') - self._exist_dir('b0', 'b1', 'b2') - self._exist_dir('b0', 'b1', 'b2', 'b3') - self._exist_dir('b0', 'b1', 'b2', 'b3', 'b') - self._exist_file('b0', 'b1', 'b2', 'b3', 'b', 'a.txt') + self._create_dir("a") + self._create_file("a", "a.txt") + utils.copy( + os.path.join(self.root_dir, "a"), + os.path.join(self.root_dir, "b0", "b1", "b2", "b3", "b"), + ) + self._exist_dir("b0") + self._exist_dir("b0", "b1") + self._exist_dir("b0", "b1", "b2") + self._exist_dir("b0", "b1", "b2", "b3") + self._exist_dir("b0", "b1", "b2", "b3", "b") + self._exist_file("b0", "b1", "b2", "b3", "b", "a.txt") class TestDateFormatter(unittest.TestCase): - '''Tests that the output of DateFormatter jinja filter is same as - utils.strftime''' + """Tests that the output of DateFormatter jinja filter is same as + utils.strftime""" def setUp(self): # prepare a temp content and output folder - self.temp_content = mkdtemp(prefix='pelicantests.') - self.temp_output = mkdtemp(prefix='pelicantests.') + self.temp_content = mkdtemp(prefix="pelicantests.") + self.temp_output = mkdtemp(prefix="pelicantests.") # prepare a template file - template_dir = os.path.join(self.temp_content, 'template') - template_path = os.path.join(template_dir, 'source.html') + template_dir = os.path.join(self.temp_content, "template") + template_path = os.path.join(template_dir, "source.html") os.makedirs(template_dir) - with open(template_path, 'w') as template_file: + with open(template_path, "w") as template_file: template_file.write('date = {{ date|strftime("%A, %d %B %Y") }}') self.date = utils.SafeDatetime(2012, 8, 29) @@ -666,136 +812,128 @@ class TestDateFormatter(unittest.TestCase): shutil.rmtree(self.temp_content) shutil.rmtree(self.temp_output) # reset locale to default - locale.setlocale(locale.LC_ALL, '') + locale.setlocale(locale.LC_ALL, "") - @unittest.skipUnless(locale_available('fr_FR.UTF-8') or - locale_available('French'), - 'French locale needed') + @unittest.skipUnless( + locale_available("fr_FR.UTF-8") or locale_available("French"), + "French locale needed", + ) def test_french_strftime(self): # This test tries to reproduce an issue that # occurred with python3.3 under macos10 only - temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' + temp_locale = "French" if platform == "win32" else "fr_FR.UTF-8" with utils.temporary_locale(temp_locale): date = utils.SafeDatetime(2014, 8, 14) # we compare the lower() dates since macos10 returns # "Jeudi" for %A whereas linux reports "jeudi" self.assertEqual( - 'jeudi, 14 août 2014', - utils.strftime(date, date_format="%A, %d %B %Y").lower()) + "jeudi, 14 août 2014", + utils.strftime(date, date_format="%A, %d %B %Y").lower(), + ) df = utils.DateFormatter() self.assertEqual( - 'jeudi, 14 août 2014', - df(date, date_format="%A, %d %B %Y").lower()) + "jeudi, 14 août 2014", df(date, date_format="%A, %d %B %Y").lower() + ) # Let us now set the global locale to C: - with utils.temporary_locale('C'): + with utils.temporary_locale("C"): # DateFormatter should still work as expected # since it is the whole point of DateFormatter # (This is where pre-2014/4/15 code fails on macos10) df_date = df(date, date_format="%A, %d %B %Y").lower() - self.assertEqual('jeudi, 14 août 2014', df_date) + self.assertEqual("jeudi, 14 août 2014", df_date) - @unittest.skipUnless(locale_available('fr_FR.UTF-8') or - locale_available('French'), - 'French locale needed') + @unittest.skipUnless( + locale_available("fr_FR.UTF-8") or locale_available("French"), + "French locale needed", + ) def test_french_locale(self): - if platform == 'win32': - locale_string = 'French' + if platform == "win32": + locale_string = "French" else: - locale_string = 'fr_FR.UTF-8' + locale_string = "fr_FR.UTF-8" settings = read_settings( override={ - 'LOCALE': locale_string, - 'TEMPLATE_PAGES': { - 'template/source.html': 'generated/file.html' - } - }) + "LOCALE": locale_string, + "TEMPLATE_PAGES": {"template/source.html": "generated/file.html"}, + } + ) generator = TemplatePagesGenerator( - {'date': self.date}, settings, - self.temp_content, '', self.temp_output) - generator.env.filters.update({'strftime': utils.DateFormatter()}) + {"date": self.date}, settings, self.temp_content, "", self.temp_output + ) + generator.env.filters.update({"strftime": utils.DateFormatter()}) writer = Writer(self.temp_output, settings=settings) generator.generate_output(writer) - output_path = os.path.join( - self.temp_output, 'generated', 'file.html') + output_path = os.path.join(self.temp_output, "generated", "file.html") # output file has been generated self.assertTrue(os.path.exists(output_path)) # output content is correct with utils.pelican_open(output_path) as output_file: - self.assertEqual(output_file, - utils.strftime(self.date, 'date = %A, %d %B %Y')) + self.assertEqual( + output_file, utils.strftime(self.date, "date = %A, %d %B %Y") + ) - @unittest.skipUnless(locale_available('tr_TR.UTF-8') or - locale_available('Turkish'), - 'Turkish locale needed') + @unittest.skipUnless( + locale_available("tr_TR.UTF-8") or locale_available("Turkish"), + "Turkish locale needed", + ) def test_turkish_locale(self): - if platform == 'win32': - locale_string = 'Turkish' + if platform == "win32": + locale_string = "Turkish" else: - locale_string = 'tr_TR.UTF-8' + locale_string = "tr_TR.UTF-8" settings = read_settings( override={ - 'LOCALE': locale_string, - 'TEMPLATE_PAGES': { - 'template/source.html': 'generated/file.html' - } - }) + "LOCALE": locale_string, + "TEMPLATE_PAGES": {"template/source.html": "generated/file.html"}, + } + ) generator = TemplatePagesGenerator( - {'date': self.date}, settings, - self.temp_content, '', self.temp_output) - generator.env.filters.update({'strftime': utils.DateFormatter()}) + {"date": self.date}, settings, self.temp_content, "", self.temp_output + ) + generator.env.filters.update({"strftime": utils.DateFormatter()}) writer = Writer(self.temp_output, settings=settings) generator.generate_output(writer) - output_path = os.path.join( - self.temp_output, 'generated', 'file.html') + output_path = os.path.join(self.temp_output, "generated", "file.html") # output file has been generated self.assertTrue(os.path.exists(output_path)) # output content is correct with utils.pelican_open(output_path) as output_file: - self.assertEqual(output_file, - utils.strftime(self.date, 'date = %A, %d %B %Y')) + self.assertEqual( + output_file, utils.strftime(self.date, "date = %A, %d %B %Y") + ) class TestSanitisedJoin(unittest.TestCase): def test_detect_parent_breakout(self): with self.assertRaisesRegex( - RuntimeError, - "Attempted to break out of output directory to " - "(.*?:)?/foo/test"): # (.*?:)? accounts for Windows root - utils.sanitised_join( - "/foo/bar", - "../test" - ) + RuntimeError, + "Attempted to break out of output directory to " "(.*?:)?/foo/test", + ): # (.*?:)? accounts for Windows root + utils.sanitised_join("/foo/bar", "../test") def test_detect_root_breakout(self): with self.assertRaisesRegex( - RuntimeError, - "Attempted to break out of output directory to " - "(.*?:)?/test"): # (.*?:)? accounts for Windows root - utils.sanitised_join( - "/foo/bar", - "/test" - ) + RuntimeError, + "Attempted to break out of output directory to " "(.*?:)?/test", + ): # (.*?:)? accounts for Windows root + utils.sanitised_join("/foo/bar", "/test") def test_pass_deep_subpaths(self): self.assertEqual( - utils.sanitised_join( - "/foo/bar", - "test" - ), - utils.posixize_path( - os.path.abspath(os.path.join("/foo/bar", "test"))) + utils.sanitised_join("/foo/bar", "test"), + utils.posixize_path(os.path.abspath(os.path.join("/foo/bar", "test"))), ) @@ -812,7 +950,7 @@ class TestMemoized(unittest.TestCase): container = Container() with unittest.mock.patch.object( - container, "_get", side_effect=lambda x: x + container, "_get", side_effect=lambda x: x ) as get_mock: self.assertEqual("foo", container.get("foo")) get_mock.assert_called_once_with("foo") diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 95e196ba..27102f38 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -47,74 +47,69 @@ def decode_wp_content(content, br=True): pre_index += 1 content = content + last_pre - content = re.sub(r'
\s*
', "\n\n", content) - allblocks = ('(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|' - 'td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|' - 'map|area|blockquote|address|math|style|p|h[1-6]|hr|' - 'fieldset|noscript|samp|legend|section|article|aside|' - 'hgroup|header|footer|nav|figure|figcaption|details|' - 'menu|summary)') - content = re.sub(r'(<' + allblocks + r'[^>]*>)', "\n\\1", content) - content = re.sub(r'()', "\\1\n\n", content) + content = re.sub(r"
\s*
", "\n\n", content) + allblocks = ( + "(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|" + "td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|" + "map|area|blockquote|address|math|style|p|h[1-6]|hr|" + "fieldset|noscript|samp|legend|section|article|aside|" + "hgroup|header|footer|nav|figure|figcaption|details|" + "menu|summary)" + ) + content = re.sub(r"(<" + allblocks + r"[^>]*>)", "\n\\1", content) + content = re.sub(r"()", "\\1\n\n", content) # content = content.replace("\r\n", "\n") if " inside object/embed - content = re.sub(r'\s*]*)>\s*', "", content) - content = re.sub(r'\s*\s*', '', content) + content = re.sub(r"\s*]*)>\s*", "", content) + content = re.sub(r"\s*\s*", "", content) # content = re.sub(r'/\n\n+/', '\n\n', content) - pgraphs = filter(lambda s: s != "", re.split(r'\n\s*\n', content)) + pgraphs = filter(lambda s: s != "", re.split(r"\n\s*\n", content)) content = "" for p in pgraphs: content = content + "

" + p.strip() + "

\n" # under certain strange conditions it could create # a P of entirely whitespace - content = re.sub(r'

\s*

', '', content) - content = re.sub( - r'

([^<]+)', - "

\\1

", - content) + content = re.sub(r"

\s*

", "", content) + content = re.sub(r"

([^<]+)", "

\\1

", content) # don't wrap tags - content = re.sub( - r'

\s*(]*>)\s*

', - "\\1", - content) + content = re.sub(r"

\s*(]*>)\s*

", "\\1", content) # problem with nested lists - content = re.sub(r'

(', "\\1", content) - content = re.sub(r'

]*)>', "

", content) - content = content.replace('

', '

') - content = re.sub(r'

\s*(]*>)', "\\1", content) - content = re.sub(r'(]*>)\s*

', "\\1", content) + content = re.sub(r"

(", "\\1", content) + content = re.sub(r"

]*)>", "

", content) + content = content.replace("

", "

") + content = re.sub(r"

\s*(]*>)", "\\1", content) + content = re.sub(r"(]*>)\s*

", "\\1", content) if br: + def _preserve_newline(match): return match.group(0).replace("\n", "") - content = re.sub( - r'/<(script|style).*?<\/\\1>/s', - _preserve_newline, - content) + + content = re.sub(r"/<(script|style).*?<\/\\1>/s", _preserve_newline, content) # optionally make line breaks - content = re.sub(r'(?)\s*\n', "
\n", content) + content = re.sub(r"(?)\s*\n", "
\n", content) content = content.replace("", "\n") + content = re.sub(r"(]*>)\s*
", "\\1", content) content = re.sub( - r'(]*>)\s*
', "\\1", - content) - content = re.sub( - r'
(\s*]*>)', - '\\1', - content) - content = re.sub(r'\n

', "

", content) + r"
(\s*]*>)", "\\1", content + ) + content = re.sub(r"\n

", "

", content) if pre_tags: + def _multi_replace(dic, string): - pattern = r'|'.join(map(re.escape, dic.keys())) + pattern = r"|".join(map(re.escape, dic.keys())) return re.sub(pattern, lambda m: dic[m.group()], string) + content = _multi_replace(pre_tags, content) # convert [caption] tags into
content = re.sub( - r'\[caption(?:.*?)(?:caption=\"(.*?)\")?\]' - r'((?:\)?(?:\)(?:\<\/a\>)?)\s?(.*?)\[\/caption\]', - r'
\n\2\n
\1\3
\n
', - content) + r"\[caption(?:.*?)(?:caption=\"(.*?)\")?\]" + r"((?:\)?(?:\)(?:\<\/a\>)?)\s?(.*?)\[\/caption\]", + r"
\n\2\n
\1\3
\n
", + content, + ) return content @@ -124,10 +119,12 @@ def xml_to_soup(xml): try: from bs4 import BeautifulSoup except ImportError: - error = ('Missing dependency "BeautifulSoup4" and "lxml" required to ' - 'import XML files.') + error = ( + 'Missing dependency "BeautifulSoup4" and "lxml" required to ' + "import XML files." + ) sys.exit(error) - with open(xml, encoding='utf-8') as infile: + with open(xml, encoding="utf-8") as infile: xmlfile = infile.read() soup = BeautifulSoup(xmlfile, "xml") return soup @@ -144,111 +141,125 @@ def wp2fields(xml, wp_custpost=False): """Opens a wordpress XML file, and yield Pelican fields""" soup = xml_to_soup(xml) - items = soup.rss.channel.findAll('item') + items = soup.rss.channel.findAll("item") for item in items: - - if item.find('status').string in ["publish", "draft"]: - + if item.find("status").string in ["publish", "draft"]: try: # Use HTMLParser due to issues with BeautifulSoup 3 title = unescape(item.title.contents[0]) except IndexError: - title = 'No title [%s]' % item.find('post_name').string + title = "No title [%s]" % item.find("post_name").string logger.warning('Post "%s" is lacking a proper title', title) - post_name = item.find('post_name').string - post_id = item.find('post_id').string + post_name = item.find("post_name").string + post_id = item.find("post_id").string filename = get_filename(post_name, post_id) - content = item.find('encoded').string - raw_date = item.find('post_date').string - if raw_date == '0000-00-00 00:00:00': + content = item.find("encoded").string + raw_date = item.find("post_date").string + if raw_date == "0000-00-00 00:00:00": date = None else: - date_object = SafeDatetime.strptime( - raw_date, '%Y-%m-%d %H:%M:%S') - date = date_object.strftime('%Y-%m-%d %H:%M') - author = item.find('creator').string + date_object = SafeDatetime.strptime(raw_date, "%Y-%m-%d %H:%M:%S") + date = date_object.strftime("%Y-%m-%d %H:%M") + author = item.find("creator").string - categories = [cat.string for cat - in item.findAll('category', {'domain': 'category'})] + categories = [ + cat.string for cat in item.findAll("category", {"domain": "category"}) + ] - tags = [tag.string for tag - in item.findAll('category', {'domain': 'post_tag'})] + tags = [ + tag.string for tag in item.findAll("category", {"domain": "post_tag"}) + ] # To publish a post the status should be 'published' - status = 'published' if item.find('status').string == "publish" \ - else item.find('status').string + status = ( + "published" + if item.find("status").string == "publish" + else item.find("status").string + ) - kind = 'article' - post_type = item.find('post_type').string - if post_type == 'page': - kind = 'page' + kind = "article" + post_type = item.find("post_type").string + if post_type == "page": + kind = "page" elif wp_custpost: - if post_type == 'post': + if post_type == "post": pass # Old behaviour was to name everything not a page as an # article.Theoretically all attachments have status == inherit # so no attachments should be here. But this statement is to # maintain existing behaviour in case that doesn't hold true. - elif post_type == 'attachment': + elif post_type == "attachment": pass else: kind = post_type - yield (title, content, filename, date, author, categories, - tags, status, kind, 'wp-html') + yield ( + title, + content, + filename, + date, + author, + categories, + tags, + status, + kind, + "wp-html", + ) def blogger2fields(xml): """Opens a blogger XML file, and yield Pelican fields""" soup = xml_to_soup(xml) - entries = soup.feed.findAll('entry') + entries = soup.feed.findAll("entry") for entry in entries: raw_kind = entry.find( - 'category', {'scheme': 'http://schemas.google.com/g/2005#kind'} - ).get('term') - if raw_kind == 'http://schemas.google.com/blogger/2008/kind#post': - kind = 'article' - elif raw_kind == 'http://schemas.google.com/blogger/2008/kind#comment': - kind = 'comment' - elif raw_kind == 'http://schemas.google.com/blogger/2008/kind#page': - kind = 'page' + "category", {"scheme": "http://schemas.google.com/g/2005#kind"} + ).get("term") + if raw_kind == "http://schemas.google.com/blogger/2008/kind#post": + kind = "article" + elif raw_kind == "http://schemas.google.com/blogger/2008/kind#comment": + kind = "comment" + elif raw_kind == "http://schemas.google.com/blogger/2008/kind#page": + kind = "page" else: continue try: - assert kind != 'comment' - filename = entry.find('link', {'rel': 'alternate'})['href'] + assert kind != "comment" + filename = entry.find("link", {"rel": "alternate"})["href"] filename = os.path.splitext(os.path.basename(filename))[0] except (AssertionError, TypeError, KeyError): - filename = entry.find('id').string.split('.')[-1] + filename = entry.find("id").string.split(".")[-1] - title = entry.find('title').string or '' + title = entry.find("title").string or "" - content = entry.find('content').string - raw_date = entry.find('published').string - if hasattr(SafeDatetime, 'fromisoformat'): + content = entry.find("content").string + raw_date = entry.find("published").string + if hasattr(SafeDatetime, "fromisoformat"): date_object = SafeDatetime.fromisoformat(raw_date) else: - date_object = SafeDatetime.strptime( - raw_date[:23], '%Y-%m-%dT%H:%M:%S.%f') - date = date_object.strftime('%Y-%m-%d %H:%M') - author = entry.find('author').find('name').string + date_object = SafeDatetime.strptime(raw_date[:23], "%Y-%m-%dT%H:%M:%S.%f") + date = date_object.strftime("%Y-%m-%d %H:%M") + author = entry.find("author").find("name").string # blogger posts only have tags, no category - tags = [tag.get('term') for tag in entry.findAll( - 'category', {'scheme': 'http://www.blogger.com/atom/ns#'})] + tags = [ + tag.get("term") + for tag in entry.findAll( + "category", {"scheme": "http://www.blogger.com/atom/ns#"} + ) + ] # Drafts have yes - status = 'published' + status = "published" try: - if entry.find('control').find('draft').string == 'yes': - status = 'draft' + if entry.find("control").find("draft").string == "yes": + status = "draft" except AttributeError: pass - yield (title, content, filename, date, author, None, tags, status, - kind, 'html') + yield (title, content, filename, date, author, None, tags, status, kind, "html") def dc2fields(file): @@ -256,9 +267,11 @@ def dc2fields(file): try: from bs4 import BeautifulSoup except ImportError: - error = ('Missing dependency ' - '"BeautifulSoup4" and "lxml" required ' - 'to import Dotclear files.') + error = ( + "Missing dependency " + '"BeautifulSoup4" and "lxml" required ' + "to import Dotclear files." + ) sys.exit(error) in_cat = False @@ -266,15 +279,14 @@ def dc2fields(file): category_list = {} posts = [] - with open(file, encoding='utf-8') as f: - + with open(file, encoding="utf-8") as f: for line in f: # remove final \n line = line[:-1] - if line.startswith('[category'): + if line.startswith("[category"): in_cat = True - elif line.startswith('[post'): + elif line.startswith("[post"): in_post = True elif in_cat: fields = line.split('","') @@ -294,7 +306,7 @@ def dc2fields(file): print("%i posts read." % len(posts)) - subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] + subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"] for post in posts: fields = post.split('","') @@ -329,44 +341,39 @@ def dc2fields(file): # redirect_url = fields[28][:-1] # remove seconds - post_creadt = ':'.join(post_creadt.split(':')[0:2]) + post_creadt = ":".join(post_creadt.split(":")[0:2]) - author = '' + author = "" categories = [] tags = [] if cat_id: - categories = [category_list[id].strip() for id - in cat_id.split(',')] + categories = [category_list[id].strip() for id in cat_id.split(",")] # Get tags related to a post - tag = (post_meta.replace('{', '') - .replace('}', '') - .replace('a:1:s:3:\\"tag\\";a:', '') - .replace('a:0:', '')) + tag = ( + post_meta.replace("{", "") + .replace("}", "") + .replace('a:1:s:3:\\"tag\\";a:', "") + .replace("a:0:", "") + ) if len(tag) > 1: if int(len(tag[:1])) == 1: newtag = tag.split('"')[1] tags.append( - BeautifulSoup( - newtag, - 'xml' - ) + BeautifulSoup(newtag, "xml") # bs4 always outputs UTF-8 - .decode('utf-8') + .decode("utf-8") ) else: i = 1 j = 1 - while (i <= int(tag[:1])): - newtag = tag.split('"')[j].replace('\\', '') + while i <= int(tag[:1]): + newtag = tag.split('"')[j].replace("\\", "") tags.append( - BeautifulSoup( - newtag, - 'xml' - ) + BeautifulSoup(newtag, "xml") # bs4 always outputs UTF-8 - .decode('utf-8') + .decode("utf-8") ) i = i + 1 if j < int(tag[:1]) * 2: @@ -381,116 +388,149 @@ def dc2fields(file): content = post_excerpt + post_content else: content = post_excerpt_xhtml + post_content_xhtml - content = content.replace('\\n', '') + content = content.replace("\\n", "") post_format = "html" - kind = 'article' # TODO: Recognise pages - status = 'published' # TODO: Find a way for draft posts + kind = "article" # TODO: Recognise pages + status = "published" # TODO: Find a way for draft posts - yield (post_title, content, slugify(post_title, regex_subs=subs), - post_creadt, author, categories, tags, status, kind, - post_format) + yield ( + post_title, + content, + slugify(post_title, regex_subs=subs), + post_creadt, + author, + categories, + tags, + status, + kind, + post_format, + ) def _get_tumblr_posts(api_key, blogname, offset=0): import json import urllib.request as urllib_request - url = ("https://api.tumblr.com/v2/blog/%s.tumblr.com/" - "posts?api_key=%s&offset=%d&filter=raw") % ( - blogname, api_key, offset) + + url = ( + "https://api.tumblr.com/v2/blog/%s.tumblr.com/" + "posts?api_key=%s&offset=%d&filter=raw" + ) % (blogname, api_key, offset) request = urllib_request.Request(url) handle = urllib_request.urlopen(request) - posts = json.loads(handle.read().decode('utf-8')) - return posts.get('response').get('posts') + posts = json.loads(handle.read().decode("utf-8")) + return posts.get("response").get("posts") def tumblr2fields(api_key, blogname): - """ Imports Tumblr posts (API v2)""" + """Imports Tumblr posts (API v2)""" offset = 0 posts = _get_tumblr_posts(api_key, blogname, offset) - subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] + subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"] while len(posts) > 0: for post in posts: - title = \ - post.get('title') or \ - post.get('source_title') or \ - post.get('type').capitalize() - slug = post.get('slug') or slugify(title, regex_subs=subs) - tags = post.get('tags') - timestamp = post.get('timestamp') + title = ( + post.get("title") + or post.get("source_title") + or post.get("type").capitalize() + ) + slug = post.get("slug") or slugify(title, regex_subs=subs) + tags = post.get("tags") + timestamp = post.get("timestamp") date = SafeDatetime.fromtimestamp( int(timestamp), tz=datetime.timezone.utc ).strftime("%Y-%m-%d %H:%M:%S%z") - slug = SafeDatetime.fromtimestamp( - int(timestamp), tz=datetime.timezone.utc - ).strftime("%Y-%m-%d-") + slug - format = post.get('format') - content = post.get('body') - type = post.get('type') - if type == 'photo': - if format == 'markdown': - fmtstr = '![%s](%s)' + slug = ( + SafeDatetime.fromtimestamp( + int(timestamp), tz=datetime.timezone.utc + ).strftime("%Y-%m-%d-") + + slug + ) + format = post.get("format") + content = post.get("body") + type = post.get("type") + if type == "photo": + if format == "markdown": + fmtstr = "![%s](%s)" else: fmtstr = '%s' - content = '\n'.join( - fmtstr % (photo.get('caption'), - photo.get('original_size').get('url')) - for photo in post.get('photos')) - elif type == 'quote': - if format == 'markdown': - fmtstr = '\n\n— %s' + content = "\n".join( + fmtstr + % (photo.get("caption"), photo.get("original_size").get("url")) + for photo in post.get("photos") + ) + elif type == "quote": + if format == "markdown": + fmtstr = "\n\n— %s" else: - fmtstr = '

— %s

' - content = post.get('text') + fmtstr % post.get('source') - elif type == 'link': - if format == 'markdown': - fmtstr = '[via](%s)\n\n' + fmtstr = "

— %s

" + content = post.get("text") + fmtstr % post.get("source") + elif type == "link": + if format == "markdown": + fmtstr = "[via](%s)\n\n" else: fmtstr = '

via

\n' - content = fmtstr % post.get('url') + post.get('description') - elif type == 'audio': - if format == 'markdown': - fmtstr = '[via](%s)\n\n' + content = fmtstr % post.get("url") + post.get("description") + elif type == "audio": + if format == "markdown": + fmtstr = "[via](%s)\n\n" else: fmtstr = '

via

\n' - content = fmtstr % post.get('source_url') + \ - post.get('caption') + \ - post.get('player') - elif type == 'video': - if format == 'markdown': - fmtstr = '[via](%s)\n\n' + content = ( + fmtstr % post.get("source_url") + + post.get("caption") + + post.get("player") + ) + elif type == "video": + if format == "markdown": + fmtstr = "[via](%s)\n\n" else: fmtstr = '

via

\n' - source = fmtstr % post.get('source_url') - caption = post.get('caption') + source = fmtstr % post.get("source_url") + caption = post.get("caption") players = [ # If embed_code is False, couldn't get the video - player.get('embed_code') or None - for player in post.get('player')] + player.get("embed_code") or None + for player in post.get("player") + ] # If there are no embeddable players, say so, once - if len(players) > 0 and all( - player is None for player in players): + if len(players) > 0 and all(player is None for player in players): players = "

(This video isn't available anymore.)

\n" else: - players = '\n'.join(players) + players = "\n".join(players) content = source + caption + players - elif type == 'answer': - title = post.get('question') - content = ('

' - '%s' - ': %s' - '

\n' - ' %s' % (post.get('asking_name'), - post.get('asking_url'), - post.get('question'), - post.get('answer'))) + elif type == "answer": + title = post.get("question") + content = ( + "

" + '%s' + ": %s" + "

\n" + " %s" + % ( + post.get("asking_name"), + post.get("asking_url"), + post.get("question"), + post.get("answer"), + ) + ) - content = content.rstrip() + '\n' - kind = 'article' - status = 'published' # TODO: Find a way for draft posts + content = content.rstrip() + "\n" + kind = "article" + status = "published" # TODO: Find a way for draft posts - yield (title, content, slug, date, post.get('blog_name'), [type], - tags, status, kind, format) + yield ( + title, + content, + slug, + date, + post.get("blog_name"), + [type], + tags, + status, + kind, + format, + ) offset += len(posts) posts = _get_tumblr_posts(api_key, blogname, offset) @@ -499,145 +539,167 @@ def tumblr2fields(api_key, blogname): def feed2fields(file): """Read a feed and yield pelican fields""" import feedparser + d = feedparser.parse(file) - subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] + subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"] for entry in d.entries: - date = (time.strftime('%Y-%m-%d %H:%M', entry.updated_parsed) - if hasattr(entry, 'updated_parsed') else None) - author = entry.author if hasattr(entry, 'author') else None - tags = ([e['term'] for e in entry.tags] - if hasattr(entry, 'tags') else None) + date = ( + time.strftime("%Y-%m-%d %H:%M", entry.updated_parsed) + if hasattr(entry, "updated_parsed") + else None + ) + author = entry.author if hasattr(entry, "author") else None + tags = [e["term"] for e in entry.tags] if hasattr(entry, "tags") else None slug = slugify(entry.title, regex_subs=subs) - kind = 'article' - yield (entry.title, entry.description, slug, date, - author, [], tags, None, kind, 'html') + kind = "article" + yield ( + entry.title, + entry.description, + slug, + date, + author, + [], + tags, + None, + kind, + "html", + ) -def build_header(title, date, author, categories, tags, slug, - status=None, attachments=None): +def build_header( + title, date, author, categories, tags, slug, status=None, attachments=None +): """Build a header from a list of fields""" from docutils.utils import column_width - header = '{}\n{}\n'.format(title, '#' * column_width(title)) + header = "{}\n{}\n".format(title, "#" * column_width(title)) if date: - header += ':date: %s\n' % date + header += ":date: %s\n" % date if author: - header += ':author: %s\n' % author + header += ":author: %s\n" % author if categories: - header += ':category: %s\n' % ', '.join(categories) + header += ":category: %s\n" % ", ".join(categories) if tags: - header += ':tags: %s\n' % ', '.join(tags) + header += ":tags: %s\n" % ", ".join(tags) if slug: - header += ':slug: %s\n' % slug + header += ":slug: %s\n" % slug if status: - header += ':status: %s\n' % status + header += ":status: %s\n" % status if attachments: - header += ':attachments: %s\n' % ', '.join(attachments) - header += '\n' + header += ":attachments: %s\n" % ", ".join(attachments) + header += "\n" return header -def build_asciidoc_header(title, date, author, categories, tags, slug, - status=None, attachments=None): +def build_asciidoc_header( + title, date, author, categories, tags, slug, status=None, attachments=None +): """Build a header from a list of fields""" - header = '= %s\n' % title + header = "= %s\n" % title if author: - header += '%s\n' % author + header += "%s\n" % author if date: - header += '%s\n' % date + header += "%s\n" % date if categories: - header += ':category: %s\n' % ', '.join(categories) + header += ":category: %s\n" % ", ".join(categories) if tags: - header += ':tags: %s\n' % ', '.join(tags) + header += ":tags: %s\n" % ", ".join(tags) if slug: - header += ':slug: %s\n' % slug + header += ":slug: %s\n" % slug if status: - header += ':status: %s\n' % status + header += ":status: %s\n" % status if attachments: - header += ':attachments: %s\n' % ', '.join(attachments) - header += '\n' + header += ":attachments: %s\n" % ", ".join(attachments) + header += "\n" return header -def build_markdown_header(title, date, author, categories, tags, - slug, status=None, attachments=None): +def build_markdown_header( + title, date, author, categories, tags, slug, status=None, attachments=None +): """Build a header from a list of fields""" - header = 'Title: %s\n' % title + header = "Title: %s\n" % title if date: - header += 'Date: %s\n' % date + header += "Date: %s\n" % date if author: - header += 'Author: %s\n' % author + header += "Author: %s\n" % author if categories: - header += 'Category: %s\n' % ', '.join(categories) + header += "Category: %s\n" % ", ".join(categories) if tags: - header += 'Tags: %s\n' % ', '.join(tags) + header += "Tags: %s\n" % ", ".join(tags) if slug: - header += 'Slug: %s\n' % slug + header += "Slug: %s\n" % slug if status: - header += 'Status: %s\n' % status + header += "Status: %s\n" % status if attachments: - header += 'Attachments: %s\n' % ', '.join(attachments) - header += '\n' + header += "Attachments: %s\n" % ", ".join(attachments) + header += "\n" return header -def get_ext(out_markup, in_markup='html'): - if out_markup == 'asciidoc': - ext = '.adoc' - elif in_markup == 'markdown' or out_markup == 'markdown': - ext = '.md' +def get_ext(out_markup, in_markup="html"): + if out_markup == "asciidoc": + ext = ".adoc" + elif in_markup == "markdown" or out_markup == "markdown": + ext = ".md" else: - ext = '.rst' + ext = ".rst" return ext -def get_out_filename(output_path, filename, ext, kind, - dirpage, dircat, categories, wp_custpost, slug_subs): +def get_out_filename( + output_path, + filename, + ext, + kind, + dirpage, + dircat, + categories, + wp_custpost, + slug_subs, +): filename = os.path.basename(filename) # Enforce filename restrictions for various filesystems at once; see # https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words # we do not need to filter words because an extension will be appended - filename = re.sub(r'[<>:"/\\|?*^% ]', '-', filename) # invalid chars - filename = filename.lstrip('.') # should not start with a dot + filename = re.sub(r'[<>:"/\\|?*^% ]', "-", filename) # invalid chars + filename = filename.lstrip(".") # should not start with a dot if not filename: - filename = '_' + filename = "_" filename = filename[:249] # allow for 5 extra characters out_filename = os.path.join(output_path, filename + ext) # option to put page posts in pages/ subdirectory - if dirpage and kind == 'page': - pages_dir = os.path.join(output_path, 'pages') + if dirpage and kind == "page": + pages_dir = os.path.join(output_path, "pages") if not os.path.isdir(pages_dir): os.mkdir(pages_dir) out_filename = os.path.join(pages_dir, filename + ext) - elif not dirpage and kind == 'page': + elif not dirpage and kind == "page": pass # option to put wp custom post types in directories with post type # names. Custom post types can also have categories so option to # create subdirectories with category names - elif kind != 'article': + elif kind != "article": if wp_custpost: typename = slugify(kind, regex_subs=slug_subs) else: - typename = '' - kind = 'article' + typename = "" + kind = "article" if dircat and (len(categories) > 0): - catname = slugify( - categories[0], regex_subs=slug_subs, preserve_case=True) + catname = slugify(categories[0], regex_subs=slug_subs, preserve_case=True) else: - catname = '' - out_filename = os.path.join(output_path, typename, - catname, filename + ext) + catname = "" + out_filename = os.path.join(output_path, typename, catname, filename + ext) if not os.path.isdir(os.path.join(output_path, typename, catname)): os.makedirs(os.path.join(output_path, typename, catname)) # option to put files in directories with categories names elif dircat and (len(categories) > 0): - catname = slugify( - categories[0], regex_subs=slug_subs, preserve_case=True) + catname = slugify(categories[0], regex_subs=slug_subs, preserve_case=True) out_filename = os.path.join(output_path, catname, filename + ext) if not os.path.isdir(os.path.join(output_path, catname)): os.mkdir(os.path.join(output_path, catname)) @@ -650,18 +712,19 @@ def get_attachments(xml): of the attachment_urls """ soup = xml_to_soup(xml) - items = soup.rss.channel.findAll('item') + items = soup.rss.channel.findAll("item") names = {} attachments = [] for item in items: - kind = item.find('post_type').string - post_name = item.find('post_name').string - post_id = item.find('post_id').string + kind = item.find("post_type").string + post_name = item.find("post_name").string + post_id = item.find("post_id").string - if kind == 'attachment': - attachments.append((item.find('post_parent').string, - item.find('attachment_url').string)) + if kind == "attachment": + attachments.append( + (item.find("post_parent").string, item.find("attachment_url").string) + ) else: filename = get_filename(post_name, post_id) names[post_id] = filename @@ -686,23 +749,23 @@ def download_attachments(output_path, urls): path = urlparse(url).path # teardown path and rebuild to negate any errors with # os.path.join and leading /'s - path = path.split('/') + path = path.split("/") filename = path.pop(-1) - localpath = '' + localpath = "" for item in path: - if sys.platform != 'win32' or ':' not in item: + if sys.platform != "win32" or ":" not in item: localpath = os.path.join(localpath, item) full_path = os.path.join(output_path, localpath) # Generate percent-encoded URL scheme, netloc, path, query, fragment = urlsplit(url) - if scheme != 'file': + if scheme != "file": path = quote(path) url = urlunsplit((scheme, netloc, path, query, fragment)) if not os.path.exists(full_path): os.makedirs(full_path) - print('downloading {}'.format(filename)) + print("downloading {}".format(filename)) try: urlretrieve(url, os.path.join(full_path, filename)) locations[url] = os.path.join(localpath, filename) @@ -713,43 +776,61 @@ def download_attachments(output_path, urls): def is_pandoc_needed(in_markup): - return in_markup in ('html', 'wp-html') + return in_markup in ("html", "wp-html") def get_pandoc_version(): - cmd = ['pandoc', '--version'] + cmd = ["pandoc", "--version"] try: output = subprocess.check_output(cmd, universal_newlines=True) except (subprocess.CalledProcessError, OSError) as e: logger.warning("Pandoc version unknown: %s", e) return () - return tuple(int(i) for i in output.split()[1].split('.')) + return tuple(int(i) for i in output.split()[1].split(".")) def update_links_to_attached_files(content, attachments): for old_url, new_path in attachments.items(): # url may occur both with http:// and https:// - http_url = old_url.replace('https://', 'http://') - https_url = old_url.replace('http://', 'https://') + http_url = old_url.replace("https://", "http://") + https_url = old_url.replace("http://", "https://") for url in [http_url, https_url]: - content = content.replace(url, '{static}' + new_path) + content = content.replace(url, "{static}" + new_path) return content def fields2pelican( - fields, out_markup, output_path, - dircat=False, strip_raw=False, disable_slugs=False, - dirpage=False, filename_template=None, filter_author=None, - wp_custpost=False, wp_attach=False, attachments=None): - + fields, + out_markup, + output_path, + dircat=False, + strip_raw=False, + disable_slugs=False, + dirpage=False, + filename_template=None, + filter_author=None, + wp_custpost=False, + wp_attach=False, + attachments=None, +): pandoc_version = get_pandoc_version() posts_require_pandoc = [] - slug_subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'] + slug_subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"] - for (title, content, filename, date, author, categories, tags, status, - kind, in_markup) in fields: + for ( + title, + content, + filename, + date, + author, + categories, + tags, + status, + kind, + in_markup, + ) in fields: if filter_author and filter_author != author: continue if is_pandoc_needed(in_markup) and not pandoc_version: @@ -767,85 +848,120 @@ def fields2pelican( links = None ext = get_ext(out_markup, in_markup) - if ext == '.adoc': - header = build_asciidoc_header(title, date, author, categories, - tags, slug, status, attachments) - elif ext == '.md': + if ext == ".adoc": + header = build_asciidoc_header( + title, date, author, categories, tags, slug, status, attachments + ) + elif ext == ".md": header = build_markdown_header( - title, date, author, categories, tags, slug, - status, links.values() if links else None) + title, + date, + author, + categories, + tags, + slug, + status, + links.values() if links else None, + ) else: - out_markup = 'rst' - header = build_header(title, date, author, categories, - tags, slug, status, links.values() - if links else None) + out_markup = "rst" + header = build_header( + title, + date, + author, + categories, + tags, + slug, + status, + links.values() if links else None, + ) out_filename = get_out_filename( - output_path, filename, ext, kind, dirpage, dircat, - categories, wp_custpost, slug_subs) + output_path, + filename, + ext, + kind, + dirpage, + dircat, + categories, + wp_custpost, + slug_subs, + ) print(out_filename) - if in_markup in ('html', 'wp-html'): + if in_markup in ("html", "wp-html"): with tempfile.TemporaryDirectory() as tmpdir: - html_filename = os.path.join(tmpdir, 'pandoc-input.html') + html_filename = os.path.join(tmpdir, "pandoc-input.html") # Replace newlines with paragraphs wrapped with

so # HTML is valid before conversion - if in_markup == 'wp-html': + if in_markup == "wp-html": new_content = decode_wp_content(content) else: paragraphs = content.splitlines() - paragraphs = ['

{}

'.format(p) for p in paragraphs] - new_content = ''.join(paragraphs) - with open(html_filename, 'w', encoding='utf-8') as fp: + paragraphs = ["

{}

".format(p) for p in paragraphs] + new_content = "".join(paragraphs) + with open(html_filename, "w", encoding="utf-8") as fp: fp.write(new_content) if pandoc_version < (2,): - parse_raw = '--parse-raw' if not strip_raw else '' - wrap_none = '--wrap=none' \ - if pandoc_version >= (1, 16) else '--no-wrap' - cmd = ('pandoc --normalize {0} --from=html' - ' --to={1} {2} -o "{3}" "{4}"') - cmd = cmd.format(parse_raw, - out_markup if out_markup != 'markdown' else "gfm", - wrap_none, - out_filename, html_filename) + parse_raw = "--parse-raw" if not strip_raw else "" + wrap_none = ( + "--wrap=none" if pandoc_version >= (1, 16) else "--no-wrap" + ) + cmd = ( + "pandoc --normalize {0} --from=html" + ' --to={1} {2} -o "{3}" "{4}"' + ) + cmd = cmd.format( + parse_raw, + out_markup if out_markup != "markdown" else "gfm", + wrap_none, + out_filename, + html_filename, + ) else: - from_arg = '-f html+raw_html' if not strip_raw else '-f html' - cmd = ('pandoc {0} --to={1}-smart --wrap=none -o "{2}" "{3}"') - cmd = cmd.format(from_arg, - out_markup if out_markup != 'markdown' else "gfm", - out_filename, html_filename) + from_arg = "-f html+raw_html" if not strip_raw else "-f html" + cmd = 'pandoc {0} --to={1}-smart --wrap=none -o "{2}" "{3}"' + cmd = cmd.format( + from_arg, + out_markup if out_markup != "markdown" else "gfm", + out_filename, + html_filename, + ) try: rc = subprocess.call(cmd, shell=True) if rc < 0: - error = 'Child was terminated by signal %d' % -rc + error = "Child was terminated by signal %d" % -rc exit(error) elif rc > 0: - error = 'Please, check your Pandoc installation.' + error = "Please, check your Pandoc installation." exit(error) except OSError as e: - error = 'Pandoc execution failed: %s' % e + error = "Pandoc execution failed: %s" % e exit(error) - with open(out_filename, encoding='utf-8') as fs: + with open(out_filename, encoding="utf-8") as fs: content = fs.read() - if out_markup == 'markdown': + if out_markup == "markdown": # In markdown, to insert a
, end a line with two # or more spaces & then a end-of-line - content = content.replace('\\\n ', ' \n') - content = content.replace('\\\n', ' \n') + content = content.replace("\\\n ", " \n") + content = content.replace("\\\n", " \n") if wp_attach and links: content = update_links_to_attached_files(content, links) - with open(out_filename, 'w', encoding='utf-8') as fs: + with open(out_filename, "w", encoding="utf-8") as fs: fs.write(header + content) if posts_require_pandoc: - logger.error("Pandoc must be installed to import the following posts:" - "\n {}".format("\n ".join(posts_require_pandoc))) + logger.error( + "Pandoc must be installed to import the following posts:" "\n {}".format( + "\n ".join(posts_require_pandoc) + ) + ) if wp_attach and attachments and None in attachments: print("downloading attachments that don't have a parent post") @@ -856,111 +972,136 @@ def fields2pelican( def main(): parser = argparse.ArgumentParser( description="Transform feed, Blogger, Dotclear, Tumblr, or " - "WordPress files into reST (rst) or Markdown (md) files. " - "Be sure to have pandoc installed.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + "WordPress files into reST (rst) or Markdown (md) files. " + "Be sure to have pandoc installed.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument(dest="input", help="The input file to read") parser.add_argument( - dest='input', help='The input file to read') + "--blogger", action="store_true", dest="blogger", help="Blogger XML export" + ) parser.add_argument( - '--blogger', action='store_true', dest='blogger', - help='Blogger XML export') + "--dotclear", action="store_true", dest="dotclear", help="Dotclear export" + ) parser.add_argument( - '--dotclear', action='store_true', dest='dotclear', - help='Dotclear export') + "--tumblr", action="store_true", dest="tumblr", help="Tumblr export" + ) parser.add_argument( - '--tumblr', action='store_true', dest='tumblr', - help='Tumblr export') + "--wpfile", action="store_true", dest="wpfile", help="Wordpress XML export" + ) parser.add_argument( - '--wpfile', action='store_true', dest='wpfile', - help='Wordpress XML export') + "--feed", action="store_true", dest="feed", help="Feed to parse" + ) parser.add_argument( - '--feed', action='store_true', dest='feed', - help='Feed to parse') + "-o", "--output", dest="output", default="content", help="Output path" + ) parser.add_argument( - '-o', '--output', dest='output', default='content', - help='Output path') + "-m", + "--markup", + dest="markup", + default="rst", + help="Output markup format (supports rst & markdown)", + ) parser.add_argument( - '-m', '--markup', dest='markup', default='rst', - help='Output markup format (supports rst & markdown)') + "--dir-cat", + action="store_true", + dest="dircat", + help="Put files in directories with categories name", + ) parser.add_argument( - '--dir-cat', action='store_true', dest='dircat', - help='Put files in directories with categories name') + "--dir-page", + action="store_true", + dest="dirpage", + help=( + 'Put files recognised as pages in "pages/" sub-directory' + " (blogger and wordpress import only)" + ), + ) parser.add_argument( - '--dir-page', action='store_true', dest='dirpage', - help=('Put files recognised as pages in "pages/" sub-directory' - ' (blogger and wordpress import only)')) + "--filter-author", + dest="author", + help="Import only post from the specified author", + ) parser.add_argument( - '--filter-author', dest='author', - help='Import only post from the specified author') - parser.add_argument( - '--strip-raw', action='store_true', dest='strip_raw', + "--strip-raw", + action="store_true", + dest="strip_raw", help="Strip raw HTML code that can't be converted to " - "markup such as flash embeds or iframes (wordpress import only)") + "markup such as flash embeds or iframes (wordpress import only)", + ) parser.add_argument( - '--wp-custpost', action='store_true', - dest='wp_custpost', - help='Put wordpress custom post types in directories. If used with ' - '--dir-cat option directories will be created as ' - '/post_type/category/ (wordpress import only)') + "--wp-custpost", + action="store_true", + dest="wp_custpost", + help="Put wordpress custom post types in directories. If used with " + "--dir-cat option directories will be created as " + "/post_type/category/ (wordpress import only)", + ) parser.add_argument( - '--wp-attach', action='store_true', dest='wp_attach', - help='(wordpress import only) Download files uploaded to wordpress as ' - 'attachments. Files will be added to posts as a list in the post ' - 'header. All files will be downloaded, even if ' - "they aren't associated with a post. Files will be downloaded " - 'with their original path inside the output directory. ' - 'e.g. output/wp-uploads/date/postname/file.jpg ' - '-- Requires an internet connection --') + "--wp-attach", + action="store_true", + dest="wp_attach", + help="(wordpress import only) Download files uploaded to wordpress as " + "attachments. Files will be added to posts as a list in the post " + "header. All files will be downloaded, even if " + "they aren't associated with a post. Files will be downloaded " + "with their original path inside the output directory. " + "e.g. output/wp-uploads/date/postname/file.jpg " + "-- Requires an internet connection --", + ) parser.add_argument( - '--disable-slugs', action='store_true', - dest='disable_slugs', - help='Disable storing slugs from imported posts within output. ' - 'With this disabled, your Pelican URLs may not be consistent ' - 'with your original posts.') + "--disable-slugs", + action="store_true", + dest="disable_slugs", + help="Disable storing slugs from imported posts within output. " + "With this disabled, your Pelican URLs may not be consistent " + "with your original posts.", + ) parser.add_argument( - '-b', '--blogname', dest='blogname', - help="Blog name (Tumblr import only)") + "-b", "--blogname", dest="blogname", help="Blog name (Tumblr import only)" + ) args = parser.parse_args() input_type = None if args.blogger: - input_type = 'blogger' + input_type = "blogger" elif args.dotclear: - input_type = 'dotclear' + input_type = "dotclear" elif args.tumblr: - input_type = 'tumblr' + input_type = "tumblr" elif args.wpfile: - input_type = 'wordpress' + input_type = "wordpress" elif args.feed: - input_type = 'feed' + input_type = "feed" else: - error = ('You must provide either --blogger, --dotclear, ' - '--tumblr, --wpfile or --feed options') + error = ( + "You must provide either --blogger, --dotclear, " + "--tumblr, --wpfile or --feed options" + ) exit(error) if not os.path.exists(args.output): try: os.mkdir(args.output) except OSError: - error = 'Unable to create the output folder: ' + args.output + error = "Unable to create the output folder: " + args.output exit(error) - if args.wp_attach and input_type != 'wordpress': - error = ('You must be importing a wordpress xml ' - 'to use the --wp-attach option') + if args.wp_attach and input_type != "wordpress": + error = "You must be importing a wordpress xml " "to use the --wp-attach option" exit(error) - if input_type == 'blogger': + if input_type == "blogger": fields = blogger2fields(args.input) - elif input_type == 'dotclear': + elif input_type == "dotclear": fields = dc2fields(args.input) - elif input_type == 'tumblr': + elif input_type == "tumblr": fields = tumblr2fields(args.input, args.blogname) - elif input_type == 'wordpress': + elif input_type == "wordpress": fields = wp2fields(args.input, args.wp_custpost or False) - elif input_type == 'feed': + elif input_type == "feed": fields = feed2fields(args.input) if args.wp_attach: @@ -970,12 +1111,16 @@ def main(): # init logging init() - fields2pelican(fields, args.markup, args.output, - dircat=args.dircat or False, - dirpage=args.dirpage or False, - strip_raw=args.strip_raw or False, - disable_slugs=args.disable_slugs or False, - filter_author=args.author, - wp_custpost=args.wp_custpost or False, - wp_attach=args.wp_attach or False, - attachments=attachments or None) + fields2pelican( + fields, + args.markup, + args.output, + dircat=args.dircat or False, + dirpage=args.dirpage or False, + strip_raw=args.strip_raw or False, + disable_slugs=args.disable_slugs or False, + filter_author=args.author, + wp_custpost=args.wp_custpost or False, + wp_attach=args.wp_attach or False, + attachments=attachments or None, + ) diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py index 4b6d93cc..fba0c9c3 100755 --- a/pelican/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -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__": diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py index 1ad3a333..4069f99b 100755 --- a/pelican/tools/pelican_themes.py +++ b/pelican/tools/pelican_themes.py @@ -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 diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py index e00b914c..2e8cc953 100644 --- a/pelican/urlwrappers.py +++ b/pelican/urlwrappers.py @@ -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): diff --git a/pelican/utils.py b/pelican/utils.py index 09ffcfe6..08a08f7e 100644 --- a/pelican/utils.py +++ b/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 += '' % tag + out += "" % 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) diff --git a/pelican/writers.py b/pelican/writers.py index 632c6b87..ec12d125 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -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) diff --git a/samples/pelican.conf.py b/samples/pelican.conf.py index 1fa7c472..d10254e8 100755 --- a/samples/pelican.conf.py +++ b/samples/pelican.conf.py @@ -1,55 +1,59 @@ -AUTHOR = 'Alexis Métaireau' +AUTHOR = "Alexis Métaireau" SITENAME = "Alexis' log" -SITESUBTITLE = 'A personal blog.' -SITEURL = 'http://blog.notmyidea.org' +SITESUBTITLE = "A personal blog." +SITEURL = "http://blog.notmyidea.org" TIMEZONE = "Europe/Paris" # can be useful in development, but set to False when you're ready to publish RELATIVE_URLS = True -GITHUB_URL = 'http://github.com/ametaireau/' +GITHUB_URL = "http://github.com/ametaireau/" DISQUS_SITENAME = "blog-notmyidea" REVERSE_CATEGORY_ORDER = True LOCALE = "C" DEFAULT_PAGINATION = 4 DEFAULT_DATE = (2012, 3, 2, 14, 1, 1) -FEED_ALL_RSS = 'feeds/all.rss.xml' -CATEGORY_FEED_RSS = 'feeds/{slug}.rss.xml' +FEED_ALL_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 = [ - 'images', - 'extra/robots.txt', - ] + "images", + "extra/robots.txt", +] # custom page generated with a jinja2 template -TEMPLATE_PAGES = {'pages/jinja2_template.html': 'jinja2_template.html'} +TEMPLATE_PAGES = {"pages/jinja2_template.html": "jinja2_template.html"} # there is no other HTML content -READERS = {'html': None} +READERS = {"html": None} # code blocks with line numbers -PYGMENTS_RST_OPTIONS = {'linenos': 'table'} +PYGMENTS_RST_OPTIONS = {"linenos": "table"} # foobar will not be used, because it's not in caps. All configuration keys # have to be in caps diff --git a/samples/pelican.conf_FR.py b/samples/pelican.conf_FR.py index dc657404..cbca06df 100644 --- a/samples/pelican.conf_FR.py +++ b/samples/pelican.conf_FR.py @@ -1,56 +1,60 @@ -AUTHOR = 'Alexis Métaireau' +AUTHOR = "Alexis Métaireau" SITENAME = "Alexis' log" -SITEURL = 'http://blog.notmyidea.org' +SITEURL = "http://blog.notmyidea.org" TIMEZONE = "Europe/Paris" # can be useful in development, but set to False when you're ready to publish RELATIVE_URLS = True -GITHUB_URL = 'http://github.com/ametaireau/' +GITHUB_URL = "http://github.com/ametaireau/" DISQUS_SITENAME = "blog-notmyidea" PDF_GENERATOR = False REVERSE_CATEGORY_ORDER = True LOCALE = "fr_FR.UTF-8" DEFAULT_PAGINATION = 4 DEFAULT_DATE = (2012, 3, 2, 14, 1, 1) -DEFAULT_DATE_FORMAT = '%d %B %Y' +DEFAULT_DATE_FORMAT = "%d %B %Y" -ARTICLE_URL = 'posts/{date:%Y}/{date:%B}/{date:%d}/{slug}/' -ARTICLE_SAVE_AS = ARTICLE_URL + 'index.html' +ARTICLE_URL = "posts/{date:%Y}/{date:%B}/{date:%d}/{slug}/" +ARTICLE_SAVE_AS = ARTICLE_URL + "index.html" -FEED_ALL_RSS = 'feeds/all.rss.xml' -CATEGORY_FEED_RSS = 'feeds/{slug}.rss.xml' +FEED_ALL_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", +] # custom page generated with a jinja2 template -TEMPLATE_PAGES = {'pages/jinja2_template.html': 'jinja2_template.html'} +TEMPLATE_PAGES = {"pages/jinja2_template.html": "jinja2_template.html"} # code blocks with line numbers -PYGMENTS_RST_OPTIONS = {'linenos': 'table'} +PYGMENTS_RST_OPTIONS = {"linenos": "table"} # foobar will not be used, because it's not in caps. All configuration keys # have to be in caps From 271f4dd68f58313f20c2b3cf40ddc6256b5b6d69 Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Sun, 29 Oct 2023 09:57:37 -0700 Subject: [PATCH 58/88] Strip trailing whitespace --- .coveragerc | 1 - docs/_static/pelican-logo.svg | 2 +- docs/_static/theme_overrides.css | 1 - pelican/tests/TestPages/draft_page_markdown.md | 2 +- .../content/2012-11-30_md_w_filename_meta#foo-bar.md | 1 - .../content/article_with_markdown_markup_extensions.md | 1 - .../tests/content/article_with_uppercase_metadata.rst | 1 - pelican/tests/content/article_without_category.rst | 1 - pelican/tests/content/bloggerexport.xml | 2 +- pelican/tests/content/empty_with_bom.md | 2 +- pelican/tests/content/wordpress_content_encoded | 1 - pelican/tests/content/wordpressexport.xml | 2 +- pelican/themes/notmyidea/static/css/reset.css | 2 +- pelican/themes/simple/templates/author.html | 1 - pelican/themes/simple/templates/category.html | 1 - samples/content/pages/hidden_page.rst | 1 - samples/content/unbelievable.rst | 10 +++++----- 17 files changed, 11 insertions(+), 21 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2cb24879..fdd2cad6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [report] omit = pelican/tests/* - diff --git a/docs/_static/pelican-logo.svg b/docs/_static/pelican-logo.svg index 95b947bf..f36b42fa 100644 --- a/docs/_static/pelican-logo.svg +++ b/docs/_static/pelican-logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 83afc78e..e840ab6b 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -9,4 +9,3 @@ .wy-table-responsive { overflow: visible !important; } - diff --git a/pelican/tests/TestPages/draft_page_markdown.md b/pelican/tests/TestPages/draft_page_markdown.md index fda71868..0f378a55 100644 --- a/pelican/tests/TestPages/draft_page_markdown.md +++ b/pelican/tests/TestPages/draft_page_markdown.md @@ -9,4 +9,4 @@ Used for pelican test The quick brown fox . -This page is a draft \ No newline at end of file +This page is a draft diff --git a/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md b/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md index cdccfc8a..8b3c0bcf 100644 --- a/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md +++ b/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md @@ -3,4 +3,3 @@ author: Alexis Métaireau Markdown with filename metadata =============================== - diff --git a/pelican/tests/content/article_with_markdown_markup_extensions.md b/pelican/tests/content/article_with_markdown_markup_extensions.md index 6cf56403..7eff4dcb 100644 --- a/pelican/tests/content/article_with_markdown_markup_extensions.md +++ b/pelican/tests/content/article_with_markdown_markup_extensions.md @@ -5,4 +5,3 @@ Title: Test Markdown extensions ## Level1 ### Level2 - diff --git a/pelican/tests/content/article_with_uppercase_metadata.rst b/pelican/tests/content/article_with_uppercase_metadata.rst index e26cdd13..ee79f55a 100644 --- a/pelican/tests/content/article_with_uppercase_metadata.rst +++ b/pelican/tests/content/article_with_uppercase_metadata.rst @@ -3,4 +3,3 @@ This is a super article ! ######################### :Category: Yeah - diff --git a/pelican/tests/content/article_without_category.rst b/pelican/tests/content/article_without_category.rst index ff47f6ef..1cfcba71 100644 --- a/pelican/tests/content/article_without_category.rst +++ b/pelican/tests/content/article_without_category.rst @@ -3,4 +3,3 @@ This is an article without category ! ##################################### This article should be in the DEFAULT_CATEGORY. - diff --git a/pelican/tests/content/bloggerexport.xml b/pelican/tests/content/bloggerexport.xml index 4bc0985a..a3b9cdf2 100644 --- a/pelican/tests/content/bloggerexport.xml +++ b/pelican/tests/content/bloggerexport.xml @@ -1064,4 +1064,4 @@ - \ No newline at end of file + diff --git a/pelican/tests/content/empty_with_bom.md b/pelican/tests/content/empty_with_bom.md index 5f282702..e02abfc9 100644 --- a/pelican/tests/content/empty_with_bom.md +++ b/pelican/tests/content/empty_with_bom.md @@ -1 +1 @@ - \ No newline at end of file + diff --git a/pelican/tests/content/wordpress_content_encoded b/pelican/tests/content/wordpress_content_encoded index da35de3b..eefff1e9 100644 --- a/pelican/tests/content/wordpress_content_encoded +++ b/pelican/tests/content/wordpress_content_encoded @@ -52,4 +52,3 @@ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - diff --git a/pelican/tests/content/wordpressexport.xml b/pelican/tests/content/wordpressexport.xml index 4f5b3651..81ed7ea3 100644 --- a/pelican/tests/content/wordpressexport.xml +++ b/pelican/tests/content/wordpressexport.xml @@ -838,7 +838,7 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]>_edit_last - + A 2nd custom post type also in category 5 http://thisisa.test/?p=177 diff --git a/pelican/themes/notmyidea/static/css/reset.css b/pelican/themes/notmyidea/static/css/reset.css index c88e6196..f5123cf6 100644 --- a/pelican/themes/notmyidea/static/css/reset.css +++ b/pelican/themes/notmyidea/static/css/reset.css @@ -49,4 +49,4 @@ del {text-decoration: line-through;} table { border-collapse: collapse; border-spacing: 0; -} \ No newline at end of file +} diff --git a/pelican/themes/simple/templates/author.html b/pelican/themes/simple/templates/author.html index 64aadffb..c054f8ab 100644 --- a/pelican/themes/simple/templates/author.html +++ b/pelican/themes/simple/templates/author.html @@ -5,4 +5,3 @@ {% block content_title %}

Articles by {{ author }}

{% endblock %} - diff --git a/pelican/themes/simple/templates/category.html b/pelican/themes/simple/templates/category.html index f7889d00..da1a8b52 100644 --- a/pelican/themes/simple/templates/category.html +++ b/pelican/themes/simple/templates/category.html @@ -5,4 +5,3 @@ {% block content_title %}

Articles in the {{ category }} category

{% endblock %} - diff --git a/samples/content/pages/hidden_page.rst b/samples/content/pages/hidden_page.rst index ab8704ed..b1f52d95 100644 --- a/samples/content/pages/hidden_page.rst +++ b/samples/content/pages/hidden_page.rst @@ -6,4 +6,3 @@ This is a test hidden page This is great for things like error(404) pages Anyone can see this page but it's not linked to anywhere! - diff --git a/samples/content/unbelievable.rst b/samples/content/unbelievable.rst index 209e3557..afa502cf 100644 --- a/samples/content/unbelievable.rst +++ b/samples/content/unbelievable.rst @@ -45,7 +45,7 @@ Testing more sourcecode directives :lineseparator:
:linespans: foo :nobackground: - + def run(self): self.assert_has_content() try: @@ -76,8 +76,8 @@ Testing even more sourcecode directives .. sourcecode:: python :linenos: table :nowrap: - - + + formatter = self.options and VARIANTS[self.options.keys()[0]] @@ -90,8 +90,8 @@ Even if the default is line numbers, we can override it here .. sourcecode:: python :linenos: none - - + + formatter = self.options and VARIANTS[self.options.keys()[0]] From 805ca9b4a9af8975f4c2f411e162033bf0a1a500 Mon Sep 17 00:00:00 2001 From: boxydog Date: Sun, 29 Oct 2023 22:21:04 +0100 Subject: [PATCH 59/88] Run pre-commit on all files during CI test job --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59a22862..b1b7c809 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,8 @@ jobs: pdm install --no-default --dev - name: Run linters run: pdm lint --diff + - name: Run pre-commit checks on all files + uses: pre-commit/action@v3.0.0 docs: name: Build docs From f0aab11a2da8329aab1cb523365adf1441865b49 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Mon, 30 Oct 2023 00:53:15 +0300 Subject: [PATCH 60/88] Force git subprocess in tests to use utf-8 --- pelican/tests/support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 3e4da785..a395eeaf 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -232,7 +232,7 @@ def diff_subproc(first, second): '-w', first, second], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True, + encoding="utf-8", ) From 4e438ffe6073c4d67ae8074d02f9604ec6a1041f Mon Sep 17 00:00:00 2001 From: Lioman Date: Mon, 30 Oct 2023 16:04:44 +0100 Subject: [PATCH 61/88] Enable tests to validate dist build contents (#3229) --- .github/workflows/main.yml | 19 +++++- pelican/tests/build_test/conftest.py | 2 +- pelican/tests/build_test/test_build_files.py | 66 ++++++++++++++++++++ pelican/tests/build_test/test_wheel.py | 28 --------- 4 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 pelican/tests/build_test/test_build_files.py delete mode 100644 pelican/tests/build_test/test_wheel.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0127982e..85ee5ccb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,6 +64,23 @@ jobs: - name: Run linters run: pdm lint --diff + build: + name: Test build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pdm-project/setup-pdm@v3 + with: + python-version: 3.9 + cache: true + cache-dependency-path: ./pyproject.toml + - name: Install dependencies + run: pdm install --dev + - name: Build package + run: pdm build + - name: Test build + run: pdm run pytest --check-build=dist pelican/tests/build_test + docs: name: Build docs runs-on: ubuntu-latest @@ -84,7 +101,7 @@ jobs: deploy: name: Deploy environment: Deployment - needs: [test, lint, docs] + needs: [test, lint, docs, build] runs-on: ubuntu-latest if: github.ref=='refs/heads/master' && github.event_name!='pull_request' && github.repository == 'getpelican/pelican' diff --git a/pelican/tests/build_test/conftest.py b/pelican/tests/build_test/conftest.py index 548f7970..b1d1a54b 100644 --- a/pelican/tests/build_test/conftest.py +++ b/pelican/tests/build_test/conftest.py @@ -1,6 +1,6 @@ def pytest_addoption(parser): parser.addoption( - "--check-wheel", + "--check-build", action="store", default=False, help="Check wheel contents.", diff --git a/pelican/tests/build_test/test_build_files.py b/pelican/tests/build_test/test_build_files.py new file mode 100644 index 00000000..2b51d362 --- /dev/null +++ b/pelican/tests/build_test/test_build_files.py @@ -0,0 +1,66 @@ +from re import match +import tarfile +from pathlib import Path +from zipfile import ZipFile + +import pytest + + +@pytest.mark.skipif( + "not config.getoption('--check-build')", + reason="Only run when --check-build is given", +) +def test_wheel_contents(pytestconfig): + """ + This test should test the contents of the wheel to make sure + that everything that is needed is included in the final build + """ + dist_folder = pytestconfig.getoption("--check-build") + wheels = Path(dist_folder).rglob("*.whl") + for wheel_file in wheels: + files_list = ZipFile(wheel_file).namelist() + # Check if theme files are copied to wheel + simple_theme = Path("./pelican/themes/simple/templates") + for x in simple_theme.iterdir(): + assert str(x) in files_list + + # Check if tool templates are copied to wheel + tools = Path("./pelican/tools/templates") + for x in tools.iterdir(): + assert str(x) in files_list + + assert "pelican/tools/templates/tasks.py.jinja2" in files_list + + +@pytest.mark.skipif( + "not config.getoption('--check-build')", + reason="Only run when --check-build is given", +) +@pytest.mark.parametrize( + "expected_file", + [ + ("THANKS"), + ("README.rst"), + ("CONTRIBUTING.rst"), + ("docs/changelog.rst"), + ("samples/"), + ], +) +def test_sdist_contents(pytestconfig, expected_file): + """ + This test should test the contents of the source distribution to make sure + that everything that is needed is included in the final build. + """ + dist_folder = pytestconfig.getoption("--check-build") + sdist_files = Path(dist_folder).rglob("*.tar.gz") + for dist in sdist_files: + files_list = tarfile.open(dist, "r:gz").getnames() + dir_matcher = "" + if expected_file.endswith("/"): + dir_matcher = ".*" + filtered_values = [ + path + for path in files_list + if match(f"^pelican-\d\.\d\.\d/{expected_file}{dir_matcher}$", path) + ] + assert len(filtered_values) > 0 diff --git a/pelican/tests/build_test/test_wheel.py b/pelican/tests/build_test/test_wheel.py deleted file mode 100644 index 8e643981..00000000 --- a/pelican/tests/build_test/test_wheel.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path -import pytest -from zipfile import ZipFile - - -@pytest.mark.skipif( - "not config.getoption('--check-wheel')", - reason="Only run when --check-wheel is given", -) -def test_wheel_contents(pytestconfig): - """ - This test, should test the contents of the wheel to make sure, - that everything that is needed is included in the final build - """ - wheel_file = pytestconfig.getoption("--check-wheel") - assert wheel_file.endswith(".whl") - files_list = ZipFile(wheel_file).namelist() - # Check if theme files are copied to wheel - simple_theme = Path("./pelican/themes/simple/templates") - for x in simple_theme.iterdir(): - assert str(x) in files_list - - # Check if tool templates are copied to wheel - tools = Path("./pelican/tools/templates") - for x in tools.iterdir(): - assert str(x) in files_list - - assert "pelican/tools/templates/tasks.py.jinja2" in files_list From 08785f714ffdc10560072f2379cb123c6984254e Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 30 Oct 2023 19:35:59 +0100 Subject: [PATCH 62/88] Remove obsolete linters: Flake8, Black, isort --- pyproject.toml | 4 ---- requirements/style.pip | 2 -- tox.ini | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 requirements/style.pip diff --git a/pyproject.toml b/pyproject.toml index 4e3e712a..a16bb7f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,11 +91,7 @@ dev = [ "pytest-sugar>=0.9.7", "pytest-xdist>=3.3.1", "tox>=4.11.3", - "flake8>=6.1.0", - "flake8-import-order>=0.18.2", "invoke>=2.2.0", - "isort>=5.12.0", - "black>=23.10.1", "ruff>=0.1.3", "tomli>=2.0.1; python_version < \"3.11\"", ] diff --git a/requirements/style.pip b/requirements/style.pip deleted file mode 100644 index f1c82ed0..00000000 --- a/requirements/style.pip +++ /dev/null @@ -1,2 +0,0 @@ -flake8==3.9.2 -flake8-import-order diff --git a/tox.ini b/tox.ini index 361c52dd..f6f45af1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{3.8,3.9,3.10,3.11.3.12},docs,flake8 +envlist = py{3.8,3.9,3.10,3.11.3.12},docs [testenv] basepython = From 3c5799694530d5e038c71b03007eb684e3c280f5 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Mon, 30 Oct 2023 19:45:42 +0100 Subject: [PATCH 63/88] Ignore Ruff format commit in the blame view --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..7b822fd3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# .git-blame-ignore-revs +# Apply code style to project via: ruff format . +cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7 From abae21494dcd69ab8a2b11f8b9e01774dc4f52b1 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Tue, 31 Oct 2023 16:50:48 +0100 Subject: [PATCH 64/88] Adjust line length to 88 in EditorConfig --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index edb13c8a..a9c06c97 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ insert_final_newline = true trim_trailing_whitespace = true [*.py] -max_line_length = 79 +max_line_length = 88 [*.{yml,yaml}] indent_size = 2 From e6a5e2a66520a51f25336201ec876633c267769d Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 1 Nov 2023 09:07:48 +0100 Subject: [PATCH 65/88] Update pre-commit hook versions --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f68521e5..5a73aebc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for info on hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-ast @@ -14,7 +14,7 @@ repos: - id: forbid-new-submodules - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.1.3 hooks: - id: ruff - id: ruff-format From 76650898a648b4ba8d2e34e2d67abbbeafc0776e Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 1 Nov 2023 09:43:21 +0100 Subject: [PATCH 66/88] Update to Markdown 3.5.1 in test requirements --- requirements/test.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test.pip b/requirements/test.pip index 2cf1ea1f..87869e67 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -6,7 +6,7 @@ pytest-xdist[psutil] tzdata # Optional Packages -Markdown==3.4.3 +Markdown==3.5.1 BeautifulSoup4 lxml typogrify From feae8ef41c995cb0d327033aa0d261cfd4eb85df Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Wed, 1 Nov 2023 22:49:15 +0300 Subject: [PATCH 67/88] Provide a plugin_enabled Jinja test for themes --- docs/themes.rst | 17 +++++++ pelican/generators.py | 4 ++ pelican/plugins/_utils.py | 16 +++++++ pelican/tests/test_generators.py | 25 ++++++++++ pelican/tests/test_plugins.py | 82 +++++++++++++++++++++++++++++++- 5 files changed, 143 insertions(+), 1 deletion(-) diff --git a/docs/themes.rst b/docs/themes.rst index 51b5b0d5..2df717e4 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -140,6 +140,23 @@ your date according to the locale given in your settings:: .. _datetime: https://docs.python.org/3/library/datetime.html#datetime-objects .. _strftime: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior +Checking Loaded Plugins +----------------------- + +Pelican provides a ``plugin_enabled`` Jinja test for checking if a certain plugin +is enabled. This test accepts a plugin name as a string and will return a boolean. +Namespace Plugins can be specified with their full name (``pelican.plugins.plugin_name``) +or their short name (``plugin_name``). The following example uses ``webassets`` plugin +to minify CSS if it is enabled and falls back to regular CSS otherwise:: + + {% if "webassets" is plugin_enabled %} + {% assets filters="cssmin", output="css/style.min.css", "css/style.scss" %} + + {% endassets %} + {% else %} + + {% endif %} + index.html ---------- diff --git a/pelican/generators.py b/pelican/generators.py index 0bbb7268..3b5ca9e4 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -20,6 +20,7 @@ from jinja2 import ( from pelican.cache import FileStampDataCacher from pelican.contents import Article, Page, Static from pelican.plugins import signals +from pelican.plugins._utils import plugin_enabled from pelican.readers import Readers from pelican.utils import ( DateFormatter, @@ -102,6 +103,9 @@ class Generator: # get custom Jinja tests from user settings custom_tests = self.settings["JINJA_TESTS"] + self.env.tests["plugin_enabled"] = partial( + plugin_enabled, plugin_list=self.settings["PLUGINS"] + ) self.env.tests.update(custom_tests) signals.generator_init.send(self) diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py index f0c18f5c..c25f8114 100644 --- a/pelican/plugins/_utils.py +++ b/pelican/plugins/_utils.py @@ -40,6 +40,22 @@ def list_plugins(ns_pkg=None): logger.info("No plugins are installed") +def plugin_enabled(name, plugin_list=None): + if plugin_list is None or not plugin_list: + # no plugins are loaded + return False + + if name in plugin_list: + # search name as is + return True + + if "pelican.plugins.{}".format(name) in plugin_list: + # check if short name is a namespace plugin + return True + + return False + + def load_legacy_plugin(plugin, plugin_paths): if "." in plugin: # it is in a package, try to resolve package first diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 52adb2c9..af6f5b1a 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -1581,6 +1581,31 @@ class TestJinja2Environment(TestCaseWithCLocale): self._test_jinja2_helper(settings, content, expected) + def test_jinja2_filter_plugin_enabled(self): + """JINJA_FILTERS adds custom filters to Jinja2 environment""" + settings = {"PLUGINS": ["legacy_plugin", "pelican.plugins.ns_plugin"]} + jinja_template = ( + "{plugin}: " + "{{% if '{plugin}' is plugin_enabled %}}yes" + "{{% else %}}no{{% endif %}}" + ) + content = " / ".join( + ( + jinja_template.format(plugin="ns_plugin"), + jinja_template.format(plugin="pelican.plugins.ns_plugin"), + jinja_template.format(plugin="legacy_plugin"), + jinja_template.format(plugin="unknown"), + ) + ) + expected = ( + "ns_plugin: yes / " + "pelican.plugins.ns_plugin: yes / " + "legacy_plugin: yes / " + "unknown: no" + ) + + self._test_jinja2_helper(settings, content, expected) + def test_jinja2_test(self): """JINJA_TESTS adds custom tests to Jinja2 environment""" content = "foo {{ foo is custom_test }}, bar {{ bar is custom_test }}" diff --git a/pelican/tests/test_plugins.py b/pelican/tests/test_plugins.py index 4f02022c..ccce684e 100644 --- a/pelican/tests/test_plugins.py +++ b/pelican/tests/test_plugins.py @@ -2,7 +2,12 @@ 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, + plugin_enabled, +) from pelican.tests.support import unittest @@ -183,3 +188,78 @@ class PluginTest(unittest.TestCase): get_plugin_name(NoopPlugin()), "PluginTest.test_get_plugin_name..NoopPlugin", ) + + def test_plugin_enabled(self): + def get_plugin_names(plugins): + return [get_plugin_name(p) for p in plugins] + + with tmp_namespace_path(self._NS_PLUGIN_FOLDER): + # with no `PLUGINS` setting, load namespace plugins + SETTINGS = {} + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertTrue(plugin_enabled("ns_plugin", plugins)) + self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertFalse(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # disable namespace plugins with `PLUGINS = []` + SETTINGS = {"PLUGINS": []} + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertFalse(plugin_enabled("ns_plugin", plugins)) + self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertFalse(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # with `PLUGINS`, load only specified plugins + + # normal plugin + SETTINGS = { + "PLUGINS": ["normal_plugin"], + "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER], + } + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertFalse(plugin_enabled("ns_plugin", plugins)) + self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertTrue(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # normal submodule/subpackage plugins + SETTINGS = { + "PLUGINS": [ + "normal_submodule_plugin.subplugin", + "normal_submodule_plugin.subpackage.subpackage", + ], + "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER], + } + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertFalse(plugin_enabled("ns_plugin", plugins)) + self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertFalse(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # namespace plugin short + SETTINGS = {"PLUGINS": ["ns_plugin"]} + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertTrue(plugin_enabled("ns_plugin", plugins)) + self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertFalse(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # namespace plugin long + SETTINGS = {"PLUGINS": ["pelican.plugins.ns_plugin"]} + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertTrue(plugin_enabled("ns_plugin", plugins)) + self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertFalse(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) + + # normal and namespace plugin + SETTINGS = { + "PLUGINS": ["normal_plugin", "ns_plugin"], + "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER], + } + plugins = get_plugin_names(load_plugins(SETTINGS)) + self.assertTrue(plugin_enabled("ns_plugin", plugins)) + self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins)) + self.assertTrue(plugin_enabled("normal_plugin", plugins)) + self.assertFalse(plugin_enabled("unknown", plugins)) From 49aef30dab372a041acf097207033666bdd48a3e Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Wed, 1 Nov 2023 23:19:26 +0300 Subject: [PATCH 68/88] add sphinxext-opengraph to pyproject dev requirements --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a16bb7f8..d9fe1a33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ dev = [ "markdown>=3.5", "typogrify>=2.0.7", "sphinx>=7.1.2", + "sphinxext-opengraph>=0.9.0", "furo>=2023.9.10", "livereload>=2.6.3", "psutil>=5.9.6", From 32b72123f051dd23bf481eed26ab947f66a22663 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Thu, 2 Nov 2023 14:09:51 +0100 Subject: [PATCH 69/88] Modify wording slightly --- docs/themes.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/themes.rst b/docs/themes.rst index 2df717e4..2e01ec8e 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -144,10 +144,10 @@ Checking Loaded Plugins ----------------------- Pelican provides a ``plugin_enabled`` Jinja test for checking if a certain plugin -is enabled. This test accepts a plugin name as a string and will return a boolean. -Namespace Plugins can be specified with their full name (``pelican.plugins.plugin_name``) -or their short name (``plugin_name``). The following example uses ``webassets`` plugin -to minify CSS if it is enabled and falls back to regular CSS otherwise:: +is enabled. This test accepts a plugin name as a string and will return a Boolean. +Namespace plugins can be specified by full name (``pelican.plugins.plugin_name``) +or short name (``plugin_name``). The following example uses the ``webassets`` plugin +to minify CSS if the plugin is enabled and otherwise falls back to regular CSS:: {% if "webassets" is plugin_enabled %} {% assets filters="cssmin", output="css/style.min.css", "css/style.scss" %} From 8a8b952ecb83c7b7e2cd8d331a8dff23d319f1ac Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Fri, 3 Nov 2023 01:05:12 +0300 Subject: [PATCH 70/88] preserve connection order in blinker --- docs/plugins.rst | 10 ++++++---- pelican/plugins/signals.py | 6 +++++- pelican/tests/test_plugins.py | 21 +++++++++++++++++++++ pyproject.toml | 3 ++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index db7e00b4..22e1bcc6 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -94,9 +94,12 @@ which you map the signals to your plugin logic. Let's take a simple example:: your ``register`` callable or they will be garbage-collected before the signal is emitted. -If multiple plugins connect to the same signal, there is no way to guarantee or -control in which order the plugins will be executed. This is a limitation -inherited from Blinker_, the dependency Pelican uses to implement signals. +If multiple plugins connect to the same signal, plugins will be executed in the +order they are connected. With ``PLUGINS`` setting, order will be as defined in +the setting. If you rely on auto-discovered namespace plugins, no ``PLUGINS`` +setting, they will be connected in the same order they are discovered (same +order as ``pelican-plugins`` output). If you want to specify the order +explicitly, disable auto-discovery by defining ``PLUGINS`` in the desired order. Namespace plugin structure -------------------------- @@ -341,4 +344,3 @@ custom article, using the ``article_generator_pretaxonomy`` signal:: .. _Pip: https://pip.pypa.io/ .. _pelican-plugins bug #314: https://github.com/getpelican/pelican-plugins/issues/314 -.. _Blinker: https://pythonhosted.org/blinker/ diff --git a/pelican/plugins/signals.py b/pelican/plugins/signals.py index ff129cb4..27177367 100644 --- a/pelican/plugins/signals.py +++ b/pelican/plugins/signals.py @@ -1,4 +1,8 @@ -from blinker import signal +from blinker import signal, Signal +from ordered_set import OrderedSet + +# Signals will call functions in the order of connection, i.e. plugin order +Signal.set_class = OrderedSet # Run-level signals: diff --git a/pelican/tests/test_plugins.py b/pelican/tests/test_plugins.py index ccce684e..55fa8a6a 100644 --- a/pelican/tests/test_plugins.py +++ b/pelican/tests/test_plugins.py @@ -8,6 +8,7 @@ from pelican.plugins._utils import ( load_plugins, plugin_enabled, ) +from pelican.plugins.signals import signal from pelican.tests.support import unittest @@ -263,3 +264,23 @@ class PluginTest(unittest.TestCase): self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins)) self.assertTrue(plugin_enabled("normal_plugin", plugins)) self.assertFalse(plugin_enabled("unknown", plugins)) + + def test_blinker_is_ordered(self): + """ensure that call order is connetion order""" + dummy_signal = signal("dummpy_signal") + + functions = [] + expected = [] + for i in range(50): + # function appends value of i to a list + def func(input, i=i): + input.append(i) + + functions.append(func) + # we expect functions to be run in the connection order + dummy_signal.connect(func) + expected.append(i) + + input = [] + dummy_signal.send(input) + self.assertEqual(input, expected) diff --git a/pyproject.toml b/pyproject.toml index d9fe1a33..816a25f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,11 @@ classifiers = [ ] requires-python = ">=3.8.1,<4.0" dependencies = [ - "blinker>=1.6.3", + "blinker>=1.7.0", "docutils>=0.20.1", "feedgenerator>=2.1.0", "jinja2>=3.1.2", + "ordered-set>=4.1.0", "pygments>=2.16.1", "python-dateutil>=2.8.2", "rich>=13.6.0", From 451b094a940ab28060784a1030b84bd39a2a523d Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sat, 4 Nov 2023 00:54:21 +0300 Subject: [PATCH 71/88] remove social icons from notmyidea theme redistribution of these icons may not be compatible with AGPL --- pelican/tests/output/basic/theme/css/main.css | 33 ------------------ .../basic/theme/images/icons/aboutme.png | Bin 411 -> 0 bytes .../basic/theme/images/icons/bitbucket.png | Bin 3178 -> 0 bytes .../basic/theme/images/icons/delicious.png | Bin 827 -> 0 bytes .../basic/theme/images/icons/facebook.png | Bin 150 -> 0 bytes .../basic/theme/images/icons/github.png | Bin 606 -> 0 bytes .../basic/theme/images/icons/gitorious.png | Bin 223 -> 0 bytes .../basic/theme/images/icons/gittip.png | Bin 402 -> 0 bytes .../theme/images/icons/google-groups.png | Bin 420 -> 0 bytes .../basic/theme/images/icons/google-plus.png | Bin 511 -> 0 bytes .../basic/theme/images/icons/hackernews.png | Bin 2771 -> 0 bytes .../basic/theme/images/icons/lastfm.png | Bin 840 -> 0 bytes .../basic/theme/images/icons/linkedin.png | Bin 625 -> 0 bytes .../basic/theme/images/icons/reddit.png | Bin 458 -> 0 bytes .../output/basic/theme/images/icons/rss.png | Bin 751 -> 0 bytes .../basic/theme/images/icons/slideshare.png | Bin 435 -> 0 bytes .../basic/theme/images/icons/speakerdeck.png | Bin 580 -> 0 bytes .../theme/images/icons/stackoverflow.png | Bin 414 -> 0 bytes .../basic/theme/images/icons/twitter.png | Bin 416 -> 0 bytes .../output/basic/theme/images/icons/vimeo.png | Bin 349 -> 0 bytes .../basic/theme/images/icons/youtube.png | Bin 316 -> 0 bytes .../tests/output/custom/theme/css/main.css | 33 ------------------ .../custom/theme/images/icons/aboutme.png | Bin 411 -> 0 bytes .../custom/theme/images/icons/bitbucket.png | Bin 3178 -> 0 bytes .../custom/theme/images/icons/delicious.png | Bin 827 -> 0 bytes .../custom/theme/images/icons/facebook.png | Bin 150 -> 0 bytes .../custom/theme/images/icons/github.png | Bin 606 -> 0 bytes .../custom/theme/images/icons/gitorious.png | Bin 223 -> 0 bytes .../custom/theme/images/icons/gittip.png | Bin 402 -> 0 bytes .../theme/images/icons/google-groups.png | Bin 420 -> 0 bytes .../custom/theme/images/icons/google-plus.png | Bin 511 -> 0 bytes .../custom/theme/images/icons/hackernews.png | Bin 2771 -> 0 bytes .../custom/theme/images/icons/lastfm.png | Bin 840 -> 0 bytes .../custom/theme/images/icons/linkedin.png | Bin 625 -> 0 bytes .../custom/theme/images/icons/reddit.png | Bin 458 -> 0 bytes .../output/custom/theme/images/icons/rss.png | Bin 751 -> 0 bytes .../custom/theme/images/icons/slideshare.png | Bin 435 -> 0 bytes .../custom/theme/images/icons/speakerdeck.png | Bin 580 -> 0 bytes .../theme/images/icons/stackoverflow.png | Bin 414 -> 0 bytes .../custom/theme/images/icons/twitter.png | Bin 416 -> 0 bytes .../custom/theme/images/icons/vimeo.png | Bin 349 -> 0 bytes .../custom/theme/images/icons/youtube.png | Bin 316 -> 0 bytes .../output/custom_locale/theme/css/main.css | 33 ------------------ .../theme/images/icons/aboutme.png | Bin 411 -> 0 bytes .../theme/images/icons/bitbucket.png | Bin 3178 -> 0 bytes .../theme/images/icons/delicious.png | Bin 827 -> 0 bytes .../theme/images/icons/facebook.png | Bin 150 -> 0 bytes .../theme/images/icons/github.png | Bin 606 -> 0 bytes .../theme/images/icons/gitorious.png | Bin 223 -> 0 bytes .../theme/images/icons/gittip.png | Bin 402 -> 0 bytes .../theme/images/icons/google-groups.png | Bin 420 -> 0 bytes .../theme/images/icons/google-plus.png | Bin 511 -> 0 bytes .../theme/images/icons/hackernews.png | Bin 2771 -> 0 bytes .../theme/images/icons/lastfm.png | Bin 840 -> 0 bytes .../theme/images/icons/linkedin.png | Bin 625 -> 0 bytes .../theme/images/icons/reddit.png | Bin 458 -> 0 bytes .../custom_locale/theme/images/icons/rss.png | Bin 751 -> 0 bytes .../theme/images/icons/slideshare.png | Bin 435 -> 0 bytes .../theme/images/icons/speakerdeck.png | Bin 580 -> 0 bytes .../theme/images/icons/stackoverflow.png | Bin 414 -> 0 bytes .../theme/images/icons/twitter.png | Bin 416 -> 0 bytes .../theme/images/icons/vimeo.png | Bin 349 -> 0 bytes .../theme/images/icons/youtube.png | Bin 316 -> 0 bytes pelican/themes/notmyidea/static/css/main.css | 33 ------------------ .../notmyidea/static/images/icons/aboutme.png | Bin 411 -> 0 bytes .../static/images/icons/bitbucket.png | Bin 3178 -> 0 bytes .../static/images/icons/delicious.png | Bin 827 -> 0 bytes .../static/images/icons/facebook.png | Bin 150 -> 0 bytes .../notmyidea/static/images/icons/github.png | Bin 606 -> 0 bytes .../static/images/icons/gitorious.png | Bin 223 -> 0 bytes .../notmyidea/static/images/icons/gittip.png | Bin 402 -> 0 bytes .../static/images/icons/google-groups.png | Bin 420 -> 0 bytes .../static/images/icons/google-plus.png | Bin 511 -> 0 bytes .../static/images/icons/hackernews.png | Bin 2771 -> 0 bytes .../notmyidea/static/images/icons/lastfm.png | Bin 840 -> 0 bytes .../static/images/icons/linkedin.png | Bin 625 -> 0 bytes .../notmyidea/static/images/icons/reddit.png | Bin 458 -> 0 bytes .../notmyidea/static/images/icons/rss.png | Bin 751 -> 0 bytes .../static/images/icons/slideshare.png | Bin 435 -> 0 bytes .../static/images/icons/speakerdeck.png | Bin 580 -> 0 bytes .../static/images/icons/stackoverflow.png | Bin 414 -> 0 bytes .../notmyidea/static/images/icons/twitter.png | Bin 416 -> 0 bytes .../notmyidea/static/images/icons/vimeo.png | Bin 349 -> 0 bytes .../notmyidea/static/images/icons/youtube.png | Bin 316 -> 0 bytes 84 files changed, 132 deletions(-) delete mode 100644 pelican/tests/output/basic/theme/images/icons/aboutme.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/bitbucket.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/delicious.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/facebook.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/github.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/gitorious.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/gittip.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/google-groups.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/google-plus.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/hackernews.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/lastfm.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/linkedin.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/reddit.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/rss.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/slideshare.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/speakerdeck.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/stackoverflow.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/twitter.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/vimeo.png delete mode 100644 pelican/tests/output/basic/theme/images/icons/youtube.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/aboutme.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/bitbucket.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/delicious.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/facebook.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/github.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/gitorious.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/gittip.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/google-groups.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/google-plus.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/hackernews.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/lastfm.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/linkedin.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/reddit.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/rss.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/slideshare.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/speakerdeck.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/stackoverflow.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/twitter.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/vimeo.png delete mode 100644 pelican/tests/output/custom/theme/images/icons/youtube.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/aboutme.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/bitbucket.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/delicious.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/facebook.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/github.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/gitorious.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/gittip.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/google-groups.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/google-plus.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/hackernews.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/lastfm.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/linkedin.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/reddit.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/rss.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/slideshare.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/stackoverflow.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/twitter.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/vimeo.png delete mode 100644 pelican/tests/output/custom_locale/theme/images/icons/youtube.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/aboutme.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/bitbucket.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/delicious.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/facebook.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/github.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/gitorious.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/gittip.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/google-groups.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/google-plus.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/hackernews.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/lastfm.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/linkedin.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/reddit.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/rss.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/slideshare.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/speakerdeck.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/stackoverflow.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/twitter.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/vimeo.png delete mode 100644 pelican/themes/notmyidea/static/images/icons/youtube.png diff --git a/pelican/tests/output/basic/theme/css/main.css b/pelican/tests/output/basic/theme/css/main.css index a4aa51a1..49de03d9 100644 --- a/pelican/tests/output/basic/theme/css/main.css +++ b/pelican/tests/output/basic/theme/css/main.css @@ -322,39 +322,6 @@ div.figure p.caption, figure p.caption { /* margin provided by figure */ max-width: 175px; } - #extras div[class='social'] a { - background-repeat: no-repeat; - background-position: 3px 6px; - padding-left: 25px; - } - - /* Icons */ - .social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');} - .social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');} - .social a[href*='delicious.com'] {background-image: url('../images/icons/delicious.png');} - .social a[href*='facebook.com'] {background-image: url('../images/icons/facebook.png');} - .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');} - .social a[href*='github.com'], - .social a[href*='git.io'] { - background-image: url('../images/icons/github.png'); - background-size: 16px 16px; - } - .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');} - .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');} - .social a[href*='groups.google.com'] {background-image: url('../images/icons/google-groups.png');} - .social a[href*='news.ycombinator.com'], - .social a[href*='hackernewsers.com'] {background-image: url('../images/icons/hackernews.png');} - .social a[href*='last.fm'], .social a[href*='lastfm.'] {background-image: url('../images/icons/lastfm.png');} - .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');} - .social a[href*='reddit.com'] {background-image: url('../images/icons/reddit.png');} - .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');} - .social a[href*='slideshare.net'] {background-image: url('../images/icons/slideshare.png');} - .social a[href*='speakerdeck.com'] {background-image: url('../images/icons/speakerdeck.png');} - .social a[href*='stackoverflow.com'] {background-image: url('../images/icons/stackoverflow.png');} - .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');} - .social a[href*='vimeo.com'] {background-image: url('../images/icons/vimeo.png');} - .social a[href*='youtube.com'] {background-image: url('../images/icons/youtube.png');} - /* About *****************/ diff --git a/pelican/tests/output/basic/theme/images/icons/aboutme.png b/pelican/tests/output/basic/theme/images/icons/aboutme.png deleted file mode 100644 index 600110f2f63811ebea40af1b943fded0275b6a39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 411 zcmV;M0c8G(P)UqK~#7FjgmEk6+sXNzkTlh65QS0-8I48iI50)i1CK$|J3O zMSiddRs94I-RZ%BpPVQlG-*wE)^@pKur#bMkyS)ii4hvVB0PPwtX?$Lk2sgu3yxdr zMHBD`H`aw%Ysl&kiS1h=1)tbo!~Ye`fCpjdO!(yU+ZHs&^bQiZX+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@*Jzj349X=?9_$gI2kIcwdunCZtMPOnPe)w(oZZmY=P7Ks9%FQb&p8>E zTGTu^G&~wGL&GC!#VuGydpI#q;n4F`-2p*w5hggKWfvm1-Ht!-D0%;*r0sX&mtTmS z=#a1o5c3rNC06S#lEEUl4ZT(7hJXc%h^4bgLQ=J-kX6=dv2qWx(KFxw<| z8p~uAa?0C01wO*eV=5{z+2r5xRmC+O#kC#3;ww#bTsi?_p2FbJ$cF2&dW*;dEIS`r z-{$NC7APVX0b-s4bL>@`-m2iuJf@Y`cU}9I16ZJlSOkdq3%}wjQF(Q}Mc#EUA+xj< z?7W)*1-w0hpaS1pO{?AaFnQ`>1)C;df$n>lghe2z(B3_8<9lvWK~rg6XL?CXWL7;$ zD`|ly6*S-encvkr7*yZ~ue};=vMZZ!#$EfG^I9zdEKo!aRp=d<^B=Mdj&Pkt)pJqZACTW#!hM@D5IKDqq;%eMz!tbv+awqP8IHt=!XVS zsZ`n>$2lgOwFdh8dv^BsZ>()0em94EiuYK~#M+HZNki~hy!n`DO0RgV7kx83G-6kVzbw2peZ^$T0XF+WR zfC!}kbTrMykUD-`pyq(>!2GmFl&qi|8BD#Yimm6&NW;(o1*9a20zlURQu0~F;$lpk zC^lANi08=`Jt9})*0WlBM`^dLR$E*Uq-h9fXc|BQRMFBY=8w1tql8shMigC06q$^` z3o;x$Brcc5Vx|EggvrrZTv~ueFR9S5n?9&@2ra;ZlJa{y#y*#;tE-3i@BjIXTFqx< zWQM^b2eIvt(ZSC+IB^l#ZO?Jct~%A$Zme8|ylK!@D6=^GmdF`z&B`yo_%8qUJKqup zI`#T8-~s^<(cM|*r>_LNwibZ|C;c5580x>Z^zql9MEVifl5$XO00aq!?ra*2Oy6Bcd7s& z4E&Wd&NypYm|1Hm3>yT#OOnPY6%hC_U@Jle48x$&@MzjDei*iPb+`A(ty}jT{_VFp0@p^D9%vpdEdoQ&A~UVr|G*1s z*N$#`bZl0nsoeDaV0>!ow??I6E} zTD@V0|CC##a5uK^=-$j1%KE$Sep%nY|D6y3VDFw|_#0Q*b=;R(Ji7n@002ovPDHLk FV1ku*k$3ca`voa6f0k zeB{69k@N|?Km7UBZZ))YUHu=w|Mw?(E)O$?WixY_FPL7s$Pm-8fstds@^ATL%bUet yZgfGChJzsUtA%$MR;8+S88GxaEa+z7`Ew3v5re0zpUXO@geCxdur~kz diff --git a/pelican/tests/output/basic/theme/images/icons/github.png b/pelican/tests/output/basic/theme/images/icons/github.png deleted file mode 100644 index 5d9109deab2b3669c845845515f4d53ab5d6daa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 606 zcmV-k0-^nhP)3g@6&i?1|w)NXSf84|8m&o+0 zUbR{k48>ZU#v6!wqi1V1r%EE$MYAT@gfEZ`ebJgG`3x?P75EOx(Rb}pK9^g49TLfP zG|6;$SGbAH zWw$>tFUyCTTU=}VL5$UQcykD>ruLPAMktgpS2)vHd2`;>>D~O#r0q^piwN%{Eu7xl ze_3-g6EyPr+mZ( z$~zD9`4D^$F>WOyU!f<&c%QI`>dHS@;3~xO7I+5=MISM;>bsLPrcC8T?Gyg2EhYt{Z#{?9|afq=U$pRKgVkaa6A5hOW zm~E0;qoMsELHiVUdq+qHW_!=pLlCuB&nsXkrlThcr&yqB2Ez}bh}WQodCMG=62#-T(jq diff --git a/pelican/tests/output/basic/theme/images/icons/gitorious.png b/pelican/tests/output/basic/theme/images/icons/gitorious.png deleted file mode 100644 index a6705d0f462b505be4d9a5a372743d206c141a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!O@L2`D?^o)dRwqYcev@q#DMvQ z*(*AHH`kW0om8=NO8&uVRmbKn*uQ@5(H+Zf?%R0x*ww37FTJ_{>eZ{eAD{pK|9`68 zWgnn9ah@)YAr*|ZCyp{U2QWBn{C>Cgf8^^ph2sKmrZ@0Db>47eliif`GZN3kOR|4E zp1HSr>(=i-J2K=SnYPGB*}QnjI%8UJ(Sx8gTe~DWM4fr0!yB diff --git a/pelican/tests/output/basic/theme/images/icons/gittip.png b/pelican/tests/output/basic/theme/images/icons/gittip.png deleted file mode 100644 index b9f67aaa32a0acd166d7271b3dbf9113717d3f00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMfoB*E?S0J6}W0T-*4J5$~5D8`b z*u=V-n;Wh0%9W86%yoICmD^QXBJ+TuMeduj_mynB29_Kl1X*A2_&{r~^}-@kve zdYW2FGLG%r8Ru!aX~lxyzki)LushDvqC7Rcx2`zG-R$h~L;wH%fBfKXl#6k)zx}VD zKk}k|sxxDHYKs!Qtrkt~nbKapYTmR2Z<_=!t4|TTzXRRJSrX(I%%C#k)~^Ns|6P3T zwz~@CcuyC{kP61s^QK8h9R!*aGdC-5mQ6dy==6QR=MEp?G>PQz`~2?eFFMS(r+K1g zp!&qb32C!ZG_9s->U*tRla<=$@$cY?|5H+adAMETx@7!MusHjBPl?KXziQ+6?1D=V ze?2)-gt;N$>Wd8<>nsYyl^Lt{Fxgl%o_?@o{T-$+24C7lzi+*sf1s9QvY5Z$XS=Iy lUyMDUTPOc|vvcNeU4iM^B0ctPia>WVc)I$ztaD0e0syFMv7G<_ diff --git a/pelican/tests/output/basic/theme/images/icons/google-groups.png b/pelican/tests/output/basic/theme/images/icons/google-groups.png deleted file mode 100644 index bbd0a0fd41295e94081103efe82742a5c6411765..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmV;V0bBlwP)wr?kW++e@xB+qT~SFYEF=HvR&ieDr#i-_xj} zRHo4+e;!+Noelr;cc?77Y;{c*CNr6Zes7m1^J^+M9sV--D^!{k1_1#;6aW?Y?FKU> z$wUFckfr-O)R-Be!K8BMN$?ljo<0akOpD)OEKI~9+x!b$boZC|3lK-`{C{xHEuU!d zCn!ydf)v}GF)B$#1SpIDL5ep&8ms%oIbw(Zf<>Eu zV{!X$x!~%@{^9sNR&mx%AN_fQnat#ZJ3j~K-$A9Q{GPt1rQRSFm;ow5sT2?@^QR4r zg$anDgeCx$iqb6h2iG6)ockYoL!0v;mgd!$p2~_@_rOO_1Na`l%Wwj6hQdz( O0000oJ)blrcs(C@6ijHhGjL zrFRi_0z}#gh+z_?jx?ulJ6ZEDq_MICdrx(I>4q_hBh(7t5P3nx z6N?eUwA{x^#MIz^w4#;vg#Q`{-|sTaZm+z|gUicmu%q_q_{S4K=37O~-YZFy$l zCeshS!0W4y4-;Rs{O>veK>w%APq})#_~VzlN~kv#Z!>>U^FI6esN%Y~QuIs1d#~iY zS5C<<$DO!3cexVMKA`VP&KrXDnvg`iD8?-qH)mw8u{$e0Ls5IrFnpzz@8B4^@ijQjs{a4Z+iYWeG zt$5N5AMVx+R~iI;l~A9E$d3ce%4xvm2W_r2NEe$slb&w_>f!(Z002ovPDHLkV1lnz B@09=m diff --git a/pelican/tests/output/basic/theme/images/icons/hackernews.png b/pelican/tests/output/basic/theme/images/icons/hackernews.png deleted file mode 100644 index 8e05e3ee207e114a96f3811f46f68d17d52fe80b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2771 zcmV;^3M}=BP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@QN3Mg3?1|NsB>i~#tuXaD5J_o76e z$B4WD001vZL_t(|0b{@m1Qi$r!3;wKW??Xc(Sbz(%wQEZ41g$d6oINX6oxVw1@JNO Z0sz-j0)=#JDs2D&002ovPDHLkV1kM1SuX$p diff --git a/pelican/tests/output/basic/theme/images/icons/lastfm.png b/pelican/tests/output/basic/theme/images/icons/lastfm.png deleted file mode 100644 index 2eedd2daa1429ba026c0b3c81f200c7df55d199a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 840 zcmV-O1GoH%P)KCJu$;;6i zN)?b2a3_#VfTsWs$6EHV5TGNlK%}w4u2N|o9mJtskW6u)FfMY|?6HLq0N~tSmCYiQ z2itn^l^G`qTae2D0xESFZh|%tXmDcRU;!-yQXzzdgu?j^sE-_`#F)I#a0AjAIORAv z?nua{5j_L2Xf^mv7?p>M&VsISuyh@C_u_&8Tdjpsq*G;#gR3rt3(wqpYJX|(pqnm- zVi_(t9gaQ}dWUeQ9S7b0nAMvJMjCC()%*h2T()P6`Q<<4KoQZ3;E)b@mqq9E^qzbJ^&kHt zb@z3sXYQl9ZY#2oD@eP!I~#uc8*V^AwupI;L7D|Jvi|(8qu0 z2LL$zBz)6?fx+w8bl#l{x;rm&{_hjrZqU60&R?r z_q^a%r%rJ@Z+x)Fd-;e{U-Iff*T}z9(5p5g3wcDXPJR3rG$+nsWZfoYCWAb*gT_Bo z7fg}|L9J&75r5f0ZeI>gIn5MAw<$RbKrq)K4wK5le| zR(k=@SlVyBWV*`avXNJ2NP$f#q_(8G+6X;MtJy>;4`+;2 znIjMBZmisPo~Hw3I)yO?atTx+i^wMNq>KkRoaf=R#1u=&Mhm*)MMoa?yQ=3?Z|U=o zU9OHh!itfmFwG$0g0M5@al}`~v399M8f%oORSV+K_KnG|j?V;kMf)2sF5LyF&y{nJ Sd8X6=0000 diff --git a/pelican/tests/output/basic/theme/images/icons/linkedin.png b/pelican/tests/output/basic/theme/images/icons/linkedin.png deleted file mode 100644 index 06a88016f191d1f7596e5d159eb9e3ef276b6ad4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 625 zcmV-%0*?KOP)NJD$VFG5yq%r(;8zR}7Ydty&~ue@oPv z!go{cG`vvH)n(hYdJfur_4kM7-}`N$BLW71f|-O65QELNn>4;$_2zi#L7VQdUf*VG zc0B*xR8@Sfo>nmHN}+MDnqn5J#;f9g$TZW=OS!!C<4lU-jh|=cs;0XYW~;nhJEE0r zdMWpJ#bDNMT&}cGH>t7P-yM4W=hFCXaLv`alDWtkCwY0;!z58#z z#&T71?c1;2yXPjW|3Lt-K|fa>vgM24P7nS%my|*-?u{?k1z4y`Q@qLr7+I)}xOVW2 zJ3sxy0Crrjx2{T+o3K*EU*7;Otu&GVZhd>`xi2Rl`h0wRu?9M0pr|e$T%t()-9X(G zM?fxx1SUyc#XyCAwXc5_{TIPFaU$>TzBm6pmt%Vs1;7F00000 LNkvXXu0mjfHfAMI diff --git a/pelican/tests/output/basic/theme/images/icons/reddit.png b/pelican/tests/output/basic/theme/images/icons/reddit.png deleted file mode 100644 index d826d3e746a18b712db7a68746c0c523af802fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmV;*0X6=KP)rn|{qWNU7Dgl~R~ zWNK}Sjg6(Fp|rEIpPrq2fPh0AJ zxy#GTfq{X3etxmY-Bn&^YHMt-t*g_~&)L`2qqxp^h?r+{fMacVpMN&`0001qNklWKP}*}Is^$| zQ}1hfWmjT2av-PYqbE!!n)oua2c5|Qn7a$5l#p|w6z7}`F{d?z0T5WQglfOLER+{E--Hu?5@wDUpV?F@=Mkg+ zN{1=uqDlzrnkbYp)J3q!I?Vn|_~M zVqi53FAJQ^xo7*4+R80OflZT^0WscTaBuO7ca6A_aA-H9M3Y@{DSLGBL)| zTym{{me}!*Z3962jmy>w<<#@)IsY9R%I3sYbB>~@mM@C5Ks~s4nX-}TW8S>D*g7F! hew!yYJnD{z;D0NLV2ni3qF(?2002ovPDHLkV1k{La&G_t diff --git a/pelican/tests/output/basic/theme/images/icons/slideshare.png b/pelican/tests/output/basic/theme/images/icons/slideshare.png deleted file mode 100644 index 9cbe8588e24976076e5fc3898fa97856122f6fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmV;k0ZjghP)YNn`_&)?H$|J+jM18jcAeNE5840mI#38{rmUny@9DE z#Xg@8y@9DE0*}W7yWQR+m>Mc@yWPA^!~YvlZXQ7Ti(&Z??RignZicC$0;ki-+teNz zN*Qw@h$6fTDiChi(;mQdV>IdC=QLyP(`3?fGmSD0E706p2ko0(Fn89&YPG`Qa4@$> zdhWPhszyk06L$T`#EcuEoIB7dp?k0q>OF&bXs#{9!Ot=1fBXlJ={G`9+@@d!27`fI z*^>PO`g==w@1W}FBwRnS8UK$h$DgmS5K-O+o6QEJ(Z~waYBd=2dOa)_3-qUlQ$o(J zM$*}Z*m~g-OePa_IvoOeR-jNQz{PlZ6lKDV2#PRA1rmt_T+Fw|P-fhVCCq=C0AY>_ z#9}czIyxxk)H|>~8V7Uy1sn}2#;nIlxE)_f_XF?+t*xyTxm-@(N8ncJn41xc#e6|8 dAc8(0=Pod^EtOP(ARYh!002ovPDHLkV1k4J!&(3U diff --git a/pelican/tests/output/basic/theme/images/icons/speakerdeck.png b/pelican/tests/output/basic/theme/images/icons/speakerdeck.png deleted file mode 100644 index 7281ec48b41095180a83d71eaf0b00c85116965b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xZ*P)K92#E{?_PJ+8Tth@W(+a>28uk~_0`h`<(yY|`h z@3?AaV0?I7E@5C8LL5^B7?_#m1O5GLKL6{LaQ3ZN%$$19RhM^ilM4(QGb91Pk}#td z%#5L?Rn9&W7zkqrdeL$z%L))wjb8cZK2wtEL}mE*O&6H3%~&#zdHx68i}xJQZ+34dIdgm^E12V4($L!Sax!Q)0op z%%0y@llS*=IU_?ugf;+>a;DdHv`xcYwwSu^B5bgwWG8rbaUIq4<$P}6+C9-V(ZSGy!i4XI zBkkxgITji#coxBn7F*@>CvnH5Im-dI=x{a@nz|+mZz(oB8CqTLuD=F=p|2qrpWMi5G S=mtXo0000YRoeQcY09>#1xxKwS5F z8mlP6JGx3wQgxp~4T-?1`Rz0oPN3$iK>QzwU%sBkR`YrWi#XVjbZs+WxcmSb0KzU< z4R|<_pXuFnCY5*7ncLn>WBm*@=rz!g=9fLZ0#9?*eV!!i{{b4*NYE=UCUZ&x4QhHf zjrr}HsjUBBPiJ}aY9eRTvkFsE%bi<+hJ_q$)(X1QD;kN@ z$%`wz`8QVB1Z}9WYXRb&Kzt8~{{vn2e{DI?z%r-*E6ZK%Ff``oJb76HYP@>p``(Y0Zs!*!BjN>0I%aNVnQ8)X9Pc^Y zIsS936mToi>>TyPEkcc+2 z`5!z^ZFZ_O#v*`YvS&^RN+0_pGFvTxh}Lp2wqGagAdVyz;6#9udAenJvhWwC3QjN} ztg*<~69%xm!G(wSxK?V6H7%K70|sCIX&%<6rLrHmSw|10dx_{@95hn1(i_a}5^hC>VT?7YQHpjC*P~ez&J%_J0000< KMNUMnLSTY<*|#qM diff --git a/pelican/tests/output/basic/theme/images/icons/vimeo.png b/pelican/tests/output/basic/theme/images/icons/vimeo.png deleted file mode 100644 index 4b9d72127c574237090a2f5f2f7eb7be94a2b62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349 zcmV-j0iyniP)K~#7FV*mo~S^J1YP}SCZ?(wd@Mi2p&f%E_|kV7PbYe1Nxz5O|y=nOth{(bt=TS6pPua;?J=e5) zBGkM)Pk5GtRBXBib(QPnRVW%@oC6PDyoRVi_QQr_=YS?+52c0sPeBv`1LxxFPe2CH z8{$R%ID<81>CXSCz@?@iUpz?mZ$MU^)He_47^%Sg0P#sgK~#7Fl~6T81W^oq2cX(5aX1R@6<62}xdAmT6>b%79aG~T97?pp zUBiU8?wcQZ@ytL(4yKyhs>_L_h@KZq&4RGqy&kuUuOC&N_n)+)st0x|+@BHFkRd@&PPEwb z&tB6>xX7cYComKsCfH0jmXR=>tbysq2Gd0WOmZB{y88Z3Wj9^}w-Gbm)y>{S4W9T7 zfp3d!a2x}~TiXWa<}BOO6S3ho2QJ^`nhs+IjSU!vHHb*~-y}#B5s^RLsrl%w&%5IQ O0000UqK~#7FjgmEk6+sXNzkTlh65QS0-8I48iI50)i1CK$|J3O zMSiddRs94I-RZ%BpPVQlG-*wE)^@pKur#bMkyS)ii4hvVB0PPwtX?$Lk2sgu3yxdr zMHBD`H`aw%Ysl&kiS1h=1)tbo!~Ye`fCpjdO!(yU+ZHs&^bQiZX+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@*Jzj349X=?9_$gI2kIcwdunCZtMPOnPe)w(oZZmY=P7Ks9%FQb&p8>E zTGTu^G&~wGL&GC!#VuGydpI#q;n4F`-2p*w5hggKWfvm1-Ht!-D0%;*r0sX&mtTmS z=#a1o5c3rNC06S#lEEUl4ZT(7hJXc%h^4bgLQ=J-kX6=dv2qWx(KFxw<| z8p~uAa?0C01wO*eV=5{z+2r5xRmC+O#kC#3;ww#bTsi?_p2FbJ$cF2&dW*;dEIS`r z-{$NC7APVX0b-s4bL>@`-m2iuJf@Y`cU}9I16ZJlSOkdq3%}wjQF(Q}Mc#EUA+xj< z?7W)*1-w0hpaS1pO{?AaFnQ`>1)C;df$n>lghe2z(B3_8<9lvWK~rg6XL?CXWL7;$ zD`|ly6*S-encvkr7*yZ~ue};=vMZZ!#$EfG^I9zdEKo!aRp=d<^B=Mdj&Pkt)pJqZACTW#!hM@D5IKDqq;%eMz!tbv+awqP8IHt=!XVS zsZ`n>$2lgOwFdh8dv^BsZ>()0em94EiuYK~#M+HZNki~hy!n`DO0RgV7kx83G-6kVzbw2peZ^$T0XF+WR zfC!}kbTrMykUD-`pyq(>!2GmFl&qi|8BD#Yimm6&NW;(o1*9a20zlURQu0~F;$lpk zC^lANi08=`Jt9})*0WlBM`^dLR$E*Uq-h9fXc|BQRMFBY=8w1tql8shMigC06q$^` z3o;x$Brcc5Vx|EggvrrZTv~ueFR9S5n?9&@2ra;ZlJa{y#y*#;tE-3i@BjIXTFqx< zWQM^b2eIvt(ZSC+IB^l#ZO?Jct~%A$Zme8|ylK!@D6=^GmdF`z&B`yo_%8qUJKqup zI`#T8-~s^<(cM|*r>_LNwibZ|C;c5580x>Z^zql9MEVifl5$XO00aq!?ra*2Oy6Bcd7s& z4E&Wd&NypYm|1Hm3>yT#OOnPY6%hC_U@Jle48x$&@MzjDei*iPb+`A(ty}jT{_VFp0@p^D9%vpdEdoQ&A~UVr|G*1s z*N$#`bZl0nsoeDaV0>!ow??I6E} zTD@V0|CC##a5uK^=-$j1%KE$Sep%nY|D6y3VDFw|_#0Q*b=;R(Ji7n@002ovPDHLk FV1ku*k$3ca`voa6f0k zeB{69k@N|?Km7UBZZ))YUHu=w|Mw?(E)O$?WixY_FPL7s$Pm-8fstds@^ATL%bUet yZgfGChJzsUtA%$MR;8+S88GxaEa+z7`Ew3v5re0zpUXO@geCxdur~kz diff --git a/pelican/tests/output/custom/theme/images/icons/github.png b/pelican/tests/output/custom/theme/images/icons/github.png deleted file mode 100644 index 5d9109deab2b3669c845845515f4d53ab5d6daa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 606 zcmV-k0-^nhP)3g@6&i?1|w)NXSf84|8m&o+0 zUbR{k48>ZU#v6!wqi1V1r%EE$MYAT@gfEZ`ebJgG`3x?P75EOx(Rb}pK9^g49TLfP zG|6;$SGbAH zWw$>tFUyCTTU=}VL5$UQcykD>ruLPAMktgpS2)vHd2`;>>D~O#r0q^piwN%{Eu7xl ze_3-g6EyPr+mZ( z$~zD9`4D^$F>WOyU!f<&c%QI`>dHS@;3~xO7I+5=MISM;>bsLPrcC8T?Gyg2EhYt{Z#{?9|afq=U$pRKgVkaa6A5hOW zm~E0;qoMsELHiVUdq+qHW_!=pLlCuB&nsXkrlThcr&yqB2Ez}bh}WQodCMG=62#-T(jq diff --git a/pelican/tests/output/custom/theme/images/icons/gitorious.png b/pelican/tests/output/custom/theme/images/icons/gitorious.png deleted file mode 100644 index a6705d0f462b505be4d9a5a372743d206c141a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!O@L2`D?^o)dRwqYcev@q#DMvQ z*(*AHH`kW0om8=NO8&uVRmbKn*uQ@5(H+Zf?%R0x*ww37FTJ_{>eZ{eAD{pK|9`68 zWgnn9ah@)YAr*|ZCyp{U2QWBn{C>Cgf8^^ph2sKmrZ@0Db>47eliif`GZN3kOR|4E zp1HSr>(=i-J2K=SnYPGB*}QnjI%8UJ(Sx8gTe~DWM4fr0!yB diff --git a/pelican/tests/output/custom/theme/images/icons/gittip.png b/pelican/tests/output/custom/theme/images/icons/gittip.png deleted file mode 100644 index b9f67aaa32a0acd166d7271b3dbf9113717d3f00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMfoB*E?S0J6}W0T-*4J5$~5D8`b z*u=V-n;Wh0%9W86%yoICmD^QXBJ+TuMeduj_mynB29_Kl1X*A2_&{r~^}-@kve zdYW2FGLG%r8Ru!aX~lxyzki)LushDvqC7Rcx2`zG-R$h~L;wH%fBfKXl#6k)zx}VD zKk}k|sxxDHYKs!Qtrkt~nbKapYTmR2Z<_=!t4|TTzXRRJSrX(I%%C#k)~^Ns|6P3T zwz~@CcuyC{kP61s^QK8h9R!*aGdC-5mQ6dy==6QR=MEp?G>PQz`~2?eFFMS(r+K1g zp!&qb32C!ZG_9s->U*tRla<=$@$cY?|5H+adAMETx@7!MusHjBPl?KXziQ+6?1D=V ze?2)-gt;N$>Wd8<>nsYyl^Lt{Fxgl%o_?@o{T-$+24C7lzi+*sf1s9QvY5Z$XS=Iy lUyMDUTPOc|vvcNeU4iM^B0ctPia>WVc)I$ztaD0e0syFMv7G<_ diff --git a/pelican/tests/output/custom/theme/images/icons/google-groups.png b/pelican/tests/output/custom/theme/images/icons/google-groups.png deleted file mode 100644 index bbd0a0fd41295e94081103efe82742a5c6411765..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmV;V0bBlwP)wr?kW++e@xB+qT~SFYEF=HvR&ieDr#i-_xj} zRHo4+e;!+Noelr;cc?77Y;{c*CNr6Zes7m1^J^+M9sV--D^!{k1_1#;6aW?Y?FKU> z$wUFckfr-O)R-Be!K8BMN$?ljo<0akOpD)OEKI~9+x!b$boZC|3lK-`{C{xHEuU!d zCn!ydf)v}GF)B$#1SpIDL5ep&8ms%oIbw(Zf<>Eu zV{!X$x!~%@{^9sNR&mx%AN_fQnat#ZJ3j~K-$A9Q{GPt1rQRSFm;ow5sT2?@^QR4r zg$anDgeCx$iqb6h2iG6)ockYoL!0v;mgd!$p2~_@_rOO_1Na`l%Wwj6hQdz( O0000oJ)blrcs(C@6ijHhGjL zrFRi_0z}#gh+z_?jx?ulJ6ZEDq_MICdrx(I>4q_hBh(7t5P3nx z6N?eUwA{x^#MIz^w4#;vg#Q`{-|sTaZm+z|gUicmu%q_q_{S4K=37O~-YZFy$l zCeshS!0W4y4-;Rs{O>veK>w%APq})#_~VzlN~kv#Z!>>U^FI6esN%Y~QuIs1d#~iY zS5C<<$DO!3cexVMKA`VP&KrXDnvg`iD8?-qH)mw8u{$e0Ls5IrFnpzz@8B4^@ijQjs{a4Z+iYWeG zt$5N5AMVx+R~iI;l~A9E$d3ce%4xvm2W_r2NEe$slb&w_>f!(Z002ovPDHLkV1lnz B@09=m diff --git a/pelican/tests/output/custom/theme/images/icons/hackernews.png b/pelican/tests/output/custom/theme/images/icons/hackernews.png deleted file mode 100644 index 8e05e3ee207e114a96f3811f46f68d17d52fe80b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2771 zcmV;^3M}=BP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@QN3Mg3?1|NsB>i~#tuXaD5J_o76e z$B4WD001vZL_t(|0b{@m1Qi$r!3;wKW??Xc(Sbz(%wQEZ41g$d6oINX6oxVw1@JNO Z0sz-j0)=#JDs2D&002ovPDHLkV1kM1SuX$p diff --git a/pelican/tests/output/custom/theme/images/icons/lastfm.png b/pelican/tests/output/custom/theme/images/icons/lastfm.png deleted file mode 100644 index 2eedd2daa1429ba026c0b3c81f200c7df55d199a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 840 zcmV-O1GoH%P)KCJu$;;6i zN)?b2a3_#VfTsWs$6EHV5TGNlK%}w4u2N|o9mJtskW6u)FfMY|?6HLq0N~tSmCYiQ z2itn^l^G`qTae2D0xESFZh|%tXmDcRU;!-yQXzzdgu?j^sE-_`#F)I#a0AjAIORAv z?nua{5j_L2Xf^mv7?p>M&VsISuyh@C_u_&8Tdjpsq*G;#gR3rt3(wqpYJX|(pqnm- zVi_(t9gaQ}dWUeQ9S7b0nAMvJMjCC()%*h2T()P6`Q<<4KoQZ3;E)b@mqq9E^qzbJ^&kHt zb@z3sXYQl9ZY#2oD@eP!I~#uc8*V^AwupI;L7D|Jvi|(8qu0 z2LL$zBz)6?fx+w8bl#l{x;rm&{_hjrZqU60&R?r z_q^a%r%rJ@Z+x)Fd-;e{U-Iff*T}z9(5p5g3wcDXPJR3rG$+nsWZfoYCWAb*gT_Bo z7fg}|L9J&75r5f0ZeI>gIn5MAw<$RbKrq)K4wK5le| zR(k=@SlVyBWV*`avXNJ2NP$f#q_(8G+6X;MtJy>;4`+;2 znIjMBZmisPo~Hw3I)yO?atTx+i^wMNq>KkRoaf=R#1u=&Mhm*)MMoa?yQ=3?Z|U=o zU9OHh!itfmFwG$0g0M5@al}`~v399M8f%oORSV+K_KnG|j?V;kMf)2sF5LyF&y{nJ Sd8X6=0000 diff --git a/pelican/tests/output/custom/theme/images/icons/linkedin.png b/pelican/tests/output/custom/theme/images/icons/linkedin.png deleted file mode 100644 index 06a88016f191d1f7596e5d159eb9e3ef276b6ad4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 625 zcmV-%0*?KOP)NJD$VFG5yq%r(;8zR}7Ydty&~ue@oPv z!go{cG`vvH)n(hYdJfur_4kM7-}`N$BLW71f|-O65QELNn>4;$_2zi#L7VQdUf*VG zc0B*xR8@Sfo>nmHN}+MDnqn5J#;f9g$TZW=OS!!C<4lU-jh|=cs;0XYW~;nhJEE0r zdMWpJ#bDNMT&}cGH>t7P-yM4W=hFCXaLv`alDWtkCwY0;!z58#z z#&T71?c1;2yXPjW|3Lt-K|fa>vgM24P7nS%my|*-?u{?k1z4y`Q@qLr7+I)}xOVW2 zJ3sxy0Crrjx2{T+o3K*EU*7;Otu&GVZhd>`xi2Rl`h0wRu?9M0pr|e$T%t()-9X(G zM?fxx1SUyc#XyCAwXc5_{TIPFaU$>TzBm6pmt%Vs1;7F00000 LNkvXXu0mjfHfAMI diff --git a/pelican/tests/output/custom/theme/images/icons/reddit.png b/pelican/tests/output/custom/theme/images/icons/reddit.png deleted file mode 100644 index d826d3e746a18b712db7a68746c0c523af802fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmV;*0X6=KP)rn|{qWNU7Dgl~R~ zWNK}Sjg6(Fp|rEIpPrq2fPh0AJ zxy#GTfq{X3etxmY-Bn&^YHMt-t*g_~&)L`2qqxp^h?r+{fMacVpMN&`0001qNklWKP}*}Is^$| zQ}1hfWmjT2av-PYqbE!!n)oua2c5|Qn7a$5l#p|w6z7}`F{d?z0T5WQglfOLER+{E--Hu?5@wDUpV?F@=Mkg+ zN{1=uqDlzrnkbYp)J3q!I?Vn|_~M zVqi53FAJQ^xo7*4+R80OflZT^0WscTaBuO7ca6A_aA-H9M3Y@{DSLGBL)| zTym{{me}!*Z3962jmy>w<<#@)IsY9R%I3sYbB>~@mM@C5Ks~s4nX-}TW8S>D*g7F! hew!yYJnD{z;D0NLV2ni3qF(?2002ovPDHLkV1k{La&G_t diff --git a/pelican/tests/output/custom/theme/images/icons/slideshare.png b/pelican/tests/output/custom/theme/images/icons/slideshare.png deleted file mode 100644 index 9cbe8588e24976076e5fc3898fa97856122f6fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmV;k0ZjghP)YNn`_&)?H$|J+jM18jcAeNE5840mI#38{rmUny@9DE z#Xg@8y@9DE0*}W7yWQR+m>Mc@yWPA^!~YvlZXQ7Ti(&Z??RignZicC$0;ki-+teNz zN*Qw@h$6fTDiChi(;mQdV>IdC=QLyP(`3?fGmSD0E706p2ko0(Fn89&YPG`Qa4@$> zdhWPhszyk06L$T`#EcuEoIB7dp?k0q>OF&bXs#{9!Ot=1fBXlJ={G`9+@@d!27`fI z*^>PO`g==w@1W}FBwRnS8UK$h$DgmS5K-O+o6QEJ(Z~waYBd=2dOa)_3-qUlQ$o(J zM$*}Z*m~g-OePa_IvoOeR-jNQz{PlZ6lKDV2#PRA1rmt_T+Fw|P-fhVCCq=C0AY>_ z#9}czIyxxk)H|>~8V7Uy1sn}2#;nIlxE)_f_XF?+t*xyTxm-@(N8ncJn41xc#e6|8 dAc8(0=Pod^EtOP(ARYh!002ovPDHLkV1k4J!&(3U diff --git a/pelican/tests/output/custom/theme/images/icons/speakerdeck.png b/pelican/tests/output/custom/theme/images/icons/speakerdeck.png deleted file mode 100644 index 7281ec48b41095180a83d71eaf0b00c85116965b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xZ*P)K92#E{?_PJ+8Tth@W(+a>28uk~_0`h`<(yY|`h z@3?AaV0?I7E@5C8LL5^B7?_#m1O5GLKL6{LaQ3ZN%$$19RhM^ilM4(QGb91Pk}#td z%#5L?Rn9&W7zkqrdeL$z%L))wjb8cZK2wtEL}mE*O&6H3%~&#zdHx68i}xJQZ+34dIdgm^E12V4($L!Sax!Q)0op z%%0y@llS*=IU_?ugf;+>a;DdHv`xcYwwSu^B5bgwWG8rbaUIq4<$P}6+C9-V(ZSGy!i4XI zBkkxgITji#coxBn7F*@>CvnH5Im-dI=x{a@nz|+mZz(oB8CqTLuD=F=p|2qrpWMi5G S=mtXo0000YRoeQcY09>#1xxKwS5F z8mlP6JGx3wQgxp~4T-?1`Rz0oPN3$iK>QzwU%sBkR`YrWi#XVjbZs+WxcmSb0KzU< z4R|<_pXuFnCY5*7ncLn>WBm*@=rz!g=9fLZ0#9?*eV!!i{{b4*NYE=UCUZ&x4QhHf zjrr}HsjUBBPiJ}aY9eRTvkFsE%bi<+hJ_q$)(X1QD;kN@ z$%`wz`8QVB1Z}9WYXRb&Kzt8~{{vn2e{DI?z%r-*E6ZK%Ff``oJb76HYP@>p``(Y0Zs!*!BjN>0I%aNVnQ8)X9Pc^Y zIsS936mToi>>TyPEkcc+2 z`5!z^ZFZ_O#v*`YvS&^RN+0_pGFvTxh}Lp2wqGagAdVyz;6#9udAenJvhWwC3QjN} ztg*<~69%xm!G(wSxK?V6H7%K70|sCIX&%<6rLrHmSw|10dx_{@95hn1(i_a}5^hC>VT?7YQHpjC*P~ez&J%_J0000< KMNUMnLSTY<*|#qM diff --git a/pelican/tests/output/custom/theme/images/icons/vimeo.png b/pelican/tests/output/custom/theme/images/icons/vimeo.png deleted file mode 100644 index 4b9d72127c574237090a2f5f2f7eb7be94a2b62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349 zcmV-j0iyniP)K~#7FV*mo~S^J1YP}SCZ?(wd@Mi2p&f%E_|kV7PbYe1Nxz5O|y=nOth{(bt=TS6pPua;?J=e5) zBGkM)Pk5GtRBXBib(QPnRVW%@oC6PDyoRVi_QQr_=YS?+52c0sPeBv`1LxxFPe2CH z8{$R%ID<81>CXSCz@?@iUpz?mZ$MU^)He_47^%Sg0P#sgK~#7Fl~6T81W^oq2cX(5aX1R@6<62}xdAmT6>b%79aG~T97?pp zUBiU8?wcQZ@ytL(4yKyhs>_L_h@KZq&4RGqy&kuUuOC&N_n)+)st0x|+@BHFkRd@&PPEwb z&tB6>xX7cYComKsCfH0jmXR=>tbysq2Gd0WOmZB{y88Z3Wj9^}w-Gbm)y>{S4W9T7 zfp3d!a2x}~TiXWa<}BOO6S3ho2QJ^`nhs+IjSU!vHHb*~-y}#B5s^RLsrl%w&%5IQ O0000UqK~#7FjgmEk6+sXNzkTlh65QS0-8I48iI50)i1CK$|J3O zMSiddRs94I-RZ%BpPVQlG-*wE)^@pKur#bMkyS)ii4hvVB0PPwtX?$Lk2sgu3yxdr zMHBD`H`aw%Ysl&kiS1h=1)tbo!~Ye`fCpjdO!(yU+ZHs&^bQiZX+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@*Jzj349X=?9_$gI2kIcwdunCZtMPOnPe)w(oZZmY=P7Ks9%FQb&p8>E zTGTu^G&~wGL&GC!#VuGydpI#q;n4F`-2p*w5hggKWfvm1-Ht!-D0%;*r0sX&mtTmS z=#a1o5c3rNC06S#lEEUl4ZT(7hJXc%h^4bgLQ=J-kX6=dv2qWx(KFxw<| z8p~uAa?0C01wO*eV=5{z+2r5xRmC+O#kC#3;ww#bTsi?_p2FbJ$cF2&dW*;dEIS`r z-{$NC7APVX0b-s4bL>@`-m2iuJf@Y`cU}9I16ZJlSOkdq3%}wjQF(Q}Mc#EUA+xj< z?7W)*1-w0hpaS1pO{?AaFnQ`>1)C;df$n>lghe2z(B3_8<9lvWK~rg6XL?CXWL7;$ zD`|ly6*S-encvkr7*yZ~ue};=vMZZ!#$EfG^I9zdEKo!aRp=d<^B=Mdj&Pkt)pJqZACTW#!hM@D5IKDqq;%eMz!tbv+awqP8IHt=!XVS zsZ`n>$2lgOwFdh8dv^BsZ>()0em94EiuYK~#M+HZNki~hy!n`DO0RgV7kx83G-6kVzbw2peZ^$T0XF+WR zfC!}kbTrMykUD-`pyq(>!2GmFl&qi|8BD#Yimm6&NW;(o1*9a20zlURQu0~F;$lpk zC^lANi08=`Jt9})*0WlBM`^dLR$E*Uq-h9fXc|BQRMFBY=8w1tql8shMigC06q$^` z3o;x$Brcc5Vx|EggvrrZTv~ueFR9S5n?9&@2ra;ZlJa{y#y*#;tE-3i@BjIXTFqx< zWQM^b2eIvt(ZSC+IB^l#ZO?Jct~%A$Zme8|ylK!@D6=^GmdF`z&B`yo_%8qUJKqup zI`#T8-~s^<(cM|*r>_LNwibZ|C;c5580x>Z^zql9MEVifl5$XO00aq!?ra*2Oy6Bcd7s& z4E&Wd&NypYm|1Hm3>yT#OOnPY6%hC_U@Jle48x$&@MzjDei*iPb+`A(ty}jT{_VFp0@p^D9%vpdEdoQ&A~UVr|G*1s z*N$#`bZl0nsoeDaV0>!ow??I6E} zTD@V0|CC##a5uK^=-$j1%KE$Sep%nY|D6y3VDFw|_#0Q*b=;R(Ji7n@002ovPDHLk FV1ku*k$3ca`voa6f0k zeB{69k@N|?Km7UBZZ))YUHu=w|Mw?(E)O$?WixY_FPL7s$Pm-8fstds@^ATL%bUet yZgfGChJzsUtA%$MR;8+S88GxaEa+z7`Ew3v5re0zpUXO@geCxdur~kz diff --git a/pelican/tests/output/custom_locale/theme/images/icons/github.png b/pelican/tests/output/custom_locale/theme/images/icons/github.png deleted file mode 100644 index 5d9109deab2b3669c845845515f4d53ab5d6daa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 606 zcmV-k0-^nhP)3g@6&i?1|w)NXSf84|8m&o+0 zUbR{k48>ZU#v6!wqi1V1r%EE$MYAT@gfEZ`ebJgG`3x?P75EOx(Rb}pK9^g49TLfP zG|6;$SGbAH zWw$>tFUyCTTU=}VL5$UQcykD>ruLPAMktgpS2)vHd2`;>>D~O#r0q^piwN%{Eu7xl ze_3-g6EyPr+mZ( z$~zD9`4D^$F>WOyU!f<&c%QI`>dHS@;3~xO7I+5=MISM;>bsLPrcC8T?Gyg2EhYt{Z#{?9|afq=U$pRKgVkaa6A5hOW zm~E0;qoMsELHiVUdq+qHW_!=pLlCuB&nsXkrlThcr&yqB2Ez}bh}WQodCMG=62#-T(jq diff --git a/pelican/tests/output/custom_locale/theme/images/icons/gitorious.png b/pelican/tests/output/custom_locale/theme/images/icons/gitorious.png deleted file mode 100644 index a6705d0f462b505be4d9a5a372743d206c141a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!O@L2`D?^o)dRwqYcev@q#DMvQ z*(*AHH`kW0om8=NO8&uVRmbKn*uQ@5(H+Zf?%R0x*ww37FTJ_{>eZ{eAD{pK|9`68 zWgnn9ah@)YAr*|ZCyp{U2QWBn{C>Cgf8^^ph2sKmrZ@0Db>47eliif`GZN3kOR|4E zp1HSr>(=i-J2K=SnYPGB*}QnjI%8UJ(Sx8gTe~DWM4fr0!yB diff --git a/pelican/tests/output/custom_locale/theme/images/icons/gittip.png b/pelican/tests/output/custom_locale/theme/images/icons/gittip.png deleted file mode 100644 index b9f67aaa32a0acd166d7271b3dbf9113717d3f00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMfoB*E?S0J6}W0T-*4J5$~5D8`b z*u=V-n;Wh0%9W86%yoICmD^QXBJ+TuMeduj_mynB29_Kl1X*A2_&{r~^}-@kve zdYW2FGLG%r8Ru!aX~lxyzki)LushDvqC7Rcx2`zG-R$h~L;wH%fBfKXl#6k)zx}VD zKk}k|sxxDHYKs!Qtrkt~nbKapYTmR2Z<_=!t4|TTzXRRJSrX(I%%C#k)~^Ns|6P3T zwz~@CcuyC{kP61s^QK8h9R!*aGdC-5mQ6dy==6QR=MEp?G>PQz`~2?eFFMS(r+K1g zp!&qb32C!ZG_9s->U*tRla<=$@$cY?|5H+adAMETx@7!MusHjBPl?KXziQ+6?1D=V ze?2)-gt;N$>Wd8<>nsYyl^Lt{Fxgl%o_?@o{T-$+24C7lzi+*sf1s9QvY5Z$XS=Iy lUyMDUTPOc|vvcNeU4iM^B0ctPia>WVc)I$ztaD0e0syFMv7G<_ diff --git a/pelican/tests/output/custom_locale/theme/images/icons/google-groups.png b/pelican/tests/output/custom_locale/theme/images/icons/google-groups.png deleted file mode 100644 index bbd0a0fd41295e94081103efe82742a5c6411765..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmV;V0bBlwP)wr?kW++e@xB+qT~SFYEF=HvR&ieDr#i-_xj} zRHo4+e;!+Noelr;cc?77Y;{c*CNr6Zes7m1^J^+M9sV--D^!{k1_1#;6aW?Y?FKU> z$wUFckfr-O)R-Be!K8BMN$?ljo<0akOpD)OEKI~9+x!b$boZC|3lK-`{C{xHEuU!d zCn!ydf)v}GF)B$#1SpIDL5ep&8ms%oIbw(Zf<>Eu zV{!X$x!~%@{^9sNR&mx%AN_fQnat#ZJ3j~K-$A9Q{GPt1rQRSFm;ow5sT2?@^QR4r zg$anDgeCx$iqb6h2iG6)ockYoL!0v;mgd!$p2~_@_rOO_1Na`l%Wwj6hQdz( O0000oJ)blrcs(C@6ijHhGjL zrFRi_0z}#gh+z_?jx?ulJ6ZEDq_MICdrx(I>4q_hBh(7t5P3nx z6N?eUwA{x^#MIz^w4#;vg#Q`{-|sTaZm+z|gUicmu%q_q_{S4K=37O~-YZFy$l zCeshS!0W4y4-;Rs{O>veK>w%APq})#_~VzlN~kv#Z!>>U^FI6esN%Y~QuIs1d#~iY zS5C<<$DO!3cexVMKA`VP&KrXDnvg`iD8?-qH)mw8u{$e0Ls5IrFnpzz@8B4^@ijQjs{a4Z+iYWeG zt$5N5AMVx+R~iI;l~A9E$d3ce%4xvm2W_r2NEe$slb&w_>f!(Z002ovPDHLkV1lnz B@09=m diff --git a/pelican/tests/output/custom_locale/theme/images/icons/hackernews.png b/pelican/tests/output/custom_locale/theme/images/icons/hackernews.png deleted file mode 100644 index 8e05e3ee207e114a96f3811f46f68d17d52fe80b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2771 zcmV;^3M}=BP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@QN3Mg3?1|NsB>i~#tuXaD5J_o76e z$B4WD001vZL_t(|0b{@m1Qi$r!3;wKW??Xc(Sbz(%wQEZ41g$d6oINX6oxVw1@JNO Z0sz-j0)=#JDs2D&002ovPDHLkV1kM1SuX$p diff --git a/pelican/tests/output/custom_locale/theme/images/icons/lastfm.png b/pelican/tests/output/custom_locale/theme/images/icons/lastfm.png deleted file mode 100644 index 2eedd2daa1429ba026c0b3c81f200c7df55d199a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 840 zcmV-O1GoH%P)KCJu$;;6i zN)?b2a3_#VfTsWs$6EHV5TGNlK%}w4u2N|o9mJtskW6u)FfMY|?6HLq0N~tSmCYiQ z2itn^l^G`qTae2D0xESFZh|%tXmDcRU;!-yQXzzdgu?j^sE-_`#F)I#a0AjAIORAv z?nua{5j_L2Xf^mv7?p>M&VsISuyh@C_u_&8Tdjpsq*G;#gR3rt3(wqpYJX|(pqnm- zVi_(t9gaQ}dWUeQ9S7b0nAMvJMjCC()%*h2T()P6`Q<<4KoQZ3;E)b@mqq9E^qzbJ^&kHt zb@z3sXYQl9ZY#2oD@eP!I~#uc8*V^AwupI;L7D|Jvi|(8qu0 z2LL$zBz)6?fx+w8bl#l{x;rm&{_hjrZqU60&R?r z_q^a%r%rJ@Z+x)Fd-;e{U-Iff*T}z9(5p5g3wcDXPJR3rG$+nsWZfoYCWAb*gT_Bo z7fg}|L9J&75r5f0ZeI>gIn5MAw<$RbKrq)K4wK5le| zR(k=@SlVyBWV*`avXNJ2NP$f#q_(8G+6X;MtJy>;4`+;2 znIjMBZmisPo~Hw3I)yO?atTx+i^wMNq>KkRoaf=R#1u=&Mhm*)MMoa?yQ=3?Z|U=o zU9OHh!itfmFwG$0g0M5@al}`~v399M8f%oORSV+K_KnG|j?V;kMf)2sF5LyF&y{nJ Sd8X6=0000 diff --git a/pelican/tests/output/custom_locale/theme/images/icons/linkedin.png b/pelican/tests/output/custom_locale/theme/images/icons/linkedin.png deleted file mode 100644 index 06a88016f191d1f7596e5d159eb9e3ef276b6ad4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 625 zcmV-%0*?KOP)NJD$VFG5yq%r(;8zR}7Ydty&~ue@oPv z!go{cG`vvH)n(hYdJfur_4kM7-}`N$BLW71f|-O65QELNn>4;$_2zi#L7VQdUf*VG zc0B*xR8@Sfo>nmHN}+MDnqn5J#;f9g$TZW=OS!!C<4lU-jh|=cs;0XYW~;nhJEE0r zdMWpJ#bDNMT&}cGH>t7P-yM4W=hFCXaLv`alDWtkCwY0;!z58#z z#&T71?c1;2yXPjW|3Lt-K|fa>vgM24P7nS%my|*-?u{?k1z4y`Q@qLr7+I)}xOVW2 zJ3sxy0Crrjx2{T+o3K*EU*7;Otu&GVZhd>`xi2Rl`h0wRu?9M0pr|e$T%t()-9X(G zM?fxx1SUyc#XyCAwXc5_{TIPFaU$>TzBm6pmt%Vs1;7F00000 LNkvXXu0mjfHfAMI diff --git a/pelican/tests/output/custom_locale/theme/images/icons/reddit.png b/pelican/tests/output/custom_locale/theme/images/icons/reddit.png deleted file mode 100644 index d826d3e746a18b712db7a68746c0c523af802fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmV;*0X6=KP)rn|{qWNU7Dgl~R~ zWNK}Sjg6(Fp|rEIpPrq2fPh0AJ zxy#GTfq{X3etxmY-Bn&^YHMt-t*g_~&)L`2qqxp^h?r+{fMacVpMN&`0001qNklWKP}*}Is^$| zQ}1hfWmjT2av-PYqbE!!n)oua2c5|Qn7a$5l#p|w6z7}`F{d?z0T5WQglfOLER+{E--Hu?5@wDUpV?F@=Mkg+ zN{1=uqDlzrnkbYp)J3q!I?Vn|_~M zVqi53FAJQ^xo7*4+R80OflZT^0WscTaBuO7ca6A_aA-H9M3Y@{DSLGBL)| zTym{{me}!*Z3962jmy>w<<#@)IsY9R%I3sYbB>~@mM@C5Ks~s4nX-}TW8S>D*g7F! hew!yYJnD{z;D0NLV2ni3qF(?2002ovPDHLkV1k{La&G_t diff --git a/pelican/tests/output/custom_locale/theme/images/icons/slideshare.png b/pelican/tests/output/custom_locale/theme/images/icons/slideshare.png deleted file mode 100644 index 9cbe8588e24976076e5fc3898fa97856122f6fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmV;k0ZjghP)YNn`_&)?H$|J+jM18jcAeNE5840mI#38{rmUny@9DE z#Xg@8y@9DE0*}W7yWQR+m>Mc@yWPA^!~YvlZXQ7Ti(&Z??RignZicC$0;ki-+teNz zN*Qw@h$6fTDiChi(;mQdV>IdC=QLyP(`3?fGmSD0E706p2ko0(Fn89&YPG`Qa4@$> zdhWPhszyk06L$T`#EcuEoIB7dp?k0q>OF&bXs#{9!Ot=1fBXlJ={G`9+@@d!27`fI z*^>PO`g==w@1W}FBwRnS8UK$h$DgmS5K-O+o6QEJ(Z~waYBd=2dOa)_3-qUlQ$o(J zM$*}Z*m~g-OePa_IvoOeR-jNQz{PlZ6lKDV2#PRA1rmt_T+Fw|P-fhVCCq=C0AY>_ z#9}czIyxxk)H|>~8V7Uy1sn}2#;nIlxE)_f_XF?+t*xyTxm-@(N8ncJn41xc#e6|8 dAc8(0=Pod^EtOP(ARYh!002ovPDHLkV1k4J!&(3U diff --git a/pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png b/pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png deleted file mode 100644 index 7281ec48b41095180a83d71eaf0b00c85116965b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xZ*P)K92#E{?_PJ+8Tth@W(+a>28uk~_0`h`<(yY|`h z@3?AaV0?I7E@5C8LL5^B7?_#m1O5GLKL6{LaQ3ZN%$$19RhM^ilM4(QGb91Pk}#td z%#5L?Rn9&W7zkqrdeL$z%L))wjb8cZK2wtEL}mE*O&6H3%~&#zdHx68i}xJQZ+34dIdgm^E12V4($L!Sax!Q)0op z%%0y@llS*=IU_?ugf;+>a;DdHv`xcYwwSu^B5bgwWG8rbaUIq4<$P}6+C9-V(ZSGy!i4XI zBkkxgITji#coxBn7F*@>CvnH5Im-dI=x{a@nz|+mZz(oB8CqTLuD=F=p|2qrpWMi5G S=mtXo0000YRoeQcY09>#1xxKwS5F z8mlP6JGx3wQgxp~4T-?1`Rz0oPN3$iK>QzwU%sBkR`YrWi#XVjbZs+WxcmSb0KzU< z4R|<_pXuFnCY5*7ncLn>WBm*@=rz!g=9fLZ0#9?*eV!!i{{b4*NYE=UCUZ&x4QhHf zjrr}HsjUBBPiJ}aY9eRTvkFsE%bi<+hJ_q$)(X1QD;kN@ z$%`wz`8QVB1Z}9WYXRb&Kzt8~{{vn2e{DI?z%r-*E6ZK%Ff``oJb76HYP@>p``(Y0Zs!*!BjN>0I%aNVnQ8)X9Pc^Y zIsS936mToi>>TyPEkcc+2 z`5!z^ZFZ_O#v*`YvS&^RN+0_pGFvTxh}Lp2wqGagAdVyz;6#9udAenJvhWwC3QjN} ztg*<~69%xm!G(wSxK?V6H7%K70|sCIX&%<6rLrHmSw|10dx_{@95hn1(i_a}5^hC>VT?7YQHpjC*P~ez&J%_J0000< KMNUMnLSTY<*|#qM diff --git a/pelican/tests/output/custom_locale/theme/images/icons/vimeo.png b/pelican/tests/output/custom_locale/theme/images/icons/vimeo.png deleted file mode 100644 index 4b9d72127c574237090a2f5f2f7eb7be94a2b62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349 zcmV-j0iyniP)K~#7FV*mo~S^J1YP}SCZ?(wd@Mi2p&f%E_|kV7PbYe1Nxz5O|y=nOth{(bt=TS6pPua;?J=e5) zBGkM)Pk5GtRBXBib(QPnRVW%@oC6PDyoRVi_QQr_=YS?+52c0sPeBv`1LxxFPe2CH z8{$R%ID<81>CXSCz@?@iUpz?mZ$MU^)He_47^%Sg0P#sgK~#7Fl~6T81W^oq2cX(5aX1R@6<62}xdAmT6>b%79aG~T97?pp zUBiU8?wcQZ@ytL(4yKyhs>_L_h@KZq&4RGqy&kuUuOC&N_n)+)st0x|+@BHFkRd@&PPEwb z&tB6>xX7cYComKsCfH0jmXR=>tbysq2Gd0WOmZB{y88Z3Wj9^}w-Gbm)y>{S4W9T7 zfp3d!a2x}~TiXWa<}BOO6S3ho2QJ^`nhs+IjSU!vHHb*~-y}#B5s^RLsrl%w&%5IQ O0000UqK~#7FjgmEk6+sXNzkTlh65QS0-8I48iI50)i1CK$|J3O zMSiddRs94I-RZ%BpPVQlG-*wE)^@pKur#bMkyS)ii4hvVB0PPwtX?$Lk2sgu3yxdr zMHBD`H`aw%Ysl&kiS1h=1)tbo!~Ye`fCpjdO!(yU+ZHs&^bQiZX+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@*Jzj349X=?9_$gI2kIcwdunCZtMPOnPe)w(oZZmY=P7Ks9%FQb&p8>E zTGTu^G&~wGL&GC!#VuGydpI#q;n4F`-2p*w5hggKWfvm1-Ht!-D0%;*r0sX&mtTmS z=#a1o5c3rNC06S#lEEUl4ZT(7hJXc%h^4bgLQ=J-kX6=dv2qWx(KFxw<| z8p~uAa?0C01wO*eV=5{z+2r5xRmC+O#kC#3;ww#bTsi?_p2FbJ$cF2&dW*;dEIS`r z-{$NC7APVX0b-s4bL>@`-m2iuJf@Y`cU}9I16ZJlSOkdq3%}wjQF(Q}Mc#EUA+xj< z?7W)*1-w0hpaS1pO{?AaFnQ`>1)C;df$n>lghe2z(B3_8<9lvWK~rg6XL?CXWL7;$ zD`|ly6*S-encvkr7*yZ~ue};=vMZZ!#$EfG^I9zdEKo!aRp=d<^B=Mdj&Pkt)pJqZACTW#!hM@D5IKDqq;%eMz!tbv+awqP8IHt=!XVS zsZ`n>$2lgOwFdh8dv^BsZ>()0em94EiuYK~#M+HZNki~hy!n`DO0RgV7kx83G-6kVzbw2peZ^$T0XF+WR zfC!}kbTrMykUD-`pyq(>!2GmFl&qi|8BD#Yimm6&NW;(o1*9a20zlURQu0~F;$lpk zC^lANi08=`Jt9})*0WlBM`^dLR$E*Uq-h9fXc|BQRMFBY=8w1tql8shMigC06q$^` z3o;x$Brcc5Vx|EggvrrZTv~ueFR9S5n?9&@2ra;ZlJa{y#y*#;tE-3i@BjIXTFqx< zWQM^b2eIvt(ZSC+IB^l#ZO?Jct~%A$Zme8|ylK!@D6=^GmdF`z&B`yo_%8qUJKqup zI`#T8-~s^<(cM|*r>_LNwibZ|C;c5580x>Z^zql9MEVifl5$XO00aq!?ra*2Oy6Bcd7s& z4E&Wd&NypYm|1Hm3>yT#OOnPY6%hC_U@Jle48x$&@MzjDei*iPb+`A(ty}jT{_VFp0@p^D9%vpdEdoQ&A~UVr|G*1s z*N$#`bZl0nsoeDaV0>!ow??I6E} zTD@V0|CC##a5uK^=-$j1%KE$Sep%nY|D6y3VDFw|_#0Q*b=;R(Ji7n@002ovPDHLk FV1ku*k$3ca`voa6f0k zeB{69k@N|?Km7UBZZ))YUHu=w|Mw?(E)O$?WixY_FPL7s$Pm-8fstds@^ATL%bUet yZgfGChJzsUtA%$MR;8+S88GxaEa+z7`Ew3v5re0zpUXO@geCxdur~kz diff --git a/pelican/themes/notmyidea/static/images/icons/github.png b/pelican/themes/notmyidea/static/images/icons/github.png deleted file mode 100644 index 5d9109deab2b3669c845845515f4d53ab5d6daa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 606 zcmV-k0-^nhP)3g@6&i?1|w)NXSf84|8m&o+0 zUbR{k48>ZU#v6!wqi1V1r%EE$MYAT@gfEZ`ebJgG`3x?P75EOx(Rb}pK9^g49TLfP zG|6;$SGbAH zWw$>tFUyCTTU=}VL5$UQcykD>ruLPAMktgpS2)vHd2`;>>D~O#r0q^piwN%{Eu7xl ze_3-g6EyPr+mZ( z$~zD9`4D^$F>WOyU!f<&c%QI`>dHS@;3~xO7I+5=MISM;>bsLPrcC8T?Gyg2EhYt{Z#{?9|afq=U$pRKgVkaa6A5hOW zm~E0;qoMsELHiVUdq+qHW_!=pLlCuB&nsXkrlThcr&yqB2Ez}bh}WQodCMG=62#-T(jq diff --git a/pelican/themes/notmyidea/static/images/icons/gitorious.png b/pelican/themes/notmyidea/static/images/icons/gitorious.png deleted file mode 100644 index a6705d0f462b505be4d9a5a372743d206c141a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!O@L2`D?^o)dRwqYcev@q#DMvQ z*(*AHH`kW0om8=NO8&uVRmbKn*uQ@5(H+Zf?%R0x*ww37FTJ_{>eZ{eAD{pK|9`68 zWgnn9ah@)YAr*|ZCyp{U2QWBn{C>Cgf8^^ph2sKmrZ@0Db>47eliif`GZN3kOR|4E zp1HSr>(=i-J2K=SnYPGB*}QnjI%8UJ(Sx8gTe~DWM4fr0!yB diff --git a/pelican/themes/notmyidea/static/images/icons/gittip.png b/pelican/themes/notmyidea/static/images/icons/gittip.png deleted file mode 100644 index b9f67aaa32a0acd166d7271b3dbf9113717d3f00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMfoB*E?S0J6}W0T-*4J5$~5D8`b z*u=V-n;Wh0%9W86%yoICmD^QXBJ+TuMeduj_mynB29_Kl1X*A2_&{r~^}-@kve zdYW2FGLG%r8Ru!aX~lxyzki)LushDvqC7Rcx2`zG-R$h~L;wH%fBfKXl#6k)zx}VD zKk}k|sxxDHYKs!Qtrkt~nbKapYTmR2Z<_=!t4|TTzXRRJSrX(I%%C#k)~^Ns|6P3T zwz~@CcuyC{kP61s^QK8h9R!*aGdC-5mQ6dy==6QR=MEp?G>PQz`~2?eFFMS(r+K1g zp!&qb32C!ZG_9s->U*tRla<=$@$cY?|5H+adAMETx@7!MusHjBPl?KXziQ+6?1D=V ze?2)-gt;N$>Wd8<>nsYyl^Lt{Fxgl%o_?@o{T-$+24C7lzi+*sf1s9QvY5Z$XS=Iy lUyMDUTPOc|vvcNeU4iM^B0ctPia>WVc)I$ztaD0e0syFMv7G<_ diff --git a/pelican/themes/notmyidea/static/images/icons/google-groups.png b/pelican/themes/notmyidea/static/images/icons/google-groups.png deleted file mode 100644 index bbd0a0fd41295e94081103efe82742a5c6411765..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmV;V0bBlwP)wr?kW++e@xB+qT~SFYEF=HvR&ieDr#i-_xj} zRHo4+e;!+Noelr;cc?77Y;{c*CNr6Zes7m1^J^+M9sV--D^!{k1_1#;6aW?Y?FKU> z$wUFckfr-O)R-Be!K8BMN$?ljo<0akOpD)OEKI~9+x!b$boZC|3lK-`{C{xHEuU!d zCn!ydf)v}GF)B$#1SpIDL5ep&8ms%oIbw(Zf<>Eu zV{!X$x!~%@{^9sNR&mx%AN_fQnat#ZJ3j~K-$A9Q{GPt1rQRSFm;ow5sT2?@^QR4r zg$anDgeCx$iqb6h2iG6)ockYoL!0v;mgd!$p2~_@_rOO_1Na`l%Wwj6hQdz( O0000oJ)blrcs(C@6ijHhGjL zrFRi_0z}#gh+z_?jx?ulJ6ZEDq_MICdrx(I>4q_hBh(7t5P3nx z6N?eUwA{x^#MIz^w4#;vg#Q`{-|sTaZm+z|gUicmu%q_q_{S4K=37O~-YZFy$l zCeshS!0W4y4-;Rs{O>veK>w%APq})#_~VzlN~kv#Z!>>U^FI6esN%Y~QuIs1d#~iY zS5C<<$DO!3cexVMKA`VP&KrXDnvg`iD8?-qH)mw8u{$e0Ls5IrFnpzz@8B4^@ijQjs{a4Z+iYWeG zt$5N5AMVx+R~iI;l~A9E$d3ce%4xvm2W_r2NEe$slb&w_>f!(Z002ovPDHLkV1lnz B@09=m diff --git a/pelican/themes/notmyidea/static/images/icons/hackernews.png b/pelican/themes/notmyidea/static/images/icons/hackernews.png deleted file mode 100644 index 8e05e3ee207e114a96f3811f46f68d17d52fe80b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2771 zcmV;^3M}=BP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@QN3Mg3?1|NsB>i~#tuXaD5J_o76e z$B4WD001vZL_t(|0b{@m1Qi$r!3;wKW??Xc(Sbz(%wQEZ41g$d6oINX6oxVw1@JNO Z0sz-j0)=#JDs2D&002ovPDHLkV1kM1SuX$p diff --git a/pelican/themes/notmyidea/static/images/icons/lastfm.png b/pelican/themes/notmyidea/static/images/icons/lastfm.png deleted file mode 100644 index 2eedd2daa1429ba026c0b3c81f200c7df55d199a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 840 zcmV-O1GoH%P)KCJu$;;6i zN)?b2a3_#VfTsWs$6EHV5TGNlK%}w4u2N|o9mJtskW6u)FfMY|?6HLq0N~tSmCYiQ z2itn^l^G`qTae2D0xESFZh|%tXmDcRU;!-yQXzzdgu?j^sE-_`#F)I#a0AjAIORAv z?nua{5j_L2Xf^mv7?p>M&VsISuyh@C_u_&8Tdjpsq*G;#gR3rt3(wqpYJX|(pqnm- zVi_(t9gaQ}dWUeQ9S7b0nAMvJMjCC()%*h2T()P6`Q<<4KoQZ3;E)b@mqq9E^qzbJ^&kHt zb@z3sXYQl9ZY#2oD@eP!I~#uc8*V^AwupI;L7D|Jvi|(8qu0 z2LL$zBz)6?fx+w8bl#l{x;rm&{_hjrZqU60&R?r z_q^a%r%rJ@Z+x)Fd-;e{U-Iff*T}z9(5p5g3wcDXPJR3rG$+nsWZfoYCWAb*gT_Bo z7fg}|L9J&75r5f0ZeI>gIn5MAw<$RbKrq)K4wK5le| zR(k=@SlVyBWV*`avXNJ2NP$f#q_(8G+6X;MtJy>;4`+;2 znIjMBZmisPo~Hw3I)yO?atTx+i^wMNq>KkRoaf=R#1u=&Mhm*)MMoa?yQ=3?Z|U=o zU9OHh!itfmFwG$0g0M5@al}`~v399M8f%oORSV+K_KnG|j?V;kMf)2sF5LyF&y{nJ Sd8X6=0000 diff --git a/pelican/themes/notmyidea/static/images/icons/linkedin.png b/pelican/themes/notmyidea/static/images/icons/linkedin.png deleted file mode 100644 index 06a88016f191d1f7596e5d159eb9e3ef276b6ad4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 625 zcmV-%0*?KOP)NJD$VFG5yq%r(;8zR}7Ydty&~ue@oPv z!go{cG`vvH)n(hYdJfur_4kM7-}`N$BLW71f|-O65QELNn>4;$_2zi#L7VQdUf*VG zc0B*xR8@Sfo>nmHN}+MDnqn5J#;f9g$TZW=OS!!C<4lU-jh|=cs;0XYW~;nhJEE0r zdMWpJ#bDNMT&}cGH>t7P-yM4W=hFCXaLv`alDWtkCwY0;!z58#z z#&T71?c1;2yXPjW|3Lt-K|fa>vgM24P7nS%my|*-?u{?k1z4y`Q@qLr7+I)}xOVW2 zJ3sxy0Crrjx2{T+o3K*EU*7;Otu&GVZhd>`xi2Rl`h0wRu?9M0pr|e$T%t()-9X(G zM?fxx1SUyc#XyCAwXc5_{TIPFaU$>TzBm6pmt%Vs1;7F00000 LNkvXXu0mjfHfAMI diff --git a/pelican/themes/notmyidea/static/images/icons/reddit.png b/pelican/themes/notmyidea/static/images/icons/reddit.png deleted file mode 100644 index d826d3e746a18b712db7a68746c0c523af802fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmV;*0X6=KP)rn|{qWNU7Dgl~R~ zWNK}Sjg6(Fp|rEIpPrq2fPh0AJ zxy#GTfq{X3etxmY-Bn&^YHMt-t*g_~&)L`2qqxp^h?r+{fMacVpMN&`0001qNklWKP}*}Is^$| zQ}1hfWmjT2av-PYqbE!!n)oua2c5|Qn7a$5l#p|w6z7}`F{d?z0T5WQglfOLER+{E--Hu?5@wDUpV?F@=Mkg+ zN{1=uqDlzrnkbYp)J3q!I?Vn|_~M zVqi53FAJQ^xo7*4+R80OflZT^0WscTaBuO7ca6A_aA-H9M3Y@{DSLGBL)| zTym{{me}!*Z3962jmy>w<<#@)IsY9R%I3sYbB>~@mM@C5Ks~s4nX-}TW8S>D*g7F! hew!yYJnD{z;D0NLV2ni3qF(?2002ovPDHLkV1k{La&G_t diff --git a/pelican/themes/notmyidea/static/images/icons/slideshare.png b/pelican/themes/notmyidea/static/images/icons/slideshare.png deleted file mode 100644 index 9cbe8588e24976076e5fc3898fa97856122f6fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmV;k0ZjghP)YNn`_&)?H$|J+jM18jcAeNE5840mI#38{rmUny@9DE z#Xg@8y@9DE0*}W7yWQR+m>Mc@yWPA^!~YvlZXQ7Ti(&Z??RignZicC$0;ki-+teNz zN*Qw@h$6fTDiChi(;mQdV>IdC=QLyP(`3?fGmSD0E706p2ko0(Fn89&YPG`Qa4@$> zdhWPhszyk06L$T`#EcuEoIB7dp?k0q>OF&bXs#{9!Ot=1fBXlJ={G`9+@@d!27`fI z*^>PO`g==w@1W}FBwRnS8UK$h$DgmS5K-O+o6QEJ(Z~waYBd=2dOa)_3-qUlQ$o(J zM$*}Z*m~g-OePa_IvoOeR-jNQz{PlZ6lKDV2#PRA1rmt_T+Fw|P-fhVCCq=C0AY>_ z#9}czIyxxk)H|>~8V7Uy1sn}2#;nIlxE)_f_XF?+t*xyTxm-@(N8ncJn41xc#e6|8 dAc8(0=Pod^EtOP(ARYh!002ovPDHLkV1k4J!&(3U diff --git a/pelican/themes/notmyidea/static/images/icons/speakerdeck.png b/pelican/themes/notmyidea/static/images/icons/speakerdeck.png deleted file mode 100644 index 7281ec48b41095180a83d71eaf0b00c85116965b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xZ*P)K92#E{?_PJ+8Tth@W(+a>28uk~_0`h`<(yY|`h z@3?AaV0?I7E@5C8LL5^B7?_#m1O5GLKL6{LaQ3ZN%$$19RhM^ilM4(QGb91Pk}#td z%#5L?Rn9&W7zkqrdeL$z%L))wjb8cZK2wtEL}mE*O&6H3%~&#zdHx68i}xJQZ+34dIdgm^E12V4($L!Sax!Q)0op z%%0y@llS*=IU_?ugf;+>a;DdHv`xcYwwSu^B5bgwWG8rbaUIq4<$P}6+C9-V(ZSGy!i4XI zBkkxgITji#coxBn7F*@>CvnH5Im-dI=x{a@nz|+mZz(oB8CqTLuD=F=p|2qrpWMi5G S=mtXo0000YRoeQcY09>#1xxKwS5F z8mlP6JGx3wQgxp~4T-?1`Rz0oPN3$iK>QzwU%sBkR`YrWi#XVjbZs+WxcmSb0KzU< z4R|<_pXuFnCY5*7ncLn>WBm*@=rz!g=9fLZ0#9?*eV!!i{{b4*NYE=UCUZ&x4QhHf zjrr}HsjUBBPiJ}aY9eRTvkFsE%bi<+hJ_q$)(X1QD;kN@ z$%`wz`8QVB1Z}9WYXRb&Kzt8~{{vn2e{DI?z%r-*E6ZK%Ff``oJb76HYP@>p``(Y0Zs!*!BjN>0I%aNVnQ8)X9Pc^Y zIsS936mToi>>TyPEkcc+2 z`5!z^ZFZ_O#v*`YvS&^RN+0_pGFvTxh}Lp2wqGagAdVyz;6#9udAenJvhWwC3QjN} ztg*<~69%xm!G(wSxK?V6H7%K70|sCIX&%<6rLrHmSw|10dx_{@95hn1(i_a}5^hC>VT?7YQHpjC*P~ez&J%_J0000< KMNUMnLSTY<*|#qM diff --git a/pelican/themes/notmyidea/static/images/icons/vimeo.png b/pelican/themes/notmyidea/static/images/icons/vimeo.png deleted file mode 100644 index 4b9d72127c574237090a2f5f2f7eb7be94a2b62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349 zcmV-j0iyniP)K~#7FV*mo~S^J1YP}SCZ?(wd@Mi2p&f%E_|kV7PbYe1Nxz5O|y=nOth{(bt=TS6pPua;?J=e5) zBGkM)Pk5GtRBXBib(QPnRVW%@oC6PDyoRVi_QQr_=YS?+52c0sPeBv`1LxxFPe2CH z8{$R%ID<81>CXSCz@?@iUpz?mZ$MU^)He_47^%Sg0P#sgK~#7Fl~6T81W^oq2cX(5aX1R@6<62}xdAmT6>b%79aG~T97?pp zUBiU8?wcQZ@ytL(4yKyhs>_L_h@KZq&4RGqy&kuUuOC&N_n)+)st0x|+@BHFkRd@&PPEwb z&tB6>xX7cYComKsCfH0jmXR=>tbysq2Gd0WOmZB{y88Z3Wj9^}w-Gbm)y>{S4W9T7 zfp3d!a2x}~TiXWa<}BOO6S3ho2QJ^`nhs+IjSU!vHHb*~-y}#B5s^RLsrl%w&%5IQ O0000 Date: Sat, 4 Nov 2023 01:00:51 +0300 Subject: [PATCH 72/88] add notmyidea font license --- .../theme/fonts/Yanone_Kaffeesatz_LICENSE.txt | 93 +++++++++++++++++++ .../theme/fonts/Yanone_Kaffeesatz_LICENSE.txt | 93 +++++++++++++++++++ .../theme/fonts/Yanone_Kaffeesatz_LICENSE.txt | 93 +++++++++++++++++++ .../fonts/Yanone_Kaffeesatz_LICENSE.txt | 93 +++++++++++++++++++ 4 files changed, 372 insertions(+) create mode 100644 pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt create mode 100644 pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt create mode 100644 pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt create mode 100644 pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_LICENSE.txt diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt new file mode 100644 index 00000000..309fd710 --- /dev/null +++ b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt new file mode 100644 index 00000000..309fd710 --- /dev/null +++ b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt new file mode 100644 index 00000000..309fd710 --- /dev/null +++ b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_LICENSE.txt b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_LICENSE.txt new file mode 100644 index 00000000..c70bcad3 --- /dev/null +++ b/pelican/themes/notmyidea/static/fonts/Yanone_Kaffeesatz_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. From 6059675d55d0abbb2bcba73f365d88473fb8029f Mon Sep 17 00:00:00 2001 From: Paolo Melchiorre Date: Sat, 11 Nov 2023 14:10:08 +0100 Subject: [PATCH 73/88] Fix #3233 -- Simple theme classless semantic HTML --- .gitignore | 1 + docs/settings.rst | 6 ++++ .../custom/author/alexis-metaireau.html | 12 ++++--- .../custom/author/alexis-metaireau2.html | 16 ++++++---- .../custom/author/alexis-metaireau3.html | 12 ++++--- pelican/tests/output/custom/index.html | 12 ++++--- pelican/tests/output/custom/index2.html | 16 ++++++---- pelican/tests/output/custom/index3.html | 12 ++++--- .../author/alexis-metaireau.html | 12 ++++--- .../author/alexis-metaireau2.html | 16 ++++++---- .../author/alexis-metaireau3.html | 12 ++++--- pelican/tests/output/custom_locale/index.html | 12 ++++--- .../tests/output/custom_locale/index2.html | 16 ++++++---- .../tests/output/custom_locale/index3.html | 12 ++++--- pelican/themes/simple/templates/archives.html | 2 +- pelican/themes/simple/templates/article.html | 32 +++++++++---------- pelican/themes/simple/templates/author.html | 2 +- pelican/themes/simple/templates/authors.html | 2 +- pelican/themes/simple/templates/base.html | 18 +++++++---- .../themes/simple/templates/categories.html | 2 +- pelican/themes/simple/templates/category.html | 2 +- pelican/themes/simple/templates/index.html | 25 +++++++-------- pelican/themes/simple/templates/page.html | 8 ++++- .../themes/simple/templates/pagination.html | 16 ++++++---- .../simple/templates/period_archives.html | 2 +- pelican/themes/simple/templates/tag.html | 2 +- pelican/themes/simple/templates/tags.html | 2 +- 27 files changed, 163 insertions(+), 119 deletions(-) diff --git a/.gitignore b/.gitignore index b27f3eb9..473efea2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ samples/output *.pem *.lock .pdm-python +.venv diff --git a/docs/settings.rst b/docs/settings.rst index a7768514..88a32d23 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -1231,6 +1231,12 @@ Following are example ways to specify your preferred theme:: # Specify a customized theme, via absolute path THEME = "/home/myuser/projects/mysite/themes/mycustomtheme" +The built-in ``simple`` theme can be customized using the following settings. + +.. data:: STYLESHEET_URL + + The URL of the stylesheet to use. + The built-in ``notmyidea`` theme can make good use of the following settings. Feel free to use them in your themes as well. diff --git a/pelican/tests/output/custom/author/alexis-metaireau.html b/pelican/tests/output/custom/author/alexis-metaireau.html index a9c73e6b..aef8c6e6 100644 --- a/pelican/tests/output/custom/author/alexis-metaireau.html +++ b/pelican/tests/output/custom/author/alexis-metaireau.html @@ -120,11 +120,13 @@

There are comments.

-

- Page 1 / 3 - » - -

+
diff --git a/pelican/tests/output/custom/author/alexis-metaireau2.html b/pelican/tests/output/custom/author/alexis-metaireau2.html index 41f00605..8d17eed5 100644 --- a/pelican/tests/output/custom/author/alexis-metaireau2.html +++ b/pelican/tests/output/custom/author/alexis-metaireau2.html @@ -133,13 +133,15 @@ YEAH !

There are comments.

-

- - « - Page 2 / 3 - » - -

+
diff --git a/pelican/tests/output/custom/author/alexis-metaireau3.html b/pelican/tests/output/custom/author/alexis-metaireau3.html index 45a5f0d1..48fe75ba 100644 --- a/pelican/tests/output/custom/author/alexis-metaireau3.html +++ b/pelican/tests/output/custom/author/alexis-metaireau3.html @@ -85,11 +85,13 @@ pelican.conf, it will …

There are comments.

-

- - « - Page 3 / 3 -

+
diff --git a/pelican/tests/output/custom/index.html b/pelican/tests/output/custom/index.html index ceb6e4f5..6c4d7a94 100644 --- a/pelican/tests/output/custom/index.html +++ b/pelican/tests/output/custom/index.html @@ -120,11 +120,13 @@

There are comments.

-

- Page 1 / 3 - » - -

+
diff --git a/pelican/tests/output/custom/index2.html b/pelican/tests/output/custom/index2.html index ddd4e96e..043f8c33 100644 --- a/pelican/tests/output/custom/index2.html +++ b/pelican/tests/output/custom/index2.html @@ -133,13 +133,15 @@ YEAH !

There are comments.

-

- - « - Page 2 / 3 - » - -

+
diff --git a/pelican/tests/output/custom/index3.html b/pelican/tests/output/custom/index3.html index 698139a0..f8ebaac0 100644 --- a/pelican/tests/output/custom/index3.html +++ b/pelican/tests/output/custom/index3.html @@ -85,11 +85,13 @@ pelican.conf, it will …

There are comments.

-

- - « - Page 3 / 3 -

+
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau.html b/pelican/tests/output/custom_locale/author/alexis-metaireau.html index 41a21a98..df76ccf6 100644 --- a/pelican/tests/output/custom_locale/author/alexis-metaireau.html +++ b/pelican/tests/output/custom_locale/author/alexis-metaireau.html @@ -120,11 +120,13 @@

There are comments.

-

- Page 1 / 3 - » - -

+
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau2.html b/pelican/tests/output/custom_locale/author/alexis-metaireau2.html index 6412784f..42a929d0 100644 --- a/pelican/tests/output/custom_locale/author/alexis-metaireau2.html +++ b/pelican/tests/output/custom_locale/author/alexis-metaireau2.html @@ -133,13 +133,15 @@ YEAH !

There are comments.

-

- - « - Page 2 / 3 - » - -

+
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau3.html b/pelican/tests/output/custom_locale/author/alexis-metaireau3.html index 2679b0a6..941cdc46 100644 --- a/pelican/tests/output/custom_locale/author/alexis-metaireau3.html +++ b/pelican/tests/output/custom_locale/author/alexis-metaireau3.html @@ -85,11 +85,13 @@ pelican.conf, it will …

There are comments.

-

- - « - Page 3 / 3 -

+
diff --git a/pelican/tests/output/custom_locale/index.html b/pelican/tests/output/custom_locale/index.html index 4a661093..054011cc 100644 --- a/pelican/tests/output/custom_locale/index.html +++ b/pelican/tests/output/custom_locale/index.html @@ -120,11 +120,13 @@

There are comments.

-

- Page 1 / 3 - » - -

+
diff --git a/pelican/tests/output/custom_locale/index2.html b/pelican/tests/output/custom_locale/index2.html index 60fee085..fa2c4d2d 100644 --- a/pelican/tests/output/custom_locale/index2.html +++ b/pelican/tests/output/custom_locale/index2.html @@ -133,13 +133,15 @@ YEAH !

There are comments.

-

- - « - Page 2 / 3 - » - -

+
diff --git a/pelican/tests/output/custom_locale/index3.html b/pelican/tests/output/custom_locale/index3.html index 76597e82..71ccb4b1 100644 --- a/pelican/tests/output/custom_locale/index3.html +++ b/pelican/tests/output/custom_locale/index3.html @@ -85,11 +85,13 @@ pelican.conf, it will …

There are comments.

-

- - « - Page 3 / 3 -

+
diff --git a/pelican/themes/simple/templates/archives.html b/pelican/themes/simple/templates/archives.html index b7754c45..c7fb2127 100644 --- a/pelican/themes/simple/templates/archives.html +++ b/pelican/themes/simple/templates/archives.html @@ -3,7 +3,7 @@ {% block title %}{{ SITENAME|striptags }} - Archives{% endblock %} {% block content %} -

Archives for {{ SITENAME }}

+

Archives for {{ SITENAME }}

{% for article in dates %} diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index a17f2759..07e5534d 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -22,44 +22,44 @@ {% endblock %} {% block content %} +
-

+

{{ article.title }}

+ title="Permalink to {{ article.title|striptags }}">{{ article.title }} {% import 'translations.html' as translations with context %} {{ translations.translations_for(article) }}
-
-
{% endblock %} diff --git a/pelican/themes/simple/templates/author.html b/pelican/themes/simple/templates/author.html index c054f8ab..9b30dfe2 100644 --- a/pelican/themes/simple/templates/author.html +++ b/pelican/themes/simple/templates/author.html @@ -3,5 +3,5 @@ {% block title %}{{ SITENAME|striptags }} - Articles by {{ author }}{% endblock %} {% block content_title %} -

Articles by {{ author }}

+

Articles by {{ author }}

{% endblock %} diff --git a/pelican/themes/simple/templates/authors.html b/pelican/themes/simple/templates/authors.html index 9b80b499..01b4f6f1 100644 --- a/pelican/themes/simple/templates/authors.html +++ b/pelican/themes/simple/templates/authors.html @@ -3,7 +3,7 @@ {% block title %}{{ SITENAME|striptags }} - Authors{% endblock %} {% block content %} -

Authors on {{ SITENAME }}

+

Authors on {{ SITENAME }}

    {% for author, articles in authors|sort %}
  • {{ author }} ({{ articles|count }})
  • diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index 94a16930..e006cba1 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -6,6 +6,12 @@ + {% if SITESUBTITLE %} + + {% endif %} + {% if STYLESHEET_URL %} + + {% endif %} {% if FEED_ALL_ATOM %} {% endif %} @@ -35,32 +41,32 @@
    -

    {{ SITENAME }}{% if SITESUBTITLE %} {{ SITESUBTITLE }}{% endif %}

    -
    +

    {{ SITENAME }}

    {% if SITESUBTITLE %}

    {{ SITESUBTITLE }}

    {% endif %}
    +
    {% block content %} {% endblock %}
    -
    +
    Proudly powered by Pelican, which takes great advantage of Python. -
    +
    diff --git a/pelican/themes/simple/templates/categories.html b/pelican/themes/simple/templates/categories.html index f099e88f..7da19fb4 100644 --- a/pelican/themes/simple/templates/categories.html +++ b/pelican/themes/simple/templates/categories.html @@ -3,7 +3,7 @@ {% block title %}{{ SITENAME|striptags }} - Categories{% endblock %} {% block content %} -

    Categories on {{ SITENAME }}

    +

    Categories on {{ SITENAME }}

      {% for category, articles in categories|sort %}
    • {{ category }} ({{ articles|count }})
    • diff --git a/pelican/themes/simple/templates/category.html b/pelican/themes/simple/templates/category.html index da1a8b52..16525fb2 100644 --- a/pelican/themes/simple/templates/category.html +++ b/pelican/themes/simple/templates/category.html @@ -3,5 +3,5 @@ {% block title %}{{ SITENAME|striptags }} - {{ category }} category{% endblock %} {% block content_title %} -

      Articles in the {{ category }} category

      +

      Articles in the {{ category }} category

      {% endblock %} diff --git a/pelican/themes/simple/templates/index.html b/pelican/themes/simple/templates/index.html index ab4bc345..c9837b54 100644 --- a/pelican/themes/simple/templates/index.html +++ b/pelican/themes/simple/templates/index.html @@ -1,28 +1,27 @@ {% extends "base.html" %} {% block content %} -
      {% block content_title %}

      All articles

      {% endblock %} -
        + {% for article in articles_page.object_list %} -
      1. + + {% endfor %} -
      + {% if articles_page.has_other_pages() %} {% include 'pagination.html' %} {% endif %} -
      + {% endblock content %} diff --git a/pelican/themes/simple/templates/page.html b/pelican/themes/simple/templates/page.html index eea816a9..38452d1d 100644 --- a/pelican/themes/simple/templates/page.html +++ b/pelican/themes/simple/templates/page.html @@ -13,15 +13,21 @@ {% endblock %} {% block content %} -

      {{ page.title }}

      +
      +
      +

      {{ page.title }}

      +
      {% import 'translations.html' as translations with context %} {{ translations.translations_for(page) }} {{ page.content }} {% if page.modified %} +

      Last updated: {{ page.locale_modified }}

      +
      {% endif %} +
      {% endblock %} diff --git a/pelican/themes/simple/templates/pagination.html b/pelican/themes/simple/templates/pagination.html index 588f130c..45e7f167 100644 --- a/pelican/themes/simple/templates/pagination.html +++ b/pelican/themes/simple/templates/pagination.html @@ -1,15 +1,17 @@ {% if DEFAULT_PAGINATION %} {% set first_page = articles_paginator.page(1) %} {% set last_page = articles_paginator.page(articles_paginator.num_pages) %} -

      +

      {% endif %} diff --git a/pelican/themes/simple/templates/period_archives.html b/pelican/themes/simple/templates/period_archives.html index 9cdc354d..595def50 100644 --- a/pelican/themes/simple/templates/period_archives.html +++ b/pelican/themes/simple/templates/period_archives.html @@ -3,7 +3,7 @@ {% block title %}{{ SITENAME|striptags }} - {{ period | reverse | join(' ') }} archives{% endblock %} {% block content %} -

      Archives for {{ period | reverse | join(' ') }}

      +

      Archives for {{ period | reverse | join(' ') }}

      {% for article in dates %} diff --git a/pelican/themes/simple/templates/tag.html b/pelican/themes/simple/templates/tag.html index 59725a05..f9b71f48 100644 --- a/pelican/themes/simple/templates/tag.html +++ b/pelican/themes/simple/templates/tag.html @@ -3,5 +3,5 @@ {% block title %}{{ SITENAME|striptags }} - {{ tag }} tag{% endblock %} {% block content_title %} -

      Articles tagged with {{ tag }}

      +

      Articles tagged with {{ tag }}

      {% endblock %} diff --git a/pelican/themes/simple/templates/tags.html b/pelican/themes/simple/templates/tags.html index 92c142d2..6b5c6b1c 100644 --- a/pelican/themes/simple/templates/tags.html +++ b/pelican/themes/simple/templates/tags.html @@ -3,7 +3,7 @@ {% block title %}{{ SITENAME|striptags }} - Tags{% endblock %} {% block content %} -

      Tags for {{ SITENAME }}

      +

      Tags for {{ SITENAME }}

        {% for tag, articles in tags|sort %}
      • {{ tag }} ({{ articles|count }})
      • From 39ff56a08260f057538cbb2e39e147bffacdf357 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 13:38:30 +0100 Subject: [PATCH 74/88] Update development dependencies --- .pre-commit-config.yaml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a73aebc..333bc3c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: forbid-new-submodules - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.5 hooks: - id: ruff - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 816a25f3..03ba81b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "BeautifulSoup4>=4.12.2", "jinja2>=3.1.2", "lxml>=4.9.3", - "markdown>=3.5", + "markdown>=3.5.1", "typogrify>=2.0.7", "sphinx>=7.1.2", "sphinxext-opengraph>=0.9.0", @@ -91,10 +91,10 @@ dev = [ "pytest>=7.4.3", "pytest-cov>=4.1.0", "pytest-sugar>=0.9.7", - "pytest-xdist>=3.3.1", + "pytest-xdist>=3.4.0", "tox>=4.11.3", "invoke>=2.2.0", - "ruff>=0.1.3", + "ruff>=0.1.5", "tomli>=2.0.1; python_version < \"3.11\"", ] From 903ce3ce33d614dfaa39a55db96b7ec44fc7df43 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 13:41:38 +0100 Subject: [PATCH 75/88] Pin Furo doc theme version We override its page.html template with our own, so it is better to manually and explicity upgrade rather than have a future version of the Furo theme potentially break the documentation build. --- pyproject.toml | 2 +- requirements/docs.pip | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03ba81b4..d40e6bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ dev = [ "typogrify>=2.0.7", "sphinx>=7.1.2", "sphinxext-opengraph>=0.9.0", - "furo>=2023.9.10", + "furo==2023.9.10", "livereload>=2.6.3", "psutil>=5.9.6", "pygments>=2.16.1", diff --git a/requirements/docs.pip b/requirements/docs.pip index 961a6473..7b0f37cc 100644 --- a/requirements/docs.pip +++ b/requirements/docs.pip @@ -1,5 +1,5 @@ -sphinx<6.0 +sphinx sphinxext-opengraph -furo +furo==2023.9.10 livereload tomli;python_version<"3.11" From ecd598f293161a52564aa6e8dfdcc8284dc93970 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 13:53:02 +0100 Subject: [PATCH 76/88] Update code base for Python 3.8 and above Result of: pipx run pyupgrade --py38-plus pelican/**/*.py --- pelican/__init__.py | 6 ++---- pelican/contents.py | 2 +- pelican/paginator.py | 2 +- pelican/plugins/_utils.py | 8 +++---- pelican/readers.py | 16 +++++++------- pelican/settings.py | 6 +++--- pelican/tests/build_test/test_build_files.py | 2 +- pelican/tests/support.py | 2 +- pelican/tests/test_readers.py | 4 +--- pelican/tests/test_settings.py | 2 +- pelican/tools/pelican_import.py | 8 +++---- pelican/tools/pelican_quickstart.py | 22 ++++++++++---------- pelican/tools/pelican_themes.py | 12 +++++------ pelican/urlwrappers.py | 8 +++---- pelican/utils.py | 22 +++++++++----------- 15 files changed, 58 insertions(+), 64 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index a0ff4989..25c493b9 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -273,7 +273,7 @@ class PrintSettings(argparse.Action): ) ) else: - console.print("\n{} is not a recognized setting.".format(setting)) + console.print(f"\n{setting} is not a recognized setting.") break else: # No argument was given to --print-settings, so print all settings @@ -611,9 +611,7 @@ def listen(server, port, output, excqueue=None): return try: - console.print( - "Serving site at: http://{}:{} - Tap CTRL-C to stop".format(server, port) - ) + console.print(f"Serving site at: http://{server}:{port} - Tap CTRL-C to stop") httpd.serve_forever() except Exception as e: if excqueue is not None: diff --git a/pelican/contents.py b/pelican/contents.py index f99e6426..474e5bbf 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -235,7 +235,7 @@ class Content: def _expand_settings(self, key, klass=None): if not klass: klass = self.__class__.__name__ - fq_key = ("{}_{}".format(klass, key)).upper() + fq_key = (f"{klass}_{key}").upper() return str(self.settings[fq_key]).format(**self.url_format) def get_url_setting(self, key): diff --git a/pelican/paginator.py b/pelican/paginator.py index 930c915b..e1d50881 100644 --- a/pelican/paginator.py +++ b/pelican/paginator.py @@ -81,7 +81,7 @@ class Page: self.settings = settings def __repr__(self): - return "".format(self.number, self.paginator.num_pages) + return f"" def has_next(self): return self.number < self.paginator.num_pages diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py index c25f8114..805ed049 100644 --- a/pelican/plugins/_utils.py +++ b/pelican/plugins/_utils.py @@ -49,7 +49,7 @@ def plugin_enabled(name, plugin_list=None): # search name as is return True - if "pelican.plugins.{}".format(name) in plugin_list: + if f"pelican.plugins.{name}" in plugin_list: # check if short name is a namespace plugin return True @@ -68,7 +68,7 @@ def load_legacy_plugin(plugin, plugin_paths): # If failed, try to find it in normal importable locations spec = importlib.util.find_spec(plugin) if spec is None: - raise ImportError("Cannot import plugin `{}`".format(plugin)) + raise ImportError(f"Cannot import plugin `{plugin}`") else: # Avoid loading the same plugin twice if spec.name in sys.modules: @@ -106,8 +106,8 @@ def load_plugins(settings): # try to find in namespace plugins if plugin in namespace_plugins: plugin = namespace_plugins[plugin] - elif "pelican.plugins.{}".format(plugin) in namespace_plugins: - plugin = namespace_plugins["pelican.plugins.{}".format(plugin)] + elif f"pelican.plugins.{plugin}" in namespace_plugins: + plugin = namespace_plugins[f"pelican.plugins.{plugin}"] # try to import it else: try: diff --git a/pelican/readers.py b/pelican/readers.py index 5033c0bd..60b9765a 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -401,7 +401,7 @@ class HTMLReader(BaseReader): self._in_body = False self._in_top_level = True elif self._in_body: - self._data_buffer += "".format(escape(tag)) + self._data_buffer += f"" def handle_startendtag(self, tag, attrs): if tag == "meta" and self._in_head: @@ -410,28 +410,28 @@ class HTMLReader(BaseReader): self._data_buffer += self.build_tag(tag, attrs, True) def handle_comment(self, data): - self._data_buffer += "".format(data) + self._data_buffer += f"" def handle_data(self, data): self._data_buffer += data def handle_entityref(self, data): - self._data_buffer += "&{};".format(data) + self._data_buffer += f"&{data};" def handle_charref(self, data): - self._data_buffer += "&#{};".format(data) + self._data_buffer += f"&#{data};" def build_tag(self, tag, attrs, close_tag): - result = "<{}".format(escape(tag)) + result = f"<{escape(tag)}" for k, v in attrs: result += " " + escape(k) if v is not None: # If the attribute value contains a double quote, surround # with single quotes, otherwise use double quotes. if '"' in v: - result += "='{}'".format(escape(v, quote=False)) + result += f"='{escape(v, quote=False)}'" else: - result += '="{}"'.format(escape(v, quote=False)) + result += f'="{escape(v, quote=False)}"' if close_tag: return result + " />" return result + ">" @@ -439,7 +439,7 @@ class HTMLReader(BaseReader): def _handle_meta_tag(self, attrs): name = self._attr_value(attrs, "name") if name is None: - attr_list = ['{}="{}"'.format(k, v) for k, v in attrs] + attr_list = [f'{k}="{v}"' for k, v in attrs] attr_serialized = ", ".join(attr_list) logger.warning( "Meta tag in file %s does not have a 'name' " diff --git a/pelican/settings.py b/pelican/settings.py index 2c84b6f0..4a4f2901 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -265,7 +265,7 @@ def _printf_s_to_format_field(printf_string, format_field): format_field ) if result.format(**{format_field: TEST_STRING}) != expected: - raise ValueError("Failed to safely replace %s with {{{}}}".format(format_field)) + raise ValueError(f"Failed to safely replace %s with {{{format_field}}}") return result @@ -350,9 +350,9 @@ def handle_deprecated_settings(settings): ), ]: if old in settings: - message = "The {} setting has been removed in favor of {}".format(old, new) + message = f"The {old} setting has been removed in favor of {new}" if doc: - message += ", see {} for details".format(doc) + message += f", see {doc} for details" logger.warning(message) # PAGINATED_DIRECT_TEMPLATES -> PAGINATED_TEMPLATES diff --git a/pelican/tests/build_test/test_build_files.py b/pelican/tests/build_test/test_build_files.py index 2b51d362..9aad990d 100644 --- a/pelican/tests/build_test/test_build_files.py +++ b/pelican/tests/build_test/test_build_files.py @@ -61,6 +61,6 @@ def test_sdist_contents(pytestconfig, expected_file): filtered_values = [ path for path in files_list - if match(f"^pelican-\d\.\d\.\d/{expected_file}{dir_matcher}$", path) + if match(rf"^pelican-\d\.\d\.\d/{expected_file}{dir_matcher}$", path) ] assert len(filtered_values) > 0 diff --git a/pelican/tests/support.py b/pelican/tests/support.py index e3813849..16060441 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -133,7 +133,7 @@ def skipIfNoExecutable(executable): res = None if res is None: - return unittest.skip("{} executable not found".format(executable)) + return unittest.skip(f"{executable} executable not found") return lambda func: func diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py index cf0f39f1..04049894 100644 --- a/pelican/tests/test_readers.py +++ b/pelican/tests/test_readers.py @@ -32,9 +32,7 @@ class ReaderTest(unittest.TestCase): % (key, value, real_value), ) else: - self.fail( - "Expected %s to have value %s, but was not in Dict" % (key, value) - ) + self.fail(f"Expected {key} to have value {value}, but was not in Dict") class TestAssertDictHasSubset(ReaderTest): diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 0e77674d..f370f7eb 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -170,7 +170,7 @@ class TestSettingsConfiguration(unittest.TestCase): def test__printf_s_to_format_field(self): for s in ("%s", "{%s}", "{%s"): - option = "foo/{}/bar.baz".format(s) + option = f"foo/{s}/bar.baz" result = _printf_s_to_format_field(option, "slug") expected = option % "qux" found = result.format(slug="qux") diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index 27102f38..681a5c45 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -41,7 +41,7 @@ def decode_wp_content(content, br=True): if start == -1: content = content + pre_part continue - name = "
        ".format(pre_index)
        +            name = f"
        "
                     pre_tags[name] = pre_part[start:] + ""
                     content = content + pre_part[0:start] + name
                     pre_index += 1
        @@ -765,7 +765,7 @@ def download_attachments(output_path, urls):
         
                 if not os.path.exists(full_path):
                     os.makedirs(full_path)
        -        print("downloading {}".format(filename))
        +        print(f"downloading {filename}")
                 try:
                     urlretrieve(url, os.path.join(full_path, filename))
                     locations[url] = os.path.join(localpath, filename)
        @@ -782,7 +782,7 @@ def is_pandoc_needed(in_markup):
         def get_pandoc_version():
             cmd = ["pandoc", "--version"]
             try:
        -        output = subprocess.check_output(cmd, universal_newlines=True)
        +        output = subprocess.check_output(cmd, text=True)
             except (subprocess.CalledProcessError, OSError) as e:
                 logger.warning("Pandoc version unknown: %s", e)
                 return ()
        @@ -898,7 +898,7 @@ def fields2pelican(
                             new_content = decode_wp_content(content)
                         else:
                             paragraphs = content.splitlines()
        -                    paragraphs = ["

        {}

        ".format(p) for p in paragraphs] + paragraphs = [f"

        {p}

        " for p in paragraphs] new_content = "".join(paragraphs) with open(html_filename, "w", encoding="utf-8") as fp: fp.write(new_content) diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py index fba0c9c3..db00ce70 100755 --- a/pelican/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -90,9 +90,9 @@ def ask(question, answer=str, default=None, length=None): r = "" while True: if default: - r = input("> {} [{}] ".format(question, default)) + r = input(f"> {question} [{default}] ") else: - r = input("> {} ".format(question)) + r = input(f"> {question} ") r = r.strip() @@ -104,7 +104,7 @@ def ask(question, answer=str, default=None, length=None): print("You must enter something") else: if length and len(r) != length: - print("Entry must be {} characters long".format(length)) + print(f"Entry must be {length} characters long") else: break @@ -114,11 +114,11 @@ 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(f"> {question} (Y/n) ") elif default is False: - r = input("> {} (y/N) ".format(question)) + r = input(f"> {question} (y/N) ") else: - r = input("> {} (y/n) ".format(question)) + r = input(f"> {question} (y/n) ") r = r.strip().lower() @@ -138,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(f"> {question} [{default}] ") else: - r = input("> {} ".format(question)) + r = input(f"> {question} ") r = r.strip() @@ -180,7 +180,7 @@ def render_jinja_template(tmpl_name: str, tmpl_vars: Mapping, target_path: str): _template = _jinja_env.get_template(tmpl_name) fd.write(_template.render(**tmpl_vars)) except OSError as e: - print("Error: {}".format(e)) + print(f"Error: {e}") def main(): @@ -376,12 +376,12 @@ needed by Pelican. try: os.makedirs(os.path.join(CONF["basedir"], "content")) except OSError as e: - print("Error: {}".format(e)) + print(f"Error: {e}") try: os.makedirs(os.path.join(CONF["basedir"], "output")) except OSError as e: - print("Error: {}".format(e)) + print(f"Error: {e}") conf_python = dict() for key, value in CONF.items(): diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py index 4069f99b..c5b49b9f 100755 --- a/pelican/tools/pelican_themes.py +++ b/pelican/tools/pelican_themes.py @@ -58,7 +58,7 @@ def main(): "-V", "--version", action="version", - version="pelican-themes v{}".format(__version__), + version=f"pelican-themes v{__version__}", help="Print the version of this script", ) @@ -224,7 +224,7 @@ def install(path, v=False, u=False): install(path, v) else: if v: - print("Copying '{p}' to '{t}' ...".format(p=path, t=theme_path)) + print(f"Copying '{path}' to '{theme_path}' ...") try: shutil.copytree(path, theme_path) @@ -264,7 +264,7 @@ def symlink(path, v=False): err(path + " : already exists") else: if v: - print("Linking `{p}' to `{t}' ...".format(p=path, t=theme_path)) + print(f"Linking `{path}' to `{theme_path}' ...") try: os.symlink(path, theme_path) except Exception as e: @@ -288,12 +288,12 @@ 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(f"Removing {path}") try: os.remove(path) except OSError: - print("Error: cannot remove {}".format(path)) + print(f"Error: cannot remove {path}") else: c += 1 - print("\nRemoved {} broken links".format(c)) + print(f"\nRemoved {c} broken links") diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py index 2e8cc953..6d705d4c 100644 --- a/pelican/urlwrappers.py +++ b/pelican/urlwrappers.py @@ -31,7 +31,7 @@ class URLWrapper: @property def slug(self): if self._slug is None: - class_key = "{}_REGEX_SUBSTITUTIONS".format(self.__class__.__name__.upper()) + class_key = f"{self.__class__.__name__.upper()}_REGEX_SUBSTITUTIONS" regex_subs = self.settings.get( class_key, self.settings.get("SLUG_REGEX_SUBSTITUTIONS", []) ) @@ -60,7 +60,7 @@ class URLWrapper: return hash(self.slug) def _normalize_key(self, key): - class_key = "{}_REGEX_SUBSTITUTIONS".format(self.__class__.__name__.upper()) + class_key = f"{self.__class__.__name__.upper()}_REGEX_SUBSTITUTIONS" regex_subs = self.settings.get( class_key, self.settings.get("SLUG_REGEX_SUBSTITUTIONS", []) ) @@ -98,7 +98,7 @@ class URLWrapper: return self.name def __repr__(self): - return "<{} {}>".format(type(self).__name__, repr(self._name)) + return f"<{type(self).__name__} {repr(self._name)}>" def _from_settings(self, key, get_page_name=False): """Returns URL information as defined in settings. @@ -108,7 +108,7 @@ class URLWrapper: "cat/{slug}" Useful for pagination. """ - setting = "{}_{}".format(self.__class__.__name__.upper(), key) + setting = f"{self.__class__.__name__.upper()}_{key}" value = self.settings[setting] if isinstance(value, pathlib.Path): value = str(value) diff --git a/pelican/utils.py b/pelican/utils.py index 08a08f7e..02ffb9d1 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -35,9 +35,7 @@ def sanitised_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) - ) + raise RuntimeError(f"Attempted to break out of output directory to {joined}") return joined @@ -71,7 +69,7 @@ def strftime(date, date_format): # check for '-' prefix if len(candidate) == 3: # '-' prefix - candidate = "%{}".format(candidate[-1]) + candidate = f"%{candidate[-1]}" conversion = strip_zeros else: conversion = None @@ -178,11 +176,11 @@ def deprecated_attribute(old, new, since=None, remove=None, doc=None): def _warn(): version = ".".join(str(x) for x in since) - message = ["{} has been deprecated since {}".format(old, version)] + message = [f"{old} has been deprecated since {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)) + message.append(f" and will be removed by version {version}") + message.append(f". Use {new} instead.") logger.warning("".join(message)) logger.debug("".join(str(x) for x in traceback.format_stack())) @@ -210,7 +208,7 @@ def get_date(string): try: return dateutil.parser.parse(string, default=default) except (TypeError, ValueError): - raise ValueError("{!r} is not a valid date".format(string)) + raise ValueError(f"{string!r} is not a valid date") @contextmanager @@ -763,9 +761,9 @@ def order_content(content_list, order_by="slug"): def wait_for_changes(settings_file, reader_class, settings): content_path = settings.get("PATH", "") theme_path = settings.get("THEME", "") - ignore_files = set( + ignore_files = { fnmatch.translate(pattern) for pattern in settings.get("IGNORE_FILES", []) - ) + } candidate_paths = [ settings_file, @@ -838,7 +836,7 @@ def split_all(path): return None else: raise TypeError( - '"path" was {}, must be string, None, or pathlib.Path'.format(type(path)) + f'"path" was {type(path)}, must be string, None, or pathlib.Path' ) @@ -873,7 +871,7 @@ def maybe_pluralize(count, singular, plural): selection = plural if count == 1: selection = singular - return "{} {}".format(count, selection) + return f"{count} {selection}" @contextmanager From db241feaa445375dc05e189e69287000ffe5fa8e Mon Sep 17 00:00:00 2001 From: Paolo Melchiorre Date: Sun, 12 Nov 2023 15:06:02 +0100 Subject: [PATCH 77/88] Fix #2888 -- Apply ruff and pyupgrade to templates --- pelican/tools/templates/pelicanconf.py.jinja2 | 22 ++-- pelican/tools/templates/publishconf.py.jinja2 | 11 +- pelican/tools/templates/tasks.py.jinja2 | 113 ++++++++++-------- 3 files changed, 82 insertions(+), 64 deletions(-) diff --git a/pelican/tools/templates/pelicanconf.py.jinja2 b/pelican/tools/templates/pelicanconf.py.jinja2 index 1112ac88..d2e92d4b 100644 --- a/pelican/tools/templates/pelicanconf.py.jinja2 +++ b/pelican/tools/templates/pelicanconf.py.jinja2 @@ -1,8 +1,8 @@ AUTHOR = {{author}} SITENAME = {{sitename}} -SITEURL = '' +SITEURL = "" -PATH = 'content' +PATH = "content" TIMEZONE = {{timezone}} @@ -16,16 +16,20 @@ AUTHOR_FEED_ATOM = None AUTHOR_FEED_RSS = None # Blogroll -LINKS = (('Pelican', 'https://getpelican.com/'), - ('Python.org', 'https://www.python.org/'), - ('Jinja2', 'https://palletsprojects.com/p/jinja/'), - ('You can modify those links in your config file', '#'),) +LINKS = ( + ("Pelican", "https://getpelican.com/"), + ("Python.org", "https://www.python.org/"), + ("Jinja2", "https://palletsprojects.com/p/jinja/"), + ("You can modify those links in your config file", "#"), +) # Social widget -SOCIAL = (('You can add links in your config file', '#'), - ('Another social link', '#'),) +SOCIAL = ( + ("You can add links in your config file", "#"), + ("Another social link", "#"), +) DEFAULT_PAGINATION = {{default_pagination}} # Uncomment following line if you want document-relative URLs when developing -#RELATIVE_URLS = True +# RELATIVE_URLS = True diff --git a/pelican/tools/templates/publishconf.py.jinja2 b/pelican/tools/templates/publishconf.py.jinja2 index e119222c..301e4dfa 100755 --- a/pelican/tools/templates/publishconf.py.jinja2 +++ b/pelican/tools/templates/publishconf.py.jinja2 @@ -3,19 +3,20 @@ import os import sys + sys.path.append(os.curdir) from pelicanconf import * # If your site is available via HTTPS, make sure SITEURL begins with https:// -SITEURL = '{{siteurl}}' +SITEURL = "{{siteurl}}" RELATIVE_URLS = False -FEED_ALL_ATOM = 'feeds/all.atom.xml' -CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml' +FEED_ALL_ATOM = "feeds/all.atom.xml" +CATEGORY_FEED_ATOM = "feeds/{slug}.atom.xml" DELETE_OUTPUT_DIRECTORY = True # Following items are often useful when publishing -#DISQUS_SITENAME = "" -#GOOGLE_ANALYTICS = "" +# DISQUS_SITENAME = "" +# GOOGLE_ANALYTICS = "" diff --git a/pelican/tools/templates/tasks.py.jinja2 b/pelican/tools/templates/tasks.py.jinja2 index f3caed56..1a0f02d1 100644 --- a/pelican/tools/templates/tasks.py.jinja2 +++ b/pelican/tools/templates/tasks.py.jinja2 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os import shlex import shutil @@ -14,61 +12,66 @@ from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer from pelican.settings import DEFAULT_CONFIG, get_settings_from_file OPEN_BROWSER_ON_SERVE = True -SETTINGS_FILE_BASE = 'pelicanconf.py' +SETTINGS_FILE_BASE = "pelicanconf.py" SETTINGS = {} SETTINGS.update(DEFAULT_CONFIG) LOCAL_SETTINGS = get_settings_from_file(SETTINGS_FILE_BASE) SETTINGS.update(LOCAL_SETTINGS) CONFIG = { - 'settings_base': SETTINGS_FILE_BASE, - 'settings_publish': 'publishconf.py', + "settings_base": SETTINGS_FILE_BASE, + "settings_publish": "publishconf.py", # Output path. Can be absolute or relative to tasks.py. Default: 'output' - 'deploy_path': SETTINGS['OUTPUT_PATH'], + "deploy_path": SETTINGS["OUTPUT_PATH"], {% if ssh %} # Remote server configuration - 'ssh_user': '{{ssh_user}}', - 'ssh_host': '{{ssh_host}}', - 'ssh_port': '{{ssh_port}}', - 'ssh_path': '{{ssh_target_dir}}', + "ssh_user": "{{ssh_user}}", + "ssh_host": "{{ssh_host}}", + "ssh_port": "{{ssh_port}}", + "ssh_path": "{{ssh_target_dir}}", {% endif %} {% if cloudfiles %} # Rackspace Cloud Files configuration settings - 'cloudfiles_username': '{{cloudfiles_username}}', - 'cloudfiles_api_key': '{{cloudfiles_api_key}}', - 'cloudfiles_container': '{{cloudfiles_container}}', + "cloudfiles_username": "{{cloudfiles_username}}", + "cloudfiles_api_key": "{{cloudfiles_api_key}}", + "cloudfiles_container": "{{cloudfiles_container}}", {% endif %} {% if github %} # Github Pages configuration - 'github_pages_branch': '{{github_pages_branch}}', - 'commit_message': "'Publish site on {}'".format(datetime.date.today().isoformat()), + "github_pages_branch": "{{github_pages_branch}}", + "commit_message": f"'Publish site on {datetime.date.today().isoformat()}'", {% endif %} # Host and port for `serve` - 'host': 'localhost', - 'port': 8000, + "host": "localhost", + "port": 8000, } + @task def clean(c): """Remove generated files""" - if os.path.isdir(CONFIG['deploy_path']): - shutil.rmtree(CONFIG['deploy_path']) - os.makedirs(CONFIG['deploy_path']) + if os.path.isdir(CONFIG["deploy_path"]): + shutil.rmtree(CONFIG["deploy_path"]) + os.makedirs(CONFIG["deploy_path"]) + @task def build(c): """Build local version of site""" - pelican_run('-s {settings_base}'.format(**CONFIG)) + pelican_run("-s {settings_base}".format(**CONFIG)) + @task def rebuild(c): """`build` with the delete switch""" - pelican_run('-d -s {settings_base}'.format(**CONFIG)) + pelican_run("-d -s {settings_base}".format(**CONFIG)) + @task def regenerate(c): """Automatically regenerate site upon file modification""" - pelican_run('-r -s {settings_base}'.format(**CONFIG)) + pelican_run("-r -s {settings_base}".format(**CONFIG)) + @task def serve(c): @@ -78,28 +81,32 @@ def serve(c): allow_reuse_address = True server = AddressReuseTCPServer( - CONFIG['deploy_path'], - (CONFIG['host'], CONFIG['port']), - ComplexHTTPRequestHandler) + CONFIG["deploy_path"], + (CONFIG["host"], CONFIG["port"]), + ComplexHTTPRequestHandler, + ) if OPEN_BROWSER_ON_SERVE: # Open site in default browser import webbrowser + webbrowser.open("http://{host}:{port}".format(**CONFIG)) - sys.stderr.write('Serving at {host}:{port} ...\n'.format(**CONFIG)) + sys.stderr.write("Serving at {host}:{port} ...\n".format(**CONFIG)) server.serve_forever() + @task def reserve(c): """`build`, then `serve`""" build(c) serve(c) + @task def preview(c): """Build production version of site""" - pelican_run('-s {settings_publish}'.format(**CONFIG)) + pelican_run("-s {settings_publish}".format(**CONFIG)) @task def livereload(c): @@ -107,25 +114,25 @@ def livereload(c): from livereload import Server def cached_build(): - cmd = '-s {settings_base} -e CACHE_CONTENT=true LOAD_CONTENT_CACHE=true' + cmd = "-s {settings_base} -e CACHE_CONTENT=true LOAD_CONTENT_CACHE=true" pelican_run(cmd.format(**CONFIG)) cached_build() server = Server() - theme_path = SETTINGS['THEME'] + theme_path = SETTINGS["THEME"] watched_globs = [ - CONFIG['settings_base'], - '{}/templates/**/*.html'.format(theme_path), + CONFIG["settings_base"], + f"{theme_path}/templates/**/*.html", ] - content_file_extensions = ['.md', '.rst'] + content_file_extensions = [".md", ".rst"] for extension in content_file_extensions: - content_glob = '{0}/**/*{1}'.format(SETTINGS['PATH'], extension) + content_glob = "{}/**/*{}".format(SETTINGS["PATH"], extension) watched_globs.append(content_glob) - static_file_extensions = ['.css', '.js'] + static_file_extensions = [".css", ".js"] for extension in static_file_extensions: - static_file_glob = '{0}/static/**/*{1}'.format(theme_path, extension) + static_file_glob = f"{theme_path}/static/**/*{extension}" watched_globs.append(static_file_glob) for glob in watched_globs: @@ -134,43 +141,49 @@ def livereload(c): if OPEN_BROWSER_ON_SERVE: # Open site in default browser import webbrowser + webbrowser.open("http://{host}:{port}".format(**CONFIG)) - server.serve(host=CONFIG['host'], port=CONFIG['port'], root=CONFIG['deploy_path']) + server.serve(host=CONFIG["host"], port=CONFIG["port"], root=CONFIG["deploy_path"]) {% if cloudfiles %} @task def cf_upload(c): """Publish to Rackspace Cloud Files""" rebuild(c) - with cd(CONFIG['deploy_path']): - c.run('swift -v -A https://auth.api.rackspacecloud.com/v1.0 ' - '-U {cloudfiles_username} ' - '-K {cloudfiles_api_key} ' - 'upload -c {cloudfiles_container} .'.format(**CONFIG)) + with cd(CONFIG["deploy_path"]): + c.run( + "swift -v -A https://auth.api.rackspacecloud.com/v1.0 " + "-U {cloudfiles_username} " + "-K {cloudfiles_api_key} " + "upload -c {cloudfiles_container} .".format(**CONFIG) + ) {% endif %} @task def publish(c): """Publish to production via rsync""" - pelican_run('-s {settings_publish}'.format(**CONFIG)) + pelican_run("-s {settings_publish}".format(**CONFIG)) c.run( 'rsync --delete --exclude ".DS_Store" -pthrvz -c ' '-e "ssh -p {ssh_port}" ' - '{} {ssh_user}@{ssh_host}:{ssh_path}'.format( - CONFIG['deploy_path'].rstrip('/') + '/', - **CONFIG)) + "{} {ssh_user}@{ssh_host}:{ssh_path}".format( + CONFIG["deploy_path"].rstrip("/") + "/", **CONFIG + ) + ) {% if github %} @task def gh_pages(c): """Publish to GitHub Pages""" preview(c) - c.run('ghp-import -b {github_pages_branch} ' - '-m {commit_message} ' - '{deploy_path} -p'.format(**CONFIG)) + c.run( + "ghp-import -b {github_pages_branch} " + "-m {commit_message} " + "{deploy_path} -p".format(**CONFIG) + ) {% endif %} def pelican_run(cmd): - cmd += ' ' + program.core.remainder # allows to pass-through args to pelican + cmd += " " + program.core.remainder # allows to pass-through args to pelican pelican_main(shlex.split(cmd)) From 2238dcab077eac83fb6855b2ab4914cb05882ca3 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 15:53:13 +0100 Subject: [PATCH 78/88] Ignore code format commits from `git blame` --- .git-blame-ignore-revs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 7b822fd3..0d92c9d9 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,7 @@ # .git-blame-ignore-revs # Apply code style to project via: ruff format . cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7 +# Upgrade code base for Python 3.8 and above +ecd598f293161a52564aa6e8dfdcc8284dc93970 +# Apply Ruff and pyupgrade to Jinja templates +db241feaa445375dc05e189e69287000ffe5fa8e From 0c5d63c69ee53d1891644a6b97e96b36e5d8721e Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 17:11:23 +0100 Subject: [PATCH 79/88] Update documentation related to contributing --- CONTRIBUTING.rst | 28 ++++++++++------------------ docs/contribute.rst | 4 +--- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c1175aa4..4faace91 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -79,6 +79,10 @@ don't spend a lot of time working on something that would be rejected for a known reason. Consider also whether your new feature might be better suited as a ':pelican-doc:`plugins` — you can `ask for help`_ to make that determination. +Also, if you intend to submit a pull request to address something for which there +is no existing issue, there is no need to create a new issue and then immediately +submit a pull request that closes it. You can submit the pull request by itself. + Using Git and GitHub -------------------- @@ -87,7 +91,8 @@ Using Git and GitHub * **Don't put multiple unrelated fixes/features in the same branch / pull request.** For example, if you're working on a new feature and find a bugfix that doesn't *require* your new feature, **make a new distinct branch and pull - request** for the bugfix. + request** for the bugfix. Similarly, any proposed changes to code style + formatting should be in a completely separate pull request. * Add a ``RELEASE.md`` file in the root of the project that contains the release type (major, minor, patch) and a summary of the changes that will be used as the release changelog entry. For example:: @@ -106,15 +111,8 @@ Using Git and GitHub detailed explanation (when relevant). * `Squash your commits`_ to eliminate merge commits and ensure a clean and readable commit history. -* If you have previously filed a GitHub issue and want to contribute code that - addresses that issue, **please use** ``hub pull-request`` instead of using - GitHub's web UI to submit the pull request. This isn't an absolute - requirement, but makes the maintainers' lives much easier! Specifically: - `install hub `_ and then run - `hub pull-request -i [ISSUE] `_ - to turn your GitHub issue into a pull request containing your code. * After you have issued a pull request, the continuous integration (CI) system - will run the test suite for all supported Python versions and check for PEP8 + will run the test suite on all supported Python versions and check for code style compliance. If any of these checks fail, you should fix them. (If tests fail on the CI system but seem to pass locally, ensure that local test runs aren't skipping any tests.) @@ -122,13 +120,7 @@ Using Git and GitHub Contribution quality standards ------------------------------ -* Adhere to `PEP8 coding standards`_. This can be eased via the `pycodestyle - `_ or `flake8 - `_ tools, the latter of which in - particular will give you some useful hints about ways in which the - code/formatting can be improved. We try to keep line length within the - 79-character maximum specified by PEP8. Because that can sometimes compromise - readability, the hard/enforced maximum is 88 characters. +* Adhere to the project's code style standards. See: `Development Environment`_ * Ensure your code is compatible with the `officially-supported Python releases`_. * Add docs and tests for your changes. Undocumented and untested features will not be accepted. @@ -142,6 +134,6 @@ need assistance or have any questions about these guidelines. .. _`Create a new branch`: https://github.com/getpelican/pelican/wiki/Git-Tips#making-your-changes .. _`Squash your commits`: https://github.com/getpelican/pelican/wiki/Git-Tips#squashing-commits .. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips -.. _`PEP8 coding standards`: https://www.python.org/dev/peps/pep-0008/ .. _`ask for help`: `How to get help`_ -.. _`officially-supported Python releases`: https://devguide.python.org/#status-of-python-branches +.. _`Development Environment`: https://docs.getpelican.com/en/latest/contribute.html#setting-up-the-development-environment +.. _`officially-supported Python releases`: https://devguide.python.org/versions/#versions diff --git a/docs/contribute.rst b/docs/contribute.rst index 33a62064..6a5a417e 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -46,7 +46,6 @@ Install the needed dependencies and set up the project:: python -m pip install invoke invoke setup - python -m pip install -e ~/projects/pelican Your local environment should now be ready to go! @@ -159,8 +158,7 @@ check for code style compliance via:: If style violations are found, many of them can be addressed automatically via:: - invoke black - invoke isort + invoke format If style violations are found even after running the above auto-formatters, you will need to make additional manual changes until ``invoke lint`` no longer From 86d689851793e61b0cee1fe0bf72c8ebc991b6c1 Mon Sep 17 00:00:00 2001 From: Deniz Turgut Date: Sun, 12 Nov 2023 19:43:26 +0300 Subject: [PATCH 80/88] remove WRITE_SELECTED Implementation is buggy and unreliable. Therefore, it is better to remove the functionality until a robust implementation is added. --- docs/faq.rst | 4 ---- docs/publish.rst | 12 ------------ docs/settings.rst | 22 ---------------------- pelican/__init__.py | 11 ----------- pelican/settings.py | 14 +++++++------- pelican/tests/test_pelican.py | 23 ----------------------- pelican/utils.py | 13 ------------- pelican/writers.py | 12 +----------- 8 files changed, 8 insertions(+), 103 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index c065b4ed..cecc1157 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -217,10 +217,6 @@ changed. A simple solution is to make ``rsync`` use the ``--checksum`` option, which will make it compare the file checksums in a much faster way than Pelican would. -When only several specific output files are of interest (e.g. when working on -some specific page or the theme templates), the ``WRITE_SELECTED`` option may -help, see :ref:`writing_only_selected_content`. - How to process only a subset of all articles? ============================================= diff --git a/docs/publish.rst b/docs/publish.rst index f5ebfff5..e687b65a 100644 --- a/docs/publish.rst +++ b/docs/publish.rst @@ -18,18 +18,6 @@ folder, using the default theme to produce a simple site. The default theme consists of very simple HTML without styling and is provided so folks may use it as a basis for creating their own themes. -When working on a single article or page, it is possible to generate only the -file that corresponds to that content. To do this, use the ``--write-selected`` -argument, like so:: - - pelican --write-selected output/posts/my-post-title.html - -Note that you must specify the path to the generated *output* file — not the -source content. To determine the output file name and location, use the -``--debug`` flag. If desired, ``--write-selected`` can take a comma-separated -list of paths or can be configured as a setting. (See: -:ref:`writing_only_selected_content`) - You can also tell Pelican to watch for your modifications, instead of manually re-running it every time you want to see your changes. To enable this, run the ``pelican`` command with the ``-r`` or ``--autoreload`` option. On non-Windows diff --git a/docs/settings.rst b/docs/settings.rst index 88a32d23..e9edffde 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -362,13 +362,6 @@ Basic settings If ``True``, load unmodified content from caches. -.. data:: WRITE_SELECTED = [] - - If this list is not empty, **only** output files with their paths in this - list are written. Paths should be either absolute or relative to the current - Pelican working directory. For possible use cases see - :ref:`writing_only_selected_content`. - .. data:: FORMATTED_FIELDS = ['summary'] A list of metadata fields containing reST/Markdown content to be parsed and @@ -1400,21 +1393,6 @@ modification times of the generated ``*.html`` files will always change. Therefore, ``rsync``-based uploading may benefit from the ``--checksum`` option. -.. _writing_only_selected_content: - - -Writing only selected content -============================= - -When only working on a single article or page, or making tweaks to your theme, -it is often desirable to generate and review your work as quickly as possible. -In such cases, generating and writing the entire site output is often -unnecessary. By specifying only the desired files as output paths in the -``WRITE_SELECTED`` list, **only** those files will be written. This list can be -also specified on the command line using the ``--write-selected`` option, which -accepts a comma-separated list of output file paths. By default this list is -empty, so all output is written. See :ref:`site_generation` for more details. - Example settings ================ diff --git a/pelican/__init__.py b/pelican/__init__.py index 25c493b9..a25f5624 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -434,15 +434,6 @@ def parse_arguments(argv=None): help="Ignore content cache " "from previous runs by not loading cache files.", ) - parser.add_argument( - "-w", - "--write-selected", - type=str, - dest="selected_paths", - default=None, - help="Comma separated list of selected paths to write", - ) - parser.add_argument( "--fatal", metavar="errors|warnings", @@ -527,8 +518,6 @@ def get_config(args): config["LOAD_CONTENT_CACHE"] = False if args.cache_path: config["CACHE_PATH"] = args.cache_path - if args.selected_paths: - config["WRITE_SELECTED"] = args.selected_paths.split(",") if args.relative_paths: config["RELATIVE_URLS"] = args.relative_paths if args.port is not None: diff --git a/pelican/settings.py b/pelican/settings.py index 4a4f2901..33ec210a 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -169,7 +169,6 @@ DEFAULT_CONFIG = { "GZIP_CACHE": True, "CHECK_MODIFIED_METHOD": "mtime", "LOAD_CONTENT_CACHE": False, - "WRITE_SELECTED": [], "FORMATTED_FIELDS": ["summary"], "PORT": 8000, "BIND": "127.0.0.1", @@ -557,6 +556,13 @@ def handle_deprecated_settings(settings): ) settings[old] = settings[new] + # Warn if removed WRITE_SELECTED is present + if "WRITE_SELECTED" in settings: + logger.warning( + "WRITE_SELECTED is present in settings but this functionality was removed. " + "It will have no effect." + ) + return settings @@ -585,12 +591,6 @@ def configure_settings(settings): else: raise Exception("Could not find the theme %s" % settings["THEME"]) - # make paths selected for writing absolute if necessary - settings["WRITE_SELECTED"] = [ - os.path.abspath(path) - for path in settings.get("WRITE_SELECTED", DEFAULT_CONFIG["WRITE_SELECTED"]) - ] - # standardize strings to lowercase strings for key in ["DEFAULT_LANG"]: if key in settings: diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index 3c0c0572..075f55eb 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -202,29 +202,6 @@ class TestPelican(LoggedTestCase): 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"), - }, - ) - 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) - def test_cyclic_intersite_links_no_warnings(self): settings = read_settings( path=None, diff --git a/pelican/utils.py b/pelican/utils.py index 02ffb9d1..eda53d3f 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -840,19 +840,6 @@ def split_all(path): ) -def is_selected_for_writing(settings, path): - """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"] - else: - return True - - def path_to_file_url(path): """Convert file-system path to file:// URL""" return urllib.parse.urljoin("file://", urllib.request.pathname2url(path)) diff --git a/pelican/writers.py b/pelican/writers.py index ec12d125..d405fc88 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -11,7 +11,6 @@ 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, @@ -145,9 +144,6 @@ class Writer: name should be skipped to keep that one) :param feed_title: the title of the feed.o """ - if not is_selected_for_writing(self.settings, path): - return - self.site_url = context.get("SITEURL", path_to_url(get_relative_path(path))) self.feed_domain = context.get("FEED_DOMAIN") @@ -203,13 +199,7 @@ 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 == "": return elif not name: # other stuff, just return for now From 7ca66ee9d0ac672e88c845d347457be7ad4def41 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 12 Nov 2023 18:03:36 +0100 Subject: [PATCH 81/88] Prepare release --- RELEASE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..33991cb6 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,21 @@ +Release type: minor + +* Upgrade code to new minimum supported Python version: 3.8 +* Settings support for ``pathlib.Path`` `(#2758) `_ +* Various improvements to Simple theme (`#2976 `_ & `#3234 `_) +* Use Furo as Sphinx documentation theme `(#3023) `_ +* Default to 100 articles maximum in feeds `(#3127) `_ +* Add ``period_archives common context`` variable `(#3148) `_ +* Use ``watchfiles`` as the file-watching backend `(#3151) `_ +* Add GitHub Actions workflow for GitHub Pages `(#3189) `_ +* Allow dataclasses in settings `(#3204) `_ +* Switch build tool to PDM instead of Setuptools/Poetry `(#3220) `_ +* Provide a ``plugin_enabled`` Jinja test for themes `(#3235) `_ +* Preserve connection order in Blinker `(#3238) `_ +* Remove social icons from default ``notmyidea`` theme `(#3240) `_ +* Remove unreliable ``WRITE_SELECTED`` feature `(#3243) `_ +* Importer: Report broken embedded video links when importing from Tumblr `(#3177) `_ +* Importer: Remove newline addition when iterating Photo post types `(#3178) `_ +* Importer: Force timestamp conversion in Tumblr importer to be UTC with offset `(#3221) `_ +* Importer: Use tempfile for intermediate HTML file for Pandoc `(#3221) `_ +* Switch linters to Ruff `(#3223) `_ From 6cd707a66893a7ed509d982639cd4e7a2bc8cf9c Mon Sep 17 00:00:00 2001 From: botpub <52496925+botpub@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:44:45 +0000 Subject: [PATCH 82/88] Release Pelican 4.9.0 --- RELEASE.md | 21 --------------------- docs/changelog.rst | 23 +++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 24 insertions(+), 22 deletions(-) delete mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 33991cb6..00000000 --- a/RELEASE.md +++ /dev/null @@ -1,21 +0,0 @@ -Release type: minor - -* Upgrade code to new minimum supported Python version: 3.8 -* Settings support for ``pathlib.Path`` `(#2758) `_ -* Various improvements to Simple theme (`#2976 `_ & `#3234 `_) -* Use Furo as Sphinx documentation theme `(#3023) `_ -* Default to 100 articles maximum in feeds `(#3127) `_ -* Add ``period_archives common context`` variable `(#3148) `_ -* Use ``watchfiles`` as the file-watching backend `(#3151) `_ -* Add GitHub Actions workflow for GitHub Pages `(#3189) `_ -* Allow dataclasses in settings `(#3204) `_ -* Switch build tool to PDM instead of Setuptools/Poetry `(#3220) `_ -* Provide a ``plugin_enabled`` Jinja test for themes `(#3235) `_ -* Preserve connection order in Blinker `(#3238) `_ -* Remove social icons from default ``notmyidea`` theme `(#3240) `_ -* Remove unreliable ``WRITE_SELECTED`` feature `(#3243) `_ -* Importer: Report broken embedded video links when importing from Tumblr `(#3177) `_ -* Importer: Remove newline addition when iterating Photo post types `(#3178) `_ -* Importer: Force timestamp conversion in Tumblr importer to be UTC with offset `(#3221) `_ -* Importer: Use tempfile for intermediate HTML file for Pandoc `(#3221) `_ -* Switch linters to Ruff `(#3223) `_ diff --git a/docs/changelog.rst b/docs/changelog.rst index 88353ed4..98da5b20 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,29 @@ Release history ############### +4.9.0 - 2023-11-12 +================== + +* Upgrade code to new minimum supported Python version: 3.8 +* Settings support for ``pathlib.Path`` `(#2758) `_ +* Various improvements to Simple theme (`#2976 `_ & `#3234 `_) +* Use Furo as Sphinx documentation theme `(#3023) `_ +* Default to 100 articles maximum in feeds `(#3127) `_ +* Add ``period_archives common context`` variable `(#3148) `_ +* Use ``watchfiles`` as the file-watching backend `(#3151) `_ +* Add GitHub Actions workflow for GitHub Pages `(#3189) `_ +* Allow dataclasses in settings `(#3204) `_ +* Switch build tool to PDM instead of Setuptools/Poetry `(#3220) `_ +* Provide a ``plugin_enabled`` Jinja test for themes `(#3235) `_ +* Preserve connection order in Blinker `(#3238) `_ +* Remove social icons from default ``notmyidea`` theme `(#3240) `_ +* Remove unreliable ``WRITE_SELECTED`` feature `(#3243) `_ +* Importer: Report broken embedded video links when importing from Tumblr `(#3177) `_ +* Importer: Remove newline addition when iterating Photo post types `(#3178) `_ +* Importer: Force timestamp conversion in Tumblr importer to be UTC with offset `(#3221) `_ +* Importer: Use tempfile for intermediate HTML file for Pandoc `(#3221) `_ +* Switch linters to Ruff `(#3223) `_ + 4.8.0 - 2022-07-11 ================== diff --git a/pyproject.toml b/pyproject.toml index d40e6bce..e0e118f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "pelican" authors = [{ name = "Justin Mayer", email = "authors@getpelican.com" }] description = "Static site generator supporting Markdown and reStructuredText" -version = "4.8.0" +version = "4.9.0" license = { text = "AGPLv3" } readme = "README.rst" keywords = ["static site generator", "static sites", "ssg"] From 1d0fd456e875d743516b56458ab9767511f3e5d3 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Mon, 13 Nov 2023 12:18:41 -0700 Subject: [PATCH 83/88] Add `tzdata` requirement on Windows --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e0e118f5..cf3c23c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "unidecode>=1.3.7", "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", "watchfiles>=0.21.0", + "tzdata; sys_platform == 'win32'", ] [project.optional-dependencies] From a2525f7db442e99c4210c07aed9e4a04afb494be Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Mon, 13 Nov 2023 14:42:29 -0700 Subject: [PATCH 84/88] Remove `tzdata` from testing requirements. Should be installed by pelican directly, if needed, based on OS. --- requirements/test.pip | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/test.pip b/requirements/test.pip index 87869e67..8eb1029f 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -3,7 +3,6 @@ Pygments==2.14.0 pytest pytest-cov pytest-xdist[psutil] -tzdata # Optional Packages Markdown==3.5.1 From f510b4b21f8c7d707eb42d6261d910e327b71bc0 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 15 Nov 2023 17:52:41 +0100 Subject: [PATCH 85/88] Remove reference to non-existent requirements file --- requirements/developer.pip | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/developer.pip b/requirements/developer.pip index 5c2f5a69..58b2a1dc 100644 --- a/requirements/developer.pip +++ b/requirements/developer.pip @@ -1,3 +1,2 @@ -r test.pip -r docs.pip --r style.pip From 76f7343b6149e300dc014fd9128f6f4214099b91 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Wed, 15 Nov 2023 18:08:10 +0100 Subject: [PATCH 86/88] Prepare release --- RELEASE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..b61180cb --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: patch + +* Ensure ``tzdata`` dependency is installed on Windows From 7194cf579571dbbca85faad5bace71627c56152d Mon Sep 17 00:00:00 2001 From: botpub <52496925+botpub@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:16:23 +0000 Subject: [PATCH 87/88] Release Pelican 4.9.1 --- RELEASE.md | 3 --- docs/changelog.rst | 5 +++++ pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index b61180cb..00000000 --- a/RELEASE.md +++ /dev/null @@ -1,3 +0,0 @@ -Release type: patch - -* Ensure ``tzdata`` dependency is installed on Windows diff --git a/docs/changelog.rst b/docs/changelog.rst index 98da5b20..5ef19b17 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Release history ############### +4.9.1 - 2023-11-15 +================== + +* Ensure ``tzdata`` dependency is installed on Windows + 4.9.0 - 2023-11-12 ================== diff --git a/pyproject.toml b/pyproject.toml index cf3c23c0..c8bbe985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "pelican" authors = [{ name = "Justin Mayer", email = "authors@getpelican.com" }] description = "Static site generator supporting Markdown and reStructuredText" -version = "4.9.0" +version = "4.9.1" license = { text = "AGPLv3" } readme = "README.rst" keywords = ["static site generator", "static sites", "ssg"] From d9b2bc3a4ea9f11aea58180aaaea69cc95db1f8c Mon Sep 17 00:00:00 2001 From: Vivek Bharadwaj <67718556+vbharadwaj-bk@users.noreply.github.com> Date: Sun, 19 Nov 2023 01:48:13 -0800 Subject: [PATCH 88/88] Add GH pages action to fix file permissions (#3248) --- .github/workflows/github_pages.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/github_pages.yml b/.github/workflows/github_pages.yml index ccf172b4..7eae170a 100644 --- a/.github/workflows/github_pages.yml +++ b/.github/workflows/github_pages.yml @@ -44,6 +44,11 @@ jobs: --settings "${{ inputs.settings }}" \ --extra-settings SITEURL='"${{ steps.pages.outputs.base_url }}"' \ --output "${{ inputs.output-path }}" + - name: Fix permissions + run: | + chmod -c -R +rX "${{ inputs.output-path }}" | while read line; do + echo "::warning title=Invalid file permissions automatically fixed::$line" + done - name: Upload artifact uses: actions/upload-pages-artifact@v2 with: