We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
feat: memtable() should support nullable pandas types
### What happened?

nullable dtypes like pd.Int64Dtype(). Probably fails with other ones too, but I haven't tested.

```python
import pandas as pd
import ibis

df = pd.DataFrame(
    {
        "x": pd.Series([1, 2, None], dtype=pd.Int64Dtype()),
    }
)
ibis.memtable(df)
```

### What version of ibis are you using?

4.0.0

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

duckdb

### Relevant log output

```sh
stacktrace
---------------------------------------------------------------------------
IbisTypeError                             Traceback (most recent call last)
Cell In[14], line 9
      2 import ibis
      4 df = pd.DataFrame(
      5     {
      6         "x": pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype()),
      7     }
      8 )
----> 9 ibis.memtable(df)

File ~/.pyenv/versions/3.10.4/lib/python3.10/functools.py:889, in singledispatch.<locals>.wrapper(*args, **kw)
    885 if not args:
    886     raise TypeError(f'{funcname} requires at least '
    887                     '1 positional argument')
--> 889 return dispatch(args[0].__class__)(*args, **kw)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/api.py:409, in memtable(data, columns, schema, name)
    403     newcols = getattr(
    404         schema,
    405         "names",
    406         (f"col{i:d}" for i in range(len(cols))),
    407     )
    408     df = df.rename(columns=dict(zip(cols, newcols)))
--> 409 return _memtable_from_dataframe(df, name=name, schema=schema)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/api.py:425, in _memtable_from_dataframe(df, name, schema)
    415 def _memtable_from_dataframe(
    416     df: pd.DataFrame,
    417     *,
    418     name: str | None = None,
    419     schema: SupportsSchema | None = None,
    420 ) -> Table:
    421     from ibis.backends.pandas.client import DataFrameProxy, PandasInMemoryTable
    423     op = PandasInMemoryTable(
    424         name=name if name is not None else next(_gen_memtable_name),
--> 425         schema=sch.infer(df) if schema is None else schema,
    426         data=DataFrameProxy(df),
    427     )
    428     return op.to_expr()

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/multipledispatch/dispatcher.py:278, in Dispatcher.__call__(self, *args, **kwargs)
    276     self._cache[types] = func
    277 try:
--> 278     return func(*args, **kwargs)
    280 except MDNotImplementedError:
    281     funcs = self.dispatch_iter(*types)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/pandas/client.py:81, in infer_pandas_schema(df, schema)
     79         ibis_dtype = dt.dtype(schema[column_name])
     80     else:
---> 81         ibis_dtype = dt.infer(df[column_name]).value_type
     83     pairs.append((column_name, ibis_dtype))
     85 return sch.schema(pairs)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/common/dispatch.py:88, in lazy_singledispatch.<locals>.call(arg, *args, **kwargs)
     86 @functools.wraps(func)
     87 def call(arg, *args, **kwargs):
---> 88     return dispatch(type(arg))(arg, *args, **kwargs)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/datatypes/value.py:233, in infer_pandas_series(value)
    231     value_dtype = _infer_object_array_dtype(value)
    232 else:
--> 233     value_dtype = dt.dtype(value.dtype)
    235 return dt.Array(value_dtype)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/multipledispatch/dispatcher.py:278, in Dispatcher.__call__(self, *args, **kwargs)
    276     self._cache[types] = func
    277 try:
--> 278     return func(*args, **kwargs)
    280 except MDNotImplementedError:
    281     funcs = self.dispatch_iter(*types)

File ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/datatypes/core.py:29, in dtype_from_object(value, **kwargs)
     27 @dtype.register(object)
     28 def dtype_from_object(value, **kwargs) -> DataType:
---> 29     raise IbisTypeError(f'Value {value!r} is not a valid datatype')

IbisTypeError: Value Int64Dtype() is not a valid datatype
```


