Skip to content
Open
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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ I/O
- :meth:`HDFStore.put` and :meth:`HDFStore.append` now support storing :class:`Series` and :class:`DataFrame` columns with :class:`PeriodDtype` in both ``"fixed"`` and ``"table"`` formats (:issue:`41978`)
- Bug in :meth:`DataFrame.__repr__` raising ``TypeError`` for a column with a NumPy structured dtype (e.g. produced by :meth:`DataFrame.from_records` from a structured ``ndarray``) (:issue:`55011`)
- Bug in :meth:`DataFrame.__repr__` where horizontally truncated output could exceed the terminal width by up to 4 characters (:issue:`32461`)
- Bug in :meth:`DataFrame.to_json` and :meth:`Series.to_json` with ``orient="table"`` silently dropping the timezone of columns with a fixed-offset timezone (e.g. ``datetime.timezone(timedelta(hours=1))``), so the offset was lost on round-trip through :func:`read_json` (:issue:`39537`)
- Bug in :meth:`DataFrame.to_stata` raising ``KeyError`` when column names require renaming and ``convert_dates`` is specified for a different column (:issue:`60536`)
- Bug in :meth:`DataFrame.to_string` where ``formatters`` dict was applied to wrong columns when output was horizontally truncated via ``max_cols`` (:issue:`35410`)
- Fixed :func:`read_json` with ``lines=True`` and ``nrows=0`` to return an empty DataFrame (:issue:`64025`)
Expand Down
5 changes: 5 additions & 0 deletions pandas/io/json/_table_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

from datetime import timezone
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -149,6 +150,10 @@ def convert_pandas_type_to_json_field(arr) -> dict[str, JSONSerializable]:
zone = timezones.get_timezone(dtype.tz)
if isinstance(zone, str):
field["tz"] = zone
elif isinstance(zone, timezone):
# fixed-offset stdlib timezone, e.g. timezone(timedelta(hours=1));
# str gives a round-trippable "UTC+HH:MM" (GH#39537)
field["tz"] = str(zone)
elif isinstance(dtype, ExtensionDtype):
field["extDtype"] = dtype.name
return field
Expand Down
30 changes: 30 additions & 0 deletions pandas/tests/io/json/test_json_table_schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Tests for Table Schema integration."""

from collections import OrderedDict
from datetime import (
timedelta,
timezone,
)
from io import StringIO
import json

Expand Down Expand Up @@ -499,6 +503,23 @@ def test_convert_pandas_type_to_json_field_datetime(
expected.update(extra_exp)
assert result == expected

@pytest.mark.parametrize(
"offset,expected_tz",
[
(timedelta(hours=1), "UTC+01:00"),
(timedelta(hours=-5, minutes=-30), "UTC-05:30"),
],
)
def test_convert_pandas_type_to_json_field_fixed_offset(self, offset, expected_tz):
# GH#39537 fixed-offset stdlib timezones were silently dropped from the
# schema instead of being serialized as a round-trippable "UTC+HH:MM"
data = pd.Series(
pd.to_datetime([1.0], utc=True).tz_convert(timezone(offset)),
name="values",
)
result = convert_pandas_type_to_json_field(data)
assert result == {"name": "values", "type": "datetime", "tz": expected_tz}

def test_convert_pandas_type_to_json_period_range(self):
arr = pd.period_range("2016", freq="Y-DEC", periods=4)
result = convert_pandas_type_to_json_field(arr)
Expand Down Expand Up @@ -708,6 +729,15 @@ class TestTableOrientReader:
"2016-01-01", freq="D", periods=4, tz="US/Central", unit="ns"
) # added in # GH 35973
},
{
"fixed_offset": pd.date_range(
"2016-01-01",
freq="D",
periods=4,
tz=timezone(timedelta(hours=1)),
unit="ns",
) # GH#39537
},
],
)
def test_read_json_table_orient(self, index_nm, vals):
Expand Down
Loading