Coverage for gws-app/gws/plugin/alkis/data/geo_info_dok/generator.py: 0%

503 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""Schema generator. 

2 

3Generate python APIs and object databases from GeoInfoDok sources. 

4 

5For version 6 use RR cat files Basisschema.cat and Fachschema.cat 

6For version 7 use the QEA (sqlite) file AAA-7.1.2.qea 

7 

8Usage:: 

9 

10 generator.py 6 /path/to/Basisschema.cat /path/to/Fachschema.cat 

11 generator.py 7 /path/to/AAA-7.1.2.qea 

12 

13""" 

14 

15import re 

16import os 

17import json 

18import textwrap 

19import sys 

20import html 

21import sqlalchemy as sa 

22 

23 

24def main(version, *paths): 

25 if version == '6': 

26 nodes = Parser6().parse(paths) 

27 

28 elif version == '7': 

29 nodes = Parser7().parse(paths) 

30 

31 else: 

32 raise ValueError('invalid version') 

33 

34 # dumps(f'{CDIR}/db{version}.json', db) 

35 

36 py = PythonGenerator(nodes, version).build() 

37 

38 with open(f'{CDIR}/gid{version}.py', 'w') as fp: 

39 fp.write(py) 

40 

41 

42## 

43 

44CDIR = os.path.dirname(__file__) 

45TAB = ' ' * 4 

46TAB2 = TAB * 2 

47Q3 = '"""' 

48WRAP_WIDTH = 110 

49 

50CATEGORY_ROOTS = { 

51 'AFIS-ALKIS-ATKIS Fachschema': 'fs', 

52 'AAA Basisschema': 'bs', 

53 'AAA_Objektartenkatalog': 'ak', 

54} 

55 

56T_CLASS = 'class' 

57T_CATEGORY = 'category' 

58T_ENUM = 'enum' 

59T_UNION = 'union' 

60 

61PY_HEAD = '''\ 

62"""GeoInfoDok <VERSION> schema. 

63 

64(c) 2023 Arbeitsgemeinschaft der Vermessungsverwaltungen der Länder der Bundesrepublik Deutschland 

65 

66https://www.adv-online.de/GeoInfoDok/ 

67 

68This code is automatically generated from .CAT/.QEA source files. 

69""" 

70 

71from typing import Any, Literal, Optional, TypeAlias, Union 

72from datetime import date, datetime 

73 

74 

75# gws:nospec 

76 

77class Object: 

78 pass 

79 

80 

81class Category: 

82 pass 

83 

84 

85class Enumeration: 

86 pass 

87 

88 

89def object__getattr__(self, item): 

90 if item.startswith('_'): 

91 raise AttributeError() 

92 return None 

93 

94 

95setattr(Object, '__getattr__', object__getattr__) 

96setattr(Category, '__getattr__', object__getattr__) 

97setattr(Enumeration, '__getattr__', object__getattr__) 

98 

99''' 

100 

101STD_TYPES = { 

102 'Angle': 'float', 

103 'Area': 'float', 

104 'Boolean': 'bool', 

105 'CharacterString': 'str', 

106 'Date': 'date', 

107 'DateTime': 'datetime', 

108 'Distance': 'float', 

109 'GenericName': 'str', 

110 'Integer': 'int', 

111 'Length': 'int', 

112 'LocalName': 'str', 

113 'Measure': 'str', 

114 'Query': 'str', 

115 'Real': 'float', 

116 'SC_CRS': 'str', 

117 'URI': 'str', 

118 'URL': 'str', 

119 'Volume': 'float', 

120} 

121 

122 

123## 

124 

125class Node: 

126 def __init__(self, **kwargs): 

127 vars(self).update(kwargs) 

128 

129 def __getattr__(self, item): 

130 return None 

131 

132 

133## 

134 

135class Parser: 

136 nodes: list[Node] = [] 

137 

138 def finalize(self): 

139 for node in self.nodes: 

140 self.make_key(node) 

141 

142 self.filter_category_roots() 

143 self.resolve_supers() 

144 

145 for node in self.nodes: 

146 self.check_flag(node, 'is_aa', 'AA_Objekt') 

147 self.check_flag(node, 'is_reo', 'AA_REO') 

