Coverage for gws-app/gws/config/loader.py: 23%

216 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1from typing import Optional 

2import sys 

3 

4import gws 

5import gws.spec.runtime 

6import gws.lib.jsonx 

7import gws.lib.osx 

8import gws.lib.dynimport 

9 

10from . import parser 

11 

12_ERROR_PREFIX = 'CONFIGURATION ERROR' 

13 

14_ROOT_NAME = 'gws_root_object' 

15 

16_DEFAULT_STORE_PATH = gws.c.CONFIG_DIR + '/config.pickle' 

17 

18_DEFAULT_CONFIG_PATHS = [ 

19 '/data/config.cx', 

20 '/data/config.json', 

21 '/data/config.yaml', 

22 '/data/config.py', 

23] 

24 

25_DEFAULT_MANIFEST_PATHS = [ 

26 '/data/MANIFEST.json', 

27] 

28 

29 

30class Object: 

31 ctx: gws.ConfigContext 

32 manifestPath: str 

33 configPath: str 

34 fallbackConfig: Optional[gws.Config] 

35 withSpecCache: bool 

36 

37 def __init__( 

38 self, 

39 manifest_path='', 

40 config_path='', 

41 specs=None, 

42 raw_config=None, 

43 fallback_config=None, 

44 with_spec_cache=False, 

45 hooks=None, 

46 ): 

47 self.tm1 = _time_and_memory() 

48 

49 self.ctx = gws.ConfigContext( 

50 errors=[], 

51 ) 

52 

53 self.manifestPath = real_manifest_path(manifest_path) 

54 if self.manifestPath: 

55 gws.log.info(f'using manifest {self.manifestPath!r}...') 

56 

57 self.configPath = real_config_path(config_path) 

58 self.rawConfig = raw_config 

59 self.fallbackConfig = fallback_config 

60 self.withSpecCache = with_spec_cache 

61 self.hooks = hooks or [] 

62 self.specs = specs 

63 

64 self.config = None 

65 self.root = None 

66 

67 def configure(self) -> gws.ConfigResult: 

68 if not self._init_specs(): 

69 return self._result() 

70 

71 self._run_hook('preConfigure') 

72 if not self.config: 

73 self.config = self._create_config() 

74 self._run_hook('postConfigure') 

75 

76 if self.config: 

77 self._run_hook('preInitialize') 

78 if not self.root: 

79 self.root = self._create_root(self.config) 

80 self._run_hook('postInitialize') 

81 

82 if not self.root and self.ctx.specs.manifest.withFallbackConfig and self.fallbackConfig: 

83 gws.log.warning(f'using fallback config') 

84 self.root = self._create_root(self.fallbackConfig) 

85 

86 if not self.root: 

87 return self._result() 

88 

89 if self.ctx.errors and self.ctx.specs.manifest.withStrictConfig: 

90 self.root = None 

91 return self._result() 

92 

93 self.root.configPaths = list(self.ctx.paths) 

94 return self._result() 

95 

96 def parse(self) -> gws.ConfigResult: 

97 if not self._init_specs(): 

98 return self._result() 

99 

100 self.config = self._create_config() 

101 if not self.config: 

102 return self._result() 

103 

104 return self._result() 

105 

106 ## 

107 

108 def _init_specs(self): 

109 if self.specs: 

110 self.ctx.specs = self.specs 

111 return True 

112 

113 try: 

114 self.ctx.specs = gws.spec.runtime.create( 

115 manifest_path=self.manifestPath, 

116 read_cache=self.withSpecCache, 

117 write_cache=self.withSpecCache, 

118 ) 

119 return True 

120 except Exception as exc: 

121 gws.log.exception() 

122 self._error(exc) 

123 return False 

124 

125 def _create_config(self): 

126 if self.rawConfig: 

127 return parser.parse_app_dict(self.rawConfig, '', self.ctx) 

128 if not self.configPath: 

129 self._error(gws.ConfigurationError('no configuration file found')) 

130 return 

131 gws.log.info(f'using config {self.configPath!r}...') 

132 return parser.parse_app_from_path(self.configPath, self.ctx) 

133 

134 def _create_root(self, cfg): 

135 root = initialize(self.ctx.specs, cfg) 

136 if root: 

137 for ce in root.configErrors: 

138 self.ctx.errors.append(gws.ConfigErrorInfo(ce)) 

139 return root 

140 

141 def _run_hook(self, event): 

142 for evt, fn in self.hooks: 

143 if event != evt: 

144 continue 

145 try: 

146 fn(self) 

147 except Exception as exc: 

148 gws.log.exception() 

149 self._error(exc) 

150 

151 def _error(self, exc): 

152 cei = gws.ConfigErrorInfo(message=str(exc)) 

153 if exc.__cause__: 

154 cei.cause = repr(exc.__cause__) 

155 self.ctx.errors.append(cei) 

156 

157 def _result(self): 

158 return gws.ConfigResult( 

159 errors=self.ctx.errors, 

160 root=self.root, 

161 config=self.config, 

162 info=_info_string(self.root, self.tm1), 

163 ) 

164 

165 

166def configure( 

167 manifest_path='', 

168 config_path='', 

169 specs: Optional[gws.SpecRuntime] = None, 

170 raw_config: dict | gws.Data = None, 

171 fallback_config: dict | gws.Data = None, 

172 with_spec_cache=False, 

173 hooks: list = None, 

174) -> gws.ConfigResult: 

175 """Configure the server.""" 

176 

177 ldr = Object( 

178 manifest_path, 

179 config_path, 

180 specs, 

181 raw_config, 

182 fallback_config, 

183 with_spec_cache, 

184 hooks, 

185 ) 

186 return ldr.configure() 

187 

188 

