Skip to content

API/BUG: treat different UTC tzinfos as equal #39216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ Timedelta

Timezones
^^^^^^^^^

- Bug in different ``tzinfo`` objects representing UTC not being treated as equivalent (:issue:`39216`)
-
-

Expand Down
2 changes: 2 additions & 0 deletions pandas/_libs/tslibs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"to_offset",
"Tick",
"BaseOffset",
"tz_compare",
]

from . import dtypes
Expand All @@ -35,6 +36,7 @@
from .period import IncompatibleFrequency, Period
from .timedeltas import Timedelta, delta_to_nanoseconds, ints_to_pytimedelta
from .timestamps import Timestamp
from .timezones import tz_compare
from .tzconversion import tz_convert_from_utc_single
from .vectorized import (
dt64arr_to_periodarr,
Expand Down
6 changes: 2 additions & 4 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -647,14 +647,12 @@ cdef class _Timestamp(ABCTimestamp):

try:
stamp += self.strftime('%z')
if self.tzinfo:
zone = get_timezone(self.tzinfo)
except ValueError:
year2000 = self.replace(year=2000)
stamp += year2000.strftime('%z')
if self.tzinfo:
zone = get_timezone(self.tzinfo)

if self.tzinfo:
zone = get_timezone(self.tzinfo)
try:
stamp += zone.strftime(' %%Z')
except AttributeError:
Expand Down
6 changes: 6 additions & 0 deletions pandas/_libs/tslibs/timezones.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,12 @@ cpdef bint tz_compare(tzinfo start, tzinfo end):
bool
"""
# GH 18523
if is_utc(start):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test to ensure all the components of the utc_fixture are equivalent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just tried this and we have several utc_fixture members that arent recognized by is_utc, will need to fix that separately

# GH#38851 consider pytz/dateutil/stdlib UTCs as equivalent
return is_utc(end)
elif is_utc(end):
# Ensure we don't treat tzlocal as equal to UTC when running in UTC
return False
return get_timezone(start) == get_timezone(end)


Expand Down
2 changes: 1 addition & 1 deletion pandas/core/dtypes/cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
conversion,
iNaT,
ints_to_pydatetime,
tz_compare,
)
from pandas._libs.tslibs.timezones import tz_compare
from pandas._typing import AnyArrayLike, ArrayLike, Dtype, DtypeObj, Scalar
from pandas.util._validators import validate_bool_kwarg

Expand Down
14 changes: 11 additions & 3 deletions pandas/core/dtypes/dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@
import pytz

from pandas._libs.interval import Interval
from pandas._libs.tslibs import NaT, Period, Timestamp, dtypes, timezones, to_offset
from pandas._libs.tslibs.offsets import BaseOffset
from pandas._libs.tslibs import (
BaseOffset,
NaT,
Period,
Timestamp,
dtypes,
timezones,
to_offset,
tz_compare,
)
from pandas._typing import Dtype, DtypeObj, Ordered

from pandas.core.dtypes.base import ExtensionDtype, register_extension_dtype
Expand Down Expand Up @@ -764,7 +772,7 @@ def __eq__(self, other: Any) -> bool:
return (
isinstance(other, DatetimeTZDtype)
and self.unit == other.unit
and str(self.tz) == str(other.tz)
and tz_compare(self.tz, other.tz)
)

def __setstate__(self, state) -> None:
Expand Down
8 changes: 6 additions & 2 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@
from pandas._libs import algos as libalgos, index as libindex, lib
import pandas._libs.join as libjoin
from pandas._libs.lib import is_datetime_array, no_default
from pandas._libs.tslibs import IncompatibleFrequency, OutOfBoundsDatetime, Timestamp
from pandas._libs.tslibs.timezones import tz_compare
from pandas._libs.tslibs import (
IncompatibleFrequency,
OutOfBoundsDatetime,
Timestamp,
tz_compare,
)
from pandas._typing import AnyArrayLike, ArrayLike, Dtype, DtypeObj, Shape, final
from pandas.compat.numpy import function as nv
from pandas.errors import DuplicateLabelError, InvalidIndexError
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/dtypes/cast/test_promote.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import numpy as np
import pytest

from pandas._libs.tslibs import NaT
from pandas._libs.tslibs import NaT, tz_compare

from pandas.core.dtypes.cast import maybe_promote
from pandas.core.dtypes.common import (
Expand Down Expand Up @@ -431,7 +431,7 @@ def test_maybe_promote_datetimetz_with_datetimetz(tz_aware_fixture, tz_aware_fix

# filling datetimetz with datetimetz casts to object, unless tz matches
exp_val_for_scalar = fill_value
if dtype.tz == fill_dtype.tz:
if tz_compare(dtype.tz, fill_dtype.tz):
expected_dtype = dtype
else:
expected_dtype = np.dtype(object)
Expand Down
16 changes: 12 additions & 4 deletions pandas/tests/series/methods/test_fillna.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,10 +727,18 @@ def test_fillna_method_and_limit_invalid(self):

def test_fillna_datetime64_with_timezone_tzinfo(self):
# https://github.com/pandas-dev/pandas/issues/38851
s = Series(date_range("2020", periods=3, tz="UTC"))
expected = s.astype(object)
s[1] = NaT
result = s.fillna(datetime(2020, 1, 2, tzinfo=timezone.utc))
# different tzinfos representing UTC treated as equal
ser = Series(date_range("2020", periods=3, tz="UTC"))
expected = ser.copy()
ser[1] = NaT
result = ser.fillna(datetime(2020, 1, 2, tzinfo=timezone.utc))
tm.assert_series_equal(result, expected)

# but we dont (yet) consider distinct tzinfos for non-UTC tz equivalent
ts = Timestamp("2000-01-01", tz="US/Pacific")
ser2 = Series(ser._values.tz_convert("dateutil/US/Pacific"))
result = ser2.fillna(ts)
expected = Series([ser[0], ts, ser[2]], dtype=object)
tm.assert_series_equal(result, expected)


Expand Down
1 change: 1 addition & 0 deletions pandas/tests/tslibs/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def test_namespace():
"localize_pydatetime",
"tz_convert_from_utc_single",
"to_offset",
"tz_compare",
]

expected = set(submodules + api)
Expand Down
8 changes: 8 additions & 0 deletions pandas/tests/tslibs/test_timezones.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def test_tzlocal_offset():
assert ts.value + offset == Timestamp("2011-01-01").value


def test_tzlocal_is_not_utc():
# even if the machine running the test is localized to UTC
tz = dateutil.tz.tzlocal()
assert not timezones.is_utc(tz)

assert not timezones.tz_compare(tz, dateutil.tz.tzutc())


@pytest.fixture(
params=[
(pytz.timezone("US/Eastern"), lambda tz, x: tz.localize(x)),
Expand Down