dsviper-ge¶
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/dsviper-ge.Entry point —
graph_editor.py.Dependencies —
PySide6,dsviper(from PyPI).
What it demonstrates¶
dsviper-ge is a worked example of every layer the documentation introduces in isolation:
Layer |
Where in dsviper-ge |
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 │
│ ge/context.py — singleton wrapping CommitStore │
├─────────────────────────────────────────────────────────────┤
│ 3. Business Logic (hand-written Python) │
│ ge/*.py — vertex.py, graph.py, selection_*.py, … │
├─────────────────────────────────────────────────────────────┤
│ 4. Generated Data (Kibo output) │
│ gei/ — the graph unit, containers, definitions() │
├─────────────────────────────────────────────────────────────┤
│ 5. dsviper Runtime │
│ CommitDatabase, CommitStore, CommitMutableState, Value │
└─────────────────────────────────────────────────────────────┘
Repository layout¶
dsviper-ge/
├── graph_editor.py # Application entry point (QApplication, MainWindow)
├── kibo.toml # How gei/ is generated from the DSM model
├── gei/ # Kibo-generated infrastructure, never edited by hand
│ ├── __init__.py # definitions(), the units, AnyConceptKey, Key, …
│ ├── containers.py # One class per container shape the model uses
│ ├── graph/ # The DSM namespace Graph
│ │ ├── data.py # Concept keys, structures, their runtime ids
│ │ ├── commit_attachments.py # The attachments over a Commit state: one class per concept
│ │ └── fields.py, paths.py # Field names and paths, for the field operations
│ ├── resources.py # Embedded DSM definitions
│ └── _codegen/ # The runtime the generated package carries
├── ge/ # 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¶
ge/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, CommitStateBuilder, CommitState, CommitMutableState, ValueCommitId
from gei import definitions, graph
from gei.graph import attachments
from ge import graph as ge_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 create_database(self, file_path: str) -> CommitDatabase:
result = CommitDatabase.create(file_path, documentation="A Graph Editor Commit Database")
result.extend_definitions(definitions())
return result
def use(self, database: CommitDatabase):
if not database.commit_ids():
self._create_initial_commit(database, CommitStateBuilder.initial_state(database))
commit_id = database.last_commit_id()
self.store.set_state(CommitStateBuilder.state(database, commit_id))
self.store.set_database(database)
self.store.notify_database_did_open()
self.load()
The generated package is imported twice over: graph, the module of the DSM namespace
Graph (its keys and structures), and attachments, the attachments of that namespace,
grouped by the concept they are keyed on. The application’s own module ge.graph has
the same short name as the unit, so it is imported under an alias.
The Context exposes the store directly (context.store.dispatch(...))
and adds a small facade for scripting (context.dispatch,
context.undo, context.redo).
See dsviper-ge/ge/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 a
CommitAttachmentMutating 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))
graph.Rectangle is a structure of the generated unit, and model_random is the
business module ge.random, imported under that name. The body of the lambda is
plain Python that calls into ge/. Since
business logic is already in Python, no function pool is needed — the
lambda calls ge.random.add_vertex() directly. This is the central
simplification that distinguishes dsviper-ge (and dsviper-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 — ge/¶
ge/ is hand-written Python organised by functional domain. Each module
exposes pure functions taking a CommitAttachmentMutating plus the keys/values
they need to operate on, and returns the keys they create. They are
trivially unit-testable in isolation.
# ge/vertex.py
from dsviper import CommitAttachmentMutating
from gei.graph import attachments
from gei import containers, graph
def add(attachment_mutating: CommitAttachmentMutating,
graph_key: graph.GraphKey,
value: int,
position: graph.Position,
color: graph.Color) -> graph.VertexKey:
vertex_key = create(attachment_mutating, value, position, color)
vertex_keys = containers.Set_of_Graph_VertexKey()
vertex_keys.add(vertex_key)
attachments.Graph.topology.union_vertex_keys(attachment_mutating, graph_key, vertex_keys)
return vertex_key
ge/ modules import from gei/ (the generated layer) — never the other
way around.
Generated data — gei/¶
gei/ is the Kibo output for the Python target, generated with kibo 2 and the
kibo-template-viper 2.1 pack. kibo.toml at the root of the repository declares it — the
model’s definitions, the infrastructure name gei, the pack’s line 2 and the Base and
CommitAttachments features, the usage of an editor whose document is a Commit database — and kibo-project regenerates it after a model
change:
python3 ../kibo-project/kibo_project.py generate
It is never edited by hand. What the rest of the app consumes:
Name |
What it is |
|---|---|
|
The DSM namespace |
|
The attachments over a Commit state, by concept: |
|
One class per container shape: |
|
The model’s definitions, loaded into the database |
The mapping from DSM names to Python identifiers, and the surface of each of these, is described in Using the generated SDK — Python.
Notification flow¶
The CommitStore exposes signals that the UI subscribes to. The two most
load-bearing for dsviper-ge:
# 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 dsviper-ge builds for its own domain (components/), the
application inherits a complete suite of administration, sync,
and scripting features by importing from dsviper-components. None of
this code lives in dsviper-ge — 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.
Sync — fetch / push over a commit server¶
dsviper-ge’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 connection details
(host, port, socket path).
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 local commits that the server does not yet have.
Sync — fetch + push in one step.
The shared DAG itself is a property of the CommitDatabase — there is no
separate “merge” step in the application. Deterministic reduction is what
the commit DAG is for.
Embedded Python scripting — DSCodeEditorDialog¶
dsviper-ge 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 ge 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). dsviper-ge’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.
For the catalogue of what dsviper-components exposes and the
conventions for instantiating each widget, see
dsviper-components. The model-agnostic
limit case — generic database editors with no DSM model of their own —
is walked through in cdbe.
Where to read first¶
graph_editor.pylines around_setup_connections— how the UI wires itself to the store’s signals.ge/context.py— the singleton facade and database lifecycle.ge/vertex.py,ge/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 dsviper-ge’s data model is written in.
Kibo — the generator that produces
gei/, driven by kibo-project.Using the generated SDK — Python — the surface of
gei/.dsviper — the runtime exercised through
Context.store.dsviper-components — the shared widget library the UI is built on.