### 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/backends/pandas/client.py`
Content:
```
1 """The pandas client implementation."""
2 
3 from __future__ import annotations
4 
5 import json
6 
7 import numpy as np
8 import pandas as pd
9 import toolz
10 from dateutil.parser import parse as date_parse
11 from pandas.api.types import CategoricalDtype, DatetimeTZDtype
12 
13 import ibis.expr.datatypes as dt
14 import ibis.expr.operations as ops
15 import ibis.expr.rules as rlz
16 import ibis.expr.schema as sch
17 from ibis import util
18 from ibis.backends.base import Database
19 from ibis.common.grounds import Immutable
20 
21 _ibis_dtypes = toolz.valmap(
22     np.dtype,
23     {
24         dt.Boolean: np.bool_,
25         dt.Null: np.object_,
26         dt.Array: np.object_,
27         dt.String: np.object_,
28         dt.Binary: np.object_,
29         dt.Date: 'datetime64[ns]',
30         dt.Time: 'timedelta64[ns]',
31         dt.Timestamp: 'datetime64[ns]',
32         dt.Int8: np.int8,
33         dt.Int16: np.int16,
34         dt.Int32: np.int32,
35         dt.Int64: np.int64,
36         dt.UInt8: np.uint8,
37         dt.UInt16: np.uint16,
38         dt.UInt32: np.uint32,
39         dt.UInt64: np.uint64,
40         dt.Float16: np.float16,
41         dt.Float32: np.float32,
42         dt.Float64: np.float64,
43         dt.Decimal: np.object_,
44         dt.Struct: np.object_,
45     },
46 )
47 
48 
49 @dt.dtype.register(DatetimeTZDtype)
50 def from_pandas_tzdtype(value):
51     return dt.Timestamp(timezone=str(value.tz))
52 
53 
54 @dt.dtype.register(CategoricalDtype)
55 def from_pandas_categorical(_):
56     return dt.Category()
57 
58 
59 @dt.dtype.register(pd.core.arrays.string_.StringDtype)
60 def from_pandas_string(_):
61     return dt.String()
62 
63 
64 @sch.schema.register(pd.Series)
65 def schema_from_series(s):
66     return sch.schema(tuple(s.items()))
67 
68 
69 @sch.infer.register(pd.DataFrame)
70 def infer_pandas_schema(df: pd.DataFrame, schema=None):
71     schema = schema if schema is not None else {}
72 
73     pairs = []
74     for column_name in df.dtypes.keys():
75         if not isinstance(column_name, str):
76             raise TypeError('Column names must be strings to use the pandas backend')
77 
78         if column_name in schema:
79             ibis_dtype = dt.dtype(schema[column_name])
80         else:
81             ibis_dtype = dt.infer(df[column_name]).value_type
82 
83         pairs.append((column_name, ibis_dtype))
84 
85     return sch.schema(pairs)
86 
87 
88 def ibis_dtype_to_pandas(ibis_dtype: dt.DataType):
89     """Convert ibis dtype to the pandas / numpy alternative."""
90     assert isinstance(ibis_dtype, dt.DataType)
91 
92     if ibis_dtype.is_timestamp() and ibis_dtype.timezone:
93         return DatetimeTZDtype('ns', ibis_dtype.timezone)
94     elif ibis_dtype.is_interval():
95         return np.dtype(f'timedelta64[{ibis_dtype.unit}]')
96     elif ibis_dtype.is_category():
97         return CategoricalDtype()
98     else:
99         return _ibis_dtypes.get(type(ibis_dtype), np.dtype(np.object_))
100 
101 
102 def ibis_schema_to_pandas(schema):
103     return list(zip(schema.names, map(ibis_dtype_to_pandas, schema.types)))
104 
105 
106 @sch.convert.register(DatetimeTZDtype, dt.Timestamp, pd.Series)
107 def convert_datetimetz_to_timestamp(_, out_dtype, column):
108     output_timezone = out_dtype.timezone
109     if output_timezone is not None:
110         return column.dt.tz_convert(output_timezone)
111     return column.astype(out_dtype.to_pandas(), errors='ignore')
112 
113 
114 PANDAS_STRING_TYPES = {'string', 'unicode', 'bytes'}
115 PANDAS_DATE_TYPES = {'datetime', 'datetime64', 'date'}
116 
117 
118 @sch.convert.register(np.dtype, dt.Interval, pd.Series)
119 def convert_any_to_interval(_, out_dtype, column):
120     return column.values.astype(out_dtype.to_pandas())
121 
122 
123 @sch.convert.register(np.dtype, dt.String, pd.Series)
124 def convert_any_to_string(_, out_dtype, column):
125     result = column.astype(out_dtype.to_pandas(), errors='ignore')
126     return result
127 
128 
129 @sch.convert.register(np.dtype, dt.Boolean, pd.Series)
130 def convert_boolean_to_series(in_dtype, out_dtype, column):
131     # XXX: this is a workaround until #1595 can be addressed
132     in_dtype_type = in_dtype.type
133     out_dtype_type = out_dtype.to_pandas().type
134     if column.empty or (
135         in_dtype_type != np.object_ and in_dtype_type != out_dtype_type
136     ):
137         return column.astype(out_dtype_type)
138     return column
139 
140 
141 @sch.convert.register(DatetimeTZDtype, dt.Date, pd.Series)
142 def convert_timestamp_to_date(in_dtype, out_dtype, column):
143     if in_dtype.tz is not None:
144         column = column.dt.tz_convert("UTC").dt.tz_localize(None)
145     return column.astype(out_dtype.to_pandas(), errors='ignore').dt.normalize()
146 
147 
148 @sch.convert.register(object, dt.DataType, pd.Series)
149 def convert_any_to_any(_, out_dtype, column):
150     try:
151         return column.astype(out_dtype.to_pandas())
152     except pd.errors.OutOfBoundsDatetime:
153         try:
154             return column.map(date_parse)
155         except TypeError:
156             return column
157     except Exception:  # noqa: BLE001
158         return column
159 
160 
161 @sch.convert.register(object, dt.Struct, pd.Series)
162 def convert_struct_to_dict(_, out_dtype, column):
163     def convert_element(values, names=out_dtype.names):
164         if values is None or isinstance(values, dict) or pd.isna(values):
165             return values
166         return dict(zip(names, values))
167 
168     return column.map(convert_element)
169 
170 
171 @sch.convert.register(np.dtype, dt.Array, pd.Series)
172 def convert_array_to_series(in_dtype, out_dtype, column):
173     return column.map(lambda x: x if x is None else list(x))
174 
175 
176 @sch.convert.register(np.dtype, dt.JSON, pd.Series)
177 def convert_json_to_series(in_, out, col: pd.Series):
178     def try_json(x):
179         if x is None:
180             return x
181         try:
182             return json.loads(x)
183         except (TypeError, json.JSONDecodeError):
184             return x
185 
186     return pd.Series(list(map(try_json, col)), dtype="object")
187 
188 
189 class DataFrameProxy(Immutable, util.ToFrame):
190     __slots__ = ('_df', '_hash')
191 
192     def __init__(self, df):
193         object.__setattr__(self, "_df", df)
194         object.__setattr__(self, "_hash", hash((type(df), id(df))))
195 
196     def __hash__(self):
197         return self._hash
198 
199     def __repr__(self):
200         df_repr = util.indent(repr(self._df), spaces=2)
201         return f"{self.__class__.__name__}:\n{df_repr}"
202 
203     def to_frame(self):
204         return self._df
205 
206 
207 class PandasInMemoryTable(ops.InMemoryTable):
208     data = rlz.instance_of(DataFrameProxy)
209 
210 
211 class PandasTable(ops.DatabaseTable):
212     pass
213 
214 
215 class PandasDatabase(Database):
216     pass
217
```
--- 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/backends/pandas/client.py b/ibis/backends/pandas/client.py
--- a/ibis/backends/pandas/client.py
+++ b/ibis/backends/pandas/client.py
@@ -56,9 +56,16 @@
     return dt.Category()
 
 
