Skip to content

Commit

Permalink
Introduce typeguard in tests (#2073)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kakadus authored Dec 11, 2023
1 parent dd0e899 commit 162500b
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 8 deletions.
7 changes: 5 additions & 2 deletions evap/evaluation/tests/test_models_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from model_bakery import baker

from evap.evaluation.models import Contribution, Course, Evaluation, Questionnaire, UserProfile
from evap.evaluation.models_logging import FieldAction
from evap.evaluation.models_logging import FieldAction, InstanceActionType


class TestLoggedModel(TestCase):
Expand Down Expand Up @@ -52,7 +52,10 @@ def test_data_attribute_is_correctly_parsed_to_fieldactions(self):
)

def test_deletion_data(self):
self.assertEqual(self.evaluation._get_change_data(action_type="delete")["course"]["delete"][0], self.course.id)
self.assertEqual(
self.evaluation._get_change_data(action_type=InstanceActionType.DELETE)["course"]["delete"][0],
self.course.id,
)
self.evaluation.delete()
self.assertEqual(self.evaluation.related_logentries().count(), 0)

Expand Down
17 changes: 13 additions & 4 deletions evap/evaluation/tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import typing
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Iterable, Mapping
Expand All @@ -12,13 +13,17 @@
from django.forms.formsets import BaseFormSet
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.datastructures import MultiValueDict
from django.utils.translation import get_language
from django.views.generic import FormView
from django_stubs_ext import StrOrPromise

M = TypeVar("M", bound=Model)
T = TypeVar("T")
Key = TypeVar("Key")
Value = TypeVar("Value")
CellValue = str | int | float | None
CV = TypeVar("CV", bound=CellValue)


def unordered_groupby(key_value_pairs: Iterable[tuple[Key, Value]]) -> dict[Key, list[Value]]:
Expand All @@ -35,7 +40,9 @@ def unordered_groupby(key_value_pairs: Iterable[tuple[Key, Value]]) -> dict[Key,
return dict(result)


def get_object_from_dict_pk_entry_or_logged_40x(model_cls: type[M], dict_obj: Mapping[str, Any], key: str) -> M:
def get_object_from_dict_pk_entry_or_logged_40x(
model_cls: type[M], dict_obj: MultiValueDict[str, Any] | Mapping[str, Any], key: str
) -> M:
try:
return get_object_or_404(model_cls, pk=dict_obj[key])
# ValidationError happens for UUID id fields when passing invalid arguments
Expand Down Expand Up @@ -130,7 +137,8 @@ def clean_email(email: EmailT) -> EmailT:
return email


def capitalize_first(string: str) -> str:
def capitalize_first(string: StrOrPromise) -> str:
"""Realize lazy promise objects and capitalize first letter."""
return string[0].upper() + string[1:]


Expand Down Expand Up @@ -174,6 +182,7 @@ def formset_valid(self, formset) -> HttpResponse:
return super().form_valid(formset)


@typing.runtime_checkable
class HasFormValid(Protocol):
def form_valid(self, form):
pass
Expand Down Expand Up @@ -262,7 +271,7 @@ def __init__(self) -> None:
else:
self.cur_sheet = None

def write_cell(self, label: str | None = "", style: str = "default") -> None:
def write_cell(self, label: CellValue = "", style: str = "default") -> None:
"""Write a single cell and move to the next column."""
self.cur_sheet.write(
self.cur_row,
Expand All @@ -276,7 +285,7 @@ def next_row(self) -> None:
self.cur_col = 0
self.cur_row += 1

def write_row(self, vals: Iterable[str], style: str = "default") -> None:
def write_row(self, vals: Iterable[CV], style: str | typing.Callable[[CV], str] = "default") -> None:
"""
Write a cell for every value and go to the next row.
Styling can be chosen
Expand Down
6 changes: 5 additions & 1 deletion evap/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,12 @@ def CHARACTER_ALLOWED_IN_NAME(character): # pylint: disable=invalid-name

TESTING = "test" in sys.argv or "pytest" in sys.modules

# speed up tests
# speed up tests and activate typeguard introspection
if TESTING:
from typeguard import install_import_hook

install_import_hook(("evap", "tools"))

# do not use ManifestStaticFilesStorage as it requires running collectstatic beforehand
STORAGES["staticfiles"]["BACKEND"] = "django.contrib.staticfiles.storage.StaticFilesStorage"

Expand Down
9 changes: 8 additions & 1 deletion evap/staff/importers/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ def differing_fields(self, other) -> set[str]:
return {field.name for field in fields(self) if getattr(self, field.name) != getattr(other, field.name)}


class ValidCourseData(CourseData):
class ValidCourseDataMeta(type):
def __instancecheck__(cls, instance: object) -> TypeGuard["ValidCourseData"]:
if not isinstance(instance, CourseData):
return False
return all_fields_valid(instance)


class ValidCourseData(CourseData, metaclass=ValidCourseDataMeta):
"""Typing: CourseData instance where no element is invalid_value"""

degrees: set[Degree]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ pylint-django~=2.5.4
pylint~=3.0.1
tblib~=3.0.0
xlrd~=2.0.1
typeguard~=4.1.5

0 comments on commit 162500b

Please sign in to comment.