Skip to content

Commit b830b0e

Browse files
chrisgervangclaude
andauthored
fix(pydeck): Support pandas 3.x compatibility without breaking pandas 2.x (#9988)
Replace module-based DataFrame detection with duck-typing to work with both pandas 2.x and 3.x module paths. Add defensive error handling in JSON serialization to prevent crashes from objects without __dict__. Includes additional tests for edge cases. Fixes #9986 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a241b63 commit b830b0e

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

bindings/pydeck/pydeck/bindings/json_tools.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,18 @@ def default_serialize(o, remap_function=lower_camel_case_keys):
7272
"""Default method for rendering JSON from a dictionary"""
7373
if issubclass(type(o), PydeckType):
7474
return repr(o)
75-
attrs = vars(o)
75+
76+
# Handle objects without __dict__ (e.g., pandas 3.x DataFrames if detection fails)
77+
try:
78+
attrs = vars(o)
79+
except TypeError:
80+
if hasattr(o, "to_dict") and callable(getattr(o, "to_dict", None)):
81+
try:
82+
return o.to_dict(orient="records")
83+
except (TypeError, ValueError):
84+
pass
85+
return str(o)
86+
7687
attrs = {k: v for k, v in attrs.items() if v is not None}
7788
for ignore_attr in IGNORE_KEYS:
7889
if ignore_attr in attrs:

bindings/pydeck/pydeck/data_utils/type_checking.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ def is_pandas_df(obj):
66
bool
77
Returns True if object is a Pandas DataFrame and False otherwise
88
"""
9-
return obj.__class__.__module__ == "pandas.core.frame" and obj.to_records and obj.to_dict
9+
# Use duck-typing approach that works with both pandas 2.x and 3.x
10+
# Check for DataFrame-specific methods and the class name
11+
try:
12+
return (
13+
obj.__class__.__name__ == "DataFrame"
14+
and hasattr(obj, "to_records")
15+
and hasattr(obj, "to_dict")
16+
and hasattr(obj, "columns")
17+
and callable(getattr(obj, "to_records", None))
18+
and callable(getattr(obj, "to_dict", None))
19+
)
20+
except (AttributeError, TypeError):
21+
return False
1022

1123

1224
def has_geo_interface(obj):

bindings/pydeck/tests/test_data_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ def test_is_pandas_df():
4949
assert is_pandas_df(pd.DataFrame())
5050

5151

52+
def test_is_pandas_df_negative_cases():
53+
"""Test that is_pandas_df correctly rejects non-DataFrame objects"""
54+
assert not is_pandas_df(None)
55+
assert not is_pandas_df([1, 2, 3])
56+
assert not is_pandas_df({"a": 1})
57+
assert not is_pandas_df("string")
58+
assert not is_pandas_df(42)
59+
60+
# Test object with some but not all DataFrame methods
61+
class FakeDataFrame:
62+
def to_records(self):
63+
pass
64+
65+
assert not is_pandas_df(FakeDataFrame())
66+
67+
68+
def test_is_pandas_df_duck_typing():
69+
"""Test that is_pandas_df works with DataFrame duck-typing"""
70+
df = pd.DataFrame({"a": [1, 2, 3]})
71+
assert is_pandas_df(df)
72+
73+
# Verify the methods we rely on exist and are callable
74+
assert hasattr(df, "to_records")
75+
assert hasattr(df, "to_dict")
76+
assert hasattr(df, "columns")
77+
assert callable(df.to_records)
78+
assert callable(df.to_dict)
79+
80+
5281
def test_compute_view():
5382
actual = compute_view(POINTS, 0.95, ViewState)
5483
actual_pandas = compute_view(pd.DataFrame(POINTS), 0.95, ViewState)

0 commit comments

Comments
 (0)