ge-py — Graph Editor (Qt Widgets)

PySide6 desktop application that exercises the whole DevKit value chain from a single DSM model: code generation with Kibo, persistence and versioning through dsviper, and a Qt Widgets UI assembled from the shared dsviper-components library.

  • Source repositorydigital-substrate/ge-py.

  • Entry pointgraph_editor.py.

  • DependenciesPySide6, dsviper (from PyPI).

What it demonstrates

ge-py is a worked example of every layer the documentation introduces in isolation:

Layer

Where in ge-py

DevKit doc

DSM model

DSM definitions of the Graph

DSM

Code-gen

ge/ package (Kibo output)

Kibo

Runtime

dsviper.CommitStore usage

dsviper

Shared UI

dsviper_components/

dsviper-components

A single Python application reads its data model from one DSM source and ships a versioned, undo-aware editor — the value-chain end-to-end.

Architecture

Pure-Python applications collapse the C++ 6-layer architecture to five layers, because business logic lives in the same language as the application — function pools are no longer needed to cross a language boundary.

┌─────────────────────────────────────────────────────────────┐
│  1. UI Layer                                                │
│     PySide6 widgets (graph_editor.py, components/, render/) │
├─────────────────────────────────────────────────────────────┤
│  2. CommitStore Facade                                      │
│     model/context.py — singleton wrapping CommitStore       │
├─────────────────────────────────────────────────────────────┤
│  3. Business Logic (hand-written Python)                    │
│     model/*.py — vertex.py, graph.py, selection_*.py, …     │
├─────────────────────────────────────────────────────────────┤
│  4. Generated Data (Kibo output)                            │
│     ge/*.py — data, attachments, definitions, value_type    │
├─────────────────────────────────────────────────────────────┤
│  5. dsviper Runtime                                         │
│     CommitDatabase, CommitStore, CommitMutableState, Value  │
└─────────────────────────────────────────────────────────────┘

Repository layout

ge-py/
├── graph_editor.py          # Application entry point (QApplication, MainWindow)
├── ge/                      # Kibo-generated infrastructure
│   ├── data.py              # Concept keys, structures, enums
│   ├── attachments.py       # Typed attachment accessors
│   ├── definitions.py       # Embedded DSM definitions
│   └── value_type.py        # Type registry helpers
├── model/                   # Hand-written business logic
│   ├── context.py           # Singleton: store + graph_key + facade
│   ├── graph.py             # Graph creation
│   ├── vertex.py            # Vertex creation
│   ├── edge.py              # Edge creation
│   ├── graph_topology.py    # Topology operations
│   ├── selection_*.py       # Selection management
│   ├── random.py            # Random-data generators
│   └── script_*.py          # Reusable scripts
├── components/              # Project-specific widgets (vertex panel,
│                            #   list panel, render panel, comments, tags…)
├── dsviper_components/      # Vendored copy of the shared dsviper-components
│                            #   library (synced with dev/sync_dsviper_components.py)
├── render/                  # 2-D canvas (paint, hit-testing)
└── list/                    # List-view items

The Context singleton

model/context.py is the single point of truth for the running database, the active graph, and the CommitStore that drives undo/redo and UI notifications. The UI never talks to a CommitDatabase directly.

from dsviper import CommitStore, CommitDatabase, CommitState, CommitMutableState
from ge import attachments, definitions
from ge.data import Graph_GraphKey
from model import graph


class Context:

    @classmethod
    def instance(cls) -> "Context":
        if not hasattr(cls, "_instance"):
            setattr(cls, "_instance", cls())
        return getattr(cls, "_instance")

    def __init__(self):
        self.store = CommitStore()
        self.graph_key = Graph_GraphKey.create()

    def use(self, database: CommitDatabase):
        if not database.commit_ids():
            self._create_initial_commit(database, database.initial_state())
        commit_id = database.last_commit_id()
        self.store.set_state(database.state(commit_id))
        self.store.set_database(database)
        self.store.notify_database_did_open()
        self.load()

The Context exposes the store directly (context.store.dispatch(...)) and adds a small facade for scripting (context.dispatch, context.undo, context.redo).

See ge-py/model/context.py for the full implementation.

The dispatch pattern

Every UI action that mutates the model goes through one entry point: store.dispatch(label, callable). The callable receives an AttachmentMutating and applies all mutations for this transaction. After the lambda returns, the store commits the mutations and notifies the UI.

# graph_editor.py — "Random Vertex" menu action
def _random_vertex_triggered(self):
    context = Context.instance()
    size = self._render_component.render_widget().size()
    rect = Graph_Rectangle()
    rect.x, rect.y, rect.w, rect.h = 0, 0, size.width(), size.height()
    context.store.dispatch(
        "Random Vertex",
        lambda m: model_random.add_vertex(m, context.graph_key, rect),
    )

The body of the lambda is plain Python that calls into model/. Since business logic is already in Python, no function pool is needed — the lambda calls model.random.add_vertex() directly. This is the central simplification that distinguishes ge-py (and ge-qml) from the C++ equivalent.

Tip

The dispatch label ("Random Vertex" above) is what appears in the undo stack and in the commit history. Pick labels that describe user intent, not implementation steps.

Business logic — model/

model/ is hand-written Python organised by functional domain. Each module exposes pure functions taking an AttachmentMutating plus the keys/values they need to operate on, and returns the keys they create. They are trivially unit-testable in isolation.

# model/vertex.py
def add(attachment_mutating: AttachmentMutating,
        graph_key: Graph_GraphKey,
        value: int,
        position: Graph_Position,
        color: Graph_Color) -> Graph_VertexKey:
    vertex_key = create(attachment_mutating, value, position, color)

    vertex_keys = Set_Graph_VertexKey()
    vertex_keys.add(vertex_key)
    attachments.graph_graph_topology_union_vertex_keys(
        attachment_mutating, graph_key, vertex_keys)

    return vertex_key

model/ modules import from ge/ (the generated layer) — never the other way around.

Generated data — ge/

ge/ is the Kibo output for the Python target. It is regenerated from the DSM model and never edited by hand. The four files most consumed by the rest of the app:

File

Purpose

ge/data.py

Concept keys (Graph_VertexKey), structures, enums

ge/attachments.py

Typed accessors (graph_vertex_visual_attributes_set)

ge/definitions.py

Embedded DSM definitions, loaded into the database

ge/value_type.py

Type registry helpers

The mapping from DSM concepts to Python identifiers follows Kibo’s naming conventions.

Notification flow

The CommitStore exposes signals that the UI subscribes to. The two most load-bearing for ge-py:

# graph_editor.py — _setup_connections
notifier.database_did_open.connect(self._store_database_did_open)
notifier.state_did_change.connect(self._store_state_did_change)

Their handlers refresh every panel that reads from the store. This is the only path by which the UI updates: a dispatch returns, the store fires state_did_change, and the connected slots redraw.

UI action  ──►  store.dispatch(label, λ)
                       │
                       ▼
                 λ(mutating)            ← business logic runs here
                       │
                       ▼
            store.commit_mutations()    ← persisted to the DAG
                       │
                       ▼
       store.notify_state_did_change()  ← Qt signal
                       │
                       ▼
              UI handlers redraw

What dsviper-components ships with the application

Beyond the panels ge-py builds for its own domain (components/), the application inherits a complete suite of administration, collaboration and scripting features by importing from dsviper-components. None of this code lives in ge-py — the application only instantiates the widgets and wires them into its menus.

This is the load-bearing reason every Commit-based application in the ecosystem is built on dsviper-components: the moment you adopt the shared library, your application gets a database inspector, a commit-DAG browser, an undo-stack viewer, a remote-server sync pipeline, and an embedded Python REPL — none of which are domain-specific.

The Admin menu — database introspection and history

The Admin menu of ge-py is assembled from eight pre-built dialogs:

# graph_editor.py — _setup_dialog
store = Context.instance().store
self._inspect_dialog        = DSInspectDialog()
self._commits_dialog        = DSCommitsDialog(store)
self._commit_documents_dialog = DSCommitDocumentsDialog(store)
self._commit_program_dialog = DSCommitProgramDialog(store)
self._commit_undo_dialog    = DSCommitUndoDialog(store)
self._commit_blobs_dialog   = DSCommitBlobsDialog(store)
self._commit_actions_dialog = DSCommitActionsDialog(store)
self._commit_sync_log_dialog = DSCommitSyncLogDialog()

Each dialog is constructed with the running CommitStore and reacts on its own to state_did_change notifications — the application has nothing to do beyond toggling visibility.

Dialog

What it shows

DSInspectDialog

Live tree of every attachment / structure in the state

DSCommitsDialog

The full commit DAG (browse, jump to, compare commits)

DSCommitDocumentsDialog

Documents + attachments grouped by commit

DSCommitProgramDialog

Python program embedded in a commit (replayable)

DSCommitSettingsDialog

Per-commit settings

DSCommitUndoDialog

The undo stack as a navigable list

DSCommitBlobsDialog

Every blob referenced across commits

DSCommitActionsDialog

Enable / disable / reset commits in batch

Adopting these dialogs is what turns a domain editor into a fully-introspectable Commit application. The same set is consumed by every Commit-based editor in the ecosystem.

Collaboration — fetch / push / sync over a commit server

ge-py’s File menu and toolbar expose three actions — Fetch, Push, Sync — that operate on a remote commit server. The wiring is again fully in dsviper-components:

# graph_editor.py
from dsviper_components.ds_commit_synchronizer_thread import DSCommitSynchronizerThread
from dsviper_components.ds_connect_to_server_dialog   import DSConnectToServerDialog
from dsviper_components.ds_commit_sync_log_dialog     import DSCommitSyncLogDialog

self._sync_logger  = DSLogger(Logging.LEVEL_ALL)
self._sync_logging = Logging.create(self._sync_logger)

DSConnectToServerDialog collects the server address and credentials. DSCommitSynchronizerThread runs the sync I/O off the UI thread and emits progress signals consumed by DSCommitSyncLogDialog (the sync log visible from the Admin menu). Three menu actions drive the workflow:

  • Fetch — pull remote commits into the local DAG without applying them.

  • Push — send unmerged local commits to the server.

  • Sync — fetch + push in one step.

The collaborative DAG itself is a property of the CommitDatabase — there is no separate “merge” step in the application. Merging is what the commit DAG is for.

Embedded Python scripting — DSCodeEditorDialog

ge-py embeds a fully functional Python editor that runs scripts in the same interpreter as the running application — with the application’s Context and CommitStore exposed as globals:

# graph_editor.py — _setup_dialog
from dsviper_components.python_editor_model import PythonEditorModel

scripts_folder = str(Path(__file__).parent / "scripts")
self._python_editor_model = PythonEditorModel(
    scripts_folder,
    namespace_vars={
        "ctx":              Context.instance(),
        "store":            store,
        "render_model":     self._render_component,
        "_documents_panel": self._commit_documents_dialog,
    },
)
self._code_editor_dialog = DSCodeEditorDialog(self._python_editor_model)
self._python_editor_model.run_init_script()

The scripts a user writes from the Editor have direct access to:

  • ctx.dispatch("label", lambda m: …) — the same dispatch path the UI uses.

  • store.state(), store.attachment_getting() — read-only access to the current state.

  • domain modules (from model import ) — the same business logic exposed to the menu actions.

The editor itself contributes a pre-wired Editor menu (open / save / run / show description / refresh syntax). ge-py’s _setup_menu simply appends those actions:

# graph_editor.py — _setup_menu
editor = self._code_editor_dialog.editor
editor_menu = self.menuBar().addMenu(self.tr("&Editor"))
editor_menu.addAction(editor.open_script_action)
editor_menu.addAction(editor.save_script_action)
# ...
editor_menu.addAction(editor.run_script_action)
editor_menu.addAction(editor.show_description_action)

Tip

Scripts run in the live interpreter — they can build complex sequences of mutations that go through the same dispatch path as menu actions, which means every script ends up as a single commit in the DAG, replayable and undoable like any other operation.

Why this matters for any new Commit application

The value the shared library provides is not “widgets you can re-use” — it is the full administrative surface of a Commit-based application, already implemented. The work left to a new application is the part that is genuinely domain-specific: the DSM model, the business functions in model/, and the rendering of the domain in components/. Everything else — history browsing, undo, sync, scripting, inspection — is acquired by composition.

For the catalogue of what dsviper-components exposes and the conventions for instantiating each widget, see dsviper-components.

Where to read first

  1. graph_editor.py lines around _setup_connections — how the UI wires itself to the store’s signals.

  2. model/context.py — the singleton facade and database lifecycle.

  3. model/vertex.py, model/graph.py — the smallest, most readable examples of model functions.

  4. graph_editor.py lines around _random_vertex_triggered — a complete round trip: UI action → dispatch → model → notification → redraw.

  5. components/list.py — a panel built on dsviper-components that both reads the state and dispatches selection mutations back.

Reference

  • DSM — the language ge-py’s data model is written in.

  • Kibo — the generator that produces ge/.

  • dsviper — the runtime exercised through Context.store.

  • dsviper-components — the shared widget library the UI is built on.