Coverage for gws-app/gws/spec/generator/configref.py: 92%

175 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 22:59 +0200

1"""Generate configuration references.""" 

2 

3import re 

4import json 

5 

6from . import base 

7 

8STRINGS = {} 

9 

10STRINGS['en'] = { 

11 'head_property': 'property', 

12 'head_variant': 'one of the following objects:', 

13 'head_type': 'type', 

14 'head_default': 'default', 

15 'head_value': 'value', 

16 'head_member': 'class', 

17 'category_variant': 'variant', 

18 'category_object': 'obj', 

19 'category_enum': 'enum', 

20 'category_type': 'type', 

21 'label_added': 'added', 

22 'label_deprecated': 'deprecated', 

23 'label_changed': 'changed', 

24} 

25 

26STRINGS['de'] = { 

27 'head_property': 'Eigenschaft', 

28 'head_variant': 'Eines der folgenden Objekte:', 

29 'head_type': 'Typ', 

30 'head_default': 'Default', 

31 'head_value': 'Wert', 

32 'head_member': 'Objekt', 

33 'category_variant': 'variant', 

34 'category_object': 'obj', 

35 'category_enum': 'enum', 

36 'category_type': 'type', 

37 'label_added': 'neu', 

38 'label_deprecated': 'veraltet', 

39 'label_changed': 'geändert', 

40} 

41 

42LIST_FORMAT = '<nobr>{}**[ ]**</nobr>' 

43DEFAULT_FORMAT = ' _{}:_ {}.' 

44 

45LABELS = 'added|deprecated|changed' 

46 

47 

48def create(gen: base.Generator, lang: str): 

49 return _Creator(gen, lang).run() 

50 

51 

52## 

53 

54 

55class _Creator: 

56 start_tid = 'gws.base.application.core.Config' 

57 exclude_props = ['uid', 'access', 'type'] 

58 

59 def __init__(self, gen: base.Generator, lang: str): 

60 self.gen = gen 

61 self.lang = lang 

62 self.strings = STRINGS[lang] 

63 self.queue = [] 

64 self.blocks = [] 

65 

66 def run(self): 

67 self.queue = [self.start_tid] 

68 self.blocks = [] 

69 done = set() 

70 

71 while self.queue: 

72 tid = self.queue.pop(0) 

73 if tid in done: 

74 continue 

75 done.add(tid) 

76 self.process(tid) 

77 

78 return nl(b[-1] for b in sorted(self.blocks)) 

79 

80 def process(self, tid): 

81 typ = self.gen.require_type(tid) 

82 

83 if typ.c == base.c.CLASS: 

84 key = 0 if tid == self.start_tid else 1 

85 self.blocks.append([key, tid.lower(), nl(self.process_class(tid))]) 

86 

87 if typ.c == base.c.TYPE: 

88 self.blocks.append([2, tid.lower(), nl(self.process_type(tid))]) 

89 

90 if typ.c == base.c.ENUM: 

91 self.blocks.append([3, tid.lower(), nl(self.process_enum(tid))]) 

92 

93 if typ.c == base.c.VARIANT: 

94 self.blocks.append([4, tid.lower(), nl(self.process_variant(tid))]) 

95 

96 if typ.c == base.c.LIST: 

97 self.queue.append(typ.tItem) 

98 

99 def process_class(self, tid): 

100 typ = self.gen.require_type(tid) 

101 

102 yield header('object', tid) 

103 yield subhead(self.strings['category_object'], self.docstring_as_header(tid)) 

104 

105 rows = {False: [], True: []} 

106 

107 for prop_name, prop_tid in sorted(typ.tProperties.items()): 

108 if prop_name in self.exclude_props: 

109 continue 

110 prop_typ = self.gen.require_type(prop_tid) 

111 self.queue.append(prop_typ.tValue) 

112 rows[prop_typ.hasDefault].append( 

113 [ 

114 as_propname(prop_name) if prop_typ.hasDefault else as_required(prop_name), 

115 self.type_string(prop_typ.tValue), 

116 self.docstring_as_cell(prop_tid), 

117 ] 

118 ) 

119 

120 yield table( 

121 [ 

122 self.strings['head_property'], 

123 self.strings['head_type'], 

124 '', 

125 ], 

126 rows[False] + rows[True], 

127 ) 

128 

129 def process_enum(self, tid): 

130 typ = self.gen.require_type(tid) 

131 

132 yield header('enum', tid) 

133 yield subhead(self.strings['category_enum'], self.docstring_as_header(tid)) 

134 yield table( 

135 [ 

136 self.strings['head_value'], 

137 '', 

138 ], 

139 [[as_literal(key), self.docstring_as_cell(tid, key)] for key in typ.enumValues], 

140 ) 

141 

142 def process_variant(self, tid): 

143 typ = self.gen.require_type(tid) 

144 

145 yield header('variant', tid) 

146 yield subhead(self.strings['category_variant'], self.strings['head_variant']) 

147 

148 rows = [] 

149 for member_name, member_tid in sorted(typ.tMembers.items()): 

150 self.queue.append(member_tid) 

151 rows.append([as_literal(member_name), self.type_string(member_tid)]) 

152 

153 yield table( 

154 [ 

155 self.strings['head_type'], 

156 '', 

157 ], 

158 rows, 

159 ) 

160 

161 def process_type(self, tid): 

162 yield header('type', tid) 

163 yield subhead(self.strings['category_type'], self.docstring_as_header(tid)) 

164 

