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 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""Generate configuration references."""
3import re
4import json
6from . import base
8STRINGS = {}
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}
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}
42LIST_FORMAT = '<nobr>{}**[ ]**</nobr>'
43DEFAULT_FORMAT = ' _{}:_ {}.'
45LABELS = 'added|deprecated|changed'
48def create(gen: base.Generator, lang: str):
49 return _Creator(gen, lang).run()
52##
55class _Creator:
56 start_tid = 'gws.base.application.core.Config'
57 exclude_props = ['uid', 'access', 'type']
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 = []
66 def run(self):
67 self.queue = [self.start_tid]
68 self.blocks = []
69 done = set()
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)
78 return nl(b[-1] for b in sorted(self.blocks))
80 def process(self, tid):
81 typ = self.gen.require_type(tid)
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))])
87 if typ.c == base.c.TYPE:
88 self.blocks.append([2, tid.lower(), nl(self.process_type(tid))])
90 if typ.c == base.c.ENUM:
91 self.blocks.append([3, tid.lower(), nl(self.process_enum(tid))])
93 if typ.c == base.c.VARIANT:
94 self.blocks.append([4, tid.lower(), nl(self.process_variant(tid))])
96 if typ.c == base.c.LIST:
97 self.queue.append(typ.tItem)
99 def process_class(self, tid):
100 typ = self.gen.require_type(tid)
102 yield header('object', tid)
103 yield subhead(self.strings['category_object'], self.docstring_as_header(tid))
105 rows = {False: [], True: []}
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 )
120 yield table(
121 [
122 self.strings['head_property'],
123 self.strings['head_type'],
124 '',
125 ],
126 rows[False] + rows[True],
127 )
129 def process_enum(self, tid):
130 typ = self.gen.require_type(tid)
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 )
142 def process_variant(self, tid):
143 typ = self.gen.require_type(tid)
145 yield header('variant', tid)
146 yield subhead(self.strings['category_variant'], self.strings['head_variant'])
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)])
153 yield table(
154 [
155 self.strings['head_type'],
156 '',
157 ],
158 rows,
159 )
161 def process_type(self, tid):
162 yield header('type', tid)
163 yield subhead(self.strings['category_type'], self.docstring_as_header(tid))
165 def type_string(self, tid):
166 typ = self.gen.require_type(tid)
168 if typ.c in {base.c.CLASS, base.c.TYPE, base.c.ENUM, base.c.VARIANT}:
169 return link(tid, as_typename(tid))
171 if typ.c == base.c.DICT:
172 return as_code('dict')
174 if typ.c == base.c.LIST:
175 return LIST_FORMAT.format(self.type_string(typ.tItem))
177 if typ.c == base.c.ATOM:
178 return as_typename(tid)
180 if typ.c == base.c.LITERAL:
181 return r' | '.join(as_literal(s) for s in typ.literalValues)
183 return typ.c
185 def default_string(self, tid):
186 typ = self.gen.require_type(tid)
187 val = typ.tValue
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)
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)
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
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
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)
219 dev_label = ''
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}}'
229 local_text = local_text or en_text
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)
237 dflt = self.default_string(tid)
238 if dflt:
239 text += DEFAULT_FORMAT.format(self.strings['head_default'], dflt)
241 return [text, label, dev_label]
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
254def as_literal(s):
255 v = json.dumps(s, ensure_ascii=False)
256 return f'`{v}`{{.configref_literal}}'
259def as_typename(s):
260 return f'`{s}`{{.configref_typename}}'
263def as_category(s):
264 return f'`{s}`{{.configref_category}}'
267def as_propname(s):
268 return f'`{s}`{{.configref_propname}}'
271def as_required(s):
272 return f'`{s}`{{.configref_required}}'
275def as_code(s):
276 return f'`{s}`'
279def header(cat, tid):
280 return f'\n## <span class="configref_category_{cat}"></span>{tid} :{tid}\n'
283def subhead(category, text):
284 # return as_category(category) + ' ' + text + '\n'
285 return text + '\n'
288def link(target, text):
289 return f'[{text}](../{target})'
292def first_line(s):
293 return (s or '').strip().split('\n')[0].strip()
296def table(heads, rows):
297 widths = [len(h) for h in heads]
299 for r in rows:
300 widths = [max(a, b) for a, b in zip(widths, [len(str(s)) for s in r])]
302 def field(n, v):
303 return str(v).ljust(widths[n])
305 def row(r):
306 return ' | '.join(field(n, v) for n, v in enumerate(r))
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'
313def escape(s, quote=True):
314 s = s.replace('&', '&')
315 s = s.replace('<', '<')
316 s = s.replace('>', '>')
317 if quote:
318 s = s.replace('"', '"')
319 return s
322nl = '\n'.join