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 repository —
digital-substrate/ge-py.Entry point —
graph_editor.py.Dependencies —
PySide6,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 |
|
Code-gen |
|
|
Runtime |
|
|
Shared UI |
|
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 |
|---|---|
|
Concept keys ( |
|
Typed accessors ( |
|
Embedded DSM definitions, loaded into the database |
|
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.
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¶
graph_editor.pylines around_setup_connections— how the UI wires itself to the store’s signals.model/context.py— the singleton facade and database lifecycle.model/vertex.py,model/graph.py— the smallest, most readable examples of model functions.graph_editor.pylines around_random_vertex_triggered— a complete round trip: UI action → dispatch → model → notification → redraw.components/list.py— a panel built ondsviper-componentsthat 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.