-@dt.dtype.register(pd.core.arrays.string_.StringDtype)
-def from_pandas_string(_):
-    return dt.String()
+@dt.dtype.register(pd.core.dtypes.base.ExtensionDtype)
+def from_pandas_extension_dtype(t):
+    return getattr(dt, t.__class__.__name__.replace("Dtype", "").lower())
+
+
+@dt.dtype.register(pd.core.arrays.arrow.dtype.ArrowDtype)
+def from_pandas_arrow_extension_dtype(t):
+    import ibis.backends.pyarrow.datatypes as _  # noqa: F401
+
+    return dt.dtype(t.pyarrow_dtype)
 
 
 @sch.schema.register(pd.Series)

{"golden_diff": "diff --git a/ibis/backends/pandas/client.py b/ibis/backends/pandas/client.py\n--- a/ibis/backends/pandas/client.py\n+++ b/ibis/backends/pandas/client.py\n@@ -56,9 +56,16 @@\n     return dt.Category()\n \n \n-@dt.dtype.register(pd.core.arrays.string_.StringDtype)\n-def from_pandas_string(_):\n-    return dt.String()\n+@dt.dtype.register(pd.core.dtypes.base.ExtensionDtype)\n+def from_pandas_extension_dtype(t):\n+    return getattr(dt, t.__class__.__name__.replace(\"Dtype\", \"\").lower())\n+\n+\n+@dt.dtype.register(pd.core.arrays.arrow.dtype.ArrowDtype)\n+def from_pandas_arrow_extension_dtype(t):\n+    import ibis.backends.pyarrow.datatypes as _  # noqa: F401\n+\n+    return dt.dtype(t.pyarrow_dtype)\n \n \n @sch.schema.register(pd.Series)\n", "issue": "feat: memtable() should support nullable pandas types\n### What happened?\n\nnullable dtypes like pd.Int64Dtype(). Probably fails with other ones too, but I haven't tested.\r\n\r\n```python\r\nimport pandas as pd\r\nimport ibis\r\n\r\ndf = pd.DataFrame(\r\n    {\r\n        \"x\": pd.Series([1, 2, None], dtype=pd.Int64Dtype()),\r\n    }\r\n)\r\nibis.memtable(df)\r\n```\n\n### What version of ibis are you using?\n\n4.0.0\n\n### What backend(s) are you using, if any?\n\nduckdb\n\n### Relevant log output\n\n```sh\nstacktrace\r\n---------------------------------------------------------------------------\r\nIbisTypeError                             Traceback (most recent call last)\r\nCell In[14], line 9\r\n      2 import ibis\r\n      4 df = pd.DataFrame(\r\n      5     {\r\n      6         \"x\": pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype()),\r\n      7     }\r\n      8 )\r\n----> 9 ibis.memtable(df)\r\n\r\nFile ~/.pyenv/versions/3.10.4/lib/python3.10/functools.py:889, in singledispatch.<locals>.wrapper(*args, **kw)\r\n    885 if not args:\r\n    886     raise TypeError(f'{funcname} requires at least '\r\n    887                     '1 positional argument')\r\n--> 889 return dispatch(args[0].__class__)(*args, **kw)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/api.py:409, in memtable(data, columns, schema, name)\r\n    403     newcols = getattr(\r\n    404         schema,\r\n    405         \"names\",\r\n    406         (f\"col{i:d}\" for i in range(len(cols))),\r\n    407     )\r\n    408     df = df.rename(columns=dict(zip(cols, newcols)))\r\n--> 409 return _memtable_from_dataframe(df, name=name, schema=schema)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/api.py:425, in _memtable_from_dataframe(df, name, schema)\r\n    415 def _memtable_from_dataframe(\r\n    416     df: pd.DataFrame,\r\n    417     *,\r\n    418     name: str | None = None,\r\n    419     schema: SupportsSchema | None = None,\r\n    420 ) -> Table:\r\n    421     from ibis.backends.pandas.client import DataFrameProxy, PandasInMemoryTable\r\n    423     op = PandasInMemoryTable(\r\n    424         name=name if name is not None else next(_gen_memtable_name),\r\n--> 425         schema=sch.infer(df) if schema is None else schema,\r\n    426         data=DataFrameProxy(df),\r\n    427     )\r\n    428     return op.to_expr()\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/multipledispatch/dispatcher.py:278, in Dispatcher.__call__(self, *args, **kwargs)\r\n    276     self._cache[types] = func\r\n    277 try:\r\n--> 278     return func(*args, **kwargs)\r\n    280 except MDNotImplementedError:\r\n    281     funcs = self.dispatch_iter(*types)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/backends/pandas/client.py:81, in infer_pandas_schema(df, schema)\r\n     79         ibis_dtype = dt.dtype(schema[column_name])\r\n     80     else:\r\n---> 81         ibis_dtype = dt.infer(df[column_name]).value_type\r\n     83     pairs.append((column_name, ibis_dtype))\r\n     85 return sch.schema(pairs)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/common/dispatch.py:88, in lazy_singledispatch.<locals>.call(arg, *args, **kwargs)\r\n     86 @functools.wraps(func)\r\n     87 def call(arg, *args, **kwargs):\r\n---> 88     return dispatch(type(arg))(arg, *args, **kwargs)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/datatypes/value.py:233, in infer_pandas_series(value)\r\n    231     value_dtype = _infer_object_array_dtype(value)\r\n    232 else:\r\n--> 233     value_dtype = dt.dtype(value.dtype)\r\n    235 return dt.Array(value_dtype)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/multipledispatch/dispatcher.py:278, in Dispatcher.__call__(self, *args, **kwargs)\r\n    276     self._cache[types] = func\r\n    277 try:\r\n--> 278     return func(*args, **kwargs)\r\n    280 except MDNotImplementedError:\r\n    281     funcs = self.dispatch_iter(*types)\r\n\r\nFile ~/Library/Application Support/hatch/env/virtual/noatak-UM6-FHel/noatak/lib/python3.10/site-packages/ibis/expr/datatypes/core.py:29, in dtype_from_object(value, **kwargs)\r\n     27 @dtype.register(object)\r\n     28 def dtype_from_object(value, **kwargs) -> DataType:\r\n---> 29     raise IbisTypeError(f'Value {value!r} is not a valid datatype')\r\n\r\nIbisTypeError: Value Int64Dtype() is not a valid datatype\n```\n\n\n### Code of Conduct\n\n- [X] I agree to follow this project's Code of Conduct\n", "before_files": [{"content": "\"\"\"The pandas client implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\n\nimport numpy as np\nimport pandas as pd\nimport toolz\nfrom dateutil.parser import parse as date_parse\nfrom pandas.api.types import CategoricalDtype, DatetimeTZDtype\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.operations as ops\nimport ibis.expr.rules as rlz\nimport ibis.expr.schema as sch\nfrom ibis import util\nfrom ibis.backends.base import Database\nfrom ibis.common.grounds import Immutable\n\n_ibis_dtypes = toolz.valmap(\n    np.dtype,\n    {\n        dt.Boolean: np.bool_,\n        dt.Null: np.object_,\n        dt.Array: np.object_,\n        dt.String: np.object_,\n        dt.Binary: np.object_,\n        dt.Date: 'datetime64[ns]',\n        dt.Time: 'timedelta64[ns]',\n        dt.Timestamp: 'datetime64[ns]',\n        dt.Int8: np.int8,\n        dt.Int16: np.int16,\n        dt.Int32: np.int32,\n        dt.Int64: np.int64,\n        dt.UInt8: np.uint8,\n        dt.UInt16: np.uint16,\n        dt.UInt32: np.uint32,\n        dt.UInt64: np.uint64,\n        dt.Float16: np.float16,\n        dt.Float32: np.float32,\n        dt.Float64: np.float64,\n        dt.Decimal: np.object_,\n        dt.Struct: np.object_,\n    },\n)\n\n\n@dt.dtype.register(DatetimeTZDtype)\ndef from_pandas_tzdtype(value):\n    return dt.Timestamp(timezone=str(value.tz))\n\n\n@dt.dtype.register(CategoricalDtype)\ndef from_pandas_categorical(_):\n    return dt.Category()\n\n\n@dt.dtype.register(pd.core.arrays.string_.StringDtype)\ndef from_pandas_string(_):\n    return dt.String()\n\n\n@sch.schema.register(pd.Series)\ndef schema_from_series(s):\n    return sch.schema(tuple(s.items()))\n\n\n@sch.infer.register(pd.DataFrame)\ndef infer_pandas_schema(df: pd.DataFrame, 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('Column names must be strings to use the pandas backend')\n\n        if column_name in schema:\n            ibis_dtype = dt.dtype(schema[column_name])\n        else:\n            ibis_dtype = dt.infer(df[column_name]).value_type\n\n        pairs.append((column_name, ibis_dtype))\n\n    return sch.schema(pairs)\n\n\ndef ibis_dtype_to_pandas(ibis_dtype: dt.DataType):\n    \"\"\"Convert ibis dtype to the pandas / numpy alternative.\"\"\"\n    assert isinstance(ibis_dtype, dt.DataType)\n\n    if ibis_dtype.is_timestamp() and ibis_dtype.timezone:\n        return DatetimeTZDtype('ns', ibis_dtype.timezone)\n    elif ibis_dtype.is_interval():\n        return np.dtype(f'timedelta64[{ibis_dtype.unit}]')\n    elif ibis_dtype.is_category():\n        return CategoricalDtype()\n    else:\n        return _ibis_dtypes.get(type(ibis_dtype), np.dtype(np.object_))\n\n\ndef ibis_schema_to_pandas(schema):\n    return list(zip(schema.names, map(ibis_dtype_to_pandas, schema.types)))\n\n\n@sch.convert.register(DatetimeTZDtype, dt.Timestamp, pd.Series)\ndef convert_datetimetz_to_timestamp(_, out_dtype, column):\n    output_timezone = out_dtype.timezone\n    if output_timezone is not None:\n        return column.dt.tz_convert(output_timezone)\n    return column.astype(out_dtype.to_pandas(), errors='ignore')\n\n\nPANDAS_STRING_TYPES = {'string', 'unicode', 'bytes'}\nPANDAS_DATE_TYPES = {'datetime', 'datetime64', 'date'}\n\n\n@sch.convert.register(np.dtype, dt.Interval, pd.Series)\ndef convert_any_to_interval(_, out_dtype, column):\n    return column.values.astype(out_dtype.to_pandas())\n\n\n@sch.convert.register(np.dtype, dt.String, pd.Series)\ndef convert_any_to_string(_, out_dtype, column):\n    result = column.astype(out_dtype.to_pandas(), errors='ignore')\n    return result\n\n\n@sch.convert.register(np.dtype, dt.Boolean, pd.Series)\ndef convert_boolean_to_series(in_dtype, out_dtype, column):\n    # XXX: this is a workaround until #1595 can be addressed\n    in_dtype_type = in_dtype.type\n    out_dtype_type = out_dtype.to_pandas().type\n    if column.empty or (\n        in_dtype_type != np.object_ and in_dtype_type != out_dtype_type\n    ):\n        return column.astype(out_dtype_type)\n    return column\n\n\n@sch.convert.register(DatetimeTZDtype, dt.Date, pd.Series)\ndef convert_timestamp_to_date(in_dtype, out_dtype, column):\n    if in_dtype.tz is not None:\n        column = column.dt.tz_convert(\"UTC\").dt.tz_localize(None)\n    return column.astype(out_dtype.to_pandas(), errors='ignore').dt.normalize()\n\n\n@sch.convert.register(object, dt.DataType, pd.Series)\ndef convert_any_to_any(_, out_dtype, column):\n    try:\n        return column.astype(out_dtype.to_pandas())\n    except pd.errors.OutOfBoundsDatetime:\n        try:\n            return column.map(date_parse)\n        except TypeError:\n            return column\n    except Exception:  # noqa: BLE001\n        return column\n\n\n@sch.convert.register(object, dt.Struct, pd.Series)\ndef convert_struct_to_dict(_, out_dtype, column):\n    def convert_element(values, names=out_dtype.names):\n        if values is None or isinstance(values, dict) or pd.isna(values):\n            return values\n        return dict(zip(names, values))\n\n    return column.map(convert_element)\n\n\n@sch.convert.register(np.dtype, dt.Array, pd.Series)\ndef convert_array_to_series(in_dtype, out_dtype, column):\n    return column.map(lambda x: x if x is None else list(x))\n\n\n@sch.convert.register(np.dtype, dt.JSON, pd.Series)\ndef convert_json_to_series(in_, out, col: pd.Series):\n    def try_json(x):\n        if x is None:\n            return x\n        try:\n            return json.loads(x)\n        except (TypeError, json.JSONDecodeError):\n            return x\n\n    return pd.Series(list(map(try_json, col)), dtype=\"object\")\n\n\nclass DataFrameProxy(Immutable, util.ToFrame):\n    __slots__ = ('_df', '_hash')\n\n    def __init__(self, df):\n        object.__setattr__(self, \"_df\", df)\n        object.__setattr__(self, \"_hash\", hash((type(df), id(df))))\n\n    def __hash__(self):\n        return self._hash\n\n    def __repr__(self):\n        df_repr = util.indent(repr(self._df), spaces=2)\n        return f\"{self.__class__.__name__}:\\n{df_repr}\"\n\n    def to_frame(self):\n        return self._df\n\n\nclass PandasInMemoryTable(ops.InMemoryTable):\n    data = rlz.instance_of(DataFrameProxy)\n\n\nclass PandasTable(ops.DatabaseTable):\n    pass\n\n\nclass PandasDatabase(Database):\n    pass\n", "path": "ibis/backends/pandas/client.py"}], "after_files": [{"content": "\"\"\"The pandas client implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\n\nimport numpy as np\nimport pandas as pd\nimport toolz\nfrom dateutil.parser import parse as date_parse\nfrom pandas.api.types import CategoricalDtype, DatetimeTZDtype\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.operations as ops\nimport ibis.expr.rules as rlz\nimport ibis.expr.schema as sch\nfrom ibis import util\nfrom ibis.backends.base import Database\nfrom ibis.common.grounds import Immutable\n\n_ibis_dtypes = toolz.valmap(\n    np.dtype,\n    {\n        dt.Boolean: np.bool_,\n        dt.Null: np.object_,\n        dt.Array: np.object_,\n        dt.String: np.object_,\n        dt.Binary: np.object_,\n        dt.Date: 'datetime64[ns]',\n        dt.Time: 'timedelta64[ns]',\n        dt.Timestamp: 'datetime64[ns]',\n        dt.Int8: np.int8,\n        dt.Int16: np.int16,\n        dt.Int32: np.int32,\n        dt.Int64: np.int64,\n        dt.UInt8: np.uint8,\n        dt.UInt16: np.uint16,\n        dt.UInt32: np.uint32,\n        dt.UInt64: np.uint64,\n        dt.Float16: np.float16,\n        dt.Float32: np.float32,\n        dt.Float64: np.float64,\n        dt.Decimal: np.object_,\n        dt.Struct: np.object_,\n    },\n)\n\n\n@dt.dtype.register(DatetimeTZDtype)\ndef from_pandas_tzdtype(value):\n    return dt.Timestamp(timezone=str(value.tz))\n\n\n@dt.dtype.register(CategoricalDtype)\ndef from_pandas_categorical(_):\n    return dt.Category()\n\n\n@dt.dtype.register(pd.core.dtypes.base.ExtensionDtype)\ndef from_pandas_extension_dtype(t):\n    return getattr(dt, t.__class__.__name__.replace(\"Dtype\", \"\").lower())\n\n\n@dt.dtype.register(pd.core.arrays.arrow.dtype.ArrowDtype)\ndef from_pandas_arrow_extension_dtype(t):\n    import ibis.backends.pyarrow.datatypes as _  # noqa: F401\n\n    return dt.dtype(t.pyarrow_dtype)\n\n\n@sch.schema.register(pd.Series)\ndef schema_from_series(s):\n    return sch.schema(tuple(s.items()))\n\n\n@sch.infer.register(pd.DataFrame)\ndef infer_pandas_schema(df: pd.DataFrame, 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('Column names must be strings to use the pandas backend')\n\n        if column_name in schema:\n            ibis_dtype = dt.dtype(schema[column_name])\n        else:\n            ibis_dtype = dt.infer(df[column_name]).value_type\n\n        pairs.append((column_name, ibis_dtype))\n\n    return sch.schema(pairs)\n\n\ndef ibis_dtype_to_pandas(ibis_dtype: dt.DataType):\n    \"\"\"Convert ibis dtype to the pandas / numpy alternative.\"\"\"\n    assert isinstance(ibis_dtype, dt.DataType)\n\n    if ibis_dtype.is_timestamp() and ibis_dtype.timezone:\n        return DatetimeTZDtype('ns', ibis_dtype.timezone)\n    elif ibis_dtype.is_interval():\n        return np.dtype(f'timedelta64[{ibis_dtype.unit}]')\n    elif ibis_dtype.is_category():\n        return CategoricalDtype()\n    else:\n        return _ibis_dtypes.get(type(ibis_dtype), np.dtype(np.object_))\n\n\ndef ibis_schema_to_pandas(schema):\n    return list(zip(schema.names, map(ibis_dtype_to_pandas, schema.types)))\n\n\n@sch.convert.register(DatetimeTZDtype, dt.Timestamp, pd.Series)\ndef convert_datetimetz_to_timestamp(_, out_dtype, column):\n    output_timezone = out_dtype.timezone\n    if output_timezone is not None:\n        return column.dt.tz_convert(output_timezone)\n    return column.astype(out_dtype.to_pandas(), errors='ignore')\n\n\nPANDAS_STRING_TYPES = {'string', 'unicode', 'bytes'}\nPANDAS_DATE_TYPES = {'datetime', 'datetime64', 'date'}\n\n\n@sch.convert.register(np.dtype, dt.Interval, pd.Series)\ndef convert_any_to_interval(_, out_dtype, column):\n    return column.values.astype(out_dtype.to_pandas())\n\n\n@sch.convert.register(np.dtype, dt.String, pd.Series)\ndef convert_any_to_string(_, out_dtype, column):\n    result = column.astype(out_dtype.to_pandas(), errors='ignore')\n    return result\n\n\n@sch.convert.register(np.dtype, dt.Boolean, pd.Series)\ndef convert_boolean_to_series(in_dtype, out_dtype, column):\n    # XXX: this is a workaround until #1595 can be addressed\n    in_dtype_type = in_dtype.type\n    out_dtype_type = out_dtype.to_pandas().type\n    if column.empty or (\n        in_dtype_type != np.object_ and in_dtype_type != out_dtype_type\n    ):\n        return column.astype(out_dtype_type)\n    return column\n\n\n@sch.convert.register(DatetimeTZDtype, dt.Date, pd.Series)\ndef convert_timestamp_to_date(in_dtype, out_dtype, column):\n    if in_dtype.tz is not None:\n        column = column.dt.tz_convert(\"UTC\").dt.tz_localize(None)\n    return column.astype(out_dtype.to_pandas(), errors='ignore').dt.normalize()\n\n\n@sch.convert.register(object, dt.DataType, pd.Series)\ndef convert_any_to_any(_, out_dtype, column):\n    try:\n        return column.astype(out_dtype.to_pandas())\n    except pd.errors.OutOfBoundsDatetime:\n        try:\n            return column.map(date_parse)\n        except TypeError:\n            return column\n    except Exception:  # noqa: BLE001\n        return column\n\n\n@sch.convert.register(object, dt.Struct, pd.Series)\ndef convert_struct_to_dict(_, out_dtype, column):\n    def convert_element(values, names=out_dtype.names):\n        if values is None or isinstance(values, dict) or pd.isna(values):\n            return values\n        return dict(zip(names, values))\n\n    return column.map(convert_element)\n\n\n@sch.convert.register(np.dtype, dt.Array, pd.Series)\ndef convert_array_to_series(in_dtype, out_dtype, column):\n    return column.map(lambda x: x if x is None else list(x))\n\n\n@sch.convert.register(np.dtype, dt.JSON, pd.Series)\ndef convert_json_to_series(in_, out, col: pd.Series):\n    def try_json(x):\n        if x is None:\n            return x\n        try:\n            return json.loads(x)\n        except (TypeError, json.JSONDecodeError):\n            return x\n\n    return pd.Series(list(map(try_json, col)), dtype=\"object\")\n\n\nclass DataFrameProxy(Immutable, util.ToFrame):\n    __slots__ = ('_df', '_hash')\n\n    def __init__(self, df):\n        object.__setattr__(self, \"_df\", df)\n        object.__setattr__(self, \"_hash\", hash((type(df), id(df))))\n\n    def __hash__(self):\n        return self._hash\n\n    def __repr__(self):\n        df_repr = util.indent(repr(self._df), spaces=2)\n        return f\"{self.__class__.__name__}:\\n{df_repr}\"\n\n    def to_frame(self):\n        return self._df\n\n\nclass PandasInMemoryTable(ops.InMemoryTable):\n    data = rlz.instance_of(DataFrameProxy)\n\n\nclass PandasTable(ops.DatabaseTable):\n    pass\n\n\nclass PandasDatabase(Database):\n    pass\n", "path": "ibis/backends/pandas/client.py"}]}