189def parse( 

190 manifest_path='', 

191 config_path='', 

192 specs: Optional[gws.SpecRuntime] = None, 

193) -> gws.ConfigResult: 

194 """Parse input configuration.""" 

195 

196 ldr = Object( 

197 manifest_path, 

198 config_path, 

199 specs, 

200 ) 

201 return ldr.parse() 

202 

203 

204def initialize(specs: gws.SpecRuntime, config: gws.Config) -> gws.Root: 

205 root = gws.create_root(specs) 

206 root.create_application(config) 

207 root.post_initialize() 

208 return root 

209 

210 

211def activate(root: gws.Root): 

212 root.activate() 

213 return gws.u.set_app_global(_ROOT_NAME, root) 

214 

215 

216def deactivate(): 

217 return gws.u.delete_app_global(_ROOT_NAME) 

218 

219 

220def store(root: gws.Root, path=None) -> str: 

221 path = path or _DEFAULT_STORE_PATH 

222 gws.log.debug(f'writing config to {path!r}') 

223 try: 

224 gws.lib.jsonx.to_path(f'{path}.syspath.json', sys.path) 

225 gws.u.serialize_to_path(root, path) 

226 return path 

227 except Exception as exc: 

228 raise gws.ConfigurationError('unable to store configuration') from exc 

229 

230 

231def load(path=None) -> gws.Root: 

232 ui = gws.lib.osx.user_info() 

233 path = path or _DEFAULT_STORE_PATH 

234 gws.log.info(f'loading config from {path!r}, user {ui["pw_name"]} ({ui["pw_uid"]}:{ui["pw_gid"]})') 

235 try: 

236 return _load(path) 

237 except Exception as exc: 

238 raise gws.ConfigurationError('unable to load configuration') from exc 

239 

240 

241def _load(path) -> gws.Root: 

242 sys_path = gws.lib.jsonx.from_path(f'{path}.syspath.json') 

243 for p in sys_path: 

244 if p not in sys.path: 

245 sys.path.insert(0, p) 

246 gws.log.debug(f'path {p!r} added to sys.path') 

247 

248 tm1 = _time_and_memory() 

249 root = gws.u.unserialize_from_path(path) 

250 activate(root) 

251 info = _info_string(root, tm1) 

252 gws.log.info(f'configuration loaded, {info}') 

253 

254 return root 

255 

256 

257def get_root() -> gws.Root: 

258 def _err(): 

259 raise gws.Error('no configuration root found') 

260 

261 return gws.u.get_app_global(_ROOT_NAME, _err) 

262 

263 

264def real_config_path(config_path: str) -> str: 

265 p = config_path or gws.env.GWS_CONFIG 

266 if p: 

267 for s in p.split(','): 

268 s = s.strip() 

269 if gws.u.is_file(s): 

270 return s 

271 return '' 

272 for p in _DEFAULT_CONFIG_PATHS: 

273 if gws.u.is_file(p): 

274 return p 

275 return '' 

276 

277 

278def real_manifest_path(manifest_path: str) -> str: 

279 p = manifest_path or gws.env.GWS_MANIFEST 

280 if p: 

281 return p 

282 for p in _DEFAULT_MANIFEST_PATHS: 

283 if gws.u.is_file(p): 

284 return p 

285 return '' 

286 

287 

288def log_report(cr: gws.ConfigResult): 

289 err_cnt = len(cr.errors) if cr.errors else 0 

290 ln = '*' * 80 

291 

292 if err_cnt == 0: 

293 gws.log.info(ln) 

294 gws.log.info(f'configured: {cr.info}') 

295 gws.log.info(ln) 

296 return 

297 

298 gws.log.error(ln) 

299 gws.log.error(f'configured wth errors: errors: {err_cnt}, {cr.info}') 

300 gws.log.error(ln) 

301 

302 # cr.errors.sort(key=lambda ce: ce.message) 

303 

304 for n, cei in enumerate(cr.errors, 1): 

305 gws.log.error(f'{_ERROR_PREFIX}: {n} of {err_cnt}') 

306 _log_error(cei) 

307 gws.log.error(f'{_ERROR_PREFIX}: ') 

308 

309 gws.log.error(ln) 

310 

311 

312def _log_error(cei: gws.ConfigErrorInfo): 

313 ls = [] 

314 ls.append(cei.message) 

315 tab = ' ' * 4 

316 

317 if cei.path: 

318 ls.append(f'PATH: {cei.path}') 

319 if cei.line: 

320 ls.append(f'LINE: {cei.line}') 

321 if cei.value: 

322 ls.append(f'VALUE: {cei.value}') 

323 if cei.cause: 

324 ls.append(f'CAUSE: {cei.cause}') 

325 if cei.stack: 

326 for loc in cei.stack: 

327 p = [ 

328 loc.objectType, 

329 repr(loc.objectName) if loc.objectName else None, 

330 f'uid={loc.objectUid}' if loc.objectUid else None, 

331 ] 

332 p = '<' + ' '.join(gws.u.compact(p)) + '>' 

333 if loc.propName: 

334 p = f'{loc.propName!r} {p}' 

335 ls.append(f'{tab}in {p}') 

336 if cei.contextLines: 

337 ls.extend(cei.contextLines) 

338 

339 for s in ls: 

340 gws.log.error(f'{_ERROR_PREFIX}: {s}') 

341 

342 

343def _time_and_memory(): 

344 return gws.u.stime(), gws.lib.osx.process_rss_size() 

345 

346 

347def _info_string(root, tm1): 

348 tm2 = _time_and_memory() 

349 return 'objects: {:d}, time: {:d}s., memory: {:.2f} MB'.format( 

350 root.object_count() if root else 0, 

351 tm2[0] - tm1[0], 

352 tm2[1] - tm1[1], 

353 )