|
1 | | -""" schema.yml format representation """ |
2 | | - |
3 | | -import pathlib |
4 | | -import re |
5 | | -from dataclasses import dataclass, field |
6 | | -from typing import List, Set, Union, Dict, ClassVar, Optional |
7 | | -from toposort import toposort_flatten |
8 | | - |
9 | | -import yaml |
10 | | - |
11 | | - |
12 | | -class Error(Exception): |
13 | | - |
14 | | - def __str__(self): |
15 | | - return self.args[0] |
16 | | - |
17 | | - |
18 | | -root_class_name = "Element" |
19 | | - |
20 | | - |
21 | | -@dataclass |
22 | | -class Property: |
23 | | - is_single: ClassVar = False |
24 | | - is_optional: ClassVar = False |
25 | | - is_repeated: ClassVar = False |
26 | | - is_predicate: ClassVar = False |
27 | | - |
28 | | - name: str |
29 | | - type: str = None |
30 | | - is_child: bool = False |
31 | | - pragmas: List[str] = field(default_factory=list) |
32 | | - |
33 | | - |
34 | | -@dataclass |
35 | | -class SingleProperty(Property): |
36 | | - is_single: ClassVar = True |
37 | | - |
38 | | - |
39 | | -@dataclass |
40 | | -class OptionalProperty(Property): |
41 | | - is_optional: ClassVar = True |
42 | | - |
43 | | - |
44 | | -@dataclass |
45 | | -class RepeatedProperty(Property): |
46 | | - is_repeated: ClassVar = True |
47 | | - |
48 | | - |
49 | | -@dataclass |
50 | | -class RepeatedOptionalProperty(Property): |
51 | | - is_optional: ClassVar = True |
52 | | - is_repeated: ClassVar = True |
53 | | - |
54 | | - |
55 | | -@dataclass |
56 | | -class PredicateProperty(Property): |
57 | | - is_predicate: ClassVar = True |
58 | | - |
59 | | - |
60 | | -@dataclass |
61 | | -class IpaInfo: |
62 | | - from_class: Optional[str] = None |
63 | | - on_arguments: Optional[Dict[str, str]] = None |
64 | | - |
65 | | - |
66 | | -@dataclass |
67 | | -class Class: |
68 | | - name: str |
69 | | - bases: List[str] = field(default_factory=set) |
70 | | - derived: Set[str] = field(default_factory=set) |
71 | | - properties: List[Property] = field(default_factory=list) |
72 | | - dir: pathlib.Path = pathlib.Path() |
73 | | - pragmas: List[str] = field(default_factory=list) |
74 | | - ipa: Optional[IpaInfo] = None |
75 | | - |
76 | | - @property |
77 | | - def final(self): |
78 | | - return not self.derived |
79 | | - |
80 | | - |
81 | | -@dataclass |
82 | | -class Schema: |
83 | | - classes: Dict[str, Class] |
84 | | - includes: Set[str] = field(default_factory=set) |
85 | | - |
86 | | - |
87 | | -_StrOrList = Union[str, List[str]] |
88 | | - |
89 | | - |
90 | | -def _auto_list(data: _StrOrList) -> List[str]: |
91 | | - if isinstance(data, list): |
92 | | - return data |
93 | | - return [data] |
94 | | - |
95 | | - |
96 | | -def _parse_property(name: str, data: Union[str, Dict[str, _StrOrList]], is_child: bool = False): |
97 | | - if isinstance(data, dict): |
98 | | - if "type" not in data: |
99 | | - raise Error(f"property {name} has no type") |
100 | | - pragmas = _auto_list(data.pop("_pragma", [])) |
101 | | - type = data.pop("type") |
102 | | - if data: |
103 | | - raise Error(f"unknown metadata {', '.join(data)} in property {name}") |
104 | | - else: |
105 | | - pragmas = [] |
106 | | - type = data |
107 | | - if is_child and type[0].islower(): |
108 | | - raise Error(f"children must have class type, got {type} for {name}") |
109 | | - if type.endswith("?*"): |
110 | | - return RepeatedOptionalProperty(name, type[:-2], is_child=is_child, pragmas=pragmas) |
111 | | - elif type.endswith("*"): |
112 | | - return RepeatedProperty(name, type[:-1], is_child=is_child, pragmas=pragmas) |
113 | | - elif type.endswith("?"): |
114 | | - return OptionalProperty(name, type[:-1], is_child=is_child, pragmas=pragmas) |
115 | | - elif type == "predicate": |
116 | | - return PredicateProperty(name, pragmas=pragmas) |
117 | | - else: |
118 | | - return SingleProperty(name, type, is_child=is_child, pragmas=pragmas) |
119 | | - |
120 | | - |
121 | | -def _parse_ipa(data: Dict[str, Union[str, Dict[str, str]]]): |
122 | | - return IpaInfo(from_class=data.get("from"), |
123 | | - on_arguments=data.get(True)) # 'on' is parsed as boolean True in yaml |
124 | | - |
125 | | - |
126 | | -class _DirSelector: |
127 | | - """ Default output subdirectory selector for generated QL files, based on the `_directories` global field""" |
128 | | - |
129 | | - def __init__(self, dir_to_patterns): |
130 | | - self.selector = [(re.compile(p), pathlib.Path(d)) for d, p in dir_to_patterns] |
131 | | - self.selector.append((re.compile(""), pathlib.Path())) |
132 | | - |
133 | | - def get(self, name): |
134 | | - return next(d for p, d in self.selector if p.search(name)) |
135 | | - |
136 | | - |
137 | | -def load(path): |
138 | | - """ Parse the schema from the file at `path` """ |
139 | | - with open(path) as input: |
140 | | - data = yaml.load(input, Loader=yaml.SafeLoader) |
141 | | - grouper = _DirSelector(data.get("_directories", {}).items()) |
142 | | - classes = {root_class_name: Class(root_class_name)} |
143 | | - classes.update((cls, Class(cls, dir=grouper.get(cls))) for cls in data if not cls.startswith("_")) |
144 | | - for name, info in data.items(): |
145 | | - if name.startswith("_"): |
146 | | - continue |
147 | | - if not name[0].isupper(): |
148 | | - raise Error(f"keys in the schema file must be capitalized class names or metadata, got {name}") |
149 | | - cls = classes[name] |
150 | | - for k, v in info.items(): |
151 | | - if not k.startswith("_"): |
152 | | - cls.properties.append(_parse_property(k, v)) |
153 | | - elif k == "_extends": |
154 | | - cls.bases = _auto_list(v) |
155 | | - for base in cls.bases: |
156 | | - classes[base].derived.add(name) |
157 | | - elif k == "_dir": |
158 | | - cls.dir = pathlib.Path(v) |
159 | | - elif k == "_children": |
160 | | - cls.properties.extend(_parse_property(kk, vv, is_child=True) for kk, vv in v.items()) |
161 | | - elif k == "_pragma": |
162 | | - cls.pragmas = _auto_list(v) |
163 | | - elif k == "_synth": |
164 | | - cls.ipa = _parse_ipa(v) |
165 | | - else: |
166 | | - raise Error(f"unknown metadata {k} for class {name}") |
167 | | - if not cls.bases and cls.name != root_class_name: |
168 | | - cls.bases = [root_class_name] |
169 | | - classes[root_class_name].derived.add(name) |
170 | | - |
171 | | - groups = {} |
172 | | - |
173 | | - for name, cls in classes.items(): |
174 | | - groups.setdefault(cls.dir, []).append(name) |
175 | | - |
176 | | - sorted_classes = {} |
177 | | - |
178 | | - for dir in sorted(groups): |
179 | | - group = groups[dir] |
180 | | - inheritance = {name: classes[name].bases for name in group} |
181 | | - for name in toposort_flatten(inheritance): |
182 | | - sorted_classes[name] = classes[name] |
183 | | - |
184 | | - return Schema(classes=sorted_classes, includes=set(data.get("_includes", []))) |
| 1 | +from .schema import * |
0 commit comments