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.
- streamlit_editjson-1.0.0/LICENSE +22 -0
- streamlit_editjson-1.0.0/MANIFEST.in +3 -0
- streamlit_editjson-1.0.0/PKG-INFO +73 -0
- streamlit_editjson-1.0.0/README.md +53 -0
- streamlit_editjson-1.0.0/setup.cfg +4 -0
- streamlit_editjson-1.0.0/setup.py +22 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/__init__.py +59 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/frontend/__init__.py +1 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/frontend/index.html +25 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/frontend/main.js +272 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/frontend/streamlit-component-lib.js +34 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson/frontend/style.css +72 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson.egg-info/PKG-INFO +73 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson.egg-info/SOURCES.txt +17 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson.egg-info/dependency_links.txt +1 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson.egg-info/requires.txt +2 -0
- streamlit_editjson-1.0.0/src/streamlit_editjson.egg-info/top_level.txt +1 -0
- streamlit_editjson-1.0.0/tests/test_editjson.py +79 -0
- streamlit_editjson-1.0.0/tests/test_frontend_js.py +231 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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()
|