diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f683ebbd8513..3e536db05229 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,3 +51,17 @@ jobs: - name: Stop containers if: always() run: scripts/docker down + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - uses: actions/setup-python@v2 + with: + python-version: 3.9 + architecture: x64 + - name: Install mypy + run: pip install mypy + - name: Run mypy + run: mypy --install-types --non-interactive @mypy_typed_modules.txt diff --git a/corehq/motech/serializers.py b/corehq/motech/serializers.py index d63e9677deca..e9d31bceadbd 100644 --- a/corehq/motech/serializers.py +++ b/corehq/motech/serializers.py @@ -15,7 +15,7 @@ """ import datetime import re -from typing import Any +from typing import Any, Callable, Optional from dateutil import parser as dateutil_parser @@ -101,7 +101,7 @@ def to_datetime_str(value): return value.isoformat(timespec='milliseconds') -serializers = { +serializers: dict[tuple[Optional[str], Optional[str]], Callable] = { # (from_data_type, to_data_type): function (None, COMMCARE_DATA_TYPE_BOOLEAN): to_boolean, (None, COMMCARE_DATA_TYPE_DECIMAL): to_decimal, diff --git a/corehq/motech/value_source.py b/corehq/motech/value_source.py index 6f5be8c1b2e0..3fd382b67efd 100644 --- a/corehq/motech/value_source.py +++ b/corehq/motech/value_source.py @@ -1,4 +1,7 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union import attr from jsonobject.containers import JsonDict @@ -27,20 +30,22 @@ from .serializers import serializers from .utils import simplify_list +T = TypeVar('T') -@attr.s + +@dataclass class CaseTriggerInfo: - domain = attr.ib() - case_id = attr.ib() - type = attr.ib(default=None) - name = attr.ib(default=None) - owner_id = attr.ib(default=None) - modified_by = attr.ib(default=None) - updates = attr.ib(factory=dict) - created = attr.ib(default=None) - closed = attr.ib(default=None) - extra_fields = attr.ib(factory=dict) - form_question_values = attr.ib(factory=dict) + domain: str + case_id: str + type: Optional[str] = None + name: Optional[str] = None + owner_id: Optional[str] = None + modified_by: Optional[str] = None + updates: dict[str, Any] = field(default_factory=dict) + created: Optional[bool] = None + closed: Optional[bool] = None + extra_fields: dict[str, Any] = field(default_factory=dict) + form_question_values: dict[str, Any] = field(default_factory=dict) def __str__(self): if self.name: @@ -48,11 +53,11 @@ def __str__(self): return f"" -def recurse_subclasses(cls): - return ( - cls.__subclasses__() - + [subsub for sub in cls.__subclasses__() for subsub in recurse_subclasses(sub)] - ) +def recurse_subclasses(cls: Type[T]) -> list[Type[T]]: + return cls.__subclasses__() + [ + subsub for sub in cls.__subclasses__() + for subsub in recurse_subclasses(sub) + ] @attr.s(auto_attribs=True, kw_only=True) @@ -86,7 +91,7 @@ class ValueSource: jsonpath: Optional[str] = None @classmethod - def wrap(cls, data: dict): + def wrap(cls, data: dict) -> ValueSource: """ Allows us to duck-type JsonObject, and useful for doing pre-instantiation transforms / dropping unwanted attributes. diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000000..8788d153bfbe --- /dev/null +++ b/mypy.ini @@ -0,0 +1,20 @@ +# Global options: + +[mypy] +python_version = 3.9 +ignore_missing_imports = True +follow_imports = silent + +disallow_subclassing_any = True +warn_return_any = True + +warn_redundant_casts = True +warn_unused_ignores = True +warn_unused_configs = True +show_error_codes = True + +# Per-module options: + +[corehq.motech.value_source] +disallow_untyped_defs = True +disallow_any_generics = True diff --git a/mypy_typed_modules.txt b/mypy_typed_modules.txt new file mode 100644 index 000000000000..600535e75404 --- /dev/null +++ b/mypy_typed_modules.txt @@ -0,0 +1 @@ +-mcorehq.motech.value_source diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index 3ff0b78b0312..1dcdd781bd51 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -333,8 +333,12 @@ matplotlib-inline==0.1.3 # via ipython mccabe==0.6.1 # via flake8 +mypy==0.931 + # via -r test-requirements.in mypy-extensions==0.4.3 - # via black + # via + # black + # mypy nose==1.3.7 # via # -r test-requirements.in @@ -632,7 +636,9 @@ tinys3==0.1.12 toml==0.10.2 # via pep517 tomli==2.0.1 - # via black + # via + # black + # mypy toposort==1.7 # via -r base-requirements.in traitlets==5.1.1 @@ -649,6 +655,7 @@ typing-extensions==4.1.1 # via # black # django-countries + # mypy ua-parser==0.10.0 # via user-agents unidecode==1.2.0 diff --git a/requirements/test-requirements.in b/requirements/test-requirements.in index b279adfba252..beb9668a1ab7 100644 --- a/requirements/test-requirements.in +++ b/requirements/test-requirements.in @@ -3,6 +3,7 @@ beautifulsoup4 django-nose @ https://github.com/dimagi/django-nose/raw/fast-first-1.4.6.1/releases/django_nose-1.4.6.1-py2.py3-none-any.whl fakecouch +mypy nose nose-exclude pip-tools>6.4.0 diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt index f1c1fd3a6ef6..de4836d0b9c7 100644 --- a/requirements/test-requirements.txt +++ b/requirements/test-requirements.txt @@ -283,6 +283,10 @@ markupsafe==1.1.1 # via # jinja2 # mako +mypy==0.931 + # via -r test-requirements.in +mypy-extensions==0.4.3 + # via mypy nose==1.3.7 # via # -r test-requirements.in @@ -511,6 +515,8 @@ tinys3==0.1.12 # via -r base-requirements.in toml==0.10.2 # via pep517 +tomli==2.0.1 + # via mypy toposort==1.7 # via -r base-requirements.in tropo-webapi-python==0.1.3 @@ -520,7 +526,9 @@ turn-python==0.0.1 twilio==6.5.1 # via -r base-requirements.in typing-extensions==4.1.1 - # via django-countries + # via + # django-countries + # mypy ua-parser==0.10.0 # via user-agents unidecode==1.2.0