Skip to content

Commit 0d2c579

Browse files
authored
BUG: avoid segfault in tz_localize with ZoneInfo (#49936)
1 parent 3c4b9b9 commit 0d2c579

File tree

6 files changed

+60
-43
lines changed

6 files changed

+60
-43
lines changed

Diff for: pandas/_libs/tslibs/tzconversion.pyx

+7
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ timedelta-like}
233233
int64_t shift_delta = 0
234234
ndarray[int64_t] result_a, result_b, dst_hours
235235
int64_t[::1] result
236+
bint is_zi = False
236237
bint infer_dst = False, is_dst = False, fill = False
237238
bint shift_forward = False, shift_backward = False
238239
bint fill_nonexist = False
@@ -304,6 +305,7 @@ timedelta-like}
304305
# Determine whether each date lies left of the DST transition (store in
305306
# result_a) or right of the DST transition (store in result_b)
306307
if is_zoneinfo(tz):
308+
is_zi = True
307309
result_a, result_b =_get_utc_bounds_zoneinfo(
308310
vals, tz, creso=creso
309311
)
@@ -385,6 +387,11 @@ timedelta-like}
385387
# nonexistent times
386388
new_local = val - remaining_mins - 1
387389

390+
if is_zi:
391+
raise NotImplementedError(
392+
"nonexistent shifting is not implemented with ZoneInfo tzinfos"
393+
)
394+
388395
delta_idx = bisect_right_i8(info.tdata, new_local, info.ntrans)
389396

390397
delta_idx = delta_idx - delta_idx_offset

Diff for: pandas/conftest.py

