We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bug: non-string Categorical from pandas crash memtable()
### What happened?

```python
import ibis
import pandas as pd

df = pd.DataFrame(
    {
        "cat_string": pd.Series(["a", "b", "c"], dtype="category"),
        "cat_int": pd.Series([1, 2, 3], dtype="category"),
    }
)
ibis.memtable(df).execute()
# ArrowTypeError: ('Expected a string or bytes dtype, got int64', 'Conversion failed for column cat_int with type category')
```

### What version of ibis are you using?

main (5998143f09add788bb569b9e3ea091240ba00a26, Jan 22, 2024)

### What backend(s) are you using, if any?

duckdb

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ibis/formats/pandas.py`
Content:
```
1 from __future__ import annotations
2 
3 import contextlib
4 import datetime
5 import warnings
6 
7 import numpy as np
8 import pandas as pd
9 import pandas.api.types as pdt
10 import pyarrow as pa
11 
12 import ibis.expr.datatypes as dt
13 import ibis.expr.schema as sch
14 from ibis.common.temporal import normalize_timezone
15 from ibis.formats import DataMapper, SchemaMapper, TableProxy
16 from ibis.formats.numpy import NumpyType
17 from ibis.formats.pyarrow import PyArrowData, PyArrowSchema, PyArrowType
18 
19 _has_arrow_dtype = hasattr(pd, "ArrowDtype")
20 
21 if not _has_arrow_dtype:
22     warnings.warn(
23         f"The `ArrowDtype` class is not available in pandas {pd.__version__}. "
24         "Install pandas >= 1.5.0 for interop with pandas and arrow dtype support"
25     )
26 
27 
28 class PandasType(NumpyType):
29     @classmethod
30     def to_ibis(cls, typ, nullable=True):
31         if isinstance(typ, pdt.DatetimeTZDtype):
32             return dt.Timestamp(timezone=str(typ.tz), nullable=nullable)
33         elif pdt.is_datetime64_dtype(typ):
34             return dt.Timestamp(nullable=nullable)
35         elif isinstance(typ, pdt.CategoricalDtype):
36             return dt.String(nullable=nullable)
37         elif pdt.is_extension_array_dtype(typ):
38             if _has_arrow_dtype and isinstance(typ, pd.ArrowDtype):
39                 return PyArrowType.to_ibis(typ.pyarrow_dtype, nullable=nullable)
40             else:
41                 name = typ.__class__.__name__.replace("Dtype", "")
42                 klass = getattr(dt, name)
43                 return klass(nullable=nullable)
44         else:
45             return super().to_ibis(typ, nullable=nullable)
46 
47     @classmethod
48     def from_ibis(cls, dtype):
49         if dtype.is_timestamp() and dtype.timezone:
50             return pdt.DatetimeTZDtype("ns", dtype.timezone)
51         elif dtype.is_interval():
52             return np.dtype(f"timedelta64[{dtype.unit.short}]")
53         else:
54             return super().from_ibis(dtype)
55 
56 
57 class PandasSchema(SchemaMapper):
58     @classmethod
59     def to_ibis(cls, pandas_schema):
60         if isinstance(pandas_schema, pd.Series):
61             pandas_schema = pandas_schema.to_list()
62 
63         fields = {name: PandasType.to_ibis(t) for name, t in pandas_schema}
64 
65         return sch.Schema(fields)
66 
67     @classmethod
68     def from_ibis(cls, schema):
69         names = schema.names
70         types = [PandasType.from_ibis(t) for t in schema.types]
71         return list(zip(names, types))
72 
73 
74 class PandasData(DataMapper):
75     @classmethod
76     def infer_scalar(cls, s):
77         return PyArrowData.infer_scalar(s)
78 
79     @classmethod
80     def infer_column(cls, s):
81         return PyArrowData.infer_column(s)
82 
83     @classmethod
84     def infer_table(cls, df, schema=None):
85         schema = schema if schema is not None else {}
86 
87         pairs = []
88         for column_name in df.dtypes.keys():
89             if not isinstance(column_name, str):
90                 raise TypeError(
91                     "Column names must be strings to use the pandas backend"
92                 )
93 
94             if column_name in schema:
95                 ibis_dtype = schema[column_name]
96             else:
97                 pandas_column = df[column_name]
98                 pandas_dtype = pandas_column.dtype
99                 if pandas_dtype == np.object_:
100                     ibis_dtype = cls.infer_column(pandas_column)
101                 else:
102                     ibis_dtype = PandasType.to_ibis(pandas_dtype)
103 
104             pairs.append((column_name, ibis_dtype))
105 
106         return sch.Schema.from_tuples(pairs)
107 
108     @classmethod
109     def convert_table(cls, df, schema):
110         if len(schema) != len(df.columns):
111             raise ValueError(
112                 "schema column count does not match input data column count"
113             )
114 
115         for (name, series), dtype in zip(df.items(), schema.types):
116             df[name] = cls.convert_column(series, dtype)
117 
118         # return data with the schema's columns which may be different than the
119         # input columns
120         df.columns = schema.names
121         return df
122 
123     @classmethod
124     def convert_column(cls, obj, dtype):
125         pandas_type = PandasType.from_ibis(dtype)
126 
127         if obj.dtype == pandas_type and dtype.is_primitive():
128             return obj
129 
130         method_name = f"convert_{dtype.__class__.__name__}"
131         convert_method = getattr(cls, method_name, cls.convert_default)
132 
133         result = convert_method(obj, dtype, pandas_type)
134         assert not isinstance(result, np.ndarray), f"{convert_method} -> {type(result)}"
135         return result
136 
137     @classmethod
138     def convert_scalar(cls, obj, dtype):
139         df = PandasData.convert_table(obj, sch.Schema({obj.columns[0]: dtype}))
140         return df.iat[0, 0]
141 
142     @classmethod
143     def convert_GeoSpatial(cls, s, dtype, pandas_type):
144         return s
145 
146     convert_Point = (
147         convert_LineString
148     ) = (
149         convert_Polygon
150     ) = (
151         convert_MultiLineString
152     ) = convert_MultiPoint = convert_MultiPolygon = convert_GeoSpatial
153 
154     @classmethod
155     def convert_default(cls, s, dtype, pandas_type):
156         try:
157             return s.astype(pandas_type)
158         except Exception:  # noqa: BLE001
159             return s
160 
161     @classmethod
162     def convert_Boolean(cls, s, dtype, pandas_type):
163         if s.empty:
164             return s.astype(pandas_type)
165         elif pdt.is_object_dtype(s.dtype):
166             return s
167         elif s.dtype != pandas_type:
168             return s.map(bool, na_action="ignore")
169         else:
170             return s
171 
172     @classmethod
173     def convert_Timestamp(cls, s, dtype, pandas_type):
174         if isinstance(dtype, pd.DatetimeTZDtype):
175             return s.dt.tz_convert(dtype.timezone)
176         elif pdt.is_datetime64_dtype(s.dtype):
177             return s.dt.tz_localize(dtype.timezone)
178         else:
179             try:
180                 return s.astype(pandas_type)
181             except pd.errors.OutOfBoundsDatetime:  # uncovered
182                 try:
183                     from dateutil.parser import parse as date_parse
184 
185                     return s.map(date_parse, na_action="ignore")
186                 except TypeError:
187                     return s
188             except TypeError:
189                 try:
190                     return pd.to_datetime(s).dt.tz_convert(dtype.timezone)
191                 except TypeError:
192                     return pd.to_datetime(s).dt.tz_localize(dtype.timezone)
193 
194     @classmethod
195     def convert_Date(cls, s, dtype, pandas_type):
196         if isinstance(s.dtype, pd.DatetimeTZDtype):
197             s = s.dt.tz_convert("UTC").dt.tz_localize(None)
198         try:
199             return s.astype(pandas_type).dt.date
200         except (TypeError, pd._libs.tslibs.OutOfBoundsDatetime):
201 
202             def try_date(v):
203                 if isinstance(v, datetime.datetime):
204                     return v.date()
205                 elif isinstance(v, str):
206                     return datetime.date.fromisoformat(v)
207                 else:
208                     return v
209 
210             return s.map(try_date, na_action="ignore")
211 
212     @classmethod
213     def convert_Interval(cls, s, dtype, pandas_type):
214         values = s.values
215         try:
216             result = values.astype(pandas_type)
217         except ValueError:  # can happen when `column` is DateOffsets  # uncovered
218             result = s
219         else:
220             result = s.__class__(result, index=s.index, name=s.name)
221         return result
222 
223     @classmethod
224     def convert_String(cls, s, dtype, pandas_type):
225         return s.astype(pandas_type, errors="ignore")
226 
227     @classmethod
228     def convert_UUID(cls, s, dtype, pandas_type):
229         return s.map(cls.get_element_converter(dtype), na_action="ignore")
230 
231     @classmethod
232     def convert_Struct(cls, s, dtype, pandas_type):
233         return s.map(cls.get_element_converter(dtype), na_action="ignore")
234 
235     @classmethod
236     def convert_Array(cls, s, dtype, pandas_type):
237         return s.map(cls.get_element_converter(dtype), na_action="ignore")
238 
239     @classmethod
240     def convert_Map(cls, s, dtype, pandas_type):
241         return s.map(cls.get_element_converter(dtype), na_action="ignore")
242 
243     @classmethod
244     def convert_JSON(cls, s, dtype, pandas_type):
245         return s.map(cls.get_element_converter(dtype), na_action="ignore").astype(
246             "object"
247         )
248 
249     @classmethod
250     def get_element_converter(cls, dtype):
251         name = f"convert_{type(dtype).__name__}_element"
252         funcgen = getattr(cls, name, lambda _: lambda x: x)
253         return funcgen(dtype)
254 
255     @classmethod
256     def convert_Struct_element(cls, dtype):
257         converters = tuple(map(cls.get_element_converter, dtype.types))
258 
259         def convert(values, names=dtype.names, converters=converters):
260             if values is None:
261                 return values
262 
263             items = values.items() if isinstance(values, dict) else zip(names, values)
264             return {
265                 k: converter(v) if v is not None else v
266                 for converter, (k, v) in zip(converters, items)
267             }
268 
269         return convert
270 
271     @classmethod
272     def convert_JSON_element(cls, _):
273         import json
274 
275         def convert(value):
276             if value is None:
277                 return value
278             try:
279                 return json.loads(value)
280             except (TypeError, json.JSONDecodeError):
281                 return value
282 
283         return convert
284 
285     @classmethod
286     def convert_Timestamp_element(cls, dtype):
287         def converter(value, dtype=dtype):
288             if value is None:
289                 return value
290 
291             with contextlib.suppress(AttributeError):
292                 value = value.item()
293 
294             if isinstance(value, int):
295                 # this can only mean a numpy or pandas timestamp because they
296                 # both support nanosecond precision
297                 #
298                 # when the precision is less than or equal to the value
299                 # supported by Python datetime.dateimte a call to .item() will
300                 # return a datetime.datetime but when the precision is higher
301                 # than the value supported by Python the value is an integer
302                 #
303                 # TODO: can we do better than implicit truncation to microseconds?
304                 value = datetime.datetime.utcfromtimestamp(value / 1e9)
305 
306             if (tz := dtype.timezone) is not None:
307                 return value.astimezone(normalize_timezone(tz))
308 
309             return value.replace(tzinfo=None)
310 
311         return converter
312 
313     @classmethod
314     def convert_Array_element(cls, dtype):
315         convert_value = cls.get_element_converter(dtype.value_type)
316 
317         def convert(values):
318             if values is None:
319                 return values
320 
321             return [
322                 convert_value(value) if value is not None else value for value in values
323             ]
324 
325         return convert
326 
327     @classmethod
328     def convert_Map_element(cls, dtype):
329         convert_key = cls.get_element_converter(dtype.key_type)
330         convert_value = cls.get_element_converter(dtype.value_type)
331 
332         def convert(raw_row):
333             if raw_row is None:
334                 return raw_row
335 
336             row = dict(raw_row)
337             return dict(
338                 zip(map(convert_key, row.keys()), map(convert_value, row.values()))
339             )
340 
341         return convert
342 
343     @classmethod
344     def convert_UUID_element(cls, _):
345         from uuid import UUID
346 
347         def convert(value):
348             if value is None:
349                 return value
350             elif isinstance(value, UUID):
351                 return value
352             return UUID(value)
353 
354         return convert
355 
356 
357 class DaskData(PandasData):
358     @classmethod
359     def infer_column(cls, s):
360         return PyArrowData.infer_column(s.compute())
361 
362 
363 class PandasDataFrameProxy(TableProxy[pd.DataFrame]):
364     def to_frame(self) -> pd.DataFrame:
365         return self.obj
366 
367     def to_pyarrow(self, schema: sch.Schema) -> pa.Table:
368         pyarrow_schema = PyArrowSchema.from_ibis(schema)
369         return pa.Table.from_pandas(self.obj, schema=pyarrow_schema)
370
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ibis/formats/pandas.py b/ibis/formats/pandas.py
--- a/ibis/formats/pandas.py
+++ b/ibis/formats/pandas.py
@@ -33,7 +33,9 @@
         elif pdt.is_datetime64_dtype(typ):
             return dt.Timestamp(nullable=nullable)
         elif isinstance(typ, pdt.CategoricalDtype):