148 

149 return [node for node in self.nodes if node.T] 

150 

151 def make_key(self, node): 

152 if node.key: 

153 return node.key 

154 

155 parent_key = '' 

156 parent = popattr(node, 'pParent') 

157 if parent: 

158 parent_key = self.make_key(parent) 

159 

160 node.key = parent_key + '/' + node.name.lower() 

161 return node.key 

162 

163 def filter_category_roots(self): 

164 new_nodes = [] 

165 roots = {'/' + to_name(k).lower(): '/' + v for k, v in CATEGORY_ROOTS.items()} 

166 

167 for node in self.nodes: 

168 for k, v in roots.items(): 

169 if k in node.key: 

170 _, _, rest = node.key.partition(k) 

171 node.key = v + rest 

172 new_nodes.append(node) 

173 

174 self.nodes = new_nodes 

175 

176 def resolve_supers(self): 

177 for node in self.nodes: 

178 for sup_name in popattr(node, 'pSuperNames', []): 

179 sup = self.find_node(sup_name) 

180 if sup and sup != node: 

181 node.supers.append(sup) 

182 

183 def check_flag(self, node, prop, root): 

184 a = getattr(node, prop) 

185 if a is not None: 

186 return a 

187 if node.name == root: 

188 v = True 

189 elif node.supers: 

190 v = any(self.check_flag(super_node, prop, root) for super_node in node.supers) 

191 else: 

192 v = False 

193 setattr(node, prop, v) 

194 return v 

195 

196 def find_node(self, name): 

197 for node in self.nodes: 

198 if node.name == name: 

199 return node 

200 

201 def get_doc(self, rec): 

202 s = rec.get('documentation') or rec.get('Note') or rec.get('Notes') or '' 

203 s = s.strip() 

204 s = html.unescape(s) 

205 # remove the [X] prefix, as in 

206 # "[E] 'Person' ist eine natürliche... 

207 if s.startswith('['): 

208 return s.partition(']')[-1].strip() 

209 return s 

210 

211 def get_hname(self, node): 

212 # sometimes, the first quoted word in the name of the object, as in 

213 # sonstigeEigenschaft: "'Sonstige Eigenschaft' sind Informationen zum Grenzpunkt... 

214 # 

215 # however, this should not be extracted 

216 # weistAuf: "'Flurstück' weist auf 'Lagebezeichnung mit Hausnummer'... 

217 

218 if not node.doc: 

219 return 

220 

221 cmp_name = to_name(node.name) 

222 cmp_name = re.sub(r'[A-Z]+_(.+)', r'\1', cmp_name) 

223 cmp_name = cmp_name.replace('_', '').lower() 

224 

225 patterns = [ 

226 r"^\'(.+?)\'", 

227 r"^\"(.+?)\"", 

228 r"^(.+?)\.$", 

229 r"^(\w+)", 

230 ] 

231 

232 for pat in patterns: 

233 m = re.match(pat, node.doc) 

234 if m: 

235 s = m.group(1) 

236 c = to_name(s).replace('_', '').lower() 

237 if c == cmp_name: 

238 return s 

239 

240 if node.name == 'funktion': 

241 # fix a spelling mistake in some docstrings 

242 return 'Funktion' 

243 

244 def add_enum_value(self, node, k, v): 

245 if k is None: 

246 k = len(node.values) + 1 

247 node.values[k] = v 

248 

249 def set_type_from_record(self, node, rec): 

250 self.set_type_from_string(node, rec.get('type', '') or rec.get('Type', '')) 

251 

252 def set_type_from_string(self, node, s): 

253 m = re.match(r'(Sequence|Set)<(.+?)>', s) 

254 if m: 

255 node.type = m.group(2) 

256 node.list = True 

257 else: 

258 node.type = s 

259 

260 def set_cardinality_from_string(self, node, s=None): 

261 if not s: 

262 return 

263 elif s == '0..1': 

264 node.optional = True 

265 elif '..' in s: 

266 node.list = True 

267 

268 def set_cardinality_from_record(self, node, rec): 

269 lb = str(rec['LowerBound']) 

270 ub = str(rec['UpperBound']) 

