Offline copy, generated from danieltuzes.github.io/structile — some content there may have moved on since this was downloaded.

Python / Jupyter library

structile is a thin Python package that renders the same viewer wherever you're running — an inline Jupyter widget, a browser tab, a written HTML file, or a terminal summary — depending on which renderer is active. It has no rendering logic of its own: every renderer either drives an embedded-mode protocol or reuses the same standalone viewer page described in the standalone viewer guide.

Install

pip install structile

Add pip install "structile[numpy]" if you also want numpy scalar support and don't already have numpy. pip install "structile[convert]" (pulls in mini-racer, a prebuilt V8 engine — no Node.js needed) is only required for structile.convert()'s non-JSON/Python paths and for resolving an interpreter's parsed value directly from Python; everyday open()/diff() calls with an interpreter never need it, since those always run the interpreter client-side in the browser instead.

Basic use

import structile as st

st.open({"a": 1, "nested": {"b": 2, "tags": ["x", "y"]}})   # a literal value
st.open("results.json")                                     # or load a file by path

Call it as the last expression in a notebook cell (or wrap it in IPython.display.display(...)) to render it. It always returns a RenderHandle.

obj can be any mix of dict, list, tuple, set/frozenset, None, bool, int (any size — big integers are kept exact end-to-end, never rounded), float, str, and numpy scalars (if numpy is installed). A pathlib.Path, or a plain str that happens to name an existing file, is read from disk and parsed instead of shown as a literal value.

Anything else raises TypeError by default — pass default= to handle it, the same escape hatch json.dumps(..., default=...) uses:

st.open(my_data, default=str)   # anything unsupported -> str(value)

Renderers

open() renders through whichever renderer is active, matplotlib-backend style (matplotlib.use(...) <-> structile.use(...)):

Renderer What it does Auto-selected when
widget Inline, editable Jupyter widget. A notebook kernel with a rich frontend — Jupyter classic/lab, VS Code notebooks, Colab.
browser Writes one self-contained HTML file and opens it in a browser tab — the standalone viewer, with full editing/undo/search/Save. Outside a notebook, with a display available.
file Same HTML as browser, but just writes it and returns the path — never opens a tab. Headless (no display, SSH session, or webbrowser.open() fails).
none Renders nothing. CI or PYTEST_CURRENT_TEST is set.
text A compact terminal summary. Never auto-selected — opt in with renderer="text".
st.open(my_data, renderer="browser")   # force it, this call only
st.use("file")                         # module default, matplotlib.use()-style

out= writes the standalone HTML somewhere specific; auto_open=False stops the browser renderer from opening a tab:

st.open(my_data, out="snapshot.html")                    # write only, don't open
st.open(my_data, renderer="browser", auto_open=False)     # write, don't open

Every call returns a RenderHandle:

h = st.open(my_data)
h.path        # where the standalone HTML was last written, or None
h.value       # the displayed value — live (updates on Save) for `widget`
h.open()      # open a browser tab
h.to_html()   # the standalone HTML as a string
h.save(path)  # write that HTML to `path`

Command line

python -m structile data.json
python -m structile data.xml --interpreter path/to/interpreter.js
python -m structile data.json --renderer file --out snapshot.html

Same renderer resolution as calling open() from a script.

Editing and saving back to Python

The widget renderer is read-write: edit inline, then Save (or Ctrl+S) sends the edited content back to Python instead of downloading it.

h = st.open("results.json")             # loaded from a path: Save writes back to it
h = st.open(my_data, path="out.json")   # in-memory obj: Save writes to out.json
h = st.open(my_data)                    # in-memory, no path: Save only updates h.value

h.value holds the last-saved (parsed) value, updated on every save. Nothing syncs live per-keystroke — only an explicit Save commits.

Gotcha: editing never mutates the object you passed in — st.open(my_dict) normalizes it into a separate structure the widget owns. Read h.value after saving to get the edited result.

Custom formats via an interpreter

Pass interpreter= — a path to a .js file, raw JS source, or a list of candidates tried in order — to hand the viewer raw text instead of a Python value, parsed (and, on save, serialized back) client-side:

st.open("config.xml", interpreter="path/to/generic_xml.js")

st.register_interpreter(".xml", "path/to/generic_xml.js")
st.open("config.xml")   # auto-selected from the registry, no interpreter= needed

See Writing and distributing interpreters for the full contract and how to package one up so a colleague never has to think about it at all.

Comparing two files (structile.diff)

import structile as st

st.diff({"a": 1, "b": 2}, {"a": 1, "b": 3})

left/right each accept anything open()'s obj does. interpreter=/ format=/name= each accept a single value (applied to both sides) or a (left, right) tuple, so the two sides can even be different, mutually-incompatible schemas, each resolved through its own interpreter:

st.diff(
    "schema_a.xml", "schema_b.xml",
    interpreter=("generic_xml.js", "tagged_xml.js"),
    format="xml",
)

renderer=/out=/auto_open= work the same as open()'s. view= ("unified"/"split") matches the standalone viewer's own header buttons. renderer="widget" renders an inline two-sided diff widget in Jupyter; each side's source pane can be independently edited and saved — h.left_value/h.right_value read through to the live widget, updated on that side's own Save.

Converting between formats

import structile as st

st.convert("data.json", "python")   # -> Python-repr text
st.convert("data.py", "json")       # -> JSON text
st.convert("data.xml", "json", interpreter="generic_xml.js")

"json" <-> "python" needs no extra dependency; anything else needs the optional mini-racer package (pip install "structile[convert]") — a real embedded V8 engine, no Node.js/npm/system install needed.

Configuring the viewer

import structile as st

st.set_option("theme", "dark")       # module-level default, applies to every call after this
st.get_option("theme")               # read a module-level default back
st.reset_option("theme")             # unset it again

st.open(my_data, gap=8, theme="dark")   # per-call override

The same mechanism reaches display overrides for special values:

st.open(my_data, forNull="N/A")                       # None -> "N/A" everywhere
st.open(my_data, forEmpty="(blank)")                   # "" -> "(blank)"

Logging

import logging
logging.basicConfig(level=logging.DEBUG)

structile emits to logging.getLogger("structile") — DEBUG for internal decisions (which renderer/interpreter was picked and why), INFO for real actions (a file written, a browser tab opened), WARNING for recoverable hiccups. It never configures handlers/levels itself.

Current limitations