Skip to content

Commit ef98171

Browse files
committed
#15 and #16: improve Ecore support and support of transpilation traces.
1 parent bebfc3c commit ef98171

8 files changed

+169
-30
lines changed

pylasu/emf/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .metamodel_builder import MetamodelBuilder
21
from .model import *
2+
from .metamodel_builder import MetamodelBuilder

pylasu/emf/metamodel_builder.py

+80-7
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,107 @@
1-
from pyecore.ecore import EAttribute, EObject, EPackage, EReference, MetaEClass, EString, EInt
1+
import typing
2+
from dataclasses import is_dataclass, fields
3+
from enum import Enum, EnumMeta
4+
from types import resolve_bases
5+
6+
from pyecore.ecore import EAttribute, ECollection, EObject, EPackage, EReference, MetaEClass, EBoolean, EString, EInt, EEnum
27
from pyecore.resources import Resource
38

49
from pylasu import StrumentaLanguageSupport as starlasu
510
from pylasu.StrumentaLanguageSupport import ASTNode
11+
from pylasu.emf.model import find_eclassifier
612
from pylasu.model import Node
13+
from pylasu.model.model import InternalField
714

815

916
class MetamodelBuilder:
1017
def __init__(self, package_name: str, ns_uri: str, ns_prefix: str = None, resource: Resource = None):
1118
self.package = EPackage(package_name, ns_uri, ns_prefix)
1219
if resource:
1320
resource.append(self.package)
21+
self.data_types = {
22+
bool: EBoolean,
23+
int: EInt,
24+
str: EString,
25+
}
26+
self.forward_references = []
1427

1528
def can_provide_class(self, cls: type):
1629
return cls.__module__ == self.package.name
1730

1831
def provide_class(self, cls: type):
32+
if cls == Node:
33+
return ASTNode
1934
if not self.can_provide_class(cls):
35+
if self.package.eResource:
36+
eclass = find_eclassifier(self.package.eResource, cls)
37+
if eclass:
38+
return eclass
2039
raise Exception(self.package.name + " cannot provide class " + str(cls))
2140
eclass = self.package.getEClassifier(cls.__name__)
2241
if not eclass:
2342
anns = getannotations(cls)
2443
nmspc = {
2544
"position": EReference("position", starlasu.Position, containment=True)
2645
}
27-
for attr in anns:
28-
if anns[attr] == str:
29-
nmspc[attr] = EAttribute(attr, EString)
30-
elif anns[attr] == int:
31-
nmspc[attr] = EAttribute(attr, EInt)
46+
for attr in anns if anns else []:
47+
if is_dataclass(cls):
48+
field = next((f for f in fields(cls) if f.name == attr), None)
49+
if isinstance(field, InternalField):
50+
continue
51+
attr_type = anns[attr]
52+
nmspc[attr] = self.to_structural_feature(attr, attr_type)
3253
bases = []
3354
for c in cls.__mro__[1:]:
3455
if c == Node:
3556
bases.append(ASTNode)
3657
elif self.can_provide_class(c):
3758
bases.append(self.provide_class(c))
59+
elif self.package.eResource:
60+
esuperclass = find_eclassifier(self.package.eResource, c)
61+
if esuperclass:
62+
bases.append(esuperclass)
3863
bases.append(EObject)
39-
eclass = MetaEClass(cls.__name__, tuple(bases), nmspc)
64+
eclass = MetaEClass(cls.__name__, resolve_bases(tuple(bases)), nmspc)
4065
eclass.eClass.ePackage = self.package
66+
for (type_name, ref) in self.forward_references:
67+
if type_name == cls.__name__:
68+
ref.eType = eclass
69+
self.forward_references = [(t, r) for t, r in self.forward_references if not r.eType]
4170
return eclass
4271

72+
def to_structural_feature(self, attr, attr_type):
73+
if isinstance(attr_type, str):
74+
resolved = self.package.getEClassifier(attr_type)
75+
if resolved:
76+
return EReference(attr, resolved, containment=True)
77+
else:
78+
forward_reference = EReference(attr, containment=True)
79+
self.forward_references.append((attr_type, forward_reference))
80+
return forward_reference
81+
elif attr_type in self.data_types:
82+
return EAttribute(attr, self.data_types[attr_type])
83+
elif attr_type == object:
84+
return EAttribute(attr)
85+
elif isinstance(attr_type, type) and issubclass(attr_type, Node):
86+
return EReference(attr, self.provide_class(attr_type), containment=True)
87+
elif typing.get_origin(attr_type) == list:
88+
type_args = typing.get_args(attr_type)
89+
if type_args and len(type_args) == 1:
90+
ft = self.to_structural_feature(attr, type_args[0])
91+
ft.upperBound = -1
92+
return ft
93+
elif isinstance(attr_type, EnumMeta) and issubclass(attr_type, Enum):
94+
tp = EEnum(name=attr_type.__name__, literals=attr_type.__members__)
95+
tp.ePackage = self.package
96+
self.data_types[attr_type] = tp
97+
return EAttribute(attr, tp)
98+
else:
99+
raise Exception("Unsupported type " + str(attr_type) + " for attribute " + attr)
100+
43101
def generate(self):
102+
if self.forward_references:
103+
raise Exception("The following classes are missing from " + self.package.name + ": " +
104+
", ".join(n for n, _ in self.forward_references))
44105
return self.package
45106

