Motra is a librairie providing model transformations facilities to PyEcore. The goal of the librairie is to propose a set of embedded DSLs in Python for models to models transformations (M2M) and model to text transformations (M2T) with advanced traceability mechanism. Here are some characteristics about Motra M2M:
- it proposes a semantic close to QVTo, imperative, based on
mappings
where the execution order is defined by the developer in each mapping, - it supports multiple input and multiple outputs,
- each
mapping
result is cached and when a mapping is called twice with a same set of parameters, the exact same created object is returned, - by default, any object created in a
mapping
that is not explicitaly placed in a container is automatically added as model root, - used metamodels are automatically registered for smooth load/save of any models.
- it supports
mapping
polymorphism without having to rely on manual coding a dispatch with adisjunct
(if the mappings own the same name)
Documentation is still a WIP and can be found here: https://motra.readthedocs.io/.
For more examples, please refer to transformations examples in examples
.
To avoid the need to load/install special metamodels, all the transformations examples are given directly over Ecore.
The transformations are gathered in simple modules depending to their characteristics: in-place, in-out, endogenous or exogenous.
Each transformation must be defined in it's own Python module (even if multiple transformations can be defined in one module).
# import the input and output metamodels
import ghmde # based on https://github.com/kolovos/datasets/blob/master/github-mde/ghmde.ecore
import graph # based on a simple graph metamodel
# import motra for utils and for M2M transformation definition
import motra
from motra import m2m
# M2M transformation "signature" definition
ghmde2graph = m2m.Transformation('ghmde2graph',
inputs=['ghmde_model'],
outputs=['graph_model'])
# defines the entry point of the transformation
@ghmde2graph.main
def main(ghmde_model, graph_model):
print('Transforming repository to graph', graph_model)
for f in motra.objects_of_kind(ghmde_model, ghmde.File):
file2node(f)
for repository in motra.objects_of_kind(ghmde_model, ghmde.Repository):
repository2graph(repository, postfix='_graph')
# m2m.objects_of_kind
# defines a first mapping transforming Files in Node
@ghmde2graph.mapping
def file2node(self: ghmde.File) -> Node:
result.name = self.path # The "result" variable is automatically created and injected in the current context
# defines a conditional mapping from Repository to Graph
def does_not_starts_with(self, postfix):
return not self.name.startswith(postfix)
@ghmde2graph.mapping(when=does_not_starts_with)
def repository2graph(self: ghmde.Repository, postfix: str) -> Graph:
result.name = self.name + postfix
for repo_file in self.files:
result.nodes.append(file2node(repo_file))
Then, it can be imported and directly used from another module. Currently, there is no default runner, but there will be in the future, a way of defining models transformations chains.
# Import the transformation
from transfo_example import ghmde2graph
# Just run it. Input can be a "Resource" or directly a file
result_context = ghmde2graph.run(ghmde_model="input_model.xmi")*
# A result context gives access to:
# * the inputs
# * the outputs
# * the execution trace (still WIP)
# * the transformation definition
# * the used resource set for this transformation
result_context.inputs.ghmde_model.save(output="input_copy.xmi")
result_context.outputs.graph_model.save(output="test.xmi")
As for M2M, a M2T transformation must be defined in it's own Python module (even if multiple transformations can be defined in one module as defined in the module examples/m2t/sample.py
).
Each template code is written as __doc__
of template functions.
from motra import m2t
import pyecore.ecore as ecore
# M2T transformation "signature" definition
ecore2simplejava = m2t.Transformation("ecore2simplejava")
# Definition of the main entry point.
# At the moment, entry-point cannot have "when=" parameter
# The special <%motra:file ><%/motra:file> is used to specify blocs
# where the code must be written. Multiple "file" tags can be introduced by template.
@ecore2simplejava.main
def eclass2class(self: ecore.EClass):
"""
<%motra:file path="examples/outputs/${self.ePackage.name}/${self.name}.java">
public class ${self.name.capitalize()} {
% for feature in self.eStructuralFeatures:
// ${override(feature)}
${feature2attribute(feature)}
% endfor
}
</%motra:file>
"""
@ecore2simplejava.template(
when=lambda self: self.many
)
def feature2attribute(self: ecore.EAttribute):
"""List<${self.eType.name}> ${self.name}; // many attribute"""
@ecore2simplejava.template
def feature2attribute(self: ecore.EAttribute):
"""${self.eType.name} ${self.name}; // single attribute"""
@ecore2simplejava.template(
when=lambda self: self.many
)
def feature2attribute(self: ecore.EReference):
"""List<${self.eType.name}> ${self.name}; // many reference"""
@ecore2simplejava.template
def feature2attribute(self: ecore.EReference):
"""List<${self.eType.name}> ${self.name}; // single reference"""
@ecore2simplejava.template
def override(self: ecore.EAttribute):
"""Attribut ${self.name}: ${self.eType.name} [${self.lowerBound}..${upper2symbol(self)}]"""
@ecore2simplejava.template
def override(self: ecore.EReference):
"""Reference ${self.name}: ${self.eType.name} [${self.lowerBound}..${upper2symbol(self)}]"""
@ecore2simplejava.helper
def upper2symbol(self: ecore.EStructuralFeature):
return '*' if self.many else self.upperBound
Then, it can be imported and directly used from another module. Currently, there is no default runner, but there will be in the future, a way of defining models transformations chains.
# Import the transformation
from examples.m2t.sample import ecore2simplejava
# Just run it. Input can be a "Resource", a model or directly a file
ecore2simplejava.run('examples/inputs/input.ecore')