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 23:09 +0200

1import re 

2 

3from . import base 

4 

5 

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) 

16 

17 

18## 

19 

20 

21def _add_global_aliases(gen: base.Generator): 

22 """Add globals aliases. 

23 

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

27 

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 

40 

41 

42def _expand_aliases(gen: base.Generator): 

43 """Expand aliases. 

44 

45 Given t1 -> alias of t2, t2 -> alias of t3, establish t1 -> t3. 

46 """ 

47 

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 

58 

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 

66 

67 

68_type_scalars = [ 

69 'tArg', 

70 'tItem', 

71 'tKey', 

72 'tValue', 

73 'tTarget', 

74 'tOwner', 

75 'tReturn', 

76] 

77 

78_type_lists = [ 

79 'tArgs', 

80 'tItems', 

81 'tSupers', 

82] 

83 

84 

85def _resolve_aliases(gen: base.Generator): 

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

87 

88 new_type_dict = {} 

89 

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 

98 

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

100 if typ.uid in new_type_dict: 

101 continue 

102 

103 if typ.uid in gen.aliases: 

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

105 continue 

106 

107 dct = vars(typ) 

108 

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) 

117 

118 new_type_dict[typ.uid] = typ 

119 

120 gen.typeDict = new_type_dict 

121 

122 

123def _eval_expressions(gen: base.Generator): 

124 """Replace enum and constant values with literal values""" 

125 

126 def _get_type(name): 

127 if name in gen.aliases: 

128 name = gen.aliases[name] 

129 return gen.typeDict.get(name) 

130 

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] 

137 

138 if isinstance(value, dict): 

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

140 

141 # constant? 

142 typ = _get_type(value) 

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

144 return typ.constValue 

145 

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] 

151 

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

153 return None 

154 

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

160 

161 

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

164 

165 # don't need this for now 

166 

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

168 

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

190 

191 

192def _synthesize_ext_variant_types(gen: base.Generator): 

193 """Synthesize by-category variant types for ext objects 

194 

195 Example: 

196 

197 When we have 

198 

199 gws.ext.object.layer.qgis 

200 gws.ext.object.layer.wms 

201 gws.ext.object.layer.wfs 

202 

203 This will create a Variant `gws.ext.object.layer` with the members `qgis`, `wms`, `wfs` 

204 """ 

205 

206 variants = {} 

207 

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 

217 

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

226 

227 

228def _synthesize_ext_type_properties(gen: base.Generator): 

229 """Synthesize ``type`` properties for ext.config and ext.props objects""" 

230 

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 ) 

251 

252 

253def _make_props(gen: base.Generator): 

254 done = {} 

255 own_props_by_name = {} 

256 

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 

261 

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 

269 

270 props[name] = p 

271 

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

277 

278 props = {} 

279 

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

286 

287 if typ.name in own_props_by_name: 

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

289 

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

291 

292 done[typ.name] = props 

293 return props 

294 

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

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

297 _make(typ, []) 

298 

299 

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

314 

315 

316DOT = '.' 

317COMMA = ','