Skip to content

Commit daa9c84

Browse files
committed
#16 Export transpilation traces
1 parent ccab73a commit daa9c84

10 files changed

+241
-64
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ lib/
55
venv
66
build
77
dist
8+
.venv
89
pyvenv.cfg
910
*.egg-info
1011
.eggs/

pylasu/emf/metamodel_builder.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pyecore.ecore import EAttribute, EObject, EPackage, EReference, MetaEClass, EString, EInt
2+
from pyecore.resources import Resource
3+
4+
from pylasu import StrumentaLanguageSupport as starlasu
5+
from pylasu.StrumentaLanguageSupport import ASTNode
6+
from pylasu.model import Node
7+
8+
9+
class MetamodelBuilder:
10+
def __init__(self, package_name: str, ns_uri: str, ns_prefix: str = None, resource: Resource = None):
11+
self.package = EPackage(package_name, ns_uri, ns_prefix)
12+
if resource:
13+
resource.append(self.package)
14+
15+
def can_provide_class(self, cls: type):
16+
return cls.__module__ == self.package.name
17+
18+
def provide_class(self, cls: type):
19+
if not self.can_provide_class(cls):
20+
raise Exception(self.package.name + " cannot provide class " + str(cls))
21+
eclass = self.package.getEClassifier(cls.__name__)
22+
if not eclass:
23+
anns = getannotations(cls)
24+
nmspc = {
25+
"position": EReference("position", starlasu.Position, containment=True)
26+
}
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)
32+
bases = []
33+
for c in cls.__mro__[1:]:
34+
if c == Node:
35+
bases.append(ASTNode)
36+
elif self.can_provide_class(c):
37+
bases.append(self.provide_class(c))
38+
bases.append(EObject)
39+
eclass = MetaEClass(cls.__name__, tuple(bases), nmspc)
40+
eclass.eClass.ePackage = self.package
41+
return eclass
42+
43+
44+
def getannotations(cls):
45+
import inspect
46+
try: # On Python 3.10+
47+
return inspect.getannotations(cls)
48+
except AttributeError:
49+
if isinstance(cls, type):
50+
return cls.__dict__.get('__annotations__', None)
51+
else:
52+
return getattr(cls, '__annotations__', None)

pylasu/emf/model.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from pyecore.ecore import EPackage
2+
from pyecore.resources import Resource
3+
4+
from pylasu.model import Node
5+
from pylasu.support import extension_method
6+
7+
8+
def find_eclass_in_resource(cls: type, resource: Resource):
9+
pkg_name = cls.__module__
10+
for p in resource.contents:
11+
if isinstance(p, EPackage) and p.name == pkg_name:
12+
return p.getEClassifier(cls.__name__)
13+
14+
15+
@extension_method(Resource)
16+
def find_eclass(self: Resource, cls: type):
17+
eclass = find_eclass_in_resource(cls, self)
18+
if not eclass:
19+
for r in (self.resource_set.resources if self.resource_set else []):
20+
if r != self:
21+
eclass = find_eclass_in_resource(cls, r)
22+
if eclass:
23+
return eclass
24+
return eclass
25+
26+
27+
@extension_method(Node)
28+
def to_eobject(self: Node, resource: Resource, mappings=None):
29+
if mappings is None:
30+
mappings = {}
31+
eclass = resource.find_eclass(type(self))
32+
if not eclass:
33+
raise Exception("Unknown eclass for " + str(type(self)))
34+
eobject = eclass()
35+
return eobject

pylasu/playground/__init__.py

