Jupyter y Scripting

Una forma de trabajar con los modelos es a través de la GUI. Sin embargo, también puede interesarte sacar más partido de tus modelos interactuando con ellos mediante scripts de Python y Jupyter notebooks.

Puede utilizar scripts para:

  • Explorar el modelo, comprobar las condiciones (no)válidas.

  • Genere código, como se hace para el modelo de datos de Gaphor.

  • Actualizar un modelo a partir de otra fuente, como un archivo CSV.

Dado que Gaphor está escrito en Python, también funciona como una biblioteca.

Primeros pasos

Para empezar, necesitarás una consola Python. Puedes usar la consola interactiva en Gaphor, usar un Jupyter notebook, aunque eso requerirá configurar un entorno de desarrollo Python.

Consultar un modelo

The first step is to load a model. For this you’ll need an gaphor.core.modeling.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()

El módulo gaphor.storage contiene todo lo necesario para cargar y guardar modelos. Gaphor soporta múltiples modeling languages. El ModelingLanguageService consolida esos idiomas y hace que sea fácil para la lógica del cargador encontrar las clases apropiadas.

Nota

En versiones anteriores a la 2.13, se requiere un EventManager. En versiones posteriores, el ModelingLanguageService puede ser inicializado sin gestor de eventos.

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", encoding="utf-8") as file_obj:
    storage.load(
        file_obj,
        element_factory,
        modeling_language,
    )

En este punto el modelo se carga en la element_factory y puede ser consultado.

Nota

Un lenguaje de modelado se compone de los elementos del modelo y de los elementos del diagrama. Los componentes gráficos se cargan por separado. Para las manipulaciones más básicas, GTK (el conjunto de herramientas GUI que utilizamos) no es necesario, pero puede encontrarse con situaciones en las que Gaphor intente cargar la biblioteca GTK.

Un truco para evitar esto (al menos cuando se generan documentos Sphinx) es usar la función mock de autodoc para imitar las librerías GTK y GDK. Sin embargo, Pango necesita ser instalado para el renderizado de texto.

Una consulta simple sólo te dice qué elementos hay en el modelo. El método ElementFactory.select() devuelve un iterador. A veces es más fácil obtener una lista directamente. Para esos casos puedes usar ElementFatory.lselect(). Aquí seleccionamos los últimos cinco elementos:

for element in element_factory.lselect()[:5]:
    print(element)
<gaphor.UML.uml.Package element 3867dda4-7a95-11ea-a112-7f953848cf85>
<gaphor.UML.uml.Diagram element 3867dda5-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>
<gaphor.UML.classes.association.AssociationItem element 68e63fac-7a95-11ea-a112-7f953848cf85>

Los elementos también pueden consultarse por tipo y con una función de predicado:

from gaphor import UML
for element in element_factory.select(UML.Class):
    print(element.name)
Diagram
Presentation
StyleSheet
Property
Tagged
ElementChange
ValueChange
RefChange
PendingChange
Base
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>

Ahora, digamos que queremos hacer una generación simple de (pseudo) código. Podemos iterar atributos de clase y escribir algunos resultados.

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(Root.Base):
    diagramType: String
    ownedPresentation: Presentation.Presentation

Para saber qué relaciones se pueden consultar, eche un vistazo a la documentación de modeling language. Los modelos de datos de Gaphor se han construido utilizando el lenguaje UML.

Puede obtener más información sobre una propiedad de modelo imprimiéndola.

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

En este caso nos dice que el tipo de UML.Class.ownedAttribute es UML.Property. El atributo UML.Property.class_ se establece en la clase propietaria cuando se establece ownedAttribute. Se trata de una relación bidireccional.

Dibujar un diagrama

Otra buena función es la de dibujar diagramas. En este momento esto requiere una función. Este comportamiento es similar a la 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/8055ed121116ba11eb0165656cbd844f45f5a3c552b336b70af5a8d920ef47b4.svg

Crear un diagrama

(Requiere Gaphor 2.13)

Ahora vamos a hacer algo un poco más elegante. Todavía tenemos el modelo central cargado en la fábrica de elementos. A partir de este modelo podemos crear un diagrama personalizado. Con un poco de ayuda del servicio de auto-diseño, podemos hacer que sea un diagrama legible.

Para crear el diagrama, drop elements en el diagrama. Los elementos en un diagrama representan un elemento en el modelo. También arrastraremos todas las asociaciones en el modelo. Solo si ambos extremos se pueden conectar, la asociación será añadida.

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

temp_diagram = element_factory.create(Diagram)

for name in ["Presentation", "Diagram", "Base"]:
    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/505bcb5aa0beeb5e8c16f7104aba67a1cc396760e71181215c6d3655eaaf804a.svg

El diagrama no es perfecto, pero te haces una idea.

Actualizar un modelo

La actualización de un modelo siempre empieza por la fábrica de elementos: es ahí donde se crean los elementos.

Para crear una instancia de clase UML, puede:

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

