diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fad14e..2aa1cd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,3 +22,8 @@ repos: hooks: - id: djlint-jinja types_or: ['html', 'jinja'] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.10.0' # Use the sha / tag you want to point at + hooks: + - id: mypy + additional_dependencies: [types-jmespath, types-requests, types-beautifulsoup4, types-flask] diff --git a/app/__init__.py b/app/__init__.py index dad7d1f..33055ae 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -41,7 +41,7 @@ def create_app(config_class=Config) -> Flask: logging.init_app(app) app.jinja_env.lstrip_blocks = True app.jinja_env.trim_blocks = True - app.jinja_loader = ChoiceLoader( + app.jinja_loader = ChoiceLoader( # type: ignore # TODO: fixme [ PackageLoader("app"), PrefixLoader( @@ -111,8 +111,8 @@ def setup_funds_and_auth(app: Flask) -> None: app.config["AUTH_MAPPINGS"] = AuthService( fund_to_auth_mappings={ - towns_fund.fund_name: AuthMapping(towns_fund.auth_class, tf_auth), - pathfinders.fund_name: AuthMapping(pathfinders.auth_class, pf_auth), + towns_fund.fund_name: AuthMapping(towns_fund.auth_class, tf_auth), # type: ignore # TODO: fixme + pathfinders.fund_name: AuthMapping(pathfinders.auth_class, pf_auth), # type: ignore # TODO: fixme } ) diff --git a/app/main/data_requests.py b/app/main/data_requests.py index f726173..d41922e 100644 --- a/app/main/data_requests.py +++ b/app/main/data_requests.py @@ -6,7 +6,7 @@ from config import Config -def post_ingest(file: FileStorage, data: dict = None) -> tuple[dict | None, dict | None, dict | None]: +def post_ingest(file: FileStorage, data: dict | None = None) -> tuple[dict | None, dict | None, dict | None]: """Send an HTTP POST request to ingest into the data store server and return the response. diff --git a/app/main/notify.py b/app/main/notify.py index ea07d1d..da43054 100644 --- a/app/main/notify.py +++ b/app/main/notify.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import BinaryIO import notifications_python_client as notify from flask import current_app @@ -16,7 +15,7 @@ def send_email( email_address: str, template_id: str, - file: BinaryIO | FileStorage = None, + file: FileStorage | None = None, **kwargs, ) -> None: """Send email to the specified email address via the GovUK Notify service. diff --git a/app/utils.py b/app/utils.py index db8c14e..caf2412 100644 --- a/app/utils.py +++ b/app/utils.py @@ -6,7 +6,7 @@ from config import Config -def get_friendly_fund_type(fund_type_id: str) -> str | None: +def get_friendly_fund_type(fund_type_id) -> str | None: """Gets a friendly fund type name from an ID. If fund_type_id is not recognised, it is logged and None is returned. @@ -19,6 +19,8 @@ def get_friendly_fund_type(fund_type_id: str) -> str | None: except KeyError: current_app.logger.error("Unknown fund type id found: {fund_type_id}", extra=dict(fund_type_id=fund_type_id)) + return None + def days_between_dates(date1: date, date2: date) -> int: """Calculate the number of days between two dates. diff --git a/config/__init__.py b/config/__init__.py index c2762fe..42d09ec 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -10,15 +10,15 @@ case "unit_test": from config.envs.unit_test import UnitTestConfig as Config case "development": - from config.envs.development import DevelopmentConfig as Config + from config.envs.development import DevelopmentConfig as Config # type: ignore # TODO: fixme case "dev" | "test" | "production": - from config.envs.aws import AwsConfig as Config + from config.envs.aws import AwsConfig as Config # type: ignore # TODO: fixme case _: - from config.envs.default import DefaultConfig as Config + from config.envs.default import DefaultConfig as Config # type: ignore # TODO: fixme try: - Config.pretty_print() + Config.pretty_print() # type: ignore # TODO: fixme except AttributeError: print({"msg": "Config doesn't have pretty_print function."}) -__all__ = [Config] +__all__ = ["Config"] diff --git a/config/envs/default.py b/config/envs/default.py index 4605489..24a29e2 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -40,7 +40,7 @@ class DefaultConfig(object): # RSA 256 KEYS RSA256_PUBLIC_KEY_BASE64 = os.getenv("RSA256_PUBLIC_KEY_BASE64") if RSA256_PUBLIC_KEY_BASE64: - RSA256_PUBLIC_KEY = base64.b64decode(RSA256_PUBLIC_KEY_BASE64).decode() + RSA256_PUBLIC_KEY: str = base64.b64decode(RSA256_PUBLIC_KEY_BASE64).decode() TF_ADDITIONAL_EMAIL_LOOKUPS = ast.literal_eval(os.getenv("TF_ADDITIONAL_EMAIL_LOOKUPS", "{}")) if not isinstance(TF_ADDITIONAL_EMAIL_LOOKUPS, dict): diff --git a/config/envs/development.py b/config/envs/development.py index a4417c1..7cccdde 100644 --- a/config/envs/development.py +++ b/config/envs/development.py @@ -10,7 +10,7 @@ class DevelopmentConfig(DefaultConfig): # RSA 256 KEYS if not hasattr(DefaultConfig, "RSA256_PUBLIC_KEY"): _test_public_key_path = DefaultConfig.FLASK_ROOT + "/tests/keys/rsa256/public.pem" - with open(_test_public_key_path, mode="rb") as public_key_file: + with open(_test_public_key_path, mode="r") as public_key_file: RSA256_PUBLIC_KEY = public_key_file.read() # devs can submit for these LAs and places diff --git a/config/envs/unit_test.py b/config/envs/unit_test.py index aee0273..ea258c5 100644 --- a/config/envs/unit_test.py +++ b/config/envs/unit_test.py @@ -9,7 +9,7 @@ class UnitTestConfig(DefaultConfig): # RSA 256 KEYS if not hasattr(DefaultConfig, "RSA256_PUBLIC_KEY"): _test_public_key_path = DefaultConfig.FLASK_ROOT + "/tests/keys/rsa256/public.pem" - with open(_test_public_key_path, mode="rb") as public_key_file: + with open(_test_public_key_path, mode="r") as public_key_file: RSA256_PUBLIC_KEY = public_key_file.read() EXAMPLE_INGEST_WRONG_FORMAT = DefaultConfig.FLASK_ROOT + "/tests/resources/wrong_format_test_file.txt" diff --git a/app.py b/main_app.py similarity index 100% rename from app.py rename to main_app.py diff --git a/pyproject.toml b/pyproject.toml index ae0ee1e..abdda0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,3 +21,14 @@ exclude = [ "cache", ] mccabe.max-complexity = 12 + +[tool.mypy] +python_version = "3.11" + +[[tool.mypy.overrides]] +module = [ + "fsd_utils.*", + "notifications_python_client.*", + "flask_assets" +] +ignore_missing_imports = true diff --git a/requirements-dev.in b/requirements-dev.in index bf9f135..3a61e4d 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -10,6 +10,7 @@ pip-tools==6.14.0 ruff pep8-naming==0.13.3 pur==7.1.0 +mypy #----------------------------------- # Testing diff --git a/requirements-dev.txt b/requirements-dev.txt index 8a294bd..e35332a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -157,6 +157,10 @@ markupsafe==2.1.3 # werkzeug mccabe==0.7.0 # via flake8 +mypy==1.10.0 + # via -r requirements-dev.in +mypy-extensions==1.0.0 + # via mypy notifications-python-client==8.1.0 # via -r requirements.txt packaging==23.1 @@ -288,6 +292,7 @@ typing-extensions==4.7.1 # via # -r requirements.txt # alembic + # mypy # sqlalchemy urllib3==1.26.18 # via diff --git a/static_assets.py b/static_assets.py index 50a404e..679b98b 100644 --- a/static_assets.py +++ b/static_assets.py @@ -32,7 +32,3 @@ def build_bundles(static_folder): bundles = init_assets(static_folder=static_folder) for bundle in bundles: bundle.build() - - -if __name__ == "__main__": - build_bundles() diff --git a/tests/conftest.py b/tests/conftest.py index 00c3896..6ca3bbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from typing import Generator + import pytest from flask.testing import FlaskClient from werkzeug.datastructures import FileStorage @@ -48,7 +50,7 @@ def mocked_pf_and_tf_auth(mocker): @pytest.fixture() -def flask_test_client(mocked_auth) -> FlaskClient: +def flask_test_client(mocked_auth) -> Generator[FlaskClient, None, None]: """ Creates the test client we will be using to test the responses from our app, this is a test fixture. @@ -62,7 +64,7 @@ def flask_test_client(mocked_auth) -> FlaskClient: @pytest.fixture() -def unauthenticated_flask_test_client() -> FlaskClient: +def unauthenticated_flask_test_client() -> Generator[FlaskClient, None, None]: """ :return: An unauthenticated flask test client. """ @@ -71,14 +73,14 @@ def unauthenticated_flask_test_client() -> FlaskClient: @pytest.fixture(scope="function") -def example_pre_ingest_data_file() -> FileStorage: +def example_pre_ingest_data_file() -> Generator[FileStorage, None, None]: """An example pre ingest spreadsheet in towns-fund format.""" with open(UnitTestConfig.EXAMPLE_INGEST_DATA_PATH, "rb") as file: - yield file + yield FileStorage(file) @pytest.fixture(scope="function") -def example_ingest_wrong_format() -> FileStorage: +def example_ingest_wrong_format() -> Generator[FileStorage, None, None]: """An example pre ingest spreadsheet in towns-fund format.""" with open(UnitTestConfig.EXAMPLE_INGEST_WRONG_FORMAT, "rb") as file: - yield file + yield FileStorage(file)