Skip to content

Commit 4c80f22

Browse files
authored
feat(python!): consistently convert to given time zone in Series constructor (#16828)
1 parent 6811b44 commit 4c80f22

File tree

18 files changed

+214
-220
lines changed

18 files changed

+214
-220
lines changed

py-polars/polars/_utils/construction/series.py

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import contextlib
4-
import warnings
54
from datetime import date, datetime, time, timedelta
65
from decimal import Decimal as PyDecimal
76
from itertools import islice
@@ -24,7 +23,6 @@
2423
is_simple_numpy_backed_pandas_series,
2524
)
2625
from polars._utils.various import (
27-
find_stacklevel,
2826
range_to_series,
2927
)
3028
from polars._utils.wrap import wrap_s
@@ -64,7 +62,6 @@
6462
from polars.dependencies import numpy as np
6563
from polars.dependencies import pandas as pd
6664
from polars.dependencies import pyarrow as pa
67-
from polars.exceptions import TimeZoneAwareConstructorWarning
6865

6966
with contextlib.suppress(ImportError): # Module not available when building docs
7067
from polars.polars import PySeries
@@ -203,41 +200,13 @@ def sequence_to_pyseries(
203200
s = wrap_s(py_series).dt.cast_time_unit(time_unit)
204201

205202
if (values_dtype == Date) & (dtype == Datetime):
206-
return (
207-
s.cast(Datetime(time_unit or "us"))
208-
.dt.replace_time_zone(
209-
time_zone,
210-
ambiguous="raise" if strict else "null",
211-
non_existent="raise" if strict else "null",
212-
)
213-
._s
214-
)
203+
result = s.cast(Datetime(time_unit or "us"))
204+
if time_zone is not None:
205+
result = result.dt.convert_time_zone(time_zone)
206+
return result._s
215207

216208
if (dtype == Datetime) and (value.tzinfo is not None or time_zone is not None):
217-
values_tz = str(value.tzinfo) if value.tzinfo is not None else None
218-
dtype_tz = time_zone
219-
if values_tz is not None and (dtype_tz is not None and dtype_tz != "UTC"):
220-
msg = (
221-
"time-zone-aware datetimes are converted to UTC"
222-
"\n\nPlease either drop the time zone from the dtype, or set it to 'UTC'."
223-
" To convert to a different time zone, please use `.dt.convert_time_zone`."
224-
)
225-
raise ValueError(msg)
226-
if values_tz != "UTC" and dtype_tz is None:
227-
warnings.warn(
228-
"Constructing a Series with time-zone-aware "
229-
"datetimes results in a Series with UTC time zone. "
230-
"To silence this warning, you can filter "
231-
"warnings of class TimeZoneAwareConstructorWarning, or "
232-
"set 'UTC' as the time zone of your datatype.",
233-
TimeZoneAwareConstructorWarning,
234-
stacklevel=find_stacklevel(),
235-
)
236-
return s.dt.replace_time_zone(
237-
dtype_tz or "UTC",
238-
ambiguous="raise" if strict else "null",
239-
non_existent="raise" if strict else "null",
240-
)._s
209+
return s.dt.convert_time_zone(time_zone or "UTC")._s
241210
return s._s
242211

243212
elif (

py-polars/polars/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,6 @@ class PolarsInefficientMapWarning(PolarsWarning): # type: ignore[misc]
145145
"""Warning issued when a potentially slow `map_*` operation is performed."""
146146

147147

148-
class TimeZoneAwareConstructorWarning(PolarsWarning): # type: ignore[misc]
149-
"""Warning issued when constructing Series from non-UTC time-zone-aware inputs."""
150-
151-
152148
class UnstableWarning(PolarsWarning): # type: ignore[misc]
153149
"""Warning issued when unstable functionality is used."""
154150

py-polars/polars/selectors.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,17 +1330,20 @@ def datetime(
13301330
13311331
Examples
13321332
--------
1333-
>>> from datetime import datetime, date
1333+
>>> from datetime import datetime, date, timezone
13341334
>>> import polars.selectors as cs
1335+
>>> from zoneinfo import ZoneInfo
1336+
>>> tokyo_tz = ZoneInfo("Asia/Tokyo")
1337+
>>> utc_tz = timezone.utc
13351338
>>> df = pl.DataFrame(
13361339
... {
13371340
... "tstamp_tokyo": [
1338-
... datetime(1999, 7, 21, 5, 20, 16, 987654),
1339-
... datetime(2000, 5, 16, 6, 21, 21, 123465),
1341+
... datetime(1999, 7, 21, 5, 20, 16, 987654, tzinfo=tokyo_tz),
1342+
... datetime(2000, 5, 16, 6, 21, 21, 123465, tzinfo=tokyo_tz),
13401343
... ],
13411344
... "tstamp_utc": [
1342-
... datetime(2023, 4, 10, 12, 14, 16, 999000),
1343-
... datetime(2025, 8, 25, 14, 18, 22, 666000),
1345+
... datetime(2023, 4, 10, 12, 14, 16, 999000, tzinfo=utc_tz),
1346+
... datetime(2025, 8, 25, 14, 18, 22, 666000, tzinfo=utc_tz),
13441347
... ],
13451348
... "tstamp": [
13461349
... datetime(2000, 11, 20, 18, 12, 16, 600000),

py-polars/tests/unit/constructors/test_constructors.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from polars._utils.construction.utils import try_get_type_hints
1717
from polars.datatypes import PolarsDataType, numpy_char_code_to_dtype
1818
from polars.dependencies import dataclasses, pydantic
19-
from polars.exceptions import TimeZoneAwareConstructorWarning
2019
from polars.testing import assert_frame_equal, assert_series_equal
2120

2221
if TYPE_CHECKING:
@@ -897,21 +896,15 @@ def test_init_1d_sequence() -> None:
897896
[datetime(2020, 1, 1, tzinfo=timezone.utc)], schema={"ts": pl.Datetime("ms")}
898897
)
899898
assert df.schema == {"ts": pl.Datetime("ms", "UTC")}
900-
with pytest.warns(
901-
TimeZoneAwareConstructorWarning, match="Series with UTC time zone"
902-
):
903-
df = pl.DataFrame(
904-
[datetime(2020, 1, 1, tzinfo=timezone(timedelta(hours=1)))],
905-
schema={"ts": pl.Datetime("ms")},
906-
)
899+
df = pl.DataFrame(
900+
[datetime(2020, 1, 1, tzinfo=timezone(timedelta(hours=1)))],
901+
schema={"ts": pl.Datetime("ms")},
902+
)
907903
assert df.schema == {"ts": pl.Datetime("ms", "UTC")}
908-
with pytest.warns(
909-
TimeZoneAwareConstructorWarning, match="Series with UTC time zone"
910-
):
911-
df = pl.DataFrame(
912-
[datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Kathmandu"))],
913-
schema={"ts": pl.Datetime("ms")},
914-
)
904+
df = pl.DataFrame(
905+
[datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Kathmandu"))],
906+
schema={"ts": pl.Datetime("ms")},
907+
)
915908
assert df.schema == {"ts": pl.Datetime("ms", "UTC")}
916909

917910

py-polars/tests/unit/constructors/test_series.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,27 @@ def test_series_init_ambiguous_datetime() -> None:
106106
value = datetime(2001, 10, 28, 2)
107107
dtype = pl.Datetime(time_zone="Europe/Belgrade")
108108

109-
with pytest.raises(pl.ComputeError, match="ambiguous"):
110-
pl.Series([value], dtype=dtype, strict=True)
109+
result = pl.Series([value], dtype=dtype, strict=True)
110+
expected = pl.Series([datetime(2001, 10, 28, 3)]).dt.replace_time_zone(
111+
"Europe/Belgrade"
112+
)
113+
assert_series_equal(result, expected)
111114

112115
result = pl.Series([value], dtype=dtype, strict=False)
113-
expected = pl.Series([None], dtype=dtype)
114116
assert_series_equal(result, expected)
115117

116118

117119
def test_series_init_nonexistent_datetime() -> None:
118120
value = datetime(2024, 3, 31, 2, 30)
119121
dtype = pl.Datetime(time_zone="Europe/Amsterdam")
120122

121-
with pytest.raises(pl.ComputeError, match="non-existent"):
122-
pl.Series([value], dtype=dtype, strict=True)
123+
result = pl.Series([value], dtype=dtype, strict=True)
124+
expected = pl.Series([datetime(2024, 3, 31, 4, 30)]).dt.replace_time_zone(
125+
"Europe/Amsterdam"
126+
)
127+
assert_series_equal(result, expected)
123128

124129
result = pl.Series([value], dtype=dtype, strict=False)
125-
expected = pl.Series([None], dtype=dtype)
126130
assert_series_equal(result, expected)
127131

128132

py-polars/tests/unit/dataframe/test_df.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import contextlib
43
import sys
54
import typing
65
from collections import OrderedDict
@@ -18,7 +17,6 @@
1817
import polars.selectors as cs
1918
from polars._utils.construction import iterable_to_pydf
2019
from polars.datatypes import DTYPE_TEMPORAL_UNITS, INTEGER_DTYPES
21-
from polars.exceptions import TimeZoneAwareConstructorWarning
2220
from polars.testing import (
2321
assert_frame_equal,
2422
assert_frame_not_equal,
@@ -2427,7 +2425,10 @@ def test_init_datetimes_with_timezone() -> None:
24272425
},
24282426
):
24292427
result = pl.DataFrame( # type: ignore[arg-type]
2430-
data={"d1": [dtm], "d2": [dtm]},
2428+
data={
2429+
"d1": [dtm.replace(tzinfo=ZoneInfo(tz_us))],
2430+
"d2": [dtm.replace(tzinfo=ZoneInfo(tz_europe))],
2431+
},
24312432
**type_overrides,
24322433
)
24332434
expected = pl.DataFrame(
@@ -2446,25 +2447,22 @@ def test_init_datetimes_with_timezone() -> None:
24462447
"dtype_time_zone",
24472448
"expected_time_zone",
24482449
"expected_item",
2449-
"warn",
24502450
),
24512451
[
2452-
(None, "", None, None, datetime(2020, 1, 1), False),
2452+
(None, "", None, None, datetime(2020, 1, 1)),
24532453
(
24542454
timezone(timedelta(hours=-8)),
24552455
"-08:00",
24562456
"UTC",
24572457
"UTC",
24582458
datetime(2020, 1, 1, 8, tzinfo=timezone.utc),
2459-
False,
24602459
),
24612460
(
24622461
timezone(timedelta(hours=-8)),
24632462
"-08:00",
24642463
None,
24652464
"UTC",
24662465
datetime(2020, 1, 1, 8, tzinfo=timezone.utc),
2467-
True,
24682466
),
24692467
],
24702468
)
@@ -2474,19 +2472,11 @@ def test_init_vs_strptime_consistency(
24742472
dtype_time_zone: str | None,
24752473
expected_time_zone: str,
24762474
expected_item: datetime,
2477-
warn: bool,
24782475
) -> None:
2479-
msg = r"UTC time zone"
2480-
context_manager: contextlib.AbstractContextManager[pytest.WarningsRecorder | None]
2481-
if warn:
2482-
context_manager = pytest.warns(TimeZoneAwareConstructorWarning, match=msg)
2483-
else:
2484-
context_manager = contextlib.nullcontext()
2485-
with context_manager:
2486-
result_init = pl.Series(
2487-
[datetime(2020, 1, 1, tzinfo=tzinfo)],
2488-
dtype=pl.Datetime("us", dtype_time_zone),
2489-
)
2476+
result_init = pl.Series(
2477+
[datetime(2020, 1, 1, tzinfo=tzinfo)],
2478+
dtype=pl.Datetime("us", dtype_time_zone),
2479+
)
24902480
result_strptime = pl.Series([f"2020-01-01 00:00{offset}"]).str.strptime(
24912481
pl.Datetime("us", dtype_time_zone)
24922482
)
@@ -2495,13 +2485,12 @@ def test_init_vs_strptime_consistency(
24952485
assert_series_equal(result_init, result_strptime)
24962486

24972487

2498-
def test_init_vs_strptime_consistency_raises() -> None:
2499-
msg = "-aware datetimes are converted to UTC"
2500-
with pytest.raises(ValueError, match=msg):
2501-
pl.Series(
2502-
[datetime(2020, 1, 1, tzinfo=timezone(timedelta(hours=-8)))],
2503-
dtype=pl.Datetime("us", "US/Pacific"),
2504-
)
2488+
def test_init_vs_strptime_consistency_converts() -> None:
2489+
result = pl.Series(
2490+
[datetime(2020, 1, 1, tzinfo=timezone(timedelta(hours=-8)))],
2491+
dtype=pl.Datetime("us", "US/Pacific"),
2492+
).item()
2493+
assert result == datetime(2020, 1, 1, 0, 0, tzinfo=ZoneInfo(key="US/Pacific"))
25052494
result = (
25062495
pl.Series(["2020-01-01 00:00-08:00"])
25072496
.str.strptime(pl.Datetime("us", "US/Pacific"))

0 commit comments

Comments
 (0)