Compare commits
4 commits
dac91406c7
...
39014e5a2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 39014e5a2c | |||
| a779be4a81 | |||
| 8c48a3f318 | |||
| 0ffc72f766 |
9 changed files with 695 additions and 92 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,6 +5,17 @@ All notable changes to WuttJamaican will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v0.26.0 (2025-12-17)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- add "complete" timezone support
|
||||||
|
- add `localtime()` function, app method
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- remove unused import
|
||||||
|
|
||||||
## v0.25.0 (2025-12-15)
|
## v0.25.0 (2025-12-15)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
|
"alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
|
||||||
|
"dateutil": ("https://dateutil.readthedocs.io/en/stable/", None),
|
||||||
"humanize": ("https://humanize.readthedocs.io/en/stable/", None),
|
"humanize": ("https://humanize.readthedocs.io/en/stable/", None),
|
||||||
"mako": ("https://docs.makotemplates.org/en/latest/", None),
|
"mako": ("https://docs.makotemplates.org/en/latest/", None),
|
||||||
"packaging": ("https://packaging.python.org/en/latest/", None),
|
"packaging": ("https://packaging.python.org/en/latest/", None),
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ Contents
|
||||||
glossary
|
glossary
|
||||||
narr/install/index
|
narr/install/index
|
||||||
narr/config/index
|
narr/config/index
|
||||||
|
narr/datetime
|
||||||
narr/db/index
|
narr/db/index
|
||||||
narr/cli/index
|
narr/cli/index
|
||||||
narr/email/index
|
narr/email/index
|
||||||
|
|
|
||||||
279
docs/narr/datetime.rst
Normal file
279
docs/narr/datetime.rst
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
|
||||||
|
===================
|
||||||
|
DateTime Behavior
|
||||||
|
===================
|
||||||
|
|
||||||
|
There must be a way to handle :class:`~python:datetime.datetime` data,
|
||||||
|
such that we can keep straight any timezone(s) which may be involved
|
||||||
|
for the app.
|
||||||
|
|
||||||
|
As a rule, we store datetime values as "naive/UTC" within the
|
||||||
|
:term:`app database`, and convert to "aware/local" as needed for
|
||||||
|
display to the user etc.
|
||||||
|
|
||||||
|
A related rule is that any *naive* datetime is assumed to be UTC. If
|
||||||
|
you have a naive/local value then you should convert it to aware/local
|
||||||
|
or else the framework logic will misinterpret it. (See below,
|
||||||
|
:ref:`convert-to-localtime`.)
|
||||||
|
|
||||||
|
With these rules in place, the workhorse methods are:
|
||||||
|
|
||||||
|
* :meth:`~wuttjamaican.app.AppHandler.localtime()`
|
||||||
|
* :meth:`~wuttjamaican.app.AppHandler.make_utc()`
|
||||||
|
* :meth:`~wuttjamaican.app.AppHandler.render_datetime()`
|
||||||
|
|
||||||
|
|
||||||
|
Time Zone Config/Lookup
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Technically no config is required; the default timezone can be gleaned
|
||||||
|
from the OS::
|
||||||
|
|
||||||
|
# should always return *something* :)
|
||||||
|
tz_default = app.get_timezone()
|
||||||
|
|
||||||
|
The default (aka. local) timezone is used by
|
||||||
|
:meth:`~wuttjamaican.app.AppHandler.localtime()` and therefore also
|
||||||
|
:meth:`~wuttjamaican.app.AppHandler.render_datetime()`.
|
||||||
|
|
||||||
|
Config can override the default/local timezone; it's assumed most apps
|
||||||
|
will do this. If desired, other alternate timezone(s) may be
|
||||||
|
configured as well:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[wutta]
|
||||||
|
timezone.default = America/Chicago
|
||||||
|
timezone.eastcoast = America/New_York
|
||||||
|
timezone.westcoast = America/Los_Angeles
|
||||||
|
|
||||||
|
Corresponding :class:`python:datetime.tzinfo` objects can be fetched
|
||||||
|
via :meth:`~wuttjamaican.app.AppHandler.get_timezone()`::
|
||||||
|
|
||||||
|
tz_default = app.get_timezone() # => America/Chicago
|
||||||
|
|
||||||
|
tz_eastcoast = app.get_timezone("eastcoast")
|
||||||
|
tz_westcoast = app.get_timezone("westcoast")
|
||||||
|
|
||||||
|
|
||||||
|
UTC vs. Local Time
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Since we store values as naive/UTC, but display to the user as
|
||||||
|
aware/local, we often need to convert values between these (and
|
||||||
|
related) formats.
|
||||||
|
|
||||||
|
|
||||||
|
.. _convert-to-utc:
|
||||||
|
|
||||||
|
Convert to UTC
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When a datetime value is written to the app DB, it must be naive/UTC.
|
||||||
|
|
||||||
|
Below are 4 examples for converting values to UTC time zone. In
|
||||||
|
short, use :meth:`~wuttjamaican.app.AppHandler.make_utc()` - but if
|
||||||
|
providing a naive value, it must already be UTC! (Because *all* naive
|
||||||
|
values are assumed to be UTC. Provide zone-aware values when
|
||||||
|
necessary to avoid confusion.)
|
||||||
|
|
||||||
|
These examples assume ``America/Chicago`` (UTC-0600) for the "local"
|
||||||
|
timezone, with a local time value of 2:15 PM (so, 8:15 PM UTC)::
|
||||||
|
|
||||||
|
# naive/UTC => naive/UTC
|
||||||
|
# (nb. this conversion is not actually needed of course, but we
|
||||||
|
# show the example to be thorough)
|
||||||
|
# nb. value has no timezone but is already correct (8:15 PM UTC)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 20, 15)
|
||||||
|
utc = app.make_utc(dt)
|
||||||
|
|
||||||
|
# aware/UTC => naive/UTC
|
||||||
|
# nb. value has expicit timezone, w/ correct time (8:15 PM UTC)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 20, 15, tzinfo=datetime.timezone.utc)
|
||||||
|
utc = app.make_utc(dt)
|
||||||
|
|
||||||
|
# aware/local => naive/UTC
|
||||||
|
tzlocal = app.get_timezone()
|
||||||
|
# nb. value has expicit timezone, w/ correct time (2:15 PM local)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 14, 15, tzinfo=tzlocal)
|
||||||
|
utc = app.make_utc(dt)
|
||||||
|
|
||||||
|
If your value is naive/local then you can't simply pass it to
|
||||||
|
:meth:`~wuttjamaican.app.AppHandler.make_utc()` - since that assumes
|
||||||
|
naive values are already UTC. (Again, *all* naive values are assumed
|
||||||
|
to be UTC.)
|
||||||
|
|
||||||
|
Instead, first call :meth:`~wuttjamaican.app.AppHandler.localtime()`
|
||||||
|
with ``from_utc=False`` to add local time zone awareness::
|
||||||
|
|
||||||
|
# naive/local => naive/UTC
|
||||||
|
# nb. value has no timezone but is correct for local zone (2:15 PM)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 14, 15)
|
||||||
|
# must first convert, and be sure to specify it's *not* UTC
|
||||||
|
# (in practice this just sets the local timezone)
|
||||||
|
dt = app.localtime(dt, from_utc=False)
|
||||||
|
# value is now "aware/local" so can proceed
|
||||||
|
utc = app.make_utc(dt)
|
||||||
|
|
||||||
|
The result of all examples shown above (8:15 PM UTC)::
|
||||||
|
|
||||||
|
>>> utc
|
||||||
|
datetime.datetime(2025, 12, 16, 20, 15)
|
||||||
|
|
||||||
|
|
||||||
|
.. _convert-to-localtime:
|
||||||
|
|
||||||
|
Convert to Local Time
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When a datetime value is read from the app DB, it must be converted
|
||||||
|
(from naive/UTC) to aware/local for display to user.
|
||||||
|
|
||||||
|
Below are 4 examples for converting values to local time zone. In
|
||||||
|
short, use :meth:`~wuttjamaican.app.AppHandler.localtime()` - but if
|
||||||
|
providing a naive value, you should specify ``from_utc`` param as
|
||||||
|
needed.
|
||||||
|
|
||||||
|
These examples assume ``America/Chicago`` (UTC-0600) for the "local"
|
||||||
|
timezone, with a local time value of 2:15 PM (so, 8:15 PM UTC)::
|
||||||
|
|
||||||
|
# naive/UTC => aware/local
|
||||||
|
# nb. value has no timezone but is already correct (8:15 PM UTC)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 20, 15)
|
||||||
|
# nb. can omit from_utc since it is assumed for naive values
|
||||||
|
local = app.localtime(dt)
|
||||||
|
# nb. or, specify it explicitly anyway
|
||||||
|
local = app.localtime(dt, from_utc=True)
|
||||||
|
|
||||||
|
# aware/UTC => aware/local
|
||||||
|
# nb. value has expicit timezone, w/ correct time (8:15 PM UTC)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 20, 15, tzinfo=datetime.timezone.utc)
|
||||||
|
local = app.localtime(dt)
|
||||||
|
|
||||||
|
# aware/local => aware/local
|
||||||
|
# (nb. this conversion is not actually needed of course, but we
|
||||||
|
# show the example to be thorough)
|
||||||
|
tzlocal = app.get_timezone()
|
||||||
|
# nb. value has expicit timezone, w/ correct time (2:15 PM local)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 14, 15, tzinfo=tzlocal)
|
||||||
|
# nb. the input and output values are the same here, both aware/local
|
||||||
|
local = app.localtime(dt)
|
||||||
|
|
||||||
|
If your value is naive/local then you can't simply pass it to
|
||||||
|
:meth:`~wuttjamaican.app.AppHandler.localtime()` with no qualifiers -
|
||||||
|
since that assumes naive values are already UTC by default.
|
||||||
|
|
||||||
|
Instead specify ``from_utc=False`` to ensure the value is interpreted
|
||||||
|
correctly::
|
||||||
|
|
||||||
|
# naive/local => aware/local
|
||||||
|
# nb. value has no timezone but is correct for local zone (2:15 PM)
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 14, 15)
|
||||||
|
# nb. must specify from_utc to avoid misinterpretation
|
||||||
|
local = app.localtime(dt, from_utc=False)
|
||||||
|
|
||||||
|
The result of all examples shown above (2:15 PM local)::
|
||||||
|
|
||||||
|
>>> local
|
||||||
|
datetime.datetime(2025, 12, 16, 14, 15, tzinfo=zoneinfo.ZoneInfo("America/Chicago"))
|
||||||
|
|
||||||
|
|
||||||
|
Displaying to the User
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Whenever a datetime should be displayed to the user, call
|
||||||
|
:meth:`~wuttjamaican.app.AppHandler.render_datetime()`. That will (by
|
||||||
|
default) convert the value to aware/local and then render it using a
|
||||||
|
common format.
|
||||||
|
|
||||||
|
(Once again, this will *not* work for naive/local values. Those must
|
||||||
|
be explicitly converted to aware/local since the framework assumes
|
||||||
|
*all* naive values are in UTC.)
|
||||||
|
|
||||||
|
You can specify ``local=False`` when calling ``render_datetime()`` to
|
||||||
|
avoid its default conversion.
|
||||||
|
|
||||||
|
See also :ref:`convert-to-localtime` (above) for examples of how to
|
||||||
|
convert any value to aware/local.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# naive/UTC
|
||||||
|
dt = app.make_utc()
|
||||||
|
print(app.render_datetime(dt))
|
||||||
|
|
||||||
|
# aware/UTC
|
||||||
|
dt = app.make_utc(tzinfo=True)
|
||||||
|
print(app.render_datetime(dt))
|
||||||
|
|
||||||
|
# aware/local
|
||||||
|
dt = app.localtime()
|
||||||
|
print(app.render_datetime(dt))
|
||||||
|
|
||||||
|
# naive/local
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
# nb. must explicitly convert to aware/local
|
||||||
|
dt = app.localtime(dt, from_utc=False)
|
||||||
|
# nb. can skip redundant conversion via local=False
|
||||||
|
print(app.render_datetime(dt, local=False))
|
||||||
|
|
||||||
|
|
||||||
|
Within the Database
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
This section describes storage and access details for datetime values
|
||||||
|
within the :term:`app database`.
|
||||||
|
|
||||||
|
|
||||||
|
Column Type
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
There is not a consistent/simple way to store timezone for datetime
|
||||||
|
values in all database backends. Therefore we must always store the
|
||||||
|
values as naive/UTC so app logic can reliably interpret them. (Hence
|
||||||
|
that particular rule.)
|
||||||
|
|
||||||
|
All built-in :term:`data models <data model>` use the
|
||||||
|
:class:`~sqlalchemy:sqlalchemy.types.DateTime` column type (for
|
||||||
|
datetime fields), with its default behavior. Any app schema
|
||||||
|
extensions should (usually) do the same.
|
||||||
|
|
||||||
|
|
||||||
|
Writing to the DB
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When a datetime value is written to the app DB, it must be naive/UTC.
|
||||||
|
|
||||||
|
See :ref:`convert-to-utc` (above) for examples of how to convert any
|
||||||
|
value to naive/UTC.
|
||||||
|
|
||||||
|
Apps typically "write" data via the ORM. Regardless, the key point is
|
||||||
|
that you should only pass naive/UTC values into the DB::
|
||||||
|
|
||||||
|
model = app.model
|
||||||
|
session = app.make_session()
|
||||||
|
|
||||||
|
# obtain aware/local value (for example)
|
||||||
|
tz = app.get_timezone()
|
||||||
|
dt = datetime.datetime(2025, 12, 16, 14, 15, tzinfo=tz)
|
||||||
|
|
||||||
|
# convert to naive/UTC when passing to DB
|
||||||
|
sprocket = model.Sprocket()
|
||||||
|
sprocket.some_dt_attr = app.make_utc(dt)
|
||||||
|
sprocket.created = app.make_utc() # "now"
|
||||||
|
session.add(sprocket)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
Reading from the DB
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Nothing special happens when reading datetime values from the DB; they
|
||||||
|
will be naive/UTC just like they were written::
|
||||||
|
|
||||||
|
sprocket = session.get(model.Sprocket, uuid)
|
||||||
|
|
||||||
|
# these will be naive/UTC
|
||||||
|
dt = sprocket.some_dt_attr
|
||||||
|
dt2 = sprocket.created
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttJamaican"
|
name = "WuttJamaican"
|
||||||
version = "0.25.0"
|
version = "0.26.0"
|
||||||
description = "Base package for Wutta Framework"
|
description = "Base package for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
@ -27,6 +27,7 @@ classifiers = [
|
||||||
requires-python = ">= 3.8"
|
requires-python = ">= 3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
'python-dateutil; python_version < "3.9"',
|
||||||
"humanize",
|
"humanize",
|
||||||
'importlib-metadata; python_version < "3.10"',
|
'importlib-metadata; python_version < "3.10"',
|
||||||
"importlib_resources ; python_version < '3.9'",
|
"importlib_resources ; python_version < '3.9'",
|
||||||
|
|
@ -42,7 +43,7 @@ dependencies = [
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
db = ["SQLAlchemy", "alembic", "alembic-postgresql-enum"]
|
db = ["SQLAlchemy", "alembic", "alembic-postgresql-enum"]
|
||||||
docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"]
|
docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"]
|
||||||
tests = ["pylint", "pytest", "pytest-cov", "tox"]
|
tests = ["pylint", "pytest", "pytest-cov", "tox", "python-dateutil"]
|
||||||
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ WuttJamaican - app handler
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -35,6 +36,7 @@ from importlib.metadata import version
|
||||||
import humanize
|
import humanize
|
||||||
|
|
||||||
from wuttjamaican.util import (
|
from wuttjamaican.util import (
|
||||||
|
get_timezone_by_name,
|
||||||
localtime,
|
localtime,
|
||||||
load_entry_points,
|
load_entry_points,
|
||||||
load_object,
|
load_object,
|
||||||
|
|
@ -110,6 +112,7 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
|
self.timezones = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def appname(self):
|
def appname(self):
|
||||||
|
|
@ -528,19 +531,117 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
return make_full_name(*parts)
|
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
|
Get the configured (or system default) timezone object.
|
||||||
convenience wrapper around
|
|
||||||
:func:`~wuttjamaican.util.localtime()`.
|
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).
|
||||||
|
|
||||||
|
For usage examples see :ref:`convert-to-localtime`.
|
||||||
|
|
||||||
|
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):
|
def make_utc(self, dt=None, tzinfo=False):
|
||||||
"""
|
"""
|
||||||
This returns a datetime local to the UTC timezone. It is a
|
This returns a datetime local to the UTC timezone. It is a
|
||||||
convenience wrapper around
|
convenience wrapper around
|
||||||
:func:`~wuttjamaican.util.make_utc()`.
|
:func:`~wuttjamaican.util.make_utc()`.
|
||||||
|
|
||||||
|
For usage examples see :ref:`convert-to-utc`.
|
||||||
|
|
||||||
|
See also :meth:`localtime()` which is sort of the inverse.
|
||||||
"""
|
"""
|
||||||
return make_utc(dt=dt, tzinfo=tzinfo)
|
return make_utc(dt=dt, tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
@ -795,7 +896,7 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
||||||
return ""
|
return ""
|
||||||
return value.strftime(self.display_format_date)
|
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.
|
Return a human-friendly display string for the given datetime.
|
||||||
|
|
||||||
|
|
@ -804,14 +905,16 @@ class AppHandler: # pylint: disable=too-many-public-methods
|
||||||
:param value: A :class:`python:datetime.datetime` instance (or
|
:param value: A :class:`python:datetime.datetime` instance (or
|
||||||
``None``).
|
``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:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
|
if local:
|
||||||
if not value.tzinfo:
|
|
||||||
value = self.localtime(value)
|
value = self.localtime(value)
|
||||||
|
|
||||||
return value.strftime(self.display_format_datetime)
|
return value.strftime(self.display_format_datetime)
|
||||||
|
|
||||||
def render_error(self, error):
|
def render_error(self, error):
|
||||||
|
|
|
||||||
|
|
@ -190,56 +190,93 @@ def make_full_name(*parts):
|
||||||
return " ".join(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
|
Retrieve a timezone object by name.
|
||||||
it will be *zone-aware*.
|
|
||||||
|
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
|
See also the shortcut
|
||||||
:meth:`~wuttjamaican.app.AppHandler.localtime()` method on the app
|
:meth:`~wuttjamaican.app.AppHandler.localtime()` method on the app
|
||||||
handler.
|
handler. For usage examples see :ref:`convert-to-localtime`.
|
||||||
|
|
||||||
See also :func:`make_utc()` which is sort of the inverse.
|
See also :func:`make_utc()` which is sort of the inverse.
|
||||||
|
|
||||||
:param dt: Optional :class:`python:datetime.datetime` instance.
|
:param dt: Optional :class:`python:datetime.datetime` instance.
|
||||||
If not specified, the current time will be used.
|
If not specified, the current time will be used.
|
||||||
|
|
||||||
:param tzinfo: Boolean indicating whether the return value should
|
:param from_utc: Boolean indicating whether a naive ``dt`` is
|
||||||
have its :attr:`~python:datetime.datetime.tzinfo` attribute
|
already (effectively) in UTC timezone. Set this to false when
|
||||||
set. This is true by default in which case the return value
|
providing a naive ``dt`` which is already in "local" timezone
|
||||||
will be zone-aware.
|
instead of UTC. This flag is ignored if ``dt`` is zone-aware.
|
||||||
|
|
||||||
:returns: :class:`python:datetime.datetime` instance in
|
:param want_tzinfo: Boolean indicating whether the resulting
|
||||||
system-local timezone.
|
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
|
# use current time if none provided
|
||||||
if dt is None:
|
if dt is None:
|
||||||
dt = datetime.datetime.now(datetime.timezone.utc)
|
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
|
# set dt's timezone if needed
|
||||||
if dt.tzinfo:
|
if not dt.tzinfo:
|
||||||
dt = dt.astimezone()
|
# UTC is default assumption unless caller says otherwise
|
||||||
if tzinfo:
|
if from_utc:
|
||||||
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)
|
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):
|
def make_utc(dt=None, tzinfo=False):
|
||||||
|
|
@ -250,7 +287,7 @@ def make_utc(dt=None, tzinfo=False):
|
||||||
|
|
||||||
See also the shortcut
|
See also the shortcut
|
||||||
:meth:`~wuttjamaican.app.AppHandler.make_utc()` method on the app
|
:meth:`~wuttjamaican.app.AppHandler.make_utc()` method on the app
|
||||||
handler.
|
handler. For usage examples see :ref:`convert-to-utc`.
|
||||||
|
|
||||||
See also :func:`localtime()` which is sort of the inverse.
|
See also :func:`localtime()` which is sort of the inverse.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ from mako.template import Template
|
||||||
|
|
||||||
import wuttjamaican.enum
|
import wuttjamaican.enum
|
||||||
from wuttjamaican import app as mod
|
from wuttjamaican import app as mod
|
||||||
|
from wuttjamaican.exc import ConfigurationError
|
||||||
from wuttjamaican.progress import ProgressBase
|
from wuttjamaican.progress import ProgressBase
|
||||||
from wuttjamaican.conf import WuttaConfig
|
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.testing import FileTestCase, ConfigTestCase
|
||||||
from wuttjamaican.batch import BatchHandler
|
from wuttjamaican.batch import BatchHandler
|
||||||
|
|
||||||
|
|
@ -55,8 +56,7 @@ class TestAppHandler(FileTestCase):
|
||||||
self.assertIs(obj, UNSPECIFIED)
|
self.assertIs(obj, UNSPECIFIED)
|
||||||
|
|
||||||
def test_get_appdir(self):
|
def test_get_appdir(self):
|
||||||
|
mockdir = self.mkdtemp()
|
||||||
mockdir = self.mkdir("mockdir")
|
|
||||||
|
|
||||||
# default appdir
|
# default appdir
|
||||||
with patch.object(sys, "prefix", new=mockdir):
|
with patch.object(sys, "prefix", new=mockdir):
|
||||||
|
|
@ -426,6 +426,80 @@ app_title = WuttaTest
|
||||||
name = self.app.make_full_name("Fred", "", "Flintstone", "")
|
name = self.app.make_full_name("Fred", "", "Flintstone", "")
|
||||||
self.assertEqual(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):
|
def test_localtime(self):
|
||||||
dt = self.app.localtime()
|
dt = self.app.localtime()
|
||||||
self.assertIsInstance(dt, datetime.datetime)
|
self.assertIsInstance(dt, datetime.datetime)
|
||||||
|
|
@ -516,15 +590,38 @@ app_title = WuttaTest
|
||||||
self.assertEqual(self.app.render_date(dt), "2024-12-11")
|
self.assertEqual(self.app.render_date(dt), "2024-12-11")
|
||||||
|
|
||||||
def test_render_datetime(self):
|
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), "")
|
self.assertEqual(self.app.render_datetime(None), "")
|
||||||
|
|
||||||
dt = datetime.datetime(2024, 12, 11, 8, 30, tzinfo=datetime.timezone.utc)
|
# naive UTC
|
||||||
self.assertEqual(self.app.render_datetime(dt), "2024-12-11 08:30+0000")
|
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)
|
# aware UTC
|
||||||
text = self.app.render_datetime(dt)
|
dt = datetime.datetime(2024, 12, 17, 1, 12, tzinfo=datetime.timezone.utc)
|
||||||
# TODO: should override local timezone for more complete test
|
self.assertEqual(
|
||||||
self.assertTrue(text.startswith("2024-12-"))
|
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):
|
def test_render_error(self):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,49 +165,122 @@ class TestLoadObject(TestCase):
|
||||||
self.assertIs(result, 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):
|
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):
|
def test_current_time(self):
|
||||||
|
tz = datetime.timezone(-datetime.timedelta(hours=5))
|
||||||
|
|
||||||
# has tzinfo by default
|
# overriding local_zone
|
||||||
dt = mod.localtime()
|
result = mod.localtime(local_zone=tz)
|
||||||
self.assertIsInstance(dt, datetime.datetime)
|
self.assertIsInstance(result, datetime.datetime)
|
||||||
self.assertIsNotNone(dt.tzinfo)
|
self.assertIs(result.tzinfo, tz)
|
||||||
now = datetime.datetime.now()
|
|
||||||
self.assertAlmostEqual(int(dt.timestamp()), int(now.timestamp()))
|
# 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
|
# no tzinfo
|
||||||
dt = mod.localtime(tzinfo=False)
|
result = mod.localtime(want_tzinfo=False)
|
||||||
self.assertIsInstance(dt, datetime.datetime)
|
self.assertIsNone(result.tzinfo)
|
||||||
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):
|
class TestMakeUTC(TestCase):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue