Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static type checking #68

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
47b1a3e
upgrade requirements
matthew-shaw Apr 28, 2023
2c9a0ed
add mypy static type checker
matthew-shaw Apr 28, 2023
3d22623
add python type annotations
matthew-shaw Apr 28, 2023
a123ca7
string return types
matthew-shaw Apr 28, 2023
7a4a328
upgrade requirements
matthew-shaw Apr 29, 2023
83757e2
parameter type hints
matthew-shaw Apr 30, 2023
1b2d4d2
add type and import checks
matthew-shaw Apr 30, 2023
bce796c
upgrade requirements
matthew-shaw May 4, 2023
b4baa3b
fix cookie form assignment
matthew-shaw May 4, 2023
889d997
Merge branch 'main' into typing
matthew-shaw May 9, 2023
4a8dbd7
upgrade requirements
matthew-shaw May 9, 2023
09dc5db
Merge branch 'main' into typing
matthew-shaw Jul 28, 2023
d771484
upgrade requirements
matthew-shaw Jul 28, 2023
2718831
Merge branch 'main' into typing
matthew-shaw Sep 27, 2023
bd2445b
update mypy
matthew-shaw Sep 27, 2023
67a2d8e
WIP
matthew-shaw Oct 9, 2023
14118b9
Merge branch 'typing' of github.com:LandRegistry/govuk-frontend-flask…
matthew-shaw Oct 9, 2023
4b701af
upgrade requirements
matthew-shaw Oct 9, 2023
cf277a9
Merge branch 'main' into typing
matthew-shaw Feb 14, 2024
db50220
Merge branch 'main' into typing
matthew-shaw May 31, 2024
d96fcf2
upgrade requirement
matthew-shaw May 31, 2024
a563ee4
Merge branch 'main' into typing
matthew-shaw Oct 15, 2024
fdf4fae
ignore missing stubs
matthew-shaw Oct 17, 2024
8c56d4d
add mypy to github action
matthew-shaw Oct 17, 2024
c6cdb85
install type stubs first
matthew-shaw Oct 17, 2024
3481b2f
try again
matthew-shaw Oct 17, 2024
d830b4b
and again
matthew-shaw Oct 17, 2024
44f9878
python 3.13
matthew-shaw Nov 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
run: pip-audit -r requirements.txt
- name: Check code for potential security vulnerabilities
run: bandit -r . -x /tests
- name: Static type checking
run: mypy . --install-types --non-interactive
- name: Check code formatting
run: |
black . -t py312 -l 120 --check
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12-slim
FROM python:3.13-slim

RUN useradd appuser

Expand Down
10 changes: 6 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Type

from flask import Flask
from flask_assets import Bundle, Environment
from flask_assets import Bundle, Environment # type: ignore
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_wtf.csrf import CSRFProtect
from govuk_frontend_wtf.main import WTFormsHelpers
from flask_wtf.csrf import CSRFProtect # type: ignore
from govuk_frontend_wtf.main import WTFormsHelpers # type: ignore
from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader
from werkzeug.middleware.proxy_fix import ProxyFix

Expand All @@ -17,7 +19,7 @@
)


def create_app(config_class=Config):
def create_app(config_class: Type[Config] = Config) -> Flask:
app = Flask(__name__, static_url_path="/assets")
app.config.from_object(config_class)
app.jinja_env.lstrip_blocks = True
Expand Down
4 changes: 2 additions & 2 deletions app/demos/custom_validators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from wtforms.validators import InputRequired
from wtforms.validators import InputRequired # type: ignore


