streamlit-editjson 1.0.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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Thomas Beer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,3 @@
1
+ recursive-include src/streamlit_editjson/frontend *
2
+ include README.md
3
+ include LICENSE
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamlit-editjson
3
+ Version: 1.0.0
4
+ Summary: edit and view json
5
+ Author: Thomas Beer
6
+ Author-email: thomas.beer04@outlook.com
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: streamlit>=1.2
11
+ Requires-Dist: jinja2
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: license-file
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # streamlit-editjson
22
+
23
+ A Streamlit component to view and edit JSON with a user-friendly UI.
24
+
25
+ ## Installation
26
+
27
+ ```sh
28
+ pip install streamlit-editjson
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ import streamlit as st
35
+ from streamlit_editjson import editjson
36
+
37
+ value = editjson(
38
+ filepath="test.json",
39
+ key_editable=False, # default
40
+ value_editable=True, # default
41
+ )
42
+
43
+ st.write(value) # Python dict
44
+ ```
45
+
46
+ ## API
47
+
48
+ `editjson(filepath, key_editable=False, value_editable=True, key=None) -> dict`
49
+
50
+ - `filepath`: path to a JSON file.
51
+ - `key_editable`: allow editing JSON keys.
52
+ - `value_editable`: allow editing JSON values.
53
+ - `key`: optional Streamlit component key.
54
+
55
+ The component returns the edited JSON object as a Python `dict`.
56
+
57
+ ## Testing
58
+
59
+ ### Python unit tests
60
+
61
+ Run:
62
+
63
+ ```sh
64
+ python -m unittest discover -s tests -p "test_*.py"
65
+ ```
66
+
67
+ ### Running locally
68
+
69
+ Run:
70
+
71
+ ```sh
72
+ python -m streamlit run src/streamlit_editjson/__init__.py --server.port 8501
73
+ ```
@@ -0,0 +1,53 @@
1
+ # streamlit-editjson
2
+
3
+ A Streamlit component to view and edit JSON with a user-friendly UI.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pip install streamlit-editjson
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import streamlit as st
15
+ from streamlit_editjson import editjson
16
+
17
+ value = editjson(
18
+ filepath="test.json",
19
+ key_editable=False, # default
20
+ value_editable=True, # default
21
+ )
22
+
23
+ st.write(value) # Python dict
24
+ ```
25
+
26
+ ## API
27
+
28
+ `editjson(filepath, key_editable=False, value_editable=True, key=None) -> dict`
29
+
30
+ - `filepath`: path to a JSON file.
31
+ - `key_editable`: allow editing JSON keys.
32
+ - `value_editable`: allow editing JSON values.
33
+ - `key`: optional Streamlit component key.
34
+
35
+ The component returns the edited JSON object as a Python `dict`.
36
+
37
+ ## Testing
38
+
39
+ ### Python unit tests
40
+
41
+ Run:
42
+
43
+ ```sh
44
+ python -m unittest discover -s tests -p "test_*.py"
45
+ ```
46
+
47
+ ### Running locally
48
+
49
+ Run:
50
+
51
+ ```sh
52
+ python -m streamlit run src/streamlit_editjson/__init__.py --server.port 8501
53
+ ```
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ from pathlib import Path
2
+
3
+ import setuptools
4
+
5
+ this_directory = Path(__file__).parent
6
+ long_description = (this_directory / "README.md").read_text()
7
+
8
+ setuptools.setup(
9
+ name="streamlit-editjson",
10
+ version="1.0.0",
11
+ author="Thomas Beer",
12
+ author_email="thomas.beer04@outlook.com",
13
+ description="edit and view json",
14
+ long_description=long_description,
15
+ long_description_content_type="text/markdown",
16
+ packages=setuptools.find_packages(where="src"),
17
+ package_dir={"": "src"},
18
+ include_package_data=True,
19
+ classifiers=[],
20
+ python_requires=">=3.7",
21
+ install_requires=["streamlit>=1.2", "jinja2"],
22
+ )
@@ -0,0 +1,59 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Optional
4
+
5
+ import streamlit as st
6
+ import streamlit.components.v1 as components
7
+
8
+ # Tell streamlit that there is a component called streamlit_editjson,
9
+ # and that the code to display that component is in the "frontend" folder
10
+ frontend_dir = (Path(__file__).parent / "frontend").absolute()
11
+ _component_func = components.declare_component(
12
+ "streamlit_editjson", path=str(frontend_dir)
13
+ )
14
+
15
+
16
+ # Create the python function that will be called
17
+ def editjson(
18
+ filepath: str,
19
+ key_editable: Optional[bool] = False,
20
+ value_editable: Optional[bool] = True,
21
+ key: Optional[str] = None,
22
+ ) -> Dict[str, Any]:
23
+ """
24
+ Render a JSON editor component and return the edited JSON as a Python dict.
25
+
26
+ Args:
27
+ filepath: Path to a JSON file.
28
+ key_editable: Whether JSON keys can be edited. Default is False.
29
+ value_editable: Whether JSON values can be edited. Default is True.
30
+ key: Optional Streamlit component key.
31
+
32
+ Returns:
33
+ The edited JSON object as a Python dict.
34
+ """
35
+ with open(filepath, "r", encoding="utf-8") as f:
36
+ json_obj = json.load(f)
37
+
38
+ component_value = _component_func(
39
+ json_str=json.dumps(json_obj),
40
+ key_editable=key_editable,
41
+ value_editable=value_editable,
42
+ key=key,
43
+ default=json_obj,
44
+ )
45
+
46
+ if component_value is None:
47
+ return json_obj
48
+
49
+ return component_value
50
+
51
+
52
+ def main():
53
+ st.write("## Example")
54
+ value = editjson("test.json", key_editable=False, value_editable=True)
55
+ st.write(value)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1 @@
1
+ # Frontend package marker for setuptools package discovery.
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>streamlit-editjson</title>
9
+ <script src="./streamlit-component-lib.js"></script>
10
+ <script src="./main.js"></script>
11
+ <link rel="stylesheet" href="./style.css" />
12
+ </head>
13
+
14
+ <body>
15
+ <div id="root">
16
+ <div id="json-container">
17
+ <!--
18
+ <span data-color-code="key">key</span>: "<span data-color-code="value" contenteditable
19
+ spellcheck="false">myname</span>"
20
+ -->
21
+ </div>
22
+ </div>
23
+ </body>
24
+
25
+ </html>
@@ -0,0 +1,272 @@
1
+ // The `Streamlit` object exists because our html file includes
2
+ // `streamlit-component-lib.js`.
3
+
4
+ const INDENT_PX = 18;
5
+
6
+ const editorState = {
7
+ initialized: false,
8
+ sourceJson: null,
9
+ data: null,
10
+ keyEditable: false,
11
+ valueEditable: true,
12
+ };
13
+
14
+ function createLabel(text, className) {
15
+ const node = document.createElement("span");
16
+ node.className = className;
17
+ node.textContent = text;
18
+ return node;
19
+ }
20
+
21
+ function createTextInput(value, className, editable) {
22
+ const input = document.createElement("input");
23
+ input.className = className;
24
+ input.type = "text";
25
+ input.value = value;
26
+ input.disabled = !editable;
27
+ return input;
28
+ }
29
+
30
+ function createNumberInput(value, editable) {
31
+ const input = document.createElement("input");
32
+ input.className = "json-value json-value-number";
33
+ input.type = "number";
34
+ input.step = "any";
35
+ input.value = Number.isFinite(value) ? String(value) : "0";
36
+ input.disabled = !editable;
37
+ return input;
38
+ }
39
+
40
+ function createBooleanSelect(value, editable) {
41
+ const select = document.createElement("select");
42
+ select.className = "json-value json-value-boolean";
43
+ select.disabled = !editable;
44
+
45
+ const trueOption = document.createElement("option");
46
+ trueOption.value = "true";
47
+ trueOption.textContent = "true";
48
+
49
+ const falseOption = document.createElement("option");
50
+ falseOption.value = "false";
51
+ falseOption.textContent = "false";
52
+
53
+ select.appendChild(trueOption);
54
+ select.appendChild(falseOption);
55
+ select.value = value ? "true" : "false";
56
+ return select;
57
+ }
58
+
59
+ function parseLoosePrimitive(text) {
60
+ const trimmed = text.trim();
61
+
62
+ if (trimmed === "null") {
63
+ return null;
64
+ }
65
+ if (trimmed === "true") {
66
+ return true;
67
+ }
68
+ if (trimmed === "false") {
69
+ return false;
70
+ }
71
+
72
+ const asNumber = Number(trimmed);
73
+ if (trimmed !== "" && !Number.isNaN(asNumber)) {
74
+ return asNumber;
75
+ }
76
+
77
+ return text;
78
+ }
79
+
80
+ function emitChange() {
81
+ Streamlit.setComponentValue(editorState.data);
82
+ Streamlit.setFrameHeight(document.body.scrollHeight + 20);
83
+ }
84
+
85
+ function renderPrimitiveControl(parentRow, value, onChange) {
86
+ const editable = editorState.valueEditable;
87
+ const valueType = value === null ? "null" : typeof value;
88
+
89
+ if (valueType === "number") {
90
+ const input = createNumberInput(value, editable);
91
+ input.addEventListener("change", () => {
92
+ onChange(Number(input.value));
93
+ emitChange();
94
+ });
95
+ parentRow.appendChild(input);
96
+ return;
97
+ }
98
+
99
+ if (valueType === "boolean") {
100
+ const select = createBooleanSelect(value, editable);
101
+ select.addEventListener("change", () => {
102
+ onChange(select.value === "true");
103
+ emitChange();
104
+ });
105
+ parentRow.appendChild(select);
106
+ return;
107
+ }
108
+
109
+ const text = value === null ? "null" : String(value);
110
+ const input = createTextInput(text, "json-value json-value-text", editable);
111
+ input.addEventListener("change", () => {
112
+ if (value === null) {
113
+ onChange(parseLoosePrimitive(input.value));
114
+ } else if (typeof value === "string") {
115
+ onChange(input.value);
116
+ } else {
117
+ onChange(parseLoosePrimitive(input.value));
118
+ }
119
+ emitChange();
120
+ });
121
+ parentRow.appendChild(input);
122
+ }
123
+
124
+ function renderNode(container, value, indentLevel) {
125
+ if (Array.isArray(value)) {
126
+ renderArray(container, value, indentLevel);
127
+ return;
128
+ }
129
+
130
+ if (value !== null && typeof value === "object") {
131
+ renderObject(container, value, indentLevel);
132
+ return;
133
+ }
134
+
135
+ const row = document.createElement("div");
136
+ row.className = "json-row";
137
+ row.style.marginLeft = `${indentLevel * INDENT_PX}px`;
138
+ renderPrimitiveControl(row, value, (nextValue) => {
139
+ editorState.data = nextValue;
140
+ Streamlit.setComponentValue(editorState.data);
141
+ });
142
+ container.appendChild(row);
143
+ }
144
+
145
+ function renderObject(container, obj, indentLevel) {
146
+ const open = createLabel("{", "json-punct");
147
+ open.style.marginLeft = `${indentLevel * INDENT_PX}px`;
148
+ container.appendChild(open);
149
+
150
+ const entries = Object.entries(obj);
151
+
152
+ entries.forEach(([entryKey, entryValue]) => {
153
+ const row = document.createElement("div");
154
+ row.className = "json-row";
155
+ row.style.marginLeft = `${(indentLevel + 1) * INDENT_PX}px`;
156
+
157
+ let currentKey = entryKey;
158
+ const keyInput = createTextInput(
159
+ currentKey,
160
+ "json-key",
161
+ editorState.keyEditable,
162
+ );
163
+
164
+ keyInput.addEventListener("change", () => {
165
+ const nextKey = keyInput.value.trim();
166
+
167
+ if (!editorState.keyEditable) {
168
+ keyInput.value = currentKey;
169
+ return;
170
+ }
171
+ if (nextKey.length === 0 || nextKey === currentKey) {
172
+ keyInput.value = currentKey;
173
+ return;
174
+ }
175
+ if (Object.prototype.hasOwnProperty.call(obj, nextKey)) {
176
+ keyInput.value = currentKey;
177
+ return;
178
+ }
179
+
180
+ obj[nextKey] = obj[currentKey];
181
+ delete obj[currentKey];
182
+ currentKey = nextKey;
183
+
184
+ renderEditor();
185
+ emitChange();
186
+ });
187
+
188
+ row.appendChild(keyInput);
189
+ row.appendChild(createLabel(": ", "json-punct"));
190
+
191
+ if (entryValue !== null && typeof entryValue === "object") {
192
+ container.appendChild(row);
193
+ renderNode(container, entryValue, indentLevel + 1);
194
+ } else {
195
+ renderPrimitiveControl(row, entryValue, (nextValue) => {
196
+ obj[currentKey] = nextValue;
197
+ });
198
+ container.appendChild(row);
199
+ }
200
+ });
201
+
202
+ const close = createLabel("}", "json-punct");
203
+ close.style.marginLeft = `${indentLevel * INDENT_PX}px`;
204
+ container.appendChild(close);
205
+ }
206
+
207
+ function renderArray(container, arr, indentLevel) {
208
+ const open = createLabel("[", "json-punct");
209
+ open.style.marginLeft = `${indentLevel * INDENT_PX}px`;
210
+ container.appendChild(open);
211
+
212
+ arr.forEach((item, index) => {
213
+ const row = document.createElement("div");
214
+ row.className = "json-row";
215
+ row.style.marginLeft = `${(indentLevel + 1) * INDENT_PX}px`;
216
+ row.appendChild(createLabel(`${index}: `, "json-index"));
217
+
218
+ if (item !== null && typeof item === "object") {
219
+ container.appendChild(row);
220
+ renderNode(container, item, indentLevel + 1);
221
+ } else {
222
+ renderPrimitiveControl(row, item, (nextValue) => {
223
+ arr[index] = nextValue;
224
+ });
225
+ container.appendChild(row);
226
+ }
227
+ });
228
+
229
+ const close = createLabel("]", "json-punct");
230
+ close.style.marginLeft = `${indentLevel * INDENT_PX}px`;
231
+ container.appendChild(close);
232
+ }
233
+
234
+ function renderEditor() {
235
+ const jsonContainer = document.getElementById("json-container");
236
+ jsonContainer.innerHTML = "";
237
+ renderNode(jsonContainer, editorState.data, 0);
238
+ Streamlit.setFrameHeight(document.body.scrollHeight + 20);
239
+ }
240
+
241
+ /**
242
+ * Called whenever the component receives data from Streamlit.
243
+ */
244
+ function onRender(event) {
245
+ const {
246
+ json_str,
247
+ key_editable = false,
248
+ value_editable = true,
249
+ } = event.detail.args;
250
+
251
+ const shouldReset =
252
+ !editorState.initialized || editorState.sourceJson !== json_str;
253
+
254
+ if (shouldReset) {
255
+ editorState.data = JSON.parse(json_str);
256
+ editorState.sourceJson = json_str;
257
+ editorState.initialized = true;
258
+ }
259
+
260
+ editorState.keyEditable = Boolean(key_editable);
261
+ editorState.valueEditable = Boolean(value_editable);
262
+
263
+ renderEditor();
264
+
265
+ if (shouldReset) {
266
+ Streamlit.setComponentValue(editorState.data);
267
+ }
268
+ }
269
+
270
+ Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, onRender);
271
+ Streamlit.setComponentReady();
272
+ Streamlit.setFrameHeight(120);
@@ -0,0 +1,34 @@
1
+
2
+ // Borrowed minimalistic Streamlit API from Thiago
3
+ // https://discuss.streamlit.io/t/code-snippet-create-components-without-any-frontend-tooling-no-react-babel-webpack-etc/13064
4
+ function sendMessageToStreamlitClient(type, data) {
5
+ console.log(type, data)
6
+ const outData = Object.assign({
7
+ isStreamlitMessage: true,
8
+ type: type,
9
+ }, data);
10
+ window.parent.postMessage(outData, "*");
11
+ }
12
+
13
+ const Streamlit = {
14
+ setComponentReady: function() {
15
+ sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1});
16
+ },
17
+ setFrameHeight: function(height) {
18
+ sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height});
19
+ },
20
+ setComponentValue: function(value) {
21
+ sendMessageToStreamlitClient("streamlit:setComponentValue", {value: value});
22
+ },
23
+ RENDER_EVENT: "streamlit:render",
24
+ events: {
25
+ addEventListener: function(type, callback) {
26
+ window.addEventListener("message", function(event) {
27
+ if (event.data.type === type) {
28
+ event.detail = event.data
29
+ callback(event);
30
+ }
31
+ });
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,72 @@
1
+ #root {
2
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
3
+ }
4
+
5
+ #json-container {
6
+ color: #d9e2ef;
7
+ background: #0f1724;
8
+ border: 1px solid #253247;
9
+ border-radius: 8px;
10
+ padding: 10px 12px;
11
+ line-height: 1.5;
12
+ }
13
+
14
+ .json-row {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 6px;
18
+ margin: 3px 0;
19
+ }
20
+
21
+ .json-punct {
22
+ color: #8aa2c1;
23
+ font-family: Consolas, "Courier New", monospace;
24
+ }
25
+
26
+ .json-index {
27
+ color: #8fbcff;
28
+ min-width: 28px;
29
+ font-family: Consolas, "Courier New", monospace;
30
+ }
31
+
32
+ .json-key {
33
+ min-width: 110px;
34
+ max-width: 280px;
35
+ background: #132034;
36
+ color: #6de89a;
37
+ border: 1px solid #314663;
38
+ border-radius: 4px;
39
+ padding: 2px 6px;
40
+ }
41
+
42
+ .json-value {
43
+ min-width: 100px;
44
+ background: #132034;
45
+ color: #ffd27d;
46
+ border: 1px solid #314663;
47
+ border-radius: 4px;
48
+ padding: 2px 6px;
49
+ }
50
+
51
+ .json-key:disabled,
52
+ .json-value:disabled,
53
+ .json-value-boolean:disabled {
54
+ opacity: 0.7;
55
+ cursor: not-allowed;
56
+ }
57
+
58
+ .json-key:focus,
59
+ .json-value:focus,
60
+ .json-value-boolean:focus {
61
+ outline: 1px solid #7db3ff;
62
+ border-color: #7db3ff;
63
+ }
64
+
65
+ .json-value-boolean {
66
+ min-width: 95px;
67
+ background: #132034;
68
+ color: #ffd27d;
69
+ border: 1px solid #314663;
70
+ border-radius: 4px;
71
+ padding: 2px 6px;
72
+ }
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamlit-editjson
3
+ Version: 1.0.0
4
+ Summary: edit and view json
5
+ Author: Thomas Beer
6
+ Author-email: thomas.beer04@outlook.com
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: streamlit>=1.2
11
+ Requires-Dist: jinja2
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: license-file
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # streamlit-editjson
22
+
23
+ A Streamlit component to view and edit JSON with a user-friendly UI.
24
+
25
+ ## Installation
26
+
27
+ ```sh
28
+ pip install streamlit-editjson
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ import streamlit as st
35
+ from streamlit_editjson import editjson
36
+
37
+ value = editjson(
38
+ filepath="test.json",
39
+ key_editable=False, # default
40
+ value_editable=True, # default
41
+ )
42
+
43
+ st.write(value) # Python dict
44
+ ```
45
+
46
+ ## API
47
+
48
+ `editjson(filepath, key_editable=False, value_editable=True, key=None) -> dict`
49
+
50
+ - `filepath`: path to a JSON file.
51
+ - `key_editable`: allow editing JSON keys.
52
+ - `value_editable`: allow editing JSON values.
53
+ - `key`: optional Streamlit component key.
54
+
55
+ The component returns the edited JSON object as a Python `dict`.
56
+
57
+ ## Testing
58
+
59
+ ### Python unit tests
60
+
61
+ Run:
62
+
63
+ ```sh
64
+ python -m unittest discover -s tests -p "test_*.py"
65
+ ```
66
+
67
+ ### Running locally
68
+
69
+ Run:
70
+
71
+ ```sh
72
+ python -m streamlit run src/streamlit_editjson/__init__.py --server.port 8501
73
+ ```
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ setup.py
5
+ src/streamlit_editjson/__init__.py
6
+ src/streamlit_editjson.egg-info/PKG-INFO
7
+ src/streamlit_editjson.egg-info/SOURCES.txt
8
+ src/streamlit_editjson.egg-info/dependency_links.txt
9
+ src/streamlit_editjson.egg-info/requires.txt
10
+ src/streamlit_editjson.egg-info/top_level.txt
11
+ src/streamlit_editjson/frontend/__init__.py
12
+ src/streamlit_editjson/frontend/index.html
13
+ src/streamlit_editjson/frontend/main.js
14
+ src/streamlit_editjson/frontend/streamlit-component-lib.js
15
+ src/streamlit_editjson/frontend/style.css
16
+ tests/test_editjson.py
17
+ tests/test_frontend_js.py
@@ -0,0 +1,2 @@
1
+ streamlit>=1.2
2
+ jinja2
@@ -0,0 +1 @@
1
+ streamlit_editjson
@@ -0,0 +1,79 @@
1
+ import importlib
2
+ import json
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import Mock, patch
8
+
9
+ # Ensure local package import from src/
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ SRC_PATH = PROJECT_ROOT / "src"
12
+ if str(SRC_PATH) not in sys.path:
13
+ sys.path.insert(0, str(SRC_PATH))
14
+
15
+
16
+ class TestEditJson(unittest.TestCase):
17
+ @classmethod
18
+ def setUpClass(cls):
19
+ cls.streamlit_editjson = importlib.import_module("streamlit_editjson")
20
+
21
+ def _create_temp_json_file(self, data):
22
+ tmp_dir = tempfile.TemporaryDirectory()
23
+ self.addCleanup(tmp_dir.cleanup)
24
+
25
+ filepath = Path(tmp_dir.name) / "input.json"
26
+ filepath.write_text(json.dumps(data), encoding="utf-8")
27
+ return str(filepath)
28
+
29
+ def test_returns_original_json_when_component_returns_none(self):
30
+ original = {"name": "Alice", "age": 31}
31
+ filepath = self._create_temp_json_file(original)
32
+
33
+ mocked_component = Mock(return_value=None)
34
+
35
+ with patch.object(
36
+ self.streamlit_editjson, "_component_func", mocked_component
37
+ ):
38
+ result = self.streamlit_editjson.editjson(filepath)
39
+
40
+ self.assertEqual(result, original)
41
+
42
+ def test_returns_component_json_when_component_returns_value(self):
43
+ original = {"name": "Alice", "age": 31}
44
+ edited = {"name": "Bob", "age": 42}
45
+ filepath = self._create_temp_json_file(original)
46
+
47
+ mocked_component = Mock(return_value=edited)
48
+
49
+ with patch.object(
50
+ self.streamlit_editjson, "_component_func", mocked_component
51
+ ):
52
+ result = self.streamlit_editjson.editjson(
53
+ filepath, key_editable=True, value_editable=False, key="editor_1"
54
+ )
55
+
56
+ self.assertEqual(result, edited)
57
+
58
+ def test_passes_expected_arguments_to_component(self):
59
+ original = {"enabled": True, "count": 3}
60
+ filepath = self._create_temp_json_file(original)
61
+
62
+ mocked_component = Mock(return_value=None)
63
+
64
+ with patch.object(
65
+ self.streamlit_editjson, "_component_func", mocked_component
66
+ ):
67
+ self.streamlit_editjson.editjson(filepath)
68
+
69
+ _, kwargs = mocked_component.call_args
70
+
71
+ self.assertEqual(kwargs["json_str"], json.dumps(original))
72
+ self.assertEqual(kwargs["key_editable"], False)
73
+ self.assertEqual(kwargs["value_editable"], True)
74
+ self.assertIsNone(kwargs["key"])
75
+ self.assertEqual(kwargs["default"], original)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ unittest.main()
@@ -0,0 +1,231 @@
1
+ import json
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+ from py_mini_racer import py_mini_racer
6
+
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
9
+ MAIN_JS_PATH = PROJECT_ROOT / "src" / "streamlit_editjson" / "frontend" / "main.js"
10
+
11
+
12
+ class TestFrontendJS(unittest.TestCase):
13
+ @classmethod
14
+ def setUpClass(cls):
15
+ cls.main_js = MAIN_JS_PATH.read_text(encoding="utf-8")
16
+
17
+ def _ctx(self):
18
+ ctx = py_mini_racer.MiniRacer()
19
+ ctx.eval(
20
+ """
21
+ var window = {};
22
+ var document = {
23
+ body: { scrollHeight: 120 },
24
+ getElementById: function () {
25
+ return {
26
+ innerHTML: "",
27
+ appendChild: function () {}
28
+ };
29
+ },
30
+ createElement: function (tag) {
31
+ return {
32
+ tagName: tag,
33
+ className: "",
34
+ type: "",
35
+ step: "",
36
+ value: "",
37
+ disabled: false,
38
+ textContent: "",
39
+ style: {},
40
+ children: [],
41
+ appendChild: function (child) { this.children.push(child); },
42
+ addEventListener: function (_name, cb) { this._listener = cb; }
43
+ };
44
+ }
45
+ };
46
+
47
+ var Streamlit = {
48
+ setComponentValue: function (_value) {},
49
+ setFrameHeight: function (_height) {},
50
+ setComponentReady: function () {},
51
+ RENDER_EVENT: "render",
52
+ events: {
53
+ addEventListener: function (_event, _cb) {}
54
+ }
55
+ };
56
+ """
57
+ )
58
+ ctx.eval(self.main_js)
59
+ return ctx
60
+
61
+ def test_parse_loose_primitive_all_primitives(self):
62
+ ctx = self._ctx()
63
+
64
+ self.assertIsNone(ctx.eval('parseLoosePrimitive(" null ")'))
65
+ self.assertTrue(ctx.eval('parseLoosePrimitive("true")'))
66
+ self.assertFalse(ctx.eval('parseLoosePrimitive("false")'))
67
+ self.assertEqual(ctx.eval('parseLoosePrimitive("42")'), 42)
68
+ self.assertEqual(ctx.eval('parseLoosePrimitive("3.5")'), 3.5)
69
+ self.assertEqual(ctx.eval('parseLoosePrimitive("hello")'), "hello")
70
+ self.assertEqual(ctx.eval('parseLoosePrimitive(" ")'), " ")
71
+
72
+ def test_create_boolean_select_shape(self):
73
+ ctx = self._ctx()
74
+
75
+ payload_true = json.loads(
76
+ ctx.eval(
77
+ """
78
+ JSON.stringify((function () {
79
+ var s = createBooleanSelect(true, false);
80
+ return {
81
+ className: s.className,
82
+ disabled: s.disabled,
83
+ value: s.value,
84
+ childCount: s.children.length,
85
+ optionValues: s.children.map(function (x) { return x.value; })
86
+ };
87
+ })())
88
+ """
89
+ )
90
+ )
91
+
92
+ self.assertEqual(payload_true["className"], "json-value json-value-boolean")
93
+ self.assertTrue(payload_true["disabled"])
94
+ self.assertEqual(payload_true["value"], "true")
95
+ self.assertEqual(payload_true["childCount"], 2)
96
+ self.assertEqual(payload_true["optionValues"], ["true", "false"])
97
+
98
+ payload_false = json.loads(
99
+ ctx.eval(
100
+ """
101
+ JSON.stringify((function () {
102
+ var s = createBooleanSelect(false, true);
103
+ return {
104
+ disabled: s.disabled,
105
+ value: s.value
106
+ };
107
+ })())
108
+ """
109
+ )
110
+ )
111
+ self.assertFalse(payload_false["disabled"])
112
+ self.assertEqual(payload_false["value"], "false")
113
+
114
+ def test_render_primitive_control_dispatches_correct_creator(self):
115
+ ctx = self._ctx()
116
+
117
+ ctx.eval(
118
+ """
119
+ var __calls = [];
120
+ createNumberInput = function (value, editable) {
121
+ __calls.push(["number", value, editable]);
122
+ return { addEventListener: function () {} };
123
+ };
124
+ createBooleanSelect = function (value, editable) {
125
+ __calls.push(["boolean", value, editable]);
126
+ return { addEventListener: function () {} };
127
+ };
128
+ createTextInput = function (value, className, editable) {
129
+ __calls.push(["text", value, className, editable]);
130
+ return { addEventListener: function () {} };
131
+ };
132
+
133
+ editorState.valueEditable = true;
134
+ var __parent = { children: [], appendChild: function (x) { this.children.push(x); } };
135
+
136
+ renderPrimitiveControl(__parent, 7, function () {});
137
+ renderPrimitiveControl(__parent, true, function () {});
138
+ renderPrimitiveControl(__parent, "abc", function () {});
139
+ renderPrimitiveControl(__parent, null, function () {});
140
+ """
141
+ )
142
+
143
+ calls = json.loads(ctx.eval("JSON.stringify(__calls)"))
144
+
145
+ self.assertEqual(calls[0], ["number", 7, True])
146
+ self.assertEqual(calls[1], ["boolean", True, True])
147
+ self.assertEqual(calls[2], ["text", "abc", "json-value json-value-text", True])
148
+ self.assertEqual(calls[3], ["text", "null", "json-value json-value-text", True])
149
+
150
+ def test_render_object_key_rename_updates_underlying_object(self):
151
+ ctx = self._ctx()
152
+
153
+ result = json.loads(
154
+ ctx.eval(
155
+ """
156
+ JSON.stringify((function () {
157
+ editorState.keyEditable = true;
158
+ var obj = { old_key: 123 };
159
+ editorState.data = obj;
160
+ var container = {
161
+ children: [],
162
+ appendChild: function (x) { this.children.push(x); }
163
+ };
164
+
165
+ renderObject(container, obj, 0);
166
+
167
+ var keyInput = container.children[1].children[0];
168
+ keyInput.value = "new_key";
169
+ keyInput._listener();
170
+
171
+ return {
172
+ hasOldKey: Object.prototype.hasOwnProperty.call(obj, "old_key"),
173
+ hasNewKey: Object.prototype.hasOwnProperty.call(obj, "new_key"),
174
+ newValue: obj.new_key
175
+ };
176
+ })())
177
+ """
178
+ )
179
+ )
180
+
181
+ self.assertFalse(result["hasOldKey"])
182
+ self.assertTrue(result["hasNewKey"])
183
+ self.assertEqual(result["newValue"], 123)
184
+
185
+ def test_render_object_key_rename_rejects_empty_and_collision(self):
186
+ ctx = self._ctx()
187
+
188
+ result = json.loads(
189
+ ctx.eval(
190
+ """
191
+ JSON.stringify((function () {
192
+ editorState.keyEditable = true;
193
+ var obj = { first: 1, second: 2 };
194
+ editorState.data = obj;
195
+ var container = {
196
+ children: [],
197
+ appendChild: function (x) { this.children.push(x); }
198
+ };
199
+
200
+ renderObject(container, obj, 0);
201
+ var firstKeyInput = container.children[1].children[0];
202
+
203
+ firstKeyInput.value = " ";
204
+ firstKeyInput._listener();
205
+
206
+ var keysAfterEmpty = Object.keys(obj).sort();
207
+
208
+ firstKeyInput.value = "second";
209
+ firstKeyInput._listener();
210
+
211
+ var keysAfterCollision = Object.keys(obj).sort();
212
+
213
+ return {
214
+ keysAfterEmpty: keysAfterEmpty,
215
+ keysAfterCollision: keysAfterCollision,
216
+ firstValue: obj.first,
217
+ secondValue: obj.second
218
+ };
219
+ })())
220
+ """
221
+ )
222
+ )
223
+
224
+ self.assertEqual(result["keysAfterEmpty"], ["first", "second"])
225
+ self.assertEqual(result["keysAfterCollision"], ["first", "second"])
226
+ self.assertEqual(result["firstValue"], 1)
227
+ self.assertEqual(result["secondValue"], 2)
228
+
229
+
230
+ if __name__ == "__main__":
231
+ unittest.main()