fix: decode tuple/frozenset payloads with non-finite floats#9365
Conversation
Python's `json.dumps` emits bare `NaN`/`Infinity`/`-Infinity` for
non-finite floats inside embedded tuple/frozenset payloads (e.g.
`text/plain+tuple:[NaN]`). The outer JSON still parses because those
tokens live inside a JSON string, but `formatTuplePayload` and
`formatFrozensetPayload` used strict `JSON.parse` and threw.
Swap both to `jsonParseWithSpecialChar` (same parser we already use
at the outer level) and render non-finite elements as `float('nan')`
/ `float('inf')` / `-float('inf')`, matching the scalar-key form.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Fixes frontend decoding of text/plain+tuple: and text/plain+frozenset: embedded JSON payloads that may contain bare NaN/Infinity tokens emitted by Python’s json.dumps, aligning behavior with the existing special-float parsing used elsewhere.
Changes:
- Update tuple/frozenset (and set) payload decoding to use
jsonParseWithSpecialChar, and render non-finite floats asfloat('nan')/float('inf')/-float('inf')in copy output. - Add frontend regression coverage for tuple/frozenset payloads containing bare
NaN/Infinity. - Add backend tests exercising tuple/frozenset encodings involving non-finite floats (outer JSON round-trip).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tests/_output/formatters/test_structures.py | Adds regression tests for tuple/frozenset structures involving non-finite floats. |
| frontend/src/components/editor/output/tests/json-output.test.ts | Adds a regression test ensuring copy output handles embedded bare NaN/Infinity tokens. |
| frontend/src/components/editor/output/JsonOutput.tsx | Switches tuple/frozenset/set payload parsing to jsonParseWithSpecialChar and formats non-finite numbers into Python float(...) literals. |
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/src/components/editor/output/JsonOutput.tsx">
<violation number="1" location="frontend/src/components/editor/output/JsonOutput.tsx:414">
P2: `jsonParseWithSpecialChar` silently returns `{}` on parse failure instead of throwing, but this function assumes the result is always an array and immediately accesses `.length` / `.map`. For a malformed payload, `{}.map` will throw a `TypeError` at render time. Add an `Array.isArray(items)` guard (falling back to the raw payload string), matching the defensive pattern already used by `formatSetPayload`.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Kernel as Python Kernel
participant UI as JsonOutput Component
participant Parser as jsonParseWithSpecialChar
participant Clipboard as User/Clipboard
Note over Kernel, UI: Data Serialization (marimo/_output/formatters)
Kernel->>Kernel: Format collection (tuple/frozenset)
Note right of Kernel: Python json.dumps emits<br/>bare NaN/Infinity inside strings
Kernel-->>UI: JSON response with "text/plain+tuple:[NaN, Infinity]"
Note over UI, Parser: Frontend Decoding Flow
UI->>UI: Detect KEY_ENCODED_PREFIX
rect rgb(240, 240, 240)
Note right of UI: NEW: Handling non-finite floats in collections
UI->>Parser: CHANGED: jsonParseWithSpecialChar(payload)
Note right of Parser: Parses non-standard tokens<br/>(NaN, Infinity, -Infinity)
Parser-->>UI: Array of JS numbers/values
UI->>UI: NEW: formatCollectionItems()
alt is non-finite number
UI->>UI: Map to "float('nan')" or "float('inf')"
else standard value
UI->>UI: JSON.stringify(x)
end
end
UI->>UI: Join items with Python syntax (e.g. (item,))
UI-->>Clipboard: Rendered string / Copy to clipboard
Note right of Clipboard: Result: "(float('nan'), float('inf'))"
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
- formatTuplePayload / formatFrozensetPayload / formatSetPayload now
Array.isArray-guard the jsonParseWithSpecialChar result. It returns
{} on parse failure (doesn't throw), so without the guard a malformed
payload would crash rendering/copy on .length/.map. Fall back to the
raw payload (matching formatSetPayload's existing pattern), and add
a frontend test covering the malformed path.
- The "outer JSON is strict" tests now decode with
parse_constant=reject to actually enforce strictness. Python's
json.loads accepts bare NaN/Infinity by default — that's too lenient
to test JS JSON.parse compatibility, which is the actual contract.
| if (Number.isNaN(x)) { | ||
| return "float('nan')"; | ||
| } | ||
| return x > 0 ? "float('inf')" : "-float('inf')"; |
There was a problem hiding this comment.
do you know if this affects pd.NaT, np.nan
Bundle ReportChanges will increase total bundle size by 25.12MB (100.0%) ⬆️
|
Python's
json.dumpsemits bareNaN/Infinity/-Infinityfor non-finite floats inside embedded tuple/frozenset payloads (e.g.text/plain+tuple:[NaN]). The outer JSON still parses because those tokens live inside a JSON string, butformatTuplePayloadandformatFrozensetPayloadused strictJSON.parseand threw.Swap both to
jsonParseWithSpecialChar(same parser we already use at the outer level) and render non-finite elements asfloat('nan')/float('inf')/-float('inf'), matching the scalar-key form.