class RequiredIf(InputRequired):
Expand All @@ -14,7 +14,7 @@ def __init__(

super(RequiredIf, self).__init__(*args, **kwargs)

def __call__(self, form, field):
def __call__(self, form, field) -> None:
other_field = form._fields.get(self.other_field_name)

if other_field is None:
Expand Down
8 changes: 4 additions & 4 deletions app/demos/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask_wtf import FlaskForm
from govuk_frontend_wtf.wtforms_widgets import (
from flask_wtf import FlaskForm # type: ignore
from govuk_frontend_wtf.wtforms_widgets import ( # type: ignore
GovCharacterCount,
GovCheckboxesInput,
GovCheckboxInput,
Expand All @@ -12,7 +12,7 @@
GovTextArea,
GovTextInput,
)
from wtforms.fields import (
from wtforms.fields import ( # type: ignore
BooleanField,
DateField,
DateTimeField,
Expand All @@ -29,7 +29,7 @@
SubmitField,
TextAreaField,
)
from wtforms.validators import Email, EqualTo, InputRequired, Length, Optional, Regexp, ValidationError
from wtforms.validators import Email, EqualTo, InputRequired, Length, Optional, Regexp, ValidationError # type: ignore

from app.demos.custom_validators import RequiredIf

Expand Down
14 changes: 8 additions & 6 deletions app/demos/routes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
from typing import Union

import yaml
from flask import flash, redirect, render_template, url_for
from werkzeug import Response
from werkzeug.exceptions import NotFound

from app.demos import bp
Expand All @@ -17,7 +19,7 @@ def components():


@bp.route("/components/<string:component>", methods=["GET"])
def component(component):
def component(component: str) -> str:
try:
with open(f"app/demos/govuk_components/{component}/{component}.yaml") as yaml_file:
fixtures = yaml.safe_load(yaml_file)
Expand All @@ -32,12 +34,12 @@ def component(component):


@bp.route("/forms", methods=["GET"])
def forms():
def forms() -> str:
return render_template("forms.html")


@bp.route("/forms/bank-details", methods=["GET", "POST"])
def bank_details():
def bank_details() -> Union[str, Response]:
form = BankDetailsForm()
if form.validate_on_submit():
flash("Demo form successfully submitted", "success")
Expand All @@ -46,7 +48,7 @@ def bank_details():


@bp.route("/forms/create-account", methods=["GET", "POST"])
def create_account():
def create_account() -> Union[str, Response]:
form = CreateAccountForm()
if form.validate_on_submit():
flash("Demo form successfully submitted", "success")
Expand All @@ -55,7 +57,7 @@ def create_account():


@bp.route("/forms/kitchen-sink", methods=["GET", "POST"])
def kitchen_sink():
def kitchen_sink() -> Union[str, Response]:
form = KitchenSinkForm()
if form.validate_on_submit():
flash("Demo form successfully submitted", "success")
Expand All @@ -64,7 +66,7 @@ def kitchen_sink():


@bp.route("/forms/conditional-reveal", methods=["GET", "POST"])
def conditional_reveal():
def conditional_reveal() -> Union[str, Response]:
form = ConditionalRevealForm()
if form.validate_on_submit():
flash("Demo form successfully submitted", "success")
Expand Down
8 changes: 4 additions & 4 deletions app/main/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask_wtf import FlaskForm
from govuk_frontend_wtf.wtforms_widgets import GovRadioInput, GovSubmitInput
from wtforms.fields import RadioField, SubmitField
from wtforms.validators import InputRequired
from flask_wtf import FlaskForm # type: ignore
from govuk_frontend_wtf.wtforms_widgets import GovRadioInput, GovSubmitInput # type: ignore
from wtforms.fields import RadioField, SubmitField # type: ignore
from wtforms.validators import InputRequired # type: ignore


class CookiesForm(FlaskForm):
Expand Down
23 changes: 13 additions & 10 deletions app/main/routes.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
from typing import Optional, Tuple, Union

from flask import flash, json, make_response, redirect, render_template, request
from flask_wtf.csrf import CSRFError
from flask_wtf.csrf import CSRFError # type: ignore
from werkzeug import Response
from werkzeug.exceptions import HTTPException

from app.main import bp
from app.main.forms import CookiesForm


@bp.route("/", methods=["GET"])
def index():
def index() -> str:
return render_template("index.html")


@bp.route("/accessibility", methods=["GET"])
def accessibility():
def accessibility() -> str:
return render_template("accessibility.html")


@bp.route("/cookies", methods=["GET", "POST"])
def cookies():
def cookies() -> Union[str, Response]:
form = CookiesForm()
# Default cookies policy to reject all categories of cookie
cookies_policy = {"functional": "no", "analytics": "no"}

if form.validate_on_submit():
# Update cookies policy consent from form data
cookies_policy["functional"] = form.functional.data
cookies_policy["analytics"] = form.analytics.data
cookies_policy["functional"] = str(form.functional.data)
cookies_policy["analytics"] = str(form.analytics.data)

# Create flash message confirmation before rendering template
flash("You’ve set your cookie preferences.", "success")
Expand All @@ -44,7 +47,7 @@ def cookies():
elif request.method == "GET":
if request.cookies.get("cookies_policy"):
# Set cookie consent radios to current consent
cookies_policy = json.loads(request.cookies.get("cookies_policy"))
cookies_policy = json.loads(str(request.cookies.get("cookies_policy")))
form.functional.data = cookies_policy["functional"]
form.analytics.data = cookies_policy["analytics"]
else:
Expand All @@ -55,16 +58,16 @@ def cookies():


@bp.route("/privacy", methods=["GET"])
def privacy():
def privacy() -> str:
return render_template("privacy.html")


@bp.app_errorhandler(HTTPException)
def http_exception(error):
def http_exception(error: HTTPException) -> Tuple[str, Optional[int]]:
return render_template(f"{error.code}.html"), error.code


@bp.app_errorhandler(CSRFError)
def csrf_error(error):
def csrf_error(error: CSRFError) -> Response:
flash("The form you were submitting has expired. Please try again.")
return redirect(request.full_path)
14 changes: 7 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile requirements.in
Expand Down Expand Up @@ -29,7 +29,7 @@ flask-assets==2.1.0
# via -r requirements.in
flask-limiter[redis]==3.8.0
# via -r requirements.in
flask-wtf==1.2.1
flask-wtf==1.2.2
# via govuk-frontend-wtf
govuk-frontend-jinja==3.4.0
# via
Expand Down Expand Up @@ -58,7 +58,7 @@ limits[redis]==3.13.0
# via flask-limiter
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.1
markupsafe==3.0.2
# via
# jinja2
# werkzeug
Expand All @@ -75,21 +75,21 @@ pygments==2.18.0
# via rich
pyyaml==6.0.2
# via -r requirements.in
redis==5.1.1
redis==5.2.0
# via limits
rich==13.9.2
rich==13.9.4
# via flask-limiter
typing-extensions==4.12.2
# via
# flask-limiter
# limits
webassets==2.0
# via flask-assets
werkzeug==3.0.4
werkzeug==3.1.1
# via flask
wrapt==1.16.0
# via deprecated
wtforms==3.1.2
wtforms==3.2.1
# via
# flask-wtf
# govuk-frontend-wtf
Loading