struct2ui 0.1.0__py3-none-any.whl

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,204 @@
1
+ """C source -> cfg_t-style schema dict (the reverse of the JSON->UI flow).
2
+
3
+ Pure data, no Qt. Single responsibility: scan a .c/.h file with simple regex
4
+ lexing and produce a dict shaped exactly like the existing cfg_t/*.json files
5
+ (see cfg_t/a_cfg_t.json for the target shape).
6
+
7
+ Supported constructs (deliberately limited; the project restricts macro use):
8
+ - simple `#define NAME VALUE` -> top-level int/float const
9
+ - `typedef enum { A=0, B, } name_t;` -> {"type":"enum","items":{...}}
10
+ - `typedef struct { ... } name_t;` -> {"type":"struct","items":[...]}
11
+
12
+ Inside a struct, each member line `type name;`, `type name[N];` or
13
+ `type name[N][M];` becomes a field spec. char[N] keeps `count` so the schema
14
+ layer recognises it as a string; other arrays keep `count` (or a `count` list
15
+ for multi-dimensional arrays, matching how shapes flatten elsewhere).
16
+
17
+ Anything the regex cannot classify is collected as a warning so the caller can
18
+ surface it instead of silently dropping members.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from typing import Any, Dict, List, Tuple
25
+
26
+ # Strip // line comments and /* ... */ block comments before lexing.
27
+ _LINE_COMMENT = re.compile(r'//[^\n]*')
28
+ _BLOCK_COMMENT = re.compile(r'/\*.*?\*/', re.DOTALL)
29
+
30
+ # #define NAME VALUE (only simple object-like macros; function-like macros,
31
+ # i.e. `#define F(x) ...`, are skipped).
32
+ _DEFINE = re.compile(
33
+ r'^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)[ \t]+(.+?)[ \t]*$',
34
+ re.MULTILINE,
35
+ )
36
+
37
+ # typedef enum { body } name ;
38
+ _ENUM = re.compile(
39
+ r'typedef\s+enum\s*(?:[A-Za-z_]\w*\s*)?\{(?P<body>.*?)\}\s*'
40
+ r'(?P<name>[A-Za-z_]\w*)\s*;',
41
+ re.DOTALL,
42
+ )
43
+
44
+ # typedef struct { body } name ;
45
+ _STRUCT = re.compile(
46
+ r'typedef\s+struct\s*(?:[A-Za-z_]\w*\s*)?\{(?P<body>.*?)\}\s*'
47
+ r'(?P<name>[A-Za-z_]\w*)\s*;',
48
+ re.DOTALL,
49
+ )
50
+
51
+ # One struct member: optional `const`, a type (one or more words to allow
52
+ # `unsigned int`), a name, optional [..] dimensions, trailing `;`.
53
+ _MEMBER = re.compile(
54
+ r'^\s*(?:const\s+)?'
55
+ r'(?P<type>(?:struct\s+)?[A-Za-z_]\w*(?:\s+[A-Za-z_]\w*)*?)\s+'
56
+ r'(?P<name>[A-Za-z_]\w*)\s*'
57
+ r'(?P<dims>(?:\[[^\]]*\])*)\s*;\s*$'
58
+ )
59
+
60
+ _DIM = re.compile(r'\[([^\]]*)\]')
61
+
62
+
63
+ class CParseResult:
64
+ """Container for one parse pass: the schema dict plus collected warnings."""
65
+
66
+ def __init__(self) -> None:
67
+ self.schema: Dict[str, Any] = {}
68
+ self.consts: Dict[str, Any] = {}
69
+ self.warnings: List[str] = []
70
+
71
+ @property
72
+ def has_content(self) -> bool:
73
+ return bool(self.schema) or bool(self.consts)
74
+
75
+
76
+ def _strip_comments(text: str) -> str:
77
+ text = _BLOCK_COMMENT.sub(' ', text)
78
+ text = _LINE_COMMENT.sub('', text)
79
+ return text
80
+
81
+
82
+ def _parse_define_value(raw: str) -> Any:
83
+ """Best-effort numeric value of a simple #define. Returns None if it is
84
+ not a plain int/float (e.g. an expression or another macro)."""
85
+ token = raw.strip()
86
+ # Allow a single set of wrapping parentheses: `#define N (40)`.
87
+ if token.startswith('(') and token.endswith(')'):
88
+ token = token[1:-1].strip()
89
+ # Drop integer suffixes like 40U, 40UL, 1.0f.
90
+ int_match = re.fullmatch(r'[+-]?\d+[uUlL]*', token)
91
+ if int_match:
92
+ return int(re.sub(r'[uUlL]+$', '', token))
93
+ float_match = re.fullmatch(r'[+-]?\d*\.\d+[fF]?|[+-]?\d+\.\d*[fF]?', token)
94
+ if float_match:
95
+ return float(token.rstrip('fF'))
96
+ return None
97
+
98
+
99
+ def _parse_defines(text: str, result: CParseResult) -> None:
100
+ for name, raw in _DEFINE.findall(text):
101
+ # Skip function-like macros: `#define F (x)` would have raw "(x)" but
102
+ # the real giveaway is `NAME(` with no space, which the regex above
103
+ # already excludes by requiring whitespace before the value. A value
104
+ # we cannot reduce to a number is recorded as a warning.
105
+ value = _parse_define_value(raw)
106
+ if value is None:
107
+ result.warnings.append(
108
+ f"#define {name}: value '{raw}' is not a plain number; skipped")
109
+ continue
110
+ result.consts[name] = value
111
+
112
+
113
+ def _parse_enums(text: str, result: CParseResult) -> None:
114
+ for m in _ENUM.finditer(text):
115
+ name = m.group('name')
116
+ items: Dict[str, int] = {}
117
+ next_value = 0
118
+ for entry in m.group('body').split(','):
119
+ entry = entry.strip()
120
+ if not entry:
121
+ continue
122
+ if '=' in entry:
123
+ key, _, val = entry.partition('=')
124
+ key = key.strip()
125
+ val = val.strip()
126
+ parsed = _parse_define_value(val)
127
+ if parsed is None or not isinstance(parsed, int):
128
+ parsed = result.consts.get(val)
129
+ if not isinstance(parsed, int):
130
+ result.warnings.append(
131
+ f"enum {name}: cannot resolve value of '{key} = {val}'")
132
+ next_value += 1
133
+ continue
134
+ next_value = parsed
135
+ else:
136
+ key = entry
137
+ items[key] = next_value
138
+ next_value += 1
139
+ result.schema[name] = {'type': 'enum', 'items': items}
140
+
141
+
142
+ def _parse_member(line: str) -> Tuple[Dict[str, Any], str]:
143
+ """Parse one struct member line. Returns (field_spec, '') on success or
144
+ ({}, reason) on failure."""
145
+ m = _MEMBER.match(line)
146
+ if not m:
147
+ return {}, f"unrecognized member: '{line.strip()}'"
148
+ ctype = re.sub(r'\s+', ' ', m.group('type')).strip()
149
+ fname = m.group('name')
150
+ dims = _DIM.findall(m.group('dims') or '')
151
+
152
+ field: Dict[str, Any] = {'name': fname, 'type': ctype}
153
+ if dims:
154
+ counts: List[Any] = []
155
+ for d in dims:
156
+ d = d.strip()
157
+ num = _parse_define_value(d)
158
+ counts.append(num if num is not None else d)
159
+ field['count'] = counts[0] if len(counts) == 1 else counts
160
+ return field, ''
161
+
162
+
163
+ def _parse_structs(text: str, result: CParseResult) -> None:
164
+ for m in _STRUCT.finditer(text):
165
+ name = m.group('name')
166
+ body = m.group('body')
167
+ items: List[Dict[str, Any]] = []
168
+ # Split on ';' so members spanning whitespace/newlines stay together.
169
+ for chunk in body.split(';'):
170
+ chunk = chunk.strip()
171
+ if not chunk:
172
+ continue
173
+ field, reason = _parse_member(chunk + ';')
174
+ if reason:
175
+ result.warnings.append(f"struct {name}: {reason}")
176
+ continue
177
+ items.append(field)
178
+ result.schema[name] = {'type': 'struct', 'items': items}
179
+
180
+
181
+ def parse_c_source(text: str) -> CParseResult:
182
+ """Parse C source text into a cfg_t-style schema result."""
183
+ result = CParseResult()
184
+ clean = _strip_comments(text)
185
+ _parse_defines(clean, result)
186
+ _parse_enums(clean, result)
187
+ _parse_structs(clean, result)
188
+ return result
189
+
190
+
191
+ def build_schema_dict(result: CParseResult) -> Dict[str, Any]:
192
+ """Merge consts + enums + structs into one ordered cfg_t-style dict,
193
+ matching the layout of the hand-written cfg_t/*.json files (consts and
194
+ enums first, then structs)."""
195
+ out: Dict[str, Any] = {}
196
+ for k, v in result.consts.items():
197
+ out[k] = v
198
+ for k, v in result.schema.items():
199
+ if v.get('type') == 'enum':
200
+ out[k] = v
201
+ for k, v in result.schema.items():
202
+ if v.get('type') == 'struct':
203
+ out[k] = v
204
+ return out
@@ -0,0 +1,341 @@
1
+ """ELF/DWARF layout verifier: compares the schema-derived C-ABI layout of
2
+ each pipeline cfg struct against the ground-truth layout recorded in an ELF's
3
+ DWARF debug info.
4
+
5
+ Strictest policy (all mismatches are errors):
6
+ - member name + order must match exactly
7
+ - each member's byte offset must match
8
+ - each member's byte size must match
9
+ - the struct's total byte size must match
10
+
11
+ Type names are matched against DWARF as-is: a stage's `type` (e.g. "a_cfg_t")
12
+ is looked up among the ELF's struct/typedef names verbatim. A type not found
13
+ in the ELF is an error.
14
+
15
+ No Qt here; the verifier returns a LoadReport so the UI can render it with the
16
+ same panels used for cfg_t / pipeline issues.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Any, Dict, List, Optional, Tuple
23
+
24
+ from ..schema import (
25
+ Field, ScalarField, EnumField, StructField, ArrayField,
26
+ SchemaRegistry, LoadReport,
27
+ )
28
+ from .bin_emitter import _alignof, _sizeof, merge_abi
29
+
30
+
31
+ # --------------------------------------------------------------------------- #
32
+ # DWARF model
33
+ # --------------------------------------------------------------------------- #
34
+
35
+ class DwarfMember:
36
+ __slots__ = ('name', 'offset', 'size')
37
+
38
+ def __init__(self, name: str, offset: int, size: int) -> None:
39
+ self.name = name
40
+ self.offset = offset
41
+ self.size = size
42
+
43
+
44
+ class DwarfStruct:
45
+ __slots__ = ('name', 'byte_size', 'members')
46
+
47
+ def __init__(self, name: Optional[str], byte_size: int,
48
+ members: List[DwarfMember]) -> None:
49
+ self.name = name
50
+ self.byte_size = byte_size
51
+ self.members = members
52
+
53
+
54
+ class ElfLayout:
55
+ """A lookup of type-name -> DwarfStruct parsed from one ELF file."""
56
+
57
+ def __init__(self, structs: Dict[str, DwarfStruct]) -> None:
58
+ self._structs = structs
59
+
60
+ def get(self, type_name: str) -> Optional[DwarfStruct]:
61
+ return self._structs.get(type_name)
62
+
63
+ def __contains__(self, type_name: str) -> bool:
64
+ return type_name in self._structs
65
+
66
+
67
+ # --------------------------------------------------------------------------- #
68
+ # DWARF parsing
69
+ # --------------------------------------------------------------------------- #
70
+
71
+ def load_elf_layout(elf_path: str,
72
+ wanted: Optional[set] = None) -> ElfLayout:
73
+ """Parse the ELF's DWARF and index named structs (and typedefs that point
74
+ at structs) by name. Raises on parse failure; callers turn that into a
75
+ LoadReport error.
76
+
77
+ Only top-level DIEs of each compilation unit are scanned, which is where
78
+ cfg structs / typedefs live; this avoids walking every nested DIE in a
79
+ large firmware ELF (orders of magnitude faster). When `wanted` is given,
80
+ iteration stops as soon as every wanted name has been indexed.
81
+ """
82
+ from elftools.elf.elffile import ELFFile # lazy: optional dependency
83
+
84
+ structs: Dict[str, DwarfStruct] = {}
85
+ with open(elf_path, 'rb') as f:
86
+ elf = ELFFile(f)
87
+ if not elf.has_dwarf_info():
88
+ raise ValueError('ELF has no DWARF debug info')
89
+ dwarf = elf.get_dwarf_info()
90
+ for cu in dwarf.iter_CUs():
91
+ top = cu.get_top_DIE()
92
+ for die in top.iter_children():
93
+ name = _die_name(die)
94
+ if not name:
95
+ continue
96
+ if die.tag == 'DW_TAG_structure_type':
97
+ structs.setdefault(name, _parse_struct(cu, die))
98
+ elif die.tag == 'DW_TAG_typedef':
99
+ target = _resolve_to_struct(cu, die)
100
+ if target is not None:
101
+ structs.setdefault(name, _parse_struct(cu, target))
102
+ if wanted and wanted.issubset(structs.keys()):
103
+ break
104
+ return ElfLayout(structs)
105
+
106
+
107
+ def _die_name(die) -> Optional[str]:
108
+ attr = die.attributes.get('DW_AT_name')
109
+ if attr is None:
110
+ return None
111
+ val = attr.value
112
+ return val.decode(errors='replace') if isinstance(val, bytes) else str(val)
113
+
114
+
115
+ def _deref_type(cu, die):
116
+ attr = die.attributes.get('DW_AT_type')
117
+ if attr is None:
118
+ return None
119
+ return cu.get_DIE_from_refaddr(attr.value + cu.cu_offset)
120
+
121
+
122
+ def _strip_cv_typedef(cu, die):
123
+ """Follow typedef / const / volatile wrappers to the underlying type."""
124
+ seen = 0
125
+ while die is not None and seen < 32:
126
+ if die.tag in ('DW_TAG_typedef', 'DW_TAG_const_type',
127
+ 'DW_TAG_volatile_type'):
128
+ die = _deref_type(cu, die)
129
+ seen += 1
130
+ continue
131
+ return die
132
+ return die
133
+
134
+
135
+ def _resolve_to_struct(cu, die):
136
+ target = _strip_cv_typedef(cu, _deref_type(cu, die))
137
+ if target is not None and target.tag == 'DW_TAG_structure_type':
138
+ return target
139
+ return None
140
+
141
+
142
+ def _parse_struct(cu, die) -> DwarfStruct:
143
+ byte_size = _attr_int(die, 'DW_AT_byte_size', 0)
144
+ members: List[DwarfMember] = []
145
+ for child in die.iter_children():
146
+ if child.tag != 'DW_TAG_member':
147
+ continue
148
+ name = _die_name(child) or '<anon>'
149
+ offset = _attr_int(child, 'DW_AT_data_member_location', 0)
150
+ size = _member_byte_size(cu, child)
151
+ members.append(DwarfMember(name, offset, size))
152
+ return DwarfStruct(_die_name(die), byte_size, members)
153
+
154
+
155
+ def _attr_int(die, key: str, default: int) -> int:
156
+ attr = die.attributes.get(key)
157
+ if attr is None:
158
+ return default
159
+ val = attr.value
160
+ if isinstance(val, list): # location expression form; ignore, rare here
161
+ return default
162
+ return int(val)
163
+
164
+
165
+ def _member_byte_size(cu, member) -> int:
166
+ """Byte size of a member, following its type chain.
167
+
168
+ For an array, multiply the element size by every dimension's count so the
169
+ result is the full in-struct footprint (matching the schema side, which
170
+ reports the whole array as one member).
171
+ """
172
+ type_die = _deref_type(cu, member)
173
+ return _type_byte_size(cu, type_die)
174
+
175
+
176
+ def _type_byte_size(cu, die) -> int:
177
+ seen = 0
178
+ while die is not None and seen < 64:
179
+ size_attr = die.attributes.get('DW_AT_byte_size')
180
+ if die.tag == 'DW_TAG_array_type':
181
+ elem = _strip_cv_typedef(cu, _deref_type(cu, die))
182
+ elem_size = _type_byte_size(cu, elem)
183
+ count = 1
184
+ for sub in die.iter_children():
185
+ if sub.tag == 'DW_TAG_subrange_type':
186
+ ub = sub.attributes.get('DW_AT_upper_bound')
187
+ cnt = sub.attributes.get('DW_AT_count')
188
+ if cnt is not None:
189
+ count *= int(cnt.value)
190
+ elif ub is not None:
191
+ count *= int(ub.value) + 1
192
+ return elem_size * count
193
+ if size_attr is not None:
194
+ return int(size_attr.value)
195
+ nxt = _deref_type(cu, die)
196
+ if nxt is None:
197
+ return 0
198
+ die = nxt
199
+ seen += 1
200
+ return 0
201
+
202
+
203
+ # --------------------------------------------------------------------------- #
204
+ # Schema-side layout (mirrors the C-ABI offsets the bin emitter would use)
205
+ # --------------------------------------------------------------------------- #
206
+
207
+ class SchemaMember:
208
+ __slots__ = ('name', 'offset', 'size')
209
+
210
+ def __init__(self, name: str, offset: int, size: int) -> None:
211
+ self.name = name
212
+ self.offset = offset
213
+ self.size = size
214
+
215
+
216
+ def schema_struct_layout(node: StructField,
217
+ abi: Dict[str, Any]) -> Tuple[int, List[SchemaMember]]:
218
+ """Compute (total_size, members) for a StructField using the same C-ABI
219
+ rules as the .bin emitter (natural alignment, pack cap, tail padding).
220
+ """
221
+ members: List[SchemaMember] = []
222
+ offset = 0
223
+ for child in node.children:
224
+ align = _alignof(child, abi)
225
+ rem = offset % align
226
+ if rem:
227
+ offset += align - rem
228
+ size = _sizeof(child, abi)
229
+ members.append(SchemaMember(child.name, offset, size))
230
+ offset += size
231
+ struct_align = _alignof(node, abi)
232
+ rem = offset % struct_align
233
+ if rem:
234
+ offset += struct_align - rem
235
+ return offset, members
236
+
237
+
238
+ # --------------------------------------------------------------------------- #
239
+ # Comparison
240
+ # --------------------------------------------------------------------------- #
241
+
242
+ def verify_sections(sections, registry: SchemaRegistry, elf_path: str,
243
+ abi: Optional[Dict[str, Any]] = None) -> LoadReport:
244
+ """Verify every section's cfg struct against the ELF DWARF layout.
245
+
246
+ `sections` is a sequence of (label, type_name, values); only label and
247
+ type_name are used. Each distinct type_name is verified once. Returns a
248
+ LoadReport whose `file` is the ELF path so the UI panels stay uniform.
249
+ """
250
+ report = LoadReport()
251
+ if not elf_path:
252
+ return report
253
+
254
+ elf_label = os.path.basename(elf_path) or elf_path
255
+ if not os.path.exists(elf_path):
256
+ report.error(elf_label, '', f'ELF file not found: {elf_path}')
257
+ return report
258
+
259
+ wanted = {t for _l, t, _v in sections if t}
260
+ try:
261
+ layout = load_elf_layout(elf_path, wanted)
262
+ except ImportError:
263
+ report.error(
264
+ elf_label, '',
265
+ 'pyelftools is required for ELF verification (pip install pyelftools)')
266
+ return report
267
+ except Exception as exc: # noqa: BLE001 - surface any parse failure
268
+ report.error(elf_label, '', f'failed to read DWARF: {exc}')
269
+ return report
270
+
271
+ abi = abi if abi is not None else merge_abi(None)
272
+
273
+ seen: set = set()
274
+ for _label, type_name, _values in sections:
275
+ if not type_name or type_name in seen:
276
+ continue
277
+ seen.add(type_name)
278
+ _verify_type(report, registry, layout, type_name, elf_label, abi)
279
+ return report
280
+
281
+
282
+ def _verify_type(report: LoadReport, registry: SchemaRegistry,
283
+ layout: ElfLayout, type_name: str, elf_label: str,
284
+ abi: Dict[str, Any]) -> None:
285
+ dwarf = layout.get(type_name)
286
+ if dwarf is None:
287
+ report.error(
288
+ elf_label, type_name,
289
+ f"type '{type_name}' not found in ELF DWARF")
290
+ return
291
+
292
+ root = registry.build(type_name)
293
+ total, members = schema_struct_layout(root, abi)
294
+
295
+ # Total struct size.
296
+ if total != dwarf.byte_size:
297
+ report.error(
298
+ elf_label, type_name,
299
+ f"struct size mismatch: schema {total} bytes, "
300
+ f"ELF {dwarf.byte_size} bytes")
301
+
302
+ # Member count / name+order, walked position by position so we can report
303
+ # the first divergence precisely without masking later members.
304
+ n = max(len(members), len(dwarf.members))
305
+ for i in range(n):
306
+ sm = members[i] if i < len(members) else None
307
+ dm = dwarf.members[i] if i < len(dwarf.members) else None
308
+ loc = f'{type_name}[{i}]'
309
+
310
+ if sm is None:
311
+ report.error(
312
+ elf_label, loc,
313
+ f"ELF has extra member '{dm.name}' "
314
+ f"(offset {dm.offset}, size {dm.size}) "
315
+ f"not present in schema")
316
+ continue
317
+ if dm is None:
318
+ report.error(
319
+ elf_label, f'{type_name} -> {sm.name}',
320
+ f"schema member '{sm.name}' has no matching member in ELF")
321
+ continue
322
+
323
+ if sm.name != dm.name:
324
+ report.error(
325
+ elf_label, loc,
326
+ f"member name/order mismatch: schema '{sm.name}', "
327
+ f"ELF '{dm.name}'")
328
+ # Names diverged: positional offset/size comparison would be
329
+ # noise, so move on to the next position.
330
+ continue
331
+
332
+ member_loc = f'{type_name} -> {sm.name}'
333
+ if sm.offset != dm.offset:
334
+ report.error(
335
+ elf_label, member_loc,
336
+ f"offset mismatch: schema {sm.offset}, ELF {dm.offset}")
337
+ if sm.size != dm.size:
338
+ report.error(
339
+ elf_label, member_loc,
340
+ f"size mismatch: schema {sm.size} bytes, "
341
+ f"ELF {dm.size} bytes")
@@ -0,0 +1,137 @@
1
+ """Pretty-print a pipeline document as JSON with .c-style compaction.
2
+
3
+ Structural heuristic (no schema needed):
4
+ - A list of scalars -> single line: [1, 2, 3]
5
+ - A list of dicts -> one dict per line, each dict on a single line
6
+ - Anything else (dict, or -> normal indented expansion
7
+ mixed/nested lists)
8
+
9
+ The dict-per-line rule mirrors how table widgets render struct arrays in the
10
+ exported .c, keeping the saved JSON readable for the same data.
11
+
12
+ Optional schema-aware multi-line wrapping:
13
+ Callers may pass `wrap_resolver`, a function (path) -> int that, given the
14
+ JSON path to a scalar list (a tuple of dict keys / list indices from the
15
+ document root), returns how many values to place per line (0 = single line).
16
+ This lets `widget=multiline` arrays save as line-broken blocks while the
17
+ printer itself stays schema-agnostic.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from typing import Any, Callable, Optional, Tuple
24
+
25
+ INDENT = ' '
26
+
27
+ # Wrap scalar lists longer than this many elements onto multiple lines, even
28
+ # without an explicit widget=multiline hint. Keeps saved JSON readable.
29
+ MAX_ROW = 64
30
+
31
+ WrapResolver = Callable[[Tuple[Any, ...]], int]
32
+
33
+
34
+ def dumps(data: Any, wrap_resolver: Optional[WrapResolver] = None) -> str:
35
+ return _emit(data, 0, (), wrap_resolver) + '\n'
36
+
37
+
38
+ def _emit(value: Any, level: int, path: Tuple[Any, ...],
39
+ wrap: Optional[WrapResolver]) -> str:
40
+ if isinstance(value, dict):
41
+ return _emit_dict(value, level, path, wrap)
42
+ if isinstance(value, list):
43
+ return _emit_list(value, level, path, wrap)
44
+ return _scalar(value)
45
+
46
+
47
+ def _emit_dict(obj: dict, level: int, path: Tuple[Any, ...],
48
+ wrap: Optional[WrapResolver]) -> str:
49
+ if not obj:
50
+ return '{}'
51
+ pad = INDENT * (level + 1)
52
+ close = INDENT * level
53
+ lines = [
54
+ f'{pad}{json.dumps(k, ensure_ascii=False)}: '
55
+ f'{_emit(v, level + 1, path + (k,), wrap)}'
56
+ for k, v in obj.items()
57
+ ]
58
+ return '{\n' + ',\n'.join(lines) + f'\n{close}}}'
59
+
60
+
61
+ def _emit_list(arr: list, level: int, path: Tuple[Any, ...],
62
+ wrap: Optional[WrapResolver]) -> str:
63
+ if not arr:
64
+ return '[]'
65
+ if all(_is_scalar(x) for x in arr):
66
+ row_size = wrap(path) if wrap is not None else 0
67
+ if not (row_size and row_size > 0) and len(arr) > MAX_ROW:
68
+ row_size = MAX_ROW
69
+ if row_size and row_size > 0 and len(arr) > row_size:
70
+ return _emit_wrapped_scalars(arr, level, row_size)
71
+ return '[' + ', '.join(_scalar(x) for x in arr) + ']'
72
+ if all(isinstance(x, dict) for x in arr):
73
+ pad = INDENT * (level + 1)
74
+ close = INDENT * level
75
+ rows = _compact_dicts_aligned(arr, pad)
76
+ return '[\n' + ',\n'.join(rows) + f'\n{close}]'
77
+ pad = INDENT * (level + 1)
78
+ close = INDENT * level
79
+ rows = [pad + _emit(x, level + 1, path + (i,), wrap)
80
+ for i, x in enumerate(arr)]
81
+ return '[\n' + ',\n'.join(rows) + f'\n{close}]'
82
+
83
+
84
+ def _emit_wrapped_scalars(arr: list, level: int, row_size: int) -> str:
85
+ pad = INDENT * (level + 1)
86
+ close = INDENT * level
87
+ cells = [_scalar(x) for x in arr]
88
+ grid = [cells[i:i + row_size] for i in range(0, len(cells), row_size)]
89
+ widths = [max((len(row[j]) for row in grid if j < len(row)), default=0)
90
+ for j in range(row_size)]
91
+ rows = []
92
+ for row in grid:
93
+ padded = [c.ljust(widths[j]) for j, c in enumerate(row)]
94
+ rows.append(pad + ', '.join(padded).rstrip())
95
+ return '[\n' + ',\n'.join(rows) + f'\n{close}]'
96
+
97
+
98
+ def _compact_dicts_aligned(arr: list, pad: str) -> list:
99
+ """One compact dict per line. When every dict has the same keys in the
100
+ same order (i.e. a table), pad each column so values line up vertically.
101
+ """
102
+ keyseqs = [tuple(x.keys()) for x in arr]
103
+ uniform = len(arr) > 1 and len(set(keyseqs)) == 1
104
+ if not uniform:
105
+ return [pad + _compact_dict(x) for x in arr]
106
+
107
+ keys = keyseqs[0]
108
+ cols = [[f'{json.dumps(k, ensure_ascii=False)}: {_compact(x[k])}' for k in keys]
109
+ for x in arr]
110
+ widths = [max(len(row[j]) for row in cols) for j in range(len(keys))]
111
+ rows = []
112
+ for row in cols:
113
+ cells = [c.ljust(widths[j]) for j, c in enumerate(row)]
114
+ rows.append(pad + '{' + ', '.join(cells).rstrip() + '}')
115
+ return rows
116
+
117
+
118
+ def _compact_dict(obj: dict) -> str:
119
+ parts = [f'{json.dumps(k, ensure_ascii=False)}: {_compact(v)}'
120
+ for k, v in obj.items()]
121
+ return '{' + ', '.join(parts) + '}'
122
+
123
+
124
+ def _compact(value: Any) -> str:
125
+ if isinstance(value, dict):
126
+ return _compact_dict(value)
127
+ if isinstance(value, list):
128
+ return '[' + ', '.join(_compact(x) for x in value) + ']'
129
+ return _scalar(value)
130
+
131
+
132
+ def _is_scalar(value: Any) -> bool:
133
+ return not isinstance(value, (dict, list))
134
+
135
+
136
+ def _scalar(value: Any) -> str:
137
+ return json.dumps(value, ensure_ascii=False)
Binary file
Binary file