46107

@@ -53,3 +114,15 @@ def getannotations(cls):
53114
return cls.__dict__.get('__annotations__', None)
54115
else:
55116
return getattr(cls, '__annotations__', None)
117+
118+
119+
# Monkey patch until fix
120+
update_opposite = ECollection._update_opposite
121+
122+
123+
def update_opposite_if_not_none(self, owner, new_value, remove=False):
124+
if owner:
125+
update_opposite(self, owner, new_value, remove)
126+
127+
128+
ECollection._update_opposite = update_opposite_if_not_none

pylasu/emf/model.py

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,64 @@
1+
from enum import Enum
2+
13
from pyecore.ecore import EPackage
24
from pyecore.resources import Resource
35

46
from pylasu.model import Node
57
from pylasu.support import extension_method
68

79

8-
def find_eclass_in_resource(cls: type, resource: Resource):
10+
def find_eclassifier_in_resource(cls: type, resource: Resource):
911
pkg_name = cls.__module__
1012
for p in resource.contents:
1113
if isinstance(p, EPackage) and p.name == pkg_name:
1214
return p.getEClassifier(cls.__name__)
1315

1416

1517
@extension_method(Resource)
16-
def find_eclass(self: Resource, cls: type):
17-
eclass = find_eclass_in_resource(cls, self)
18+
def find_eclassifier(self: Resource, cls: type):
19+
eclass = find_eclassifier_in_resource(cls, self)
1820
if not eclass:
1921
for uri in (self.resource_set.resources if self.resource_set else {}):
2022
resource = self.resource_set.resources[uri]
2123
if resource != self:
22-
eclass = find_eclass_in_resource(cls, resource)
24+
eclass = find_eclassifier_in_resource(cls, resource)
2325
if eclass:
2426
return eclass
2527
return eclass
2628

2729

2830
@extension_method(Node)
2931
def to_eobject(self: Node, resource: Resource, mappings=None):
32+
if self is None:
33+
return None
3034
if mappings is None:
3135
mappings = {}
32-
eclass = resource.find_eclass(type(self))
36+
elif id(self) in mappings:
37+
return mappings[self]
38+
eclass = resource.find_eclassifier(type(self))
3339
if not eclass:
34-
raise Exception("Unknown eclass for " + str(type(self)))
40+
raise Exception("Unknown classifier for " + str(type(self)))
3541
eobject = eclass()
42+
mappings[id(self)] = eobject
43+
for (p, v) in self.properties:
44+
ev = translate_value(v, resource, mappings)
45+
if isinstance(v, list):
46+
eobject.eGet(p).extend(ev)
47+
else:
48+
eobject.eSet(p, ev)
3649
return eobject
50+
51+
52+
def translate_value(v, resource, mappings):
53+
if isinstance(v, Enum):
54+
enum_type = resource.find_eclassifier(type(v))
55+
if enum_type:
56+
return enum_type.getEEnumLiteral(v.name)
57+
else:
58+
raise Exception("Unknown enum " + str(type(v)))
59+
if isinstance(v, list):
60+
return [translate_value(x, resource, mappings) for x in v]
61+
elif isinstance(v, Node):
62+
return to_eobject(v, resource, mappings)
63+
else:
64+
return v

pylasu/model/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .model import Node, Origin
1+
from .model import Node, Origin, internal_field, internal_properties
22
from .position import Point, Position, pos
33
from .traversing import walk, walk_leaves_first
44
from .processing import children

pylasu/model/model.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
from abc import ABC, abstractmethod
3+
from dataclasses import Field, MISSING
34
from typing import Optional, Callable
45

56
from .position import Position, Source
@@ -16,6 +17,19 @@ def decorate(cls: type):
1617
return decorate
1718

1819

20+
class InternalField(Field):
21+
pass
22+
23+
24+
def internal_field(
25+
*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None):
26+
"""Return an object to identify internal dataclass fields. The arguments are the same as dataclasses.field."""
27+
28+
if default is not MISSING and default_factory is not MISSING:
29+
raise ValueError('cannot specify both default and default_factory')
30+
return InternalField(default, default_factory, init, repr, hash, compare, metadata)
31+
32+
1933
class Origin(ABC):
2034
@internal_property
2135
@abstractmethod
@@ -32,14 +46,14 @@ def source(self) -> Optional[Source]:
3246

3347

3448
def is_internal_property_or_method(value):
35-
return isinstance(value, internal_property) or isinstance(value, Callable)
49+
return isinstance(value, internal_property) or isinstance(value, InternalField) or isinstance(value, Callable)
3650

3751

