Coverage for gws-app/gws/spec/generator/normalizer.py: 84%
178 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +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 _eval_expressions(gen)
11 # _synthesize_ext_configs_and_props(gen)
12 _synthesize_ext_variant_types(gen)
13 _synthesize_ext_type_properties(gen)
14 _check_undefined(gen)
15 _make_props(gen)
18##
21def _add_global_aliases(gen: base.Generator):
22 """Add globals aliases.
24 If we have `mod.GlobalName` and `mod.some.module.GlobalName`, and `mod.some.module`
25 is in `GLOBAL_MODULES`, the former should an alias for the latter.
26 """
28 for typ in gen.typeDict.values():
29 if typ.name in gen.aliases:
30 continue
31 m = re.match(r'^gws\.([A-Z].*)$', typ.name)
32 if not m:
33 continue
34 for mod in base.v.GLOBAL_MODULES:
35 name = mod + DOT + m.group(1)
36 if name in gen.typeDict:
37 base.log.debug(f'global alias {typ.name!r} => {name!r}')
38 gen.aliases[typ.name] = name
39 break
42def _expand_aliases(gen: base.Generator):
43 """Expand aliases.
45 Given t1 -> alias of t2, t2 -> alias of t3, establish t1 -> t3.
46 """
48 def _exp(target, stack):
49 if target in gen.typeDict:
50 return target
51 if target in stack:
52 raise base.GeneratorError(f'circular alias {stack!r} => {target!r}')
53 if target in gen.aliases:
54 return _exp(gen.aliases[target], stack + [target])
55 if target.startswith(base.v.APP_NAME):
56 base.log.warning(f'unbound alias {target!r}')
57 return target
59 new_aliases = {}
60 for src, target in gen.aliases.items():
61 new_target = _exp(target, [])
62 if new_target != target:
63 base.log.debug(f'alias expanded: {src!r} => {target!r} => {new_target!r}')
64 new_aliases[src] = new_target
65 gen.aliases = new_aliases
68_type_scalars = [
69 'tArg',
70 'tItem',
71 'tKey',
72 'tValue',
73 'tTarget',
74 'tOwner',
75 'tReturn',
76]
78_type_lists = [
79 'tArgs',
80 'tItems',
81 'tSupers',
82]
85def _resolve_aliases(gen: base.Generator):
86 """Replace references to aliases with their target type uids."""
88 new_type_dict = {}
90 def _rename_uid(uid):
91 if uid in gen.aliases:
92 new = gen.aliases[uid]
93 else:
94 new = COMMA.join(gen.aliases.get(s) or s for s in uid.split(COMMA))
95 if new != uid:
96 base.log.debug(f'resolved alias {uid!r} => {new!r}')
97 return new
99 for typ in gen.typeDict.values():
100 if typ.uid in new_type_dict:
101 continue
103 if typ.uid in gen.aliases:
104 base.log.debug(f'skip resolving {typ.uid} {typ.c}')
105 continue
107 dct = vars(typ)
109 for f in _type_scalars:
110 if f in dct:
111 dct[f] = _rename_uid(dct[f])
112 for f in _type_lists:
113 if f in dct:
114 dct[f] = [_rename_uid(s) for s in dct[f]]
115 if not typ.name:
116 typ.uid = _rename_uid(typ.uid)
118 new_type_dict[typ.uid] = typ
120 gen.typeDict = new_type_dict
123def _eval_expressions(gen: base.Generator):
124 """Replace enum and constant values with literal values"""
126 def _get_type(name):
127 if name in gen.aliases:
128 name = gen.aliases[name]
129 return gen.typeDict.get(name)
131 def _eval(base_type, val):
132 c, value = val
133 if c == base.c.LITERAL:
134 return value
135 if isinstance(value, list):
136 return [_eval(base_type, v) for v in value]
138 if isinstance(value, dict):
139 return {k: _eval(base_type, v) for k, v in value.items()}
141 # constant?
142 typ = _get_type(value)
143 if typ and typ.c == base.c.CONSTANT:
144 return typ.constValue
146 # enum?
147 obj_name, _, item = value.rpartition('.')
148 typ = _get_type(obj_name)
149 if typ and typ.c == base.c.ENUM and item in typ.enumValues:
150 return typ.enumValues[item]
152 base.log.warning(f'invalid expression {value!r} in {base_type.name!r}')
153 return None
155 for typ in gen.typeDict.values():
156 if typ.defaultExpression:
157 typ.defaultValue = _eval(typ, typ.defaultExpression)
158 typ.hasDefault = True
159 base.log.debug(f'evaluated {typ.defaultExpression!r} => {typ.defaultValue!r}')
162def _synthesize_ext_configs_and_props(gen: base.Generator):
163 """Synthesize gws.ext.config... and gws.ext.props for ext objects that don't define them explicitly"""
165 # don't need this for now
167 existing_names = set(t.extName for t in gen.typeDict.values() if t.extName)
169 for typ in list(gen.typeDict.values()):
170 if not typ.extName or not typ.extName.startswith(base.v.EXT_OBJECT_PREFIX):
171 continue
172 for kind in ['config', 'props']:
173 parts = typ.extName.split('.')
174 parts[2] = kind
175 ext_name = DOT.join(parts)
176 if ext_name in existing_names:
177 continue
178 new_typ = gen.add_type(
179 c=base.c.CLASS,
180 doc=typ.doc,
181 ident='_' + parts[-1],
182 # e.g. gws.ext.object.modelField.integer becomes gws.ext.props.modelField._integer
183 name=DOT.join(parts[:-1]) + '._' + parts[-1],
184 pos=typ.pos,
185 tSupers=[base.v.DEFAULT_EXT_SUPERS[kind]],
186 extName=ext_name,
187 _SYNTHESIZED=True,
188 )
189 base.log.debug(f'synthesized {new_typ.uid!r} from {typ.uid!r}')
192def _synthesize_ext_variant_types(gen: base.Generator):
193 """Synthesize by-category variant types for ext objects
195 Example:
197 When we have
199 gws.ext.object.layer.qgis
200 gws.ext.object.layer.wms
201 gws.ext.object.layer.wfs
203 This will create a Variant `gws.ext.object.layer` with the members `qgis`, `wms`, `wfs`
204 """
206 variants = {}
208 for typ in gen.typeDict.values():
209 if typ.c == base.c.EXT:
210 target_typ = gen.get_type(typ.tTarget)
211 if not target_typ:
212 base.log.debug(f'not found {typ.tTarget!r} for {typ.extName!r}')
213 continue
214 target_typ.extName = typ.extName
215 category, _, name = typ.extName.rpartition(DOT)
216 variants.setdefault(category, {})[name] = target_typ.uid
218 for name, members in variants.items():
219 variant_typ = gen.add_type(
220 c=base.c.VARIANT,
221 tMembers=members,
222 name=name,
223 extName=name,
224 )
225 base.log.debug(f'created variant {variant_typ.uid!r} for {list(members.values())}')
228def _synthesize_ext_type_properties(gen: base.Generator):
229 """Synthesize ``type`` properties for ext.config and ext.props objects"""
231 for typ in list(gen.typeDict.values()):
232 if not typ.extName or not typ.extName.startswith((base.v.EXT_CONFIG_PREFIX, base.v.EXT_PROPS_PREFIX)):
233 continue
234 name = typ.extName.rpartition(DOT)[-1]
235 literal_typ = gen.add_type(
236 c=base.c.LITERAL,
237 literalValues=[name],
238 pos=typ.pos,
239 )
240 gen.add_type(
241 c=base.c.PROPERTY,
242 doc='Object type.',
243 ident=base.v.VARIANT_TAG,
244 name=typ.name + DOT + base.v.VARIANT_TAG,
245 pos=typ.pos,
246 defaultValue='default',
247 hasDefault=True,
248 tValue=literal_typ.uid,
249 tOwner=typ.uid,
250 )
253def _make_props(gen: base.Generator):
254 done = {}
255 own_props_by_name = {}
257 for typ in gen.typeDict.values():
258 if typ.c == base.c.PROPERTY:
259 obj_name, _, prop_name = typ.name.rpartition('.')
260 own_props_by_name.setdefault(obj_name, {})[prop_name] = typ
262 def _merge(typ, props, own_props):
263 for name, p in own_props.items():
264 if name in props:
265 # cannot weaken a required prop to optional
266 if p.hasDefault and not props[name].hasDefault:
267 p.defaultValue = None
268 p.hasDefault = False
270 props[name] = p
272 def _make(typ, stack):
273 if typ.name in done:
274 return done[typ.name]
275 if typ.name in stack:
276 raise base.GeneratorError(f'circular inheritance {stack!r}->{typ.name!r}')
278 props = {}
280 for sup in typ.tSupers:
281 super_typ = gen.typeDict.get(sup)
282 if super_typ:
283 props.update(_make(super_typ, stack + [typ.name]))
284 elif sup.startswith(base.v.APP_NAME) and 'vendor' not in sup:
285 base.log.warning(f'unknown supertype {sup!r}')
287 if typ.name in own_props_by_name:
288 _merge(typ, props, own_props_by_name[typ.name])
290 typ.tProperties = {k: v.name for k, v in props.items()}
292 done[typ.name] = props
293 return props
295 for typ in gen.typeDict.values():
296 if typ.c == base.c.CLASS:
297 _make(typ, [])
300def _check_undefined(gen: base.Generator):
301 for typ in gen.typeDict.values():
302 if typ.c != base.c.UNDEFINED:
303 continue
304 if not typ.name.startswith(base.v.APP_NAME):
305 # foreign module
306 continue
307 if '.vendor.' in typ.name:
308 # vendor module
309 continue
310 if '._' in typ.name:
311 # private type
312 continue
313 base.log.warning(f'undefined type {typ.uid!r} in {typ.pos}')
316DOT = '.'
317COMMA = ','