Skip to content

Commit

Permalink
Further work on a new release:
Browse files Browse the repository at this point in the history
* Update optional Akismet dependency to 24.5
* Use the test utilities from modern Akismet
* Rework the Akismet contact form and client-creation utilities
* Plain asserts in tests wherever possible.
* Change version numbreing scheme.
  • Loading branch information
ubernostrum committed May 24, 2024
1 parent 87edfc0 commit e6a7a65
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 188 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-ast
Expand All @@ -16,7 +16,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 24.2.0
rev: 24.4.2
hooks:
- id: black
language_version: python3.12
Expand All @@ -27,7 +27,7 @@ repos:
- id: flake8
name: flake8 (Python linter)
- repo: https://github.com/econchick/interrogate
rev: 1.5.0
rev: 1.7.0
hooks:
- id: interrogate
name: interrogate (Python docstring enforcer)
Expand Down
38 changes: 16 additions & 22 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,12 @@ def clean(paths: typing.Iterable[os.PathLike] = ARTIFACT_PATHS) -> None:
@nox.parametrize(
"python,django",
[
# Python/Django testing matrix. Tests Django 3.2, 4.2, 5.0, on Python 3.8
# Python/Django testing matrix. Tests Django 4.2, and 5.0, on Python 3.8
# through 3.11, skipping unsupported combinations.
(python, django)
for python in ["3.8", "3.9", "3.10", "3.11", "3.12"]
for django in ["3.2", "4.2", "5.0"]
if (python, django)
not in [
("3.11", "3.2"),
("3.12", "3.2"),
("3.8", "5.0"),
("3.9", "5.0"),
]
for django in ["4.2", "5.0"]
if (python, django) not in [("3.8", "5.0"), ("3.9", "5.0")]
],
)
def tests_with_coverage(session: nox.Session, django: str) -> None:
Expand Down Expand Up @@ -112,7 +106,7 @@ def tests_with_coverage(session: nox.Session, django: str) -> None:
# -----------------------------------------------------------------------------------


@nox.session(python=["3.11"], tags=["docs"])
@nox.session(python=["3.12"], tags=["docs"])
def docs_build(session: nox.Session) -> None:
"""
Build the package's documentation as HTML.
Expand All @@ -134,7 +128,7 @@ def docs_build(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["docs"])
@nox.session(python=["3.12"], tags=["docs"])
def docs_docstrings(session: nox.Session) -> None:
"""
Enforce the presence of docstrings on all modules, classes, functions, and
Expand All @@ -157,7 +151,7 @@ def docs_docstrings(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["docs"])
@nox.session(python=["3.12"], tags=["docs"])
def docs_spellcheck(session: nox.Session) -> None:
"""
Spell-check the package's documentation.
Expand Down Expand Up @@ -192,7 +186,7 @@ def docs_spellcheck(session: nox.Session) -> None:
# -----------------------------------------------------------------------------------


@nox.session(python=["3.11"], tags=["formatters"])
@nox.session(python=["3.12"], tags=["formatters"])
def format_black(session: nox.Session) -> None:
"""
Check code formatting with Black.
Expand All @@ -214,7 +208,7 @@ def format_black(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["formatters"])
@nox.session(python=["3.12"], tags=["formatters"])
def format_isort(session: nox.Session) -> None:
"""
Check code formating with Black.
Expand All @@ -240,7 +234,7 @@ def format_isort(session: nox.Session) -> None:
# -----------------------------------------------------------------------------------


@nox.session(python=["3.11"], tags=["linters", "security"])
@nox.session(python=["3.12"], tags=["linters", "security"])
def lint_bandit(session: nox.Session) -> None:
"""
Lint code with the Bandit security analyzer.
Expand All @@ -261,7 +255,7 @@ def lint_bandit(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["linters"])
@nox.session(python=["3.12"], tags=["linters"])
def lint_flake8(session: nox.Session) -> None:
"""
Lint code with flake8.
Expand All @@ -281,7 +275,7 @@ def lint_flake8(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["linters"])
@nox.session(python=["3.12"], tags=["linters"])
def lint_pylint(session: nox.Session) -> None:
"""
Lint code with Pyling.
Expand All @@ -299,7 +293,7 @@ def lint_pylint(session: nox.Session) -> None:
# -----------------------------------------------------------------------------------


@nox.session(python=["3.11"], tags=["packaging"])
@nox.session(python=["3.12"], tags=["packaging"])
def package_build(session: nox.Session) -> None:
"""
Check that the package builds.
Expand All @@ -311,7 +305,7 @@ def package_build(session: nox.Session) -> None:
session.run(f"{session.bin}/python{session.python}", "-Im", "build")


@nox.session(python=["3.11"], tags=["packaging"])
@nox.session(python=["3.12"], tags=["packaging"])
def package_description(session: nox.Session) -> None:
"""
Check that the package description will render on the Python Package Index.
Expand Down Expand Up @@ -339,7 +333,7 @@ def package_description(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["packaging"])
@nox.session(python=["3.12"], tags=["packaging"])
def package_manifest(session: nox.Session) -> None:
"""
Check that the set of files in the package matches the set under version control.
Expand All @@ -355,7 +349,7 @@ def package_manifest(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["packaging"])
@nox.session(python=["3.12"], tags=["packaging"])
def package_pyroma(session: nox.Session) -> None:
"""
Check package quality with pyroma.
Expand All @@ -371,7 +365,7 @@ def package_pyroma(session: nox.Session) -> None:
clean()