271 if lb == '0' and ub == '1': 

272 node.optional = True 

273 elif lb != ub: 

274 node.list = True 

275 

276 

277class Parser6(Parser): 

278 def parse(self, paths): 

279 for path in paths: 

280 cat = CatParser().parse(path) 

281 self.parse_object(cat[1], None) 

282 self.parse_associations(cat[1]) 

283 return self.finalize() 

284 

285 def parse_object(self, rec, parent): 

286 node = Node(name=rec['NAME'], pParent=parent, doc=self.get_doc(rec)) 

287 self.nodes.append(node) 

288 

289 # e.g. zugriffsartProduktkennungBenutzung [0..*] 

290 m = re.match(r'^(\S+)\s*\[(.+?)]$', node.name) 

291 if m: 

292 node.name = m.group(1) 

293 self.set_cardinality_from_string(node, m.group(2)) 

294 

295 node.hname = self.get_hname(node) 

296 node.name = to_name(node.name) 

297 

298 for a in rec.get('attributes', []): 

299 if a['name'] == 'Kennung': 

300 node.uid = a['value'] 

301 

302 stereo = rec.get('stereotype', '').lower() 

303 

304 if rec['TYPE'] == 'Class_Category': 

305 node.T = T_CATEGORY 

306 for r2 in rec.get('logical_models', []): 

307 self.parse_object(r2, node) 

308 

309 elif rec['TYPE'] == 'Class' and stereo in {'codelist', 'enumeration'}: 

310 node.T = T_ENUM 

311 node.values = {} 

312 for r2 in rec.get('class_attributes', []): 

313 self.add_enum_value(node, r2.get('initv'), r2['NAME']) 

314 

315 elif rec['TYPE'] == 'Class' and stereo == 'union': 

316 node.T = T_UNION 

317 node.attributes = [ 

318 self.parse_object(r2, node) 

319 for r2 in rec.get('class_attributes', []) 

320 ] 

321 

322 elif rec['TYPE'] == 'Class': 

323 node.T = T_CLASS 

324 node.attributes = [ 

325 self.parse_object(r2, node) 

326 for r2 in rec.get('class_attributes', []) 

327 ] 

328 node.supers = [] 

329 node.pSuperNames = [ 

330 r2['supplier'].split(':')[-1] 

331 for r2 in rec.get('superclasses', []) 

332 ] 

333 

334 elif rec['TYPE'] == 'ClassAttribute': 

335 self.set_type_from_record(node, rec) 

336 

337 elif rec['TYPE'] == 'Role': 

338 self.set_type_from_string(node, rec['supplier'].split(':')[-1]) 

339 self.set_cardinality_from_string(node, rec.get('client_cardinality')) 

340 

341 return node 

342 

343 def parse_associations(self, rec): 

344 if rec['TYPE'] != 'Association': 

345 for o2 in rec.get('logical_models', []): 

346 self.parse_associations(o2) 

347 return 

348 

349 # This is what an association looks like in a parsed .cat: 

350 # 

351 # { 

352 # "NAME": "$UNNAMED$38", 

353 # "TYPE": "Association", 

354 # "quid": "40FED632018C", 

355 # "roles": [ 

356 # { 

357 # "NAME": "weistZum", 

358 # "TYPE": "Role", 

359 # "client_cardinality": "0..1", 

360 # "documentation": "Eine 'Lagebezeichnung mit Hausnummer' weist zum 'Turm'.", 

361 # "supplier": "...:AX_Turm" 

362 # }, 

363 # { 

364 # "NAME": "zeigtAuf", 

365 # "TYPE": "Role", 

366 # "client_cardinality": "0..*", 

367 # "documentation": "'Turm' zeigt auf eine 'Lagebezeichnung mit Hausnummer'.", 

368 # "supplier": "...:AX_LagebezeichnungMitHausnummer" 

369 # } 

370 # ] 

371 # } 

372 

373 role1 = rec['roles'][0] 

374 role2 = rec['roles'][1] 

375 

376 type1 = role1['supplier'].split(':')[-1] 

377 type2 = role2['supplier'].split(':')[-1] 

378 

379 cls1 = self.find_node(type1) 

380 cls2 = self.find_node(type2) 