-            return dt.String(nullable=nullable)
+            if typ.categories is None or pdt.is_string_dtype(typ.categories):
+                return dt.String(nullable=nullable)
+            return cls.to_ibis(typ.categories.dtype, nullable=nullable)
         elif pdt.is_extension_array_dtype(typ):
             if _has_arrow_dtype and isinstance(typ, pd.ArrowDtype):
                 return PyArrowType.to_ibis(typ.pyarrow_dtype, nullable=nullable)

{"golden_diff": "diff --git a/ibis/formats/pandas.py b/ibis/formats/pandas.py\n--- a/ibis/formats/pandas.py\n+++ b/ibis/formats/pandas.py\n@@ -33,7 +33,9 @@\n         elif pdt.is_datetime64_dtype(typ):\n             return dt.Timestamp(nullable=nullable)\n         elif isinstance(typ, pdt.CategoricalDtype):\n-            return dt.String(nullable=nullable)\n+            if typ.categories is None or pdt.is_string_dtype(typ.categories):\n+                return dt.String(nullable=nullable)\n+            return cls.to_ibis(typ.categories.dtype, nullable=nullable)\n         elif pdt.is_extension_array_dtype(typ):\n             if _has_arrow_dtype and isinstance(typ, pd.ArrowDtype):\n                 return PyArrowType.to_ibis(typ.pyarrow_dtype, nullable=nullable)\n", "issue": "bug: non-string Categorical from pandas crash memtable()\n### What happened?\n\n```python\r\nimport ibis\r\nimport pandas as pd\r\n\r\ndf = pd.DataFrame(\r\n    {\r\n        \"cat_string\": pd.Series([\"a\", \"b\", \"c\"], dtype=\"category\"),\r\n        \"cat_int\": pd.Series([1, 2, 3], dtype=\"category\"),\r\n    }\r\n)\r\nibis.memtable(df).execute()\r\n# ArrowTypeError: ('Expected a string or bytes dtype, got int64', 'Conversion failed for column cat_int with type category')\r\n```\n\n### What version of ibis are you using?\n\nmain (5998143f09add788bb569b9e3ea091240ba00a26, Jan 22, 2024)\n\n### What backend(s) are you using, if any?\n\nduckdb\n\n### Relevant log output\n\n_No response_\n\n### Code of Conduct\n\n- [X] I agree to follow this project's Code of Conduct\n", "before_files": [{"content": "from __future__ import annotations\n\nimport contextlib\nimport datetime\nimport warnings\n\nimport numpy as np\nimport pandas as pd\nimport pandas.api.types as pdt\nimport pyarrow as pa\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.schema as sch\nfrom ibis.common.temporal import normalize_timezone\nfrom ibis.formats import DataMapper, SchemaMapper, TableProxy\nfrom ibis.formats.numpy import NumpyType\nfrom ibis.formats.pyarrow import PyArrowData, PyArrowSchema, PyArrowType\n\n_has_arrow_dtype = hasattr(pd, \"ArrowDtype\")\n\nif not _has_arrow_dtype:\n    warnings.warn(\n        f\"The `ArrowDtype` class is not available in pandas {pd.__version__}. \"\n        \"Install pandas >= 1.5.0 for interop with pandas and arrow dtype support\"\n    )\n\n\nclass PandasType(NumpyType):\n    @classmethod\n    def to_ibis(cls, typ, nullable=True):\n        if isinstance(typ, pdt.DatetimeTZDtype):\n            return dt.Timestamp(timezone=str(typ.tz), nullable=nullable)\n        elif pdt.is_datetime64_dtype(typ):\n            return dt.Timestamp(nullable=nullable)\n        elif isinstance(typ, pdt.CategoricalDtype):\n            return dt.String(nullable=nullable)\n        elif pdt.is_extension_array_dtype(typ):\n            if _has_arrow_dtype and isinstance(typ, pd.ArrowDtype):\n                return PyArrowType.to_ibis(typ.pyarrow_dtype, nullable=nullable)\n            else:\n                name = typ.__class__.__name__.replace(\"Dtype\", \"\")\n                klass = getattr(dt, name)\n                return klass(nullable=nullable)\n        else:\n            return super().to_ibis(typ, nullable=nullable)\n\n    @classmethod\n    def from_ibis(cls, dtype):\n        if dtype.is_timestamp() and dtype.timezone:\n            return pdt.DatetimeTZDtype(\"ns\", dtype.timezone)\n        elif dtype.is_interval():\n            return np.dtype(f\"timedelta64[{dtype.unit.short}]\")\n        else:\n            return super().from_ibis(dtype)\n\n\nclass PandasSchema(SchemaMapper):\n    @classmethod\n    def to_ibis(cls, pandas_schema):\n        if isinstance(pandas_schema, pd.Series):\n            pandas_schema = pandas_schema.to_list()\n\n        fields = {name: PandasType.to_ibis(t) for name, t in pandas_schema}\n\n        return sch.Schema(fields)\n\n    @classmethod\n    def from_ibis(cls, schema):\n        names = schema.names\n        types = [PandasType.from_ibis(t) for t in schema.types]\n        return list(zip(names, types))\n\n\nclass PandasData(DataMapper):\n    @classmethod\n    def infer_scalar(cls, s):\n        return PyArrowData.infer_scalar(s)\n\n    @classmethod\n    def infer_column(cls, s):\n        return PyArrowData.infer_column(s)\n\n    @classmethod\n    def infer_table(cls, df, schema=None):\n        schema = schema if schema is not None else {}\n\n        pairs = []\n        for column_name in df.dtypes.keys():\n            if not isinstance(column_name, str):\n                raise TypeError(\n                    \"Column names must be strings to use the pandas backend\"\n                )\n\n            if column_name in schema:\n                ibis_dtype = schema[column_name]\n            else:\n                pandas_column = df[column_name]\n                pandas_dtype = pandas_column.dtype\n                if pandas_dtype == np.object_:\n                    ibis_dtype = cls.infer_column(pandas_column)\n                else:\n                    ibis_dtype = PandasType.to_ibis(pandas_dtype)\n\n            pairs.append((column_name, ibis_dtype))\n\n        return sch.Schema.from_tuples(pairs)\n\n    @classmethod\n    def convert_table(cls, df, schema):\n        if len(schema) != len(df.columns):\n            raise ValueError(\n                \"schema column count does not match input data column count\"\n            )\n\n        for (name, series), dtype in zip(df.items(), schema.types):\n            df[name] = cls.convert_column(series, dtype)\n\n        # return data with the schema's columns which may be different than the\n        # input columns\n        df.columns = schema.names\n        return df\n\n    @classmethod\n    def convert_column(cls, obj, dtype):\n        pandas_type = PandasType.from_ibis(dtype)\n\n        if obj.dtype == pandas_type and dtype.is_primitive():\n            return obj\n\n        method_name = f\"convert_{dtype.__class__.__name__}\"\n        convert_method = getattr(cls, method_name, cls.convert_default)\n\n        result = convert_method(obj, dtype, pandas_type)\n        assert not isinstance(result, np.ndarray), f\"{convert_method} -> {type(result)}\"\n        return result\n\n    @classmethod\n    def convert_scalar(cls, obj, dtype):\n        df = PandasData.convert_table(obj, sch.Schema({obj.columns[0]: dtype}))\n        return df.iat[0, 0]\n\n    @classmethod\n    def convert_GeoSpatial(cls, s, dtype, pandas_type):\n        return s\n\n    convert_Point = (\n        convert_LineString\n    ) = (\n        convert_Polygon\n    ) = (\n        convert_MultiLineString\n    ) = convert_MultiPoint = convert_MultiPolygon = convert_GeoSpatial\n\n    @classmethod\n    def convert_default(cls, s, dtype, pandas_type):\n        try:\n            return s.astype(pandas_type)\n        except Exception:  # noqa: BLE001\n            return s\n\n    @classmethod\n    def convert_Boolean(cls, s, dtype, pandas_type):\n        if s.empty:\n            return s.astype(pandas_type)\n        elif pdt.is_object_dtype(s.dtype):\n            return s\n        elif s.dtype != pandas_type:\n            return s.map(bool, na_action=\"ignore\")\n        else:\n            return s\n\n    @classmethod\n    def convert_Timestamp(cls, s, dtype, pandas_type):\n        if isinstance(dtype, pd.DatetimeTZDtype):\n            return s.dt.tz_convert(dtype.timezone)\n        elif pdt.is_datetime64_dtype(s.dtype):\n            return s.dt.tz_localize(dtype.timezone)\n        else:\n            try:\n                return s.astype(pandas_type)\n            except pd.errors.OutOfBoundsDatetime:  # uncovered\n                try:\n                    from dateutil.parser import parse as date_parse\n\n                    return s.map(date_parse, na_action=\"ignore\")\n                except TypeError:\n                    return s\n            except TypeError:\n                try:\n                    return pd.to_datetime(s).dt.tz_convert(dtype.timezone)\n                except TypeError:\n                    return pd.to_datetime(s).dt.tz_localize(dtype.timezone)\n\n    @classmethod\n    def convert_Date(cls, s, dtype, pandas_type):\n        if isinstance(s.dtype, pd.DatetimeTZDtype):\n            s = s.dt.tz_convert(\"UTC\").dt.tz_localize(None)\n        try:\n            return s.astype(pandas_type).dt.date\n        except (TypeError, pd._libs.tslibs.OutOfBoundsDatetime):\n\n            def try_date(v):\n                if isinstance(v, datetime.datetime):\n                    return v.date()\n                elif isinstance(v, str):\n                    return datetime.date.fromisoformat(v)\n                else:\n                    return v\n\n            return s.map(try_date, na_action=\"ignore\")\n\n    @classmethod\n    def convert_Interval(cls, s, dtype, pandas_type):\n        values = s.values\n        try:\n            result = values.astype(pandas_type)\n        except ValueError:  # can happen when `column` is DateOffsets  # uncovered\n            result = s\n        else:\n            result = s.__class__(result, index=s.index, name=s.name)\n        return result\n\n    @classmethod\n    def convert_String(cls, s, dtype, pandas_type):\n        return s.astype(pandas_type, errors=\"ignore\")\n\n    @classmethod\n    def convert_UUID(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Struct(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Array(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Map(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_JSON(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\").astype(\n            \"object\"\n        )\n\n    @classmethod\n    def get_element_converter(cls, dtype):\n        name = f\"convert_{type(dtype).__name__}_element\"\n        funcgen = getattr(cls, name, lambda _: lambda x: x)\n        return funcgen(dtype)\n\n    @classmethod\n    def convert_Struct_element(cls, dtype):\n        converters = tuple(map(cls.get_element_converter, dtype.types))\n\n        def convert(values, names=dtype.names, converters=converters):\n            if values is None:\n                return values\n\n            items = values.items() if isinstance(values, dict) else zip(names, values)\n            return {\n                k: converter(v) if v is not None else v\n                for converter, (k, v) in zip(converters, items)\n            }\n\n        return convert\n\n    @classmethod\n    def convert_JSON_element(cls, _):\n        import json\n\n        def convert(value):\n            if value is None:\n                return value\n            try:\n                return json.loads(value)\n            except (TypeError, json.JSONDecodeError):\n                return value\n\n        return convert\n\n    @classmethod\n    def convert_Timestamp_element(cls, dtype):\n        def converter(value, dtype=dtype):\n            if value is None:\n                return value\n\n            with contextlib.suppress(AttributeError):\n                value = value.item()\n\n            if isinstance(value, int):\n                # this can only mean a numpy or pandas timestamp because they\n                # both support nanosecond precision\n                #\n                # when the precision is less than or equal to the value\n                # supported by Python datetime.dateimte a call to .item() will\n                # return a datetime.datetime but when the precision is higher\n                # than the value supported by Python the value is an integer\n                #\n                # TODO: can we do better than implicit truncation to microseconds?\n                value = datetime.datetime.utcfromtimestamp(value / 1e9)\n\n            if (tz := dtype.timezone) is not None:\n                return value.astimezone(normalize_timezone(tz))\n\n            return value.replace(tzinfo=None)\n\n        return converter\n\n    @classmethod\n    def convert_Array_element(cls, dtype):\n        convert_value = cls.get_element_converter(dtype.value_type)\n\n        def convert(values):\n            if values is None:\n                return values\n\n            return [\n                convert_value(value) if value is not None else value for value in values\n            ]\n\n        return convert\n\n    @classmethod\n    def convert_Map_element(cls, dtype):\n        convert_key = cls.get_element_converter(dtype.key_type)\n        convert_value = cls.get_element_converter(dtype.value_type)\n\n        def convert(raw_row):\n            if raw_row is None:\n                return raw_row\n\n            row = dict(raw_row)\n            return dict(\n                zip(map(convert_key, row.keys()), map(convert_value, row.values()))\n            )\n\n        return convert\n\n    @classmethod\n    def convert_UUID_element(cls, _):\n        from uuid import UUID\n\n        def convert(value):\n            if value is None:\n                return value\n            elif isinstance(value, UUID):\n                return value\n            return UUID(value)\n\n        return convert\n\n\nclass DaskData(PandasData):\n    @classmethod\n    def infer_column(cls, s):\n        return PyArrowData.infer_column(s.compute())\n\n\nclass PandasDataFrameProxy(TableProxy[pd.DataFrame]):\n    def to_frame(self) -> pd.DataFrame:\n        return self.obj\n\n    def to_pyarrow(self, schema: sch.Schema) -> pa.Table:\n        pyarrow_schema = PyArrowSchema.from_ibis(schema)\n        return pa.Table.from_pandas(self.obj, schema=pyarrow_schema)\n", "path": "ibis/formats/pandas.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport contextlib\nimport datetime\nimport warnings\n\nimport numpy as np\nimport pandas as pd\nimport pandas.api.types as pdt\nimport pyarrow as pa\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.schema as sch\nfrom ibis.common.temporal import normalize_timezone\nfrom ibis.formats import DataMapper, SchemaMapper, TableProxy\nfrom ibis.formats.numpy import NumpyType\nfrom ibis.formats.pyarrow import PyArrowData, PyArrowSchema, PyArrowType\n\n_has_arrow_dtype = hasattr(pd, \"ArrowDtype\")\n\nif not _has_arrow_dtype:\n    warnings.warn(\n        f\"The `ArrowDtype` class is not available in pandas {pd.__version__}. \"\n        \"Install pandas >= 1.5.0 for interop with pandas and arrow dtype support\"\n    )\n\n\nclass PandasType(NumpyType):\n    @classmethod\n    def to_ibis(cls, typ, nullable=True):\n        if isinstance(typ, pdt.DatetimeTZDtype):\n            return dt.Timestamp(timezone=str(typ.tz), nullable=nullable)\n        elif pdt.is_datetime64_dtype(typ):\n            return dt.Timestamp(nullable=nullable)\n        elif isinstance(typ, pdt.CategoricalDtype):\n            if typ.categories is None or pdt.is_string_dtype(typ.categories):\n                return dt.String(nullable=nullable)\n            return cls.to_ibis(typ.categories.dtype, nullable=nullable)\n        elif pdt.is_extension_array_dtype(typ):\n            if _has_arrow_dtype and isinstance(typ, pd.ArrowDtype):\n                return PyArrowType.to_ibis(typ.pyarrow_dtype, nullable=nullable)\n            else:\n                name = typ.__class__.__name__.replace(\"Dtype\", \"\")\n                klass = getattr(dt, name)\n                return klass(nullable=nullable)\n        else:\n            return super().to_ibis(typ, nullable=nullable)\n\n    @classmethod\n    def from_ibis(cls, dtype):\n        if dtype.is_timestamp() and dtype.timezone:\n            return pdt.DatetimeTZDtype(\"ns\", dtype.timezone)\n        elif dtype.is_interval():\n            return np.dtype(f\"timedelta64[{dtype.unit.short}]\")\n        else:\n            return super().from_ibis(dtype)\n\n\nclass PandasSchema(SchemaMapper):\n    @classmethod\n    def to_ibis(cls, pandas_schema):\n        if isinstance(pandas_schema, pd.Series):\n            pandas_schema = pandas_schema.to_list()\n\n        fields = {name: PandasType.to_ibis(t) for name, t in pandas_schema}\n\n        return sch.Schema(fields)\n\n    @classmethod\n    def from_ibis(cls, schema):\n        names = schema.names\n        types = [PandasType.from_ibis(t) for t in schema.types]\n        return list(zip(names, types))\n\n\nclass PandasData(DataMapper):\n    @classmethod\n    def infer_scalar(cls, s):\n        return PyArrowData.infer_scalar(s)\n\n    @classmethod\n    def infer_column(cls, s):\n        return PyArrowData.infer_column(s)\n\n    @classmethod\n    def infer_table(cls, df, schema=None):\n        schema = schema if schema is not None else {}\n\n        pairs = []\n        for column_name in df.dtypes.keys():\n            if not isinstance(column_name, str):\n                raise TypeError(\n                    \"Column names must be strings to use the pandas backend\"\n                )\n\n            if column_name in schema:\n                ibis_dtype = schema[column_name]\n            else:\n                pandas_column = df[column_name]\n                pandas_dtype = pandas_column.dtype\n                if pandas_dtype == np.object_:\n                    ibis_dtype = cls.infer_column(pandas_column)\n                else:\n                    ibis_dtype = PandasType.to_ibis(pandas_dtype)\n\n            pairs.append((column_name, ibis_dtype))\n\n        return sch.Schema.from_tuples(pairs)\n\n    @classmethod\n    def convert_table(cls, df, schema):\n        if len(schema) != len(df.columns):\n            raise ValueError(\n                \"schema column count does not match input data column count\"\n            )\n\n        for (name, series), dtype in zip(df.items(), schema.types):\n            df[name] = cls.convert_column(series, dtype)\n\n        # return data with the schema's columns which may be different than the\n        # input columns\n        df.columns = schema.names\n        return df\n\n    @classmethod\n    def convert_column(cls, obj, dtype):\n        pandas_type = PandasType.from_ibis(dtype)\n\n        if obj.dtype == pandas_type and dtype.is_primitive():\n            return obj\n\n        method_name = f\"convert_{dtype.__class__.__name__}\"\n        convert_method = getattr(cls, method_name, cls.convert_default)\n\n        result = convert_method(obj, dtype, pandas_type)\n        assert not isinstance(result, np.ndarray), f\"{convert_method} -> {type(result)}\"\n        return result\n\n    @classmethod\n    def convert_scalar(cls, obj, dtype):\n        df = PandasData.convert_table(obj, sch.Schema({obj.columns[0]: dtype}))\n        return df.iat[0, 0]\n\n    @classmethod\n    def convert_GeoSpatial(cls, s, dtype, pandas_type):\n        return s\n\n    convert_Point = (\n        convert_LineString\n    ) = (\n        convert_Polygon\n    ) = (\n        convert_MultiLineString\n    ) = convert_MultiPoint = convert_MultiPolygon = convert_GeoSpatial\n\n    @classmethod\n    def convert_default(cls, s, dtype, pandas_type):\n        try:\n            return s.astype(pandas_type)\n        except Exception:  # noqa: BLE001\n            return s\n\n    @classmethod\n    def convert_Boolean(cls, s, dtype, pandas_type):\n        if s.empty:\n            return s.astype(pandas_type)\n        elif pdt.is_object_dtype(s.dtype):\n            return s\n        elif s.dtype != pandas_type:\n            return s.map(bool, na_action=\"ignore\")\n        else:\n            return s\n\n    @classmethod\n    def convert_Timestamp(cls, s, dtype, pandas_type):\n        if isinstance(dtype, pd.DatetimeTZDtype):\n            return s.dt.tz_convert(dtype.timezone)\n        elif pdt.is_datetime64_dtype(s.dtype):\n            return s.dt.tz_localize(dtype.timezone)\n        else:\n            try:\n                return s.astype(pandas_type)\n            except pd.errors.OutOfBoundsDatetime:  # uncovered\n                try:\n                    from dateutil.parser import parse as date_parse\n\n                    return s.map(date_parse, na_action=\"ignore\")\n                except TypeError:\n                    return s\n            except TypeError:\n                try:\n                    return pd.to_datetime(s).dt.tz_convert(dtype.timezone)\n                except TypeError:\n                    return pd.to_datetime(s).dt.tz_localize(dtype.timezone)\n\n    @classmethod\n    def convert_Date(cls, s, dtype, pandas_type):\n        if isinstance(s.dtype, pd.DatetimeTZDtype):\n            s = s.dt.tz_convert(\"UTC\").dt.tz_localize(None)\n        try:\n            return s.astype(pandas_type).dt.date\n        except (TypeError, pd._libs.tslibs.OutOfBoundsDatetime):\n\n            def try_date(v):\n                if isinstance(v, datetime.datetime):\n                    return v.date()\n                elif isinstance(v, str):\n                    return datetime.date.fromisoformat(v)\n                else:\n                    return v\n\n            return s.map(try_date, na_action=\"ignore\")\n\n    @classmethod\n    def convert_Interval(cls, s, dtype, pandas_type):\n        values = s.values\n        try:\n            result = values.astype(pandas_type)\n        except ValueError:  # can happen when `column` is DateOffsets  # uncovered\n            result = s\n        else:\n            result = s.__class__(result, index=s.index, name=s.name)\n        return result\n\n    @classmethod\n    def convert_String(cls, s, dtype, pandas_type):\n        return s.astype(pandas_type, errors=\"ignore\")\n\n    @classmethod\n    def convert_UUID(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Struct(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Array(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_Map(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\")\n\n    @classmethod\n    def convert_JSON(cls, s, dtype, pandas_type):\n        return s.map(cls.get_element_converter(dtype), na_action=\"ignore\").astype(\n            \"object\"\n        )\n\n    @classmethod\n    def get_element_converter(cls, dtype):\n        name = f\"convert_{type(dtype).__name__}_element\"\n        funcgen = getattr(cls, name, lambda _: lambda x: x)\n        return funcgen(dtype)\n\n    @classmethod\n    def convert_Struct_element(cls, dtype):\n        converters = tuple(map(cls.get_element_converter, dtype.types))\n\n        def convert(values, names=dtype.names, converters=converters):\n            if values is None:\n                return values\n\n            items = values.items() if isinstance(values, dict) else zip(names, values)\n            return {\n                k: converter(v) if v is not None else v\n                for converter, (k, v) in zip(converters, items)\n            }\n\n        return convert\n\n    @classmethod\n    def convert_JSON_element(cls, _):\n        import json\n\n        def convert(value):\n            if value is None:\n                return value\n            try:\n                return json.loads(value)\n            except (TypeError, json.JSONDecodeError):\n                return value\n\n        return convert\n\n    @classmethod\n    def convert_Timestamp_element(cls, dtype):\n        def converter(value, dtype=dtype):\n            if value is None:\n                return value\n\n            with contextlib.suppress(AttributeError):\n                value = value.item()\n\n            if isinstance(value, int):\n                # this can only mean a numpy or pandas timestamp because they\n                # both support nanosecond precision\n                #\n                # when the precision is less than or equal to the value\n                # supported by Python datetime.dateimte a call to .item() will\n                # return a datetime.datetime but when the precision is higher\n                # than the value supported by Python the value is an integer\n                #\n                # TODO: can we do better than implicit truncation to microseconds?\n                value = datetime.datetime.utcfromtimestamp(value / 1e9)\n\n            if (tz := dtype.timezone) is not None:\n                return value.astimezone(normalize_timezone(tz))\n\n            return value.replace(tzinfo=None)\n\n        return converter\n\n    @classmethod\n    def convert_Array_element(cls, dtype):\n        convert_value = cls.get_element_converter(dtype.value_type)\n\n        def convert(values):\n            if values is None:\n                return values\n\n            return [\n                convert_value(value) if value is not None else value for value in values\n            ]\n\n        return convert\n\n    @classmethod\n    def convert_Map_element(cls, dtype):\n        convert_key = cls.get_element_converter(dtype.key_type)\n        convert_value = cls.get_element_converter(dtype.value_type)\n\n        def convert(raw_row):\n            if raw_row is None:\n                return raw_row\n\n            row = dict(raw_row)\n            return dict(\n                zip(map(convert_key, row.keys()), map(convert_value, row.values()))\n            )\n\n        return convert\n\n    @classmethod\n    def convert_UUID_element(cls, _):\n        from uuid import UUID\n\n        def convert(value):\n            if value is None:\n                return value\n            elif isinstance(value, UUID):\n                return value\n            return UUID(value)\n\n        return convert\n\n\nclass DaskData(PandasData):\n    @classmethod\n    def infer_column(cls, s):\n        return PyArrowData.infer_column(s.compute())\n\n\nclass PandasDataFrameProxy(TableProxy[pd.DataFrame]):\n    def to_frame(self) -> pd.DataFrame:\n        return self.obj\n\n    def to_pyarrow(self, schema: sch.Schema) -> pa.Table:\n        pyarrow_schema = PyArrowSchema.from_ibis(schema)\n        return pa.Table.from_pandas(self.obj, schema=pyarrow_schema)\n", "path": "ibis/formats/pandas.py"}]}