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

Add type hints to fields module #1214

Merged
merged 25 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6870d40
Update MyPy ini file to include new location
liamtoozer Sep 25, 2023
f26ff9a
Move namedtuple choice types from select_handlers.py for type reuse
liamtoozer Sep 25, 2023
2a75464
Add type hints to select_field_with_detail_answer.py
liamtoozer Sep 25, 2023
9186f25
Tweak ChoiceWidgetRenderType tuple return types
liamtoozer Sep 25, 2023
030262a
Add type hints to multiple_select_field_with_detail_answer.py
liamtoozer Sep 25, 2023
9b73c02
Add type hints to date_field.py and tweak caller positional arg to kwarg
liamtoozer Sep 25, 2023
a19b425
Add missing future annotations to multiple_select_field_with_detail_a…
liamtoozer Sep 25, 2023
5dcbd9d
Add decimal and int separator classes
liamtoozer Sep 26, 2023
32ef856
Add type hints to max_text_area_field.py
liamtoozer Sep 26, 2023
a1de2cf
Add type hints to max_text_area_field.py and tweak caller positional …
liamtoozer Sep 26, 2023
00df9ec
Add type hints to year_date_field.py
liamtoozer Sep 26, 2023
06f2c06
Tweak to validator kwarg
liamtoozer Sep 26, 2023
f95c9cf
Tidy up
liamtoozer Sep 26, 2023
ec6491f
Linting
liamtoozer Sep 26, 2023
f49225f
Update tests to use named params now
liamtoozer Sep 26, 2023
d1d45f6
Merge branch 'main' into add-type-hints-to-fields-module
liamtoozer Sep 26, 2023
e9f6d10
Add missing kwarg to test
liamtoozer Sep 26, 2023
2b111a5
Formatting
liamtoozer Sep 26, 2023
00fbda1
Update handlers to include UnboundField return types
liamtoozer Sep 28, 2023
f28ac3c
Address PR comments
liamtoozer Sep 28, 2023
e9eea1d
Merge branch 'main' into add-type-hints-to-fields-module
liamtoozer Sep 28, 2023
6e84e37
Tidy up explicit kwargs usage across custom class (unless used in sco…
liamtoozer Sep 28, 2023
925e954
Add UnboundField return type to address handler
liamtoozer Sep 29, 2023
577078f
Remove now redundant kwargs from update tests
liamtoozer Sep 29, 2023
cb5692a
Merge branch 'main' into add-type-hints-to-fields-module
liamtoozer Oct 4, 2023
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
3 changes: 2 additions & 1 deletion app/forms/field_handlers/address_handler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from functools import cached_property

from wtforms import FormField
from wtforms.fields.core import UnboundField
from wtforms.validators import InputRequired

from app.forms.address_form import AddressValidatorTypes, get_address_form
Expand Down Expand Up @@ -28,7 +29,7 @@ def validators(self) -> AddressValidatorTypes:

return validate_with

def get_field(self) -> FormField:
def get_field(self) -> UnboundField | FormField:
return FormField(
get_address_form(self.validators),
label=self.label,
Expand Down
26 changes: 13 additions & 13 deletions app/forms/field_handlers/date_handlers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from datetime import datetime, timezone
from functools import cached_property
from typing import Any, Optional, Union
from typing import Any, Optional

from dateutil.relativedelta import relativedelta
from wtforms.fields.core import UnboundField

from app.forms.field_handlers.field_handler import FieldHandler
from app.forms.fields import DateField, MonthYearDateField, YearDateField
Expand All @@ -14,10 +15,7 @@
format_message_with_title,
)
from app.questionnaire.rules.utils import parse_datetime

DateValidatorTypes = list[
Union[OptionalForm, DateRequired, DateCheck, SingleDatePeriodCheck]
]
from app.utilities.types import DateValidatorType


class DateHandler(FieldHandler):
Expand All @@ -26,8 +24,8 @@ class DateHandler(FieldHandler):
DISPLAY_FORMAT = "d MMMM yyyy"

@cached_property
def validators(self) -> DateValidatorTypes:
validate_with: DateValidatorTypes = [OptionalForm()]
def validators(self) -> list[DateValidatorType]:
validate_with: list[DateValidatorType] = [OptionalForm()]

if self.answer_schema["mandatory"] is True:
validate_with = [
Expand All @@ -52,8 +50,10 @@ def validators(self) -> DateValidatorTypes:
validate_with.append(min_max_validator)
return validate_with

def get_field(self) -> DateField:
return DateField(self.validators, label=self.label, description=self.guidance)
def get_field(self) -> UnboundField | DateField:
return DateField(
katie-gardner marked this conversation as resolved.
Show resolved Hide resolved
validators=self.validators, label=self.label, description=self.guidance
)

def get_min_max_validator(
self, minimum_date: Optional[datetime], maximum_date: Optional[datetime]
Expand Down Expand Up @@ -123,9 +123,9 @@ class MonthYearDateHandler(DateHandler):
DATE_FORMAT = "yyyy-mm"
DISPLAY_FORMAT = "MMMM yyyy"

def get_field(self) -> MonthYearDateField:
def get_field(self) -> UnboundField | MonthYearDateField:
return MonthYearDateField(
self.validators, label=self.label, description=self.guidance
validators=self.validators, label=self.label, description=self.guidance
)

def get_min_max_validator(
Expand All @@ -152,9 +152,9 @@ class YearDateHandler(DateHandler):
DATE_FORMAT = "yyyy"
DISPLAY_FORMAT = "yyyy"

def get_field(self) -> YearDateField:
def get_field(self) -> UnboundField | YearDateField:
return YearDateField(
self.validators, label=self.label, description=self.guidance
validators=self.validators, label=self.label, description=self.guidance
)

def get_min_max_validator(
Expand Down
10 changes: 4 additions & 6 deletions app/forms/field_handlers/dropdown_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

from flask_babel import lazy_gettext
from wtforms import SelectField
from wtforms.fields.core import UnboundField

from app.forms.field_handlers.select_handlers import (
Choice,
ChoiceWithDetailAnswer,
SelectHandlerBase,
)
from app.forms.field_handlers.select_handlers import SelectHandlerBase
from app.questionnaire.questionnaire_schema import InvalidSchemaConfigurationException
from app.utilities.types import Choice, ChoiceWithDetailAnswer


class DropdownHandler(SelectHandlerBase):
Expand All @@ -31,7 +29,7 @@ def choices(self) -> Sequence[Choice]:
def _get_placeholder_text(self) -> str:
return self.answer_schema.get("placeholder", self.DEFAULT_PLACEHOLDER)

def get_field(self) -> SelectField:
def get_field(self) -> UnboundField | SelectField:
return SelectField(
label=self.label,
description=self.guidance,
Expand Down
3 changes: 2 additions & 1 deletion app/forms/field_handlers/duration_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from wtforms import FormField
from wtforms.fields.core import UnboundField

from app.forms.duration_form import get_duration_form
from app.forms.field_handlers.field_handler import FieldHandler
Expand All @@ -7,7 +8,7 @@
class DurationHandler(FieldHandler):
MANDATORY_MESSAGE_KEY = "MANDATORY_DURATION"

def get_field(self) -> FormField:
def get_field(self) -> UnboundField | FormField:
return FormField(
get_duration_form(self.answer_schema, dict(self.error_messages)),
label=self.label,
Expand Down
3 changes: 2 additions & 1 deletion app/forms/field_handlers/mobile_number_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Union

from wtforms import StringField
from wtforms.fields.core import UnboundField

from app.forms.field_handlers.field_handler import FieldHandler
from app.forms.validators import MobileNumberCheck, ResponseRequired
Expand All @@ -21,7 +22,7 @@ def validators(self) -> MobileNumberValidatorTypes:

return validate_with

def get_field(self) -> StringField:
def get_field(self) -> UnboundField | StringField:
return StringField(
label=self.label, description=self.guidance, validators=self.validators
)
3 changes: 2 additions & 1 deletion app/forms/field_handlers/number_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Type, Union

from wtforms import DecimalField, IntegerField
from wtforms.fields.core import UnboundField

from app.forms.field_handlers.field_handler import FieldHandler
from app.forms.fields import DecimalFieldWithSeparator, IntegerFieldWithSeparator
Expand Down Expand Up @@ -64,7 +65,7 @@ def _field_type(
else IntegerFieldWithSeparator
)

def get_field(self) -> Union[DecimalField, IntegerField]:
def get_field(self) -> UnboundField | DecimalField | IntegerField:
additional_args = (
{"places": self.max_decimals}
if self._field_type == DecimalFieldWithSeparator
Expand Down
17 changes: 6 additions & 11 deletions app/forms/field_handlers/select_handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import namedtuple
from typing import Any, Optional, Sequence, Union
from typing import Any, Optional, Sequence

from wtforms.fields.core import UnboundField

from app.forms.field_handlers.field_handler import FieldHandler
from app.forms.fields import (
Expand All @@ -8,13 +9,7 @@
)
from app.questionnaire.dynamic_answer_options import DynamicAnswerOptions
from app.questionnaire.questionnaire_schema import InvalidSchemaConfigurationException

Choice = namedtuple("Choice", "value label")
ChoiceWithDetailAnswer = namedtuple(
"ChoiceWithDetailAnswer", "value label detail_answer_id"
)

ChoiceType = Union[Choice, ChoiceWithDetailAnswer]
from app.utilities.types import ChoiceType, ChoiceWithDetailAnswer


class SelectHandlerBase(FieldHandler):
Expand Down Expand Up @@ -75,7 +70,7 @@ def coerce_str_unless_none(value: Optional[str]) -> Optional[str]:
# not providing an answer and them selecting the 'None' option otherwise.
# https://github.com/ONSdigital/eq-survey-runner/issues/1013
# See related WTForms PR: https://github.com/wtforms/wtforms/pull/288
def get_field(self) -> SelectFieldWithDetailAnswer:
def get_field(self) -> UnboundField | SelectFieldWithDetailAnswer:
return SelectFieldWithDetailAnswer(
label=self.label,
description=self.guidance,
Expand All @@ -88,7 +83,7 @@ def get_field(self) -> SelectFieldWithDetailAnswer:
class SelectMultipleHandler(SelectHandler):
MANDATORY_MESSAGE_KEY = "MANDATORY_CHECKBOX"

def get_field(self) -> MultipleSelectFieldWithDetailAnswer:
def get_field(self) -> UnboundField | MultipleSelectFieldWithDetailAnswer:
return MultipleSelectFieldWithDetailAnswer(
label=self.label,
description=self.guidance,
Expand Down
3 changes: 2 additions & 1 deletion app/forms/field_handlers/string_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Union

from wtforms import StringField, validators
from wtforms.fields.core import UnboundField
from wtforms.validators import Length

from app.forms.field_handlers.field_handler import FieldHandler
Expand Down Expand Up @@ -33,7 +34,7 @@ def max_length(self) -> int:
max_length: int = self.answer_schema.get("max_length", self.MAX_LENGTH)
return max_length

def get_field(self) -> StringField:
def get_field(self) -> UnboundField | StringField:
return StringField(
label=self.label, description=self.guidance, validators=self.validators
)
3 changes: 2 additions & 1 deletion app/forms/field_handlers/text_area_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Union

from wtforms import validators
from wtforms.fields.core import UnboundField
from wtforms.validators import Length

from app.forms.field_handlers.field_handler import FieldHandler
Expand Down Expand Up @@ -32,7 +33,7 @@ def get_length_validator(self) -> Length:

return validators.length(-1, self.max_length, message=length_message)

def get_field(self) -> MaxTextAreaField:
def get_field(self) -> UnboundField | MaxTextAreaField:
return MaxTextAreaField(
label=self.label,
description=self.guidance,
Expand Down
31 changes: 24 additions & 7 deletions app/forms/fields/date_field.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging
from functools import cached_property
from typing import Any, Callable, Sequence, Type

from werkzeug.datastructures import MultiDict
from wtforms import Form, FormField, StringField
from wtforms.utils import unset_value
from wtforms.utils import UnsetValue, unset_value

from app.utilities.types import DateValidatorType

logger = logging.getLogger(__name__)


def get_form_class(validators):
def get_form_class(validators: Sequence[DateValidatorType]) -> Type[Form]:
class DateForm(Form):
# Validation is only ever added to the 1 field that shows in all 3 variants
# This is to prevent an error message for each input box
Expand All @@ -16,7 +20,7 @@ class DateForm(Form):
day = StringField()

@cached_property
def data(self):
def data(self) -> str | None:
# pylint: disable=no-member
# wtforms Form parents are not discoverable in the 2.3.3 implementation
data = super().data
Expand All @@ -30,11 +34,24 @@ def data(self):


class DateField(FormField):
def __init__(self, validators, **kwargs):
def __init__(
self,
*,
validators: Sequence[DateValidatorType],
**kwargs: Any,
) -> None:
form_class = get_form_class(validators)
super().__init__(form_class, **kwargs)

def process(self, formdata, data=unset_value, extra_filters=None):
super().__init__(
form_class,
**kwargs,
)

def process(
self,
formdata: MultiDict | None = None,
data: str | UnsetValue = unset_value,
extra_filters: Sequence[Callable] | None = None,
) -> None:
if data is not unset_value:
substrings = data.split("-")
data = {"year": substrings[0], "month": substrings[1], "day": substrings[2]}
Expand Down
7 changes: 4 additions & 3 deletions app/forms/fields/decimal_field_with_separator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from decimal import Decimal, InvalidOperation
from typing import Any, Sequence

from wtforms import DecimalField

Expand All @@ -15,11 +16,11 @@ class DecimalFieldWithSeparator(DecimalField):
DecimalPlace validators
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.data = None
self.data: Decimal | None = None

def process_formdata(self, valuelist):
def process_formdata(self, valuelist: Sequence[str] | None = None) -> None:
if valuelist:
try:
self.data = Decimal(sanitise_number(valuelist[0]))
Expand Down
8 changes: 5 additions & 3 deletions app/forms/fields/integer_field_with_separator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any, Sequence

from wtforms import IntegerField

from app.helpers.form_helpers import sanitise_number
Expand All @@ -13,11 +15,11 @@ class IntegerFieldWithSeparator(IntegerField):
DecimalPlace validators
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.data = None
self.data: int | None = None

def process_formdata(self, valuelist):
def process_formdata(self, valuelist: Sequence[str] | None = None) -> None:
if valuelist:
try:
self.data = int(sanitise_number(valuelist[0]))
Expand Down
12 changes: 10 additions & 2 deletions app/forms/fields/max_text_area_field.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from typing import Any

from wtforms import TextAreaField


class MaxTextAreaField(TextAreaField):
def __init__(self, label="", validators=None, rows=None, maxlength=None, **kwargs):
super().__init__(label, validators, **kwargs)
def __init__(
self,
*,
rows: int,
maxlength: int,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.rows = rows
self.maxlength = maxlength
Loading