381 

382 if cls2 and not role1['NAME'].startswith('$') and cls2.attributes is not None: 

383 cls2.attributes.append(self.parse_object(role1, cls2)) 

384 if cls1 and not role2['NAME'].startswith('$') and cls1.attributes is not None: 

385 cls1.attributes.append(self.parse_object(role2, cls1)) 

386 

387 

388## 

389 

390class Parser7(Parser): 

391 engine: sa.Engine 

392 

393 def parse(self, paths): 

394 for path in paths: 

395 self.engine = sa.create_engine(f'sqlite:///' + path) 

396 self.build_from_sqlite() 

397 return self.finalize() 

398 

399 def select(self, table): 

400 with self.engine.begin() as conn: 

401 sel = sa.text(f'SELECT * FROM {table}') 

402 return list(conn.execute(sel).mappings().all()) 

403 

404 def build_from_sqlite(self): 

405 

406 nodes_by_uid = {} 

407 nodes_by_gid = {} 

408 

409 for rec in self.select('t_object'): 

410 if rec['Alias']: 

411 continue 

412 

413 node = Node(name=rec['Name'], doc=self.get_doc(rec)) 

414 self.nodes.append(node) 

415 

416 node.hname = self.get_hname(node) 

417 node.name = to_name(node.name) 

418 

419 node.Package_ID = rec['Package_ID'] 

420 

421 nodes_by_uid[rec['Object_ID']] = node 

422 nodes_by_gid[rec['ea_guid']] = node 

423 

424 if rec['Object_Type'] == 'Package': 

425 node.T = T_CATEGORY 

426 continue 

427 

428 stereo = (rec['Stereotype'] or '').lower() 

429 

430 if rec['Object_Type'] == 'Enumeration' or stereo in {'enumeration', 'codelist'}: 

431 node.T = T_ENUM 

432 node.values = {} 

433 continue 

434 

435 if rec['Object_Type'] == 'Class' and stereo == 'union': 

436 node.T = T_UNION 

437 node.attributes = [] 

438 continue 

439 

440 if rec['Object_Type'] == 'Class': 

441 node.T = T_CLASS 

442 node.attributes = [] 

443 node.supers = [] 

444 node.pSuperNames = [] 

445 continue 

446 

447 for rec in self.select('t_objectproperties'): 

448 if rec['Property'] == 'AAA:Kennung' and rec['Value']: 

449 node = nodes_by_uid.get(rec['Object_ID']) 

450 if node: 

451 node.uid = rec['Value'] 

452 

453 package_uid_to_gid = {} 

454 

455 for rec in self.select('t_package'): 

456 package_uid_to_gid[rec['Package_ID']] = rec['ea_guid'] 

457 

458 for node in self.nodes: 

459 pkg_gid = package_uid_to_gid.get(popattr(node, 'Package_ID')) 

460 if pkg_gid: 

461 pkg_node = nodes_by_gid.get(pkg_gid) 

462 if pkg_node: 

463 node.pParent = pkg_node 

464 

465 for rec in self.select('t_attribute'): 

466 node = nodes_by_uid.get(rec['Object_ID']) 

467 if node: 

468 if node.T in {T_CLASS, T_UNION}: 

469 a = Node(name=rec['Name'], doc=self.get_doc(rec), pParent=node) 

470 self.set_type_from_record(a, rec) 

471 self.set_cardinality_from_record(a, rec) 

472 node.attributes.append(a) 

473 self.nodes.append(a) 

474 if node.T == T_ENUM: 

475 self.add_enum_value(node, rec['Default'], rec['Name']) 

476 

477 for rec in self.select('t_connector'): 

478 so = nodes_by_uid.get(rec['Start_Object_ID']) 

479 eo = nodes_by_uid.get(rec['End_Object_ID']) 

480 

481 if so and eo and so.T == eo.T == T_CLASS: 

482 if rec['Connector_Type'] == 'Generalization': 

483 so.pSuperNames.append(eo.name) 

484 continue 

485 

486 if rec['Connector_Type'] == 'Association': 

