diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15aabbb..224a481 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,20 +28,18 @@ jobs: strategy: matrix: lint-command: - - bandit -r . -x ./tests - black --check --diff . - - flake8 . - - isort --check-only --diff . - - pydocstyle . + - ruff check --output-format=github . - djlint emark --reformat steps: - uses: actions/checkout@v4 + - run: sudo apt install -y gettext - uses: actions/setup-python@v5 with: python-version: "3.x" cache: 'pip' - cache-dependency-path: 'linter-requirements.txt' - - run: python -m pip install -r linter-requirements.txt + cache-dependency-path: 'pyproject.toml' + - run: python -m pip install -e .[lint] - run: ${{ matrix.lint-command }} pytest: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1046977 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: name-tests-test + args: ['--pytest-test-first'] + - id: no-commit-to-branch + args: [--branch, main] + - id: detect-private-key + - repo: local + hooks: + - id: ruff + name: ruff + language: system + entry: ruff check + types: [python] + args: [--fix, --exit-non-zero-on-fix] + + - id: black + name: black + language: system + entry: black + types: [python] + + - id: djlint + name: djlint + language: system + entry: djlint + types: [ html ] + args: [ --reformat, --profile, "django" ] diff --git a/emark/backends.py b/emark/backends.py index 9e549ed..f762d49 100644 --- a/emark/backends.py +++ b/emark/backends.py @@ -24,7 +24,7 @@ def send_messages(self, email_messages): class TrackingEmailBackendMixin: - """Add tracking framework to an email backend.""" + """Add a tracking framework to an email backend.""" def send_messages(self, email_messages): self._messages_sent = [] @@ -83,7 +83,7 @@ def write_message(self, message): msg.get_charset().get_output_charset() if msg.get_charset() else "utf-8" ) msg_data = msg_data.decode(charset) - self.stream.write("%s\n" % msg_data) + self.stream.write(f"{msg_data}\n") self.stream.write("-" * 79) self.stream.write("\n") if payload_count > 1: @@ -119,8 +119,7 @@ def write_message(self, message): class TrackingSMTPEmailBackend(TrackingEmailBackendMixin, _SMTPEmailBackend): - """ - Like the SMTP email backend but with click and open tracking. + """Like the SMTP email backend but with click and open tracking. Furthermore, all emails are sent to a single email address. If multiple to, cc, or bcc addresses are specified, a separate diff --git a/emark/message.py b/emark/message.py index 2220205..3ca2329 100644 --- a/emark/message.py +++ b/emark/message.py @@ -25,8 +25,7 @@ class MarkdownEmail(EmailMultiAlternatives): - """ - Multipart email message that renders both plaintext and HTML from markdown. + """Multipart email message that renders both plaintext and HTML from markdown. This is a full class:`EmailMultiAlternatives` subclass with additional support to send emails directly to a users. It also requires an explicit language @@ -102,7 +101,7 @@ def message(self): def get_utm_campaign_name(cls): """Return the UTM campaign name for this email.""" return "_".join( - (m.group(0) for m in CLS_NAME_TO_CAMPAIGN_RE.finditer(cls.__qualname__)) + m.group(0) for m in CLS_NAME_TO_CAMPAIGN_RE.finditer(cls.__qualname__) ).upper() def update_url_params(self, url, **params): @@ -192,8 +191,7 @@ def get_subject(self, **context): return self.subject % context def get_preheader(self): - """ - Return the email's preheader. + """Return the email's preheader. A brief text that recipients will see in their inbox before opening the email along with the subject. Unless explicitly set, the preheader will be the first @@ -217,7 +215,7 @@ def get_html(self, markdown_string, context): "markdown.extensions.extra", ], ) - context["markdown_string"] = mark_safe(html_message) # nosec + context["markdown_string"] = mark_safe(html_message) # noqa: S308 template = loader.get_template(self.base_html_template) rendered_html = template.render(context) diff --git a/emark/utils.py b/emark/utils.py index 95115e5..2fd8f91 100644 --- a/emark/utils.py +++ b/emark/utils.py @@ -9,8 +9,7 @@ @dataclasses.dataclass class Node: - """ - Simple HTML node that can be extracted into plain text. + """Simple HTML node that can be extracted into plain text. Nodes have a parent and children link to create a tree structure. Plain text extraction is done by recursively traversing the tree. diff --git a/linter-requirements.txt b/linter-requirements.txt deleted file mode 100644 index 1c023a8..0000000 --- a/linter-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -bandit==1.7.9 -black==24.8.0 -flake8==7.1.1 -isort==5.13.2 -pydocstyle[toml]==6.3.0 -djlint==1.34.1 diff --git a/pyproject.toml b/pyproject.toml index 951f763..5bd5990 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,12 @@ test = [ "pytest-django", "model_bakery", ] +lint = [ + "black==24.8.0", + "pre-commit==3.8.0", + "ruff==0.5.6", + "djlint==1.34.1", +] [project.urls] Project-URL = "https://github.com/voiio/emark" @@ -69,16 +75,32 @@ omit = ["emark/buildapi.py"] [tool.coverage.report] show_missing = true -[tool.isort] -atomic = true -line_length = 88 -known_first_party = "emark, tests" -include_trailing_comma = true -default_section = "THIRDPARTY" -combine_as_imports = true +[tool.ruff] +src = ["emark", "tests"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "S", # flake8-bandit + "D", # pydocstyle + "UP", # pyupgrade + "B", # flake8-bugbear + "C", # flake8-comprehensions +] + +ignore = ["B904", "D1", "E501", "S101"] + +[tool.ruff.lint.isort] +combine-as-imports = true +split-on-trailing-comma = true +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] +force-wrap-aliases = true -[tool.pydocstyle] -add_ignore = "D1" +[tool.ruff.lint.pydocstyle] +convention = "google" [tool.djlint] profile="django" diff --git a/tests/test_backends.py b/tests/test_backends.py index a8412d7..353930a 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -4,7 +4,6 @@ import pytest from django.core.mail import EmailMessage, EmailMultiAlternatives - from emark import backends from emark.models import Send diff --git a/tests/test_message.py b/tests/test_message.py index 872069f..2a74add 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,14 +1,13 @@ import copy from pathlib import Path +import emark.message import pytest from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.test.html import parse_html from model_bakery import baker -import emark.message - BASE_DIR = Path(__file__).resolve().parent.parent @@ -250,7 +249,7 @@ def test_get_preheader__missing(self): language="en-US", context={"donut_name": "HoneyNuts", "donut_type": "Honey"}, ) - msg.get_preheader() == "" + assert msg.get_preheader() == "" def test_get_preheader(self): email_message = MarkdownEmailTestWithPreheader( diff --git a/tests/test_views.py b/tests/test_views.py index e568a00..311f57e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,9 +3,8 @@ import pytest from django.urls import reverse from django.utils.http import urlencode -from model_bakery import baker - from emark import models +from model_bakery import baker class TestEmailDetailView: diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index 99b2082..a12bcdb 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -1,5 +1,4 @@ -""" -Django settings for testapp project. +"""Django settings for testapp project. Generated by 'django-admin startproject' using Django 4.0.4. @@ -20,7 +19,9 @@ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-4787+%emei)bk2ue$5^0(5_i3+fz-0^87v)myt+8y__n$-o4@l" +SECRET_KEY = ( + "django-insecure-4787+%emei)bk2ue$5^0(5_i3+fz-0^87v)myt+8y__n$-o4@l" # noqa: S105 +) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True