Jupyter and Scripting

One way to work with models is through the GUI. However, you may also be interested in getting more out of your models by interacting with them through Python scripts and Jupyter notebooks.

You can use scripts to:

  • Explore the model, check for (in)valid conditions.

  • Generate code, as is done for Gaphor’s data model.

  • Update a model from another source, like a CSV file.

Since Gaphor is written in Python, it also functions as a library.

Getting started

To get started, you’ll need a Python console. You can use the interactive console in Gaphor, use a Jupyter notebook, although that will require setting up a Python development environment.

Query a model

The first step is to load a model. For this you’ll need an ElementFactory. The ElementFactory is responsible to creating and maintaining the model. It acts as a repository for the model while you’re working on it.

from gaphor.core.modeling import ElementFactory

element_factory = ElementFactory()

The module gaphor.storage contains everything to load and save models. Gaphor supports multiple modeling languages. The ModelingLanguageService consolidates those languages and makes it easy for the loader logic to find the appropriate classes.

Note

In versions before 2.13, an EventManager is required. In later versions, the ModelingLanguageService can be initialized without event manager.

from gaphor.core.eventmanager import EventManager
from gaphor.services.modelinglanguage import ModelingLanguageService
from gaphor.storage import storage

event_manager = EventManager()

modeling_language = ModelingLanguageService(event_manager=event_manager)

with open("../models/Core.gaphor") as file_obj:
    storage.load(
        file_obj,
        element_factory,
        modeling_language,
    )

At this point the model is loaded in the element_factory and can be queried.

Note

A modeling language consists of the model elements, and diagram items. Graphical components are loaded separately. For the most basic manupilations, GTK (the GUI toolkit we use) is not required, but you may run into situations where Gaphor tries to load the GTK library.

One trick to avoid this (when generating Sphinx docs at least) is to use autodoc’s mock function to mock out the GTK and GDK libraries. However, Pango needs to be installed for text rendering.

A simple query only tells you what elements are in the model. The method ElementFactory.select() returns an iterator. Sometimes it’s easier to obtain a list directly. For those cases you can use ElementFatory.lselect(). Here we select the last five elements:

for element in element_factory.lselect()[:5]:
    print(element)
<gaphor.UML.uml.Package element 3867dda4-7a95-11ea-a112-7f953848cf85>
<gaphor.core.modeling.diagram.Diagram element 3867dda5-7a95-11ea-a112-7f953848cf85>
<gaphor.UML.classes.klass.ClassItem element 4cda498f-7a95-11ea-a112-7f953848cf85>
<gaphor.UML.classes.klass.ClassItem element 5cdae47f-7a95-11ea-a112-7f953848cf85>
<gaphor.UML.classes.klass.ClassItem element 639b48d1-7a95-11ea-a112-7f953848cf85>

Elements can also be queried by type and with a predicate function:

from gaphor import UML
for element in element_factory.select(UML.Class):
    print(element.name)
Element
Diagram
Presentation
Comment
StyleSheet
Property
Tagged
for diagram in element_factory.select(
    lambda e: isinstance(e, UML.Class) and e.name == "Diagram"
):
    print(diagram)
<gaphor.UML.uml.Class element 5cdae47e-7a95-11ea-a112-7f953848cf85>

Now, let’s say we want to do some simple (pseudo-)code generation. We can iterate class attributes and write some output.

diagram: UML.Class

def qname(element):
    return ".".join(element.qualifiedName)

diagram = next(element_factory.select(lambda e: isinstance(e, UML.Class) and e.name == "Diagram"))

print(f"class {diagram.name}({', '.join(qname(g) for g in diagram.general)}):")
for attribute in diagram.attribute:
    if attribute.typeValue:
        # Simple attribute
        print(f"    {attribute.name}: {attribute.typeValue}")
    elif attribute.type:
        # Association
        print(f"    {attribute.name}: {qname(attribute.type)}")
class Diagram(Core.Element):
    name: String
    element: Core.Element
    qualifiedName: String
    ownedPresentation: Core.Presentation
    diagramType: String

To find out which relations can be queried, have a look at the modeling language documentation. Gaphor’s data models have been built using the UML language.

You can find out more about a model property by printing it.

print(UML.Class.ownedAttribute)
<association ownedAttribute: Property[0..*] <>-> class_>

In this case it tells us that the type of UML.Class.ownedAttribute is UML.Property. UML.Property.class_ is set to the owner class when ownedAttribute is set. It is a bidirectional relation.

Draw a diagram

Another nice feature is drawing the diagrams. At this moment this requires a function. This behavior is similar to the diagram directive.

from gaphor.core.modeling import Diagram
from gaphor.extensions.ipython import draw

d = next(element_factory.select(Diagram))
draw(d, format="svg")
_images/bead277c1cae91f6d53ae8e186ca4b92aa60fa9242fb7a8bbb77f6505552e4fa.svg

Create a diagram

(Requires Gaphor 2.13)

Now let’s make something a little more fancy. We still have the core model loaded in the element factory. From this model we can create a custom diagram. With a little help of the auto-layout service, we can make it a readable diagram.

To create the diagram, we drop elements on the diagram. Items on a diagram represent an element in the model. We’ll also drop all associations on the model. Only if both ends can connect, the association will be added.

from gaphor.diagram.drop import drop
from gaphor.extensions.ipython import auto_layout

temp_diagram = element_factory.create(Diagram)

for name in ["Presentation", "Diagram", "Element"]:
    element = next(element_factory.select(
        lambda e: isinstance(e, UML.Class) and e.name == name
    ))
    drop(element, temp_diagram, x=0, y=0)

# Drop all assocations, see what sticks
for association in element_factory.lselect(UML.Association):
    drop(association, temp_diagram, x=0, y=0)

auto_layout(temp_diagram)

draw(temp_diagram, format="svg")
_images/971b70fd6840d759ff21b14d2d90b7728d231b14c5194c5d082f52edd24e9143.svg

The diagram is not perfect, but you get the picture.

Update a model

Updating a model always starts with the element factory: that’s where elements are created.

To create a UML Class instance, you can:

my_class = element_factory.create(UML.Class)
my_class.name = "MyClass"

To give it an attribute, create an attribute type (UML.Property) and then assign the attribute values.

my_attr = element_factory.create(UML.Property)
my_attr.name = "my_attr"
my_attr.typeValue = "string"
my_class.ownedAttribute = my_attr

Adding it to the diagram looks like this:

my_diagram = element_factory.create(Diagram)
drop(my_class, my_diagram, x=0, y=0)
draw(my_diagram, format="svg")
_images/9539d96faaf3bc38180980f8fdb7b082cf8d5f94c251bee0d2454d1003aa5565.svg

If you save the model, your changes are persisted:

with open("../my-model.gaphor", "w") as out:
    storage.save(out, element_factory)

What else

What else is there to know…

  • Gaphor supports derived associations. For example, element.owner points to the owner element. For an attribute that would be its containing class.

  • All data models are described in the Modeling Languages section of the docs.

  • If you use Gaphor’s Console, you’ll need to apply all changes in a transaction, or they will result in an error.

  • If you want a comprehensive example of a code generator, have a look at Gaphor’s coder module. This module is used to generate the code for the data models used by Gaphor.

  • This page is rendered with MyST-NB. It’s actually a Jupyter Notebook!

Examples

Here is another example: