From 49b8e31c5fa636b747e0d826f89e17e00917bebb Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 20 Jul 2024 12:01:56 -0500 Subject: [PATCH 1/3] Check for tai timestamp by identity, not string equality --- leapseconddata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index 7787402..e8c16fc 100755 --- a/leapseconddata/__init__.py +++ b/leapseconddata/__init__.py @@ -71,7 +71,7 @@ def _from_ntp_epoch(value: int) -> datetime.datetime: def datetime_is_tai(when: datetime.datetime) -> bool: """Return true if the datetime is in the TAI timescale""" - return when.tzname() == "TAI" + return when.tzinfo is tai @dataclass(frozen=True) From 940945970f29ba66aadd1ca5f1ddef67f56974f2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 20 Jul 2024 12:13:05 -0500 Subject: [PATCH 2/3] Fix passing naive timestamps & test --- leapseconddata/__init__.py | 4 +++- testleapseconddata.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index e8c16fc..84d1459 100755 --- a/leapseconddata/__init__.py +++ b/leapseconddata/__init__.py @@ -155,7 +155,9 @@ class LeapSecondData: @staticmethod def _utc_datetime(when: datetime.datetime) -> datetime.datetime: - if when.tzinfo is not None and when.tzinfo is not datetime.timezone.utc: + if when.tzinfo is None: + when = when.replace(tzinfo=datetime.timezone.utc) + elif when.tzinfo is not datetime.timezone.utc: when = when.astimezone(datetime.timezone.utc) return when diff --git a/testleapseconddata.py b/testleapseconddata.py index 1bf8315..b1541e8 100644 --- a/testleapseconddata.py +++ b/testleapseconddata.py @@ -136,6 +136,13 @@ class LeapSecondDataTest(unittest.TestCase): assert when_tai.tzinfo is leapseconddata.tai assert when_tai2.tzinfo is leapseconddata.tai + def test_to_tai_naive(self) -> None: + when = datetime.datetime(1999, 1, 1, tzinfo=None) - datetime.timedelta(seconds=1) # noqa: DTZ001 + when_tai = db.to_tai(when) + when2 = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1) + when_tai2 = db.to_tai(when2) + self.assertEqual(when_tai, when_tai2) + def assertPrints(self, code: str, expected: str) -> None: # noqa: N802 buf = io.StringIO() with contextlib.redirect_stdout(buf): From c659bd5b17873c340e049e01bae01746438a8d49 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 20 Jul 2024 12:13:34 -0500 Subject: [PATCH 3/3] Deprecate use of naive timestamps --- leapseconddata/__init__.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index 84d1459..320b2b6 100755 --- a/leapseconddata/__init__.py +++ b/leapseconddata/__init__.py @@ -29,6 +29,7 @@ import logging import pathlib import re import urllib.request +import warnings from dataclasses import dataclass, field from typing import TYPE_CHECKING, BinaryIO, ClassVar @@ -156,6 +157,7 @@ class LeapSecondData: @staticmethod def _utc_datetime(when: datetime.datetime) -> datetime.datetime: if when.tzinfo is None: + warnings.warn("Use of naive datetime objects is deprecated", DeprecationWarning, stacklevel=2) when = when.replace(tzinfo=datetime.timezone.utc) elif when.tzinfo is not datetime.timezone.utc: when = when.astimezone(datetime.timezone.utc) @@ -196,14 +198,18 @@ class LeapSecondData: def to_tai(self, when: datetime.datetime, *, check_validity: bool = True) -> datetime.datetime: """Convert the given datetime object to TAI. - :param when: Moment in time to convert. If naive, it is assumed to be in UTC. - :param check_validity: Check whether the database is valid for the given moment + A TAI timestamp is returned unchanged. - Naive timestamps are assumed to be UTC. A TAI timestamp is returned unchanged. + A naive timestamp is assumed to be UTC. This behavior is deprecated, and a future + release will raise an exception when ``when`` is naive. + + :param when: Moment in time to convert. + :param check_validity: Check whether the database is valid for the given moment """ if datetime_is_tai(when): return when when = self._utc_datetime(when) + assert when.tzinfo is not None return (when + self.tai_offset(when, check_validity=check_validity)).replace(tzinfo=tai) def tai_to_utc(self, when: datetime.datetime, *, check_validity: bool = True) -> datetime.datetime: @@ -211,12 +217,16 @@ class LeapSecondData: For a leap second, the ``fold`` property of the returned time is True. - :param when: Moment in time to convert. If not naive, its ``tzinfo`` must be `tai`. + A naive timestamp is assumed to be TAI. This behavior is deprecated, and a future + release will raise an exception when ``when`` is naive. + + :param when: Moment in time to convert. Its ``tzinfo`` must be `tai`. :param check_validity: Check whether the database is valid for the given moment """ if when.tzinfo is not None and when.tzinfo is not tai: raise ValueError("Input timestamp is not TAI or naive") if when.tzinfo is None: + warnings.warn("Use of naive datetime objects is deprecated", DeprecationWarning, stacklevel=1) when = when.replace(tzinfo=tai) result = (when - self.tai_offset(when, check_validity=check_validity)).replace(tzinfo=datetime.timezone.utc) if self.is_leap_second(when, check_validity=check_validity): @@ -226,7 +236,10 @@ class LeapSecondData: def is_leap_second(self, when: datetime.datetime, *, check_validity: bool = True) -> bool: """Return True if the given timestamp is the leap second. - :param when: Moment in time to check. If naive, it is assumed to be in UTC. + A naive timestamp is assumed to be UTC. This behavior is deprecated, and a future + release will raise an exception when ``when`` is naive. + + :param when: Moment in time to check. :param check_validity: Check whether the database is valid for the given moment For a TAI timestamp, it returns True for the leap second (the one that