|
1 | | -""" schema.yml format representation """ |
2 | | -import pathlib |
3 | | -import re |
4 | | -import types |
| 1 | +""" schema format representation """ |
5 | 2 | import typing |
6 | 3 | from dataclasses import dataclass, field |
7 | 4 | from typing import List, Set, Union, Dict, Optional |
8 | 5 | from enum import Enum, auto |
9 | 6 | import functools |
10 | | -import importlib.util |
11 | | -from toposort import toposort_flatten |
12 | | -import inflection |
13 | 7 |
|
14 | 8 |
|
15 | 9 | class Error(Exception): |
@@ -198,125 +192,3 @@ def split_doc(doc): |
198 | 192 | while trimmed and not trimmed[0]: |
199 | 193 | trimmed.pop(0) |
200 | 194 | return trimmed |
201 | | - |
202 | | - |
203 | | -@dataclass |
204 | | -class _PropertyNamer(PropertyModifier): |
205 | | - name: str |
206 | | - |
207 | | - def modify(self, prop: Property): |
208 | | - prop.name = self.name.rstrip("_") |
209 | | - |
210 | | - |
211 | | -def _get_class(cls: type) -> Class: |
212 | | - if not isinstance(cls, type): |
213 | | - raise Error(f"Only class definitions allowed in schema, found {cls}") |
214 | | - # we must check that going to dbscheme names and back is preserved |
215 | | - # In particular this will not happen if uppercase acronyms are included in the name |
216 | | - to_underscore_and_back = inflection.camelize(inflection.underscore(cls.__name__), uppercase_first_letter=True) |
217 | | - if cls.__name__ != to_underscore_and_back: |
218 | | - raise Error(f"Class name must be upper camel-case, without capitalized acronyms, found {cls.__name__} " |
219 | | - f"instead of {to_underscore_and_back}") |
220 | | - if len({b._group for b in cls.__bases__ if hasattr(b, "_group")}) > 1: |
221 | | - raise Error(f"Bases with mixed groups for {cls.__name__}") |
222 | | - if any(getattr(b, "_null", False) for b in cls.__bases__): |
223 | | - raise Error(f"Null class cannot be derived") |
224 | | - return Class(name=cls.__name__, |
225 | | - bases=[b.__name__ for b in cls.__bases__ if b is not object], |
226 | | - derived={d.__name__ for d in cls.__subclasses__()}, |
227 | | - # getattr to inherit from bases |
228 | | - group=getattr(cls, "_group", ""), |
229 | | - # in the following we don't use `getattr` to avoid inheriting |
230 | | - pragmas=cls.__dict__.get("_pragmas", []), |
231 | | - ipa=cls.__dict__.get("_ipa", None), |
232 | | - properties=[ |
233 | | - a | _PropertyNamer(n) |
234 | | - for n, a in cls.__dict__.get("__annotations__", {}).items() |
235 | | - ], |
236 | | - doc=split_doc(cls.__doc__), |
237 | | - default_doc_name=cls.__dict__.get("_doc_name"), |
238 | | - ) |
239 | | - |
240 | | - |
241 | | -def _toposort_classes_by_group(classes: typing.Dict[str, Class]) -> typing.Dict[str, Class]: |
242 | | - groups = {} |
243 | | - ret = {} |
244 | | - |
245 | | - for name, cls in classes.items(): |
246 | | - groups.setdefault(cls.group, []).append(name) |
247 | | - |
248 | | - for group, grouped in sorted(groups.items()): |
249 | | - inheritance = {name: classes[name].bases for name in grouped} |
250 | | - for name in toposort_flatten(inheritance): |
251 | | - ret[name] = classes[name] |
252 | | - |
253 | | - return ret |
254 | | - |
255 | | - |
256 | | -def _fill_ipa_information(classes: typing.Dict[str, Class]): |
257 | | - """ Take a dictionary where the `ipa` field is filled for all explicitly synthesized classes |
258 | | - and update it so that all non-final classes that have only synthesized final descendants |
259 | | - get `True` as` value for the `ipa` field |
260 | | - """ |
261 | | - if not classes: |
262 | | - return |
263 | | - |
264 | | - is_ipa: typing.Dict[str, bool] = {} |
265 | | - |
266 | | - def fill_is_ipa(name: str): |
267 | | - if name not in is_ipa: |
268 | | - cls = classes[name] |
269 | | - for d in cls.derived: |
270 | | - fill_is_ipa(d) |
271 | | - if cls.ipa is not None: |
272 | | - is_ipa[name] = True |
273 | | - elif not cls.derived: |
274 | | - is_ipa[name] = False |
275 | | - else: |
276 | | - is_ipa[name] = all(is_ipa[d] for d in cls.derived) |
277 | | - |
278 | | - root = next(iter(classes)) |
279 | | - fill_is_ipa(root) |
280 | | - |
281 | | - for name, cls in classes.items(): |
282 | | - if cls.ipa is None and is_ipa[name]: |
283 | | - cls.ipa = True |
284 | | - |
285 | | - |
286 | | -def load(m: types.ModuleType) -> Schema: |
287 | | - includes = set() |
288 | | - classes = {} |
289 | | - known = {"int", "string", "boolean"} |
290 | | - known.update(n for n in m.__dict__ if not n.startswith("__")) |
291 | | - import swift.codegen.lib.schema.defs as defs |
292 | | - null = None |
293 | | - for name, data in m.__dict__.items(): |
294 | | - if hasattr(defs, name): |
295 | | - continue |
296 | | - if name == "__includes": |
297 | | - includes = set(data) |
298 | | - continue |
299 | | - if name.startswith("__"): |
300 | | - continue |
301 | | - cls = _get_class(data) |
302 | | - if classes and not cls.bases: |
303 | | - raise Error( |
304 | | - f"Only one root class allowed, found second root {name}") |
305 | | - cls.check_types(known) |
306 | | - classes[name] = cls |
307 | | - if getattr(data, "_null", False): |
308 | | - if null is not None: |
309 | | - raise Error(f"Null class {null} already defined, second null class {name} not allowed") |
310 | | - null = name |
311 | | - cls.is_null_class = True |
312 | | - |
313 | | - _fill_ipa_information(classes) |
314 | | - |
315 | | - return Schema(includes=includes, classes=_toposort_classes_by_group(classes), null=null) |
316 | | - |
317 | | - |
318 | | -def load_file(path: pathlib.Path) -> Schema: |
319 | | - spec = importlib.util.spec_from_file_location("schema", path) |
320 | | - module = importlib.util.module_from_spec(spec) |
321 | | - spec.loader.exec_module(module) |
322 | | - return load(module) |
0 commit comments