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

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 _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) 

17 

18 

19## 

20 

21 

22def _add_global_aliases(gen): 

23 """Add globals aliases. 

24 

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

28 

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 

41 

42 

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 

54 

55 new_aliases = {} 

56 for src, target in gen.aliases.items(): 

57 new_aliases[src] = _exp(target, []) 

58 gen.aliases = new_aliases 

59 

60 

61_type_scalars = [ 

62 'tItem', 

63 'tKey', 

64 'tValue', 

65 'tTarget', 

66 'tOwner', 

67 'tReturn', 

68] 

69 

70_type_lists = [ 

71 'tArgs', 

72 'tItems', 

73 'tSupers', 

74] 

75 

76 

77def _resolve_aliases(gen): 

78 new_types = {} 

79 

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 

88 

89 for typ in gen.types.values(): 

90 if typ.uid in new_types: 

91 continue 

92 

93 if typ.uid in gen.aliases: 

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

95 continue 

96 

97 dct = vars(typ) 

98 

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) 

107 

108 new_types[typ.uid] = typ 

109 

110 gen.types = new_types 

111 

112 

113def _evaluate_defaults(gen): 

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

115 

116 def _get_type(name): 

117 if name in gen.aliases: 

118 name = gen.aliases[name] 

119 return gen.types.get(name) 

120 

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] 

127 

128 if isinstance(value, dict): 

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

130 

131 # constant? 

132 typ = _get_type(value) 

133 if typ and typ.c == base.C.CONSTANT: 

134 return typ.value 

135 

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] 

141 

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

143 return None 

144 

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

151 

152 

153def _check_variants(gen): 

154 """Create Variant objects from VariantStubs 

155 

156 Example: 

157 

158 Given 

159 

160 Foo: VariantStub { items ['Type1', 'Type2'] } 

161 Type1 { type Literal['first'] } 

162 Type2 { type Literal['second'] } 

163 

164 we create a mapping { "type value" => "type name" }, e.g; 

165 

166 Foo: Variant { 

167 tMembers { 

168 first: Type1 

169 second: Type2 

170 } 

171 } 

172 """ 

173 

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 

190 

191 

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

194 

195 upd = {} 

196 

197 existing_names = set(t.extName for t in gen.types.values() if t.extName) 

198 

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 

218 

219 gen.types.update(upd) 

220 

221 

222def _synthesize_ext_type_properties(gen): 

223 """Synthesize type properties for ext.config and ext.props objects""" 

224 

225 upd = {} 

226 

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 

232 

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 

245 

246 gen.types.update(upd) 

247 

248 

249def _synthesize_ext_variant_types(gen): 

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

251 

252 Example: 

253 

254 When we have 

255 

256 gws.ext.object.layer.qgis 

257 gws.ext.object.layer.wms 

258 gws.ext.object.layer.wfs 

259 

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

261 """ 

262 

263 variants = {} 

264 

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 

274 

275 upd = {} 

276 

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

283 

284 gen.types.update(upd) 

285 

286 

287def _make_props(gen): 

288 done = {} 

289 own_props_by_name = {} 

290 

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 

295 

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 

303 

304 props[name] = p 

305 

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

311 

312 props = {} 

313 

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

320 

321 if typ.name in own_props_by_name: 

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

323 

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

325 

326 done[typ.name] = props 

327 return props 

328 

329 for typ in gen.types.values(): 

330 if typ.c == base.C.CLASS: 

331 _make(typ, []) 

332 

333 

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

348 

349 

350DOT = '.' 

351COMMA = ','