+13
Original file line numberDiff line numberDiff line change
@@ -1895,3 +1895,16 @@ def using_copy_on_write() -> bool:
18951895
Fixture to check if Copy-on-Write is enabled.
18961896
"""
18971897
return pd.options.mode.copy_on_write and pd.options.mode.data_manager == "block"
1898+
1899+
1900+
warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"]
1901+
if zoneinfo is not None:
1902+
warsaws.append(zoneinfo.ZoneInfo("Europe/Warsaw"))
1903+
1904+
1905+
@pytest.fixture(params=warsaws)
1906+
def warsaw(request):
1907+
"""
1908+
tzinfo for Europe/Warsaw using pytz, dateutil, or zoneinfo.
1909+
"""
1910+
return request.param

Diff for: pandas/tests/indexes/datetimes/test_constructors.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -872,10 +872,16 @@ def test_constructor_with_ambiguous_keyword_arg(self):
872872
result = date_range(end=end, periods=2, ambiguous=False)
873873
tm.assert_index_equal(result, expected)
874874

875-
def test_constructor_with_nonexistent_keyword_arg(self):
875+
def test_constructor_with_nonexistent_keyword_arg(self, warsaw, request):
876876
# GH 35297
877+
if type(warsaw).__name__ == "ZoneInfo":
878+
mark = pytest.mark.xfail(
879+
reason="nonexistent-shift not yet implemented for ZoneInfo",
880+
raises=NotImplementedError,
881+
)
882+
request.node.add_marker(mark)
877883

878-
timezone = "Europe/Warsaw"
884+
timezone = warsaw
879885

880886
# nonexistent keyword in start
881887
start = Timestamp("2015-03-29 02:30:00").tz_localize(

Diff for: pandas/tests/indexes/datetimes/test_timezones.py

+2-27
Original file line numberDiff line numberDiff line change
@@ -649,30 +649,6 @@ def test_dti_tz_localize_bdate_range(self):
649649
localized = dr.tz_localize(pytz.utc)
650650
tm.assert_index_equal(dr_utc, localized)
651651

652-
@pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"])
653-
@pytest.mark.parametrize(
654-
"method, exp", [["NaT", pd.NaT], ["raise", None], ["foo", "invalid"]]
655-
)
656-
def test_dti_tz_localize_nonexistent(self, tz, method, exp):
657-
# GH 8917
658-
n = 60
659-
dti = date_range(start="2015-03-29 02:00:00", periods=n, freq="min")
660-
if method == "raise":
661-
with pytest.raises(pytz.NonExistentTimeError, match="2015-03-29 02:00:00"):
662-
dti.tz_localize(tz, nonexistent=method)
663-
elif exp == "invalid":
664-
msg = (
665-
"The nonexistent argument must be one of "
666-
"'raise', 'NaT', 'shift_forward', 'shift_backward' "
667-
"or a timedelta object"
668-
)
669-
with pytest.raises(ValueError, match=msg):
670-
dti.tz_localize(tz, nonexistent=method)
671-
else:
672-
result = dti.tz_localize(tz, nonexistent=method)
673-
expected = DatetimeIndex([exp] * n, tz=tz)
674-
tm.assert_index_equal(result, expected)
675-
676652
@pytest.mark.parametrize(
677653
"start_ts, tz, end_ts, shift",
678654
[
@@ -730,10 +706,9 @@ def test_dti_tz_localize_nonexistent_shift(
730706
tm.assert_index_equal(result, expected)
731707

732708
@pytest.mark.parametrize("offset", [-1, 1])
733-
@pytest.mark.parametrize("tz_type", ["", "dateutil/"])
734-
def test_dti_tz_localize_nonexistent_shift_invalid(self, offset, tz_type):
709+
def test_dti_tz_localize_nonexistent_shift_invalid(self, offset, warsaw):
735710
# GH 8917
736-
tz = tz_type + "Europe/Warsaw"
711+
tz = warsaw
737712
dti = DatetimeIndex([Timestamp("2015-03-29 02:20:00")])
738713
msg = "The provided timedelta will relocalize on a nonexistent time"
739714
with pytest.raises(ValueError, match=msg):

Diff for: pandas/tests/scalar/timestamp/test_timezones.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ def test_tz_localize_ambiguous_raise(self):
141141
with pytest.raises(AmbiguousTimeError, match=msg):
142142
ts.tz_localize("US/Pacific", ambiguous="raise")
143143

144-
def test_tz_localize_nonexistent_invalid_arg(self):
144+
def test_tz_localize_nonexistent_invalid_arg(self, warsaw):
145145
# GH 22644
146-
tz = "Europe/Warsaw"
146+
tz = warsaw
147147
ts = Timestamp("2015-03-29 02:00:00")
148148
msg = (
149149
"The nonexistent argument must be one of 'raise', 'NaT', "
@@ -291,27 +291,26 @@ def test_timestamp_tz_localize_nonexistent_shift(
291291
assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
292292

293293
@pytest.mark.parametrize("offset", [-1, 1])
294-
@pytest.mark.parametrize("tz_type", ["", "dateutil/"])
295-
def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset, tz_type):
294+
def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset, warsaw):
296295
# GH 8917, 24466
297-
tz = tz_type + "Europe/Warsaw"
296+
tz = warsaw
298297
ts = Timestamp("2015-03-29 02:20:00")
299298
msg = "The provided timedelta will relocalize on a nonexistent time"
300299
with pytest.raises(ValueError, match=msg):
301300
ts.tz_localize(tz, nonexistent=timedelta(seconds=offset))
302301

303-
@pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"])
304302
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
305-
def test_timestamp_tz_localize_nonexistent_NaT(self, tz, unit):
303+
def test_timestamp_tz_localize_nonexistent_NaT(self, warsaw, unit):
306304
# GH 8917
305+
tz = warsaw
307306
ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
308307
result = ts.tz_localize(tz, nonexistent="NaT")
309308
assert result is NaT
310309

311-
@pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"])
312310
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
313-
def test_timestamp_tz_localize_nonexistent_raise(self, tz, unit):
311+
def test_timestamp_tz_localize_nonexistent_raise(self, warsaw, unit):
314312
# GH 8917
313+
tz = warsaw
315314
ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
316315
msg = "2015-03-29 02:20:00"
317316
with pytest.raises(pytz.NonExistentTimeError, match=msg):

Diff for: pandas/tests/series/methods/test_tz_localize.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def test_series_tz_localize_matching_index(self):
5858
)
5959
tm.assert_series_equal(result, expected)
6060

61-
@pytest.mark.parametrize("tz", ["Europe/Warsaw", "dateutil/Europe/Warsaw"])
6261
@pytest.mark.parametrize(
6362
"method, exp",
6463
[
@@ -68,8 +67,9 @@ def test_series_tz_localize_matching_index(self):
6867
["foo", "invalid"],
6968
],
7069
)
71-
def test_tz_localize_nonexistent(self, tz, method, exp):
70+
def test_tz_localize_nonexistent(self, warsaw, method, exp):
7271
# GH 8917
72+
tz = warsaw
7373
n = 60
7474
dti = date_range(start="2015-03-29 02:00:00", periods=n, freq="min")
7575
ser = Series(1, index=dti)
@@ -85,13 +85,27 @@ def test_tz_localize_nonexistent(self, tz, method, exp):
8585
df.tz_localize(tz, nonexistent=method)
8686

8787
elif exp == "invalid":
88-
with pytest.raises(ValueError, match="argument must be one of"):
88+
msg = (
89+
"The nonexistent argument must be one of "
90+
"'raise', 'NaT', 'shift_forward', 'shift_backward' "
91+
"or a timedelta object"
92+
)
93+
with pytest.raises(ValueError, match=msg):
8994
dti.tz_localize(tz, nonexistent=method)
90-
with pytest.raises(ValueError, match="argument must be one of"):
95+
with pytest.raises(ValueError, match=msg):
9196
ser.tz_localize(tz, nonexistent=method)
92-
with pytest.raises(ValueError, match="argument must be one of"):
97+
with pytest.raises(ValueError, match=msg):
9398
df.tz_localize(tz, nonexistent=method)
9499

100+
elif method == "shift_forward" and type(tz).__name__ == "ZoneInfo":
101+
msg = "nonexistent shifting is not implemented with ZoneInfo tzinfos"
102+
with pytest.raises(NotImplementedError, match=msg):
103+
ser.tz_localize(tz, nonexistent=method)
104+
with pytest.raises(NotImplementedError, match=msg):
105+
df.tz_localize(tz, nonexistent=method)
106+
with pytest.raises(NotImplementedError, match=msg):
107+
dti.tz_localize(tz, nonexistent=method)
108+
95109
else:
96110
result = ser.tz_localize(tz, nonexistent=method)
97111
expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz))
@@ -101,6 +115,9 @@ def test_tz_localize_nonexistent(self, tz, method, exp):
101115
expected = expected.to_frame()
102116
tm.assert_frame_equal(result, expected)
103117

118+
res_index = dti.tz_localize(tz, nonexistent=method)
119+
tm.assert_index_equal(res_index, expected.index)
120+
104121
@pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"])
105122
def test_series_tz_localize_empty(self, tzstr):
106123
# GH#2248

0 commit comments

Comments
 (0)