Fix typing hints in sealed_date module

In particular, SealedDate's class methods always return an instance
of the class they're invoked through (i.e., `SealedDate.from_date_parts`
intentionally never returns `MonthSeal` or `YearSeal`).

To propertly annotate this, a type variable is needed (or the much
simpler `Self` in Python 3.11).
This commit is contained in:
Adeodato Simó 2023-10-21 18:16:50 -03:00
parent 5f619d7a39
commit 4b47646e28
No known key found for this signature in database
GPG key ID: CDF447845F1A986F

View file

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Optional, Type, TypeVar, cast
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import DateField from django.forms import DateField
@ -12,6 +13,8 @@ from django.utils import timezone
_westmost_tz = timezone.get_fixed_timezone(timedelta(hours=-12)) _westmost_tz = timezone.get_fixed_timezone(timedelta(hours=-12))
Sealed = TypeVar("Sealed", bound="SealedDate") # TODO: use Self in Python >= 3.11
# TODO: migrate SealedDate to `date` # TODO: migrate SealedDate to `date`
@ -30,12 +33,12 @@ class SealedDate(datetime):
return self.strftime("%Y-%m-%d") return self.strftime("%Y-%m-%d")
@classmethod @classmethod
def from_datetime(cls, dt) -> SealedDate: def from_datetime(cls: Type[Sealed], dt: datetime) -> Sealed:
# pylint: disable=invalid-name # pylint: disable=invalid-name
return cls.combine(dt.date(), dt.time(), tzinfo=dt.tzinfo) return cls.combine(dt.date(), dt.time(), tzinfo=dt.tzinfo)
@classmethod @classmethod
def from_date_parts(cls, year, month, day) -> SealedDate: def from_date_parts(cls: Type[Sealed], year: int, month: int, day: int) -> Sealed:
# because SealedDate is actually a datetime object, we must create it with a # because SealedDate is actually a datetime object, we must create it with a
# timezone such that its date remains stable no matter the values of USE_TZ, # timezone such that its date remains stable no matter the values of USE_TZ,
# current_timezone and default_timezone. # current_timezone and default_timezone.
@ -63,11 +66,11 @@ class YearSeal(SealedDate):
class SealedDateFormField(DateField): class SealedDateFormField(DateField):
"""date form field with support for SealedDate""" """date form field with support for SealedDate"""
def prepare_value(self, value): def prepare_value(self, value: Any) -> str:
# As a convention, Django's `SelectDateWidget` uses "0" for missing # As a convention, Django's `SelectDateWidget` uses "0" for missing
# parts. We piggy-back into that, to make it work with SealedDate. # parts. We piggy-back into that, to make it work with SealedDate.
if not isinstance(value, SealedDate): if not isinstance(value, SealedDate):
return super().prepare_value(value) return cast(str, super().prepare_value(value))
elif value.has_day: elif value.has_day:
return value.strftime("%Y-%m-%d") return value.strftime("%Y-%m-%d")
elif value.has_month: elif value.has_month:
@ -75,7 +78,7 @@ class SealedDateFormField(DateField):
else: else:
return value.strftime("%Y-0-0") return value.strftime("%Y-0-0")
def to_python(self, value) -> SealedDate: def to_python(self, value: Any) -> Optional[SealedDate]:
try: try:
date = super().to_python(value) date = super().to_python(value)
except ValidationError as ex: except ValidationError as ex: