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.
Files changed (34) hide show
  1. {structui-0.4.0 → structui-0.5.0}/PKG-INFO +2 -1
  2. {structui-0.4.0 → structui-0.5.0}/README.md +1 -0
  3. {structui-0.4.0 → structui-0.5.0}/pyproject.toml +1 -1
  4. {structui-0.4.0 → structui-0.5.0}/src/structui/parser.py +26 -0
  5. {structui-0.4.0 → structui-0.5.0}/src/structui/schema.py +3 -3
  6. {structui-0.4.0 → structui-0.5.0}/src/structui/ui.py +144 -6
  7. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/PKG-INFO +2 -1
  8. {structui-0.4.0 → structui-0.5.0}/tests/test_parser.py +26 -0
  9. {structui-0.4.0 → structui-0.5.0}/setup.cfg +0 -0
  10. {structui-0.4.0 → structui-0.5.0}/src/structui/__init__.py +0 -0
  11. {structui-0.4.0 → structui-0.5.0}/src/structui/app.py +0 -0
  12. {structui-0.4.0 → structui-0.5.0}/src/structui/cli.py +0 -0
  13. {structui-0.4.0 → structui-0.5.0}/src/structui/file_picker.py +0 -0
  14. {structui-0.4.0 → structui-0.5.0}/src/structui/state.py +0 -0
  15. {structui-0.4.0 → structui-0.5.0}/src/structui/xml_parser.py +0 -0
  16. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/SOURCES.txt +0 -0
  17. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/dependency_links.txt +0 -0
  18. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/entry_points.txt +0 -0
  19. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/requires.txt +0 -0
  20. {structui-0.4.0 → structui-0.5.0}/src/structui.egg-info/top_level.txt +0 -0
  21. {structui-0.4.0 → structui-0.5.0}/tests/test_app.py +0 -0
  22. {structui-0.4.0 → structui-0.5.0}/tests/test_cli.py +0 -0
  23. {structui-0.4.0 → structui-0.5.0}/tests/test_coverage_boost.py +0 -0
  24. {structui-0.4.0 → structui-0.5.0}/tests/test_file_picker.py +0 -0
  25. {structui-0.4.0 → structui-0.5.0}/tests/test_final_gap.py +0 -0
  26. {structui-0.4.0 → structui-0.5.0}/tests/test_schema.py +0 -0
  27. {structui-0.4.0 → structui-0.5.0}/tests/test_state.py +0 -0
  28. {structui-0.4.0 → structui-0.5.0}/tests/test_ui.py +0 -0
  29. {structui-0.4.0 → structui-0.5.0}/tests/test_ui_blur.py +0 -0
  30. {structui-0.4.0 → structui-0.5.0}/tests/test_ui_coverage.py +0 -0
  31. {structui-0.4.0 → structui-0.5.0}/tests/test_ui_extra.py +0 -0
  32. {structui-0.4.0 → structui-0.5.0}/tests/test_ui_extra2.py +0 -0
  33. {structui-0.4.0 → structui-0.5.0}/tests/test_ui_final.py +0 -0
  34. {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.4.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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structui"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "A format-agnostic, schema-driven, hierarchical configuration UI."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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 == 'boolean': return False
41
- if type_str == 'number': return 0
42
- if type_str == 'dict': return {}
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 == 'dict':
170
+ elif meta_type in ('dict', 'container'):
169
171
  data_node[k] = {}
170
- elif meta_type == 'boolean':
172
+ elif meta_type in ('boolean', 'bool'):
171
173
  data_node[k] = False
172
- elif meta_type == 'number':
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 == 'number' and val is not None and val != '':
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') == 'number' or (isinstance(v, (int, float)) and type(v) is not bool):
332
- inp = ui.input(label=label_text, value=str(v)).classes('flex-grow').on_value_change(make_on_change())
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.4.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