SealedDate renames, pt. 2

• SealedDate -> PartialDate
• MonthSeal  -> MonthParts
• YearSeal   -> YearParts
This commit is contained in:
Adeodato Simó 2023-11-09 13:38:27 -03:00
parent 0e4c5ed439
commit fa80aa54a9
No known key found for this signature in database
GPG key ID: CDF447845F1A986F
4 changed files with 59 additions and 58 deletions

View file

@ -21,7 +21,7 @@ from bookwyrm import activitypub
from bookwyrm.connectors import get_image from bookwyrm.connectors import get_image
from bookwyrm.utils.sanitizer import clean from bookwyrm.utils.sanitizer import clean
from bookwyrm.utils.sealed_date import ( from bookwyrm.utils.sealed_date import (
SealedDate, PartialDate,
PartialDateModel, PartialDateModel,
from_partial_isoformat, from_partial_isoformat,
) )
@ -560,6 +560,7 @@ class PartialDateField(ActivitypubFieldMixin, PartialDateModel):
return value.partial_isoformat() if value else None return value.partial_isoformat() if value else None
def field_from_activity(self, value, allow_external_connections=True): def field_from_activity(self, value, allow_external_connections=True):
# pylint: disable=no-else-return
try: try:
return from_partial_isoformat(value) return from_partial_isoformat(value)
except ValueError: except ValueError:
@ -572,10 +573,10 @@ class PartialDateField(ActivitypubFieldMixin, PartialDateModel):
return None return None
if timezone.is_aware(parsed): if timezone.is_aware(parsed):
return SealedDate.from_datetime(parsed) return PartialDate.from_datetime(parsed)
else: else:
# Should not happen on the wire, but truncate down to date parts. # Should not happen on the wire, but truncate down to date parts.
return SealedDate.from_date_parts(parsed.year, parsed.month, parsed.day) return PartialDate.from_date_parts(parsed.year, parsed.month, parsed.day)
# FIXME: decide whether to fix timestamps like "2023-09-30T21:00:00-03": # FIXME: decide whether to fix timestamps like "2023-09-30T21:00:00-03":
# clearly Oct 1st, not Sep 30th (an unwanted side-effect of USE_TZ). It's # clearly Oct 1st, not Sep 30th (an unwanted side-effect of USE_TZ). It's

View file

@ -3,15 +3,15 @@ from django import template
from django.template import defaultfilters from django.template import defaultfilters
from django.contrib.humanize.templatetags.humanize import naturalday from django.contrib.humanize.templatetags.humanize import naturalday
from bookwyrm.utils.sealed_date import SealedDate from bookwyrm.utils.sealed_date import PartialDate
register = template.Library() register = template.Library()
@register.filter(expects_localtime=True, is_safe=False) @register.filter(expects_localtime=True, is_safe=False)
def naturalday_partial(date): def naturalday_partial(date):
"""allow templates to easily format SealedDate objects""" """allow templates to easily format PartialDate objects"""
if not isinstance(date, SealedDate): if not isinstance(date, PartialDate):
return defaultfilters.date(date) return defaultfilters.date(date)
if date.has_day: if date.has_day:
fmt = "DATE_FORMAT" fmt = "DATE_FORMAT"

View file

