Conditions | 38 |
Total Lines | 95 |
Code Lines | 62 |
Lines | 0 |
Ratio | 0 % |
Changes | 0 |
Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.
For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.
Commonly applied refactorings include:
If many parameters/temporary variables are present:
Complex classes like pocketutils.tools.json_tools.JsonUtils.prepare() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | # SPDX-FileCopyrightText: Copyright 2020-2023, Contributors to pocketutils |
||
207 | def prepare( |
||
208 | self: Self, |
||
209 | data: Any, |
||
210 | *, |
||
211 | inf_handling: NanInfHandling, |
||
212 | nan_handling: NanInfHandling, |
||
213 | ): |
||
214 | """ |
||
215 | Recursively replaces infinite float and numpy values with strings. |
||
216 | Orjson encodes NaN, inf, and +inf as JSON null. |
||
217 | This function converts to string as needed to preserve infinite values. |
||
218 | Any float scalar (`np.floating` and `float`) will be replaced with a string. |
||
219 | Any `np.ndarray`, whether it contains an infinite value or not, will be converted |
||
220 | to an ndarray of strings. |
||
221 | The returned result may still not be serializable with orjson or :meth:`orjson_bytes`. |
||
222 | Trying those methods is the best way to test for serializeablity. |
||
223 | """ |
||
224 | # we go to great lengths to avoid importing numpy |
||
225 | # no np.isinf, np.isneginf, or np.isnan allowed |
||
226 | # we can use the fact that Numpy float types compare to float, |
||
227 | # including to -inf and +inf, where all comparisons between Inf/-Inf and NaN are False |
||
228 | # So our logic is is_infinite := (data > NEG_INF) != (data < INF) |
||
229 | # Meanwhile, we only need to deal with floats: |
||
230 | # - int and bool stay as-is |
||
231 | # - str stays as-is |
||
232 | # - complex gets converted |
||
233 | # figure out the type |
||
234 | is_dict = hasattr(data, "items") and hasattr(data, "keys") and hasattr(data, "values") |
||
235 | is_list = isinstance(data, list) |
||
236 | is_list_with_inf = ( |
||
237 | is_list and all(isinstance(e, float) for e in data) and not all((v > NEG_INF) == (v < INF) for v in data) |
||
238 | ) |
||
239 | is_list_with_nan = ( |
||
240 | is_list and all(isinstance(e, float) for e in data) and all(v == NEG_INF or v == INF for v in data) |
||
241 | ) |
||
242 | is_np_array = type(data).__name__ == "ndarray" and hasattr(data, "dtype") |
||
243 | is_np_array_with_inf = bool( |
||
244 | is_np_array and str(data.dtype).startswith("float") and not all((v > NEG_INF) == (v < INF) for v in data), |
||
245 | ) |
||
246 | is_np_array_with_nan = bool( |
||
247 | is_np_array and str(data.dtype).startswith("float") and all(v == NEG_INF or v == INF for v in data), |
||
248 | ) |
||
249 | is_inf_scalar = bool( |
||
250 | (isinstance(data, float) or str(type(data)).startswith("<class 'numpy.float")) |
||
251 | and (data > NEG_INF) != (data < INF), |
||
252 | ) |
||
253 | is_nan_scalar = bool( |
||
254 | (isinstance(data, float) or str(type(data)).startswith("<class 'numpy.float")) |
||
255 | and (data == NEG_INF or data == INF), |
||
256 | ) |
||
257 | # fix it |
||
258 | if is_dict: |
||
259 | return { |
||
260 | str(k): self.prepare(v, inf_handling=inf_handling, nan_handling=nan_handling) for k, v in data.items() |
||
261 | } |
||
262 | if (is_list_with_inf or is_np_array_with_inf) and inf_handling is NanInfHandling.raise_error: |
||
263 | raise ValueError(f"Array '{data}' contains Inf or -Inf") |
||
264 | if (is_list_with_nan or is_np_array_with_nan) and nan_handling is NanInfHandling.raise_error: |
||
265 | raise ValueError(f"Array '{data}' contains NaN") |
||
266 | if is_inf_scalar and inf_handling is NanInfHandling.raise_error: |
||
267 | raise ValueError(f"Value '{data}' is Inf or -Inf") |
||
268 | if is_nan_scalar and nan_handling is NanInfHandling.raise_error: |
||
269 | raise ValueError(f"Value '{data}' is NaN") |
||
270 | if ( |
||
271 | (is_list_with_inf or is_np_array_with_inf or is_list_with_nan or is_list_with_nan) |
||
272 | and inf_handling is NanInfHandling.convert_to_str |
||
273 | and nan_handling is NanInfHandling.convert_to_str |
||
274 | ): |
||
275 | return [str(v) for v in data] |
||
276 | if ( |
||
277 | (is_list_with_inf or is_np_array_with_inf) |
||
278 | and (is_list_with_nan or is_list_with_nan) |
||
279 | and inf_handling is NanInfHandling.convert_to_str |
||
280 | and nan_handling is NanInfHandling.convert_to_null |
||
281 | ): |
||
282 | return [None if float(v) == NAN else str(v) for v in data] |
||
283 | if ( |
||
284 | (is_list_with_inf or is_np_array_with_inf) |
||
285 | and (is_list_with_nan or is_list_with_nan) |
||
286 | and inf_handling is NanInfHandling.convert_to_null |
||
287 | and nan_handling is NanInfHandling.convert_to_str |
||
288 | ): |
||
289 | return [None if float(v) == INF or float(v) == NEG_INF else str(v) for v in data] |
||
290 | if is_np_array: |
||
291 | return data.tolist() |
||
292 | if is_list: |
||
293 | return [self.prepare(e, inf_handling=inf_handling, nan_handling=nan_handling) for e in data] |
||
294 | if ( |
||
295 | is_inf_scalar |
||
296 | and inf_handling is NanInfHandling.convert_to_str |
||
297 | or is_nan_scalar |
||
298 | and nan_handling is NanInfHandling.convert_to_str |
||
299 | ): |
||
300 | return str(data) |
||
301 | return data |
||
302 | |||
305 |