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 |
||
206 | def prepare( |
||
207 | self: Self, |
||
208 | data: Any, |
||
209 | *, |
||
210 | inf_handling: NanInfHandling, |
||
211 | nan_handling: NanInfHandling, |
||
212 | ): |
||
213 | """ |
||
214 | Recursively replaces infinite float and numpy values with strings. |
||
215 | Orjson encodes NaN, inf, and +inf as JSON null. |
||
216 | This function converts to string as needed to preserve infinite values. |
||
217 | Any float scalar (`np.floating` and `float`) will be replaced with a string. |
||
218 | Any `np.ndarray`, whether it contains an infinite value or not, will be converted |
||
219 | to an ndarray of strings. |
||
220 | The returned result may still not be serializable with orjson or :meth:`orjson_bytes`. |
||
221 | Trying those methods is the best way to test for serializeablity. |
||
222 | """ |
||
223 | # we go to great lengths to avoid importing numpy |
||
224 | # no np.isinf, np.isneginf, or np.isnan allowed |
||
225 | # we can use the fact that Numpy float types compare to float, |
||
226 | # including to -inf and +inf, where all comparisons between Inf/-Inf and NaN are False |
||
227 | # So our logic is is_infinite := (data > NEG_INF) != (data < INF) |
||
228 | # Meanwhile, we only need to deal with floats: |
||
229 | # - int and bool stay as-is |
||
230 | # - str stays as-is |
||
231 | # - complex gets converted |
||
232 | # figure out the type |
||
233 | is_dict = hasattr(data, "items") and hasattr(data, "keys") and hasattr(data, "values") |
||
234 | is_list = isinstance(data, list) |
||
235 | is_list_with_inf = ( |
||
236 | is_list and all(isinstance(e, float) for e in data) and not all((v > NEG_INF) == (v < INF) for v in data) |
||
237 | ) |
||
238 | is_list_with_nan = ( |
||
239 | is_list and all(isinstance(e, float) for e in data) and all(v == NEG_INF or v == INF for v in data) |
||
240 | ) |
||
241 | is_np_array = type(data).__name__ == "ndarray" and hasattr(data, "dtype") |
||
242 | is_np_array_with_inf = bool( |
||
243 | is_np_array and str(data.dtype).startswith("float") and not all((v > NEG_INF) == (v < INF) for v in data), |
||
244 | ) |
||
245 | is_np_array_with_nan = bool( |
||
246 | is_np_array and str(data.dtype).startswith("float") and all(v == NEG_INF or v == INF for v in data), |
||
247 | ) |
||
248 | is_inf_scalar = bool( |
||
249 | (isinstance(data, float) or str(type(data)).startswith("<class 'numpy.float")) |
||
250 | and (data > NEG_INF) != (data < INF), |
||
251 | ) |
||
252 | is_nan_scalar = bool( |
||
253 | (isinstance(data, float) or str(type(data)).startswith("<class 'numpy.float")) |
||
254 | and (data == NEG_INF or data == INF), |
||
255 | ) |
||
256 | # fix it |
||
257 | if is_dict: |
||
258 | return { |
||
259 | str(k): self.prepare(v, inf_handling=inf_handling, nan_handling=nan_handling) for k, v in data.items() |
||
260 | } |
||
261 | if (is_list_with_inf or is_np_array_with_inf) and inf_handling is NanInfHandling.raise_error: |
||
262 | raise ValueError(f"Array '{data}' contains Inf or -Inf") |
||
263 | if (is_list_with_nan or is_np_array_with_nan) and nan_handling is NanInfHandling.raise_error: |
||
264 | raise ValueError(f"Array '{data}' contains NaN") |
||
265 | if is_inf_scalar and inf_handling is NanInfHandling.raise_error: |
||
266 | raise ValueError(f"Value '{data}' is Inf or -Inf") |
||
267 | if is_nan_scalar and nan_handling is NanInfHandling.raise_error: |
||
268 | raise ValueError(f"Value '{data}' is NaN") |
||
269 | if ( |
||
270 | (is_list_with_inf or is_np_array_with_inf or is_list_with_nan or is_list_with_nan) |
||
271 | and inf_handling is NanInfHandling.convert_to_str |
||
272 | and nan_handling is NanInfHandling.convert_to_str |
||
273 | ): |
||
274 | return [str(v) for v in data] |
||
275 | if ( |
||
276 | (is_list_with_inf or is_np_array_with_inf) |
||
277 | and (is_list_with_nan or is_list_with_nan) |
||
278 | and inf_handling is NanInfHandling.convert_to_str |
||
279 | and nan_handling is NanInfHandling.convert_to_null |
||
280 | ): |
||
281 | return [None if float(v) == NAN else str(v) for v in data] |
||
282 | if ( |
||
283 | (is_list_with_inf or is_np_array_with_inf) |
||
284 | and (is_list_with_nan or is_list_with_nan) |
||
285 | and inf_handling is NanInfHandling.convert_to_null |
||
286 | and nan_handling is NanInfHandling.convert_to_str |
||
287 | ): |
||
288 | return [None if float(v) == INF or float(v) == NEG_INF else str(v) for v in data] |
||
289 | if is_np_array: |
||
290 | return data.tolist() |
||
291 | if is_list: |
||
292 | return [self.prepare(e, inf_handling=inf_handling, nan_handling=nan_handling) for e in data] |
||
293 | if ( |
||
294 | is_inf_scalar |
||
295 | and inf_handling is NanInfHandling.convert_to_str |
||
296 | or is_nan_scalar |
||
297 | and nan_handling is NanInfHandling.convert_to_str |
||
298 | ): |
||
299 | return str(data) |
||
300 | return data |
||
301 | |||
304 |