diff --git a/Makefile b/Makefile index 1ce93df..1ef9992 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisf quality: ## check coding style with pycodestyle and pylint pylint openedx_filters test_utils *.py + mypy pycodestyle openedx_filters *.py pydocstyle openedx_filters *.py isort --check-only --diff --recursive test_utils openedx_filters *.py test_settings.py diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..0704327 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +allow_untyped_globals = False +plugins = + mypy_django_plugin.main, +files = + openedx_filters + +[mypy.plugins.django-stubs] +django_settings_module = "test_utils.test_settings" diff --git a/openedx_filters/course_authoring/filters.py b/openedx_filters/course_authoring/filters.py index 0ef802b..22b38f6 100644 --- a/openedx_filters/course_authoring/filters.py +++ b/openedx_filters/course_authoring/filters.py @@ -25,7 +25,7 @@ class LMSPageURLRequested(OpenEdxPublicFilter): filter_type = "org.openedx.course_authoring.lms.page.url.requested.v1" @classmethod - def run_filter(cls, url: str, org: str) -> tuple[str, str]: + def run_filter(cls, url: str, org: str) -> tuple[str | None, str | None]: """ Process the inputs using the configured pipeline steps to modify the URL of the page requested by the user. diff --git a/openedx_filters/exceptions.py b/openedx_filters/exceptions.py index 86af73e..70087b2 100644 --- a/openedx_filters/exceptions.py +++ b/openedx_filters/exceptions.py @@ -3,6 +3,9 @@ """ +from typing import Optional + + class OpenEdxFilterException(Exception): """ Base exception for filters. @@ -18,7 +21,13 @@ class OpenEdxFilterException(Exception): exception. """ - def __init__(self, message="", redirect_to=None, status_code=None, **kwargs): + def __init__( + self, + message: str = "", + redirect_to: str = "", + status_code: Optional[int] = None, + **kwargs + ) -> None: """ Init method for OpenEdxFilterException. @@ -31,7 +40,7 @@ def __init__(self, message="", redirect_to=None, status_code=None, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) - def __str__(self): + def __str__(self) -> str: """ Show string representation of OpenEdxFilterException using its message. """ diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 67606a5..fb315bd 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -94,7 +94,7 @@ def __init__(self, message: str, response: Optional[HttpResponse] = None) -> Non super().__init__(message, response=response) @classmethod - def run_filter(cls, context: dict, template_name: str) -> tuple[dict, str]: + def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[str, Any] | None, str | None]: """ Process the input context and template_name using the configured pipeline steps to modify the account settings. @@ -159,7 +159,7 @@ def run_filter(cls, form_data: QueryDict) -> QueryDict: """ sensitive_data = cls.extract_sensitive_data(form_data) data = super().run_pipeline(form_data=form_data) - form_data = data.get("form_data") + form_data = data.get("form_data", QueryDict()) form_data.update(sensitive_data) return form_data @@ -249,7 +249,7 @@ class PreventEnrollment(OpenEdxFilterException): """ @classmethod - def run_filter(cls, user: Any, course_key: CourseKey, mode: str) -> tuple[Any, CourseKey, str]: + def run_filter(cls, user: Any, course_key: CourseKey, mode: str) -> tuple[Any, CourseKey | None, str | None]: """ Process the user, course_key, and mode using the configured pipeline steps to modify the enrollment process. @@ -345,14 +345,14 @@ class PreventCertificateCreation(OpenEdxFilterException): @classmethod def run_filter( # pylint: disable=too-many-positional-arguments - cls: type, + cls, user: Any, course_key: CourseKey, mode: str, status: str, grade: float, generation_mode: str, - ) -> tuple[Any, CourseKey, str, str, float, str]: + ) -> tuple[Any, CourseKey | None, str | None, str | None, float | None, str | None]: """ Process the inputs using the configured pipeline steps to modify the certificate creation process. @@ -460,7 +460,7 @@ def __init__(self, message: str, response: HttpResponse) -> None: ) @classmethod - def run_filter(cls, context: dict, custom_template: Any) -> tuple[dict, Any]: + def run_filter(cls, context: dict, custom_template: Any) -> tuple[dict[str, Any] | None, Any]: """ Process the context and custom_template using the configured pipeline steps to modify the certificate rendering. @@ -646,7 +646,7 @@ def __init__(self, message: str, response: HttpResponse) -> None: ) @classmethod - def run_filter(cls, context: dict, template_name: str) -> tuple[dict, str]: + def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[str, Any] | None, str | None]: """ Process the context and template_name using the configured pipeline steps to modify the course about rendering. @@ -745,7 +745,7 @@ def __init__(self, message: str, response: Optional[HttpResponse] = None) -> Non ) @classmethod - def run_filter(cls, context: dict, template_name: str) -> tuple[dict, str]: + def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[str, Any] | None, str | None]: """ Process the context and template_name using the configured pipeline steps to modify the dashboard rendering. @@ -790,7 +790,7 @@ class PreventChildBlockRender(OpenEdxFilterException): """ @classmethod - def run_filter(cls, block: Any, context: dict) -> tuple[Any, dict]: + def run_filter(cls, block: Any, context: dict[str, Any]) -> tuple[Any, dict[str, Any] | None]: """ Process the block and context using the configured pipeline steps to modify the rendering of a child block. @@ -833,7 +833,7 @@ class PreventEnrollmentQuerysetRequest(OpenEdxFilterException): """ @classmethod - def run_filter(cls, enrollments: QuerySet) -> QuerySet: + def run_filter(cls, enrollments: QuerySet) -> QuerySet | None: """ Process the enrollments QuerySet using the configured pipeline steps to modify the course enrollment data. @@ -891,7 +891,11 @@ def __init__(self, message: str, response: Optional[HttpResponse] = None): super().__init__(message, response=response) @classmethod - def run_filter(cls, context: dict, student_view_context: dict): + def run_filter( + cls, + context: dict[str, Any], + student_view_context: dict + ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: """ Process the inputs using the configured pipeline steps to modify the rendering of an XBlock. @@ -936,7 +940,13 @@ class PreventVerticalBlockRender(OpenEdxFilterException): """ @classmethod - def run_filter(cls, block: Any, fragment: Any, context: dict, view: str) -> tuple[Any, Any, dict, str]: + def run_filter( + cls, + block: Any, + fragment: Any, + context: dict[str, Any], + view: str + ) -> tuple[Any, Any, dict[str, Any] | None, str | None]: """ Process the inputs using the configured pipeline steps to modify the rendering of a vertical block. @@ -976,7 +986,7 @@ class CourseHomeUrlCreationStarted(OpenEdxPublicFilter): filter_type = "org.openedx.learning.course.homepage.url.creation.started.v1" @classmethod - def run_filter(cls, course_key: CourseKey, course_home_url: str) -> tuple[CourseKey, str]: + def run_filter(cls, course_key: CourseKey, course_home_url: str) -> tuple[CourseKey | None, str | None]: """ Process the course_key and course_home_url using the configured pipeline steps to modify the course home url. @@ -1013,7 +1023,11 @@ class CourseEnrollmentAPIRenderStarted(OpenEdxPublicFilter): filter_type = "org.openedx.learning.home.enrollment.api.rendered.v1" @classmethod - def run_filter(cls, course_key: CourseKey, serialized_enrollment: dict) -> tuple[CourseKey, dict]: + def run_filter( + cls, + course_key: CourseKey, + serialized_enrollment: dict[str, Any] + ) -> tuple[CourseKey | None, dict[str, Any] | None]: """ Process the inputs using the configured pipeline steps to modify the course enrollment data. @@ -1050,7 +1064,7 @@ class CourseRunAPIRenderStarted(OpenEdxPublicFilter): filter_type = "org.openedx.learning.home.courserun.api.rendered.started.v1" @classmethod - def run_filter(cls, serialized_courserun: dict) -> dict: + def run_filter(cls, serialized_courserun: dict[str, Any]) -> dict[str, Any] | None: """ Process the serialized_courserun using the configured pipeline steps to modify the course run data. @@ -1145,7 +1159,7 @@ def __init__(self, message: str, response: Optional[HttpResponse] = None): ) @classmethod - def run_filter(cls, context: dict, template_name: str) -> tuple[dict, str]: + def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[str, Any] | None, str | None]: """ Process the context and template_name using the configured pipeline steps to modify the instructor dashboard. @@ -1203,7 +1217,7 @@ def __init__( super().__init__(message, context=context, template_name=template_name) @classmethod - def run_filter(cls, context: dict, template_name: str) -> tuple[dict, str]: + def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[str, Any] | None, str | None]: """ Process the context and template_name using the configured pipeline steps to modify the submission view. @@ -1240,7 +1254,7 @@ class IDVPageURLRequested(OpenEdxPublicFilter): filter_type = "org.openedx.learning.idv.page.url.requested.v1" @classmethod - def run_filter(cls, url: str) -> str: + def run_filter(cls, url: str) -> str | None: """ Process the URL using the configured pipeline steps to modify the ID verification page URL. @@ -1274,7 +1288,7 @@ class CourseAboutPageURLRequested(OpenEdxPublicFilter): filter_type = "org.openedx.learning.course_about.page.url.requested.v1" @classmethod - def run_filter(cls, url: str, org: str) -> tuple[str, str]: + def run_filter(cls, url: str, org: str) -> tuple[str | None, str | None]: """ Process the URL and org using the configured pipeline steps to modify the course about page URL. @@ -1312,7 +1326,7 @@ class ScheduleQuerySetRequested(OpenEdxPublicFilter): filter_type = "org.openedx.learning.schedule.queryset.requested.v1" @classmethod - def run_filter(cls, schedules: QuerySet) -> QuerySet: + def run_filter(cls, schedules: QuerySet) -> QuerySet | None: """ Process the schedules QuerySet using the configured pipeline steps to modify the schedules data. diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index a84e31d..ef31792 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -3,7 +3,8 @@ """ from unittest.mock import Mock, patch -from ddt import data, ddt, unpack +# Ignore the type error for ddt import since it is not recognized by mypy. +from ddt import data, ddt, unpack # type: ignore from django.test import TestCase from openedx_filters.learning.filters import ( diff --git a/openedx_filters/tests/test_tooling.py b/openedx_filters/tests/test_tooling.py index 6a0fd65..3cffbad 100644 --- a/openedx_filters/tests/test_tooling.py +++ b/openedx_filters/tests/test_tooling.py @@ -3,7 +3,8 @@ """ from unittest.mock import Mock, patch -import ddt +# Ignore the type error for ddt import since it is not recognized by mypy. +import ddt # type: ignore from django.test import TestCase, override_settings from openedx_filters import PipelineStep diff --git a/openedx_filters/tooling.py b/openedx_filters/tooling.py index 5c671ef..3a548d2 100644 --- a/openedx_filters/tooling.py +++ b/openedx_filters/tooling.py @@ -2,6 +2,7 @@ Tooling necessary to use Open edX Filters. """ from logging import getLogger +from typing import Any from django.conf import settings from django.utils.module_loading import import_string @@ -18,14 +19,14 @@ class OpenEdxPublicFilter: filter_type = "" - def __repr__(self): + def __repr__(self) -> str: """ Represent OpenEdxPublicFilter as a string. """ return "".format(filter_type=self.filter_type) @classmethod - def get_steps_for_pipeline(cls, pipeline, fail_silently): + def get_steps_for_pipeline(cls, pipeline: list, fail_silently: bool = True) -> list[type]: """ Get pipeline objects from paths. @@ -68,7 +69,7 @@ def get_steps_for_pipeline(cls, pipeline, fail_silently): return step_list @classmethod - def get_pipeline_configuration(cls): + def get_pipeline_configuration(cls) -> tuple[list[str], bool, dict[str, Any]]: """ Get pipeline configuration from filter settings. @@ -97,7 +98,9 @@ def get_pipeline_configuration(cls): """ filter_config = cls.get_filter_config() - pipeline, fail_silently, extra_config = [], True, {} + pipeline: list = [] + fail_silently: bool = True + extra_config: dict = {} if not filter_config: return pipeline, fail_silently, extra_config @@ -119,7 +122,7 @@ def get_pipeline_configuration(cls): return pipeline, fail_silently, extra_config @classmethod - def get_filter_config(cls): + def get_filter_config(cls) -> dict[str, Any]: """ Get filters configuration from settings. @@ -161,7 +164,7 @@ def get_filter_config(cls): return filters_config.get(cls.filter_type, {}) @classmethod - def run_pipeline(cls, **kwargs): + def run_pipeline(cls, **kwargs: Any) -> dict[str, Any] | Any: """ Execute filters in order. diff --git a/openedx_filters/utils.py b/openedx_filters/utils.py index c3a82fc..f17080b 100644 --- a/openedx_filters/utils.py +++ b/openedx_filters/utils.py @@ -3,15 +3,18 @@ """ +from django.http import QueryDict + + class SensitiveDataManagementMixin: """ Custom class used manage sensitive data within filter arguments. """ - sensitive_form_data = [] + sensitive_form_data: list[str] = [] @classmethod - def extract_sensitive_data(cls, form_data): + def extract_sensitive_data(cls, form_data: QueryDict) -> dict[str, str]: """ Extract sensitive data from its child class input arguments. @@ -30,6 +33,6 @@ def extract_sensitive_data(cls, form_data): for key, value in base_form_data.items(): if key in cls.sensitive_form_data: form_data.pop(key) - sensitive_data[key] = value + sensitive_data[key] = str(value) return sensitive_data diff --git a/requirements/dev.txt b/requirements/dev.txt index cae6f46..4b58d26 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,6 +8,7 @@ asgiref==3.8.1 # via # -r requirements/quality.txt # django + # django-stubs astroid==3.3.8 # via # -r requirements/quality.txt @@ -25,7 +26,7 @@ cachetools==5.5.1 # via # -r requirements/ci.txt # tox -certifi==2024.12.14 +certifi==2025.1.31 # via # -r requirements/quality.txt # requests @@ -72,7 +73,7 @@ cryptography==44.0.0 # secretstorage ddt==1.7.2 # via -r requirements/quality.txt -diff-cover==9.2.1 +diff-cover==9.2.2 # via -r requirements/dev.in dill==0.3.9 # via @@ -86,6 +87,14 @@ django==4.2.18 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # django-stubs + # django-stubs-ext +django-stubs==5.1.2 + # via -r requirements/quality.txt +django-stubs-ext==5.1.2 + # via + # -r requirements/quality.txt + # django-stubs dnspython==2.7.0 # via # -r requirements/quality.txt @@ -170,6 +179,12 @@ more-itertools==10.6.0 # -r requirements/quality.txt # jaraco-classes # jaraco-functools +mypy==1.14.1 + # via -r requirements/quality.txt +mypy-extensions==1.0.0 + # via + # -r requirements/quality.txt + # mypy nh3==0.2.20 # via # -r requirements/quality.txt @@ -323,10 +338,17 @@ tox==4.24.1 # via -r requirements/ci.txt twine==6.1.0 # via -r requirements/quality.txt +types-pyyaml==6.0.12.20241230 + # via + # -r requirements/quality.txt + # django-stubs typing-extensions==4.12.2 # via # -r requirements/quality.txt + # django-stubs + # django-stubs-ext # edx-opaque-keys + # mypy urllib3==2.2.3 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 459c543..d225832 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -16,17 +16,18 @@ asgiref==3.8.1 # via # -r requirements/test.txt # django -babel==2.16.0 + # django-stubs +babel==2.17.0 # via # pydata-sphinx-theme # sphinx backports-tarfile==1.2.0 # via jaraco-context -beautifulsoup4==4.12.3 +beautifulsoup4==4.13.1 # via pydata-sphinx-theme build==1.2.2.post1 # via -r requirements/doc.in -certifi==2024.12.14 +certifi==2025.1.31 # via requests cffi==1.17.1 # via cryptography @@ -53,6 +54,14 @@ django==4.2.18 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # django-stubs + # django-stubs-ext +django-stubs==5.1.2 + # via -r requirements/test.txt +django-stubs-ext==5.1.2 + # via + # -r requirements/test.txt + # django-stubs dnspython==2.7.0 # via # -r requirements/test.txt @@ -113,6 +122,12 @@ more-itertools==10.6.0 # via # jaraco-classes # jaraco-functools +mypy==1.14.1 + # via -r requirements/test.txt +mypy-extensions==1.0.0 + # via + # -r requirements/test.txt + # mypy nh3==0.2.20 # via readme-renderer packaging==24.2 @@ -239,11 +254,19 @@ text-unidecode==1.3 # python-slugify twine==6.1.0 # via -r requirements/doc.in +types-pyyaml==6.0.12.20241230 + # via + # -r requirements/test.txt + # django-stubs typing-extensions==4.12.2 # via # -r requirements/test.txt # anyio + # beautifulsoup4 + # django-stubs + # django-stubs-ext # edx-opaque-keys + # mypy # pydata-sphinx-theme urllib3==2.2.3 # via diff --git a/requirements/quality.in b/requirements/quality.in index d477386..a59b20f 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -8,3 +8,4 @@ isort # to standardize order of imports pycodestyle # PEP 8 compliance validation pydocstyle # PEP 257 compliance validation twine # Utility for publishing Python packages on PyPI. +mypy # Static type checker diff --git a/requirements/quality.txt b/requirements/quality.txt index 455b1d0..4b77d5f 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -8,13 +8,14 @@ asgiref==3.8.1 # via # -r requirements/test.txt # django + # django-stubs astroid==3.3.8 # via # pylint # pylint-celery backports-tarfile==1.2.0 # via jaraco-context -certifi==2024.12.14 +certifi==2025.1.31 # via requests cffi==1.17.1 # via cryptography @@ -46,6 +47,14 @@ django==4.2.18 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # django-stubs + # django-stubs-ext +django-stubs==5.1.2 + # via -r requirements/test.txt +django-stubs-ext==5.1.2 + # via + # -r requirements/test.txt + # django-stubs dnspython==2.7.0 # via # -r requirements/test.txt @@ -100,6 +109,14 @@ more-itertools==10.6.0 # via # jaraco-classes # jaraco-functools +mypy==1.14.1 + # via + # -r requirements/quality.in + # -r requirements/test.txt +mypy-extensions==1.0.0 + # via + # -r requirements/test.txt + # mypy nh3==0.2.20 # via readme-renderer packaging==24.2 @@ -198,10 +215,17 @@ tomlkit==0.13.2 # via pylint twine==6.1.0 # via -r requirements/quality.in +types-pyyaml==6.0.12.20241230 + # via + # -r requirements/test.txt + # django-stubs typing-extensions==4.12.2 # via # -r requirements/test.txt + # django-stubs + # django-stubs-ext # edx-opaque-keys + # mypy urllib3==2.2.3 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt diff --git a/requirements/test.in b/requirements/test.in index 3af36bf..a3b0346 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -7,3 +7,5 @@ ddt # A library to multiply test cases pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support code-annotations # provides commands used by the pii_check make target. +django-stubs # Typing stubs for Django, so it works with mypy +mypy # static type checking diff --git a/requirements/test.txt b/requirements/test.txt index b13e89b..92f05de 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,6 +8,7 @@ asgiref==3.8.1 # via # -r requirements/base.txt # django + # django-stubs click==8.1.8 # via code-annotations code-annotations==2.2.0 @@ -20,6 +21,12 @@ django==4.2.18 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # django-stubs + # django-stubs-ext +django-stubs==5.1.2 + # via -r requirements/test.in +django-stubs-ext==5.1.2 + # via django-stubs dnspython==2.7.0 # via # -r requirements/base.txt @@ -32,6 +39,10 @@ jinja2==3.1.5 # via code-annotations markupsafe==3.0.2 # via jinja2 +mypy==1.14.1 + # via -r requirements/test.in +mypy-extensions==1.0.0 + # via mypy packaging==24.2 # via pytest pbr==6.1.0 @@ -67,7 +78,12 @@ stevedore==5.4.0 # edx-opaque-keys text-unidecode==1.3 # via python-slugify +types-pyyaml==6.0.12.20241230 + # via django-stubs typing-extensions==4.12.2 # via # -r requirements/base.txt + # django-stubs + # django-stubs-ext # edx-opaque-keys + # mypy