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

1"""Normalize parsed types: resolve aliases, evaluate defaults and complete classes.""" 

2 

3import re 

4 

5from . import base 

6 

7 

8def normalize(gen: base.Generator): 

9 """Normalize the parsed types in place. 

10 

11 The steps are: 

12 

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``. 

21 

22 Args: 

23 gen: Generator state. 

24 

25 Raises: 

26 ``GeneratorError``: On circular aliases or circular inheritance. 

27 """ 

28 

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) 

38 

39 

40## 

41 

42 

43def _add_global_aliases(gen: base.Generator): 

44 """Make ``gws.Name`` an alias of ``<global module>.Name`` for the modules in ``GLOBAL_MODULES``.""" 

45 

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 

58 

59 

60def _expand_aliases(gen: base.Generator): 

61 """Expand alias chains, so that each alias points to its final target.""" 

62 

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 

73 

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 

81 

82 

83_type_scalars = [ 

84 'tArg', 

85 'tItem', 

86 'tKey', 

87 'tValue', 

88 'tTarget', 

89 'tOwner', 

90 'tReturn', 

91] 

92 

93_type_lists = [ 

94 'tArgs', 

95 'tItems', 

96 'tSupers', 

97] 

98 

99 

100def _resolve_aliases(gen: base.Generator): 

101 """Replace references to aliases with their target type uids.""" 

102 

103 new_type_dict = {} 

104 

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 

113 

114 for typ in gen.typeDict.values(): 

115 if typ.uid in new_type_dict: 

116 continue 

117 

118 if typ.uid in gen.aliases: 

119 base.log.debug(f'skip resolving {typ.uid} {typ.c}') 

120 continue 

121 

122 dct = vars(typ) 

123 

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) 

132 

133 new_type_dict[typ.uid] = typ 

134 

135 gen.typeDict = new_type_dict 

136 

137 

138def _eval_expressions(gen: base.Generator): 

139 """Replace constant and enum references in default expressions with their values.""" 

140 

141 def _get_type(name): 

142 if name in gen.aliases: 

143 name = gen.aliases[name] 

144 return gen.typeDict.get(name) 

145 

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] 

152 

153 if isinstance(value, dict): 

154 return {k: _eval(base_type, v) for k, v in value.items()} 

155 

156 # constant? 

157 typ = _get_type(value) 

158 if typ and typ.c == base.c.CONSTANT: 

159 return typ.constValue 

160 

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] 

166 

167 base.log.warning(f'invalid expression {value!r} in {base_type.name!r}') 

168 return None 

169 

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}') 

175 

176 

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).""" 

179 

180 # don't need this for now 

181 

182 existing_names = set(t.extName for t in gen.typeDict.values() if t.extName) 

183 

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}') 

205 

206 

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.""" 

209 

210 variants = {} 

211 

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 

221 

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())}') 

230 

231 

232def _synthesize_ext_type_properties(gen: base.Generator): 

233 """Add a ``type`` property to ``gws.ext.config`` and ``gws.ext.props`` classes.""" 

234 

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 ) 

255 

256 

257def _make_props(gen: base.Generator): 

258 """Set ``tProperties`` of each class to its own and inherited properties.""" 

259 

260 done = {} 

261 own_props_by_name = {} 

262 

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 

267 

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 

275 

276 props[name] = p 

277 

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}') 

283 

284 props = {} 

285 

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}') 

292 

293 if typ.name in own_props_by_name: 

294 _merge(typ, props, own_props_by_name[typ.name]) 

295 

296 typ.tProperties = {k: v.name for k, v in sorted(props.items())} 

297 

298 done[typ.name] = props 

299 return props 

300 

301 for typ in gen.typeDict.values(): 

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

303 _make(typ, []) 

304 

305 

306def _check_undefined(gen: base.Generator): 

307 """Warn about undefined ``gws`` types, except vendor and private ones.""" 

308 

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}') 

322 

323 

324DOT = '.' 

325COMMA = ','