487 """ 

488 "SourceCard": "0..*", 

489 "SourceRole": "zeigtAuf", 

490 "SourceRoleNote": "'Turm' zeigt auf eine 'Lagebezeichnung mit Hausnummer'.", 

491 "DestRole": "weistZum", 

492 "DestRoleNote": "Eine 'Lagebezeichnung mit Hausnummer' weist zum 'Turm'.", 

493 "Start_Object_ID": 3678, 

494 "End_Object_ID": 3511, 

495 """ 

496 if rec['SourceRole']: 

497 a = Node(name=rec['SourceRole'], doc=rec['SourceRoleNote'], type=so.name, pParent=eo) 

498 self.set_cardinality_from_string(a, rec['SourceCard']) 

499 eo.attributes.append(a) 

500 self.nodes.append(a) 

501 

502 if rec['DestRole']: 

503 b = Node(name=rec['DestRole'], doc=rec['DestRoleNote'], type=eo.name, pParent=so) 

504 so.attributes.append(b) 

505 self.nodes.append(b) 

506 

507 

508class CatParser: 

509 """Parser for RR cat files.""" 

510 

511 def parse(self, path): 

512 with open(path, 'rb') as fp: 

513 text = fp.read().decode('latin-1') 

514 self.tokenize(text) 

515 return self.parse_sequence() 

516 

517 re_token = r'''(?x) 

518 ( [()] ) 

519 | 

520 ( [_a-zA-Z] \w* ) 

521 | 

522 (  

523 " (?: \\. | [^"] )* "  

524 |  

525 [^()\s]+  

526 ) 

527 ''' 

528 

529 tokens = [] 

530 token_pos = 0 

531 

532 def tokenize(self, text): 

533 docstring_buf = [] 

534 self.tokens = [] 

535 

536 for n, ln in enumerate(text.split('\n'), 1): 

537 ln = ln.strip() 

538 if not ln: 

539 continue 

540 if ln.startswith('|'): 

541 docstring_buf.append(ln[1:]) 

542 continue 

543 if docstring_buf: 

544 s = '\n'.join(p for p in docstring_buf if p).strip() or ' ' 

545 self.tokens.append(('', '', s)) 

546 docstring_buf = [] 

547 for br, name, val in re.findall(self.re_token, ln): 

548 if val.startswith('"'): 

549 # decode a string, don't allow empty strings 

550 val = val[1:-1].replace('\\', '') or ' ' 

551 self.tokens.append((br, name, val)) 

552 

553 def tok(self): 

554 return self.tokens[self.token_pos] 

555 

556 def pop(self): 

557 self.token_pos += 1 

558 

559 def eof(self): 

560 return self.token_pos >= len(self.tokens) 

561 

562 ## 

563 

564 def parse_sequence(self): 

565 items = [] 

566 while not self.eof(): 

567 br, name, val = self.tok() 

568 if br == ')': 

569 self.pop() 

570 break 

571 items.append(self.parse_item()) 

572 return items 

573 

574 def parse_item(self): 

575 br, name, val = self.tok() 

576 if val: 

577 self.pop() 

578 return val 

579 

580 if name in {'TRUE', 'FALSE'}: 

581 self.pop() 

582 return name == 'TRUE' 

583 

584 if br == '(': 

585 self.pop() 

586 br, name, val = self.tok() 

587 if name == 'list': 

588 # (list ... 

589 return self.parse_list() 

590 if name == 'object': 

591 # (object ... 

592 return self.parse_object() 

593 if name == 'value': 

594 # (value ... 

595 return self.parse_value() 

596 

597 # (val val...) 

598 return self.parse_sequence() 

599 

600 raise SyntaxError(f'invalid token {br=} {name=} {val=}') 

601 

602 def parse_list(self): 

603 # e.g. (list Attribute_Set (object... (object... 

604 

605 self.pop() # list 

606 self.pop() # type 

607 

608 return self.parse_sequence() 

609 

610 def parse_object(self): 

611 # e.g. (object ClassAttribute "Sonstiges" attr val attr val 

612 # e.g. (object Attribute 

613 

614 rec = {} 

615 

616 self.pop() # object 

617 

618 br, name, val = self.tok() 

619 rec['TYPE'] = name 

620 self.pop() 

621 

622 br, name, val = self.tok() 

