Architecture
Backend capabilities
dolphin_desktop is a multi-stack automation library — one API
that talks to native Windows apps, SAP GUI, Qt, Electron, Java
Swing, Delphi, and 3270 / 5250 terminals. Each stack has different
mechanics (UIA patterns for native, COM Scripting for SAP, CDP for
Electron, JAB for Java, direct socket protocol for mainframe). Not
every operation is possible on every stack — a mainframe terminal
does not implement .invoke() because there is no InvokePattern in
3270; an image-matching locator cannot walk an accessibility tree
because there is no tree to walk.
Capabilities are the vocabulary the library uses to answer the question "does the backend I selected support this operation?" BEFORE the test runs and BEFORE the operation fails opaquely at runtime.
The Capability enum
Every operation the library exposes is one enum member in
dolphin_desktop.Capability. Adding a new capability is a public
API change and shows up in every backend's declared set.
| Group | Members |
|---|---|
| Discovery | LOCATE, GET_TREE |
| State reads | READ_TEXT, READ_STATE |
| Input simulation (needs input desktop) | CLICK, DOUBLE_CLICK, RIGHT_CLICK, HOVER, DRAG, TYPE_TEXT, PRESS_KEY, SCROLL |
| Programmatic actions (headless-safe) | INVOKE, TOGGLE, EXPAND, COLLAPSE, SELECT, SET_VALUE |
| Media | SCREENSHOT |
The Programmatic group pairs with the programmatic action API: its
members are what dolphin's .invoke() / .toggle() / .set_value()
route through under headless. If your backend declares Capability.INVOKE, users
can call .invoke(); if it does not, they get a clean
UnsupportedCapabilityError naming the backend and pointing at
alternatives that do support it.
Preset sets
Three ready-made frozensets cover the shapes backends actually declare,
so a new backend rarely has to enumerate members by hand:
| Preset | Contains |
|---|---|
ALL_CAPABILITIES |
all 19 members — a fully-featured accessibility backend |
STANDARD_ACCESSIBILITY |
ALL_CAPABILITIES minus DRAG, SCROLL and SCREENSHOT — an accessibility API that can read and act on the tree but has no pixel or gesture support |
IMAGE_ONLY |
what template matching alone can do: LOCATE, CLICK, DOUBLE_CLICK, RIGHT_CLICK, HOVER, TYPE_TEXT, PRESS_KEY, SCREENSHOT — no tree, no state reads, no programmatic actions |
from dolphin_desktop import IMAGE_ONLY, STANDARD_ACCESSIBILITY, Capability
assert Capability.INVOKE in STANDARD_ACCESSIBILITY
assert Capability.INVOKE not in IMAGE_ONLY # no accessibility tree to invoke through
Built-in capability matrix
| Backend | LOCATE | GET_TREE | CLICK | TYPE_TEXT | INVOKE | TOGGLE | SELECT | SET_VALUE | SCREENSHOT |
|---|---|---|---|---|---|---|---|---|---|
uia (UIABackend) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
qt (QtBackend) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
delphi (DelphiBackend, marker) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
cdp (CDPBackend, marker) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
sap (SapBackend, marker) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
java (JavaBackend, marker) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
win32 (Win32Backend) |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
mainframe (MainframeBackend, marker) |
✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
image (ImageBackend) |
✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
macos, linux (stubs) |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Full matrix (all 19 capabilities) available via
dolphin_desktop.list_backends() — every entry carries a
capabilities list.
Querying capabilities from user code
Backend-level:
from dolphin_desktop import Capability, resolve_backend
b = resolve_backend("uia")
assert b.supports(Capability.INVOKE)
Desktop-level — the currently-configured backend:
from dolphin_desktop import Desktop, Capability
desktop = Desktop(backend="image")
if not desktop.backend_supports(Capability.INVOKE):
pytest.skip("image backend does not implement invoke()")
Stack-facade-level — every facade class publishes backend_id:
from dolphin_desktop import CDPSession, Capability
if CDPSession.backend_supports(Capability.SET_VALUE):
session.locator("#login").set_value("alice")
Cross-registry query — "which backends can do X?":
from dolphin_desktop import Capability, supported_backends
for bid in supported_backends(Capability.INVOKE):
print(bid, "supports .invoke()")
# → uia, qt, delphi, cdp, sap, java
Failure surface — UnsupportedCapabilityError
Every operation gated on a capability check raises
UnsupportedCapabilityError when the backend does not declare
support. The exception carries:
- The backend id
- The missing capability name
- A
hint=pointing at working alternatives from the registry
Example message:
UnsupportedCapabilityError: backend 'image' does not support Capability.INVOKE
hint: other backends that support this: cdp, delphi, java, qt, sap, uia
— pass one via Desktop(backend='…')
UnsupportedCapabilityError is distinct from
UnsupportedPatternError:
UnsupportedCapabilityError: the backend as a whole does not publish the operation — e.g. calling.invoke()onDesktop(backend="image").UnsupportedPatternError: the backend publishes the operation but the specific element does not implement it — e.g. a JLabel is not toggleable even though the JAB backend supports toggle in general.
Both derive from dolphin_desktop.DolphinError so tests can catch
either with the base class.
Writing a plugin backend
Third-party packages register additional backends via the
dolphin_desktop.backends entry-point group. Example — a fictional
Bloomberg Terminal automation stack:
# my_plugin/bloomberg_backend.py
from dolphin_desktop import Backend, Capability
class BloombergBackend(Backend):
"""Automate a Bloomberg Terminal window through its SPI API."""
id = "bloomberg"
platform = "windows"
def capabilities(self):
# Bloomberg SPI supports: locate widgets, read text, type
# commands, press function keys, take screenshots. No
# programmatic patterns or accessibility tree.
return frozenset({
Capability.LOCATE,
Capability.READ_TEXT,
Capability.TYPE_TEXT,
Capability.PRESS_KEY,
Capability.SCREENSHOT,
})
def find_element(self, parent, criteria):
self.require_capability(Capability.LOCATE)
# …call Bloomberg SPI…
def type_text(self, element, text):
self.require_capability(Capability.TYPE_TEXT)
# …dispatch through SPI…
def click(self, element, *, button="left"):
self.require_capability(Capability.CLICK) # raises — not in caps
# …etc for the other five abstract methods.
Register via pyproject.toml:
[project.entry-points."dolphin_desktop.backends"]
bloomberg = "my_plugin.bloomberg_backend:BloombergBackend"
Users of your plugin then get automatic integration:
from dolphin_desktop import Desktop, Capability
desktop = Desktop(backend="bloomberg")
desktop.require_capability(Capability.TYPE_TEXT) # passes
desktop.require_capability(Capability.INVOKE) # raises with hint
Design principles
-
Safe by default.
Backend.capabilities()default returns an empty set. A subclass that overrides nothing gets cleanUnsupportedCapabilityErroron every operation instead of silent pass-through to a broken parent implementation. -
Capabilities are boolean, not tri-state. Either the backend supports the operation across every reachable element or it does not. Per-element partial support (a JLabel is not toggleable even though JAB supports toggle) is signalled through
UnsupportedPatternError, not by omitting the capability here. -
The vocabulary is stable. Enum members are semver-visible. Adding a new capability is a minor version bump; removing one is a breaking change.
-
Marker backends declare stack capabilities.
DelphiBackend,CDPBackend,MainframeBackend,SapBackend,JavaBackendare registry markers whose real automation surface lives on the matching facade (DelphiApp, CDPSession, etc.). They still publishcapabilities()because callers askCDPSession.backend_supports(...)and the facade forwards to the backend registry — the marker must return the right answer.