Skip to content

Commit

Permalink
Merge pull request #30 from maykinmedia/chore/add-mypy-type-checking
Browse files Browse the repository at this point in the history
Add type checking to CI
  • Loading branch information
sergei-maertens authored Mar 1, 2024
2 parents 41c2366 + 67dd855 commit 84d2b28
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 15 deletions.
13 changes: 13 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ tests_require =
black
flake8
freezegun
django-stubs[compatible-mypy]

[options.packages.find]
include =
Expand All @@ -66,6 +67,7 @@ tests =
black
flake8
freezegun
django-stubs[compatible-mypy]
pep8 = flake8
coverage = pytest-cov
docs =
Expand Down Expand Up @@ -96,3 +98,14 @@ DJANGO_SETTINGS_MODULE=testapp.settings
max-line-length=88
exclude=env,.tox,doc
ignore=E203,W503

[mypy]
plugins =
mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = "testapp.settings"

[mypy-factory]
# typing support for factory-boy *is* coming
ignore_missing_imports = True
5 changes: 3 additions & 2 deletions simple_certmanager/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

@admin.register(Certificate)
class CertificateAdmin(PrivateMediaMixin, admin.ModelAdmin):
model: type[Certificate]
form = CertificateAdminForm

fields = ("label", "serial_number", "type", "public_certificate", "private_key")
Expand All @@ -28,7 +29,7 @@ class CertificateAdmin(PrivateMediaMixin, admin.ModelAdmin):
private_media_no_download_fields = ("private_key",)

@admin.display(description=_("label"), ordering="label")
def get_label(self, obj):
def get_label(self, obj) -> str:
return str(obj)

@admin.display(description=_("serial number"))
Expand All @@ -51,7 +52,7 @@ def expiry_date(self, obj: Certificate):

@admin.display(description=_("valid key pair"), boolean=True)
@suppress_cryptography_errors
def is_valid_key_pair(self, obj: Certificate):
def is_valid_key_pair(self, obj: Certificate) -> bool | None:
# alias model property to catch errors
try:
return obj.is_valid_key_pair()
Expand Down
16 changes: 8 additions & 8 deletions simple_certmanager/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
"""

import logging
from typing import List

from django.db import models, transaction
from django.db.models.base import ModelBase
from django.db.models.fields.files import FieldFile

logger = logging.getLogger(__name__)


def get_file_field_names(model: ModelBase) -> List[str]:
def get_file_field_names(model: type[models.Model]) -> list[str]:
"""
Collect names of :class:`django.db.models.FileField` (& subclass) model fields.
"""
Expand Down Expand Up @@ -50,7 +48,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
return True


def _delete_obj_files(fields: List[str], obj: models.Model) -> None:
def _delete_obj_files(fields: list[str], obj: models.Model) -> None:
for name in fields:
filefield = getattr(obj, name)
with log_failed_deletes(filefield):
Expand All @@ -69,10 +67,12 @@ def delete(self, *args, **kwargs):
# in case the DB deletion errors but then the file is gone?
# Postponing that decision, as likely a number of tests will fail because they
# run in transactions.
file_field_names = get_file_field_names(type(self))
file_field_names = get_file_field_names(type(self)) # type: ignore
with transaction.atomic():
result = super().delete(*args, **kwargs)
transaction.on_commit(lambda: _delete_obj_files(file_field_names, self))
result = super().delete(*args, **kwargs) # type: ignore
transaction.on_commit(
lambda: _delete_obj_files(file_field_names, self) # type: ignore
)
return result

delete.alters_data = True
delete.alters_data = True # type: ignore
2 changes: 1 addition & 1 deletion simple_certmanager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Meta:
verbose_name = _("certificate")
verbose_name_plural = _("certificates")

def __str__(self):
def __str__(self) -> str:
return self.label or gettext("(missing label)")

@property
Expand Down
20 changes: 16 additions & 4 deletions simple_certmanager/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from functools import wraps
from typing import Any, Optional
from typing import Callable, ParamSpec, TypeVar

from django.utils.encoding import force_str

from cryptography import x509
from cryptography.hazmat.primitives.serialization import load_pem_private_key
Expand All @@ -20,17 +22,27 @@ def load_pem_x509_private_key(data: bytes):


def pretty_print_certificate_components(x509name: x509.Name) -> str:
bits = (f"{attr.rfc4514_attribute_name}: {attr.value}" for attr in x509name)
# attr.value can be bytes, in which case it is must be an UTF8String or
# PrintableString (the latter being a subset of ASCII, thus also a subset of UTF8)
# See https://www.rfc-editor.org/rfc/rfc5280.txt
bits = (
f"{attr.rfc4514_attribute_name}: {force_str(attr.value, encoding='utf-8')}"
for attr in x509name
)
return ", ".join(bits)


def suppress_cryptography_errors(func):
T = TypeVar("T")
P = ParamSpec("P")


def suppress_cryptography_errors(func: Callable[P, T], /) -> Callable[P, T | None]:
"""
Decorator to suppress exceptions thrown while processing PKI data.
"""

@wraps(func)
def wrapper(*args, **kwargs) -> Optional[Any]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None:
try:
return func(*args, **kwargs)
except ValueError as exc:
Expand Down
9 changes: 9 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ envlist =
black
flake8
docs
py310-django32-mpy
py{310,311,312}-django{42}-mypy
skip_missing_interpreters = true

[gh-actions]
Expand Down Expand Up @@ -60,3 +62,10 @@ commands=
py.test check_sphinx.py -v \
--tb=auto \
{posargs}

[testenv:py{310,311,312}-django{32,42}-mypy]
extras =
tests
testutils
skipsdist = True
commands = mypy simple_certmanager

0 comments on commit 84d2b28

Please sign in to comment.