Whitespace-only changes.
+27-54
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,27 @@
1-
from io import IOBase, BytesIO
2-
3-
from pyecore.ecore import EAttribute, EObject, EReference, EString, MetaEClass
4-
from pyecore.resources import ResourceSet, URI
5-
from pyecore.resources.json import JsonResource as BaseJsonResource
6-
7-
from pylasu.StrumentaLanguageSupport import Result, Issue
8-
9-
nsURI = "https://strumenta.com/kolasu/transpilation/v1"
10-
name = "StrumentaLanguageSupportTranspilation"
11-
12-
13-
class JsonResource(BaseJsonResource):
14-
15-
def open_out_stream(self, other=None):
16-
if isinstance(other, IOBase):
17-
return other
18-
else:
19-
return super().open_out_stream(other)
20-
21-
22-
class TranspilationTrace(EObject, metaclass=MetaEClass):
23-
# Note: we use camelCase here because Pyecore's JSON serialization doesn't handle having different names for
24-
# Python attributes and their corresponding Ecore structural features.
25-
originalCode = EAttribute(eType=EString)
26-
sourceResult = EReference(containment=True, eType=Result)
27-
targetResult = EReference(containment=True, eType=Result)
28-
generatedCode = EAttribute(eType=EString)
29-
issues = EReference(containment=True, eType=Issue, upper=-1)
30-
31-
def __init__(self, *, original_code=None, source_result=None, target_result=None, generated_code=None, issues=None):
32-
super().__init__()
33-
if original_code is not None:
34-
self.originalCode = original_code
35-
if source_result is not None:
36-
self.sourceResult = source_result
37-
if target_result is not None:
38-
self.targetResult = target_result
39-
if generated_code is not None:
40-
self.generatedCode = generated_code
41-
if issues:
42-
self.issues.extend(issues)
43-
44-
def save_as_json(self, name, *packages):
45-
rset = ResourceSet()
46-
rset.resource_factory['json'] = JsonResource
47-
resource = rset.create_resource(URI(name))
48-
for pkg in packages:
49-
package_resource = rset.create_resource(URI(pkg.nsURI))
50-
package_resource.contents.add(pkg)
51-
resource.contents.append(self)
52-
with BytesIO() as out:
53-
resource.save(out)
54-
return out.getvalue().decode('utf-8')
1+
from dataclasses import dataclass, field
2+
from typing import List
3+
4+
from pyecore.resources import Resource
5+
6+
from pylasu import StrumentaLanguageSupport as starlasu
7+
from pylasu.emf.model import to_eobject
8+
from pylasu.playground.transpilation_trace_ecore import TranspilationTrace as ETranspilationTrace
9+
from pylasu.validation.validation import Result, Issue
10+
11+
12+
@dataclass
13+
class TranspilationTrace:
14+
original_code: str
15+
source_result: Result
16+
target_result: Result
17+
generated_code: str
18+
issues: List[Issue] = field(default_factory=list)
19+
20+
def to_eobject(self, resource: Resource):
21+
mappings = {}
22+
return ETranspilationTrace(
23+
original_code=self.original_code,
24+
source_result=starlasu.Result(root=to_eobject(self.source_result.root, resource, mappings)),
25+
target_result=starlasu.Result(root=to_eobject(self.target_result.root, resource, mappings)),
26+
generated_code=self.generated_code
27+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from io import IOBase, BytesIO
2+
3+
from pyecore.ecore import EObject, MetaEClass, EAttribute, EString, EReference
4+
from pyecore.resources import ResourceSet, URI
5+
6+
from pyecore.resources.json import JsonResource as BaseJsonResource
7+
8+
from pylasu import StrumentaLanguageSupport as starlasu
9+
10+
nsURI = "https://strumenta.com/kolasu/transpilation/v1"
11+
name = "StrumentaLanguageSupportTranspilation"
12+
13+
14+
class JsonResource(BaseJsonResource):
15+
16+
def open_out_stream(self, other=None):
17+
if isinstance(other, IOBase):
18+
return other
19+
else:
20+
return super().open_out_stream(other)
21+
22+
23+
class TranspilationTrace(EObject, metaclass=MetaEClass):
24+
# Note: we use camelCase here because Pyecore's JSON serialization doesn't handle having different names for
25+
# Python attributes and their corresponding Ecore structural features.
26+
originalCode = EAttribute(eType=EString)
27+
sourceResult = EReference(containment=True, eType=starlasu.Result)
28+
targetResult = EReference(containment=True, eType=starlasu.Result)
29+
generatedCode = EAttribute(eType=EString)
30+
issues = EReference(containment=True, eType=starlasu.Issue, upper=-1)
31+
32+
def __init__(self, *, original_code=None, source_result=None, target_result=None, generated_code=None, issues=None):
33+
super().__init__()
34+
if original_code is not None:
35+
self.originalCode = original_code
36+
if source_result is not None:
37+
self.sourceResult = source_result
38+
if target_result is not None:
39+
self.targetResult = target_result
40+
if generated_code is not None:
41+
self.generatedCode = generated_code
42+
if issues:
43+
self.issues.extend(issues)
44+
45+
def save_as_json(self, name, *packages):
46+
rset = ResourceSet()
47+
rset.resource_factory['json'] = JsonResource
48+
resource = rset.create_resource(URI(name))
49+
for pkg in packages:
50+
package_resource = rset.create_resource(URI(pkg.nsURI))
51+
package_resource.contents.add(pkg)
52+
resource.contents.append(self)
53+
with BytesIO() as out:
54+
resource.save(out)
55+
return out.getvalue().decode('utf-8')

pylasu/validation/validation.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import enum
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, field
33
from typing import List
44

55
from pylasu.model import Position, Node
@@ -34,4 +34,4 @@ def __str__(self):
3434
@dataclass
3535
class Result:
3636
root: Node
37-
issues: List[Issue]
37+
issues: List[Issue] = field(default_factory=list)

tests/fixtures.py

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ class Item(Node):
1515
name: str = None
1616

1717

18+
@dataclass
19+
class ReinforcedBox(Box):
20+
strength: int = 10
21+
22+
1823
box = Box(
1924
name="root",
2025
contents=[

tests/test_metamodel_builder.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import unittest
2+
from pyecore.ecore import EObject
3+
4+
from pylasu.emf.metamodel_builder import MetamodelBuilder
5+
from tests.fixtures import Box, ReinforcedBox
6+
7+
8+
class MetamodelBuilderTest(unittest.TestCase):
9+
def test_build_metamodel_single_package(self):
10+
mb = MetamodelBuilder("tests.fixtures", "https://strumenta.com/pylasu/test/fixtures")
11+
box = mb.provide_class(Box)
12+
self.assertIsInstance(box(), EObject)
13+
self.assertEqual(box.eClass.ePackage, mb.package)
14+
self.assertTrue(box.eClass in mb.package.eContents)
15+
self.assertIsNotNone(
16+
next((a for a in box.eClass.eAllAttributes() if a.name == "name"), None))
17+
self.assertEqual(1, len(box.eClass.eAllAttributes()))
18+
19+
def test_build_metamodel_single_package_inheritance(self):
20+
mb = MetamodelBuilder("tests.fixtures", "https://strumenta.com/pylasu/test/fixtures")
21+
box = mb.provide_class(ReinforcedBox)
22+
self.assertIsInstance(box(), EObject)
23+
self.assertEqual(box.eClass.ePackage, mb.package)
24+
self.assertEqual(2, len(mb.package.eContents))
25+
self.assertTrue(box.eClass in mb.package.eContents)
26+
self.assertIsNotNone(
27+
next((a for a in box.eClass.eAllAttributes() if a.name == "name"), None))
28+
self.assertIsNotNone(
29+
next((a for a in box.eClass.eAllAttributes() if a.name == "strength"), None))
30+
self.assertEqual(2, len(box.eClass.eAllAttributes()))

tests/test_transpilation_trace.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import json
22
import unittest
33

4-
import pylasu.StrumentaLanguageSupport as starlasu_metamodel
4+
import pylasu.StrumentaLanguageSupport as starlasu
55
from pylasu.StrumentaLanguageSupport import ASTNode
6+
from pylasu.emf.metamodel_builder import MetamodelBuilder
7+
from pylasu.emf.model import to_eobject
68
from pylasu.playground.transpilation_trace import TranspilationTrace
7-
from pyecore.ecore import EString, EAttribute, EInt
9+
from pylasu.playground.transpilation_trace_ecore import TranspilationTrace as ETranspilationTrace
10+
from pyecore.ecore import EString, EAttribute, EInt, MetaEClass
11+
from pyecore.resources import Resource
812

13+
from pylasu.validation.validation import Result
14+
from tests.fixtures import Box
915

1016
nsURI = "http://mypackage.com"
1117
name = "StrumentaLanguageSupportTranspilationTest"
@@ -26,14 +32,14 @@ def __init__(self, *, name=None, value=None, **kwargs):
2632
class ModelTest(unittest.TestCase):
2733

2834
def test_serialize_transpilation_issue(self):
29-
tt = TranspilationTrace(
35+
tt = ETranspilationTrace(
3036
original_code="a:1", generated_code="b:2",
31-
source_result=starlasu_metamodel.Result(root=ANode(name="a", value=1)),
32-
target_result=starlasu_metamodel.Result(root=ANode(name="b", value=2)),
33-
issues=[starlasu_metamodel.Issue(
34-
type=starlasu_metamodel.IssueType.getEEnumLiteral("TRANSLATION"),
37+
source_result=starlasu.Result(root=ANode(name="a", value=1)),
38+
target_result=starlasu.Result(root=ANode(name="b", value=2)),
39+
issues=[starlasu.Issue(
40+
type=starlasu.IssueType.getEEnumLiteral("TRANSLATION"),
3541
message="some issue",
36-
severity=starlasu_metamodel.IssueSeverity.getEEnumLiteral("WARNING"))]
42+
severity=starlasu.IssueSeverity.getEEnumLiteral("WARNING"))]
3743
)
3844
self.assertEqual("a:1", tt.originalCode)
3945
self.assertEqual("b:2", tt.generatedCode)
@@ -67,3 +73,23 @@ def test_serialize_transpilation_issue(self):
6773
} ]
6874
}"""
6975
self.assertEqual(json.loads(expected), json.loads(tt.save_as_json("foo.json")))
76+
77+
def test_serialize_transpilation_from_nodes(self):
78+
res = Resource()
79+
mmb = MetamodelBuilder("tests.fixtures", "https://strumenta.com/pylasu/test/fixtures", resource=res)
80+
mmb.provide_class(Box)
81+
82+
tt = TranspilationTrace(
83+
original_code="box(a)[foo, bar]", generated_code='<box name="a"><foo /><bar /></box>',
84+
source_result=Result(Box("a")),
85+
target_result=Result(Box("a"))).to_eobject(res)
86+
87+
expected = """{
88+
"eClass": "https://strumenta.com/kolasu/transpilation/v1#//TranspilationTrace",
89+
"generatedCode": "<box name=\\"a\\"><foo /><bar /></box>",
90+
"originalCode": "box(a)[foo, bar]",
91+
"sourceResult": {"root": {"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box"}},
92+
"targetResult": {"root": {"eClass": "https://strumenta.com/pylasu/test/fixtures#//Box"}}
93+
}"""
94+
as_json = tt.save_as_json("foo.json")
95+
self.assertEqual(json.loads(expected), json.loads(as_json))

0 commit comments

Comments
 (0)