Quick Start¶
Quick Overview¶
PyEcore is a “Pythonic” implementation of EMF/Ecore for Python 3. Its purpose is to handle model/metamodels in Python almost the same way the Java version does.
However, PyEcore enables you to use a simple instance.attribute
notation
instead of instance.setAttribute(...)/getAttribute(...)
for the Java
version. To achieve this, PyEcore relies on reflection (a lot).
Let see by yourself how it works on a very simple metamodel created on the fly (dynamic metamodel):
>>> from pyecore.ecore import EClass, EAttribute, EString, EObject
>>> A = EClass('A') # We create metaclass named 'A'
>>> A.eStructuralFeatures.append(EAttribute('myname', EString, default_value='new_name')) # We add a name attribute to the A metaclass
>>> a1 = A() # We create an instance
>>> a1.myname
'new_name'
>>> a1.myname = 'a_instance'
>>> a1.myname
'a_instance'
>>> isinstance(a1, EObject)
True
PyEcore also support introspection and the EMF reflexive API using basic Python reflexive features:
>>> a1.eClass # some introspection
<EClass name="A">
>>> a1.eClass.eClass
<EClass name="EClass">
>>> a1.eClass.eClass is a1.eClass.eClass.eClass
True
>>> a1.eClass.eStructuralFeatures
EOrderedSet([<EStructuralFeature myname: EString(str)>])
>>> a1.eClass.eStructuralFeatures[0].name
'myname'
>>> a1.eClass.eStructuralFeatures[0].eClass
<EClass name="EAttribute">
>>> a1.__getattribute__('myname')
'a_instance'
>>> a1.__setattr__('myname', 'reflexive')
>>> a1.__getattribute__('myname')
'reflexive'
>>> a1.eSet('myname', 'newname')
>>> a1.eGet('myname')
'newname'
Runtime type checking is also performed, based on what is defined in the metamodel:
>>> a1.myname = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../pyecore/ecore.py", line 66, in setattr
raise BadValueError(got=value, expected=estruct.eType)
pyecore.ecore.BadValueError: Expected type EString(str), but got type int with value 1 instead
PyEcore supports static and dynamic metamodels. These are described in the next sections.
Dynamic Metamodels¶
Dynamic metamodels provide the ability to create metamodels “on-the-fly”. You
can create metaclass hierarchy, add EAttribute
and EReference
.
In order to create a new metaclass, you need to create an EClass
instance:
>>> import pyecore.ecore as Ecore
>>> MyMetaclass = Ecore.EClass('MyMetaclass')
You can then create instances of your metaclass:
>>> instance1 = MyMetaclass()
>>> instance2 = MyMetaclass()
>>> assert instance1 is not instance2
From the created instances, we can go back to the metaclasses:
>>> instance1.eClass
<EClass name="MyMetaclass">
Then, we can add metaproperties to the freshly created metaclass:
>>> instance1.eClass.eAttributes
[]
>>> MyMetaclass.eStructuralFeatures.append(Ecore.EAttribute('name', Ecore.EString)) # Add 'name' attribute, of type string
>>> instance1.eClass.eAttributes # Is there a new feature?
[<pyecore.ecore.EAttribute object at 0x7f7da72ba940>] # Yep, the new feature is here!
>>> str(instance1.name) # There is a default value for the new attribute
'None'
>>> instance1.name = 'mystuff'
>>> instance1.name
'mystuff'
>>> # Now the feature name may be used as a keyword argument in the constructor
>>> instance3 = MyMetaclass(name='myname')
>>> instance3.name
'myname'
We can also create a new metaclass B
and a new meta-reference to B
:
>>> B = Ecore.EClass('B')
>>> MyMetaclass.eStructuralFeatures.append(Ecore.EReference('toB', B, containment=True))
>>> b1 = B()
>>> instance1.toB = b1
>>> instance1.toB
<pyecore.ecore.B object at 0x7f7da70531d0>
>>> b1.eContainer() is instance1 # because 'toB' is a containment reference
True
Opposite and ‘collection’ meta-references are also managed:
>>> C = Ecore.EClass('C')
>>> C.eStructuralFeatures.append(Ecore.EReference('toMy', MyMetaclass))
>>> MyMetaclass.eStructuralFeatures.append(Ecore.EReference('toCs', C, upper=-1, eOpposite=C.eStructuralFeatures[0]))
>>> instance1.toCs
[]
>>> c1 = C()
>>> c1.toMy = instance1
>>> instance1.toCs # 'toCs' should contain 'c1' because 'toMy' is opposite relation of 'toCs'
[<pyecore.ecore.C object at 0x7f7da7053390>]
Explore Dynamic metamodel/model objects¶
It is possible, when you are handling an object in the Python console, to ask
for all the meta-attributes/meta-references and meta-operations that can
be called on it using dir()
. This will work with either a dynamic metamodel
object or a model instance. This allows you to quickly experiment and find the
information you are looking for:
>>> A = EClass('A')
>>> dir(A)
['abstract',
'delete',
'eAllContents',
'eAllOperations',
'eAllReferences',
'eAllStructuralFeatures',
'eAllSuperTypes',
'eAnnotations',
'eAttributes',
'eContainer',
# <snip ... many more >
'findEOperation',
'findEStructuralFeature',
'getEAnnotation',
'instanceClassName',
'interface',
'name']
>>> a = A()
>>> dir(a)
[]
>>> A.eStructuralFeatures.append(EAttribute('myname', EString))
>>> dir(a)
['myname']
Enhance the Dynamic metamodel¶
Even if you define or use a dynamic metamodel, you can add dedicated methods
(e.g: __repr__
) to the equivalent Python class. Each EClass
instance is
linked to a Python class which can be reached using the python_class
field:
>>> A = EClass('A')
>>> A.python_class
pyecore.ecore.A
You can directly add new “non-metamodel” methods to this class:
>>> a = A()
>>> a
<pyecore.ecore.A at 0x7f4f0839b7b8>
>>> A.python_class.__repr__ = lambda self: 'My repr for real'
>>> a
My repr for real
Static Metamodels¶
The static definition of a metamodel using PyEcore mostly relies on the
classical class definitions in Python. Each Python class is linked to an
EClass
instance and has a special metaclass. The static code for metamodel
also provides a model layer and the ability to easily refer/navigate inside the
defined meta-layer.
# 'library' directory content
# +- library
# `- __init__.py library.py
# 'library/library.py' content
# ... stuffs here
class Writer(EObject, metaclass=MetaEClass):
name = EAttribute(eType=EString)
books = EReference(upper=-1)
def __init__(self, name=None, books=None, **kwargs):
if kwargs:
raise AttributeError('unexpected arguments: {}'.format(kwargs))
super().__init__()
if name is not None:
self.name = name
if books:
self.books.extend(books)
# ... Other stuff here
# Session example
>>> import library
>>> # we can create elements and handle the model level
>>> smith = library.Writer(name='smith')
>>> book1 = library.Book(title='Ye Old Book1')
>>> book1.pages = 100
>>> smith.books.append(book1)
>>> assert book1 in smith.books
>>> assert smith in book1.authors
>>> # ...
>>> # We can also navigate the meta-level
>>> import pyecore.ecore as Ecore # import the Ecore metamodel for tests
>>> assert isinstance(library.Book.authors, Ecore.EReference)
>>> library.Book.authors.upperBound
-1
>>> assert isinstance(library.Writer.name, Ecore.EAttribute)
There are two main ways of creating static EClass
with PyEcore. The first
one relies on code generation while the second one uses manual
definition.
The code generator defines a Python package hierarchy instead of just a Python module. This allows more freedom for dedicated operations and references between packages.
How to Generate the Static Metamodel Code¶
The static code is generated from an .ecore
where your metamodel is defined
(the EMF .genmodel
files are not yet supported (probably in future version).
There are currently two ways of generating the code for your metamodel. The first
one is to use a MTL generator (in /generator
) and the second one is to use a
dedicated command line tool written in Python, using Pymultigen, Jinja and PyEcore.
Using the Acceleo/MTL Generator¶
Note: the Acceleo generator is deprecated. Use of pyecoregen is now prefered.
To use this generator, you need Eclipse and the right Acceleo plugins. Once
Eclipse is installed with the right plugins, you need to create a new Acceleo
project, copy the PyEcore generator in it, configure a new Acceleo runner,
select your .ecore
and your good to go. There is plenty of documentation
on Internet for Acceleo/MTL project creation and management.
Using the Dedicated CLI Generator (PyEcoregen)¶
For more complex metamodels and more robust generation, pyecoregen is better, and is the recommended solution for the static metamodel code generation. Advantages (over Acceleo) include:
Provides a simple command line interface
Provides the cability to perform generation programmatically
Provides a framework for code generation WITHIN PyEcore
Allows you to code dedicated behavior in mixin classes,
Much simpler installation, with all dependencies, using pip.
This generator source code can be found at this address with mode details: https://github.com/pyecore/pyecoregen and is available on Pypi, so you can install it using:
$ pip install pyecoregen
This will automatically install all the required dependencies and give you a new
CLI tool: pyecoregen
.
Using this tool, your static code generation is very simple:
$ pyecoregen -e your_ecore_file.ecore -o your_output_path
Once the code is generated, you can import it and use it in your Python code.
Manually define static EClass
¶
To manually define static EClass
, simply create a Python class, and
add the @EMetaclass
class decorator. This decorator adds the right
metaclass to the defined class, and introduces any missing classes in its
inheritance tree. Defining a simple metaclass can be done like:
@EMetaclass
class Person(object):
name = EAttribute(eType=EString)
age = EAttribute(eType=EInt)
children = EReference(upper=-1, containment=True)
def __init__(self, name):
self.name = name
# As the relation is reflexive, it must be set AFTER metaclass creation
Person.children.eType = Person
p1 = Person('Parent')
p1.children.append(Person('Child'))
Without more information, all the created metaclasses will be added to a default
EPackage
, generated automatically. If the EPackage
must be controlled, a
global variable of EPackage
type, named eClass
, must be created in the
module.
eClass = EPackage(name='pack', nsURI='http://pack/1.0', nsPrefix='pack')
@EMetaclass
class TestMeta(object):
pass
assert TestMeta.eClass.ePackage is eClass
However, when @EMetaclass
is used and the newly created metaclass
inherits from a non metaclass (see the example below), the super()
call in the __init__
constructor cannot be directly called. Instead,
super(x, self)
must be called:
class Stuff(object):
def __init__(self):
self.stuff = 10
@EMetaclass
class A(Stuff):
def __init__(self, tmp):
super(A, self).__init__()
self.tmp = tmp
a = A()
assert a.stuff == 10
If you want to directly extends the PyEcore metamodel, the @EMetaclass
is
not required, and super()
can be used.
class MyNamedElement(ENamedElement):
def __init__(self, tmp=None, **kwargs):
super().__init__(**kwargs)
self.tmp = tmp
Ask for all created instances of an EClass¶
PyEcore keeps track of all created instances. You can then ask for the created
instances of a particular type using the allInstances()
method on
a metaclass. The result is a generator that contains all the instances of the
right type:
from pyecore.ecore import EClass
# Find all the instances of an EClass
for x in EClass.allInstances():
print(x)
# Create an EClass instance and some instances of this new metaclass
A = EClass('A')
a1 = A()
a2 = A()
a3 = A()
print(list(A.allInstances()) # displays 3 instances
del a1
print(list(A.allInstances()) # displays 2 instances
As all the created elements are returned, when multiple resources
are loaded it can be difficult sometimes to only filter elements from dedicated
resources. The allInstances()
method takes an optional argument:
resources
which let you specify which resources should be searched
for instances.
from pyecore.ecore import EClass
from pyecore.resources import ResourceSet
# We will create an EClass instance and some instance of this new metaclass
A = EClass('A')
a1 = A()
a2 = A()
a3 = A()
# We distribute each instance in a different resources
rset = ResourceSet()
resource1 = rset.create_resource('http://virtual1')
resource2 = rset.create_resource('http://virtual2')
resource3 = rset.create_resource('http://virtual3')
resource1.append(a1)
resource2.append(a2)
resource3.append(a3)
print(list(A.allInstances(resources=(resource1, resource2)))) # a1 and a2
print(list(A.allInstances(resources=(resource3,)))) # a3
Programmatically Create a Metamodel and Serialize it¶
Creating a metamodel programmatically is the same as creating a dynamic
metamodel. You create EClass
instances, add EAttributes
and
EReferences
to them, and add the instances in an EPackage
. For example
here is a little snippet that creates two metaclasses and adds them to the
an EPackage
instance:
from pyecore.ecore import *
# Define a Root that can contain A and B instances,
# B instances can hold references towards A instances
Root = EClass('Root')
A = EClass('A')
B = EClass('B')
A.eStructuralFeatures.append(EAttribute('name', EString))
B.eStructuralFeatures.append(EReference('to_many_a', A, upper=-1))
Root.eStructuralFeatures.append(EReference('a_container', A, containment=True))
Root.eStructuralFeatures.append(EReference('b_container', B, contaimnent=True))
# Add all the concepts to an EPackage
my_ecore_schema = EPackage('my_ecor', nsURI='http://myecore/1.0', nsPrefix='myecore')
my_ecore_schema.eClassifiers.extend([Root, A, B])
Then, in order to serialize it, it is simply a matter of adding the created
EPackage
to a Resource
(more details about Resource
are provided in
the sections Importing an Existing XMI Metamodel/Model, Exporting an
Existing XMI Resource and Dealing with JSON Resources). Here is an
example of how the created metamodel could be serialized in an XMI format:
from pyecore.resources import ResourceSet, URI
rset = ResourceSet()
resource = rset.create_resource(URI('my/location/my_ecore_schema.ecore')) # This will create an XMI resource
resource.append(my_ecore_schema) # we add the EPackage instance in the resource
resource.save() # we then serialize it
This process is identical to the one you would apply for serializing almost any kind of model.
Notifications¶
PyEcore gives you the ability to listen to modifications performed on an
element. The EObserver
class provides a basic observer which can receive
notifications from an EObject
. It is registered like:
>>> import library as lib # we use the wikipedia library example
>>> from pyecore.notification import EObserver, Kind
>>> smith = lib.Writer()
>>> b1 = lib.Book()
>>> observer = EObserver(smith, notifyChanged=lambda x: print(x))
>>> b1.authors.append(smith) # observer receive the notification from smith because 'authors' is eOpposite or 'books'
The EObserver
notification method can be set using a function (or lambda, as shown)
or by class inheritance:
>>> def print_notif(notification):
... print(notification)
...
>>> observer = EObserver()
>>> observer.observe(b1)
>>> observer.notifyChanged = print_notif
>>> b1.authors.append(smith) # observer receive the notification from b1
Using inheritance:
>>> class PrintNotification(EObserver):
... def __init__(self, notifier=None):
... super().__init__(notifier=notifier)
...
... def notifyChanged(self, notification):
... print(notification)
...
...
>>> observer = PrintNotification(b1)
>>> b1.authors.append(smith) # observer receive the notification from b1
The Notification
object contains information about the performed
modification:
new
-> the new added value (can be a collection) orNone
is remove or unsetold
-> the replaced value (alwaysNone
for collections)feature
-> theEStructuralFeature
modifiednotifier
-> the object that has been modifiedkind
-> the kind of modification performed
kind
is one of:
ADD
-> an object is added to a collectionADD_MANY
-> multiple objects are added to a collectionREMOVE
-> an object is removed from a collectionSET
-> a value is set in an attribute/referenceUNSET
-> a value is removed from an attribute/reference
Importing an Existing XMI Metamodel/Model¶
XMI support is still a little rough on the edges, but the XMI import is on good tracks.
Currently, only basic XMI metamodel (.ecore
) and model instances can be
loaded:
>>> from pyecore.resources import ResourceSet, URI
>>> rset = ResourceSet()
>>> resource = rset.get_resource(URI('path/to/mm.ecore'))
>>> mm_root = resource.contents[0]
>>> rset.metamodel_registry[mm_root.nsURI] = mm_root
>>> # At this point, the .ecore is loaded in the 'rset' as a metamodel
>>> resource = rset.get_resource(URI('path/to/instance.xmi'))
>>> model_root = resource.contents[0]
>>> # At this point, the model instance is loaded!
The ResourceSet/Resource/URI
will evolve in the future. At the moment, only
basic operations are enabled: create_resource/get_resource/load/save...
.
Dynamic Metamodel Helper¶
Once a metamodel is loaded from an XMI metamodel (from a .ecore
file), the
metamodel root that is gathered is an EPackage
instance. To access each
EClass
from the loaded resource, a getEClassifier(...)
call must be
performed:
>>> #...
>>> resource = rset.get_resource(URI('path/to/mm.ecore'))
>>> mm_root = resource.contents[0]
>>> A = mm_root.getEClassifier('A')
>>> a_instance = A()
When a big metamodel is loaded, this operation can be cumbersome. To ease this
operation, PyEcore proposes a helper that gives a quick access to each
EClass
contained in the EPackage
and its subpackages. This helper is the
DynamicEPackage
class. This shows a typical usage:
>>> #...
>>> resource = rset.get_resource(URI('path/to/mm.ecore'))
>>> mm_root = resource.contents[0]
>>> from pyecore.utils import DynamicEPackage
>>> MyMetamodel = DynamicEPackage(mm_root) # We create a DynamicEPackage for the loaded root
>>> a_instance = MyMetamodel.A()
>>> b_instance = MyMetamodel.B()
Adding External Metamodel Resources¶
External resources for metamodel loading should be added in the resource set. For example, for resources using XMLType instead of the Ecore one, the following datatypes could be created first by hand that way:
int_conversion = lambda x: int(x) # translating str to int durint load()
String = Ecore.EDataType('String', str)
Double = Ecore.EDataType('Double', int, 0, from_string=int_conversion)
Int = Ecore.EDataType('Int', int, from_string=int_conversion)
IntObject = Ecore.EDataType('IntObject', int, None,
from_string=int_conversion)
Boolean = Ecore.EDataType('Boolean', bool, False,
from_string=lambda x: x in ['True', 'true'],
to_string=lambda x: str(x).lower())
Long = Ecore.EDataType('Long', int, 0, from_string=int_conversion)
EJavaObject = Ecore.EDataType('EJavaObject', object)
xmltype = Ecore.EPackage()
xmltype.eClassifiers.extend([String,
Double,
Int,
EJavaObject,
Long,
Boolean,
IntObject])
xmltype.nsURI = 'http://www.eclipse.org/emf/2003/XMLType'
xmltype.nsPrefix = 'xmltype'
xmltype.name = 'xmltype'
rset.metamodel_registry[xmltype.nsURI] = xmltype
# Then the resource can be loaded (here from an http address)
resource = rset.get_resource(HttpURI('http://myadress.ecore'))
root = resource.contents[0]
Please note that in the case of XMLTypes, an implementation is provided with PyEcore and it is not required to create those types by hand. These types are only used here to highlight how new resources could be added from scratch. To see how to use the XMLTypes, see sections below.
Metamodel References by ‘File Path’¶
If a metamodel references other metamodels in a ‘file path’ manner, PyEcore requires that the REFERENCED metamodel be loaded first and registered against the metamodel path URI.
>>> # Suppose that metamodel A.ecore has references to B.ecore as '../../B.ecore'.
>>> # Path of A.ecore is 'a/b/A.ecore' and B.ecore is '.'
>>> resource = rset.get_resource(URI('B.ecore')) # Load B.ecore first
>>> root = resource.contents[0]
>>> rset.metamodel_registry['../../B.ecore'] = root # Register 'B' metamodel at 'file path' uri
>>> resource = rset.get_resource(URI('a/b/A.ecore')) # A.ecore now loads
Adding External resources¶
When a model references another one, they both need to be added to the same ResourceSet.
rset.get_resource(URI('uri/towards/my/first/resource'))
resource = rset.get_resource(URI('uri/towards/my/secon/resource'))
If for some reason, you want to dynamically create the resource which is required for XMI deserialization of another one, you need to create an empty resource first:
# Other model is 'external_model'
resource = rset.create_resource(URI('the/wanted/uri'))
resource.append(external_model)
Exporting an Existing XMI Resource¶
As with XMI import, support for XMI export (serialization) is still somewhat basic. Here is an example of how you could save your objects in a file:
>>> # we suppose we have an already existing model in 'root'
>>> from pyecore.resources.xmi import XMIResource
>>> from pyecore.resources import URI
>>> resource = XMIResource(URI('my/path.xmi'))
>>> resource.append(root) # We add the root to the resource
>>> resource.save() # will save the result in 'my/path.xmi'
>>> resource.save(output=URI('test/path.xmi')) # save the result in 'test/path.xmi'
You can also use a ResourceSet
:
>>> # we suppose we have an already existing model in 'root'
>>> from pyecore.resources import ResourceSet, URI
>>> rset = ResourceSet()
>>> resource = rset.create_resource(URI('my/path.xmi'))
>>> resource.append(root)
>>> resource.save()
Multiple Root XMI Resource¶
PyEcore supports XMI resources with multiple roots. When deserialized, the
contents
attribute of the loaded Resource
contains all the deserialized
roots (usually, only one is used). If you want to add a new root to your
resource, you only can simply use the append(...)
method:
from pyecore.resources import ResourceSet, URI
# we suppose we have already existing roots named 'root1' and 'root2'
rset = ResourceSet()
resource = rset.create_resource(URI('my/path.xmi'))
resource.append(root1)
resource.append(root2)
resource.save()
Altering XMI xsi:type
serialization¶
When an XMI resource is serialized, information about the type of each element
is inserted in the file. By default, the field xsi:type
is used, but in some
cases, you could want to change this field name to xmi:type
. To perform such
a switch, you can pass an option to resource serialization.
from pyecore.resources.xmi import XMIOptions
# ... with an 'XMIResource' in the 'resource' variable
resource.save(options={XMIOptions.OPTION_USE_XMI_TYPE: True})
Using UUID to reference elements instead of URI fragments during serialization¶
In the XMI resource, the elements are classicaly referenced using a URI
fragment. This fragment is built using the containment reference name and the
object position in the resource. If you load a model that contains UUIDs, the
XMI resource will automatically switch in “UUID mode” and you don’t have to
do anything else. During the next save()
, the resource will embed the
UUID of each element. However, if you build a resource from scratch, the
default behavior is to use the URI fragment. You can change this during the
resource creation, or after it has been created, with the use_uuid
attribute.
This shows switching to “UUID mode” for an existing resource , created
using a ResourceSet
):
from pyecore.resources import ResourceSet
# ... with 'root' set to the model we want to serialize
rset = ResourceSet()
resource = rset.create_resource(URI('my/path/output.xmi'))
resource.use_uuid = True
resource.append(root) # we add the root to the resource
resource.save()
The attribute use_uuid
may also be set directly when creating an XMIResource
when a ResourceSet is not needed.
from pyecore.resources.xmi import XMIResource
# ... with 'root' set to the model we want to serialize
resource = XMIResource(URI('my/path/output.xmi'), use_uuid=True)
resource.append(root)
resource.save()
Forcing default values serializations¶
When an XMI resource is serialized, default and None
values are not written
in the file. You can alter this behavior by passing the
XMIOptions.SERIALIZE_DEFAULT_VALUES
option during the save operation.
from pyecore.resources.xmi import XMIOptions
# ... with an 'XMIResource' in the 'resource' variable
resource.save(options={ XMIOptions.SERIALIZE_DEFAULT_VALUES: True })
This option will also introduce a special XML node xsi:nil="true"
when
an attribute or a reference is explicitly set to None
.
Mapping a URI to a different location/URI¶
Sometimes, it may be necessary to ‘convert’ a URI from the format found in the
XMI to a new one that is more easily understood. This is typically the case for
some Eclipse URIs that are part of the XMI resource
(eg: platform://eclipse.org/xxx/yyy
). PyEcore cannot resolve these, as it is
unaware of the Eclipse platform. The solution is to provide a mapping to allows
PyEcore how to translate the URIs appropriately.
# Given a resource 'foo.xmi' that references a metamodel
# 'platform://eclipse.org/ecore/Ecore' with references to elements
# like: 'platform://eclipse.org/ecore/Ecore#//A'
# In addition, we have references to a resources like:
# 'resources://COMMON/files/bar.xmi#//' with 'bar.xmi' in './files'
# in our system.
from pyecore.resources import ResourceSet
rset = ResourceSet()
# Here is the mapper setup
rset.uri_mapper['platform://eclipse.org/ecore/Ecore'] = 'http://www.eclipse.org/emf/2002/Ecore'
# or, alternatively,
# rset.uri_mapper['platform://eclipse.org/ecore'] = 'http://www.eclipse.org/emf/2002'
rset.uri_mapper['resources://COMMON'] = '.'
# we then load the resource
resource = rset.get_resource(URI('foo.xmi'))
**Note* Object resolution is lazy in many cases, so the mapper
setup can be made after resource loading.
Dealing with JSON Resources¶
Since version 14.0, the JSonResource is registered by default in the global registry, you don’t need to register it manually, you can unregister it by removing the “json” extension from the global_registry
PyEcore is also able to load/save JSON models/metamodels. The JSON format it uses
tries to be close to the one described in the emfjson-jackson project.
The way the JSON serialization/deserialization works, is similar to XMI resources,
but the JSON resource factory is not loaded by default, and must be manually registered.
The registration can be performed globally or at a ResourceSet
level.
Register the JSON resource factory for a given ResourceSet
.
>>> from pyecore.resources import ResourceSet
>>> from pyecore.resources.json import JsonResource
>>> rset = ResourceSet() # We have a resource set
>>> rset.resource_factory['json'] = # we register the factory for '.json' extensions
Register the JSON resource factory globally.
>>> from pyecore.resources import ResourceSet
>>> from pyecore.resources.json import JsonResource
>>> ResourceSet.resource_factory['json'] = JsonResource
Once the factory is registered, loadind and saving models or metamodels in JSON
is done the same as for XMI. See the XMI section for examples load/save resources using a ResourceSet
.
NOTE: Currently, the Json serialization is performed using the default Python
json
lib, by first translating the PyEcore model to a dict
before
export/import. For large models, this implies a memory and performance cost.
Deleting Elements¶
Deleting elements in EMF has issues because of the special model “shape” that can impact the deletion algorithm. PyEcore supports two ways of deleting elements. One is a real deletion, while the other is less direct.
The delete()
method¶
The first way of deleting element is to use the delete()
method which is
owned by every EObject/EProxy
:
>>> # we suppose we have an already existing element in 'elem'
>>> elem.delete()
This call is also recursive by default: every sub-object of the deleted element is also deleted. This behavior is the one by default as a “containment” reference is a strong constraint.
The behavior of the delete()
method can be confusing when there are many
EProxy
instances. As the EProxy
only gives a partial view of the
object while it is not resolved, the delete()
can only correctly remove
resolved proxies. If a resource or elements that are referenced in many
other resources must be destroyed, the best solution is to resolve all the
proxies first, then to delete them. This ensures that broken proxies are not
introduced.
Removing an element from its container¶
You can also remove elements from a model using XMI serialization. If you want
to remove an element from a Resource, you have to remove it from its container.
PyEcore does not serialize elements that are not contained by a Resource
, and
also does not serialize any references to elements that are not serialized.
Working with XMLTypes¶
PyEcore provides a partial implementation of the XMLTypes. This implementation is already shipped with PyEcore and can directly be used. This is typically done by simply importing the package.
import pyecore.type as xmltypes
# from this point, the metamodel is registered in the global registry
# meaning it is not mandatory to register it manually in a ResourceSet
The current implementation is incomplete. Some derived attributes and collections are still empty, but the main part of the metamodel is usable.
Traversing the Whole Model with Single Dispatch¶
Sometimes, you need to go across your whole model, and you want to have a
call a function for each element in the model. To achieve
this goal, PyEcore proposes a simple way of performing single dispatch which
is equivalent for dynamic and static metamodels. This is implemented using
Python’s @functools.singledispatch
.
Here is an example using the library metamodel.
import library
from pyecore.utils import dispatch
class LibrarySwitch(object):
@dispatch
def do_switch(self, o):
print('Fallback for objects of kind ', o.eClass.name)
@do_switch.register(library.Writer)
def writer_switch(self, o):
print('Visiting a ', o.eClass.name, ' named ', o.name)
@do_switch.register(library.Book)
def book_switch(self, o):
print('Reading a ', o.eClass.name, ' titled ', o.name)
switch = LibrarySwitch()
# assuming we have a Library instance in 'mylib'
for obj in mylib.eAllContents():
switch.do_switch(obj)
In this example, only the Writer
and the Book
metaclasses are dispatched
the other instances would fall in the default do_switch
method. In the case
of inheritence, if a method for a dedicated metaclass is not found, dispatch
will search for a method that has been registered for a super type of the instance.
So in the example model above, if there were library.Paperback
that extended the library.Book
metaclass, the book_switch
method would still be
called, for Paperback
instances since no method was registered explicitly for Paperback
.