3852
class Node(Origin):
3953
origin: Optional[Origin] = None
4054
parent: Optional["Node"] = None
4155
position_override: Optional[Position] = None
42-
__internal_properties__ = ["origin", "parent", "position"]
56+
__internal_properties__ = ["origin", "parent", "position", "position_override"]
4357

4458
def __init__(self, origin: Optional[Origin] = None, parent: Optional["Node"] = None,
4559
position_override: Optional[Position] = None):

tests/test_model.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ def test_scope_lookup_4(self):
106106
upper_symbol = SomeSymbol(name='a', index=0)
107107
scope = Scope(symbols={'b': [SomeSymbol(name='b', index=0)]}, parent=Scope(symbols={'a': [upper_symbol]}))
108108
result = scope.lookup(symbol_name='a', symbol_type=SomeSymbol)
109-
self.assertEquals(result, upper_symbol)
109+
self.assertEqual(result, upper_symbol)
110110
self.assertIsInstance(result, SomeSymbol)
111111

112112
def test_scope_lookup_5(self):
113113
"""Symbol found in upper scope with name and type (local with different type)"""
114114
upper_symbol = SomeSymbol(name='a', index=0)
115115
scope = Scope(symbols={'a': [AnotherSymbol(name='a', index=0)]}, parent=Scope(symbols={'a': [upper_symbol]}))
116116
result = scope.lookup(symbol_name='a', symbol_type=SomeSymbol)
117-
self.assertEquals(result, upper_symbol)
117+
self.assertEqual(result, upper_symbol)
118118
self.assertIsInstance(result, SomeSymbol)
119119

120120
def test_scope_lookup_6(self):

tests/test_processing.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ def test_replace_in_list(self):
3838
b = BW(a1, [a2, a3])
3939
b.assign_parents()
4040
a2.replace_with(a4)
41-
self.assertEquals("4", b.many_as[0].s)
42-
self.assertEquals(BW(a1, [a4, a3]), b)
41+
self.assertEqual("4", b.many_as[0].s)
42+
self.assertEqual(BW(a1, [a4, a3]), b)
4343

4444
def test_replace_in_set(self):
4545
a1 = AW("1")
@@ -56,4 +56,4 @@ def test_replace_single(self):
5656
b = BW(a1, [])
5757
b.assign_parents()
5858
a1.replace_with(a2)
59-
self.assertEquals("2", b.a.s)
59+
self.assertEqual("2", b.a.s)

tests/test_transpilation_trace.py

+32-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pylasu.emf import MetamodelBuilder
99
from pylasu.playground import TranspilationTrace, ETranspilationTrace
1010
from pylasu.validation.validation import Result
11-
from tests.fixtures import Box
11+
from tests.fixtures import Box, Item
1212

1313
nsURI = "http://mypackage.com"
1414
name = "StrumentaLanguageSupportTranspilationTest"
@@ -74,18 +74,42 @@ def test_serialize_transpilation_issue(self):
7474
def test_serialize_transpilation_from_nodes(self):
7575
mmb = MetamodelBuilder("tests.fixtures", "https://strumenta.com/pylasu/test/fixtures")
7676
mmb.provide_class(Box)
77+
mmb.provide_class(Item)
7778

7879
tt = TranspilationTrace(
79-
original_code="box(a)[foo, bar]", generated_code='<box name="a"><foo /><bar /></box>',
80-
source_result=Result(Box("a")),
81-
target_result=Result(Box("a")))
80+
original_code="box(a)[i1, bar]", generated_code='<box name="A"><i1 /><bar /></box>',
81+
source_result=Result(Box("a", [Item("i1"), Box("b", [Item("i2"), Item("i3")])])),
82+
target_result=Result(Box("A")))
8283

8384
expected = """{
8485
"eClass": "https://strumenta.com/kolasu/transpilation/v1#//TranspilationTrace",
85-
"generatedCode": "<box name=\\"a\\"><foo /><bar /></box>",
86-
"originalCode": "box(a)[foo, bar]",
87-
"sourceResult": {"root": {"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box"}},
88-
"targetResult": {"root": {"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box"}}
86+
"generatedCode": "<box name=\\"A\\"><i1 /><bar /></box>",
87+
"originalCode": "box(a)[i1, bar]",
88+
"sourceResult": { "root": {
89+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box",
90+
"name": "a",
91+
"contents": [{
92+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Item",
93+
"name": "i1"
94+
}, {
95+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box",
96+
"name": "b",
97+
"contents": [{
98+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Item",
99+
"name": "i2"
100+
}, {
101+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Item",
102+
"name": "i3"
103+
}]
104+
}]
105+
}
106+
},
107+
"targetResult": { "root": {
108+
"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box",
109+
"name": "A",
110+
"contents": []
111+
}
112+
}
89113
}"""
90114
as_json = tt.save_as_json("foo.json", mmb.generate())
91115
self.assertEqual(json.loads(expected), json.loads(as_json))

0 commit comments

Comments
 (0)