structui 0.4.0__tar.gz → 0.5.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.
- {structui-0.4.0 → structui-0.5.0}/PKG-INFO +2 -1
- {structui-0.4.0 → structui-0.5.0}/README.md +1 -0
- {structui-0.4.0 → structui-0.5.0}/pyproject.toml +1 -1
- {structui-0.4.0 → structui-0.5.0}/src/structui/parser.py +26 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/schema.py +3 -3
- {structui-0.4.0 → structui-0.5.0}/src/structui/ui.py +144 -6
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/PKG-INFO +2 -1
- {structui-0.4.0 → structui-0.5.0}/tests/test_parser.py +26 -0
- {structui-0.4.0 → structui-0.5.0}/setup.cfg +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/__init__.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/app.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/cli.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/file_picker.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/state.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui/xml_parser.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/SOURCES.txt +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/dependency_links.txt +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/entry_points.txt +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/requires.txt +0 -0
- {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/top_level.txt +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_app.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_cli.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_coverage_boost.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_file_picker.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_final_gap.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_schema.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_state.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui_blur.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui_coverage.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui_extra.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui_extra2.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_ui_final.py +0 -0
- {structui-0.4.0 → structui-0.5.0}/tests/test_xml_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A format-agnostic, schema-driven, hierarchical configuration UI.
|
|
5
5
|
Author: structui contributors
|
|
6
6
|
License: MIT
|
|
@@ -36,6 +36,7 @@ The architecture is explicitly decoupled, making it readily extensible to strict
|
|
|
36
36
|
- **Pillar B: Hierarchical UI:** Dynamic tree-based rendering with full support for multidimensional containers, dynamic polymorphic list additions, and node mapping. Powered natively by NiceGUI.
|
|
37
37
|
- **Pillar C: Data Validity:** Enforces schema metadata strictly at the UI layer. Missing fields gracefully populate via defaults, required flags trigger locking, and nested typings are continuously evaluated.
|
|
38
38
|
- **Pillar D: Extensibility & Programmatic Control:** Decomposed core logic (App, Parser, State, Schema, UI) allowing external tools and wrappers (e.g. CLI, Agent Workflows) to invoke the editor or inject properties safely.
|
|
39
|
+
- **Hex/Decimal Toggling:** Automatically detects and preserves hex formatting (`0x...`) loaded from YAML configurations. Supports inline format toggling between hex and decimal, with validation logic to restrict inputs to valid hex formats and enforce platform/64-bit size limits.
|
|
39
40
|
|
|
40
41
|
## Installation
|
|
41
42
|
|
|
@@ -15,6 +15,7 @@ The architecture is explicitly decoupled, making it readily extensible to strict
|
|
|
15
15
|
- **Pillar B: Hierarchical UI:** Dynamic tree-based rendering with full support for multidimensional containers, dynamic polymorphic list additions, and node mapping. Powered natively by NiceGUI.
|
|
16
16
|
- **Pillar C: Data Validity:** Enforces schema metadata strictly at the UI layer. Missing fields gracefully populate via defaults, required flags trigger locking, and nested typings are continuously evaluated.
|
|
17
17
|
- **Pillar D: Extensibility & Programmatic Control:** Decomposed core logic (App, Parser, State, Schema, UI) allowing external tools and wrappers (e.g. CLI, Agent Workflows) to invoke the editor or inject properties safely.
|
|
18
|
+
- **Hex/Decimal Toggling:** Automatically detects and preserves hex formatting (`0x...`) loaded from YAML configurations. Supports inline format toggling between hex and decimal, with validation logic to restrict inputs to valid hex formats and enforce platform/64-bit size limits.
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -6,6 +6,32 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from typing import Dict, Any, Optional
|
|
7
7
|
from .xml_parser import load_xml, save_xml
|
|
8
8
|
|
|
9
|
+
class HexInt(int):
|
|
10
|
+
"""Subclass of int to preserve hex formatting in YAML and UI representation."""
|
|
11
|
+
def __str__(self) -> str:
|
|
12
|
+
if self < 0:
|
|
13
|
+
return f"0x{(self & 0xffffffffffffffff):x}"
|
|
14
|
+
return f"0x{self:x}"
|
|
15
|
+
|
|
16
|
+
def __repr__(self) -> str:
|
|
17
|
+
return self.__str__()
|
|
18
|
+
|
|
19
|
+
def custom_int_constructor(loader, node):
|
|
20
|
+
val_str = loader.construct_scalar(node)
|
|
21
|
+
val = loader.construct_yaml_int(node)
|
|
22
|
+
if '0x' in val_str or '0X' in val_str or '0x' in val_str.lower():
|
|
23
|
+
return HexInt(val)
|
|
24
|
+
return val
|
|
25
|
+
|
|
26
|
+
yaml.SafeLoader.add_constructor('tag:yaml.org,2002:int', custom_int_constructor)
|
|
27
|
+
yaml.Loader.add_constructor('tag:yaml.org,2002:int', custom_int_constructor)
|
|
28
|
+
|
|
29
|
+
def hex_int_representer(dumper, data):
|
|
30
|
+
return dumper.represent_scalar('tag:yaml.org,2002:int', str(data))
|
|
31
|
+
|
|
32
|
+
yaml.SafeDumper.add_representer(HexInt, hex_int_representer)
|
|
33
|
+
yaml.Dumper.add_representer(HexInt, hex_int_representer)
|
|
34
|
+
|
|
9
35
|
class DataParser(ABC):
|
|
10
36
|
"""Abstract base class for format-agnostic configuration parsing."""
|
|
11
37
|
|
|
@@ -37,9 +37,9 @@ class SchemaManager:
|
|
|
37
37
|
|
|
38
38
|
def get_default_val_for_type(self, type_str: Optional[str]) -> Any:
|
|
39
39
|
"""Returns a sensible default value based on the given schema type."""
|
|
40
|
-
if type_str
|
|
41
|
-
if type_str
|
|
42
|
-
if type_str
|
|
40
|
+
if type_str in ('boolean', 'bool'): return False
|
|
41
|
+
if type_str in ('number', 'integer', 'float'): return 0
|
|
42
|
+
if type_str in ('dict', 'container'): return {}
|
|
43
43
|
if type_str == 'list': return []
|
|
44
44
|
return ""
|
|
45
45
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
+
import re
|
|
3
4
|
from nicegui import app, ui
|
|
4
5
|
from typing import Dict, Any, List
|
|
5
6
|
from .state import AppState
|
|
6
7
|
from .schema import SchemaManager
|
|
7
8
|
from .file_picker import LocalFilePicker
|
|
9
|
+
from .parser import HexInt
|
|
8
10
|
|
|
9
11
|
class StructUI:
|
|
10
12
|
"""The central view abstraction for managing the hierarchical NiceGUI visualization."""
|
|
@@ -165,11 +167,11 @@ class StructUI:
|
|
|
165
167
|
meta_type = self.schema_manager.get_meta(str(k)).get('type')
|
|
166
168
|
if meta_type == 'list':
|
|
167
169
|
data_node[k] = []
|
|
168
|
-
elif meta_type
|
|
170
|
+
elif meta_type in ('dict', 'container'):
|
|
169
171
|
data_node[k] = {}
|
|
170
|
-
elif meta_type
|
|
172
|
+
elif meta_type in ('boolean', 'bool'):
|
|
171
173
|
data_node[k] = False
|
|
172
|
-
elif meta_type
|
|
174
|
+
elif meta_type in ('number', 'integer', 'float'):
|
|
173
175
|
data_node[k] = 0
|
|
174
176
|
else:
|
|
175
177
|
data_node[k] = self.schema_manager.get_default_val_for_type(meta_type)
|
|
@@ -292,7 +294,7 @@ class StructUI:
|
|
|
292
294
|
def make_on_change(prop_key=k, prop_type=p_type):
|
|
293
295
|
def handler(e):
|
|
294
296
|
val = getattr(e, 'value', getattr(getattr(e, 'sender', None), 'value', None))
|
|
295
|
-
if prop_type
|
|
297
|
+
if prop_type in ('number', 'integer', 'float') and val is not None and val != '':
|
|
296
298
|
try:
|
|
297
299
|
val_str = str(val).strip()
|
|
298
300
|
if '.' in val_str:
|
|
@@ -328,8 +330,144 @@ class StructUI:
|
|
|
328
330
|
|
|
329
331
|
inp = ui.input(label=label_text, value=str(v)).classes('flex-grow').on_value_change(make_on_change())
|
|
330
332
|
ui.button(icon='folder_open', on_click=pick_file).props('flat round size=sm').tooltip('Select File')
|
|
331
|
-
elif meta.get('type')
|
|
332
|
-
|
|
333
|
+
elif meta.get('type') in ('number', 'integer', 'float') or (isinstance(v, (int, float)) and type(v) is not bool):
|
|
334
|
+
# Determine if we should display as hex
|
|
335
|
+
path_suffix = path.replace('/', '_')
|
|
336
|
+
hex_attr = f"_is_hex_{k}_{path_suffix}"
|
|
337
|
+
is_hex = getattr(self, hex_attr, None)
|
|
338
|
+
if is_hex is None:
|
|
339
|
+
is_hex = isinstance(v, HexInt)
|
|
340
|
+
setattr(self, hex_attr, is_hex)
|
|
341
|
+
|
|
342
|
+
# Toggle handler
|
|
343
|
+
def make_hex_toggle(prop_key=k, attr=hex_attr):
|
|
344
|
+
def handler(e):
|
|
345
|
+
setattr(self, attr, bool(e.value))
|
|
346
|
+
self.refresh_tree_and_editor()
|
|
347
|
+
return handler
|
|
348
|
+
|
|
349
|
+
# Switch for hex toggle
|
|
350
|
+
ui.switch(text='Hex', value=is_hex).on_value_change(make_hex_toggle())
|
|
351
|
+
|
|
352
|
+
if not hasattr(self, 'validation_errors'):
|
|
353
|
+
self.validation_errors = set()
|
|
354
|
+
|
|
355
|
+
error_key = f"{path}/{k}"
|
|
356
|
+
|
|
357
|
+
def update_validation_state(err_k, is_valid_input):
|
|
358
|
+
if is_valid_input:
|
|
359
|
+
self.validation_errors.discard(err_k)
|
|
360
|
+
else:
|
|
361
|
+
self.validation_errors.add(err_k)
|
|
362
|
+
if self.save_btn:
|
|
363
|
+
if self.validation_errors:
|
|
364
|
+
self.save_btn.disable()
|
|
365
|
+
else:
|
|
366
|
+
self.save_btn.enable()
|
|
367
|
+
|
|
368
|
+
if is_hex:
|
|
369
|
+
# Render ui.input for hex mode
|
|
370
|
+
if v is None or v == '':
|
|
371
|
+
hex_val = ''
|
|
372
|
+
else:
|
|
373
|
+
try:
|
|
374
|
+
val_int = int(v)
|
|
375
|
+
if val_int < 0:
|
|
376
|
+
hex_val = f"0x{val_int & 0xffffffffffffffff:x}"
|
|
377
|
+
else:
|
|
378
|
+
hex_val = f"0x{val_int:x}"
|
|
379
|
+
except ValueError:
|
|
380
|
+
hex_val = str(v)
|
|
381
|
+
|
|
382
|
+
# Validation rule for Hex
|
|
383
|
+
def validate_hex(val_str):
|
|
384
|
+
if not val_str:
|
|
385
|
+
update_validation_state(error_key, True)
|
|
386
|
+
return True
|
|
387
|
+
val_str = val_str.strip()
|
|
388
|
+
if val_str.lower().startswith('0x'):
|
|
389
|
+
val_str = val_str[2:]
|
|
390
|
+
if not re.match(r'^[0-9a-fA-F]+$', val_str):
|
|
391
|
+
update_validation_state(error_key, False)
|
|
392
|
+
return 'Invalid hex string'
|
|
393
|
+
try:
|
|
394
|
+
parsed = int(val_str, 16)
|
|
395
|
+
if parsed > 0xffffffffffffffff:
|
|
396
|
+
update_validation_state(error_key, False)
|
|
397
|
+
return 'Exceeds 64-bit unsigned limit'
|
|
398
|
+
except ValueError:
|
|
399
|
+
update_validation_state(error_key, False)
|
|
400
|
+
return 'Invalid hex format'
|
|
401
|
+
update_validation_state(error_key, True)
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
# On change handler for hex input
|
|
405
|
+
def make_hex_change(prop_key=k):
|
|
406
|
+
def handler(e):
|
|
407
|
+
val_str = getattr(e, 'value', getattr(getattr(e, 'sender', None), 'value', None))
|
|
408
|
+
if val_str is None or val_str == '':
|
|
409
|
+
self.state.set_data_by_path(self.selected_path["value"], str(prop_key), HexInt(0))
|
|
410
|
+
self.state.commit()
|
|
411
|
+
self.update_save_btn_state()
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
val_str_clean = val_str.strip()
|
|
415
|
+
if val_str_clean.lower().startswith('0x'):
|
|
416
|
+
val_str_clean = val_str_clean[2:]
|
|
417
|
+
|
|
418
|
+
if not re.match(r'^[0-9a-fA-F]+$', val_str_clean):
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
parsed_val = int(val_str_clean, 16)
|
|
423
|
+
if parsed_val > 0xffffffffffffffff:
|
|
424
|
+
return
|
|
425
|
+
self.state.set_data_by_path(self.selected_path["value"], str(prop_key), HexInt(parsed_val))
|
|
426
|
+
self.state.commit()
|
|
427
|
+
self.update_save_btn_state()
|
|
428
|
+
except ValueError:
|
|
429
|
+
pass
|
|
430
|
+
return handler
|
|
431
|
+
|
|
432
|
+
inp = ui.input(label=label_text, value=hex_val, validation={'Invalid hex': validate_hex}).classes('flex-grow').on_value_change(make_hex_change())
|
|
433
|
+
else:
|
|
434
|
+
# Render ui.number for decimal mode
|
|
435
|
+
def make_num_change(prop_key=k):
|
|
436
|
+
def handler(e):
|
|
437
|
+
val = getattr(e, 'value', getattr(getattr(e, 'sender', None), 'value', None))
|
|
438
|
+
if val is not None and val != '':
|
|
439
|
+
try:
|
|
440
|
+
val_str = str(val).strip()
|
|
441
|
+
if '.' in val_str:
|
|
442
|
+
parsed_val = float(val_str)
|
|
443
|
+
else:
|
|
444
|
+
parsed_val = int(val_str)
|
|
445
|
+
|
|
446
|
+
if parsed_val < -9223372036854775808 or parsed_val > 18446744073709551615:
|
|
447
|
+
return
|
|
448
|
+
self.state.set_data_by_path(self.selected_path["value"], str(prop_key), parsed_val)
|
|
449
|
+
self.state.commit()
|
|
450
|
+
self.update_save_btn_state()
|
|
451
|
+
except ValueError:
|
|
452
|
+
pass
|
|
453
|
+
return handler
|
|
454
|
+
|
|
455
|
+
def validate_num(v_val):
|
|
456
|
+
if v_val is None or v_val == '':
|
|
457
|
+
update_validation_state(error_key, True)
|
|
458
|
+
return True
|
|
459
|
+
try:
|
|
460
|
+
parsed_val = float(v_val)
|
|
461
|
+
if parsed_val < -9223372036854775808 or parsed_val > 18446744073709551615:
|
|
462
|
+
update_validation_state(error_key, False)
|
|
463
|
+
return 'Exceeds platform size limits'
|
|
464
|
+
except ValueError:
|
|
465
|
+
update_validation_state(error_key, False)
|
|
466
|
+
return 'Invalid number'
|
|
467
|
+
update_validation_state(error_key, True)
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
inp = ui.number(label=label_text, value=v, validation={'Invalid': validate_num}).classes('flex-grow').on_value_change(make_num_change())
|
|
333
471
|
else:
|
|
334
472
|
inp = ui.input(label=label_text, value=str(v)).classes('flex-grow').on_value_change(make_on_change())
|
|
335
473
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A format-agnostic, schema-driven, hierarchical configuration UI.
|
|
5
5
|
Author: structui contributors
|
|
6
6
|
License: MIT
|
|
@@ -36,6 +36,7 @@ The architecture is explicitly decoupled, making it readily extensible to strict
|
|
|
36
36
|
- **Pillar B: Hierarchical UI:** Dynamic tree-based rendering with full support for multidimensional containers, dynamic polymorphic list additions, and node mapping. Powered natively by NiceGUI.
|
|
37
37
|
- **Pillar C: Data Validity:** Enforces schema metadata strictly at the UI layer. Missing fields gracefully populate via defaults, required flags trigger locking, and nested typings are continuously evaluated.
|
|
38
38
|
- **Pillar D: Extensibility & Programmatic Control:** Decomposed core logic (App, Parser, State, Schema, UI) allowing external tools and wrappers (e.g. CLI, Agent Workflows) to invoke the editor or inject properties safely.
|
|
39
|
+
- **Hex/Decimal Toggling:** Automatically detects and preserves hex formatting (`0x...`) loaded from YAML configurations. Supports inline format toggling between hex and decimal, with validation logic to restrict inputs to valid hex formats and enforce platform/64-bit size limits.
|
|
39
40
|
|
|
40
41
|
## Installation
|
|
41
42
|
|
|
@@ -91,3 +91,29 @@ def test_abstract_parser_coverage():
|
|
|
91
91
|
p = DummyParser()
|
|
92
92
|
assert p.load("file.txt") is None
|
|
93
93
|
assert p.save("file.txt", {}) is None
|
|
94
|
+
|
|
95
|
+
def test_hex_int_loading_and_saving(tmp_path):
|
|
96
|
+
from structui.parser import HexInt, YamlParser
|
|
97
|
+
parser = YamlParser()
|
|
98
|
+
test_file = tmp_path / "hex_test.yaml"
|
|
99
|
+
|
|
100
|
+
# Write a YAML with hex values
|
|
101
|
+
test_file.write_text("hex_val: 0x1A\nnormal_val: 26\nneg_hex_val: -0x10\n", encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
loaded = parser.load(str(test_file))
|
|
104
|
+
assert isinstance(loaded["hex_val"], HexInt)
|
|
105
|
+
assert loaded["hex_val"] == 26
|
|
106
|
+
assert isinstance(loaded["normal_val"], int)
|
|
107
|
+
assert not isinstance(loaded["normal_val"], HexInt)
|
|
108
|
+
assert loaded["normal_val"] == 26
|
|
109
|
+
assert isinstance(loaded["neg_hex_val"], HexInt)
|
|
110
|
+
assert loaded["neg_hex_val"] == -16
|
|
111
|
+
|
|
112
|
+
# Now save it back
|
|
113
|
+
out_file = tmp_path / "hex_out.yaml"
|
|
114
|
+
parser.save(str(out_file), loaded)
|
|
115
|
+
|
|
116
|
+
saved_content = out_file.read_text(encoding="utf-8")
|
|
117
|
+
assert "hex_val: 0x1a" in saved_content or "hex_val: 0x1A" in saved_content
|
|
118
|
+
assert "normal_val: 26" in saved_content
|
|
119
|
+
assert "neg_hex_val: 0xfffffffffffffff0" in saved_content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|