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
« 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."""
3from typing import Optional
4import sys
6from .. import core
7from . import util
9c = core.c
10v = core.v
11Error = core.Error
12GeneratorError = core.GeneratorError
13LoadError = core.LoadError
14ReadError = core.ReadError
15Type = core.Type
18class Data:
19 """Simple data object. Missing attributes return ``None``."""
21 def __init__(self, **kwargs):
22 """Create an object with the given attributes.
24 Args:
25 **kwargs: Attribute values.
26 """
28 vars(self).update(kwargs)
30 def __repr__(self):
31 return repr(vars(self))
33 def get(self, k, default=None):
34 """Get an attribute value.
36 Args:
37 k: Attribute name.
38 default: Value to return if the attribute is not set.
40 Returns:
41 The attribute value or the default.
42 """
44 return vars(self).get(k, default)
46 def __getattr__(self, item):
47 """Return ``None`` for attributes that are not set."""
49 return None
52class _Logger:
53 """Logger that writes messages to stdout."""
55 level = 'INFO'
56 levels = 'ERROR', 'WARNING', 'INFO', 'DEBUG'
58 def set_level(self, level):
59 """Set the log level.
61 Args:
62 level: One of ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``.
63 """
65 self.level = level
67 def log(self, level, *args):
68 """Write a message if the level is enabled.
70 Args:
71 level: Message level.
72 *args: Message parts, joined with spaces.
73 """
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()
80 def error(self, *args):
81 """Log an error message.
83 Args:
84 *args: Message parts.
85 """
87 self.log('ERROR', *args)
89 def warning(self, *args):
90 """Log a warning message.
92 Args:
93 *args: Message parts.
94 """
96 self.log('WARNING', *args)
98 def info(self, *args):
99 """Log an info message.
101 Args:
102 *args: Message parts.
103 """
105 self.log('INFO', *args)
107 def debug(self, *args):
108 """Log a debug message.
110 Args:
111 *args: Message parts.
112 """
114 self.log('DEBUG', *args)
117log = _Logger()
120class Generator:
121 """Generator state, shared by all generator steps.
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 """
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
156 def add_type(self, **kwargs):
157 """Create a type and add it to the type dictionary.
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.
163 Args:
164 **kwargs: Type attributes.
166 Returns:
167 The new type.
169 Raises:
170 ``GeneratorError``: If no automatic uid can be created for the type kind.
171 """
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
181 def get_type(self, uid) -> Optional[Type]:
182 """Get a type by uid.
184 Args:
185 uid: Type uid.
187 Returns:
188 The type or ``None`` if not found.
189 """
191 return self.typeDict.get(uid)
193 def require_type(self, uid) -> Type:
194 """Get a type by uid, failing if it does not exist.
196 Args:
197 uid: Type uid.
199 Returns:
200 The type.
202 Raises:
203 ``GeneratorError``: If the type is not found.
204 """
206 typ = self.typeDict.get(uid)
207 if not typ:
208 raise GeneratorError(f'unknown type {uid!r}')
209 return typ
211 def dump(self, tag):
212 """Write the generator state to ``<outDir>/<tag>.debug.json`` in debug mode.
214 Args:
215 tag: File name tag.
216 """
218 if self.debug:
219 util.write_json(self.outDir + '/' + tag + '.debug.json', vars(self))
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}')
249_comma = ','.join