Merge branch 'master' into logs_handlers

This commit is contained in:
Justin Mayer 2024-05-30 17:08:46 +02:00 committed by GitHub
commit 37862e94ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 175 additions and 91 deletions

View file

@ -5,3 +5,5 @@ cabdb26cee66e1173cf16cb31d3fe5f9fa4392e7
ecd598f293161a52564aa6e8dfdcc8284dc93970
# Apply Ruff and pyupgrade to Jinja templates
db241feaa445375dc05e189e69287000ffe5fa8e
# Change pre-commit to run ruff and ruff-format with fixes
6d8597addb17d5fa3027ead91427939e8e4e89ec

View file

@ -1,3 +1,4 @@
# Workflow for publishing to GitHub Pages. **NO OFFICIAL SUPPORT PROVIDED.**
name: Deploy to GitHub Pages
on:
workflow_call:

View file

@ -17,7 +17,7 @@ repos:
rev: v0.1.15
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
args: ["--check"]
exclude: ^pelican/tests/output/

View file

@ -1008,6 +1008,11 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
to ``False``, the full content will be included instead. This setting
doesn't affect Atom feeds, only RSS ones.
.. data:: FEED_APPEND_REF = False
If set to ``True``, ``?ref=feed`` will be appended to links in generated
feeds for the purpose of referrer tracking.
If you don't want to generate some or any of these feeds, set the above
variables to ``None``.

View file

@ -126,8 +126,9 @@ branch of your GitHub repository::
Publishing to GitHub Pages Using a Custom GitHub Actions Workflow
-----------------------------------------------------------------
Pelican comes with a `custom workflow <https://github.com/getpelican/pelican/blob/master/.github/workflows/github_pages.yml>`_
for publishing a Pelican site. To use it:
Pelican-powered sites can be published to GitHub Pages via a `custom workflow
<https://github.com/getpelican/pelican/blob/master/.github/workflows/github_pages.yml>`_.
**No official support is provided** for this community-submitted workflow. To use it:
1. Enable GitHub Pages in your repo: go to **Settings → Pages** and choose
**GitHub Actions** for the **Source** setting.

View file

