Coverage for gws-app/gws/spec/generator/base.py: 83%

99 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Generator state, a data class and the generator logger.""" 

2 

3from typing import Optional 

4import sys 

5 

6from .. import core 

7from . import util 

8 

9c = core.c 

10v = core.v 

11Error = core.Error 

12GeneratorError = core.GeneratorError 

13LoadError = core.LoadError 

14ReadError = core.ReadError 

15Type = core.Type 

16 

17 

18class Data: 

19 """Simple data object. Missing attributes return ``None``.""" 

20 

21 def __init__(self, **kwargs): 

22 """Create an object with the given attributes. 

23 

24 Args: 

25 **kwargs: Attribute values. 

26 """ 

27 

28 vars(self).update(kwargs) 

29 

30 def __repr__(self): 

31 return repr(vars(self)) 

32 

33 def get(self, k, default=None): 

34 """Get an attribute value. 

35 

36 Args: 

37 k: Attribute name. 

38 default: Value to return if the attribute is not set. 

39 

40 Returns: 

41 The attribute value or the default. 

42 """ 

43 

44 return vars(self).get(k, default) 

45 

46 def __getattr__(self, item): 

47 """Return ``None`` for attributes that are not set.""" 

48 

49 return None 

50 

51 

52class _Logger: 

53 """Logger that writes messages to stdout.""" 

54 

55 level = 'INFO' 

56 levels = 'ERROR', 'WARNING', 'INFO', 'DEBUG' 

57 

58 def set_level(self, level): 

59 """Set the log level. 

60 

61 Args: 

62 level: One of ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``. 

63 """ 

64 

65 self.level = level 

66 

67 def log(self, level, *args): 

68 """Write a message if the level is enabled. 

69 

70 Args: 

71 level: Message level. 

72 *args: Message parts, joined with spaces. 

73 """ 

74 

75 if self.levels.index(level) <= self.levels.index(self.level): 

76 msg = f'[spec] {level}: ' + ' '.join(str(a) for a in args) 

77 sys.stdout.write(msg + '\n') 

78 sys.stdout.flush() 

79 

80 def error(self, *args): 

81 """Log an error message. 

82 

83 Args: 

84 *args: Message parts. 

85 """ 

86 

87 self.log('ERROR', *args) 

88 

89 def warning(self, *args): 

90 """Log a warning message. 

91 

92 Args: 

93 *args: Message parts. 

94 """ 

95 

96 self.log('WARNING', *args) 

97 

98 def info(self, *args): 

99 """Log an info message. 

100 

101 Args: 

102 *args: Message parts. 

103 """ 

104 

105 self.log('INFO', *args) 

106 

107 def debug(self, *args): 

108 """Log a debug message. 

109 

110 Args: 

111 *args: Message parts. 

112 """ 

113 

114 self.log('DEBUG', *args) 

115 

116 

117log = _Logger() 

118 

119 

120class Generator: 

121 """Generator state, shared by all generator steps. 

122 

123 Attributes: 

124 aliases: Maps alias names (from imports and global names) to target names. 

125 chunks: Source code chunks. 

126 meta: Build-time metadata. 

127 typeDict: All types, keyed by uid. 

128 serverTypes: Types extracted for the server. 

129 specData: The resulting spec data. 

130 configRef: Configuration references in Markdown, keyed by language. 

131 strings: Documentation strings, keyed by language and type uid. 

132 manifestPath: Path to the application manifest. 

133 outDir: Output directory. 

134 rootDir: Application root directory (``app``). 

135 selfDir: Directory of the spec package. 

136 typescript: Generated TypeScript API. 

137 debug: If True, dump the state after each step. 

138 """ 

139 

140 def __init__(self): 

141 self.aliases: dict[str, str] = {} 

142 self.chunks: list[core.Chunk] = [] 

143 self.meta: dict = {} 

144 self.typeDict: dict[str, Type] = {} 

145 self.serverTypes: list[Type] = [] 

146 self.specData: core.SpecData 

147 self.configRef = {} 

148 self.strings = {} 

149 self.manifestPath = '' 

150 self.outDir = '' 

151 self.rootDir = '' 

152 self.selfDir = '' 

153 self.typescript = '' 

154 self.debug = False 

155 

156 def add_type(self, **kwargs): 

157 """Create a type and add it to the type dictionary. 

158 

159 Named types use the name as uid. Other types get an automatic uid made 

160 of the type kind and the uids of the types they refer to, so that equal 

161 types share the uid. 

162 

163 Args: 

164 **kwargs: Type attributes. 

165 

166 Returns: 

167 The new type. 

168 

169 Raises: 

170 ``GeneratorError``: If no automatic uid can be created for the type kind. 

171 """ 

172 

173 if kwargs.get('name'): 

174 kwargs['uid'] = kwargs['name'] 

175 if not kwargs.get('uid'): 

176 kwargs['uid'] = kwargs['c'] + ':' + _auto_uid(kwargs) 

177 typ = core.make_type(kwargs) 

178 self.typeDict[typ.uid] = typ 

179 return typ 

180 

181 def get_type(self, uid) -> Optional[Type]: 

182 """Get a type by uid. 

183 

184 Args: 

185 uid: Type uid. 

186 

187 Returns: 

188 The type or ``None`` if not found. 

189 """ 

190 

191 return self.typeDict.get(uid) 

192 

193 def require_type(self, uid) -> Type: 

194 """Get a type by uid, failing if it does not exist. 

195 

196 Args: 

197 uid: Type uid. 

198 

199 Returns: 

200 The type. 

201 

202 Raises: 

203 ``GeneratorError``: If the type is not found. 

204 """ 

205 

206 typ = self.typeDict.get(uid) 

207 if not typ: 

208 raise GeneratorError(f'unknown type {uid!r}') 

209 return typ 

210 

211 def dump(self, tag): 

212 """Write the generator state to ``<outDir>/<tag>.debug.json`` in debug mode. 

213 

214 Args: 

215 tag: File name tag. 

216 """ 

217 

218 if self.debug: 

219 util.write_json(self.outDir + '/' + tag + '.debug.json', vars(self)) 

220 

221 

222def _auto_uid(args): 

223 tc = args['c'] 

224 if tc == c.DICT: 

225 return args['tKey'] + ',' + args['tValue'] 

226 if tc == c.LIST: 

227 return args['tItem'] 

228 if tc == c.SET: 

229 return args['tItem'] 

230 if tc == c.LITERAL: 

231 return _comma(repr(v) for v in args['literalValues']) 

232 if tc == c.OPTIONAL: 

233 return args['tTarget'] 

234 if tc == c.TUPLE: 

235 return _comma(args['tItems']) 

236 if tc == c.UNION: 

237 return _comma(sorted(args['tItems'])) 

238 if tc == c.CALLABLE: 

239 return _comma(sorted(args['tItems'])) 

240 if tc == c.EXT: 

241 return args['extName'] 

242 if tc == c.VARIANT: 

243 if 'tMembers' in args: 

244 return _comma(sorted(args['tMembers'].values())) 

245 return _comma(sorted(args['tItems'])) 

246 raise GeneratorError(f'auto uid for {tc!r} not implemented: {args}') 

247 

248 

249_comma = ','.join