feat: add "complete" timezone support
at least for now, this is all we need i think.. if nothing configured, will fallback to default system local timezone. or can configure the default, or alternate(s) as needed. some day when we drop support for python 3.8, can also remove the python-dateutil dependency..
This commit is contained in:
parent
dac91406c7
commit
0ffc72f766
6 changed files with 396 additions and 87 deletions
|
|
@ -30,6 +30,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|||
|
||||
intersphinx_mapping = {
|
||||
"alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
|
||||
"dateutil": ("https://dateutil.readthedocs.io/en/stable/", None),
|
||||
"humanize": ("https://humanize.readthedocs.io/en/stable/", None),
|
||||
"mako": ("https://docs.makotemplates.org/en/latest/", None),
|
||||
"packaging": ("https://packaging.python.org/en/latest/", None),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ classifiers = [
|
|||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
'python-dateutil; python_version < "3.9"',
|
||||
"humanize",
|
||||
'importlib-metadata; python_version < "3.10"',
|
||||
"importlib_resources ; python_version < '3.9'",
|
||||
|
|
@ -42,7 +43,7 @@ dependencies = [
|
|||
[project.optional-dependencies]
|
||||
db = ["SQLAlchemy", "alembic", "alembic-postgresql-enum"]
|
||||
docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"]
|
||||
tests = ["pylint", "pytest", "pytest-cov", "tox"]
|
||||
tests = ["pylint", "pytest", "pytest-cov", "tox", "python-dateutil"]
|
||||
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ WuttJamaican - app handler
|
|||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -35,6 +36,7 @@ from importlib.metadata import version
|
|||
import humanize
|
||||
|
||||
from wuttjamaican.util import (
|
||||
get_timezone_by_name,
|
||||
localtime,
|
||||
load_entry_points,
|
||||
load_object,
|
||||
|
|
@ -110,6 +112,7 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
|||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.handlers = {}
|
||||
self.timezones = {}
|
||||
|
||||
@property
|
||||
def appname(self):
|
||||
|
|
@ -528,19 +531,113 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
|||
"""
|
||||
return make_full_name(*parts)
|
||||
|
||||
def localtime(self, dt=None, tzinfo=True):
|
||||
def get_timezone(self, key="default"):
|
||||
"""
|
||||
This returns a datetime in the system-local timezone. It is a
|
||||
convenience wrapper around
|
||||
:func:`~wuttjamaican.util.localtime()`.
|
||||
Get the configured (or system default) timezone object.
|
||||
|
||||
This checks config for a setting which corresponds to the
|
||||
given ``key``, then calls
|
||||
:func:`~wuttjamaican.util.get_timezone_by_name()` to get the
|
||||
actual timezone object.
|
||||
|
||||
The default key corresponds to the true "local" timezone, but
|
||||
other keys may correspond to other configured timezones (if
|
||||
applicable).
|
||||
|
||||
As a special case for the default key only: If no config value
|
||||
is found, Python itself will determine the default system
|
||||
local timezone.
|
||||
|
||||
For any non-default key, an error is raised if no config value
|
||||
is found.
|
||||
|
||||
.. note::
|
||||
|
||||
The app handler *caches* all timezone objects, to avoid
|
||||
unwanted repetitive lookups when processing multiple
|
||||
datetimes etc. (Since this method is called by
|
||||
:meth:`localtime()`.) Therefore whenever timezone config
|
||||
values are changed, an app restart will be necessary.
|
||||
|
||||
Example config:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[wutta]
|
||||
timezone.default = America/Chicago
|
||||
timezone.westcoast = America/Los_Angeles
|
||||
|
||||
Example usage::
|
||||
|
||||
tz_default = app.get_timezone()
|
||||
tz_westcoast = app.get_timezone("westcoast")
|
||||
|
||||
See also :meth:`get_timezone_name()`.
|
||||
|
||||
:param key: Config key for desired timezone.
|
||||
|
||||
:returns: :class:`python:datetime.tzinfo` instance
|
||||
"""
|
||||
return localtime(dt=dt, tzinfo=tzinfo)
|
||||
if key not in self.timezones:
|
||||
setting = f"{self.appname}.timezone.{key}"
|
||||
tzname = self.config.get(setting)
|
||||
if tzname:
|
||||
self.timezones[key] = get_timezone_by_name(tzname)
|
||||
|
||||
elif key == "default":
|
||||
# fallback to system default
|
||||
self.timezones[key] = datetime.datetime.now().astimezone().tzinfo
|
||||
|
||||
else:
|
||||
# alternate key was specified, but no config found, so check
|
||||
# again with require() to force error
|
||||
self.timezones[key] = self.config.require(setting)
|
||||
|
||||
return self.timezones[key]
|
||||
|
||||
def get_timezone_name(self, key="default"):
|
||||
"""
|
||||
Get the display name for the configured (or system default)
|
||||
timezone.
|
||||
|
||||
This calls :meth:`get_timezone()` and then uses some
|
||||
heuristics to determine the name.
|
||||
|
||||
:param key: Config key for desired timezone.
|
||||
|
||||
:returns: String name for the timezone.
|
||||
"""
|
||||
tz = self.get_timezone(key=key)
|
||||
try:
|
||||
# TODO: this should work for zoneinfo.ZoneInfo objects,
|
||||
# but not sure yet about dateutils.tz ?
|
||||
return tz.key
|
||||
except AttributeError:
|
||||
# this should work for system default fallback, afaik
|
||||
dt = datetime.datetime.now(tz)
|
||||
return dt.tzname()
|
||||
|
||||
def localtime(self, dt=None, local_zone=None, **kw):
|
||||
"""
|
||||
This produces a datetime in the "local" timezone.
|
||||
|
||||
This is a convenience wrapper around
|
||||
:func:`~wuttjamaican.util.localtime()`; however it also calls
|
||||
:meth:`get_timezone()` to override the ``local_zone`` param
|
||||
(unless caller specifies that).
|
||||
|
||||
See also :meth:`make_utc()` which is sort of the inverse.
|
||||
"""
|
||||
kw["local_zone"] = local_zone or self.get_timezone()
|
||||
return localtime(dt=dt, **kw)
|
||||
|
||||
def make_utc(self, dt=None, tzinfo=False):
|
||||
"""
|
||||
This returns a datetime local to the UTC timezone. It is a
|
||||
convenience wrapper around
|
||||
:func:`~wuttjamaican.util.make_utc()`.
|
||||
|
||||
See also :meth:`localtime()` which is sort of the inverse.
|
||||
"""
|
||||
return make_utc(dt=dt, tzinfo=tzinfo)
|
||||
|
||||
|
|
@ -795,7 +892,7 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
|||
return ""
|
||||
return value.strftime(self.display_format_date)
|
||||
|
||||
def render_datetime(self, value):
|
||||
def render_datetime(self, value, local=True):
|
||||
"""
|
||||
Return a human-friendly display string for the given datetime.
|
||||
|
||||
|
|
@ -804,14 +901,16 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
|||
:param value: A :class:`python:datetime.datetime` instance (or
|
||||
``None``).
|
||||
|
||||
:returns: Display string.
|
||||
:param local: By default the ``value`` will first be passed to
|
||||
:meth:`localtime()` to normalize it for display. Specify
|
||||
``local=False`` to skip that and render the value as-is.
|
||||
|
||||
:returns: Rendered datetime as string.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
if not value.tzinfo:
|
||||
if local:
|
||||
value = self.localtime(value)
|
||||
|
||||
return value.strftime(self.display_format_datetime)
|
||||
|
||||
def render_error(self, error):
|
||||
|
|
|
|||
|
|
@ -190,10 +190,40 @@ def make_full_name(*parts):
|
|||
return " ".join(parts)
|
||||
|
||||
|
||||
def localtime(dt=None, tzinfo=True):
|
||||
def get_timezone_by_name(tzname):
|
||||
"""
|
||||
This returns a datetime in the system-local timezone. By default
|
||||
it will be *zone-aware*.
|
||||
Retrieve a timezone object by name.
|
||||
|
||||
This is mostly a compatibility wrapper, since older Python is
|
||||
missing the :mod:`python:zoneinfo` module.
|
||||
|
||||
For Python 3.9 and newer, this instantiates
|
||||
:class:`python:zoneinfo.ZoneInfo`.
|
||||
|
||||
For Python 3.8, this calls :func:`dateutil:dateutil.tz.gettz()`.
|
||||
|
||||
See also :meth:`~wuttjamaican.app.AppHandler.get_timezone()` on
|
||||
the app handler.
|
||||
|
||||
:param tzname: String name for timezone.
|
||||
|
||||
:returns: :class:`python:datetime.tzinfo` instance
|
||||
"""
|
||||
try:
|
||||
from zoneinfo import ZoneInfo # pylint: disable=import-outside-toplevel
|
||||
|
||||
return ZoneInfo(tzname)
|
||||
|
||||
except ImportError: # python 3.8
|
||||
from dateutil.tz import gettz # pylint: disable=import-outside-toplevel
|
||||
|
||||
return gettz(tzname)
|
||||
|
||||
|
||||
def localtime(dt=None, from_utc=True, want_tzinfo=True, local_zone=None):
|
||||
"""
|
||||
This produces a datetime in the "local" timezone. By default it
|
||||
will be *zone-aware*.
|
||||
|
||||
See also the shortcut
|
||||
:meth:`~wuttjamaican.app.AppHandler.localtime()` method on the app
|
||||
|
|
@ -204,42 +234,49 @@ def localtime(dt=None, tzinfo=True):
|
|||
: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.
|
||||
:param from_utc: Boolean indicating whether a naive ``dt`` is
|
||||
already (effectively) in UTC timezone. Set this to false when
|
||||
providing a naive ``dt`` which is already in "local" timezone
|
||||
instead of UTC. This flag is ignored if ``dt`` is zone-aware.
|
||||
|
||||
:returns: :class:`python:datetime.datetime` instance in
|
||||
system-local timezone.
|
||||
:param want_tzinfo: Boolean indicating whether the resulting
|
||||
datetime should have its
|
||||
:attr:`~python:datetime.datetime.tzinfo` attribute set. Set
|
||||
this to false if you want a naive value; it's true by default,
|
||||
for zone-aware.
|
||||
|
||||
:param local_zone: Optional :class:`python:datetime.tzinfo`
|
||||
instance to use as "local" timezone, instead of relying on
|
||||
Python to determine the system local timezone.
|
||||
|
||||
:returns: :class:`python:datetime.datetime` instance in 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
|
||||
# set dt's timezone if needed
|
||||
if not dt.tzinfo:
|
||||
# UTC is default assumption unless caller says otherwise
|
||||
if from_utc:
|
||||
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
||||
return dt.astimezone()
|
||||
elif local_zone:
|
||||
dt = dt.replace(tzinfo=local_zone)
|
||||
else: # default system local timezone
|
||||
tz = dt.astimezone().tzinfo
|
||||
dt = dt.replace(tzinfo=tz)
|
||||
|
||||
# convert to local timezone
|
||||
if local_zone:
|
||||
dt = dt.astimezone(local_zone)
|
||||
else:
|
||||
dt = dt.astimezone()
|
||||
|
||||
# maybe strip tzinfo
|
||||
if want_tzinfo:
|
||||
return dt
|
||||
return dt.replace(tzinfo=None)
|
||||
|
||||
|
||||
def make_utc(dt=None, tzinfo=False):
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ from mako.template import Template
|
|||
|
||||
import wuttjamaican.enum
|
||||
from wuttjamaican import app as mod
|
||||
from wuttjamaican.exc import ConfigurationError
|
||||
from wuttjamaican.progress import ProgressBase
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.util import UNSPECIFIED
|
||||
from wuttjamaican.util import UNSPECIFIED, get_timezone_by_name
|
||||
from wuttjamaican.testing import FileTestCase, ConfigTestCase
|
||||
from wuttjamaican.batch import BatchHandler
|
||||
|
||||
|
|
@ -426,6 +427,80 @@ app_title = WuttaTest
|
|||
name = self.app.make_full_name("Fred", "", "Flintstone", "")
|
||||
self.assertEqual(name, "Fred Flintstone")
|
||||
|
||||
def test_get_timezone(self):
|
||||
# cache is empty at first
|
||||
self.assertEqual(self.app.timezones, {})
|
||||
|
||||
# fetch default system local timezone
|
||||
# nb. actual value depends on machine where tests run
|
||||
system = self.app.get_timezone()
|
||||
self.assertIsInstance(system, datetime.tzinfo)
|
||||
# cache no longer empty
|
||||
self.assertEqual(len(self.app.timezones), 1)
|
||||
self.assertIn("default", self.app.timezones)
|
||||
self.assertIs(self.app.timezones["default"], system)
|
||||
|
||||
# fetch configured default
|
||||
self.app.timezones.clear() # clear cache
|
||||
self.config.setdefault("wuttatest.timezone.default", "Africa/Addis_Ababa")
|
||||
default = self.app.get_timezone()
|
||||
self.assertIsInstance(default, datetime.tzinfo)
|
||||
dt = datetime.datetime(2025, 12, 16, 22, 0, tzinfo=default)
|
||||
self.assertEqual(default.utcoffset(dt), datetime.timedelta(hours=3))
|
||||
# cache no longer empty
|
||||
self.assertEqual(len(self.app.timezones), 1)
|
||||
self.assertIn("default", self.app.timezones)
|
||||
self.assertIs(self.app.timezones["default"], default)
|
||||
# fetching again gives cached instance
|
||||
self.assertIs(self.app.get_timezone(), default)
|
||||
|
||||
# fetch configured alternate
|
||||
self.config.setdefault("wuttatest.timezone.foo", "America/New_York")
|
||||
foo = self.app.get_timezone("foo")
|
||||
self.assertIsInstance(foo, datetime.tzinfo)
|
||||
self.assertIn("foo", self.app.timezones)
|
||||
self.assertIs(self.app.timezones["foo"], foo)
|
||||
|
||||
# error if alternate not configured
|
||||
self.assertRaises(ConfigurationError, self.app.get_timezone, "bar")
|
||||
self.assertNotIn("bar", self.app.timezones)
|
||||
|
||||
def test_get_timezone_name(self):
|
||||
# cache is empty at first
|
||||
self.assertEqual(self.app.timezones, {})
|
||||
|
||||
# fetch default system local timezone
|
||||
# nb. actual value depends on machine where tests run
|
||||
system = self.app.get_timezone_name()
|
||||
self.assertIsInstance(system, str)
|
||||
self.assertLess(0, len(system))
|
||||
# cache no longer empty
|
||||
self.assertEqual(len(self.app.timezones), 1)
|
||||
self.assertIn("default", self.app.timezones)
|
||||
|
||||
# fetch configured default
|
||||
self.app.timezones.clear() # clear cache
|
||||
self.config.setdefault("wuttatest.timezone.default", "Africa/Addis_Ababa")
|
||||
default = self.app.get_timezone_name()
|
||||
# nb. this check won't work for python 3.8
|
||||
if sys.version_info >= (3, 9):
|
||||
self.assertEqual(default, "Africa/Addis_Ababa")
|
||||
# cache no longer empty
|
||||
self.assertEqual(len(self.app.timezones), 1)
|
||||
self.assertIn("default", self.app.timezones)
|
||||
|
||||
# fetch configured alternate
|
||||
self.config.setdefault("wuttatest.timezone.foo", "America/New_York")
|
||||
foo = self.app.get_timezone_name("foo")
|
||||
# nb. this check won't work for python 3.8
|
||||
if sys.version_info >= (3, 9):
|
||||
self.assertEqual(foo, "America/New_York")
|
||||
self.assertIn("foo", self.app.timezones)
|
||||
|
||||
# error if alternate not configured
|
||||
self.assertRaises(ConfigurationError, self.app.get_timezone_name, "bar")
|
||||
self.assertNotIn("bar", self.app.timezones)
|
||||
|
||||
def test_localtime(self):
|
||||
dt = self.app.localtime()
|
||||
self.assertIsInstance(dt, datetime.datetime)
|
||||
|
|
@ -516,15 +591,38 @@ app_title = WuttaTest
|
|||
self.assertEqual(self.app.render_date(dt), "2024-12-11")
|
||||
|
||||
def test_render_datetime(self):
|
||||
tzlocal = get_timezone_by_name("America/Los_Angeles")
|
||||
with patch.object(self.app, "get_timezone", return_value=tzlocal):
|
||||
|
||||
# null value
|
||||
self.assertEqual(self.app.render_datetime(None), "")
|
||||
|
||||
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")
|
||||
# naive UTC
|
||||
dt = datetime.datetime(2024, 12, 17, 1, 12)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=True), "2024-12-16 17:12-0800"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=False), "2024-12-17 01:12"
|
||||
)
|
||||
|
||||
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-"))
|
||||
# aware UTC
|
||||
dt = datetime.datetime(2024, 12, 17, 1, 12, tzinfo=datetime.timezone.utc)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=True), "2024-12-16 17:12-0800"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=False), "2024-12-17 01:12+0000"
|
||||
)
|
||||
|
||||
# aware local
|
||||
dt = datetime.datetime(2024, 12, 16, 19, 12, tzinfo=tzlocal)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=True), "2024-12-16 19:12-0800"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.app.render_datetime(dt, local=False), "2024-12-16 19:12-0800"
|
||||
)
|
||||
|
||||
def test_render_error(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -165,49 +165,122 @@ class TestLoadObject(TestCase):
|
|||
self.assertIs(result, TestCase)
|
||||
|
||||
|
||||
class TestGetTimezoneByName(TestCase):
|
||||
|
||||
def test_modern(self):
|
||||
try:
|
||||
import zoneinfo
|
||||
except ImportError:
|
||||
self.assertLess(sys.version_info, (3, 9))
|
||||
pytest.skip("this test is not relevant before python 3.9")
|
||||
|
||||
tz = mod.get_timezone_by_name("America/Chicago")
|
||||
self.assertIsInstance(tz, zoneinfo.ZoneInfo)
|
||||
self.assertIsInstance(tz, datetime.tzinfo)
|
||||
self.assertEqual(tz.key, "America/Chicago")
|
||||
|
||||
def test_legacy(self):
|
||||
import dateutil.tz
|
||||
|
||||
orig_import = __import__
|
||||
|
||||
def mock_import(name, *args, **kwargs):
|
||||
if name == "zoneinfo":
|
||||
raise ImportError
|
||||
return orig_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=mock_import):
|
||||
tz = mod.get_timezone_by_name("America/Chicago")
|
||||
self.assertIsInstance(tz, dateutil.tz.tzfile)
|
||||
self.assertIsInstance(tz, datetime.tzinfo)
|
||||
dt = datetime.datetime.now(tz)
|
||||
self.assertIn(dt.tzname(), ["CDT", "CST"])
|
||||
|
||||
|
||||
class TestLocaltime(TestCase):
|
||||
|
||||
def test_naive_utc(self):
|
||||
# nb. must override local_zone for test consistency
|
||||
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||
dt = datetime.datetime(2025, 12, 16, 0, 16) # utc
|
||||
result = mod.localtime(dt, local_zone=tz)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIs(result.tzinfo, tz)
|
||||
self.assertEqual(result, datetime.datetime(2025, 12, 15, 19, 16, tzinfo=tz))
|
||||
|
||||
def test_naive_local(self):
|
||||
# nb. must override local_zone for test consistency
|
||||
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||
dt = datetime.datetime(2025, 12, 15, 19, 16) # local
|
||||
|
||||
# can test precisely when overriding local_zone
|
||||
result = mod.localtime(dt, local_zone=tz, from_utc=False)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIs(result.tzinfo, tz)
|
||||
self.assertEqual(result, datetime.datetime(2025, 12, 15, 19, 16, tzinfo=tz))
|
||||
|
||||
# must test loosely for fallback to system local timezone
|
||||
result = mod.localtime(dt, from_utc=False)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIsInstance(result.tzinfo, datetime.tzinfo)
|
||||
self.assertEqual(result.year, 2025)
|
||||
self.assertEqual(result.month, 12)
|
||||
|
||||
def test_aware_utc(self):
|
||||
# nb. must override local_zone for test consistency
|
||||
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||
dt = datetime.datetime(2025, 12, 16, 0, 16, tzinfo=datetime.timezone.utc)
|
||||
result = mod.localtime(dt, local_zone=tz)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIs(result.tzinfo, tz)
|
||||
self.assertEqual(result, datetime.datetime(2025, 12, 15, 19, 16, tzinfo=tz))
|
||||
|
||||
def test_aware_local(self):
|
||||
# nb. must override local_zone for test consistency
|
||||
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||
other = datetime.timezone(-datetime.timedelta(hours=7))
|
||||
dt = datetime.datetime(2025, 12, 15, 17, 16, tzinfo=other)
|
||||
|
||||
# can test precisely when overriding local_zone
|
||||
result = mod.localtime(dt, local_zone=tz)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIs(result.tzinfo, tz)
|
||||
self.assertEqual(result, datetime.datetime(2025, 12, 15, 19, 16, tzinfo=tz))
|
||||
|
||||
# must test loosely for fallback to system local timezone
|
||||
result = mod.localtime(dt)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIsInstance(result.tzinfo, datetime.tzinfo)
|
||||
self.assertEqual(result.year, 2025)
|
||||
self.assertEqual(result.month, 12)
|
||||
|
||||
def test_current_time(self):
|
||||
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||
|
||||
# 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()))
|
||||
# overriding local_zone
|
||||
result = mod.localtime(local_zone=tz)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIs(result.tzinfo, tz)
|
||||
|
||||
# fallback to system local timezone
|
||||
result = mod.localtime()
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
self.assertIsInstance(result.tzinfo, datetime.tzinfo)
|
||||
self.assertIsNot(result.tzinfo, tz)
|
||||
|
||||
def test_want_tzinfo(self):
|
||||
|
||||
# wants tzinfo implicitly
|
||||
result = mod.localtime()
|
||||
self.assertIsInstance(result.tzinfo, datetime.tzinfo)
|
||||
|
||||
# wants tzinfo explicitly
|
||||
result = mod.localtime(want_tzinfo=True)
|
||||
self.assertIsInstance(result.tzinfo, datetime.tzinfo)
|
||||
|
||||
# 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)
|
||||
result = mod.localtime(want_tzinfo=False)
|
||||
self.assertIsNone(result.tzinfo)
|
||||
|
||||
|
||||
class TestMakeUTC(TestCase):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue