Coverage for gws-app/gws/spec/generator/normalizer.py: 83%
178 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Normalize parsed types: resolve aliases, evaluate defaults and complete classes."""
3import re
5from . import base
8def normalize(gen: base.Generator):
9 """Normalize the parsed types in place.
11 The steps are:
13 - add aliases ``gws.Name`` for names defined in the modules listed in ``GLOBAL_MODULES``,
14 - expand alias chains and replace alias references with the target uids,
15 - evaluate default expressions (constants and enum members) to values,
16 - create a ``VARIANT`` type for each ``gws.ext`` category,
17 e.g. ``gws.ext.object.layer`` with members ``qgis``, ``wms`` and so on,
18 - add a ``type`` property to each ``gws.ext.config`` and ``gws.ext.props`` class,
19 - warn about undefined ``gws`` types,
20 - collect the own and inherited properties of each class into ``tProperties``.
22 Args:
23 gen: Generator state.
25 Raises:
26 ``GeneratorError``: On circular aliases or circular inheritance.
27 """
29 _add_global_aliases(gen)
30 _expand_aliases(gen)
31 _resolve_aliases(gen)
32 _eval_expressions(gen)
33 # _synthesize_ext_configs_and_props(gen)
34 _synthesize_ext_variant_types(gen)
35 _synthesize_ext_type_properties(gen)
36 _check_undefined(gen)
37 _make_props(gen)
40##
43def _add_global_aliases(gen: base.Generator):
44 """Make ``gws.Name`` an alias of ``<global module>.Name`` for the modules in ``GLOBAL_MODULES``."""
46 for typ in gen.typeDict.values():
47 if typ.name in gen.aliases:
48 continue
49 m = re.match(r'^gws\.([A-Z].*)$', typ.name)
50 if not m:
51 continue
52 for mod in base.v.GLOBAL_MODULES:
53 name = mod + DOT + m.group(1)
54 if name in gen.typeDict:
55 base.log.debug(f'global alias {typ.name!r} => {name!r}')
56 gen.aliases[typ.name] = name
57 break
60def _expand_aliases(gen: base.Generator):
61 """Expand alias chains, so that each alias points to its final target."""
63 def _exp(target, stack):
64 if target in gen.typeDict:
65 return target
66 if target in stack:
67 raise base.GeneratorError(f'circular alias {stack!r} => {target!r}')
68 if target in gen.aliases:
69 return _exp(gen.aliases[target], stack + [target])
70 if target.startswith(base.v.APP_NAME):
71 base.log.warning(f'unbound alias {target!r}')
72 return target
74 new_aliases = {}
75 for src, target in gen.aliases.items():
76 new_target = _exp(target, [])
77 if new_target != target:
78 base.log.debug(f'alias expanded: {src!r} => {target!r} => {new_target!r}')
79 new_aliases[src] = new_target
80 gen.aliases = new_aliases
83_type_scalars = [
84 'tArg',
85 'tItem',
86 'tKey',
87 'tValue',
88 'tTarget',
89 'tOwner',
90 'tReturn',
91]
93_type_lists = [
94 'tArgs',
95 'tItems',
96 'tSupers',
97]
100def _resolve_aliases(gen: base.Generator):
101 """Replace references to aliases with their target type uids."""
103 new_type_dict = {}
105 def _rename_uid(uid):
106 if uid in gen.aliases:
107 new = gen.aliases[uid]
108 else:
109 new = COMMA.join(gen.aliases.get(s) or s for s in uid.split(COMMA))
110 if new != uid:
111 base.log.debug(f'resolved alias {uid!r} => {new!r}')
112 return new
114 for typ in gen.typeDict.values():
115 if typ.uid in new_type_dict:
116 continue
118 if typ.uid in gen.aliases:
119 base.log.debug(f'skip resolving {typ.uid} {typ.c}')
120 continue
122 dct = vars(typ)
124 for f in _type_scalars:
125 if f in dct:
126 dct[f] = _rename_uid(dct[f])
127 for f in _type_lists:
128 if f in dct:
129 dct[f] = [_rename_uid(s) for s in dct[f]]
130 if not typ.name:
131 typ.uid = _rename_uid(typ.uid)
133 new_type_dict[typ.uid] = typ
135 gen.typeDict = new_type_dict
138def _eval_expressions(gen: base.Generator):
139 """Replace constant and enum references in default expressions with their values."""
141 def _get_type(name):
142 if name in gen.aliases:
143 name = gen.aliases[name]
144 return gen.typeDict.get(name)
146 def _eval(base_type, val):
147 c, value = val
148 if c == base.c.LITERAL:
149 return value
150 if isinstance(value, list):
151 return [_eval(base_type, v) for v in value]
153 if isinstance(value, dict):
154 return {k: _eval(base_type, v) for k, v in value.items()}
156 # constant?
157 typ = _get_type(value)
158 if typ and typ.c == base.c.CONSTANT:
159 return typ.constValue
161 # enum?
162 obj_name, _, item = value.rpartition('.')
163 typ = _get_type(obj_name)
164 if typ and typ.c == base.c.ENUM and item in typ.enumValues:
165 return typ.enumValues[item]
167 base.log.warning(f'invalid expression {value!r} in {base_type.name!r}')
168 return None
170 for typ in gen.typeDict.values():
171 if typ.defaultExpression:
172 typ.defaultValue = _eval(typ, typ.defaultExpression)
173 typ.hasDefault = True
174 base.log.debug(f'evaluated {typ.defaultExpression!r} => {typ.defaultValue!r}')
177def _synthesize_ext_configs_and_props(gen: base.Generator):
178 """Create ``gws.ext.config`` and ``gws.ext.props`` classes for ext objects that lack them (not used)."""
180 # don't need this for now
182 existing_names = set(t.extName for t in gen.typeDict.values() if t.extName)
184 for typ in list(gen.typeDict.values()):
185 if not typ.extName or not typ.extName.startswith(base.v.EXT_OBJECT_PREFIX):
186 continue
187 for kind in ['config', 'props']:
188 parts = typ.extName.split('.')
189 parts[2] = kind
190 ext_name = DOT.join(parts)
191 if ext_name in existing_names:
192 continue
193 new_typ = gen.add_type(
194 c=base.c.CLASS,
195 doc=typ.doc,
196 ident='_' + parts[-1],
197 # e.g. gws.ext.object.modelField.integer becomes gws.ext.props.modelField._integer
198 name=DOT.join(parts[:-1]) + '._' + parts[-1],
199 pos=typ.pos,
200 tSupers=[base.v.DEFAULT_EXT_SUPERS[kind]],
201 extName=ext_name,
202 _SYNTHESIZED=True,
203 )
204 base.log.debug(f'synthesized {new_typ.uid!r} from {typ.uid!r}')
207def _synthesize_ext_variant_types(gen: base.Generator):
208 """Create a ``VARIANT`` type for each ``gws.ext`` category and set ``extName`` on the target classes."""
210 variants = {}
212 for typ in gen.typeDict.values():
213 if typ.c == base.c.EXT:
214 target_typ = gen.get_type(typ.tTarget)
215 if not target_typ:
216 base.log.debug(f'not found {typ.tTarget!r} for {typ.extName!r}')
217 continue
218 target_typ.extName = typ.extName
219 category, _, name = typ.extName.rpartition(DOT)
220 variants.setdefault(category, {})[name] = target_typ.uid
222 for name, members in variants.items():
223 variant_typ = gen.add_type(
224 c=base.c.VARIANT,
225 tMembers=members,
226 name=name,
227 extName=name,
228 )
229 base.log.debug(f'created variant {variant_typ.uid!r} for {list(members.values())}')
232def _synthesize_ext_type_properties(gen: base.Generator):
233 """Add a ``type`` property to ``gws.ext.config`` and ``gws.ext.props`` classes."""
235 for typ in list(gen.typeDict.values()):
236 if not typ.extName or not typ.extName.startswith((base.v.EXT_CONFIG_PREFIX, base.v.EXT_PROPS_PREFIX)):
237 continue
238 name = typ.extName.rpartition(DOT)[-1]
239 literal_typ = gen.add_type(
240 c=base.c.LITERAL,
241 literalValues=[name],
242 pos=typ.pos,
243 )
244 gen.add_type(
245 c=base.c.PROPERTY,
246 doc='Object type.',
247 ident=base.v.VARIANT_TAG,
248 name=typ.name + DOT + base.v.VARIANT_TAG,
249 pos=typ.pos,
250 defaultValue='default',
251 hasDefault=True,
252 tValue=literal_typ.uid,
253 tOwner=typ.uid,
254 )
257def _make_props(gen: base.Generator):
258 """Set ``tProperties`` of each class to its own and inherited properties."""
260 done = {}
261 own_props_by_name = {}
263 for typ in gen.typeDict.values():
264 if typ.c == base.c.PROPERTY:
265 obj_name, _, prop_name = typ.name.rpartition('.')
266 own_props_by_name.setdefault(obj_name, {})[prop_name] = typ
268 def _merge(typ, props, own_props):
269 for name, p in own_props.items():
270 if name in props:
271 # cannot weaken a required prop to optional
272 if p.hasDefault and not props[name].hasDefault:
273 p.defaultValue = None
274 p.hasDefault = False
276 props[name] = p
278 def _make(typ, stack):
279 if typ.name in done:
280 return done[typ.name]
281 if typ.name in stack:
282 raise base.GeneratorError(f'circular inheritance {stack!r}->{typ.name!r}')
284 props = {}
286 for sup in typ.tSupers:
287 super_typ = gen.typeDict.get(sup)
288 if super_typ:
289 props.update(_make(super_typ, stack + [typ.name]))
290 elif sup.startswith(base.v.APP_NAME) and 'vendor' not in sup:
291 base.log.warning(f'unknown supertype {sup!r}')
293 if typ.name in own_props_by_name:
294 _merge(typ, props, own_props_by_name[typ.name])
296 typ.tProperties = {k: v.name for k, v in sorted(props.items())}
298 done[typ.name] = props
299 return props
301 for typ in gen.typeDict.values():
302 if typ.c == base.c.CLASS:
303 _make(typ, [])
306def _check_undefined(gen: base.Generator):
307 """Warn about undefined ``gws`` types, except vendor and private ones."""
309 for typ in gen.typeDict.values():
310 if typ.c != base.c.UNDEFINED:
311 continue
312 if not typ.name.startswith(base.v.APP_NAME):
313 # foreign module
314 continue
315 if '.vendor.' in typ.name:
316 # vendor module
317 continue
318 if '._' in typ.name:
319 # private type
320 continue
321 base.log.warning(f'undefined type {typ.uid!r} in {typ.pos}')
324DOT = '.'
325COMMA = ','