623 if val: 

624 rec['NAME'] = val 

625 self.pop() 

626 

627 # evtl. more strings after name, ignore them 

628 while not self.eof(): 

629 br, name, val = self.tok() 

630 if not val: 

631 break 

632 self.pop() 

633 

634 while not self.eof(): 

635 br, name, val = self.tok() 

636 if br == ')': 

637 self.pop() 

638 break 

639 self.pop() 

640 rec[name] = self.parse_item() 

641 

642 return rec 

643 

644 def parse_value(self): 

645 # e.g. (value Text "30000") 

646 

647 self.pop() # value 

648 self.pop() # type 

649 

650 val = self.parse_item() 

651 self.pop() # ) 

652 

653 return val 

654 

655 

656class PythonGenerator: 

657 unknownTypes = set() 

658 knownTypes = set() 

659 nameToNode = {} 

660 keyToNode = {} 

661 seen = set() 

662 metadata = {} 

663 py = [] 

664 

665 def __init__(self, nodes, version: str): 

666 self.nodes = nodes 

667 self.version = version 

668 

669 def build(self): 

670 self.knownTypes = set( 

671 node.name 

672 for node in self.nodes 

673 if node.T in {T_CLASS, T_ENUM, T_UNION} 

674 ) 

675 

676 self.nameToNode = {node.name: node for node in self.nodes} 

677 self.keyToNode = {node.key: node for node in self.nodes} 

678 

679 nodes = sorted(self.nodes, key=lambda n: n.name) 

680 self.make_nodes(nodes) 

681 

682 py = nl(self.py) 

683 

684 py = re.sub(r'(\n\w+: TypeAlias)', '\n\n\\1', py) 

685 py = re.sub(r'(\nclass )', '\n\n\\1', py) 

686 

687 py = nl([ 

688 PY_HEAD, 

689 *[f'{k}: TypeAlias = {v}' for k, v in sorted(STD_TYPES.items())], 

690 '', 

691 '', 

692 *[f'class {k}: ...' for k in sorted(self.unknownTypes)], 

693 '', 

694 '', 

695 py, 

696 '', 

697 '', 

698 'METADATA = {', 

699 json_dict_body(self.metadata, TAB), 

700 '}', 

701 '', 

702 

703 ]) 

704 

705 return py.replace('<VERSION>', self.version) 

706 

707 def make_nodes(self, nodes): 

708 for ts in T_UNION, T_CATEGORY, T_ENUM, T_CLASS: 

709 for node in nodes: 

710 if node.T == ts: 

711 self.make_node(node) 

712 

713 def make_node(self, node): 

714 if node.name not in self.seen: 

715 self.seen.add(node.name) 

716 fn = getattr(self, 'make_' + node.T) 

717 fn(node) 

718 self.make_metadata(node) 

719 

720 def make_union(self, node): 

721 items = sorted(set(self.get_type(a.type) for a in node.attributes)) 

722 typ = items[0] if len(items) == 1 else 'Union[' + comma(items) + ']' 

723 

724 self.py.append(f'{node.name}: TypeAlias = {typ}') 

725 self.py.append(self.get_docstring(node, '', False)) 

726 

727 def make_category(self, node): 

728 self.py.append(f'class {node.name}(Category):') 

729 self.py.append(self.get_docstring(node, TAB, True)) 

730 

731 def make_enum(self, node): 

732 self.py.append(f'class {node.name}(Enumeration):') 

733 self.py.append(self.get_docstring(node, TAB, True)) 

734 self.py.append('') 

735 self.py.append(f'{TAB}VALUES = {{') 

736 self.py.append(json_dict_body(node.values, TAB2)) 

737 self.py.append(f'{TAB}}}') 

738 

739 def make_class(self, node): 

740 node.attributes = node.attributes or [] 

741 

742 super_types = [] 

743 

744 for super_node in (node.supers or []): 

745 self.make_node(super_node) 

746 super_types.append(self.get_type(super_node.name, quoted=False)) 

747 

748 cls = f'class {node.name}' 

749 if super_types: 

750 cls += '(' + comma(super_types) + ')' 

751 else: 

752 cls += '(Object)' 

753 

