diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 716ddd89..38287df9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,14 +16,14 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# ruff version should match the one in pyproject.toml
- rev: v0.7.2
+ rev: v0.12.2
hooks:
- - id: ruff
+ - id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/rtts/djhtml
- rev: '3.0.7'
+ rev: '3.0.8'
hooks:
- id: djhtml
- id: djcss
diff --git a/docs/conf.py b/docs/conf.py
index 4c15bc62..e335e73c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -41,7 +41,7 @@ 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]}
+.. |min_python| replace:: {project_data.get("requires-python").split(",")[0]}
"""
extlinks = {"pelican-doc": ("https://docs.getpelican.com/en/latest/%s.html", "%s")}
diff --git a/pelican/__init__.py b/pelican/__init__.py
index 89a63e84..0e92def9 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -402,8 +402,7 @@ def parse_arguments(argv=None):
"--autoreload",
dest="autoreload",
action="store_true",
- help="Relaunch pelican each time a modification occurs"
- " on the content files.",
+ help="Relaunch pelican each time a modification occurs on the content files.",
)
parser.add_argument(
@@ -446,8 +445,7 @@ def parse_arguments(argv=None):
choices=("errors", "warnings"),
default="",
help=(
- "Exit the program with non-zero status if any "
- "errors/warnings encountered."
+ "Exit the program with non-zero status if any errors/warnings encountered."
),
)
diff --git a/pelican/cache.py b/pelican/cache.py
index d1f8550e..8bd34268 100644
--- a/pelican/cache.py
+++ b/pelican/cache.py
@@ -1,3 +1,4 @@
+import gzip
import hashlib
import logging
import os
@@ -22,8 +23,6 @@ class FileDataCacher:
self._cache_path = os.path.join(self.settings["CACHE_PATH"], cache_name)
self._cache_data_policy = caching_policy
if self.settings["GZIP_CACHE"]:
- import gzip
-
self._cache_open = gzip.open
else:
self._cache_open = open
diff --git a/pelican/contents.py b/pelican/contents.py
index 1ce61b43..daf72ce6 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -342,8 +342,7 @@ class Content:
value.geturl(),
extra={
"limit_msg": (
- "Other resources were not found "
- "and their urls not replaced"
+ "Other resources were not found and their urls not replaced"
)
},
)
diff --git a/pelican/log.py b/pelican/log.py
index edf2f182..27478d14 100644
--- a/pelican/log.py
+++ b/pelican/log.py
@@ -1,4 +1,5 @@
import logging
+import warnings
from collections import defaultdict
from rich.console import Console
@@ -156,8 +157,6 @@ def init(
def log_warnings():
- import warnings
-
logging.captureWarnings(True)
warnings.simplefilter("default", DeprecationWarning)
init(logging.DEBUG, name="py.warnings")
diff --git a/pelican/paginator.py b/pelican/paginator.py
index 4a7c1aa2..9dcbb9d7 100644
--- a/pelican/paginator.py
+++ b/pelican/paginator.py
@@ -53,7 +53,7 @@ class Paginator:
"Returns the total number of pages."
if self._num_pages is None:
hits = max(1, self.count - self.orphans)
- self._num_pages = int(ceil(hits / (float(self.per_page) or 1)))
+ self._num_pages = ceil(hits / (float(self.per_page) or 1))
return self._num_pages
num_pages = property(_get_num_pages)
diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py
index 9dfc8f81..e21201a7 100644
--- a/pelican/plugins/_utils.py
+++ b/pelican/plugins/_utils.py
@@ -19,7 +19,7 @@ def iter_namespace(ns_pkg):
def get_namespace_plugins(ns_pkg=None):
if ns_pkg is None:
- import pelican.plugins as ns_pkg
+ import pelican.plugins as ns_pkg # noqa: PLC0415
return {
name: importlib.import_module(name)
@@ -29,7 +29,7 @@ def get_namespace_plugins(ns_pkg=None):
def list_plugins(ns_pkg=None):
- from pelican.log import init as init_logging
+ from pelican.log import init as init_logging # noqa: PLC0415
init_logging(logging.INFO)
ns_plugins = get_namespace_plugins(ns_pkg)
diff --git a/pelican/readers.py b/pelican/readers.py
index 182194fe..fc0a4f40 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -630,8 +630,9 @@ class Readers(FileStampDataCacher):
# eventually filter the content with typogrify if asked so
if self.settings["TYPOGRIFY"]:
- import smartypants
- from typogrify.filters import typogrify
+ # typogrify is an optional feature, user may not have it installed
+ import smartypants # noqa: PLC0415
+ from typogrify.filters import typogrify # noqa: PLC0415
typogrify_dashes = self.settings["TYPOGRIFY_DASHES"]
if typogrify_dashes == "oldschool":
@@ -657,7 +658,7 @@ class Readers(FileStampDataCacher):
return typogrify(
text,
self.settings["TYPOGRIFY_IGNORE_TAGS"],
- **{f: False for f in self.settings["TYPOGRIFY_OMIT_FILTERS"]},
+ **dict.fromkeys(self.settings["TYPOGRIFY_OMIT_FILTERS"], False),
)
except TypeError:
try:
diff --git a/pelican/settings.py b/pelican/settings.py
index 2932627e..2f3f8fa1 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -12,6 +12,7 @@ from types import ModuleType
from typing import Any, Optional
from pelican.log import LimitFilter
+from pelican.paginator import PaginationRule
def load_source(name: str, path: str) -> ModuleType:
@@ -320,8 +321,7 @@ def handle_deprecated_settings(settings: Settings) -> Settings:
# EXTRA_TEMPLATES_PATHS -> THEME_TEMPLATES_OVERRIDES
if "EXTRA_TEMPLATES_PATHS" in settings:
logger.warning(
- "EXTRA_TEMPLATES_PATHS is deprecated use "
- "THEME_TEMPLATES_OVERRIDES instead."
+ "EXTRA_TEMPLATES_PATHS is deprecated use THEME_TEMPLATES_OVERRIDES instead."
)
if settings.get("THEME_TEMPLATES_OVERRIDES"):
raise Exception(
@@ -453,8 +453,7 @@ def handle_deprecated_settings(settings: Settings) -> Settings:
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.",
+ "Failed to convert %%s to {lang} for %s. Falling back to default.",
key,
)
settings[key] = DEFAULT_CONFIG[key]
@@ -476,8 +475,7 @@ def handle_deprecated_settings(settings: Settings) -> Settings:
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.",
+ "Failed to convert %%s to {slug} for %s. Falling back to default.",
key,
)
settings[key] = DEFAULT_CONFIG[key]
@@ -689,8 +687,6 @@ def configure_settings(settings: Settings) -> Settings:
)
# fix up pagination rules
- from pelican.paginator import PaginationRule
-
pagination_rules = [
PaginationRule(*r)
for r in settings.get(
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
index 97653e33..3c3aae81 100644
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -7,7 +7,7 @@ from sys import platform
from jinja2.utils import generate_lorem_ipsum
-from pelican.contents import Article, Author, Category, Page, Static
+from pelican.contents import Article, Author, Category, Page, Static, logger
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
@@ -49,24 +49,18 @@ class TestBase(LoggedTestCase):
self._enable_limit_filter()
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):
- # make a deep copy of page_kwargs
- page_kwargs = {key: self.page_kwargs[key] for key in self.page_kwargs}
- for key in page_kwargs:
- if not isinstance(page_kwargs[key], dict):
+ # copy page_kwargs
+ page_kwargs = dict(self.page_kwargs)
+ for key, val in page_kwargs.items():
+ if not isinstance(val, dict):
break
- page_kwargs[key] = {
- subkey: page_kwargs[key][subkey] for subkey in page_kwargs[key]
- }
+ page_kwargs[key] = {subkey: val[subkey] for subkey in val}
return page_kwargs
@@ -310,18 +304,16 @@ class TestPage(TestBase):
# I doubt this can work on all platforms ...
if platform == "win32":
- locale = "jpn"
+ the_locale = "jpn"
else:
- locale = "ja_JP.utf8"
- page_kwargs["settings"]["DATE_FORMATS"] = {"jp": (locale, "%Y-%m-%d(%a)")}
+ the_locale = "ja_JP.utf8"
+ page_kwargs["settings"]["DATE_FORMATS"] = {"jp": (the_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)")
- except locale_module.Error:
+ except locale.Error:
# The constructor of ``Page`` will try to set the locale to
# ``ja_JP.utf8``. But this attempt will failed when there is no
# such locale in the system. You can see which locales there are
@@ -329,7 +321,7 @@ class TestPage(TestBase):
#
# Until we find some other method to test this functionality, we
# will simply skip this test.
- unittest.skip(f"There is no locale {locale} in this system.")
+ unittest.skip(f"There is no locale {the_locale} in this system.")
def test_template(self):
# Pages default to page, metadata overwrites
@@ -406,8 +398,7 @@ class TestPage(TestBase):
# fragment
args["content"] = (
- "A simple test, with a "
- 'link'
+ 'A simple test, with a link'
)
content = Page(**args).get_content("http://notmyidea.org")
self.assertEqual(
@@ -687,8 +678,7 @@ class TestPage(TestBase):
}
args["content"] = (
- "A simple test, with a link to a"
- 'poster'
+ 'A simple test, with a link to aposter'
)
content = Page(**args).get_content("http://notmyidea.org")
self.assertEqual(
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 01add63c..6e9f2be8 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -916,10 +916,7 @@ class TestArticlesGenerator(unittest.TestCase):
"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 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!",
diff --git a/pelican/tests/test_paginator.py b/pelican/tests/test_paginator.py
index 6a7dbe02..2f98231d 100644
--- a/pelican/tests/test_paginator.py
+++ b/pelican/tests/test_paginator.py
@@ -3,7 +3,7 @@ import locale
from jinja2.utils import generate_lorem_ipsum
from pelican.contents import Article, Author
-from pelican.paginator import Paginator
+from pelican.paginator import PaginationRule, Paginator
from pelican.settings import DEFAULT_CONFIG
from pelican.tests.support import get_settings, unittest
@@ -35,8 +35,6 @@ class TestPage(unittest.TestCase):
def test_save_as_preservation(self):
settings = get_settings()
# fix up pagination rules
- from pelican.paginator import PaginationRule
-
pagination_rules = [
PaginationRule(*r)
for r in settings.get(
@@ -56,8 +54,6 @@ class TestPage(unittest.TestCase):
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)
@@ -81,8 +77,6 @@ class TestPage(unittest.TestCase):
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)
diff --git a/pelican/tests/test_plugins.py b/pelican/tests/test_plugins.py
index 42bb2ca6..f67c5fe4 100644
--- a/pelican/tests/test_plugins.py
+++ b/pelican/tests/test_plugins.py
@@ -23,7 +23,7 @@ def tmp_namespace_path(path):
"""
# This avoids calls to internal `pelican.plugins.__path__._recalculate()`
# as it should not be necessary
- import pelican
+ import pelican # noqa: PLC0415
old_path = pelican.__path__[:]
try:
@@ -41,8 +41,8 @@ class PluginTest(unittest.TestCase):
_NORMAL_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, "normal_plugin")
def test_namespace_path_modification(self):
- import pelican
- import pelican.plugins
+ import pelican # noqa: PLC0415
+ import pelican.plugins # noqa: PLC0415
old_path = pelican.__path__[:]
diff --git a/pelican/tests/test_rstdirectives.py b/pelican/tests/test_rstdirectives.py
index 46ed6f49..dfe55961 100644
--- a/pelican/tests/test_rstdirectives.py
+++ b/pelican/tests/test_rstdirectives.py
@@ -1,12 +1,11 @@
from unittest.mock import Mock
+from pelican.rstdirectives import abbr_role
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")
diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py
index 8bcae51f..d0f2b3c2 100755
--- a/pelican/tools/pelican_import.py
+++ b/pelican/tools/pelican_import.py
@@ -2,6 +2,7 @@
import argparse
import datetime
+import json
import logging
import os
import re
@@ -9,6 +10,7 @@ import subprocess
import sys
import tempfile
import time
+import urllib.request as urllib_request
from collections import defaultdict
from html import unescape
from urllib.error import URLError
@@ -16,6 +18,7 @@ from urllib.parse import quote, urlparse, urlsplit, urlunsplit
from urllib.request import urlretrieve
import dateutil.parser
+from docutils.utils import column_width
# because logging.setLoggerClass has to be called before logging.getLogger
from pelican.log import init
@@ -118,7 +121,7 @@ def decode_wp_content(content, br=True):
def _import_bs4():
"""Import and return bs4, otherwise sys.exit."""
try:
- import bs4
+ import bs4 # noqa: PLC0415
except ImportError:
error = (
'Missing dependency "BeautifulSoup4" and "lxml" required to '
@@ -272,7 +275,7 @@ def blogger2fields(xml):
def dc2fields(file):
"""Opens a Dotclear export file, and yield pelican fields"""
try:
- from bs4 import BeautifulSoup
+ from bs4 import BeautifulSoup # noqa: PLC0415
except ImportError:
error = (
"Missing dependency "
@@ -311,7 +314,7 @@ def dc2fields(file):
else:
posts.append(line)
- print("%i posts read." % len(posts))
+ print(f"{len(posts)} posts read.")
subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"]
for post in posts:
@@ -367,7 +370,7 @@ def dc2fields(file):
.replace("a:0:", "")
)
if len(tag) > 1:
- if int(len(tag[:1])) == 1:
+ if len(tag[:1]) == 1:
newtag = tag.split('"')[1]
tags.append(
BeautifulSoup(newtag, "xml")
@@ -418,13 +421,10 @@ def dc2fields(file):
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)
+ f"https://api.tumblr.com/v2/blog/{blogname}.tumblr.com/"
+ f"posts?api_key={api_key}&offset={offset}&filter=raw"
+ )
request = urllib_request.Request(url)
handle = urllib_request.urlopen(request)
posts = json.loads(handle.read().decode("utf-8"))
@@ -673,7 +673,7 @@ def mediumposts2fields(medium_export_dir: str):
def feed2fields(file):
"""Read a feed and yield pelican fields"""
- import feedparser
+ import feedparser # noqa: PLC0415
d = feedparser.parse(file)
subs = DEFAULT_CONFIG["SLUG_REGEX_SUBSTITUTIONS"]
@@ -707,8 +707,6 @@ def build_header(
):
"""Build a header from a list of fields"""
- from docutils.utils import column_width
-
header = "{}\n{}\n".format(title, "#" * column_width(title))
if date:
header += f":date: {date}\n"
@@ -971,10 +969,10 @@ def fields2pelican(
if is_pandoc_needed(in_markup) and not pandoc_version:
posts_require_pandoc.append(filename)
- slug = not disable_slugs and filename or None
- assert slug is None or filename == os.path.basename(
- filename
- ), f"filename is not a basename: {filename}"
+ slug = (not disable_slugs and filename) or None
+ assert slug is None or filename == os.path.basename(filename), (
+ f"filename is not a basename: {filename}"
+ )
if wp_attach and attachments:
try:
@@ -1047,8 +1045,7 @@ def fields2pelican(
"--wrap=none" if pandoc_version >= (1, 16) else "--no-wrap"
)
cmd = (
- "pandoc --normalize {0} --from=html"
- ' --to={1} {2} -o "{3}" "{4}"'
+ 'pandoc --normalize {0} --from=html --to={1} {2} -o "{3}" "{4}"'
)
cmd = cmd.format(
parse_raw,
@@ -1070,7 +1067,7 @@ def fields2pelican(
try:
rc = subprocess.call(cmd, shell=True)
if rc < 0:
- error = "Child was terminated by signal %d" % -rc
+ error = f"Child was terminated by signal {-rc}"
sys.exit(error)
elif rc > 0:
diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py
index adf86dc0..1ae707e3 100755
--- a/pelican/tools/pelican_themes.py
+++ b/pelican/tools/pelican_themes.py
@@ -17,8 +17,7 @@ try:
import pelican
except ImportError:
err(
- "Cannot import pelican.\nYou must "
- "install Pelican in order to run this script.",
+ "Cannot import pelican.\nYou must install Pelican in order to run this script.",
-1,
)
diff --git a/pelican/utils.py b/pelican/utils.py
index 75889a7b..5531d2fb 100644
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -10,6 +10,7 @@ import re
import shutil
import sys
import traceback
+import unicodedata
import urllib
from collections.abc import Collection, Generator, Hashable, Iterable, Sequence
from contextlib import contextmanager
@@ -25,6 +26,7 @@ from typing import (
)
import dateutil.parser
+import unidecode
from watchfiles import Change
try:
@@ -260,10 +262,6 @@ def slugify(
look into pelican.settings.DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS'].
"""
- import unicodedata
-
- import unidecode
-
def normalize_unicode(text: str) -> str:
# normalize text by compatibility composition
# see: https://en.wikipedia.org/wiki/Unicode_equivalence
@@ -796,8 +794,7 @@ def order_content(
content.get_relative_source_path(),
extra={
"limit_msg": (
- "More files are missing "
- "the needed attribute."
+ "More files are missing the needed attribute."
)
},
)
diff --git a/pelican/writers.py b/pelican/writers.py
index 1a2cf7b0..1a99532a 100644
--- a/pelican/writers.py
+++ b/pelican/writers.py
@@ -261,8 +261,7 @@ class Writer:
# generated pages, and write
for page_num in range(next(iter(paginators.values())).num_pages):
paginated_kwargs = kwargs.copy()
- for key in paginators.keys():
- paginator = paginators[key]
+ for key, paginator in paginators.items():
previous_page = paginator.page(page_num) if page_num > 0 else None
page = paginator.page(page_num + 1)
next_page = (
diff --git a/pyproject.toml b/pyproject.toml
index 7af9366c..e12a48eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -96,7 +96,7 @@ dev = [
"tox>=4.11.3",
"invoke>=2.2.0",
# ruff version should match the one in .pre-commit-config.yaml
- "ruff==0.7.2",
+ "ruff==0.12.2",
"tomli>=2.0.1; python_version < \"3.11\"",
]
@@ -112,7 +112,6 @@ source-includes = [
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
diff --git a/tasks.py b/tasks.py
index f5a02a35..a879d2bd 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,10 +3,11 @@ from pathlib import Path
from shutil import which
from invoke import task
+from livereload import Server
PKG_NAME = "pelican"
PKG_PATH = Path(PKG_NAME)
-DOCS_PORT = os.environ.get("DOCS_PORT", 8000)
+DOCS_PORT = int(os.environ.get("DOCS_PORT", "8000"))
BIN_DIR = "bin" if os.name != "nt" else "Scripts"
PTY = os.name != "nt"
ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None)
@@ -29,8 +30,6 @@ def docbuild(c):
@task(docbuild)
def docserve(c):
"""Serve docs at http://localhost:$DOCS_PORT/ (default port is 8000)"""
- from livereload import Server
-
server = Server()
server.watch("docs/conf.py", lambda: docbuild(c))
server.watch("CONTRIBUTING.rst", lambda: docbuild(c))