165 def type_string(self, tid): 

166 typ = self.gen.require_type(tid) 

167 

168 if typ.c in {base.c.CLASS, base.c.TYPE, base.c.ENUM, base.c.VARIANT}: 

169 return link(tid, as_typename(tid)) 

170 

171 if typ.c == base.c.DICT: 

172 return as_code('dict') 

173 

174 if typ.c == base.c.LIST: 

175 return LIST_FORMAT.format(self.type_string(typ.tItem)) 

176 

177 if typ.c == base.c.ATOM: 

178 return as_typename(tid) 

179 

180 if typ.c == base.c.LITERAL: 

181 return r' | '.join(as_literal(s) for s in typ.literalValues) 

182 

183 return typ.c 

184 

185 def default_string(self, tid): 

186 typ = self.gen.require_type(tid) 

187 val = typ.tValue 

188 

189 if val in self.gen.typeDict and self.gen.typeDict[val].c == base.c.LITERAL: 

190 return '' 

191 if not typ.hasDefault: 

192 return '' 

193 v = typ.defaultValue 

194 if v is None or v == '': 

195 return '' 

196 return as_literal(v) 

197 

198 def docstring_as_header(self, tid, enum_value=None): 

199 text, label, dev_label = self.docstring_elements(tid, enum_value) 

200 lines = text.split('\n') 

201 lines[0] += label + dev_label 

202 return '\n\n'.join(lines) 

203 

204 def docstring_as_cell(self, tid, enum_value=None): 

205 text, label, dev_label = self.docstring_elements(tid, enum_value) 

206 return re.sub(r'\s+', ' ', text) + label + dev_label 

207 

208 def docstring_elements(self, tid, enum_value=None): 

209 # get the original (spec) docstring 

210 typ = self.gen.require_type(tid) 

211 en_text = typ.enumDocs.get(enum_value) if enum_value else typ.doc 

212 

213 # try the translated (from strings) docstring 

214 key = tid 

215 if enum_value: 

216 key += '.' + enum_value 

217 local_text = self.gen.strings[self.lang].get(key) 

218 

219 dev_label = '' 

220 

221 if en_text and not local_text and self.lang != 'en': 

222 # translation missing: use the english docstring and warn 

223 base.log.debug(f'missing {self.lang} translation for {key!r}') 

224 dev_label = f'`??? {key}`{{.configref_dev_missing_translation}}' 

225 local_text = self.gen.strings['en'].get(key) 

226 else: 

227 dev_label = f'`{key}`{{.configref_dev_uid}}' 

228 

229 local_text = local_text or en_text 

230 

231 # process a label, like "foobar" 

232 # it might be missing in a translation, but present in the original (spec) docstring 

233 text, label = self.extract_label(local_text) 

234 if not label and en_text != local_text: 

235 _, label = self.extract_label(en_text) 

236 

237 dflt = self.default_string(tid) 

238 if dflt: 

239 text += DEFAULT_FORMAT.format(self.strings['head_default'], dflt) 

240 

241 return [text, label, dev_label] 

242 

243 def extract_label(self, text): 

244 m = re.match(rf'(.+?)\(({LABELS}) in (\d[\d.]+)\)$', text) 

245 if not m: 

246 return text, '' 

247 kind = m.group(2).strip() 

248 name = self.strings[f'label_{kind}'] 

249 version = m.group(3) 

250 label = f'`{name}: {version}`{{.configref_label_{kind}}}' 

251 return m.group(1).strip(), label 

252 

253 

254def as_literal(s): 

255 v = json.dumps(s, ensure_ascii=False) 

256 return f'`{v}`{{.configref_literal}}' 

257 

258 

259def as_typename(s): 

260 return f'`{s}`{{.configref_typename}}' 

261 

262 

263def as_category(s): 

264 return f'`{s}`{{.configref_category}}' 

265 

266 

267def as_propname(s): 

268 return f'`{s}`{{.configref_propname}}' 

269 

270 

271def as_required(s): 

272 return f'`{s}`{{.configref_required}}' 

273 

274 

275def as_code(s): 

276 return f'`{s}`' 

277 

278 

279def header(cat, tid): 

280 return f'\n## <span class="configref_category_{cat}"></span>{tid} :{tid}\n' 

281 

282 

283def subhead(category, text): 

284 # return as_category(category) + ' ' + text + '\n' 

285 return text + '\n' 

286 

287 

288def link(target, text): 

289 return f'[{text}](../{target})' 

290 

291 

292def first_line(s): 

293 return (s or '').strip().split('\n')[0].strip() 

294 

295 

296def table(heads, rows): 

297 widths = [len(h) for h in heads] 

298 

299 for r in rows: 

300 widths = [max(a, b) for a, b in zip(widths, [len(str(s)) for s in r])] 

301 

302 def field(n, v): 

303 return str(v).ljust(widths[n]) 

304 

305 def row(r): 

306 return ' | '.join(field(n, v) for n, v in enumerate(r)) 

307 

308 out = [row(heads), '', *[row(r) for r in rows]] 

309 out[1] = '-' * len(out[0]) 

310 return '\n'.join(f'| {s} |' for s in out) + '\n' 

311 

312 

313def escape(s, quote=True): 

314 s = s.replace('&', '&amp;') 

315 s = s.replace('<', '&lt;') 

316 s = s.replace('>', '&gt;') 

317 if quote: 

318 s = s.replace('"', '&quot;') 

319 return s 

320 

321 

322nl = '\n'.join