pyobjtojson 0.9__tar.gz → 0.9.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/PKG-INFO +25 -2
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/README.md +24 -1
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson/__init__.py +85 -18
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson.egg-info/PKG-INFO +25 -2
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyproject.toml +1 -1
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_edge_cases.py +61 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/LICENSE +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson.egg-info/SOURCES.txt +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson.egg-info/dependency_links.txt +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson.egg-info/requires.txt +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/pyobjtojson.egg-info/top_level.txt +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/setup.cfg +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_basic_types.py +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_circular_references.py +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_custom_classes.py +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_dataclasses.py +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_pydantic.py +0 -0
- {pyobjtojson-0.9 → pyobjtojson-0.9.1}/tests/test_standard_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobjtojson
|
|
3
|
-
Version: 0.9
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: A Python library that simplifies serializing any Python object to JSON-friendly structures, gracefully handling circular references.
|
|
5
5
|
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -251,7 +251,7 @@ obj_to_json(data)
|
|
|
251
251
|
|
|
252
252
|
## API Reference
|
|
253
253
|
|
|
254
|
-
### `obj_to_json(obj, check_circular=True, decimal_as_float=True)`
|
|
254
|
+
### `obj_to_json(obj, check_circular=True, decimal_as_float=True, non_finite="null")`
|
|
255
255
|
|
|
256
256
|
Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializable.
|
|
257
257
|
|
|
@@ -259,10 +259,33 @@ Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializ
|
|
|
259
259
|
- `obj` (Any): The object to serialize to JSON-like structures.
|
|
260
260
|
- `check_circular` (bool, optional): If True (default), detect and mark circular references as `"<circular reference>"`.
|
|
261
261
|
- `decimal_as_float` (bool, optional): If True (default), convert `Decimal` to `float`. If False, convert to string for high precision.
|
|
262
|
+
- `non_finite` (str, optional): How to represent non-finite floats (`inf`, `-inf`, `nan`), which have no JSON literal. One of:
|
|
263
|
+
- `"null"` (default): convert to `None`, matching JavaScript's `JSON.stringify`.
|
|
264
|
+
- `"string"`: convert to `"Infinity"`, `"-Infinity"`, or `"NaN"`.
|
|
265
|
+
- `"keep"`: leave the float as-is. Note this is **not** valid JSON and will raise with `json.dumps(..., allow_nan=False)`.
|
|
266
|
+
|
|
267
|
+
An unknown value raises `ValueError`.
|
|
262
268
|
|
|
263
269
|
**Returns:**
|
|
264
270
|
- `dict | list | Any`: A JSON-serializable structure.
|
|
265
271
|
|
|
272
|
+
#### Non-finite floats
|
|
273
|
+
|
|
274
|
+
`inf`, `-inf`, and `nan` are valid Python floats but have no representation in
|
|
275
|
+
the JSON spec. Left untouched they break `json.dumps(..., allow_nan=False)` and
|
|
276
|
+
produce the non-standard `Infinity`/`NaN` tokens that strict parsers reject. By
|
|
277
|
+
default **pyobjtojson** converts them to `null` so the output is always valid,
|
|
278
|
+
portable JSON:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from pyobjtojson import obj_to_json
|
|
282
|
+
|
|
283
|
+
data = {"ratio": float("inf"), "value": float("nan"), "ok": 1.5}
|
|
284
|
+
|
|
285
|
+
obj_to_json(data) # {"ratio": None, "value": None, "ok": 1.5}
|
|
286
|
+
obj_to_json(data, non_finite="string") # {"ratio": "Infinity", "value": "NaN", "ok": 1.5}
|
|
287
|
+
```
|
|
288
|
+
|
|
266
289
|
## Type Hints
|
|
267
290
|
|
|
268
291
|
**pyobjtojson** is fully typed and passes strict mypy checking. This provides:
|
|
@@ -226,7 +226,7 @@ obj_to_json(data)
|
|
|
226
226
|
|
|
227
227
|
## API Reference
|
|
228
228
|
|
|
229
|
-
### `obj_to_json(obj, check_circular=True, decimal_as_float=True)`
|
|
229
|
+
### `obj_to_json(obj, check_circular=True, decimal_as_float=True, non_finite="null")`
|
|
230
230
|
|
|
231
231
|
Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializable.
|
|
232
232
|
|
|
@@ -234,10 +234,33 @@ Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializ
|
|
|
234
234
|
- `obj` (Any): The object to serialize to JSON-like structures.
|
|
235
235
|
- `check_circular` (bool, optional): If True (default), detect and mark circular references as `"<circular reference>"`.
|
|
236
236
|
- `decimal_as_float` (bool, optional): If True (default), convert `Decimal` to `float`. If False, convert to string for high precision.
|
|
237
|
+
- `non_finite` (str, optional): How to represent non-finite floats (`inf`, `-inf`, `nan`), which have no JSON literal. One of:
|
|
238
|
+
- `"null"` (default): convert to `None`, matching JavaScript's `JSON.stringify`.
|
|
239
|
+
- `"string"`: convert to `"Infinity"`, `"-Infinity"`, or `"NaN"`.
|
|
240
|
+
- `"keep"`: leave the float as-is. Note this is **not** valid JSON and will raise with `json.dumps(..., allow_nan=False)`.
|
|
241
|
+
|
|
242
|
+
An unknown value raises `ValueError`.
|
|
237
243
|
|
|
238
244
|
**Returns:**
|
|
239
245
|
- `dict | list | Any`: A JSON-serializable structure.
|
|
240
246
|
|
|
247
|
+
#### Non-finite floats
|
|
248
|
+
|
|
249
|
+
`inf`, `-inf`, and `nan` are valid Python floats but have no representation in
|
|
250
|
+
the JSON spec. Left untouched they break `json.dumps(..., allow_nan=False)` and
|
|
251
|
+
produce the non-standard `Infinity`/`NaN` tokens that strict parsers reject. By
|
|
252
|
+
default **pyobjtojson** converts them to `null` so the output is always valid,
|
|
253
|
+
portable JSON:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pyobjtojson import obj_to_json
|
|
257
|
+
|
|
258
|
+
data = {"ratio": float("inf"), "value": float("nan"), "ok": 1.5}
|
|
259
|
+
|
|
260
|
+
obj_to_json(data) # {"ratio": None, "value": None, "ok": 1.5}
|
|
261
|
+
obj_to_json(data, non_finite="string") # {"ratio": "Infinity", "value": "NaN", "ok": 1.5}
|
|
262
|
+
```
|
|
263
|
+
|
|
241
264
|
## Type Hints
|
|
242
265
|
|
|
243
266
|
**pyobjtojson** is fully typed and passes strict mypy checking. This provides:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import dataclasses
|
|
5
|
+
import math
|
|
5
6
|
from collections.abc import Mapping, Sequence
|
|
6
7
|
from datetime import datetime, date, time
|
|
7
8
|
from decimal import Decimal
|
|
@@ -11,12 +12,42 @@ from typing import Any
|
|
|
11
12
|
from uuid import UUID
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
# Allowed policies for representing non-finite floats (inf/-inf/nan).
|
|
16
|
+
_NON_FINITE_MODES = ("null", "string", "keep")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _non_finite_repr(value: float, non_finite: str) -> Any:
|
|
20
|
+
"""
|
|
21
|
+
Represent a non-finite float (``inf``, ``-inf`` or ``nan``) according to
|
|
22
|
+
the chosen policy. JSON has no literal for these values, so leaving them
|
|
23
|
+
as-is yields output that either breaks ``json.dumps(..., allow_nan=False)``
|
|
24
|
+
or produces the non-standard ``Infinity``/``NaN`` tokens that strict
|
|
25
|
+
parsers reject.
|
|
26
|
+
|
|
27
|
+
- ``"null"``: return ``None`` (matches JavaScript's ``JSON.stringify``).
|
|
28
|
+
- ``"string"``: return ``"Infinity"``, ``"-Infinity"`` or ``"NaN"``.
|
|
29
|
+
- ``"keep"``: return the float unchanged (legacy behavior).
|
|
30
|
+
|
|
31
|
+
:param value: A non-finite float.
|
|
32
|
+
:param non_finite: One of the policies above.
|
|
33
|
+
:return: The chosen JSON-compatible representation.
|
|
34
|
+
"""
|
|
35
|
+
if non_finite == "null":
|
|
36
|
+
return None
|
|
37
|
+
if non_finite == "string":
|
|
38
|
+
if value != value: # NaN is the only value not equal to itself
|
|
39
|
+
return "NaN"
|
|
40
|
+
return "Infinity" if value > 0 else "-Infinity"
|
|
41
|
+
return value # "keep"
|
|
42
|
+
|
|
43
|
+
|
|
14
44
|
def _serialize_for_json(
|
|
15
45
|
obj: Any,
|
|
16
46
|
visited: set[int],
|
|
17
47
|
check_circular: bool = True,
|
|
18
48
|
_skip_circular_check: bool = False,
|
|
19
|
-
decimal_as_float: bool = True
|
|
49
|
+
decimal_as_float: bool = True,
|
|
50
|
+
non_finite: str = "null"
|
|
20
51
|
) -> Any:
|
|
21
52
|
"""
|
|
22
53
|
Internal recursion logic that can handle circular references
|
|
@@ -29,6 +60,8 @@ def _serialize_for_json(
|
|
|
29
60
|
:param _skip_circular_check: Internal flag to skip adding intermediate
|
|
30
61
|
conversion results to visited set.
|
|
31
62
|
:param decimal_as_float: If True, convert Decimal to float; otherwise to string.
|
|
63
|
+
:param non_finite: How to represent non-finite floats (inf/-inf/nan):
|
|
64
|
+
"null", "string", or "keep".
|
|
32
65
|
:return: A JSON-serializable structure, or a string if it cannot be
|
|
33
66
|
converted more structurally.
|
|
34
67
|
"""
|
|
@@ -40,6 +73,12 @@ def _serialize_for_json(
|
|
|
40
73
|
if isinstance(obj, Enum):
|
|
41
74
|
return obj.value
|
|
42
75
|
|
|
76
|
+
# Non-finite floats (inf, -inf, nan) have no JSON representation. Route
|
|
77
|
+
# them through the chosen policy before the primitive fast-path returns
|
|
78
|
+
# them verbatim. bool/int are always finite, so only float needs this.
|
|
79
|
+
if isinstance(obj, float) and not math.isfinite(obj):
|
|
80
|
+
return _non_finite_repr(obj, non_finite)
|
|
81
|
+
|
|
43
82
|
# If it's None, bool, int, float, or str, it's already JSON-serializable.
|
|
44
83
|
if obj is None or isinstance(obj, (bool, int, float, str)):
|
|
45
84
|
return obj
|
|
@@ -61,7 +100,12 @@ def _serialize_for_json(
|
|
|
61
100
|
# Decimal → float or string
|
|
62
101
|
if isinstance(obj, Decimal):
|
|
63
102
|
if decimal_as_float:
|
|
64
|
-
|
|
103
|
+
# A Decimal can also be non-finite (Decimal("inf")/Decimal("nan")),
|
|
104
|
+
# so apply the same policy once converted to float.
|
|
105
|
+
as_float = float(obj)
|
|
106
|
+
if not math.isfinite(as_float):
|
|
107
|
+
return _non_finite_repr(as_float, non_finite)
|
|
108
|
+
return as_float
|
|
65
109
|
return str(obj)
|
|
66
110
|
|
|
67
111
|
# bytes, bytearray → base64
|
|
@@ -86,9 +130,9 @@ def _serialize_for_json(
|
|
|
86
130
|
# If circular checking is enabled, see if this object is already on the
|
|
87
131
|
# current traversal path. We track whether *this* frame added the id so we
|
|
88
132
|
# can remove it again on the way out (see the `finally` below). Keeping
|
|
89
|
-
# `visited` as the active path
|
|
90
|
-
#
|
|
91
|
-
#
|
|
133
|
+
# `visited` as the active path (not every object ever seen) is what makes
|
|
134
|
+
# this real cycle detection: a shared sub-object referenced from two
|
|
135
|
+
# sibling branches (a DAG, not a cycle) must not be flagged circular.
|
|
92
136
|
added_to_visited = False
|
|
93
137
|
if check_circular is True and not _skip_circular_check:
|
|
94
138
|
if obj_id in visited:
|
|
@@ -106,12 +150,12 @@ def _serialize_for_json(
|
|
|
106
150
|
# reject the returned structure even though the values are fine.
|
|
107
151
|
json_key = _serialize_key(
|
|
108
152
|
key, visited, check_circular=check_circular,
|
|
109
|
-
decimal_as_float=decimal_as_float
|
|
153
|
+
decimal_as_float=decimal_as_float, non_finite=non_finite
|
|
110
154
|
)
|
|
111
155
|
try:
|
|
112
156
|
result_dict[json_key] = _serialize_for_json(
|
|
113
157
|
value, visited, check_circular=check_circular,
|
|
114
|
-
decimal_as_float=decimal_as_float
|
|
158
|
+
decimal_as_float=decimal_as_float, non_finite=non_finite
|
|
115
159
|
)
|
|
116
160
|
except Exception as exc:
|
|
117
161
|
result_dict[json_key] = f"<serialization error: {exc}>"
|
|
@@ -127,7 +171,8 @@ def _serialize_for_json(
|
|
|
127
171
|
obj=item,
|
|
128
172
|
visited=visited,
|
|
129
173
|
check_circular=check_circular,
|
|
130
|
-
decimal_as_float=decimal_as_float
|
|
174
|
+
decimal_as_float=decimal_as_float,
|
|
175
|
+
non_finite=non_finite
|
|
131
176
|
)
|
|
132
177
|
)
|
|
133
178
|
except Exception as exc:
|
|
@@ -146,7 +191,8 @@ def _serialize_for_json(
|
|
|
146
191
|
visited=visited,
|
|
147
192
|
check_circular=check_circular,
|
|
148
193
|
_skip_circular_check=True,
|
|
149
|
-
decimal_as_float=decimal_as_float
|
|
194
|
+
decimal_as_float=decimal_as_float,
|
|
195
|
+
non_finite=non_finite
|
|
150
196
|
)
|
|
151
197
|
except Exception:
|
|
152
198
|
# Fall through to next check if this fails
|
|
@@ -162,7 +208,8 @@ def _serialize_for_json(
|
|
|
162
208
|
visited=visited,
|
|
163
209
|
check_circular=check_circular,
|
|
164
210
|
_skip_circular_check=True,
|
|
165
|
-
decimal_as_float=decimal_as_float
|
|
211
|
+
decimal_as_float=decimal_as_float,
|
|
212
|
+
non_finite=non_finite
|
|
166
213
|
)
|
|
167
214
|
except Exception:
|
|
168
215
|
# Fall through to next check if this fails
|
|
@@ -180,7 +227,8 @@ def _serialize_for_json(
|
|
|
180
227
|
visited=visited,
|
|
181
228
|
check_circular=check_circular,
|
|
182
229
|
_skip_circular_check=True,
|
|
183
|
-
decimal_as_float=decimal_as_float
|
|
230
|
+
decimal_as_float=decimal_as_float,
|
|
231
|
+
non_finite=non_finite
|
|
184
232
|
)
|
|
185
233
|
except Exception:
|
|
186
234
|
# Fall through to next check if this fails
|
|
@@ -196,7 +244,8 @@ def _serialize_for_json(
|
|
|
196
244
|
visited=visited,
|
|
197
245
|
check_circular=check_circular,
|
|
198
246
|
_skip_circular_check=True,
|
|
199
|
-
decimal_as_float=decimal_as_float
|
|
247
|
+
decimal_as_float=decimal_as_float,
|
|
248
|
+
non_finite=non_finite
|
|
200
249
|
)
|
|
201
250
|
except Exception:
|
|
202
251
|
# Fall through to next check if this fails
|
|
@@ -211,7 +260,8 @@ def _serialize_for_json(
|
|
|
211
260
|
visited=visited,
|
|
212
261
|
check_circular=check_circular,
|
|
213
262
|
_skip_circular_check=True,
|
|
214
|
-
decimal_as_float=decimal_as_float
|
|
263
|
+
decimal_as_float=decimal_as_float,
|
|
264
|
+
non_finite=non_finite
|
|
215
265
|
)
|
|
216
266
|
except Exception:
|
|
217
267
|
# Fall through to next check if this fails
|
|
@@ -236,7 +286,8 @@ def _serialize_key(
|
|
|
236
286
|
key: Any,
|
|
237
287
|
visited: set[int],
|
|
238
288
|
check_circular: bool = True,
|
|
239
|
-
decimal_as_float: bool = True
|
|
289
|
+
decimal_as_float: bool = True,
|
|
290
|
+
non_finite: str = "null"
|
|
240
291
|
) -> Any:
|
|
241
292
|
"""
|
|
242
293
|
Convert a mapping key into something ``json.dumps`` accepts as an object
|
|
@@ -254,10 +305,14 @@ def _serialize_key(
|
|
|
254
305
|
:param visited: The traversal-path set shared with the value serializer.
|
|
255
306
|
:param check_circular: Whether cycle detection is enabled.
|
|
256
307
|
:param decimal_as_float: If True, convert Decimal to float; otherwise string.
|
|
308
|
+
:param non_finite: Policy for non-finite float keys (see _serialize_for_json).
|
|
257
309
|
:return: A ``json.dumps``-compatible key.
|
|
258
310
|
"""
|
|
259
311
|
# json.dumps natively accepts these as object keys, so leave them as-is to
|
|
260
|
-
# preserve its default behavior (e.g. int key 1 -> "1").
|
|
312
|
+
# preserve its default behavior (e.g. int key 1 -> "1"). A non-finite float
|
|
313
|
+
# key is the one exception: it must follow the non_finite policy.
|
|
314
|
+
if isinstance(key, float) and not math.isfinite(key):
|
|
315
|
+
return _non_finite_repr(key, non_finite)
|
|
261
316
|
if key is None or isinstance(key, (str, bool, int, float)):
|
|
262
317
|
return key
|
|
263
318
|
|
|
@@ -267,7 +322,8 @@ def _serialize_key(
|
|
|
267
322
|
obj=key,
|
|
268
323
|
visited=visited,
|
|
269
324
|
check_circular=check_circular,
|
|
270
|
-
decimal_as_float=decimal_as_float
|
|
325
|
+
decimal_as_float=decimal_as_float,
|
|
326
|
+
non_finite=non_finite
|
|
271
327
|
)
|
|
272
328
|
except Exception as exc:
|
|
273
329
|
return f"<unserializable key: {exc}>"
|
|
@@ -286,7 +342,8 @@ def _serialize_key(
|
|
|
286
342
|
def obj_to_json(
|
|
287
343
|
obj: Any,
|
|
288
344
|
check_circular: bool = True,
|
|
289
|
-
decimal_as_float: bool = True
|
|
345
|
+
decimal_as_float: bool = True,
|
|
346
|
+
non_finite: str = "null"
|
|
290
347
|
) -> Any:
|
|
291
348
|
"""
|
|
292
349
|
Public-facing function that starts with a fresh visited set
|
|
@@ -306,12 +363,22 @@ def obj_to_json(
|
|
|
306
363
|
:param check_circular: If True, detect and mark circular references.
|
|
307
364
|
:param decimal_as_float: If True, convert Decimal to float; otherwise to string.
|
|
308
365
|
Default is True.
|
|
366
|
+
:param non_finite: How to represent non-finite floats (inf/-inf/nan), which
|
|
367
|
+
have no JSON literal. One of:
|
|
368
|
+
- "null" (default): convert to None, matching JavaScript.
|
|
369
|
+
- "string": convert to "Infinity"/"-Infinity"/"NaN".
|
|
370
|
+
- "keep": leave the float as-is (not valid JSON).
|
|
309
371
|
:return: A JSON-serializable structure (dict, list, str, int, float, bool, None).
|
|
310
372
|
"""
|
|
373
|
+
if non_finite not in _NON_FINITE_MODES:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
f"non_finite must be one of {_NON_FINITE_MODES}, got {non_finite!r}"
|
|
376
|
+
)
|
|
311
377
|
visited: set[int] = set()
|
|
312
378
|
return _serialize_for_json(
|
|
313
379
|
obj=obj,
|
|
314
380
|
visited=visited,
|
|
315
381
|
check_circular=check_circular,
|
|
316
|
-
decimal_as_float=decimal_as_float
|
|
382
|
+
decimal_as_float=decimal_as_float,
|
|
383
|
+
non_finite=non_finite
|
|
317
384
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobjtojson
|
|
3
|
-
Version: 0.9
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: A Python library that simplifies serializing any Python object to JSON-friendly structures, gracefully handling circular references.
|
|
5
5
|
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -251,7 +251,7 @@ obj_to_json(data)
|
|
|
251
251
|
|
|
252
252
|
## API Reference
|
|
253
253
|
|
|
254
|
-
### `obj_to_json(obj, check_circular=True, decimal_as_float=True)`
|
|
254
|
+
### `obj_to_json(obj, check_circular=True, decimal_as_float=True, non_finite="null")`
|
|
255
255
|
|
|
256
256
|
Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializable.
|
|
257
257
|
|
|
@@ -259,10 +259,33 @@ Returns a cycle-free structure (nested dictionaries/lists) that is JSON-serializ
|
|
|
259
259
|
- `obj` (Any): The object to serialize to JSON-like structures.
|
|
260
260
|
- `check_circular` (bool, optional): If True (default), detect and mark circular references as `"<circular reference>"`.
|
|
261
261
|
- `decimal_as_float` (bool, optional): If True (default), convert `Decimal` to `float`. If False, convert to string for high precision.
|
|
262
|
+
- `non_finite` (str, optional): How to represent non-finite floats (`inf`, `-inf`, `nan`), which have no JSON literal. One of:
|
|
263
|
+
- `"null"` (default): convert to `None`, matching JavaScript's `JSON.stringify`.
|
|
264
|
+
- `"string"`: convert to `"Infinity"`, `"-Infinity"`, or `"NaN"`.
|
|
265
|
+
- `"keep"`: leave the float as-is. Note this is **not** valid JSON and will raise with `json.dumps(..., allow_nan=False)`.
|
|
266
|
+
|
|
267
|
+
An unknown value raises `ValueError`.
|
|
262
268
|
|
|
263
269
|
**Returns:**
|
|
264
270
|
- `dict | list | Any`: A JSON-serializable structure.
|
|
265
271
|
|
|
272
|
+
#### Non-finite floats
|
|
273
|
+
|
|
274
|
+
`inf`, `-inf`, and `nan` are valid Python floats but have no representation in
|
|
275
|
+
the JSON spec. Left untouched they break `json.dumps(..., allow_nan=False)` and
|
|
276
|
+
produce the non-standard `Infinity`/`NaN` tokens that strict parsers reject. By
|
|
277
|
+
default **pyobjtojson** converts them to `null` so the output is always valid,
|
|
278
|
+
portable JSON:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from pyobjtojson import obj_to_json
|
|
282
|
+
|
|
283
|
+
data = {"ratio": float("inf"), "value": float("nan"), "ok": 1.5}
|
|
284
|
+
|
|
285
|
+
obj_to_json(data) # {"ratio": None, "value": None, "ok": 1.5}
|
|
286
|
+
obj_to_json(data, non_finite="string") # {"ratio": "Infinity", "value": "NaN", "ok": 1.5}
|
|
287
|
+
```
|
|
288
|
+
|
|
266
289
|
## Type Hints
|
|
267
290
|
|
|
268
291
|
**pyobjtojson** is fully typed and passes strict mypy checking. This provides:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Tests for edge cases and error handling."""
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import pytest
|
|
5
6
|
from pyobjtojson import obj_to_json
|
|
6
7
|
|
|
@@ -238,3 +239,63 @@ class TestEdgeCases:
|
|
|
238
239
|
}
|
|
239
240
|
result = obj_to_json(obj)
|
|
240
241
|
assert result == obj
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestNonFiniteFloats:
|
|
245
|
+
"""Test the `non_finite` policy for inf/-inf/nan floats."""
|
|
246
|
+
|
|
247
|
+
def test_default_is_null(self):
|
|
248
|
+
"""By default non-finite floats become None, producing valid JSON."""
|
|
249
|
+
result = obj_to_json(
|
|
250
|
+
{"a": float("inf"), "b": float("-inf"), "c": float("nan")}
|
|
251
|
+
)
|
|
252
|
+
assert result == {"a": None, "b": None, "c": None}
|
|
253
|
+
# The whole point: strict json.dumps must not raise.
|
|
254
|
+
json.dumps(result, allow_nan=False)
|
|
255
|
+
|
|
256
|
+
def test_finite_floats_untouched(self):
|
|
257
|
+
"""Finite floats are never altered by the policy."""
|
|
258
|
+
obj = {"x": 1.5, "y": -3.0, "z": 0.0}
|
|
259
|
+
assert obj_to_json(obj) == obj
|
|
260
|
+
|
|
261
|
+
def test_string_mode(self):
|
|
262
|
+
"""String mode yields the standard-looking token strings."""
|
|
263
|
+
result = obj_to_json(
|
|
264
|
+
{"pos": float("inf"), "neg": float("-inf"), "nan": float("nan")},
|
|
265
|
+
non_finite="string",
|
|
266
|
+
)
|
|
267
|
+
assert result == {"pos": "Infinity", "neg": "-Infinity", "nan": "NaN"}
|
|
268
|
+
json.dumps(result, allow_nan=False)
|
|
269
|
+
|
|
270
|
+
def test_keep_mode_preserves_legacy_behavior(self):
|
|
271
|
+
"""Keep mode returns the floats unchanged (not strict-JSON valid)."""
|
|
272
|
+
result = obj_to_json({"a": float("inf")}, non_finite="keep")
|
|
273
|
+
assert result["a"] == float("inf")
|
|
274
|
+
assert isinstance(result["a"], float)
|
|
275
|
+
|
|
276
|
+
def test_non_finite_decimal(self):
|
|
277
|
+
"""Non-finite Decimal values follow the same policy once floated."""
|
|
278
|
+
from decimal import Decimal
|
|
279
|
+
|
|
280
|
+
assert obj_to_json(Decimal("Infinity")) is None
|
|
281
|
+
assert obj_to_json(Decimal("NaN"), non_finite="string") == "NaN"
|
|
282
|
+
# With decimal_as_float=False the Decimal is stringified anyway.
|
|
283
|
+
assert obj_to_json(Decimal("Infinity"), decimal_as_float=False) == "Infinity"
|
|
284
|
+
|
|
285
|
+
def test_non_finite_dict_key(self):
|
|
286
|
+
"""A non-finite float used as a dict key follows the policy too."""
|
|
287
|
+
assert obj_to_json(
|
|
288
|
+
{float("inf"): "v"}, non_finite="string"
|
|
289
|
+
) == {"Infinity": "v"}
|
|
290
|
+
result = obj_to_json({float("nan"): "v"}) # default null
|
|
291
|
+
assert result == {None: "v"}
|
|
292
|
+
json.dumps(result) # None key becomes "null", still valid JSON
|
|
293
|
+
|
|
294
|
+
def test_non_finite_in_list(self):
|
|
295
|
+
"""Non-finite values inside sequences are handled."""
|
|
296
|
+
assert obj_to_json([1.0, float("nan"), float("inf")]) == [1.0, None, None]
|
|
297
|
+
|
|
298
|
+
def test_invalid_mode_raises(self):
|
|
299
|
+
"""An unknown policy is a programming error and raises ValueError."""
|
|
300
|
+
with pytest.raises(ValueError, match="non_finite must be one of"):
|
|
301
|
+
obj_to_json({"a": 1}, non_finite="bogus")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|