diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 0857b6b64..493a8e5c2 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -21,7 +21,7 @@ from bookwyrm import activitypub from bookwyrm.connectors import get_image from bookwyrm.utils.sanitizer import clean from bookwyrm.utils.sealed_date import ( - SealedDate, + PartialDate, PartialDateModel, from_partial_isoformat, ) @@ -560,6 +560,7 @@ class PartialDateField(ActivitypubFieldMixin, PartialDateModel): return value.partial_isoformat() if value else None def field_from_activity(self, value, allow_external_connections=True): + # pylint: disable=no-else-return try: return from_partial_isoformat(value) except ValueError: @@ -572,10 +573,10 @@ class PartialDateField(ActivitypubFieldMixin, PartialDateModel): return None if timezone.is_aware(parsed): - return SealedDate.from_datetime(parsed) + return PartialDate.from_datetime(parsed) else: # 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": # clearly Oct 1st, not Sep 30th (an unwanted side-effect of USE_TZ). It's diff --git a/bookwyrm/templatetags/date_ext.py b/bookwyrm/templatetags/date_ext.py index 4167893c8..d4ca988de 100644 --- a/bookwyrm/templatetags/date_ext.py +++ b/bookwyrm/templatetags/date_ext.py @@ -3,15 +3,15 @@ from django import template from django.template import defaultfilters 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.filter(expects_localtime=True, is_safe=False) def naturalday_partial(date): - """allow templates to easily format SealedDate objects""" - if not isinstance(date, SealedDate): + """allow templates to easily format PartialDate objects""" + if not isinstance(date, PartialDate): return defaultfilters.date(date) if date.has_day: fmt = "DATE_FORMAT" diff --git a/bookwyrm/tests/test_sealed_date.py b/bookwyrm/tests/test_sealed_date.py index 7986c15fa..47532f40c 100644 --- a/bookwyrm/tests/test_sealed_date.py +++ b/bookwyrm/tests/test_sealed_date.py @@ -10,8 +10,8 @@ from django.utils import translation from bookwyrm.utils import sealed_date -class SealedDateTest(unittest.TestCase): - """test SealedDate class in isolation""" +class PartialDateTest(unittest.TestCase): + """test PartialDate class in isolation""" # 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) 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("2023-10-20", sealed.partial_isoformat()) self.assertTrue(sealed.has_day) self.assertTrue(sealed.has_month) 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("2023-10", sealed.partial_isoformat()) self.assertFalse(sealed.has_day) self.assertTrue(sealed.has_month) 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("2023", sealed.partial_isoformat()) self.assertFalse(sealed.has_day) @@ -41,7 +41,7 @@ class SealedDateTest(unittest.TestCase): def test_no_naive_datetime(self): 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): parsed = sealed_date.from_partial_isoformat("1995") @@ -99,7 +99,7 @@ class SealedDateTest(unittest.TestCase): class PartialDateFormFieldTest(unittest.TestCase): - """test form support for SealedDate objects""" + """test form support for PartialDate objects""" # pylint: disable=missing-function-docstring @@ -108,32 +108,32 @@ class PartialDateFormFieldTest(unittest.TestCase): self.field = sealed_date.PartialDateFormField() 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)) 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)) 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)) def test_to_python(self): 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()) def test_to_python_month(self): 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()) with self.assertRaises(ValidationError): self.field.to_python("2022-0-25") def test_to_python_year(self): 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()) with self.assertRaises(ValidationError): self.field.to_python("0-05-25") @@ -142,5 +142,5 @@ class PartialDateFormFieldTest(unittest.TestCase): with translation.override("es"): # check super() is called 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()) diff --git a/bookwyrm/utils/sealed_date.py b/bookwyrm/utils/sealed_date.py index a819f7c3b..cd2be8eb0 100644 --- a/bookwyrm/utils/sealed_date.py +++ b/bookwyrm/utils/sealed_date.py @@ -1,4 +1,4 @@ -"""Implementation of the SealedDate class.""" +"""Implementation of the PartialDate class.""" from __future__ import annotations @@ -15,7 +15,7 @@ from django.utils import timezone # pylint: disable=no-else-return __all__ = [ - "SealedDate", + "PartialDate", "PartialDateModel", "from_partial_isoformat", ] @@ -23,14 +23,14 @@ __all__ = [ _partial_re = re.compile(r"(\d{4})(?:-(\d\d?))?(?:-(\d\d?))?$") _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` -class SealedDate(datetime): - """a date object sealed into a certain precision (day, month or year)""" +class PartialDate(datetime): + """a date object bound into a certain precision (day, month or year)""" @property def has_day(self) -> bool: @@ -47,8 +47,8 @@ class SealedDate(datetime): return self.strftime("%Y-%m-%d") @classmethod - def from_datetime(cls: Type[Sealed], dt: datetime) -> Sealed: - """construct a SealedDate object from a timezone-aware datetime + def from_datetime(cls: Type[Partial], dt: datetime) -> Partial: + """construct a PartialDate object from a timezone-aware datetime Use subclasses to specify precision. If `dt` is naive, `ValueError` is raised. @@ -59,18 +59,18 @@ class SealedDate(datetime): return cls.combine(dt.date(), dt.time(), tzinfo=dt.tzinfo) @classmethod - def from_date_parts(cls: Type[Sealed], year: int, month: int, day: int) -> Sealed: - """construct a SealedDate from year, month, day. + def from_date_parts(cls: Type[Partial], year: int, month: int, day: int) -> Partial: + """construct a PartialDate from year, month, day. 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, # current_timezone and default_timezone. return cls.from_datetime(datetime(year, month, day, tzinfo=_westmost_tz)) -class MonthSeal(SealedDate): - """a date sealed into month precision""" +class MonthParts(PartialDate): + """a date bound into month precision""" @property def has_day(self) -> bool: @@ -80,8 +80,8 @@ class MonthSeal(SealedDate): return self.strftime("%Y-%m") -class YearSeal(SealedDate): - """a date sealed into year precision""" +class YearParts(PartialDate): + """a date bound into year precision""" @property def has_month(self) -> bool: @@ -91,8 +91,8 @@ class YearSeal(SealedDate): return self.strftime("%Y") -def from_partial_isoformat(value: str) -> SealedDate: - """construct SealedDate from a partial string. +def from_partial_isoformat(value: str) -> PartialDate: + """construct PartialDate from a partial string. Accepted formats: YYYY, YYYY-MM, YYYY-MM-DD; otherwise `ValueError` 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()] if month < 0: - return YearSeal.from_date_parts(year, 1, 1) + return YearParts.from_date_parts(year, 1, 1) elif day < 0: - return MonthSeal.from_date_parts(year, month, 1) + return MonthParts.from_date_parts(year, month, 1) else: - return SealedDate.from_date_parts(year, month, day) + return PartialDate.from_date_parts(year, month, day) class PartialDateFormField(DateField): - """date form field with support for SealedDate""" + """date form field with support for PartialDate""" def prepare_value(self, value: Any) -> str: # As a convention, Django's `SelectDateWidget` uses "0" for missing - # parts. We piggy-back into that, to make it work with SealedDate. - if not isinstance(value, SealedDate): + # parts. We piggy-back into that, to make it work with PartialDate. + if not isinstance(value, PartialDate): return cast(str, super().prepare_value(value)) elif value.has_day: return value.strftime("%Y-%m-%d") @@ -127,7 +127,7 @@ class PartialDateFormField(DateField): else: return value.strftime("%Y-0-0") - def to_python(self, value: Any) -> Optional[SealedDate]: + def to_python(self, value: Any) -> Optional[PartialDate]: try: date = super().to_python(value) except ValidationError as ex: @@ -136,21 +136,21 @@ class PartialDateFormField(DateField): if not match or (day and not month) or not year: raise ex from None if not month: - return YearSeal.from_date_parts(year, 1, 1) + return YearParts.from_date_parts(year, 1, 1) elif not day: - return MonthSeal.from_date_parts(year, month, 1) + return MonthParts.from_date_parts(year, month, 1) else: if date is None: return None else: 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. _SetType = datetime -_GetType = Optional[SealedDate] +_GetType = Optional[PartialDate] class PartialDateDescriptor: @@ -160,14 +160,14 @@ class PartialDateDescriptor: """ _SEAL_TYPES: dict[Type[_SetType], str] = { - YearSeal: "YEAR", - MonthSeal: "MONTH", - SealedDate: "DAY", + YearParts: "YEAR", + MonthParts: "MONTH", + PartialDate: "DAY", } - _DATE_CLASSES: dict[Any, Type[SealedDate]] = { - "YEAR": YearSeal, - "MONTH": MonthSeal, + _DATE_CLASSES: dict[Any, Type[PartialDate]] = { + "YEAR": YearParts, + "MONTH": MonthParts, } def __init__(self, field: models.Field[_SetType, _GetType]): @@ -179,12 +179,12 @@ class PartialDateDescriptor: value = instance.__dict__.get(self.field.attname) - if not value or isinstance(value, SealedDate): + if not value or isinstance(value, PartialDate): return value - # use precision field to construct SealedDate. + # use precision field to construct PartialDate. 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. @@ -216,7 +216,7 @@ class PartialDateDescriptor: 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