Coverage for kye/types.py: 31%
111 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-01-16 14:12 -0700
« prev ^ index » next coverage.py v7.3.2, created at 2024-01-16 14:12 -0700
1from __future__ import annotations
2from typing import Optional
3from kye.parser.parser import parse_expression
4import kye.parser.kye_ast as AST
5import re
6from collections import OrderedDict
8TYPE_REF = str
9EDGE = str
11class Type:
12 """ Base Class for Types """
13 ref: TYPE_REF
14 extends: Optional[Type]
15 indexes: tuple[tuple[EDGE]]
16 assertions: list[AST.Expression]
17 _edges: OrderedDict[EDGE, Type]
18 _multiple: dict[EDGE, bool]
19 _nullable: dict[EDGE, bool]
21 def __init__(self, name: TYPE_REF):
22 assert re.match(r'\b[A-Z]+[a-z]\w+\b', name)
23 self.ref = name
24 self.indexes = tuple()
25 self.assertions = []
26 self.extends = None
27 self._edges = OrderedDict()
28 self._multiple = {}
29 self._nullable = {}
31 def define_edge(self,
32 name: EDGE,
33 type: Type,
34 nullable=False,
35 multiple=False
36 ):
37 assert re.fullmatch(r'[a-z_][a-z0-9_]+', name)
38 assert isinstance(type, Type)
39 self._edges[name] = type
40 self._nullable[name] = nullable
41 self._multiple[name] = multiple
43 def define_index(self, index: tuple[EDGE]):
44 # Convert to tuple if passed in a single string
45 if type(index) is str:
46 index = (index,)
47 else:
48 index = tuple(index)
50 # Skip if it is already part of our indexes
51 if index in self.indexes:
52 return
54 # Validate edges within index
55 for edge in index:
56 assert self.has_edge(edge), f'Cannot use undefined edge in index: "{edge}"'
57 assert not self.allows_null(edge), f'Cannot use a nullable edge in index: "{edge}"'
59 # Remove any existing indexes that are a superset of the new index
60 self.indexes = tuple(
61 existing_idx for existing_idx in self.indexes
62 if not set(index).issubset(set(existing_idx))
63 ) + (index,)
65 def define_parent(self, parent: Type):
66 assert isinstance(parent, Type)
67 if self.extends is not None:
68 assert self.extends == parent, 'Already assigned a parent'
69 return
70 self.extends = parent
71 for edge in parent._edges:
72 if not self.has_edge(edge):
73 self.define_edge(
74 name=edge,
75 type=parent._edges[edge],
76 multiple=parent.allows_multiple(edge),
77 nullable=parent.allows_null(edge),
78 )
79 self.assertions = parent.assertions + self.assertions
81 def define_assertion(self, assertion: str):
82 assert type(assertion) is str
83 ast = parse_expression(assertion)
84 self.assertions.append(ast)
86 @property
87 def index(self) -> set[EDGE]:
88 """ Flatten the 2d list of indexes """
89 return {idx for idxs in self.indexes for idx in idxs}
91 @property
92 def has_index(self) -> bool:
93 return len(self.indexes) > 0
95 @property
96 def edges(self) -> list[EDGE]:
97 return list(self._edges.keys())
99 def has_edge(self, edge: EDGE) -> bool:
100 return edge in self._edges
102 def get_edge(self, edge: EDGE) -> Type:
103 assert self.has_edge(edge)
104 return self._edges[edge]
106 def edge_origin(self, edge: EDGE) -> Optional[Type]:
107 assert self.has_edge(edge)
108 if self.extends and self.extends.has_edge(edge):
109 return self.extends.edge_origin(edge)
110 return self
112 def allows_multiple(self, edge: EDGE) -> bool:
113 assert self.has_edge(edge)
114 return self._multiple[edge]
116 def allows_null(self, edge: EDGE) -> bool:
117 assert self.has_edge(edge)
118 return self._nullable[edge]
120 def __repr__(self):
121 def get_cardinality_symbol(edge):
122 nullable = int(self.allows_null(edge))
123 multiple = int(self.allows_multiple(edge))
124 return ([['' ,'+'],
125 ['?','*']])[nullable][multiple]
127 non_index_edges = [
128 edge + get_cardinality_symbol(edge)
129 for edge in self._edges
130 if edge not in self.index
131 ]
133 return "{}{}{}".format(
134 self.ref or '',
135 ''.join('(' + ','.join(idx) + ')' for idx in self.indexes),
136 '{' + ','.join(non_index_edges) + '}' if len(non_index_edges) else '',
137 )
139GLOBALS = {
140 'Number': {},
141 'String': {'edges':{'length':'Number'}},
142 'Boolean': {},
143}
145def from_compiled(source, types: dict[TYPE_REF, Type]={}):
146 source['models'] = {**GLOBALS, **source.get('models',{})}
147 # 1. Do first iteration creating a stub type for each name
148 for ref in source.get('models',{}):
149 types[ref] = Type(ref)
151 def get_type(type_ref):
152 assert type_ref in types, f'Undefined type: "{type_ref}"'
153 return types[type_ref]
155 zipped_source_and_stub: dict[TYPE_REF, tuple[dict, Type]] = {
156 ref: (src, types[ref])
157 for ref, src in source.get('models',{}).items()
158 }
160 # 2. During second iteration define the edges, indexes & assertions
161 for src, typ in zipped_source_and_stub.values():
163 for edge_name, edge_type_ref in src.get('edges', {}).items():
164 nullable = edge_name.endswith('?') or edge_name.endswith('*')
165 multiple = edge_name.endswith('+') or edge_name.endswith('*')
166 edge_name = edge_name.rstrip('?+*')
167 typ.define_edge(
168 name=edge_name,
169 type=get_type(edge_type_ref),
170 nullable=nullable,
171 multiple=multiple,
172 )
174 if 'index' in src:
175 typ.define_index(src['index'])
176 if 'indexes' in src:
177 for idx in src['indexes']:
178 typ.define_index(idx)
180 for assertion in src.get('assertions', []):
181 typ.define_assertion(assertion)
183 # 3. Wait till the third iteration to define the extends
184 # so that parent edges & assertions will be known
185 def recursively_define_parent(type_ref):
186 src, typ = zipped_source_and_stub[type_ref]
187 if 'extends' in src:
188 parent = get_type(src['extends'])
189 recursively_define_parent(parent.ref)
190 typ.define_parent(parent)
192 for type_ref in zipped_source_and_stub.keys():
193 recursively_define_parent(type_ref)
196 # # 4. Now that all edges have been defined, parse the expressions
197 # for src, typ in zipped_source_and_stub:
198 # for assertion in src.get('assertions', []):
199 # # TODO: parse the assertion and add type information
200 # typ.define_assertion(assertion)
202 return types