diff --git a/fluent.runtime/fluent/runtime/types.py b/fluent.runtime/fluent/runtime/types.py index e57e653c..39c27d25 100644 --- a/fluent.runtime/fluent/runtime/types.py +++ b/fluent.runtime/fluent/runtime/types.py @@ -1,5 +1,5 @@ import warnings -from datetime import date, datetime +from datetime import date, datetime, time from decimal import Decimal import attr @@ -297,8 +297,11 @@ class FluentDateType(FluentType): # So we leave those alone, and implement another `_init_options` # which is called from other constructors. def _init_options(self, dt_obj: Union[date, datetime], kwargs: Dict[str, Any]) -> None: - if 'timeStyle' in kwargs and not isinstance(self, datetime): - raise TypeError("timeStyle option can only be specified for datetime instances, not date instance") + if 'timeStyle' in kwargs and not isinstance(self, (datetime, time)): + raise TypeError("timeStyle option can only be specified for datetime or time instances, not date instance") + + if 'dateStyle' in kwargs and not isinstance(self, (datetime, date)): + raise TypeError("dateStyle option can only be specified for datetime or time instances, not date instance") self.options = merge_options(DateFormatOptions, getattr(dt_obj, 'options', None), @@ -308,7 +311,7 @@ def _init_options(self, dt_obj: Union[date, datetime], kwargs: Dict[str, Any]) - warnings.warn(f"FluentDateType option {k} is not yet supported") def format(self, locale: Locale) -> str: - if isinstance(self, datetime): + if isinstance(self, (datetime, time)): selftz = _ensure_datetime_tzinfo(self, tzinfo=self.options.timeZone) else: selftz = cast(datetime, self) @@ -316,10 +319,10 @@ def format(self, locale: Locale) -> str: ds = self.options.dateStyle ts = self.options.timeStyle if ds is None: - if ts is None: + if ts is None and not isinstance(selftz, time): return format_date(selftz, format='medium', locale=locale) else: - return format_time(selftz, format=ts, locale=locale) + return format_time(selftz, format=ts or 'short', locale=locale) elif ts is None: return format_date(selftz, format=ds, locale=locale) @@ -333,7 +336,7 @@ def format(self, locale: Locale) -> str: .replace('{1}', format_date(selftz, ds, locale=locale))) -def _ensure_datetime_tzinfo(dt: datetime, tzinfo: Union[str, None] = None) -> datetime: +def _ensure_datetime_tzinfo(dt: Union[datetime, time], tzinfo: Union[str, None] = None) -> Union[datetime, time]: """ Ensure the datetime passed has an attached tzinfo. """ @@ -353,6 +356,15 @@ def from_date(cls, dt_obj: date, **kwargs: Any) -> 'FluentDate': return obj +class FluentTime(FluentDateType, time): + @classmethod + def from_time(cls, dt_obj: time, **kwargs) -> 'FluentTime': + obj = cls(dt_obj.hour, dt_obj.minute, dt_obj.second, + dt_obj.microsecond, tzinfo=dt_obj.tzinfo) + obj._init_options(dt_obj, kwargs) + return obj + + class FluentDateTime(FluentDateType, datetime): @classmethod def from_date_time(cls, dt_obj: datetime, **kwargs: Any) -> 'FluentDateTime': @@ -371,6 +383,8 @@ def fluent_date( return dt if isinstance(dt, datetime): return FluentDateTime.from_date_time(dt, **kwargs) + elif isinstance(dt, time): + return FluentTime.from_time(dt, **kwargs) elif isinstance(dt, date): return FluentDate.from_date(dt, **kwargs) elif isinstance(dt, FluentNone): diff --git a/fluent.runtime/tests/test_types.py b/fluent.runtime/tests/test_types.py index 4ab356f9..7502f01e 100644 --- a/fluent.runtime/tests/test_types.py +++ b/fluent.runtime/tests/test_types.py @@ -1,6 +1,6 @@ import unittest import warnings -from datetime import date, datetime +from datetime import date, datetime, time from decimal import Decimal import pytz @@ -166,6 +166,8 @@ def setUp(self): self.a_date = date(2018, 2, 1) self.a_datetime = datetime(2018, 2, 1, 14, 15, 16, 123456, tzinfo=pytz.UTC) + self.a_time = time(10, 31, 00, 333, + tzinfo=pytz.UTC) def test_date(self): fd = fluent_date(self.a_date) @@ -175,6 +177,16 @@ def test_date(self): self.assertEqual(fd.month, self.a_date.month) self.assertEqual(fd.day, self.a_date.day) + def test_time(self): + fd = fluent_date(self.a_time) + self.assertTrue(isinstance(fd, time)) + self.assertTrue(isinstance(fd, FluentDateType)) + self.assertEqual(fd.hour, self.a_time.hour) + self.assertEqual(fd.minute, self.a_time.minute) + self.assertEqual(fd.second, self.a_time.second) + self.assertEqual(fd.microsecond, self.a_time.microsecond) + self.assertEqual(fd.tzinfo, self.a_time.tzinfo) + def test_datetime(self): fd = fluent_date(self.a_datetime) self.assertTrue(isinstance(fd, datetime)) @@ -188,13 +200,27 @@ def test_datetime(self): self.assertEqual(fd.microsecond, self.a_datetime.microsecond) self.assertEqual(fd.tzinfo, self.a_datetime.tzinfo) - def test_format_defaults(self): + def test_date_format_defaults(self): fd = fluent_date(self.a_date) en_US = Locale.parse('en_US') en_GB = Locale.parse('en_GB') self.assertEqual(fd.format(en_GB), '1 Feb 2018') self.assertEqual(fd.format(en_US), 'Feb 1, 2018') + def test_time_format_defaults(self): + fd = fluent_date(self.a_time) + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_GB), '10:31') + self.assertRegex(fd.format(en_US), '^10:31\\sAM$') + + def test_datetime_format_defaults(self): + fd = fluent_date(self.a_datetime) + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertEqual(fd.format(en_GB), '1 Feb 2018') + self.assertEqual(fd.format(en_US), 'Feb 1, 2018') + def test_dateStyle_date(self): fd = fluent_date(self.a_date, dateStyle='long') en_US = Locale.parse('en_US') @@ -216,6 +242,13 @@ def test_timeStyle_datetime(self): self.assertRegex(fd.format(en_US), '^2:15\\sPM$') self.assertEqual(fd.format(en_GB), '14:15') + def test_timeStyle_time(self): + fd = fluent_date(self.a_datetime.time(), timeStyle='short') + en_US = Locale.parse('en_US') + en_GB = Locale.parse('en_GB') + self.assertRegex(fd.format(en_US), '^2:15\\sPM$') + self.assertEqual(fd.format(en_GB), '14:15') + def test_dateStyle_and_timeStyle_datetime(self): fd = fluent_date(self.a_datetime, timeStyle='short', dateStyle='short') en_US = Locale.parse('en_US')