Para darle un atributo, cree un tipo de atributo (“UML. Property”) y, a continuación, asigne los valores de atributo.

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

Añadiéndolo al diagrama queda así:

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

Si guarda el modelo, los cambios se mantienen:

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

Updating elements

If you need to update existing elements, this can be done by keeping track of the element ID. Each element in the model has a unique internal id. Once again we need some imports from Gaphor:

from pathlib import Path

from gaphor import UML
from gaphor import SysML
from gaphor.application import Session  # needed to run services
from gaphor.transaction import Transaction  # needed to make changes
from gaphor.storage import storage  # needed to save to file

Then start up the services we will use:

session = Session(
    services=[
        "event_manager",
        "component_registry",
        "element_factory",
        "element_dispatcher",
        "modeling_language",
    ]
)

# Get services we need.
element_factory = session.get_service("element_factory")
modeling_language = session.get_service("modeling_language")
event_manager = session.get_service("event_manager")

and load in the model to the session

with open(model_filename, encoding="utf-8") as file_obj:
    storage.load(
        file_obj,
        element_factory,
        modeling_language,
    )

# Now we query the model to get the element we want to change:
the_class = next(
    element_factory.select(
        lambda e: isinstance(e, UML.Class) and e.name == "MyClass"
    ))

Importantly, the changes are made as part of a Transaction. Here we find the element with the same id, and then update the content. We then save the altered model to a file.

# change the name and write back into the model
with Transaction(event_manager) as ctx:
    the_class.name = "Not My Class Anymore"
    the_class.ownedAttribute[0].typeValue = "updated string"

# Write changes to file here
with open(model_filename, "w") as out:
    storage.save(out, element_factory)

Qué más

Qué más hay que saber…

  • Gaphor admite asociaciones derivadas. Por ejemplo, element.owner apunta al elemento propietario. Para un atributo que sería su clase contenedora.

  • The module gaphor.UML.recipes contains several functions for manipulating elements and their associations.

  • The tests for a given modelling language are a good place to find reference examples of element creation and modification.

  • Todos los modelos de datos se describen en la sección «Lenguajes de modelado» de la documentación.

  • Si utiliza la consola de Gaphor, tendrá que aplicar todos los cambios en una transacción, o se producirá un error.

  • Si quieres un ejemplo completo de un generador de código, echa un vistazo al modulo coder de Gaphor. Este módulo se utiliza para generar el código de los modelos de datos utilizados por Gaphor.

  • Esta página se renderiza con MyST-NB. ¡En realidad es un Jupyter Notebook!

Ejemplos

Expanding on the information above the following snippetts show how to create requirements and interfaces.

Requirements from text fields

txts = ['req1', 'req2', 'bob the cat']
outfile = "requirement_example.gaphor"
with Transaction(event_manager) as ctx:
    my_diagram = element_factory.create(Diagram)
    my_diagram.name= 'my diagram'
    reqPackage = element_factory.create(UML.Package)
    reqPackage.name = "Requirements"
    drop(reqPackage, my_diagram, x=0, y=0)


    for req_id,txt in enumerate(txts):
        my_class = element_factory.create(SysML.sysml.Requirement)
        my_class.name = f"{req_id}-{txt[:3]}"
        my_class.text = f"{txt}"
        my_class.externalId = f"{req_id}"

        drop(my_class, my_diagram, x=0, y=0)

    # Save the model or export diagrams.

Interfaces from dictionaries

# get interface definitions from file into this dictionary format
interfaces = {'Interface1': ['signal1:type1', 'signal2:type1', 'signal3:type1'],
              'Interface2': ['signal4:type2', 'signal5:type2', 'signal6:type2']}
outfile = 'interface_example.gaphor'
with Transaction(event_manager) as ctx:
    my_diagram = element_factory.create(UML.Diagram)
    my_diagram.name=' my diagram'
    intPackage = element_factory.create(UML.Package)
    intPackage.name = "Interfaces"
    drop(intPackage, my_diagram, x=0, y=0)


    for interface,signals in interfaces.items():
        my_class = element_factory.create(UML.uml.Interface)
        my_class.name = f"{interface}"
        for s in signals:
            my_attr = element_factory.create(UML.Property)
            name,vtype = s.split(':')
            my_attr.name = name
            my_attr.typeValue = vtype
            my_class.ownedAttribute = my_attr

        drop(my_class, my_diagram, x=0, y=0)


    # Save the model or export diagrams.

Basic Validation

Some simple validation checks can be run using a couple of small functions to select and evaluate elements.

# As before assume we have a factory service and the model is loaded
# Define a function to select an element
def element_select(element):
    return isinstance(element, UML.Class)


# Define a validation rule - names must be capatalised
def rule(element):
    return element.name[0].isupper()


# Define a message to display if the element fails the validation
msg = "Class Names must be capitalised"

element = next(element_factory.select(element_select))
is_valid = rule(element)
if not is_valid:
    print(msg)

Aquí hay otro ejemplo: