|
19 | 19 | import uuid
|
20 | 20 | from datetime import date, datetime
|
21 | 21 | from decimal import Decimal
|
22 |
| -from typing import Any, Dict, Optional |
| 22 | +from typing import Any, Dict, Optional, Tuple |
23 | 23 |
|
24 | 24 | from .compat import string_types
|
25 | 25 | from .exceptions import ImproperlyConfigured, SerializationError
|
@@ -76,57 +76,9 @@ def default(self, data: Any) -> Any:
|
76 | 76 |
|
77 | 77 | # Special cases for numpy and pandas types
|
78 | 78 | # These are expensive to import so we try them last.
|
79 |
| - try: |
80 |
| - import numpy as np # type: ignore |
81 |
| - |
82 |
| - if isinstance( |
83 |
| - data, |
84 |
| - ( |
85 |
| - np.int_, |
86 |
| - np.intc, |
87 |
| - np.int8, |
88 |
| - np.int16, |
89 |
| - np.int32, |
90 |
| - np.int64, |
91 |
| - np.uint8, |
92 |
| - np.uint16, |
93 |
| - np.uint32, |
94 |
| - np.uint64, |
95 |
| - ), |
96 |
| - ): |
97 |
| - return int(data) |
98 |
| - elif isinstance( |
99 |
| - data, |
100 |
| - ( |
101 |
| - np.float_, |
102 |
| - np.float16, |
103 |
| - np.float32, |
104 |
| - np.float64, |
105 |
| - ), |
106 |
| - ): |
107 |
| - return float(data) |
108 |
| - elif isinstance(data, np.bool_): |
109 |
| - return bool(data) |
110 |
| - elif isinstance(data, np.datetime64): |
111 |
| - return data.item().isoformat() |
112 |
| - elif isinstance(data, np.ndarray): |
113 |
| - return data.tolist() |
114 |
| - except ImportError: |
115 |
| - pass |
116 |
| - |
117 |
| - try: |
118 |
| - import pandas as pd # type: ignore |
119 |
| - |
120 |
| - if isinstance(data, (pd.Series, pd.Categorical)): |
121 |
| - return data.tolist() |
122 |
| - elif isinstance(data, pd.Timestamp) and data is not getattr( |
123 |
| - pd, "NaT", None |
124 |
| - ): |
125 |
| - return data.isoformat() |
126 |
| - elif data is getattr(pd, "NA", None): |
127 |
| - return None |
128 |
| - except ImportError: |
129 |
| - pass |
| 79 | + serialized, value = _attempt_serialize_numpy_or_pandas(data) |
| 80 | + if serialized: |
| 81 | + return value |
130 | 82 |
|
131 | 83 | raise TypeError(f"Unable to serialize {data!r} (type: {type(data)})")
|
132 | 84 |
|
@@ -201,3 +153,94 @@ def loads(self, s: str, mimetype: Optional[str] = None) -> Any:
|
201 | 153 | )
|
202 | 154 |
|
203 | 155 | return deserializer.loads(s)
|
| 156 | + |
| 157 | + |
| 158 | +def _attempt_serialize_numpy_or_pandas(data: Any) -> Tuple[bool, Any]: |
| 159 | + """Attempts to serialize a value from the numpy or pandas libraries. |
| 160 | + This function is separate from JSONSerializer because the inner functions |
| 161 | + are rewritten to be no-ops if either library isn't available to avoid |
| 162 | + attempting to import and raising an ImportError over and over again. |
| 163 | +
|
| 164 | + Returns a tuple of (bool, Any) where the bool corresponds to whether |
| 165 | + the second value contains a properly serialized value and thus |
| 166 | + should be returned by JSONSerializer.default(). |
| 167 | + """ |
| 168 | + serialized, value = _attempt_serialize_numpy(data) |
| 169 | + if serialized: |
| 170 | + return serialized, value |
| 171 | + |
| 172 | + serialized, value = _attempt_serialize_pandas(data) |
| 173 | + if serialized: |
| 174 | + return serialized, value |
| 175 | + |
| 176 | + return False, None |
| 177 | + |
| 178 | + |
| 179 | +def _attempt_serialize_numpy(data: Any) -> Tuple[bool, Any]: |
| 180 | + global _attempt_serialize_numpy |
| 181 | + try: |
| 182 | + import numpy as np # type: ignore |
| 183 | + |
| 184 | + if isinstance( |
| 185 | + data, |
| 186 | + ( |
| 187 | + np.int_, |
| 188 | + np.intc, |
| 189 | + np.int8, |
| 190 | + np.int16, |
| 191 | + np.int32, |
| 192 | + np.int64, |
| 193 | + np.uint8, |
| 194 | + np.uint16, |
| 195 | + np.uint32, |
| 196 | + np.uint64, |
| 197 | + ), |
| 198 | + ): |
| 199 | + return True, int(data) |
| 200 | + elif isinstance( |
| 201 | + data, |
| 202 | + ( |
| 203 | + np.float_, |
| 204 | + np.float16, |
| 205 | + np.float32, |
| 206 | + np.float64, |
| 207 | + ), |
| 208 | + ): |
| 209 | + return True, float(data) |
| 210 | + elif isinstance(data, np.bool_): |
| 211 | + return True, bool(data) |
| 212 | + elif isinstance(data, np.datetime64): |
| 213 | + return True, data.item().isoformat() |
| 214 | + elif isinstance(data, np.ndarray): |
| 215 | + return True, data.tolist() |
| 216 | + |
| 217 | + except ImportError: |
| 218 | + # Since we failed to import 'numpy' we don't want to try again. |
| 219 | + _attempt_serialize_numpy = _attempt_serialize_noop |
| 220 | + |
| 221 | + return False, None |
| 222 | + |
| 223 | + |
| 224 | +def _attempt_serialize_pandas(data: Any) -> Tuple[bool, Any]: |
| 225 | + global _attempt_serialize_pandas |
| 226 | + try: |
| 227 | + import pandas as pd # type: ignore |
| 228 | + |
| 229 | + if isinstance(data, (pd.Series, pd.Categorical)): |
| 230 | + return True, data.tolist() |
| 231 | + elif isinstance(data, pd.Timestamp) and data is not getattr(pd, "NaT", None): |
| 232 | + return True, data.isoformat() |
| 233 | + elif data is getattr(pd, "NA", None): |
| 234 | + return True, None |
| 235 | + |
| 236 | + except ImportError: |
| 237 | + # Since we failed to import 'pandas' we don't want to try again. |
| 238 | + _attempt_serialize_pandas = _attempt_serialize_noop |
| 239 | + |
| 240 | + return False, None |
| 241 | + |
| 242 | + |
| 243 | +def _attempt_serialize_noop(data: Any) -> Tuple[bool, Any]: # noqa |
| 244 | + # Short-circuit if the above functions can't import |
| 245 | + # the corresponding library on the first attempt. |
| 246 | + return False, None |
0 commit comments