Skip to content

Commit 4f8bb5e

Browse files
authored
API/BUG: treat different UTC tzinfos as equal (#39216)
1 parent a2db6fd commit 4f8bb5e

File tree

11 files changed

+52
-17
lines changed

11 files changed

+52
-17
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Timedelta
241241

242242
Timezones
243243
^^^^^^^^^
244-
244+
- Bug in different ``tzinfo`` objects representing UTC not being treated as equivalent (:issue:`39216`)
245245
-
246246
-
247247

pandas/_libs/tslibs/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"to_offset",
2525
"Tick",
2626
"BaseOffset",
27+
"tz_compare",
2728
]
2829

2930
from . import dtypes
@@ -35,6 +36,7 @@
3536
from .period import IncompatibleFrequency, Period
3637
from .timedeltas import Timedelta, delta_to_nanoseconds, ints_to_pytimedelta
3738
from .timestamps import Timestamp
39+
from .timezones import tz_compare
3840
from .tzconversion import tz_convert_from_utc_single
3941
from .vectorized import (
4042
dt64arr_to_periodarr,

pandas/_libs/tslibs/timestamps.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -647,14 +647,12 @@ cdef class _Timestamp(ABCTimestamp):
647647

648648
try:
649649
stamp += self.strftime('%z')
650-
if self.tzinfo:
651-
zone = get_timezone(self.tzinfo)
652650
except ValueError:
653651
year2000 = self.replace(year=2000)
654652
stamp += year2000.strftime('%z')
655-
if self.tzinfo:
656-
zone = get_timezone(self.tzinfo)
657653

654+
if self.tzinfo:
655+
zone = get_timezone(self.tzinfo)
658656
try:
659657
stamp += zone.strftime(' %%Z')
660658
except AttributeError:

pandas/_libs/tslibs/timezones.pyx

+6
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,12 @@ cpdef bint tz_compare(tzinfo start, tzinfo end):
341341
bool
342342
"""
343343
# GH 18523
344+
if is_utc(start):
345+
# GH#38851 consider pytz/dateutil/stdlib UTCs as equivalent
346+
return is_utc(end)
347+
elif is_utc(end):
348+
# Ensure we don't treat tzlocal as equal to UTC when running in UTC
349+
return False
344350
return get_timezone(start) == get_timezone(end)
345351

346352

pandas/core/dtypes/cast.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
conversion,
3636
iNaT,
3737
ints_to_pydatetime,
38+
tz_compare,
3839
)
39-
from pandas._libs.tslibs.timezones import tz_compare
4040
from pandas._typing import AnyArrayLike, ArrayLike, Dtype, DtypeObj, Scalar
4141
from pandas.util._validators import validate_bool_kwarg
4242

pandas/core/dtypes/dtypes.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@
2121
import pytz
2222

2323
from pandas._libs.interval import Interval
24-
from pandas._libs.tslibs import NaT, Period, Timestamp, dtypes, timezones, to_offset
25-
from pandas._libs.tslibs.offsets import BaseOffset
24+
from pandas._libs.tslibs import (
25+
BaseOffset,
26+
NaT,
27+
Period,
28+
Timestamp,
29+
dtypes,
30+
timezones,
31+
to_offset,
32+
tz_compare,
33+
)
2634
from pandas._typing import Dtype, DtypeObj, Ordered
2735

2836
from pandas.core.dtypes.base import ExtensionDtype, register_extension_dtype
@@ -764,7 +772,7 @@ def __eq__(self, other: Any) -> bool:
764772
return (
765773
isinstance(other, DatetimeTZDtype)
766774
and self.unit == other.unit
767-
and str(self.tz) == str(other.tz)
775+
and tz_compare(self.tz, other.tz)
768776
)
769777

770778
def __setstate__(self, state) -> None:

pandas/core/indexes/base.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@
2828
from pandas._libs import algos as libalgos, index as libindex, lib
2929
import pandas._libs.join as libjoin
3030
from pandas._libs.lib import is_datetime_array, no_default
31-
from pandas._libs.tslibs import IncompatibleFrequency, OutOfBoundsDatetime, Timestamp
32-
from pandas._libs.tslibs.timezones import tz_compare
31+
from pandas._libs.tslibs import (
32+
IncompatibleFrequency,
33+
OutOfBoundsDatetime,
34+
Timestamp,
35+
tz_compare,
36+
)
3337
from pandas._typing import AnyArrayLike, ArrayLike, Dtype, DtypeObj, Shape, final
3438
from pandas.compat.numpy import function as nv
3539
from pandas.errors import DuplicateLabelError, InvalidIndexError

pandas/tests/dtypes/cast/test_promote.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy as np
88
import pytest
99

10-
from pandas._libs.tslibs import NaT
10+
from pandas._libs.tslibs import NaT, tz_compare
1111

1212
from pandas.core.dtypes.cast import maybe_promote
1313
from pandas.core.dtypes.common import (
@@ -431,7 +431,7 @@ def test_maybe_promote_datetimetz_with_datetimetz(tz_aware_fixture, tz_aware_fix
431431

432432
# filling datetimetz with datetimetz casts to object, unless tz matches
433433
exp_val_for_scalar = fill_value
434-
if dtype.tz == fill_dtype.tz:
434+
if tz_compare(dtype.tz, fill_dtype.tz):
435435
expected_dtype = dtype
436436
else:
437437
expected_dtype = np.dtype(object)

pandas/tests/series/methods/test_fillna.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -727,10 +727,18 @@ def test_fillna_method_and_limit_invalid(self):
727727

728728
def test_fillna_datetime64_with_timezone_tzinfo(self):
729729
# https://github.com/pandas-dev/pandas/issues/38851
730-
s = Series(date_range("2020", periods=3, tz="UTC"))
731-
expected = s.astype(object)
732-
s[1] = NaT
733-
result = s.fillna(datetime(2020, 1, 2, tzinfo=timezone.utc))
730+
# different tzinfos representing UTC treated as equal
731+
ser = Series(date_range("2020", periods=3, tz="UTC"))
732+
expected = ser.copy()
733+
ser[1] = NaT
734+
result = ser.fillna(datetime(2020, 1, 2, tzinfo=timezone.utc))
735+
tm.assert_series_equal(result, expected)
736+
737+
# but we dont (yet) consider distinct tzinfos for non-UTC tz equivalent
738+
ts = Timestamp("2000-01-01", tz="US/Pacific")
739+
ser2 = Series(ser._values.tz_convert("dateutil/US/Pacific"))
740+
result = ser2.fillna(ts)
741+
expected = Series([ser[0], ts, ser[2]], dtype=object)
734742
tm.assert_series_equal(result, expected)
735743

736744

pandas/tests/tslibs/test_api.py

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def test_namespace():
4949
"localize_pydatetime",
5050
"tz_convert_from_utc_single",
5151
"to_offset",
52+
"tz_compare",
5253
]
5354

5455
expected = set(submodules + api)

pandas/tests/tslibs/test_timezones.py

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def test_tzlocal_offset():
4848
assert ts.value + offset == Timestamp("2011-01-01").value
4949

5050

51+
def test_tzlocal_is_not_utc():
52+
# even if the machine running the test is localized to UTC
53+
tz = dateutil.tz.tzlocal()
54+
assert not timezones.is_utc(tz)
55+
56+
assert not timezones.tz_compare(tz, dateutil.tz.tzutc())
57+
58+
5159
@pytest.fixture(
5260
params=[
5361
(pytz.timezone("US/Eastern"), lambda tz, x: tz.localize(x)),

0 commit comments

Comments
 (0)