anyjsonviewer 0.1.0__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.
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # JS
13
+ node_modules/
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: anyjsonviewer
3
+ Version: 0.1.0
4
+ Summary: An anywidget wrapper for @textea/json-viewer
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: anywidget>=0.10.0
7
+ Requires-Dist: traitlets>=5.14.3
8
+ Description-Content-Type: text/markdown
9
+
10
+ # anyjsonviewer
11
+
12
+ An [anywidget](https://anywidget.dev) wrapper around
13
+ [`@textea/json-viewer`](https://github.com/TexteaInc/json-viewer), giving you an
14
+ interactive, collapsible JSON viewer inside Jupyter, JupyterLab, VS Code,
15
+ Marimo, or any other frontend that supports Jupyter widgets.
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ pip install anyjsonviewer
21
+ ```
22
+
23
+ Or with uv:
24
+
25
+ ```sh
26
+ uv add anyjsonviewer
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ from anyjsonviewer import JsonViewer
33
+
34
+ JsonViewer(
35
+ value={
36
+ "name": "anyjsonviewer",
37
+ "tags": ["json", "viewer", "anywidget"],
38
+ "nested": {"count": 3, "ok": True},
39
+ },
40
+ theme="dark",
41
+ )
42
+ ```
43
+
44
+ ### Traits
45
+
46
+ All props are synced traits, so you can update them from Python and the view
47
+ re-renders live:
48
+
49
+ | Trait | Type | Default | Description |
50
+ | --- | --- | --- | --- |
51
+ | `value` | any JSON-serializable | `None` | The data to display. |
52
+ | `root_name` | `str \| None` | `"root"` | Label for the root node. `None` hides it. |
53
+ | `theme` | `str` | `"light"` | `"light"`, `"dark"`, or `"auto"`. |
54
+ | `class_name` | `str \| None` | `None` | Custom CSS class on the root element. |
55
+ | `style` | `dict \| None` | `None` | Inline CSS styles for the root element. |
56
+ | `indent_width` | `int` | `3` | Indentation width for nested objects. |
57
+ | `default_inspect_depth` | `int` | `5` | Depth to auto-expand on first render. Set to `0` to fully collapse. |
58
+ | `max_display_length` | `int` | `30` | Hide items in arrays/objects past this count. |
59
+ | `group_arrays_after_length` | `int` | `100` | Group arrays into bracketed chunks after this length. |
60
+ | `collapse_strings_after_length` | `int` | `50` | Truncate long strings; click to expand. |
61
+ | `object_sort_keys` | `bool` | `False` | Sort object keys alphabetically. |
62
+ | `quotes_on_keys` | `bool` | `True` | Wrap keys in quotes. |
63
+ | `display_data_types` | `bool` | `True` | Show type annotations next to values. |
64
+ | `display_size` | `bool` | `True` | Show length of arrays/objects. |
65
+ | `display_comma` | `bool` | `True` | Show trailing commas. |
66
+ | `highlight_updates` | `bool` | `True` | Briefly highlight values when they change. |
67
+ | `enable_clipboard` | `bool` | `False` | Show copy-to-clipboard buttons. |
68
+ | `enable_add` | `bool` | `False` | Show "add" buttons on objects/arrays. |
69
+ | `enable_delete` | `bool` | `False` | Show "delete" buttons on items. |
70
+ | `editable` | `bool` | `False` | Allow inline editing of values. |
71
+
72
+ When `editable`, `enable_add`, or `enable_delete` are on, edits round-trip
73
+ through the `value` trait — change something in the UI and the new value
74
+ becomes visible from Python.
75
+
76
+ ```python
77
+ w = JsonViewer(value={"a": 1})
78
+ w.theme = "dark" # live update
79
+ w.value = {"a": 1, "b": 2}
80
+ ```
81
+
82
+ ## Development
83
+
84
+ The widget's JavaScript lives in `js/index.jsx` and is bundled with
85
+ [`esbuild`](https://esbuild.github.io/) into `src/anyjsonviewer/static/widget.js`.
86
+
87
+ ```sh
88
+ # Install Python deps
89
+ uv sync
90
+
91
+ # Install JS deps
92
+ npm install
93
+
94
+ # One-off build
95
+ npm run build
96
+
97
+ # Watch mode (pair with ANYWIDGET_HMR=1 in your notebook)
98
+ npm run dev
99
+ ```
100
+
101
+ Building the wheel automatically runs the JS build via a custom Hatch hook:
102
+
103
+ ```sh
104
+ uv build
105
+ ```
106
+
107
+ To skip the JS build step (e.g. in CI where you've already built the bundle),
108
+ set `ANYJSONVIEWER_SKIP_JS_BUILD=1`.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,103 @@
1
+ # anyjsonviewer
2
+
3
+ An [anywidget](https://anywidget.dev) wrapper around
4
+ [`@textea/json-viewer`](https://github.com/TexteaInc/json-viewer), giving you an
5
+ interactive, collapsible JSON viewer inside Jupyter, JupyterLab, VS Code,
6
+ Marimo, or any other frontend that supports Jupyter widgets.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ pip install anyjsonviewer
12
+ ```
13
+
14
+ Or with uv:
15
+
16
+ ```sh
17
+ uv add anyjsonviewer
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from anyjsonviewer import JsonViewer
24
+
25
+ JsonViewer(
26
+ value={
27
+ "name": "anyjsonviewer",
28
+ "tags": ["json", "viewer", "anywidget"],
29
+ "nested": {"count": 3, "ok": True},
30
+ },
31
+ theme="dark",
32
+ )
33
+ ```
34
+
35
+ ### Traits
36
+
37
+ All props are synced traits, so you can update them from Python and the view
38
+ re-renders live:
39
+
40
+ | Trait | Type | Default | Description |
41
+ | --- | --- | --- | --- |
42
+ | `value` | any JSON-serializable | `None` | The data to display. |
43
+ | `root_name` | `str \| None` | `"root"` | Label for the root node. `None` hides it. |
44
+ | `theme` | `str` | `"light"` | `"light"`, `"dark"`, or `"auto"`. |
45
+ | `class_name` | `str \| None` | `None` | Custom CSS class on the root element. |
46
+ | `style` | `dict \| None` | `None` | Inline CSS styles for the root element. |
47
+ | `indent_width` | `int` | `3` | Indentation width for nested objects. |
48
+ | `default_inspect_depth` | `int` | `5` | Depth to auto-expand on first render. Set to `0` to fully collapse. |
49
+ | `max_display_length` | `int` | `30` | Hide items in arrays/objects past this count. |
50
+ | `group_arrays_after_length` | `int` | `100` | Group arrays into bracketed chunks after this length. |
51
+ | `collapse_strings_after_length` | `int` | `50` | Truncate long strings; click to expand. |
52
+ | `object_sort_keys` | `bool` | `False` | Sort object keys alphabetically. |
53
+ | `quotes_on_keys` | `bool` | `True` | Wrap keys in quotes. |
54
+ | `display_data_types` | `bool` | `True` | Show type annotations next to values. |
55
+ | `display_size` | `bool` | `True` | Show length of arrays/objects. |
56
+ | `display_comma` | `bool` | `True` | Show trailing commas. |
57
+ | `highlight_updates` | `bool` | `True` | Briefly highlight values when they change. |
58
+ | `enable_clipboard` | `bool` | `False` | Show copy-to-clipboard buttons. |
59
+ | `enable_add` | `bool` | `False` | Show "add" buttons on objects/arrays. |
60
+ | `enable_delete` | `bool` | `False` | Show "delete" buttons on items. |
61
+ | `editable` | `bool` | `False` | Allow inline editing of values. |
62
+
63
+ When `editable`, `enable_add`, or `enable_delete` are on, edits round-trip
64
+ through the `value` trait — change something in the UI and the new value
65
+ becomes visible from Python.
66
+
67
+ ```python
68
+ w = JsonViewer(value={"a": 1})
69
+ w.theme = "dark" # live update
70
+ w.value = {"a": 1, "b": 2}
71
+ ```
72
+
73
+ ## Development
74
+
75
+ The widget's JavaScript lives in `js/index.jsx` and is bundled with
76
+ [`esbuild`](https://esbuild.github.io/) into `src/anyjsonviewer/static/widget.js`.
77
+
78
+ ```sh
79
+ # Install Python deps
80
+ uv sync
81
+
82
+ # Install JS deps
83
+ npm install
84
+
85
+ # One-off build
86
+ npm run build
87
+
88
+ # Watch mode (pair with ANYWIDGET_HMR=1 in your notebook)
89
+ npm run dev
90
+ ```
91
+
92
+ Building the wheel automatically runs the JS build via a custom Hatch hook:
93
+
94
+ ```sh
95
+ uv build
96
+ ```
97
+
98
+ To skip the JS build step (e.g. in CI where you've already built the bundle),
99
+ set `ANYJSONVIEWER_SKIP_JS_BUILD=1`.
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,33 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
7
+
8
+ ROOT = Path(__file__).parent
9
+ BUNDLE = ROOT / "src" / "anyjsonviewer" / "static" / "widget.js"
10
+
11
+
12
+ class JsBuildHook(BuildHookInterface):
13
+ PLUGIN_NAME = "anyjsonviewer-js"
14
+
15
+ def initialize(self, version, build_data):
16
+ if os.environ.get("ANYJSONVIEWER_SKIP_JS_BUILD"):
17
+ return
18
+ if shutil.which("npm") is None:
19
+ if BUNDLE.exists():
20
+ self.app.display_warning(
21
+ "npm not found; using existing bundled widget.js"
22
+ )
23
+ return
24
+ raise RuntimeError(
25
+ "npm is required to build the JS bundle and no prebuilt "
26
+ "widget.js was found. Install Node.js or set "
27
+ "ANYJSONVIEWER_SKIP_JS_BUILD=1 if you know what you're doing."
28
+ )
29
+ if not (ROOT / "node_modules").exists():
30
+ subprocess.run(["npm", "install"], cwd=ROOT, check=True)
31
+ subprocess.run(["npm", "run", "build"], cwd=ROOT, check=True)
32
+ if not BUNDLE.exists():
33
+ raise RuntimeError(f"JS build did not produce {BUNDLE}")
@@ -0,0 +1,196 @@
1
+ import * as React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { JsonViewer } from "@textea/json-viewer";
4
+
5
+ // Mirrors src/jsonview/__init__.py:_TAG. Tagged dicts coming from Python
6
+ // represent values that don't survive a JSON round-trip and need to be
7
+ // rehydrated into real JS objects so @textea/json-viewer's built-in type
8
+ // handlers (dateType, the Set renderer, etc.) can display them properly.
9
+ const TAG = "__jsonview_type__";
10
+
11
+ function encodeValue(node) {
12
+ if (node === null || node === undefined) return node;
13
+ if (node instanceof Date) {
14
+ return { [TAG]: "datetime", iso: node.toISOString() };
15
+ }
16
+ if (node instanceof Set) {
17
+ return { [TAG]: "set", items: [...node].map(encodeValue) };
18
+ }
19
+ if (node instanceof Uint8Array) {
20
+ let bin = "";
21
+ for (let i = 0; i < node.length; i++) bin += String.fromCharCode(node[i]);
22
+ return { [TAG]: "bytes", b64: btoa(bin) };
23
+ }
24
+ if (Array.isArray(node)) return node.map(encodeValue);
25
+ if (typeof node === "object") {
26
+ const out = {};
27
+ for (const [k, v] of Object.entries(node)) out[k] = encodeValue(v);
28
+ return out;
29
+ }
30
+ return node;
31
+ }
32
+
33
+ function decodeValue(node) {
34
+ if (node === null || typeof node !== "object") return node;
35
+ if (Array.isArray(node)) return node.map(decodeValue);
36
+ if (typeof node[TAG] === "string") {
37
+ switch (node[TAG]) {
38
+ case "datetime":
39
+ case "date":
40
+ return new Date(node.iso);
41
+ case "set":
42
+ return new Set(node.items.map(decodeValue));
43
+ case "bytes": {
44
+ const bin = atob(node.b64);
45
+ const arr = new Uint8Array(bin.length);
46
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
47
+ return arr;
48
+ }
49
+ }
50
+ }
51
+ const out = {};
52
+ for (const [k, v] of Object.entries(node)) out[k] = decodeValue(v);
53
+ return out;
54
+ }
55
+
56
+ const PROPS = [
57
+ "value",
58
+ "root_name",
59
+ "theme",
60
+ "class_name",
61
+ "style",
62
+ "indent_width",
63
+ "default_inspect_depth",
64
+ "max_display_length",
65
+ "group_arrays_after_length",
66
+ "collapse_strings_after_length",
67
+ "object_sort_keys",
68
+ "quotes_on_keys",
69
+ "display_data_types",
70
+ "display_size",
71
+ "display_comma",
72
+ "highlight_updates",
73
+ "enable_clipboard",
74
+ "enable_add",
75
+ "enable_delete",
76
+ "editable",
77
+ ];
78
+
79
+ // Apply an edit/add/delete operation to a JSON value at the given path.
80
+ function applyChange(root, path, newValue, op) {
81
+ // Clone along the path so React/traitlets sees a new object identity.
82
+ if (path.length === 0) return newValue;
83
+ const cloneStep = (node) => (Array.isArray(node) ? [...node] : { ...node });
84
+ const newRoot = cloneStep(root);
85
+ let parent = newRoot;
86
+ for (let i = 0; i < path.length - 1; i++) {
87
+ const key = path[i];
88
+ parent[key] = cloneStep(parent[key]);
89
+ parent = parent[key];
90
+ }
91
+ const lastKey = path[path.length - 1];
92
+ if (op === "delete") {
93
+ if (Array.isArray(parent)) parent.splice(lastKey, 1);
94
+ else delete parent[lastKey];
95
+ } else {
96
+ parent[lastKey] = newValue;
97
+ }
98
+ return newRoot;
99
+ }
100
+
101
+ // Props that @textea/json-viewer bakes into its internal Zustand store on
102
+ // first mount and never re-reads (see createJsonViewerStore in
103
+ // node_modules/@textea/json-viewer/dist/index.mjs — the useMemo there has an
104
+ // empty deps array). Changing any of these requires remounting the component
105
+ // via a different React `key`. The remaining props (value, theme, class_name,
106
+ // style) are read directly from props on every render and update live.
107
+ const STORE_BAKED_PROPS = [
108
+ "root_name",
109
+ "indent_width",
110
+ "default_inspect_depth",
111
+ "max_display_length",
112
+ "group_arrays_after_length",
113
+ "collapse_strings_after_length",
114
+ "object_sort_keys",
115
+ "quotes_on_keys",
116
+ "display_data_types",
117
+ "display_size",
118
+ "display_comma",
119
+ "highlight_updates",
120
+ "enable_clipboard",
121
+ "enable_add",
122
+ "enable_delete",
123
+ "editable",
124
+ ];
125
+
126
+ function Widget({ model }) {
127
+ const [, force] = React.useReducer((x) => x + 1, 0);
128
+
129
+ React.useEffect(() => {
130
+ for (const p of PROPS) model.on(`change:${p}`, force);
131
+ return () => {
132
+ for (const p of PROPS) model.off(`change:${p}`, force);
133
+ };
134
+ }, [model]);
135
+
136
+ const remountKey = STORE_BAKED_PROPS.map((p) => JSON.stringify(model.get(p))).join("|");
137
+ const decodedValue = React.useMemo(() => decodeValue(model.get("value")), [model.get("value")]);
138
+
139
+ const handleChange = React.useCallback(
140
+ (path, _oldVal, newVal) => {
141
+ const current = decodeValue(model.get("value"));
142
+ const next = applyChange(current, path, newVal, "set");
143
+ model.set("value", encodeValue(next));
144
+ model.save_changes();
145
+ },
146
+ [model],
147
+ );
148
+
149
+ const handleDelete = React.useCallback(
150
+ (path) => {
151
+ const current = decodeValue(model.get("value"));
152
+ const next = applyChange(current, path, undefined, "delete");
153
+ model.set("value", encodeValue(next));
154
+ model.save_changes();
155
+ },
156
+ [model],
157
+ );
158
+
159
+ return (
160
+ <JsonViewer
161
+ key={remountKey}
162
+ value={decodedValue}
163
+ rootName={model.get("root_name") ?? false}
164
+ theme={model.get("theme")}
165
+ className={model.get("class_name") ?? undefined}
166
+ style={model.get("style") ?? undefined}
167
+ indentWidth={model.get("indent_width")}
168
+ defaultInspectDepth={model.get("default_inspect_depth")}
169
+ maxDisplayLength={model.get("max_display_length")}
170
+ groupArraysAfterLength={model.get("group_arrays_after_length")}
171
+ collapseStringsAfterLength={model.get("collapse_strings_after_length")}
172
+ objectSortKeys={model.get("object_sort_keys")}
173
+ quotesOnKeys={model.get("quotes_on_keys")}
174
+ displayDataTypes={model.get("display_data_types")}
175
+ displaySize={model.get("display_size")}
176
+ displayComma={model.get("display_comma")}
177
+ highlightUpdates={model.get("highlight_updates")}
178
+ enableClipboard={model.get("enable_clipboard")}
179
+ enableAdd={model.get("enable_add")}
180
+ enableDelete={model.get("enable_delete")}
181
+ editable={model.get("editable")}
182
+ onChange={handleChange}
183
+ onDelete={handleDelete}
184
+ />
185
+ );
186
+ }
187
+
188
+ function render({ model, el }) {
189
+ const container = document.createElement("div");
190
+ el.appendChild(container);
191
+ const root = createRoot(container);
192
+ root.render(<Widget model={model} />);
193
+ return () => root.unmount();
194
+ }
195
+
196
+ export default { render };