-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmodel.py
More file actions
308 lines (245 loc) · 10.7 KB
/
model.py
File metadata and controls
308 lines (245 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import dataclasses
import inspect
import sys
import typing
from abc import ABC, abstractmethod, ABCMeta
from dataclasses import Field, MISSING, dataclass, field
from typing import Optional, Callable, List, Union
from .naming import ReferenceByName
from .position import Position, Source
from .reflection import Multiplicity, PropertyDescription
from ..reflection import get_type_annotations, get_type_arguments, is_sequence_type
from ..reflection.reflection import get_type_origin
PYLASU_FEATURE = "pylasu_feature"
class internal_property(property):
pass
def internal_properties(*props: str):
def decorate(cls: type):
cls.__internal_properties__ = (
getattr(cls, "__internal_properties__", []) + [*Node.__internal_properties__, *props])
return cls
return decorate
class InternalField(Field):
pass
def internal_field(
*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None,
kw_only=False):
"""Return an object to identify internal dataclass fields. The arguments are the same as dataclasses.field."""
if default is not MISSING and default_factory is not MISSING:
raise ValueError('cannot specify both default and default_factory')
try:
# Python 3.10+
return InternalField(default, default_factory, init, repr, hash, compare, metadata, kw_only)
except TypeError:
return InternalField(default, default_factory, init, repr, hash, compare, metadata)
def node_property(default=MISSING):
description = PropertyDescription(
"", None,
multiplicity=Multiplicity.OPTIONAL if default is None else Multiplicity.SINGULAR)
return field(default=default, metadata={PYLASU_FEATURE: description})
def node_containment(multiplicity: Multiplicity = Multiplicity.SINGULAR):
description = PropertyDescription("", None, is_containment=True, multiplicity=multiplicity)
if multiplicity == Multiplicity.SINGULAR:
return field(metadata={PYLASU_FEATURE: description})
elif multiplicity == Multiplicity.OPTIONAL:
return field(default=None, metadata={PYLASU_FEATURE: description})
elif multiplicity == Multiplicity.MANY:
return field(default_factory=list, metadata={PYLASU_FEATURE: description})
class Origin(ABC):
@internal_property
@abstractmethod
def position(self) -> Optional[Position]:
pass
@internal_property
def source_text(self) -> Optional[str]:
return None
@internal_property
def source(self) -> Optional[Source]:
return self.position.source if self.position is not None else None
@dataclass
class CompositeOrigin(Origin):
elements: List[Origin] = field(default_factory=list)
position: Optional[Position] = None
source_text: Optional[str] = None
class Destination(ABC):
pass
@dataclass
class CompositeDestination(Destination):
elements: List[Destination] = field(default_factory=list)
@dataclass
class TextFileDestination(Destination):
position: Optional[Position] = None
def is_internal_property_or_method(value):
return isinstance(value, internal_property) or isinstance(value, InternalField) or isinstance(value, Callable)
def provides_nodes(decl_type):
if get_type_origin(decl_type) is Union:
provides = None
for tp in get_type_arguments(decl_type):
if tp is type(None):
continue
arg_provides = provides_nodes(tp)
if provides is None:
provides = arg_provides
elif provides != arg_provides:
raise Exception(f"Type {decl_type} mixes nodes and non-nodes")
return provides
else:
return isinstance(decl_type, type) and issubclass(decl_type, Node)
def get_only_type_arg(decl_type):
"""If decl_type has a single type argument, return it, otherwise return None"""
type_args = get_type_arguments(decl_type)
if len(type_args) == 1:
return type_args[0]
else:
return None
def process_annotated_property(cl: type, name: str, decl_type):
try:
fields = dataclasses.fields(cl)
except TypeError:
fields = tuple()
for fld in fields:
if fld.name == name and PYLASU_FEATURE in fld.metadata:
feature = fld.metadata[PYLASU_FEATURE]
feature.name = name
if isinstance(decl_type, type):
feature.type = decl_type
elif type(fld.type) is str:
feature.type = try_to_resolve_string_type(fld.type, name, cl)
return feature
return compute_feature_from_annotation(cl, name, decl_type)
def compute_feature_from_annotation(cl, name, decl_type):
feature = PropertyDescription(name, None, False, False, Multiplicity.SINGULAR)
decl_type = try_to_resolve_type(decl_type, feature)
if not isinstance(decl_type, type):
fwref = None
if hasattr(typing, "ForwardRef"):
fwref = typing.ForwardRef
if fwref and isinstance(decl_type, fwref):
raise Exception(f"Feature {name}'s type is unresolved forward reference {decl_type}, "
f"please use node_containment or node_property")
elif type(decl_type) is str:
decl_type = try_to_resolve_string_type(decl_type, name, cl)
if not isinstance(decl_type, type):
raise Exception(f"Unsupported feature {name} of type {decl_type}")
feature.type = decl_type
feature.is_containment = provides_nodes(decl_type) and not feature.is_reference
return feature
def try_to_resolve_string_type(decl_type, name, cl):
try:
ns = getattr(sys.modules.get(cl.__module__, None), '__dict__', globals())
decl_type = ns[decl_type]
except KeyError:
raise Exception(f"Unsupported feature {name} of unknown type {decl_type}")
return decl_type
def try_to_resolve_type(decl_type, feature):
if get_type_origin(decl_type) is ReferenceByName:
decl_type = get_only_type_arg(decl_type) or decl_type
feature.is_reference = True
if is_sequence_type(decl_type):
decl_type = get_only_type_arg(decl_type) or decl_type
feature.multiplicity = Multiplicity.MANY
if get_type_origin(decl_type) is Union:
type_args = get_type_arguments(decl_type)
if len(type_args) == 1:
decl_type = type_args[0]
elif len(type_args) == 2:
if type_args[0] is type(None):
decl_type = type_args[1]
elif type_args[1] is type(None):
decl_type = type_args[0]
else:
raise Exception(f"Unsupported feature {feature.name} of union type {decl_type}")
if feature.multiplicity == Multiplicity.SINGULAR:
feature.multiplicity = Multiplicity.OPTIONAL
else:
raise Exception(f"Unsupported feature {feature.name} of union type {decl_type}")
return decl_type
class Concept(ABCMeta):
def __init__(cls, what, bases=None, dict=None):
super().__init__(what, bases, dict)
cls.__internal_properties__ = []
for base in bases:
if hasattr(base, "__internal_properties__"):
cls.__internal_properties__.extend(base.__internal_properties__)
if not cls.__internal_properties__:
cls.__internal_properties__ = ["origin", "destination", "parent", "position", "position_override"]
cls.__internal_properties__.extend([n for n, v in inspect.getmembers(cls, is_internal_property_or_method)])
@property
def node_properties(cls):
names = set()
for cl in cls.__mro__:
yield from cls._direct_node_properties(cl, names)
def _direct_node_properties(cls, cl, known_property_names):
if not isinstance(cls, Concept):
return
anns = get_type_annotations(cl)
if not anns:
return
for name in anns:
if name not in known_property_names and cls.is_node_property(name):
feature = process_annotated_property(cl, name, anns[name])
known_property_names.add(name)
yield feature
for name in dir(cl):
if name not in known_property_names and cls.is_node_property(name):
feature = PropertyDescription(name, None, False, False)
known_property_names.add(name)
yield feature
def is_node_property(cls, name):
return not name.startswith('_') and name not in cls.__internal_properties__
class Node(Origin, Destination, metaclass=Concept):
origin: Optional[Origin] = None
destination: Optional[Destination] = None
parent: Optional["Node"] = None
position_override: Optional[Position] = None
def __init__(self, origin: Optional[Origin] = None, parent: Optional["Node"] = None,
position_override: Optional[Position] = None):
self.origin = origin
self.parent = parent
self.position_override = position_override
def with_origin(self, origin: Optional[Origin]):
self.origin = origin
return self
def with_parent(self, parent: Optional["Node"]):
self.parent = parent
return self
def with_position(self, position: Optional[Position]):
self.position = position
return self
@internal_property
def position(self) -> Optional[Position]:
return self.position_override if self.position_override is not None\
else self.origin.position if self.origin is not None else None
@position.setter
def position(self, position: Optional[Position]):
self.position_override = position
@internal_property
def source_text(self) -> Optional[str]:
return self.origin.source_text if self.origin is not None else None
@internal_property
def source(self) -> Optional[Source]:
return self.origin.source if self.origin is not None else None
@internal_property
def properties(self):
return (PropertyDescription(p.name, p.type,
is_containment=p.is_containment, is_reference=p.is_reference,
multiplicity=p.multiplicity, value=getattr(self, p.name))
for p in self.__class__.node_properties)
@internal_property
def _fields(self):
yield from (name for name, _ in self.properties)
@internal_property
def node_type(self):
return type(self)
def concept_of(node):
properties = dir(node)
if "__concept__" in properties:
node_type = node.__concept__
elif "node_type" in properties:
node_type = node.node_type
else:
node_type = type(node)
if isinstance(node_type, Concept):
return node_type
else:
raise Exception(f"Not a concept: {node_type} of {node}")