diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f58166c..8998a93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ repos: -- repo: https://github.com/ambv/black - rev: 21.12b0 + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.9 hooks: - - id: black -- repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 - hooks: - - id: reorder-python-imports + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df6f7c3..ba28798 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,15 +2,21 @@ ## Development environment -Create a development environment with `Python>=3.6`. +Create a development environment with `Python>=3.8`. + +Please do not create a venv directly in the project directory. +else pytest will find too many tests. You can then install the development and test dependencies with: ```bash -python -m pip install lektor -python -m pip install --editable .[test] +python -m pip install --group dev --editable . ``` +[!WARNING] +This example only works with pip in version 25.1 or newer. +Version 25.1 has a planned release date of 2025-04-30 + ## Tests To run the test suite, we use `pytest`: diff --git a/Makefile b/Makefile index b3a5af4..a6ce735 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,16 @@ -test: - tox +.DEFAULT_GOAL := help + +test-python: ## Run tests on Python files. + @echo "---> running python tests" + tox p + +.PHONY: lint +lint: ## Lint code. + pre-commit run -a + +.PHONY: test +test: lint test-python + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/lektor_atom.py b/lektor_atom.py index 3c45df5..a0eeb30 100644 --- a/lektor_atom.py +++ b/lektor_atom.py @@ -1,7 +1,7 @@ -import hashlib -import uuid from datetime import date from datetime import datetime +import hashlib +import uuid import click from feedgenerator.django.utils.feedgenerator import Atom1Feed @@ -67,7 +67,7 @@ def get_item_title(item, field): def get_item_body(item, field): if field not in item: - raise RuntimeError("Body field %r not found in %r" % (field, item)) + raise RuntimeError(f"Body field {field!r} not found in {item!r}") with get_ctx().changed_base_url(item.url_path): return str(escape(item[field])) @@ -132,14 +132,16 @@ def build_artifact(self, artifact): content=get_item_body(item, feed_source.item_body_field), link=url_to(item, external=True), unique_id=get_id( - "%s/%s" % (ctx.env.project.id, item["_path"].encode("utf-8")) + "{}/{}".format( + ctx.env.project.id, item["_path"].encode("utf-8") + ) ), author_name=item_author, updateddate=get_item_updated(item, feed_source.item_date_field), ) except Exception as exc: - msg = "%s: %s" % (item["_id"], exc) + msg = "{}: {}".format(item["_id"], exc) click.echo(click.style("E", fg="red") + " " + msg) with artifact.open("wb") as f: @@ -168,7 +170,7 @@ class AtomPlugin(Plugin): def get_atom_config(self, feed_id, key): default_value = self.defaults[key] - return self.get_config().get("%s.%s" % (feed_id, key), default_value) + return self.get_config().get(f"{feed_id}.{key}", default_value) def on_setup_env(self, **extra): self.env.add_build_program(AtomFeedSource, AtomFeedBuilderProgram) diff --git a/pyproject.toml b/pyproject.toml index 0846fc7..bf2b5c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,84 @@ [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.13.0,<2.0.0", "hatch-vcs"] +build-backend = "hatchling.build" -[tool.setuptools_scm] +[project] +name = "lektor-atom" +description = "Lektor plugin that generates Atom feeds." +keywords = ["Lektor", "plugin", "static-site", "blog", "atom", "rss"] +version = "0.4.0" +readme = "README.md" +authors = [ + {name = "A. Jesse Jiryu Davis", email = "jesse@emptysquare.net"}, +] +requires-python = ">=3.8" +license-expression = "MIT" +classifiers = [ + "Environment :: Plugins", + "Environment :: Web Environment", + "Framework :: Lektor", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "MarkupSafe", + "feedgenerator", + "tox>=4.24.1", +] + +[dependency-groups] +dev = [ + "pytest", + "ruff", + "lektor", + "lxml" + + ] +[project.urls] +Homepage = "https://github.com/lektor/lektor-atom" + +[project.entry-points."lektor.plugins"] +atom = "lektor_atom:AtomPlugin" + +[tool.hatch.build.targets.sdist] +exclude = [ + ".github", + ] + +################################################################ +# +# pylint +# +[tool.pylint.main] +ignore = [".git"] +load-plugins = "pylint.extensions.no_self_use" +extension-pkg-allow-list = "lxml" + +[tool.pylint.format] +max-line-length = 91 +max-module-lines = 2000 + +[tool.pylint."messages control"] +disable = [ + "missing-docstring", +] + + +[tool.tox] +requires = ["tox>=4.22"] +env_list = [ + "3.14", + "3.13", + "3.12", + "3.11", + "3.10", + "3.9", + "3.8", +] + +[tool.tox.env_run_base] +description = "Run test under {base_python}" +dependency_groups = [ + "dev", +] +commands = [["pytest"]] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6d73107..0000000 --- a/setup.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[metadata] -name = lektor-atom -author = A. Jesse Jiryu Davis -author_email = jesse@emptysquare.net -description = Lektor plugin that generates Atom feeds. -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/lektor/lektor-atom -keywords = Lektor, plugin, static-site, blog, atom, rss -license = MIT -classifiers = - Environment :: Plugins - Environment :: Web Environment - Framework :: Lektor - Intended Audience :: Developers - License :: OSI Approved :: MIT License - -[options] -install_requires= - MarkupSafe - feedgenerator -py_modules = lektor_atom -setup_requires = - setuptools >= 45 - setuptools_scm >= 6 - -[options.entry_points] -lektor.plugins = - atom = lektor_atom:AtomPlugin diff --git a/tests/conftest.py b/tests/conftest.py index 9e7ed01..a6d329e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,15 @@ +from datetime import datetime import os import shutil import tempfile -from datetime import datetime -import pytest +from lektor import db +from lektor.builder import Builder +from lektor.environment import Environment +from lektor.project import Project +from lektor.reporter import BufferReporter from lektor.types import Type +import pytest class DatetimeType(Type): @@ -14,15 +19,11 @@ def value_from_raw(self, raw): @pytest.fixture(scope="function") def project(request): - from lektor.project import Project - return Project.from_path(os.path.join(os.path.dirname(__file__), "demo-project")) @pytest.fixture(scope="function") def env(request, project): - from lektor.environment import Environment - e = Environment(project) e.types["datetime"] = DatetimeType # As if we had a datetime plugin. return e @@ -30,21 +31,17 @@ def env(request, project): @pytest.fixture(scope="function") def pad(request, env): - from lektor.db import Database - - return Database(env).new_pad() + return db.Database(env).new_pad() def make_builder(request, pad): - from lektor.builder import Builder - out = tempfile.mkdtemp() b = Builder(pad, out) def cleanup(): try: shutil.rmtree(out) - except (OSError, IOError): + except OSError: pass request.addfinalizer(cleanup) @@ -57,16 +54,12 @@ def builder(request, pad): @pytest.fixture(scope="function") -def F(): - from lektor.db import F - - return F +def F(): # pylint: disable=invalid-name + return db.F @pytest.fixture(scope="function") def reporter(request, env): - from lektor.reporter import BufferReporter - r = BufferReporter(env) r.push() request.addfinalizer(r.pop) diff --git a/tests/test_lektor_atom.py b/tests/test_lektor_atom.py index 9eed303..a9d0017 100644 --- a/tests/test_lektor_atom.py +++ b/tests/test_lektor_atom.py @@ -9,11 +9,12 @@ from urlparse import urljoin -def test_typical_feed(pad, builder): +def test_typical_feed(builder): failures = builder.build_all() assert not failures feed_path = os.path.join(builder.destination_path, "typical-blog/feed.xml") - feed = objectify.parse(open(feed_path)).getroot() + with open(feed_path, encoding="utf-8") as feed_stream: + feed = objectify.parse(feed_stream).getroot() assert "Feed One" == feed.title assert "My Summary" == feed.subtitle @@ -51,11 +52,12 @@ def test_typical_feed(pad, builder): assert "A. Jesse Jiryu Davis" == post1.author.name -def test_custom_feed(pad, builder): +def test_custom_feed(builder): failures = builder.build_all() assert not failures feed_path = os.path.join(builder.destination_path, "custom-blog/atom.xml") - feed = objectify.parse(open(feed_path)).getroot() + with open(feed_path, encoding="utf-8") as feed_stream: + feed = objectify.parse(feed_stream).getroot() assert "Feed Three" == feed.title assert "
My Description
" == str(feed.subtitle).strip() @@ -92,7 +94,7 @@ def test_custom_feed(pad, builder): assert "A. Jesse Jiryu Davis" == post1.author.name -def test_virtual_resolver(pad, builder): +def test_virtual_resolver(pad): # Pass a virtual source path to url_to(). feed_path = "/typical-blog@atom/feed-one" url_path = pad.get("typical-blog/post1").url_to(feed_path) @@ -117,15 +119,13 @@ def test_dependencies(pad, builder, reporter): reporter.clear() builder.build(pad.get("typical-blog@atom/feed-one")) - assert set(reporter.get_recorded_dependencies()) == set( - [ - "Website.lektorproject", - "content/typical-blog", - "content/typical-blog/contents.lr", - "content/typical-blog/post1/contents.lr", - "content/typical-blog/post2/contents.lr", - "models/blog.ini", - "models/blog-post.ini", - "configs/atom.ini", - ] - ) + assert set(reporter.get_recorded_dependencies()) == { + "Website.lektorproject", + "content/typical-blog", + "content/typical-blog/contents.lr", + "content/typical-blog/post1/contents.lr", + "content/typical-blog/post2/contents.lr", + "models/blog.ini", + "models/blog-post.ini", + "configs/atom.ini", + } diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 829f031..0000000 --- a/tox.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -minversion = 3 -envlist = py,build-dist -isolated_build = True - -[testenv] -commands = pytest {posargs:tests} -deps = - lxml - lektor - pytest - -[testenv:build-dist] -skip_install = True -deps = - build - twine -commands = - python -m build . - twine check dist/*