Coverage for gws-app/gws/spec/generator/normalizer.py: 8%
201 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1import re
3from . import base
6def normalize(gen: base.Generator):
7 _add_global_aliases(gen)
8 _expand_aliases(gen)
9 _resolve_aliases(gen)
10 _check_variants(gen)
11 _evaluate_defaults(gen)
12 # _synthesize_ext_configs_and_props(gen)
13 _synthesize_ext_variant_types(gen)
14 _synthesize_ext_type_properties(gen)
15 _check_undefined(gen)
16 _make_props(gen)
19##
22def _add_global_aliases(gen):
23 """Add globals aliases.
25 If we have `mod.GlobalName` and `mod.some.module.GlobalName`, and `mod.some.module`
26 is in `GLOBAL_MODULES`, the former should an alias for the latter.
27 """
29 for typ in gen.types.values():
30 if typ.name in gen.aliases:
31 continue
32 m = re.match(r'^gws\.([A-Z].*)$', typ.name)
33 if not m:
34 continue
35 for mod in base.GLOBAL_MODULES:
36 name = mod + DOT + m.group(1)
37 if name in gen.types:
38 base.log.debug(f'global alias {typ.name!r} => {name!r}')
39 gen.aliases[typ.name] = name
40 break
43def _expand_aliases(gen):
44 def _exp(target, stack):
45 if target in gen.types:
46 return target
47 if target in stack:
48 raise base.Error(f'circular alias {stack!r} => {target!r}')
49 if target in gen.aliases:
50 return _exp(gen.aliases[target], stack + [target])
51 if target.startswith(base.APP_NAME):
52 base.log.warning(f'unbound alias {target!r}')
53 return target
55 new_aliases = {}
56 for src, target in gen.aliases.items():
57 new_aliases[src] = _exp(target, [])
58 gen.aliases = new_aliases
61_type_scalars = [
62 'tItem',
63 'tKey',
64 'tValue',
65 'tTarget',
66 'tOwner',
67 'tReturn',
68]
70_type_lists = [
71 'tArgs',
72 'tItems',
73 'tSupers',
74]
77def _resolve_aliases(gen):
78 new_types = {}
80 def _rename_uid(uid):
81 if uid in gen.aliases:
82 new = gen.aliases[uid]
83 else:
84 new = COMMA.join(gen.aliases.get(s, s) for s in uid.split(COMMA))
85 if new != uid:
86 base.log.debug(f'resolved alias {uid!r} => {new!r}')
87 return new
89 for typ in gen.types.values():
90 if typ.uid in new_types:
91 continue
93 if typ.uid in gen.aliases:
94 base.log.debug(f'skip resolving {typ.uid} {typ.c}')
95 continue
97 dct = vars(typ)
99 for f in _type_scalars:
100 if f in dct:
101 dct[f] = _rename_uid(dct[f])
102 for f in _type_lists:
103 if f in dct:
104 dct[f] = [_rename_uid(s) for s in dct[f]]
105 if not typ.name:
106 typ.uid = _rename_uid(typ.uid)
108 new_types[typ.uid] = typ
110 gen.types = new_types
113def _evaluate_defaults(gen):
114 """Replace enum and constant values with literal values"""
116 def _get_type(name):
117 if name in gen.aliases:
118 name = gen.aliases[name]
119 return gen.types.get(name)
121 def _eval(base_type, val):
122 c, value = val
123 if c == base.C.LITERAL:
124 return value
125 if isinstance(value, list):
126 return [_eval(base_type, v) for v in value]
128 if isinstance(value, dict):
129 return {k: _eval(base_type, v) for k, v in value.items()}
131 # constant?
132 typ = _get_type(value)
133 if typ and typ.c == base.C.CONSTANT:
134 return typ.value
136 # enum?
137 obj_name, _, item = value.rpartition('.')
138 typ = _get_type(obj_name)
139 if typ and typ.c == base.C.ENUM and item in typ.enumValues:
140 return typ.enumValues[item]
142 base.log.warning(f'invalid expression {value!r} in {base_type.name!r}')
143 return None
145 for typ in gen.types.values():
146 val = getattr(typ, 'EVAL_DEFAULT', None)
147 if val:
148 typ.defaultValue = _eval(typ, val)
149 typ.hasDefault = True
150 base.log.debug(f'evaluated {val!r} => {typ.defaultValue!r}')
153def _check_variants(gen):
154 """Create Variant objects from VariantStubs
156 Example:
158 Given
160 Foo: VariantStub { items ['Type1', 'Type2'] }
161 Type1 { type Literal['first'] }
162 Type2 { type Literal['second'] }
164 we create a mapping { "type value" => "type name" }, e.g;
166 Foo: Variant {
167 tMembers {
168 first: Type1
169 second: Type2
170 }
171 }
172 """
174 for typ in gen.types.values():
175 if typ.c == base.C.VARIANT and not typ.tMembers:
176 members = {}
177 for tItem in typ.tItems:
178 try:
179 item_type = gen.types.get(tItem)
180 tag_property_type = gen.types.get(item_type.name + '.' + base.VARIANT_TAG)
181 tag_value_type = gen.types.get(tag_property_type.tValue)
182 if tag_value_type.c == base.C.LITERAL and len(tag_value_type.literalValues) == 1:
183 members[tag_value_type.literalValues[0]] = item_type.name
184 else:
185 raise ValueError()
186 except Exception:
187 raise base.Error(f'invalid Variant: {typ.pos!r}')
188 delattr(typ, 'tItems')
189 typ.tMembers = members
192def _synthesize_ext_configs_and_props(gen):
193 """Synthesize gws.ext.config... and gws.ext.props for ext objects that don't define them explicitly"""
195 upd = {}
197 existing_names = set(t.extName for t in gen.types.values() if t.extName)
199 for t in gen.types.values():
200 if t.extName.startswith(base.EXT_OBJECT_PREFIX):
201 for kind in ['config', 'props']:
202 parts = t.extName.split('.')
203 parts[2] = kind
204 ext_name = DOT.join(parts)
205 if ext_name not in existing_names:
206 new_type = gen.new_type(
207 base.C.CLASS,
208 doc=t.doc,
209 ident='_' + parts[-1],
210 # e.g. gws.ext.object.modelField.integer becomes gws.ext.props.modelField._integer
211 name=DOT.join(parts[:-1]) + '._' + parts[-1],
212 pos=t.pos,
213 tSupers=[base.DEFAULT_EXT_SUPERS[kind]],
214 extName=ext_name,
215 _SYNTHESIZED=True,
216 )
217 upd[new_type.uid] = new_type
219 gen.types.update(upd)
222def _synthesize_ext_type_properties(gen):
223 """Synthesize type properties for ext.config and ext.props objects"""
225 upd = {}
227 for t in gen.types.values():
228 if t.extName.startswith((base.EXT_CONFIG_PREFIX, base.EXT_PROPS_PREFIX)):
229 name = t.extName.rpartition(DOT)[-1]
230 literal = gen.new_type(base.C.LITERAL, literalValues=[name], pos=t.pos)
231 upd[literal.uid] = literal
233 nt = gen.new_type(
234 base.C.PROPERTY,
235 doc='object type',
236 ident=base.VARIANT_TAG,
237 name=t.name + DOT + base.VARIANT_TAG,
238 pos=t.pos,
239 defaultValue='default',
240 hasDefault=True,
241 tValue=literal.uid,
242 tOwner=t.uid,
243 )
244 upd[nt.uid] = nt
246 gen.types.update(upd)
249def _synthesize_ext_variant_types(gen):
250 """Synthesize by-category variant types for ext objects
252 Example:
254 When we have
256 gws.ext.object.layer.qgis
257 gws.ext.object.layer.wms
258 gws.ext.object.layer.wfs
260 This will create a Variant `gws.ext.object.layer` with the members `qgis`, `wms`, `wfs`
261 """
263 variants = {}
265 for typ in gen.types.values():
266 if typ.c == base.C.EXT:
267 target = gen.types.get(typ.tTarget)
268 if not target:
269 base.log.debug(f'not found {typ.tTarget!r} for {typ.extName!r}')
270 continue
271 target.extName = typ.extName
272 category, _, name = typ.extName.rpartition(DOT)
273 variants.setdefault(category, {})[name] = target.name
275 upd = {}
277 for name, members in variants.items():
278 variant_typ = gen.new_type(base.C.VARIANT, tMembers=members)
279 upd[variant_typ.uid] = variant_typ
280 alias_typ = gen.new_type(base.C.TYPE, name=name, extName=name, tTarget=variant_typ.uid)
281 upd[alias_typ.uid] = alias_typ
282 base.log.debug(f'created variant {variant_typ.uid!r} for {list(members.values())}')
284 gen.types.update(upd)
287def _make_props(gen):
288 done = {}
289 own_props_by_name = {}
291 for typ in gen.types.values():
292 if typ.c == base.C.PROPERTY:
293 obj_name, _, prop_name = typ.name.rpartition('.')
294 own_props_by_name.setdefault(obj_name, {})[prop_name] = typ
296 def _merge(typ, props, own_props):
297 for name, p in own_props.items():
298 if name in props:
299 # cannot weaken a required prop to optional
300 if p.hasDefault and not props[name].hasDefault:
301 p.defaultValue = None
302 p.hasDefault = False
304 props[name] = p
306 def _make(typ, stack):
307 if typ.name in done:
308 return done[typ.name]
309 if typ.name in stack:
310 raise base.Error(f'circular inheritance {stack!r}->{typ.name!r}')
312 props = {}
314 for sup in typ.tSupers:
315 super_typ = gen.types.get(sup)
316 if super_typ:
317 props.update(_make(super_typ, stack + [typ.name]))
318 elif sup.startswith(base.APP_NAME) and 'vendor' not in sup:
319 base.log.warning(f'unknown supertype {sup!r}')
321 if typ.name in own_props_by_name:
322 _merge(typ, props, own_props_by_name[typ.name])
324 typ.tProperties = {k: v.name for k, v in props.items()}
326 done[typ.name] = props
327 return props
329 for typ in gen.types.values():
330 if typ.c == base.C.CLASS:
331 _make(typ, [])
334def _check_undefined(gen):
335 for typ in gen.types.values():
336 if typ.c != base.C.UNDEFINED:
337 continue
338 if not typ.name.startswith(base.APP_NAME):
339 # foreign module
340 continue
341 if '.vendor.' in typ.name:
342 # vendor module
343 continue
344 if '._' in typ.name:
345 # private type
346 continue
347 base.log.warning(f'undefined type {typ.uid!r} in {typ.pos}')
350DOT = '.'
351COMMA = ','