@ -19,10 +19,10 @@ __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, DEFAULT_LOG_HANDLER
from pelican.log import console, DEFAULT_LOG_HANDLER # noqa: I001
from pelican.log import init as init_logging
from pelican.generators import (
ArticlesGenerator, # noqa: I100
ArticlesGenerator,
PagesGenerator,
SourceFileGenerator,
StaticGenerator,
@ -354,8 +354,8 @@ def parse_arguments(argv=None):
"--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),
f"automatically set to {DEFAULT_CONFIG_NAME} if a file exists with this "
"name.",
)
parser.add_argument(

View file

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

View file

@ -17,6 +17,9 @@ except ModuleNotFoundError:
from pelican.plugins import signals
from pelican.settings import DEFAULT_CONFIG, Settings
# Import these so that they're available when you import from pelican.contents.
from pelican.urlwrappers import Author, Category, Tag, URLWrapper # NOQA
from pelican.utils import (
deprecated_attribute,
memoized,
@ -28,9 +31,6 @@ from pelican.utils import (
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
logger = logging.getLogger(__name__)
@ -370,13 +370,13 @@ class Content:
def _get_intrasite_link_regex(self) -> re.Pattern:
intrasite_link_regex = self.settings["INTRASITE_LINK_REGEX"]
regex = r"""
regex = rf"""
(?P<markup><[^\>]+ # match tag with all url-value attributes
(?:href|src|poster|data|cite|formaction|action|content)\s*=\s*)
(?P<quote>["\']) # require value to be quoted
(?P<path>{}(?P<value>.*?)) # the url value
(?P=quote)""".format(intrasite_link_regex)
(?P<path>{intrasite_link_regex}(?P<value>.*?)) # the url value
(?P=quote)"""
return re.compile(regex, re.X)
def _update_content(self, content: str, siteurl: str) -> str:
@ -465,7 +465,6 @@ class Content:
@summary.setter
def summary(self, value: str):
"""Dummy function"""
pass
@property
def status(self) -> str:

View file

@ -6,7 +6,6 @@ import logging
import pkgutil
import sys
logger = logging.getLogger(__name__)

View file

@ -1,4 +1,4 @@
from blinker import signal, Signal
from blinker import Signal, signal
from ordered_set import OrderedSet
# Signals will call functions in the order of connection, i.e. plugin order

View file

@ -22,7 +22,7 @@ from pelican.utils import get_date, pelican_open, posixize_path
try:
from markdown import Markdown
except ImportError:
Markdown = False # NOQA
Markdown = False
# Metadata processors have no way to discard an unwanted value, so we have
# them return this value instead to signal that it should be discarded later.
@ -607,8 +607,8 @@ class Readers(FileStampDataCacher):
# eventually filter the content with typogrify if asked so
if self.settings["TYPOGRIFY"]:
from typogrify.filters import typogrify
import smartypants
from typogrify.filters import typogrify
typogrify_dashes = self.settings["TYPOGRIFY_DASHES"]
if typogrify_dashes == "oldschool":

View file

@ -2,7 +2,6 @@ import re
from docutils import nodes, utils
from docutils.parsers.rst import Directive, directives, roles
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import TextLexer, get_lexer_by_name

View file

@ -52,6 +52,7 @@ DEFAULT_CONFIG = {
"TRANSLATION_FEED_ATOM": "feeds/all-{lang}.atom.xml",
"FEED_MAX_ITEMS": 100,
"RSS_FEED_SUMMARY_ONLY": True,
"FEED_APPEND_REF": False,
"SITEURL": "",
"SITENAME": "A Pelican Blog",
"DISPLAY_PAGES_ON_MENU": True,
@ -266,9 +267,7 @@ def _printf_s_to_format_field(printf_string: str, format_field: str) -> str:
TEST_STRING = "PELICAN_PRINTF_S_DEPRECATION"
expected = printf_string % TEST_STRING
result = printf_string.replace("{", "{{").replace("}", "}}") % "{{{}}}".format(
format_field
)
result = printf_string.replace("{", "{{").replace("}", "}}") % f"{{{format_field}}}"
if result.format(**{format_field: TEST_STRING}) != expected:
raise ValueError(f"Failed to safely replace %s with {{{format_field}}}")
@ -411,7 +410,7 @@ def handle_deprecated_settings(settings: Settings) -> Settings:
)
logger.warning(message)
if old_values.get("SLUG"):
for f in {"CATEGORY", "TAG"}:
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", [])

View file

@ -1,6 +1,6 @@
from re import match
import tarfile
from pathlib import Path
from re import match
from zipfile import ZipFile
import pytest

View file

@ -261,9 +261,7 @@ class LoggedTestCase(unittest.TestCase):
self.assertEqual(
actual,
count,
msg="expected {} occurrences of {!r}, but found {}".format(
count, msg, actual
),
msg=f"expected {count} occurrences of {msg!r}, but found {actual}",
)

View file

@ -6,7 +6,6 @@ from unittest.mock import MagicMock
from pelican.generators import ArticlesGenerator, PagesGenerator
from pelican.tests.support import get_context, get_settings, unittest
CUR_DIR = os.path.dirname(__file__)
CONTENT_DIR = os.path.join(CUR_DIR, "content")

View file

@ -13,7 +13,6 @@ 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
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
@ -297,7 +296,6 @@ class TestPage(TestBase):
def test_signal(self):
def receiver_test_function(sender):
receiver_test_function.has_been_called = True
pass
receiver_test_function.has_been_called = False

View file

@ -13,11 +13,11 @@ from pelican.generators import (
TemplatePagesGenerator,
)
from pelican.tests.support import (
TestCaseWithCLocale,
can_symlink,
get_context,
get_settings,
unittest,
TestCaseWithCLocale,
)
from pelican.writers import Writer

View file

@ -5,11 +5,11 @@ from unittest.mock import patch
from pelican.settings import DEFAULT_CONFIG
from pelican.tests.support import (
TestCaseWithCLocale,
mute,
skipIfNoExecutable,
temporary_folder,
unittest,
TestCaseWithCLocale,
)
from pelican.tools.pelican_import import (
blogger2fields,
@ -19,12 +19,12 @@ from pelican.tools.pelican_import import (
download_attachments,
fields2pelican,
get_attachments,
tumblr2fields,
wp2fields,
medium_slug,
mediumpost2fields,
mediumposts2fields,
strip_medium_post_content,
medium_slug,
tumblr2fields,
wp2fields,
)
from pelican.utils import path_to_file_url, slugify
@ -41,7 +41,7 @@ WORDPRESS_DECODED_CONTENT_SAMPLE = os.path.join(
try:
from bs4 import BeautifulSoup
except ImportError:
BeautifulSoup = False # NOQA
BeautifulSoup = False
try:
import bs4.builder._lxml as LXML
@ -532,9 +532,7 @@ class TestWordpressXMLAttachements(TestCaseWithCLocale):
self.assertEqual(self.attachments[post], {expected_invalid})
else:
self.fail(
"all attachments should match to a " "filename or None, {}".format(
post
)
"all attachments should match to a " f"filename or None, {post}"
)
def test_download_attachments(self):

View file

@ -7,7 +7,6 @@ from pelican.paginator import Paginator
from pelican.settings import DEFAULT_CONFIG
from pelican.tests.support import get_settings, unittest
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)

View file

@ -1,7 +1,6 @@
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,
@ -9,6 +8,7 @@ from pelican.plugins._utils import (
plugin_enabled,
)
from pelican.plugins.signals import signal
from pelican.tests.dummy_plugins.normal_plugin import normal_plugin
from pelican.tests.support import unittest

View file

@ -5,7 +5,6 @@ from pelican import readers
from pelican.tests.support import get_settings, unittest
from pelican.utils import SafeDatetime
CUR_DIR = os.path.dirname(__file__)
CONTENT_PATH = os.path.join(CUR_DIR, "content")

View file

@ -3,7 +3,6 @@ import locale
import os
from os.path import abspath, dirname, join
from pelican.settings import (
DEFAULT_CONFIG,
DEFAULT_THEME,

View file

@ -23,9 +23,17 @@ from pelican.tests.support import (
from pelican.writers import Writer
class TestUtils(LoggedTestCase):
class ClassDeprAttr:
_new_attribute = "new_value"
@utils.deprecated_attribute(
old="_old_attribute", new="_new_attribute", since=(3, 1, 0), remove=(4, 1, 3)
)
def _old_attribute():
return None
class TestUtils(LoggedTestCase):
def setUp(self):
super().setUp()
self.temp_output = mkdtemp(prefix="pelicantests.")
@ -34,15 +42,10 @@ class TestUtils(LoggedTestCase):
super().tearDown()
shutil.rmtree(self.temp_output)
@utils.deprecated_attribute(
old="_old_attribute", new="_new_attribute", since=(3, 1, 0), remove=(4, 1, 3)
)
def _old_attribute():
return None
def test_deprecated_attribute(self):
value = self._old_attribute
self.assertEqual(value, self._new_attribute)
test_class = ClassDeprAttr()
value = test_class._old_attribute
self.assertEqual(value, test_class._new_attribute)
self.assertLogCountEqual(
count=1,
msg=(

View file

@ -22,7 +22,6 @@ from pelican.log import init
from pelican.settings import DEFAULT_CONFIG
from pelican.utils import SafeDatetime, slugify
logger = logging.getLogger(__name__)

View file

@ -169,7 +169,7 @@ def ask_timezone(question, default, tzurl):
r = tz_dict[r]
break
else:
print("Please enter a valid time zone:\n" " (check [{}])".format(tzurl))
print("Please enter a valid time zone:\n" f" (check [{tzurl}])")
return r
@ -205,14 +205,14 @@ def main():
args = parser.parse_args()
print(
"""Welcome to pelican-quickstart v{v}.
f"""Welcome to pelican-quickstart v{__version__}.
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__)
"""
)
project = os.path.join(os.environ.get("VIRTUAL_ENV", os.curdir), ".project")

View file

@ -240,15 +240,11 @@ def install(path, v=False, u=False):
except OSError as e:
err(
"Cannot change permissions of files "
"or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)),
f"or directory in `{theme_path}':\n{e!s}",
die=False,
)
except Exception as e:
err(
"Cannot copy `{p}' to `{t}':\n{e}".format(
p=path, t=theme_path, e=str(e)
)
)
err(f"Cannot copy `{path}' to `{theme_path}':\n{e!s}")
def symlink(path, v=False):
@ -268,11 +264,7 @@ def symlink(path, v=False):
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(f"Cannot link `{path}' to `{theme_path}':\n{e!s}")
def is_broken_link(path):

View file

@ -98,7 +98,7 @@ class URLWrapper:
return self.name
def __repr__(self):
return f"<{type(self).__name__} {repr(self._name)}>"
return f"<{type(self).__name__} {self._name!r}>"
def _from_settings(self, key, get_page_name=False):
"""Returns URL information as defined in settings.

View file

@ -23,14 +23,10 @@ from typing import (
Any,
Callable,
Collection,
Dict,
Generator,
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
Union,
)
@ -40,9 +36,8 @@ try:
from zoneinfo import ZoneInfo
except ModuleNotFoundError:
from backports.zoneinfo import ZoneInfo
from markupsafe import Markup
import watchfiles
from markupsafe import Markup
if TYPE_CHECKING:
from pelican.contents import Content
@ -158,7 +153,7 @@ class memoized:
def __init__(self, func: Callable) -> None:
self.func = func
self.cache: Dict[Any, Any] = {}
self.cache: dict[Any, Any] = {}
def __call__(self, *args) -> Any:
if not isinstance(args, Hashable):
@ -185,8 +180,8 @@ class memoized:
def deprecated_attribute(
old: str,
new: str,
since: Tuple[int, ...],
remove: Optional[Tuple[int, ...]] = None,
since: tuple[int, ...],
remove: Optional[tuple[int, ...]] = None,
doc: Optional[str] = None,
):
"""Attribute deprecation decorator for gentle upgrades
@ -256,7 +251,7 @@ def pelican_open(
def slugify(
value: str,
regex_subs: Iterable[Tuple[str, str]] = (),
regex_subs: Iterable[tuple[str, str]] = (),
preserve_case: bool = False,
use_unicode: bool = False,
) -> str:
@ -642,9 +637,9 @@ def truncate_html_words(s: str, num: int, end_text: str = "…") -> str:
def process_translations(
content_list: List[Content],
content_list: list[Content],
translation_id: Optional[Union[str, Collection[str]]] = None,
) -> Tuple[List[Content], List[Content]]:
) -> tuple[list[Content], list[Content]]:
"""Finds translations and returns them.
For each content_list item, populates the 'translations' attribute, and
@ -674,14 +669,14 @@ def process_translations(
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)
f"Cannot unpack {translation_id}, 'translation_id' must be falsy, a"
" string or a collection of strings"
)
except AttributeError:
raise AttributeError(
"Cannot use {} as 'translation_id', there "
f"Cannot use {translation_id} as 'translation_id', there "
"appear to be items without these metadata "
"attributes".format(translation_id)
"attributes"
)
for id_vals, items in groupby(content_list, attrgetter(*translation_id)):
@ -702,7 +697,7 @@ def process_translations(
return index, translations
def get_original_items(items: List[Content], with_str: str) -> List[Content]:
def get_original_items(items: list[Content], with_str: str) -> list[Content]:
def _warn_source_paths(msg, items, *extra):
args = [len(items)]
args.extend(extra)
@ -743,9 +738,9 @@ def get_original_items(items: List[Content], with_str: str) -> List[Content]:
def order_content(
content_list: List[Content],
content_list: list[Content],
order_by: Union[str, Callable[[Content], Any], None] = "slug",
) -> List[Content]:
) -> list[Content]:
"""Sorts content.
order_by can be a string of an attribute or sorting function. If order_by
@ -807,8 +802,8 @@ def order_content(
def wait_for_changes(
settings_file: str,
reader_class: Type["Readers"],
settings: "Settings",
reader_class: type[Readers],
settings: Settings,
):
content_path = settings.get("PATH", "")
theme_path = settings.get("THEME", "")

View file

@ -4,7 +4,6 @@ from posixpath import join as posix_join
from urllib.parse import urljoin
from feedgenerator import Atom1Feed, Rss201rev2Feed, get_tag_uri
from markupsafe import Markup
from pelican.paginator import Paginator
@ -53,6 +52,9 @@ class Writer:
title = Markup(item.title).striptags()
link = self.urljoiner(self.site_url, item.url)
if self.settings["FEED_APPEND_REF"]:
link = link + "?ref=feed"
if isinstance(feed, Rss201rev2Feed):
# RSS feeds use a single tag called 'description' for both the full
# content and the summary

View file

@ -111,3 +111,102 @@ source-includes = [
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.ruff.lint]
# see https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml
# "F" contains autoflake, see https://github.com/astral-sh/ruff/issues/1647
# add more rules
select = [
# default Ruff checkers as of ruff 0.1.3: E4, E7, E9, F
"E4",
"E7",
"E9",
"F", # pyflakes
# the rest in alphabetical order:
# TODO: "A", # flake8-builtins
# TODO: "ARG", # flake8-unused-arguments
"B", # flake8-bugbear
# TODO: "BLE", # flake8-blind-except
# TODO: Do I want "COM", # flake8-commas
"C4", # flake8-comprehensions
# TODO: "DJ", # flake8-django
# TODO: "DTZ", # flake8-datetimez
# TODO: "EM", # flake8-errmsg
"EXE", # flake8-executable
# TODO: "FURB", # refurb
# TODO: "FBT", # flake8-boolean-trap
# TODO: "G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"INP", # flake8-no-pep420
# TODO: "INT", # flake8-gettext
"ISC", # flake8-implicit-str-concat
# TODO: "LOG", # flake8-logging
"PERF", # perflint
"PIE", # flake8-pie
"PL", # pylint
"PYI", # flake8-pyi
# TODO: "RET", # flake8-return
"RSE", # flake8-raise
"RUF",
# TODO: "SIM", # flake8-simplify
"SLF", # flake8-self
"SLOT", # flake8-slots
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"Q", # flake8-quotes
"TCH", # flake8-type-checking
"T10", # flake8-debugger
"T20", # flake8-print
# TODO: "S", # flake8-bandit
"YTT", # flake8-2020
# TODO: add more flake8 rules
]
ignore = [
# suppression in order of # of violations in Dec 2023:
"B007", # unused-loop-control-variable
"T201", # print
"PLW2901", # redefined-loop-name
"SLF001", # private-member-access
"RUF001", # ambiguous-unicode-character-string
"PLR2004", # magic-value-comparison
"PLR0912", # too-many-branches
"PLR0913", # too-many-arguments
"RUF005", # collection-literal-concatenation
"RUF012", # mutable-class-default
"PLR0915", # too-many-statements
"INP001", # implicit-namespace-package
"RUF015", # unnecessary-iterable-allocation-for-first-element
"PLR1722", # sys-exit-alias
"ISC001", # single-line-implicit-string-concatenation
"C408", # unnecessary-collection-call
"B904", # raise-without-from-inside-except
"UP007", # use `|` operator for union type annotations (PEP 604)
"UP031", # printf-string-formatting
"PLR5501", # collapsible-else-if
"PERF203", # try-except-in-loop
"B006", # mutable-argument-default
"PLR1714", # repeated-equality-comparison
"PERF401", # manual-list-comprehension
# TODO: these only have one violation each in Dec 2023:
"SLOT000", # no-slots-in-str-subclass
"PYI024", # collections-named-tuple
"PLW0603", # global-statement
"PIE800", # unnecessary-spread
"ISC003", # explicit-string-concatenation
"EXE002", # shebang-missing-executable-file
"C401", # unnecessary-generator-set
"C416", # unnecessary `list` comprehension
"B028", # no-explicit-stacklevel
"B008", # function-call-in-default-argument
]
[tool.ruff.lint.extend-per-file-ignores]
"pelican/__init__.py" = [
# allow imports after a call to a function, see the file
"E402"
]