diff --git a/docs/source/conf.py b/docs/source/conf.py
index c5b5e17e..0fa60082 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -355,6 +355,7 @@ def setup(app):
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
+ "dateutil": ("https://dateutil.readthedocs.io/en/stable/", None),
}
autodoc_default_options = {
diff --git a/docs/source/types/_temporal_overview.rst b/docs/source/types/_temporal_overview.rst
index d6f18b8f..7a073cea 100644
--- a/docs/source/types/_temporal_overview.rst
+++ b/docs/source/types/_temporal_overview.rst
@@ -3,6 +3,11 @@ Temporal data types are implemented by the ``neo4j.time`` module.
It provides a set of types compliant with ISO-8601 and Cypher, which are similar to those found in the built-in ``datetime`` module.
Sub-second values are measured to nanosecond precision and the types are compatible with `pytz `_.
+.. warning::
+ The temporal types were designed to be used with `pytz `_.
+ Other :class:`datetime.tzinfo` implementations (e.g., :class:`datetime.timezone`, :mod:`zoneinfo`, :mod:`dateutil.tz`)
+ are not supported and are unlikely to work well.
+
The table below shows the general mappings between Cypher and the temporal types provided by the driver.
In addition, the built-in temporal types can be passed as parameters and will be mapped appropriately.
@@ -18,10 +23,6 @@ LocalDateTime :class:`neo4j.time.DateTime` :class:`python:datetime.datetime`
Duration :class:`neo4j.time.Duration` :class:`python:datetime.timedelta`
============= ============================ ================================== ============
-Sub-second values are measured to nanosecond precision and the types are mostly
-compatible with `pytz `_. Some timezones
-(e.g., ``pytz.utc``) work exclusively with the built-in ``datetime.datetime``.
-
.. Note::
Cypher has built-in support for handling temporal values, and the underlying
database supports storing these temporal values as properties on nodes and relationships,
diff --git a/src/neo4j/time/__init__.py b/src/neo4j/time/__init__.py
index 38987343..4f8ad101 100644
--- a/src/neo4j/time/__init__.py
+++ b/src/neo4j/time/__init__.py
@@ -1487,6 +1487,43 @@ def iso_calendar(self) -> tuple[int, int, int]: ...
time_base_class = object
+def _dst(
+ tz: _tzinfo | None = None, dt: DateTime | None = None
+) -> timedelta | None:
+ if tz is None:
+ return None
+ try:
+ value = tz.dst(dt)
+ except TypeError:
+ if dt is None:
+ raise
+ # For timezone implementations not compatible with the custom
+ # datetime implementations, we can't do better than this.
+ value = tz.dst(dt.to_native()) # type: ignore
+ if value is None:
+ return None
+ if isinstance(value, timedelta):
+ if value.days != 0:
+ raise ValueError("dst must be less than a day")
+ if value.seconds % 60 != 0 or value.microseconds != 0:
+ raise ValueError("dst must be a whole number of minutes")
+ return value
+ raise TypeError("dst must be a timedelta")
+
+
+def _tz_name(tz: _tzinfo | None, dt: DateTime | None) -> str | None:
+ if tz is None:
+ return None
+ try:
+ return tz.tzname(dt)
+ except TypeError:
+ if dt is None:
+ raise
+ # For timezone implementations not compatible with the custom
+ # datetime implementations, we can't do better than this.
+ return tz.tzname(dt.to_native())
+
+
class Time(time_base_class, metaclass=TimeType):
"""
Time of day.
@@ -1996,23 +2033,7 @@ def dst(self) -> timedelta | None:
:raises TypeError: if `self.tzinfo.dst(self)` does return anything but
None or a :class:`datetime.timedelta`.
"""
- if self.tzinfo is None:
- return None
- try:
- value = self.tzinfo.dst(self) # type: ignore
- except TypeError:
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- value = self.tzinfo.dst(self.to_native()) # type: ignore
- if value is None:
- return None
- if isinstance(value, timedelta):
- if value.days != 0:
- raise ValueError("dst must be less than a day")
- if value.seconds % 60 != 0 or value.microseconds != 0:
- raise ValueError("dst must be a whole number of minutes")
- return value
- raise TypeError("dst must be a timedelta")
+ return _dst(self.tzinfo, None)
def tzname(self) -> str | None:
"""
@@ -2021,14 +2042,7 @@ def tzname(self) -> str | None:
:returns: None if the time is local (i.e., has no timezone), else
return `self.tzinfo.tzname(self)`
"""
- if self.tzinfo is None:
- return None
- try:
- return self.tzinfo.tzname(self) # type: ignore
- except TypeError:
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- return self.tzinfo.tzname(self.to_native()) # type: ignore
+ return _tz_name(self.tzinfo, None)
def to_clock_time(self) -> ClockTime:
"""Convert to :class:`.ClockTime`."""
@@ -2202,16 +2216,14 @@ def now(cls, tz: _tzinfo | None = None) -> DateTime:
if tz is None:
return cls.from_clock_time(Clock().local_time(), UnixEpoch)
else:
+ utc_now = cls.from_clock_time(
+ Clock().utc_time(), UnixEpoch
+ ).replace(tzinfo=tz)
try:
- return tz.fromutc( # type: ignore
- cls.from_clock_time( # type: ignore
- Clock().utc_time(), UnixEpoch
- ).replace(tzinfo=tz)
- )
+ return tz.fromutc(utc_now) # type: ignore
except TypeError:
# For timezone implementations not compatible with the custom
# datetime implementations, we can't do better than this.
- utc_now = cls.from_clock_time(Clock().utc_time(), UnixEpoch)
utc_now_native = utc_now.to_native()
now_native = tz.fromutc(utc_now_native)
now = cls.from_native(now_native)
@@ -2809,7 +2821,7 @@ def dst(self) -> timedelta | None:
See :meth:`.Time.dst`.
"""
- return self.__time.dst()
+ return _dst(self.tzinfo, self)
def tzname(self) -> str | None:
"""
@@ -2817,7 +2829,7 @@ def tzname(self) -> str | None:
See :meth:`.Time.tzname`.
"""
- return self.__time.tzname()
+ return _tz_name(self.tzinfo, self)
def time_tuple(self):
raise NotImplementedError
diff --git a/tests/unit/common/time/test_datetime.py b/tests/unit/common/time/test_datetime.py
index ebc1deb1..46be5ffd 100644
--- a/tests/unit/common/time/test_datetime.py
+++ b/tests/unit/common/time/test_datetime.py
@@ -20,6 +20,7 @@
import itertools
import operator
import pickle
+import sys
import typing as t
from datetime import (
datetime,
@@ -180,6 +181,38 @@ def test_now_with_utc_tz(self) -> None:
assert t.dst() == timedelta()
assert t.tzname() == "UTC"
+ def test_now_with_timezone_utc_tz(self) -> None:
+ # not fully supported tzinfo implementation
+ t = DateTime.now(datetime_timezone.utc)
+ assert t.year == 1970
+ assert t.month == 1
+ assert t.day == 1
+ assert t.hour == 12
+ assert t.minute == 34
+ assert t.second == 56
+ assert t.nanosecond == 789000001
+ assert t.utcoffset() == timedelta(seconds=0)
+ assert t.dst() is None
+ assert t.tzname() == "UTC"
+
+ if sys.version_info >= (3, 9):
+
+ def test_now_with_zoneinfo_utc_tz(self) -> None:
+ # not fully supported tzinfo implementation
+ import zoneinfo
+
+ t = DateTime.now(zoneinfo.ZoneInfo("UTC"))
+ assert t.year == 1970
+ assert t.month == 1
+ assert t.day == 1
+ assert t.hour == 12
+ assert t.minute == 34
+ assert t.second == 56
+ assert t.nanosecond == 789000001
+ assert t.utcoffset() == timedelta(seconds=0)
+ assert t.dst() == timedelta(seconds=0)
+ assert t.tzname() == "UTC"
+
def test_utc_now(self) -> None:
t = DateTime.utc_now()
assert t.year == 1970