Jupyter et les scripts

L’une des façons de travailler avec les modèles est d’utiliser l’interface graphique. Cependant, vous pouvez également être intéressé par une meilleure exploitation de vos modèles en interagissant avec eux par le biais de scripts Python et de carnets Jupyter.

Vous pouvez utiliser des scripts pour :

  • Explorer le modèle, vérifier les conditions (in)valides.

  • Générer du code, comme pour le modèle de données de Gaphor.

  • Mettre à jour un modèle à partir d’une autre source, comme un fichier CSV.

Comme Gaphor est écrit en Python, il fonctionne également comme une bibliothèque.

Pour commencer

Pour commencer, vous aurez besoin d’une console Python. Vous pouvez utiliser la console interactive de Gaphor ou un bloc-notes Jupyter, bien que cela nécessite la mise en place d’un environnement de développement Python.

Interroger un modèle

La première étape consiste à charger un modèle. Pour cela, vous aurez besoin d’un gaphor.core.modeling.ElementFactory. La ElementFactory est responsable de la création et de la maintenance du modèle. Elle agit comme un référentiel pour le modèle pendant que vous travaillez dessus.

from gaphor.core.modeling import ElementFactory

element_factory = ElementFactory()

Le module gaphor.storage contient tout ce qu’il faut pour charger et sauvegarder les modèles. Gaphor supporte plusieurs langages de modélisation. Le ModelingLanguageService consolide ces langages et permet à la logique de chargement de trouver facilement les classes appropriées.

Note

Dans les versions antérieures à 2.13, un EventManager est nécessaire. Dans les versions ultérieures, le ModelingLanguageService peut être initialisé sans gestionnaire d’événements.

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,
    )

A ce stade, le modèle est chargé dans la element_factory et peut être interrogé.

Note

Un langage de modélisation se compose d’éléments de modèle et d’éléments de diagramme. Les composants graphiques sont chargés séparément. Pour les manipulations les plus basiques, GTK (la boîte à outils GUI que nous utilisons) n’est pas nécessaire, mais il peut arriver que Gaphor tente de charger la bibliothèque GTK.

Une astuce pour éviter cela (du moins lors de la génération de documents Sphinx) consiste à utiliser la fonction mock d’autodoc pour simuler les bibliothèques GTK et GDK. Cependant, Pango doit être installé pour le rendu du texte.

Une requête simple vous indique seulement quels éléments se trouvent dans le modèle. La méthode ElementFactory.select() renvoie un itérateur. Il est parfois plus facile d’obtenir une liste directement. Dans ce cas, vous pouvez utiliser la méthode ElementFatory.lselect(). Ici, nous sélectionnons les cinq derniers éléments :

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>

Les éléments peuvent également être interrogés par type et à l’aide d’une fonction de prédiction :

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>

Supposons maintenant que nous souhaitions générer du (pseudo-)code simple. Nous pouvons itérer les attributs de la classe et écrire une sortie.

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

Pour savoir quelles relations peuvent être interrogées, consultez la documentation du langage de modélisation. Les modèles de données de Gaphor ont été construits en utilisant le langage UML.

Vous pouvez en savoir plus sur une propriété de modèle en l’imprimant.

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

Dans ce cas, cela nous indique que le type de UML.Class.ownedAttribute est UML.Property. UML.Property.class_ est défini à la classe propriétaire lorsque ownedAttribute est défini. Il s’agit d’une relation bidirectionnelle.

Dessiner un diagramme

Une autre fonction intéressante consiste à dessiner les diagrammes. Pour l’instant, cela nécessite une fonction. Ce comportement est similaire à 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

Créer un diagramme

(Nécessite Gaphor 2.13)

Faisons maintenant quelque chose d’un peu plus fantaisiste. Nous avons toujours le modèle de base chargé dans la fabrique d’éléments. A partir de ce modèle, nous pouvons créer un diagramme personnalisé. Avec un peu d’aide du service de mise en page automatique, nous pouvons en faire un diagramme lisible.

Pour créer le diagramme, nous déposons drop elements sur le diagramme. Les éléments d’un diagramme représentent un élément du modèle. Nous allons également déposer toutes les associations sur le modèle. L’association ne sera ajoutée que si les deux extrémités peuvent se connecter.

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

Le schéma n’est pas parfait, mais vous avez compris.

Mise à jour d’un modèle

La mise à jour d’un modèle commence toujours par la fabrique d’éléments (element factory) : c’est là que les éléments sont créés.

Pour créer une instance de classe UML, vous pouvez :

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

Pour lui donner un attribut, créez un type d’attribut (UML.Property) et attribuez-lui des valeurs.

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

En l’ajoutant au diagramme, on obtient le résultat suivant :

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

Si vous enregistrez le modèle, vos modifications sont conservées :

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

Mise à jour des éléments

Si vous devez mettre à jour des éléments existants, vous pouvez le faire en gardant une trace de l’identifiant de l’élément. Chaque élément du modèle possède un identifiant interne unique. Une fois de plus, nous avons besoin d’importations de 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

Ensuite, nous lancerons les services que nous utiliserons :

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")

et charger le modèle dans la 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"
    ))

Il est important de noter que les changements sont effectués dans le cadre d’une Transaction. Ici, nous trouvons l’élément avec le même identifiant, puis nous mettons à jour le contenu. Nous sauvegardons ensuite le modèle modifié dans un fichier.

# 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)

Quoi d’autre

Qu’y a-t-il d’autre à savoir …

  • Gaphor supporte les associations dérivées. Par exemple, element.owner pointe vers l’élément propriétaire. Pour un attribut, il s’agirait de la classe qui le contient.

  • Le module gaphor.UML.recipes contient plusieurs fonctions pour manipuler les éléments et leurs associations.

  • Les tests d’un langage de modélisation donné sont un bon moyen de trouver des exemples de référence pour la création et la modification d’éléments.

  • Tous les modèles de données sont décrits dans la section Modeling Languages (Langages de modélisation) de la documentation.

  • Si vous utilisez la console de Gaphor, vous devrez appliquer toutes les modifications dans une transaction, sinon une erreur se produira.

  • Si vous voulez un exemple complet de générateur de code, jetez un coup d’œil au module coder de Gaphor. Ce module est utilisé pour générer le code des modèles de données utilisés par Gaphor.

  • Cette page est réalisée avec MyST-NB. Il s’agit en fait d’un carnet de notes Jupyter !

Exemples

En développant les informations ci-dessus, les extraits suivants montrent comment créer des exigences et des interfaces.

Exigences des champs de texte

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 à partir de dictionnaires

# 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.

Validation de base

Des contrôles de validation simples peuvent être effectués à l’aide de quelques petites fonctions pour sélectionner et évaluer les éléments.

# 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)

Voici un autre exemple :