diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ad98f4..89db26b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,54 +10,29 @@ on: - ".*" workflow_dispatch: -env: - PYTHON_VERSION: 3.8 - jobs: test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: [3.8] - steps: - - name: ๐Ÿ›ซ Checkout - uses: actions/checkout@v3 - - - name: ๐Ÿ Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: ๐Ÿ›  Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pipenv - pipenv install --dev - - - name: ๐Ÿ”Ž Check Code Format - run: if ! pipenv run black --check .; then exit 1; fi - - - name: ๐Ÿ”Ž Check Migrations - run: pipenv run python manage.py makemigrations --check --dry-run - - # TODO: assert code coverage target. - - name: ๐Ÿงช Test Code Units - run: pipenv run pytest -n auto + uses: ocadotechnology/codeforlife-workspace/.github/workflows/test-python-code.yaml@main + with: + # Cannot be set with an env var. Value must match in the release job. + python-version: 3.8 release: concurrency: release runs-on: ubuntu-latest needs: [test] if: github.ref_name == 'main' + env: + # Value must match in the test job. + PYTHON_VERSION: 3.8 steps: - name: ๐Ÿ›ซ Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.CFL_BOT_GITHUB_TOKEN }} fetch-depth: 0 - - name: ๐Ÿ Set up Python + - name: ๐Ÿ Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} @@ -67,10 +42,8 @@ jobs: python -m pip install --upgrade pip python -m pip install python-semantic-release~=7.33 - - name: โš™๏ธ Configure Git - run: | - git config --local user.name cfl-bot - git config --local user.email codeforlife-bot@ocado.com + - name: ๐Ÿค– Set up cfl-bot as Git User + uses: ocadotechnology/codeforlife-workspace/.github/actions/git/setup-bot@main - name: ๐Ÿš€ Publish Semantic Release env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 77638c1..5d23933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.16.2 (2024-04-16) + +### Fix + +* Polish mypy ([#110](https://github.com/ocadotechnology/codeforlife-package-python/issues/110)) ([`bcbe98f`](https://github.com/ocadotechnology/codeforlife-package-python/commit/bcbe98f6209c493de1837f12ab32761478778004)) + ## v0.16.1 (2024-04-15) ### Fix diff --git a/Pipfile.lock b/Pipfile.lock index f42aacc..e0e310f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -856,7 +856,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -1005,7 +1005,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sortedcontainers": { diff --git a/codeforlife/models/signals/pre_save.py b/codeforlife/models/signals/pre_save.py index 4787f79..44aeb10 100644 --- a/codeforlife/models/signals/pre_save.py +++ b/codeforlife/models/signals/pre_save.py @@ -48,7 +48,8 @@ def get_previous_value(field: str): return None else: - previous_instance = instance.__class__.objects.get(pk=instance.pk) + objects = instance.__class__.objects # type: ignore[attr-defined] + previous_instance = objects.get(pk=instance.pk) def get_previous_value(field: str): return getattr(previous_instance, field) diff --git a/codeforlife/tests/__init__.py b/codeforlife/tests/__init__.py index 9e95e79..fdaee24 100644 --- a/codeforlife/tests/__init__.py +++ b/codeforlife/tests/__init__.py @@ -13,5 +13,5 @@ ModelListSerializerTestCase, ModelSerializerTestCase, ) -from .model_view_set import ModelViewSetTestCase -from .test import TestCase +from .model_view_set import ModelViewSetClient, ModelViewSetTestCase +from .test import Client, TestCase diff --git a/codeforlife/tests/api.py b/codeforlife/tests/api.py index 5001cca..bacb3d5 100644 --- a/codeforlife/tests/api.py +++ b/codeforlife/tests/api.py @@ -125,7 +125,9 @@ def _login_user_type(self, user_type: t.Type[LoginUser], **credentials): otp = user.totp.at(now) with patch.object(timezone, "now", return_value=now): assert super().login( - request=self.request_factory.post(user=user), + request=self.request_factory.post( + user=t.cast(RequestUser, user) + ), otp=otp, ), f'Failed to login with OTP "{otp}" at {now}.' diff --git a/codeforlife/tests/model.py b/codeforlife/tests/model.py index 6c2c771..c1067d0 100644 --- a/codeforlife/tests/model.py +++ b/codeforlife/tests/model.py @@ -6,6 +6,7 @@ import typing as t from unittest.case import _AssertRaisesContext +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Model from django.db.utils import IntegrityError @@ -76,8 +77,9 @@ def assert_does_not_exist(self, model_or_pk: t.Union[AnyModel, t.Any]): """ model_class = self.get_model_class() - with self.assertRaises(model_class.DoesNotExist): + with self.assertRaises(ObjectDoesNotExist): if isinstance(model_or_pk, Model): model_or_pk.refresh_from_db() else: - model_class.objects.get(pk=model_or_pk) + objects = model_class.objects # type: ignore[attr-defined] + objects.get(pk=model_or_pk) diff --git a/codeforlife/tests/model_view_set.py b/codeforlife/tests/model_view_set.py index dfbc9d5..a94dd86 100644 --- a/codeforlife/tests/model_view_set.py +++ b/codeforlife/tests/model_view_set.py @@ -8,6 +8,7 @@ import typing as t from datetime import datetime +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Model from django.db.models.query import QuerySet from django.urls import reverse @@ -19,7 +20,7 @@ from ..serializers import BaseSerializer from ..types import DataDict, JsonDict, KwArgs from ..user.models import AnyUser as RequestUser -from ..user.models import User +from ..user.models import Class, Student, User from ..views import ModelViewSet from .api import APIClient, APITestCase @@ -681,8 +682,13 @@ def assert_serialized_model_equals_json_model( """ # Get the logged-in user. try: - user = User.objects.get(session=self.client.session.session_key) - except User.DoesNotExist: + user = t.cast( + RequestUser, + self.get_request_user_class().objects.get( + session=self.client.session.session_key + ), + ) + except ObjectDoesNotExist: user = None # NOTE: no user has logged in. # Create an instance of the model view set and serializer. @@ -844,7 +850,9 @@ def get_another_school_user( school = ( user.teacher.school if user.teacher - else user.student.class_field.teacher.school + else t.cast( + Class, t.cast(Student, user.student).class_field + ).teacher.school ) assert school @@ -882,12 +890,14 @@ def get_another_school_user( ) # Else, both users are students. else: + klass = t.cast( + Class, t.cast(Student, user.student).class_field + ) + assert ( - user.student.class_field - == other_user.student.class_field + klass == other_user.student.class_field if same_class - else user.student.class_field - != other_user.student.class_field + else klass != other_user.student.class_field ) else: assert school != other_school diff --git a/codeforlife/tests/test.py b/codeforlife/tests/test.py index 015539c..712b4af 100644 --- a/codeforlife/tests/test.py +++ b/codeforlife/tests/test.py @@ -3,12 +3,40 @@ Created on 10/04/2024 at 13:03:00(+01:00). """ +import typing as t from unittest.case import _AssertRaisesContext from django.core.exceptions import ValidationError +from django.http import HttpResponse +from django.test import Client as _Client from django.test import TestCase as _TestCase +class Client(_Client): + """A Django client with type hints.""" + + def generic(self, *args, **kwargs): + return t.cast(HttpResponse, super().generic(*args, **kwargs)) + + def get(self, *args, **kwargs): + return t.cast(HttpResponse, super().get(*args, **kwargs)) + + def post(self, *args, **kwargs): + return t.cast(HttpResponse, super().post(*args, **kwargs)) + + def put(self, *args, **kwargs): + return t.cast(HttpResponse, super().put(*args, **kwargs)) + + def patch(self, *args, **kwargs): + return t.cast(HttpResponse, super().patch(*args, **kwargs)) + + def delete(self, *args, **kwargs): + return t.cast(HttpResponse, super().delete(*args, **kwargs)) + + def options(self, *args, **kwargs): + return t.cast(HttpResponse, super().options(*args, **kwargs)) + + class TestCase(_TestCase): """Base test case for all tests to inherit.""" diff --git a/codeforlife/urls.py b/codeforlife/urls.py index 0e7cbeb..a7263a6 100644 --- a/codeforlife/urls.py +++ b/codeforlife/urls.py @@ -3,11 +3,13 @@ Created on 12/04/2024 at 14:42:20(+01:00). """ +import typing as t + from django.contrib import admin from django.contrib.auth.views import LogoutView from django.http import HttpResponse from django.shortcuts import render -from django.urls import include, path, re_path +from django.urls import URLPattern, URLResolver, include, path, re_path from rest_framework import status from .settings import SERVICE_IS_ROOT, SERVICE_NAME @@ -31,7 +33,7 @@ def service_urlpatterns( """ # Specific url patterns. - urlpatterns = [ + urlpatterns: t.List[t.Union[URLResolver, URLPattern]] = [ path( "admin/", admin.site.urls, diff --git a/codeforlife/user/auth/backends/user_id_and_login_id.py b/codeforlife/user/auth/backends/user_id_and_login_id.py index 8bda96b..6565174 100644 --- a/codeforlife/user/auth/backends/user_id_and_login_id.py +++ b/codeforlife/user/auth/backends/user_id_and_login_id.py @@ -5,7 +5,12 @@ import typing as t -from common.helpers.generators import get_hashed_login_id +# isort: off +from common.helpers.generators import ( # type: ignore[import-untyped] + get_hashed_login_id, +) + +# isort: on from ....request import HttpRequest from ...models import Student, StudentUser diff --git a/codeforlife/user/auth/password_validators/__init__.py b/codeforlife/user/auth/password_validators/__init__.py index 510580a..c10efbb 100644 --- a/codeforlife/user/auth/password_validators/__init__.py +++ b/codeforlife/user/auth/password_validators/__init__.py @@ -3,6 +3,6 @@ Created on 30/01/2024 at 12:28:00(+00:00). """ -from .student import StudentPasswordValidator from .independent import IndependentPasswordValidator +from .student import StudentPasswordValidator from .teacher import TeacherPasswordValidator diff --git a/codeforlife/user/filters/user.py b/codeforlife/user/filters/user.py index 54e6301..1aa1286 100644 --- a/codeforlife/user/filters/user.py +++ b/codeforlife/user/filters/user.py @@ -3,7 +3,9 @@ Created on 03/04/2024 at 16:37:44(+01:00). """ -from django_filters import rest_framework as filters +from django_filters import ( # type: ignore[import-untyped] # isort: skip + rest_framework as filters, +) from ..models import User diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index 156237a..fb34bdd 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -4,4 +4,4 @@ """ # pylint: disable-next=unused-import -from common.models import Class +from common.models import Class # type: ignore[import-untyped] diff --git a/codeforlife/user/models/school.py b/codeforlife/user/models/school.py index 55cdeb8..6c09c1e 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -4,4 +4,4 @@ """ # pylint: disable-next=unused-import -from common.models import School +from common.models import School # type: ignore[import-untyped] diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index dd01abb..f32eead 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -1,3 +1,5 @@ +# TODO: remove this in new system +# mypy: disable-error-code="import-untyped" """ ยฉ Ocado Group Created on 14/02/2024 at 17:16:44(+00:00). diff --git a/codeforlife/user/models/teacher.py b/codeforlife/user/models/teacher.py index 93b513e..b0a07fb 100644 --- a/codeforlife/user/models/teacher.py +++ b/codeforlife/user/models/teacher.py @@ -1,3 +1,5 @@ +# TODO: remove this in new system +# mypy: disable-error-code="import-untyped" """ ยฉ Ocado Group Created on 05/02/2024 at 09:49:56(+00:00). diff --git a/codeforlife/user/models/user.py b/codeforlife/user/models/user.py index 3a277b9..cad4231 100644 --- a/codeforlife/user/models/user.py +++ b/codeforlife/user/models/user.py @@ -1,3 +1,5 @@ +# TODO: remove this in new system +# mypy: disable-error-code="import-untyped" """ ยฉ Ocado Group Created on 05/02/2024 at 09:50:04(+00:00). diff --git a/codeforlife/version.py b/codeforlife/version.py index aaf0c77..58cddca 100644 --- a/codeforlife/version.py +++ b/codeforlife/version.py @@ -5,4 +5,4 @@ # Do NOT set manually! # This is auto-updated by python-semantic-release in the pipeline. -__version__ = "0.16.1" +__version__ = "0.16.2" diff --git a/setup.py b/setup.py index e7ccda3..dd65c23 100644 --- a/setup.py +++ b/setup.py @@ -88,6 +88,7 @@ def parse_requirements(packages: t.Dict[str, t.Dict[str, t.Any]]): long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/ocadotechnology/codeforlife-package-python", + # TODO: exclude test files packages=find_packages(exclude=["tests", "tests.*"]), include_package_data=True, data_files=[get_data_files(DATA_DIR), get_data_files(USER_FIXTURES_DIR)],