From: Daniele Varrazzo Date: Thu, 1 May 2025 17:38:18 +0000 (+0200) Subject: fix: fix return value of `DateFromTicks` and `TimeFromTicks` X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F1061%2Fhead;p=thirdparty%2Fpsycopg.git fix: fix return value of `DateFromTicks` and `TimeFromTicks` These function are based on the return value of `TimestampFromTicks`. Because this value returned by this function is timezone-aware and localized to the current timezone, truncating the value (and implicitly losing the timezone) might return the wrong result. For example, at tick 0, in NY, it was 1969-12-31T19:00-05:00. By truncating the timestamp on the date, the date would be the 1969-12-31 instead of the expected 1970-01-01. This changeset actually changes `TimestampFromTicks` to return an UTC timestamp instead of a local timestamp. We believe that this change is not a problem because we don't change neither the type (tz-aware datetime) nor the value of the returned object (old and new values compare equal). An alternative would have been to decouple the Timestamp function from the Date/Time functions, but I think it would be confusing if the Timestamp function returned a datetime with the date in 1969 but the Date function returned a date in 1970. Adjust the tests to be *stricter* than before, making use of the fact that now the output is not affected by the local timezone. Close #1058. --- diff --git a/docs/news.rst b/docs/news.rst index 8d4ace435..22d51b7d9 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -15,6 +15,14 @@ Psycopg 3.3.0 (unreleased) - Drop support for Python 3.8. +Psycopg 3.2.8 (unreleased) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Fix `DateFromTicks` and `TimeFromTicks` return values to return date and + time referred as UTC rather than the local timezone. Change + `TimestampFromTicks` to return a datetime in UTC rather than in the local + timezone (:ticket:`#1058`). + Psycopg 3.2.7 ^^^^^^^^^^^^^ diff --git a/psycopg/psycopg/dbapi20.py b/psycopg/psycopg/dbapi20.py index c80b7e5e4..bffcf327c 100644 --- a/psycopg/psycopg/dbapi20.py +++ b/psycopg/psycopg/dbapi20.py @@ -6,9 +6,7 @@ Compatibility objects with DBAPI 2.0 from __future__ import annotations -import time import datetime as dt -from math import floor from typing import Any from collections.abc import Sequence @@ -16,6 +14,8 @@ from . import _oids from .abc import AdaptContext, Buffer from .types.string import BytesBinaryDumper, BytesDumper +EPOCH = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) + class DBAPITypeObject: def __init__(self, name: str, oids: Sequence[int]): @@ -116,12 +116,7 @@ def Timestamp( def TimestampFromTicks(ticks: float) -> dt.datetime: - secs = floor(ticks) - frac = ticks - secs - t = time.localtime(ticks) - tzinfo = dt.timezone(dt.timedelta(seconds=t.tm_gmtoff)) - rv = dt.datetime(*t[:6], round(frac * 1_000_000), tzinfo=tzinfo) - return rv + return EPOCH + dt.timedelta(seconds=ticks) def register_dbapi20_adapters(context: AdaptContext) -> None: diff --git a/tests/test_psycopg_dbapi20.py b/tests/test_psycopg_dbapi20.py index 768b98f8b..216d9de86 100644 --- a/tests/test_psycopg_dbapi20.py +++ b/tests/test_psycopg_dbapi20.py @@ -84,6 +84,7 @@ def test_singletons(conn, typename, singleton): "ticks, want", [ (0, "1970-01-01T00:00:00.000000+0000"), + (86370, "1970-01-01T23:59:30.000000+0000"), (1273173119.99992, "2010-05-06T14:11:59.999920-0500"), ], ) @@ -97,26 +98,24 @@ def test_timestamp_from_ticks(ticks, want): "ticks, want", [ (0, "1970-01-01"), - # Returned date is local - (1273173119.99992, ["2010-05-06", "2010-05-07"]), + (86370, "1970-01-01"), # 30 sec from Jan the 2nd, to test East localization + (1273173119.99992, "2010-05-06"), ], ) def test_date_from_ticks(ticks, want): s = psycopg.DateFromTicks(ticks) - if isinstance(want, str): - want = [want] - want = [dt.datetime.strptime(w, "%Y-%m-%d").date() for w in want] - assert s in want + want = dt.datetime.strptime(want, "%Y-%m-%d").date() + assert s == want @pytest.mark.parametrize( "ticks, want", - [(0, "00:00:00.000000"), (1273173119.99992, "00:11:59.999920")], + [(0, "00:00:00.000000"), (1273173119.99992, "19:11:59.999920")], ) def test_time_from_ticks(ticks, want): s = psycopg.TimeFromTicks(ticks) want = dt.datetime.strptime(want, "%H:%M:%S.%f").time() - assert s.replace(hour=0) == want + assert s == want @pytest.mark.parametrize(