|
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