From dac91406c7f307c35d86ebc364596e6d5fac04bf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 15 Dec 2025 22:05:33 -0600 Subject: [PATCH] feat: add `localtime()` function, app method --- src/wuttjamaican/app.py | 13 ++++++++++ src/wuttjamaican/util.py | 54 ++++++++++++++++++++++++++++++++++++++++ tests/test_app.py | 10 ++++++++ tests/test_util.py | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 0056cb8..69e6218 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -35,6 +35,7 @@ from importlib.metadata import version import humanize from wuttjamaican.util import ( + localtime, load_entry_points, load_object, make_title, @@ -527,6 +528,14 @@ class AppHandler: # pylint: disable=too-many-public-methods """ return make_full_name(*parts) + def localtime(self, dt=None, tzinfo=True): + """ + This returns a datetime in the system-local timezone. It is a + convenience wrapper around + :func:`~wuttjamaican.util.localtime()`. + """ + return localtime(dt=dt, tzinfo=tzinfo) + def make_utc(self, dt=None, tzinfo=False): """ This returns a datetime local to the UTC timezone. It is a @@ -799,6 +808,10 @@ class AppHandler: # pylint: disable=too-many-public-methods """ if value is None: return "" + + if not value.tzinfo: + value = self.localtime(value) + return value.strftime(self.display_format_datetime) def render_error(self, error): diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index fd63ff2..2f8e509 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -190,6 +190,58 @@ def make_full_name(*parts): return " ".join(parts) +def localtime(dt=None, tzinfo=True): + """ + This returns a datetime in the system-local timezone. By default + it will be *zone-aware*. + + See also the shortcut + :meth:`~wuttjamaican.app.AppHandler.localtime()` method on the app + handler. + + See also :func:`make_utc()` which is sort of the inverse. + + :param dt: Optional :class:`python:datetime.datetime` instance. + If not specified, the current time will be used. + + :param tzinfo: Boolean indicating whether the return value should + have its :attr:`~python:datetime.datetime.tzinfo` attribute + set. This is true by default in which case the return value + will be zone-aware. + + :returns: :class:`python:datetime.datetime` instance in + system-local timezone. + """ + # thanks to this stackoverflow post for the timezone logic, + # since as of now we don't have that anywhere in config. + # https://stackoverflow.com/a/39079819 + # https://docs.python.org/3/library/datetime.html#datetime.datetime.astimezone + + # use current time if none provided + if dt is None: + dt = datetime.datetime.now(datetime.timezone.utc) + dt = dt.astimezone() + if tzinfo: + return dt + return dt.replace(tzinfo=None) + + # otherwise may need to convert timezone + if dt.tzinfo: + dt = dt.astimezone() + if tzinfo: + return dt + return dt.replace(tzinfo=None) + + # naive value returned as-is.. + if not tzinfo: + return dt + + # ..unless tzinfo is wanted, in which case this assumes naive + # value is in the UTC timezone + dt = dt.replace(tzinfo=datetime.timezone.utc) + return dt.astimezone() + + def make_utc(dt=None, tzinfo=False): """ This returns a datetime local to the UTC timezone. By default it @@ -200,6 +252,8 @@ def make_utc(dt=None, tzinfo=False): :meth:`~wuttjamaican.app.AppHandler.make_utc()` method on the app handler. + See also :func:`localtime()` which is sort of the inverse. + :param dt: Optional :class:`python:datetime.datetime` instance. If not specified, the current time will be used. diff --git a/tests/test_app.py b/tests/test_app.py index 936d79d..8bafb2c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -426,6 +426,11 @@ app_title = WuttaTest name = self.app.make_full_name("Fred", "", "Flintstone", "") self.assertEqual(name, "Fred Flintstone") + def test_localtime(self): + dt = self.app.localtime() + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNotNone(dt.tzinfo) + def test_make_utc(self): dt = self.app.make_utc() self.assertIsInstance(dt, datetime.datetime) @@ -516,6 +521,11 @@ app_title = WuttaTest dt = datetime.datetime(2024, 12, 11, 8, 30, tzinfo=datetime.timezone.utc) self.assertEqual(self.app.render_datetime(dt), "2024-12-11 08:30+0000") + dt = datetime.datetime(2024, 12, 11, 8, 30) + text = self.app.render_datetime(dt) + # TODO: should override local timezone for more complete test + self.assertTrue(text.startswith("2024-12-")) + def test_render_error(self): # with description diff --git a/tests/test_util.py b/tests/test_util.py index e2b9095..a73b681 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -165,6 +165,51 @@ class TestLoadObject(TestCase): self.assertIs(result, TestCase) +class TestLocaltime(TestCase): + + def test_current_time(self): + + # has tzinfo by default + dt = mod.localtime() + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNotNone(dt.tzinfo) + now = datetime.datetime.now() + self.assertAlmostEqual(int(dt.timestamp()), int(now.timestamp())) + + # no tzinfo + dt = mod.localtime(tzinfo=False) + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNone(dt.tzinfo) + now = datetime.datetime.now() + self.assertAlmostEqual(int(dt.timestamp()), int(now.timestamp())) + + def test_convert_with_tzinfo(self): + sample = datetime.datetime(2024, 9, 15, 13, 30, tzinfo=datetime.timezone.utc) + + # has tzinfo by default + dt = mod.localtime(sample) + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNotNone(dt.tzinfo) + + # no tzinfo + dt = mod.localtime(sample, tzinfo=False) + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNone(dt.tzinfo) + + def test_convert_without_tzinfo(self): + sample = datetime.datetime(2024, 9, 15, 13, 30) + + # has tzinfo by default + dt = mod.localtime(sample) + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNotNone(dt.tzinfo) + + # no tzinfo + dt = mod.localtime(sample, tzinfo=False) + self.assertIsInstance(dt, datetime.datetime) + self.assertIsNone(dt.tzinfo) + + class TestMakeUTC(TestCase): def test_current_time(self):