@ -10,8 +10,8 @@ from django.utils import translation
from bookwyrm.utils import sealed_date from bookwyrm.utils import sealed_date
class SealedDateTest(unittest.TestCase): class PartialDateTest(unittest.TestCase):
"""test SealedDate class in isolation""" """test PartialDate class in isolation"""
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@ -19,21 +19,21 @@ class SealedDateTest(unittest.TestCase):
self._dt = datetime.datetime(2023, 10, 20, 17, 33, 10, tzinfo=timezone.utc) self._dt = datetime.datetime(2023, 10, 20, 17, 33, 10, tzinfo=timezone.utc)
def test_day_seal(self): def test_day_seal(self):
sealed = sealed_date.SealedDate.from_datetime(self._dt) sealed = sealed_date.PartialDate.from_datetime(self._dt)
self.assertEqual(self._dt, sealed) self.assertEqual(self._dt, sealed)
self.assertEqual("2023-10-20", sealed.partial_isoformat()) self.assertEqual("2023-10-20", sealed.partial_isoformat())
self.assertTrue(sealed.has_day) self.assertTrue(sealed.has_day)
self.assertTrue(sealed.has_month) self.assertTrue(sealed.has_month)
def test_month_seal(self): def test_month_seal(self):
sealed = sealed_date.MonthSeal.from_datetime(self._dt) sealed = sealed_date.MonthParts.from_datetime(self._dt)
self.assertEqual(self._dt, sealed) self.assertEqual(self._dt, sealed)
self.assertEqual("2023-10", sealed.partial_isoformat()) self.assertEqual("2023-10", sealed.partial_isoformat())
self.assertFalse(sealed.has_day) self.assertFalse(sealed.has_day)
self.assertTrue(sealed.has_month) self.assertTrue(sealed.has_month)
def test_year_seal(self): def test_year_seal(self):
sealed = sealed_date.YearSeal.from_datetime(self._dt) sealed = sealed_date.YearParts.from_datetime(self._dt)
self.assertEqual(self._dt, sealed) self.assertEqual(self._dt, sealed)
self.assertEqual("2023", sealed.partial_isoformat()) self.assertEqual("2023", sealed.partial_isoformat())
self.assertFalse(sealed.has_day) self.assertFalse(sealed.has_day)
@ -41,7 +41,7 @@ class SealedDateTest(unittest.TestCase):
def test_no_naive_datetime(self): def test_no_naive_datetime(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
sealed_date.SealedDate.from_datetime(datetime.datetime(2000, 1, 1)) sealed_date.PartialDate.from_datetime(datetime.datetime(2000, 1, 1))
def test_parse_year_seal(self): def test_parse_year_seal(self):
parsed = sealed_date.from_partial_isoformat("1995") parsed = sealed_date.from_partial_isoformat("1995")
@ -99,7 +99,7 @@ class SealedDateTest(unittest.TestCase):
class PartialDateFormFieldTest(unittest.TestCase): class PartialDateFormFieldTest(unittest.TestCase):
"""test form support for SealedDate objects""" """test form support for PartialDate objects"""
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@ -108,32 +108,32 @@ class PartialDateFormFieldTest(unittest.TestCase):
self.field = sealed_date.PartialDateFormField() self.field = sealed_date.PartialDateFormField()
def test_prepare_value(self): def test_prepare_value(self):
sealed = sealed_date.SealedDate.from_datetime(self._dt) sealed = sealed_date.PartialDate.from_datetime(self._dt)
self.assertEqual("2022-11-21", self.field.prepare_value(sealed)) self.assertEqual("2022-11-21", self.field.prepare_value(sealed))
def test_prepare_value_month(self): def test_prepare_value_month(self):
sealed = sealed_date.MonthSeal.from_datetime(self._dt) sealed = sealed_date.MonthParts.from_datetime(self._dt)
self.assertEqual("2022-11-0", self.field.prepare_value(sealed)) self.assertEqual("2022-11-0", self.field.prepare_value(sealed))
def test_prepare_value_year(self): def test_prepare_value_year(self):
sealed = sealed_date.YearSeal.from_datetime(self._dt) sealed = sealed_date.YearParts.from_datetime(self._dt)
self.assertEqual("2022-0-0", self.field.prepare_value(sealed)) self.assertEqual("2022-0-0", self.field.prepare_value(sealed))
def test_to_python(self): def test_to_python(self):
date = self.field.to_python("2022-11-21") date = self.field.to_python("2022-11-21")
self.assertIsInstance(date, sealed_date.SealedDate) self.assertIsInstance(date, sealed_date.PartialDate)
self.assertEqual("2022-11-21", date.partial_isoformat()) self.assertEqual("2022-11-21", date.partial_isoformat())
def test_to_python_month(self): def test_to_python_month(self):
date = self.field.to_python("2022-11-0") date = self.field.to_python("2022-11-0")
self.assertIsInstance(date, sealed_date.SealedDate) self.assertIsInstance(date, sealed_date.PartialDate)
self.assertEqual("2022-11", date.partial_isoformat()) self.assertEqual("2022-11", date.partial_isoformat())
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.field.to_python("2022-0-25") self.field.to_python("2022-0-25")
def test_to_python_year(self): def test_to_python_year(self):
date = self.field.to_python("2022-0-0") date = self.field.to_python("2022-0-0")
self.assertIsInstance(date, sealed_date.SealedDate) self.assertIsInstance(date, sealed_date.PartialDate)
self.assertEqual("2022", date.partial_isoformat()) self.assertEqual("2022", date.partial_isoformat())
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.field.to_python("0-05-25") self.field.to_python("0-05-25")
@ -142,5 +142,5 @@ class PartialDateFormFieldTest(unittest.TestCase):
with translation.override("es"): with translation.override("es"):
# check super() is called # check super() is called
date = self.field.to_python("5/6/97") date = self.field.to_python("5/6/97")
self.assertIsInstance(date, sealed_date.SealedDate) self.assertIsInstance(date, sealed_date.PartialDate)
self.assertEqual("1997-06-05", date.partial_isoformat()) self.assertEqual("1997-06-05", date.partial_isoformat())

View file

@ -1,4 +1,4 @@
"""Implementation of the SealedDate class.""" """Implementation of the PartialDate class."""
from __future__ import annotations from __future__ import annotations
@ -15,7 +15,7 @@ from django.utils import timezone
# pylint: disable=no-else-return # pylint: disable=no-else-return
__all__ = [ __all__ = [
"SealedDate", "PartialDate",
"PartialDateModel", "PartialDateModel",
"from_partial_isoformat", "from_partial_isoformat",
] ]
@ -23,14 +23,14 @@ __all__ = [
_partial_re = re.compile(r"(\d{4})(?:-(\d\d?))?(?:-(\d\d?))?$") _partial_re = re.compile(r"(\d{4})(?:-(\d\d?))?(?:-(\d\d?))?$")
_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 Partial = TypeVar("Partial", bound="PartialDate") # TODO: use Self in Python >= 3.11
# TODO: migrate SealedDate: `datetime` => `date` # TODO: migrate PartialDate: `datetime` => `date`
# TODO: migrate PartialDateModel: `DateTimeField` => `DateField` # TODO: migrate PartialDateModel: `DateTimeField` => `DateField`
class SealedDate(datetime): class PartialDate(datetime):
"""a date object sealed into a certain precision (day, month or year)""" """a date object bound into a certain precision (day, month or year)"""
@property @property
def has_day(self) -> bool: def has_day(self) -> bool:
@ -47,8 +47,8 @@ class SealedDate(datetime):
return self.strftime("%Y-%m-%d") return self.strftime("%Y-%m-%d")
@classmethod @classmethod
def from_datetime(cls: Type[Sealed], dt: datetime) -> Sealed: def from_datetime(cls: Type[Partial], dt: datetime) -> Partial:
"""construct a SealedDate object from a timezone-aware datetime """construct a PartialDate object from a timezone-aware datetime
Use subclasses to specify precision. If `dt` is naive, `ValueError` Use subclasses to specify precision. If `dt` is naive, `ValueError`
is raised. is raised.
@ -59,18 +59,18 @@ class SealedDate(datetime):
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: Type[Sealed], year: int, month: int, day: int) -> Sealed: def from_date_parts(cls: Type[Partial], year: int, month: int, day: int) -> Partial:
"""construct a SealedDate from year, month, day. """construct a PartialDate from year, month, day.
Use sublcasses to specify precision.""" Use sublcasses to specify precision."""
# because SealedDate is actually a datetime object, we must create it with a # because PartialDate 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.
return cls.from_datetime(datetime(year, month, day, tzinfo=_westmost_tz)) return cls.from_datetime(datetime(year, month, day, tzinfo=_westmost_tz))
class MonthSeal(SealedDate): class MonthParts(PartialDate):
"""a date sealed into month precision""" """a date bound into month precision"""
@property @property
def has_day(self) -> bool: def has_day(self) -> bool:
@ -80,8 +80,8 @@ class MonthSeal(SealedDate):
return self.strftime("%Y-%m") return self.strftime("%Y-%m")
class YearSeal(SealedDate): class YearParts(PartialDate):
"""a date sealed into year precision""" """a date bound into year precision"""
@property @property
def has_month(self) -> bool: def has_month(self) -> bool:
@ -91,8 +91,8 @@ class YearSeal(SealedDate):
return self.strftime("%Y") return self.strftime("%Y")
def from_partial_isoformat(value: str) -> SealedDate: def from_partial_isoformat(value: str) -> PartialDate:
"""construct SealedDate from a partial string. """construct PartialDate from a partial string.
Accepted formats: YYYY, YYYY-MM, YYYY-MM-DD; otherwise `ValueError` Accepted formats: YYYY, YYYY-MM, YYYY-MM-DD; otherwise `ValueError`
is raised. is raised.
@ -105,20 +105,20 @@ def from_partial_isoformat(value: str) -> SealedDate:
year, month, day = [int(val) if val else -1 for val in match.groups()] year, month, day = [int(val) if val else -1 for val in match.groups()]
if month < 0: if month < 0:
return YearSeal.from_date_parts(year, 1, 1) return YearParts.from_date_parts(year, 1, 1)
elif day < 0: elif day < 0:
return MonthSeal.from_date_parts(year, month, 1) return MonthParts.from_date_parts(year, month, 1)
else: else:
return SealedDate.from_date_parts(year, month, day) return PartialDate.from_date_parts(year, month, day)
class PartialDateFormField(DateField): class PartialDateFormField(DateField):
"""date form field with support for SealedDate""" """date form field with support for PartialDate"""
def prepare_value(self, value: Any) -> str: 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 PartialDate.
if not isinstance(value, SealedDate): if not isinstance(value, PartialDate):
return cast(str, 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")
@ -127,7 +127,7 @@ class PartialDateFormField(DateField):
else: else:
return value.strftime("%Y-0-0") return value.strftime("%Y-0-0")
def to_python(self, value: Any) -> Optional[SealedDate]: def to_python(self, value: Any) -> Optional[PartialDate]:
try: try:
date = super().to_python(value) date = super().to_python(value)
except ValidationError as ex: except ValidationError as ex:
@ -136,21 +136,21 @@ class PartialDateFormField(DateField):
if not match or (day and not month) or not year: if not match or (day and not month) or not year:
raise ex from None raise ex from None
if not month: if not month:
return YearSeal.from_date_parts(year, 1, 1) return YearParts.from_date_parts(year, 1, 1)
elif not day: elif not day:
return MonthSeal.from_date_parts(year, month, 1) return MonthParts.from_date_parts(year, month, 1)
else: else:
if date is None: if date is None:
return None return None
else: else:
year, month, day = date.year, date.month, date.day year, month, day = date.year, date.month, date.day
return SealedDate.from_date_parts(year, month, day) return PartialDate.from_date_parts(year, month, day)
# For typing field and descriptor, below. # For typing field and descriptor, below.
_SetType = datetime _SetType = datetime
_GetType = Optional[SealedDate] _GetType = Optional[PartialDate]
class PartialDateDescriptor: class PartialDateDescriptor:
@ -160,14 +160,14 @@ class PartialDateDescriptor:
""" """
_SEAL_TYPES: dict[Type[_SetType], str] = { _SEAL_TYPES: dict[Type[_SetType], str] = {
YearSeal: "YEAR", YearParts: "YEAR",
MonthSeal: "MONTH", MonthParts: "MONTH",
SealedDate: "DAY", PartialDate: "DAY",
} }
_DATE_CLASSES: dict[Any, Type[SealedDate]] = { _DATE_CLASSES: dict[Any, Type[PartialDate]] = {
"YEAR": YearSeal, "YEAR": YearParts,
"MONTH": MonthSeal, "MONTH": MonthParts,
} }
def __init__(self, field: models.Field[_SetType, _GetType]): def __init__(self, field: models.Field[_SetType, _GetType]):
@ -179,12 +179,12 @@ class PartialDateDescriptor:
value = instance.__dict__.get(self.field.attname) value = instance.__dict__.get(self.field.attname)
if not value or isinstance(value, SealedDate): if not value or isinstance(value, PartialDate):
return value return value
# use precision field to construct SealedDate. # use precision field to construct PartialDate.
seal_type = getattr(instance, self.precision_field, None) seal_type = getattr(instance, self.precision_field, None)
date_class = self._DATE_CLASSES.get(seal_type, SealedDate) date_class = self._DATE_CLASSES.get(seal_type, PartialDate)
return date_class.from_datetime(value) # FIXME: drop datetimes. return date_class.from_datetime(value) # FIXME: drop datetimes.
@ -216,7 +216,7 @@ class PartialDateDescriptor:
class PartialDateModel(models.DateTimeField): # type: ignore class PartialDateModel(models.DateTimeField): # type: ignore
"""a date field for Django models, using SealedDate as values""" """a date field for Django models, using PartialDate as values"""
descriptor_class = PartialDateDescriptor descriptor_class = PartialDateDescriptor