Handle non-finite Decimals like floats in the encoder (fixes #149)#381
Handle non-finite Decimals like floats in the encoder (fixes #149)#381apoorvdarshan wants to merge 2 commits into
Conversation
Non-finite Decimal values (NaN, sNaN, Infinity) bypassed the allow_nan / ignore_nan handling that floats go through, so ignore_nan=True emitted a literal NaN instead of null and allow_nan=False silently produced invalid JSON instead of raising ValueError. Route non-finite Decimals through the same float NaN/Inf handling in both the pure-Python encoder and the C extension. Finite Decimals (including trailing-zero forms such as 0.00) are left untouched, so precision is preserved. Signaling NaN, which cannot be converted with float(), is mapped to a plain NaN. Fixes simplejson#149.
etrepum
left a comment
There was a problem hiding this comment.
It would probably be better to unconditionally convert to string and then check the first one or two characters to see if it’s a special case
Adopt the maintainer's suggested approach: unconditionally stringify the Decimal once and inspect the first (or first-two) characters to detect the non-finite case, rather than calling is_finite()/is_nan(). str(Decimal) carries a leading '-' at most, so a finite value's first significant char is always a digit while a non-finite value's is a letter (N/s for [s]NaN, I for Infinity). This drops the two interned attribute names (is_finite/is_nan), their GC traverse/clear wiring and the method-call helper from the C extension, leaving encoder_encode_decimal a self-contained str()+char-check. Behaviour is unchanged and identical across the C and pure-Python paths.
|
Good call — switched to that approach in 0dcb58c. Both the pure-Python encoder and the C extension now stringify the This also let me drop the two interned attr names ( |
| c = s[1] if s[0] == '-' else s[0] | ||
| if c <= '9': |
There was a problem hiding this comment.
| c = s[1] if s[0] == '-' else s[0] | |
| if c <= '9': | |
| c0 = s[:1] | |
| c = s[1:2] if c0 == '-' else c0 | |
| if '0' <= c <= '9': |
Might be worth being a little defensive here in case Decimal is subclassed in a strange way
| c = (Py_UCS4)0; | ||
| if (len > (negative ? 1 : 0)) | ||
| c = PyUnicode_READ_CHAR(str, negative ? 1 : 0); | ||
| if (c <= '9') { |
There was a problem hiding this comment.
| if (c <= '9') { | |
| if (IS_DIGIT(c)) { |
| @@ -1,3 +1,15 @@ | |||
| Version 4.1.2 (unreleased) | |||
There was a problem hiding this comment.
I update the changelog, you can remove this
| } | ||
|
|
||
| static PyObject * | ||
| encoder_encode_decimal(PyEncoderObject *s, PyObject *obj) |
There was a problem hiding this comment.
This breaks Python 2.7 compatibility, there should be a separate branch for that in here since PyObject_Str isn't going to return a unicode object.
_speedups.obj : error LNK2019: unresolved external symbol _PyUnicode_READ_CHAR referenced in function _encoder_encode_decimal
build\lib.win32-2.7\simplejson\_speedups.pyd : fatal error LNK1120: 1 unresolved externals
| if (c == 'I') { | ||
| floatobj = PyFloat_FromDouble(negative ? -Py_HUGE_VAL : Py_HUGE_VAL); | ||
| } | ||
| else { | ||
| floatobj = PyFloat_FromDouble(Py_NAN); | ||
| } | ||
| if (floatobj == NULL) | ||
| return NULL; | ||
| encoded = encoder_encode_float(s, floatobj); |
There was a problem hiding this comment.
these should just go through the same path that encoder_encode_float does without using a float, e.g. incref and return the correct state->JSON_* value
Summary
Fixes #149.
Non-finite
Decimalvalues were not handled consistently with thematching
float. Both the pure-Python encoder and the C extensionserialized a
Decimalby callingstr()on it directly, bypassing theallow_nan/ignore_nanhandling that floats go through. The resultwas invalid JSON:
Fix
Non-finite
Decimalvalues (NaN,sNaN,Infinity,-Infinity) arenow routed through the same float NaN/Inf handling (
floatstr/encoder_encode_float) as floats, so they followallow_nanandignore_nanidentically:allow_nan=TrueNaN/Infinity/-Infinityignore_nan=Truenullallow_nan=False(default)ValueErrorFinite
Decimalvalues are left completely untouched — they are stillstringified as-is, so precision and trailing zeros (
0.00,1.50,-0) are preserved. This is why the fix keys onis_finite()ratherthan the
is_normal()suggestion from the issue thread:is_normal()is
Falsefor0,0.00and subnormals, which would have routed themthrough
float()and dropped their trailing zeros.float(Decimal('sNaN'))raises, so signaling NaNs are mapped to a plainNaN before going through the float path.
C path
The bug reproduces on both the C extension and the pure-Python
encoder (Decimal encoding goes through whichever encoder is active), so
the fix is applied to both. The C side adds a small
encoder_encode_decimalhelper that mirrors the Python logic, usingcached interned
is_finite/is_nanattribute names in the modulestate (matching the existing
JSON_attr_*pattern) with properGC
traverse/clearwiring.Tests
Added regression tests to
simplejson/tests/test_decimal.pycoveringallow_nan=True/ignore_nan=True/allow_nan=Falsefor non-finiteDecimals as top-level values, container values and dict keys, plus a
test_decimal_finite_unaffectedguard confirming finite Decimals areunchanged. The new tests fail on
mainand pass with the fix.Verified locally with the
_cibw_runnerharness, which runs the fullsuite twice — once with the C speedups loaded and once via
NoExtensionTestSuite(pure-Python): 458 tests pass on both paths, andthe extension wiring check (
c_make_encoder is make_encoder) holds.The C extension builds clean under the project's
-Werror -Wdeclaration-after-statementCFLAGS.Also added a
CHANGES.txtentry (under a provisional4.1.2 (unreleased)heading — happy to adjust the version/date to whateveryou prefer).
Disclosure: prepared with AI assistance; reviewed and verified locally.