754 self.py.append(cls + ':') 

755 self.py.append(self.get_docstring(node, TAB, True)) 

756 

757 if node.name == 'AA_REO': 

758 self.py.append('') 

759 self.py.append(f'{TAB}geom: str') 

760 

761 for a in sorted(node.attributes, key=lambda a: a.name): 

762 typ = self.get_type(a.type) 

763 if a.list: 

764 typ = f'list[{typ}]' 

765 if a.optional: 

766 typ = f'Optional[{typ}]' 

767 self.py.append('') 

768 self.py.append(f"{TAB}{to_name(a.name)}: {typ}") 

769 self.py.append(self.get_docstring(a, TAB, False)) 

770 

771 def make_metadata(self, node): 

772 d = { 

773 'kind': node.T, 

774 'name': node.name, 

775 'uid': node.uid or '', 

776 'key': node.key or '', 

777 'title': node.hname or '', 

778 } 

779 

780 if node.T == T_CLASS: 

781 d.update(self.make_class_metadata(node)) 

782 

783 self.metadata[node.name] = d 

784 

785 def make_class_metadata(self, node): 

786 d = {} 

787 

788 d['kind'] = 'object' if node.is_aa else 'struct' 

789 d['geom'] = 1 if node.is_reo else 0 

790 d['attributes'] = [] 

791 

792 d['supers'] = [sup.name for sup in node.supers] 

793 

794 for a in node.attributes: 

795 d['attributes'].append({ 

796 'name': a.name, 

797 'title': a.hname or '', 

798 'type': a.type, 

799 'list': 1 if a.list else 0, 

800 }) 

801 

802 return d 

803 

804 def get_type(self, typ, quoted=True): 

805 if not typ: 

806 return 'Any' 

807 

808 if hasattr(__builtins__, typ): 

809 return typ 

810 

811 if typ in STD_TYPES or typ in self.knownTypes: 

812 return quote(typ) if quoted else typ 

813 

814 self.unknownTypes.add(typ) 

815 return quote(typ) if quoted else typ 

816 

817 def get_docstring(self, node, indent, prepend_name): 

818 name = node.hname or node.name or ' ' 

819 

820 if node.doc: 

821 s = node.doc 

822 if prepend_name and name: 

823 s = name + '\n\n' + s 

824 else: 

825 s = name 

826 

827 if s.endswith('"'): 

828 s += ' ' 

829 return wrap_indent(Q3 + s + Q3, indent) 

830 

831 

832## 

833 

834 

835def popattr(obj, attr, default=None): 

836 return obj.__dict__.pop(attr, default) 

837 

838 

839def wrap_indent(s, indent): 

840 return nl( 

841 nl(indent + ln for ln in textwrap.wrap(p.strip(), WRAP_WIDTH)) 

842 for p in s.split('\n') 

843 ) 

844 

845 

846def quote(s): 

847 return "'" + (s or '') + "'" 

848 

849 

850_UID_DE_TRANS = { 

851 ord('ä'): 'ae', 

852 ord('ö'): 'oe', 

853 ord('ü'): 'ue', 

854 ord('ß'): 'ss', 

855 ord('Ä'): 'Ae', 

856 ord('Ö'): 'Oe', 

857 ord('Ü'): 'Ue', 

858} 

859 

860 

861def to_name(s): 

862 if not s: 

863 return '' 

864 s = str(s) 

865 if re.match(r'^[A-Za-z_][A-Za-z_0-9]*$', s): 

866 return s 

867 s = s.strip().translate(_UID_DE_TRANS) 

868 s = re.sub(r'\W+', '_', s).strip('_') 

869 if not s: 

870 return '_' 

871 if s[0].isdigit(): 

872 s = '_' + s 

873 return s 

874 

875 

876def json_dict_body(d, indent): 

877 js = json.dumps(d, indent=len(TAB), ensure_ascii=False).split('\n')[1:-1] 

878 ind = ' ' * (len(indent) - len(TAB)) 

879 return nl(ind + p for p in js) 

880 

881 

882comma = ', '.join 

883nl = '\n'.join 

884 

885## 

886 

887if __name__ == '__main__': 

888 main(sys.argv[1], *sys.argv[2:])