@nox.session(python=["3.11"], tags=["packaging"])
@nox.session(python=["3.12"], tags=["packaging"])
def package_wheel(session: nox.Session) -> None:
"""
Check the built wheel package for common errors.
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ classifiers = [
"Topic :: Utilities",
]
name = "django-contact-form"
description = "A generic contact-form application for Django"
description = "A generic contact-form application for Django."
dependencies = [
"Django>=3.2,!=4.0.*,!=4.1.*",
"Django>=4.2",
]
dynamic = ["version"]
keywords = ["django", "email", "contact-form"]
Expand All @@ -42,7 +42,7 @@ homepage = "https://github.com/ubernostrum/django-contact-form"

[project.optional-dependencies]
akismet = [
"akismet",
"akismet>=24.5.0",
]
docs = [
"furo",
Expand All @@ -54,7 +54,7 @@ docs = [
"sphinxext-opengraph",
]
tests = [
"akismet",
"akismet>=24.5.0",
"coverage",
"tomli; python_full_version < '3.11.0a7'",
]
Expand Down
2 changes: 1 addition & 1 deletion src/django_contact_form/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"""

__version__ = "2.1a1"
__version__ = "5.0a1"
94 changes: 94 additions & 0 deletions src/django_contact_form/_akismet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Helper code for obtaining an Akismet API client.
"""

import textwrap
import warnings

import akismet
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

_akismet_client = None # pylint: disable=invalid-name


def _client_from_settings(client_class):
"""
Attempt to obtain an Akismet client from legacy configuration in Django
settings.
"""
key = getattr(settings, "AKISMET_API_KEY", None) # noqa: B009
url = getattr(settings, "AKISMET_BLOG_URL", None) # noqa: B009
if not all([key, url]):
return None
warnings.warn(
textwrap.dedent(
"""
Specifying Akismet configuration via the Django settings AKISMET_API_KEY and
AKISMET_BLOG_URL is deprecated and support for it will be removed in a
future version of django-contact-form.
Please migrate to specifying the configuration in the environment variables
PYTHON_AKISMET_API_KEY and PYTHON_AKISMET_BLOG_URL. Or, if you cannot
configure via environment variables, write a subclass of
`AkismetContactForm` and override its `get_akismet_client()` method to
construct your Akismet client.
"""
),
DeprecationWarning,
stacklevel=2,
)
client = client_class(config=akismet.Config(key, url))
if client.verify_key(key, url):
return client
raise ImproperlyConfigured(
"The Akismet configuration specified in your Django settings is invalid."
)


def _client_from_environment(client_class):
"""
Attempt to obtain an Akismet client from configuration in environment variables.
"""
try:
return client_class.validated_client()
except akismet.ConfigurationError as exc:
raise ImproperlyConfigured(
"The Akismet configuration specified in your environment variables is "
"missing or invalid."
) from exc


def _try_get_akismet_client( # pylint: disable=inconsistent-return-statements
client_class=akismet.SyncClient,
):
"""
Attempt to obtain and return an instance of the given Akismet API client class..
:raises django.core.exceptions.ImproperlyConfigured: When the Akismet client
configuration is missing or invalid.
"""
global _akismet_client # pylint: disable=global-statement

if _akismet_client is None:
for attempt in [
_client_from_settings,
_client_from_environment,
]:
_akismet_client = attempt(client_class)
if _akismet_client is not None:
return _akismet_client


def _clear_cached_instance():
"""
Clear the cached Akismet API client instance, so that it will be re-created the
next time it is requested.
"""
global _akismet_client # pylint: disable=global-statement
_akismet_client = None
34 changes: 25 additions & 9 deletions src/django_contact_form/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,25 +275,41 @@ class AkismetContactForm(ContactForm):

SPAM_MESSAGE = _("Your message was classified as spam.")

def clean_body(self):
def get_akismet_client(self):
"""
Apply Akismet spam filtering to the submission.
Obtain and return an Akismet API client.
"""
from akismet import Akismet # pylint: disable=import-outside-toplevel

akismet_api = Akismet(
key=getattr(settings, "AKISMET_API_KEY", None),
blog_url=getattr(settings, "AKISMET_BLOG_URL", None),
from ._akismet import ( # pylint: disable=import-outside-toplevel
_try_get_akismet_client,
)
akismet_kwargs = {

return _try_get_akismet_client()

def get_akismet_check_arguments(self):
"""
Return the arguments which will be passed to the Akismet spam check.
If your form contains additional fields which need to have their contents passed
to Akismet, override this to ensure those arguments are correctly set.
"""
return {
"user_ip": self.request.META["REMOTE_ADDR"],
"user_agent": self.request.META.get("HTTP_USER_AGENT"),
"comment_author": self.cleaned_data.get("name"),
"comment_author_email": self.cleaned_data.get("email"),
"comment_content": self.cleaned_data["body"],
"comment_type": "contact-form",
}
if akismet_api.comment_check(**akismet_kwargs):

def clean_body(self):
"""
Apply Akismet spam filtering to the submission.
"""
akismet_client = self.get_akismet_client()

if akismet_client.comment_check(**self.get_akismet_check_arguments()):
raise forms.ValidationError(self.SPAM_MESSAGE)
return self.cleaned_data["body"]
Loading

0 comments on commit e6a7a65

Please sign in to comment.