Files
create/docs/src/reference/cpp-file-origin-python.md
forbes 87a0af0b0f phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)
Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
2026-02-13 14:03:58 -06:00

8.8 KiB

FileOriginPython — Python-to-C++ Bridge

Header: src/Gui/FileOriginPython.h Implementation: src/Gui/FileOriginPython.cpp Python binding: src/Gui/ApplicationPy.cpp Namespace: Gui

FileOriginPython is an Adapter that wraps a Python object and presents it as a C++ FileOrigin. This is how Python addons (like SiloOrigin) integrate with the C++ OriginManager without compiling any C++ code.

Registration from Python

The bridge is exposed to Python through three FreeCADGui module functions:

import FreeCADGui

FreeCADGui.addOrigin(obj)       # register a Python origin
FreeCADGui.removeOrigin(obj)    # unregister it
FreeCADGui.getOrigin(obj)       # look up the wrapper (or None)

FreeCADGui.addOrigin(obj)

  1. Checks that obj is not already registered (duplicate check via findOrigin).
  2. Constructs a FileOriginPython wrapper around obj.
  3. Calls obj.id() immediately and caches the result — the ID must be non-empty or registration fails.
  4. Passes the wrapper to OriginManager::instance()->registerOrigin(wrapper).
  5. If registerOrigin fails (e.g., duplicate ID), the wrapper is removed from the internal instances list (OriginManager already deleted it).

FreeCADGui.removeOrigin(obj)

  1. Finds the wrapper via findOrigin(obj).
  2. Removes it from the internal _instances vector.
  3. Calls OriginManager::instance()->unregisterOrigin(id), which deletes the wrapper.

Typical Usage (from SiloOrigin)

# mods/silo/freecad/silo_origin.py

class SiloOrigin:
    def id(self):     return "silo"
    def name(self):   return "Kindred Silo"
    # ... remaining interface methods ...

_silo_origin = SiloOrigin()

def register_silo_origin():
    FreeCADGui.addOrigin(_silo_origin)

def unregister_silo_origin():
    FreeCADGui.removeOrigin(_silo_origin)

This is called from InitGui.py via a deferred QTimer.singleShot (1500 ms after GUI init) to ensure the Gui module is fully loaded.

Method Dispatch

Every C++ FileOrigin virtual method is implemented by delegating to the corresponding Python method on the wrapped object. Three internal helpers handle the marshalling:

Helper Signature Used For
callStringMethod (name, default) → std::string id(), name(), nickname(), icon()
callBoolMethod (name, default) → bool All supportsXxx(), tracksExternally(), etc.
callMethod (name, args) → Py::Object Everything else (document ops, state queries)

Dispatch Pattern

Each overridden method follows the same structure:

1. Acquire GIL                     ← Base::PyGILStateLocker lock
2. Check if Python object has attr ← _inst.hasAttr("methodName")
3. If missing → return default     ← graceful degradation
4. Build Py::Tuple args            ← marshal C++ args to Python
5. Call Python method               ← func.apply(args)
6. Convert result                  ← Py::String/Boolean/Long → C++ type
7. Return                          ← release GIL on scope exit

If the Python method is missing, the bridge returns sensible defaults (empty string, false, nullptr). If the Python method raises an exception, it is caught, reported to the FreeCAD console via Base::PyException::reportException(), and the default is returned.

Document Argument Marshalling

Methods that take App::Document* convert it to a Python object using:

Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
{
    if (!doc) return Py::None();
    return Py::asObject(doc->getPyObject());
}

This returns the standard App.Document Python wrapper so the Python origin can call doc.Objects, doc.FileName, doc.save(), etc.

Methods that return an App::Document* (like newDocument, openDocument) check the return value with:

if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
    return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
}

If the Python method returns None or a non-Document object, the bridge returns nullptr.

GIL Management

Every method acquires the GIL (Base::PyGILStateLocker) before touching any Python objects. This is critical because the C++ origin methods can be called from:

  • The main Qt event loop (menu clicks, toolbar actions)
  • OriginManager::findOwningOrigin() during document operations
  • Signal handlers (fastsignals callbacks)

All of these may run on the main thread while the GIL is not held.

Enum Mapping

Python origins return integers for enum values:

OriginType (from type())

Python int C++ Enum
0 OriginType::Local
1 OriginType::PLM
2 OriginType::Cloud
3 OriginType::Custom

ConnectionState (from connectionState())

Python int C++ Enum
0 ConnectionState::Disconnected
1 ConnectionState::Connecting
2 ConnectionState::Connected
3 ConnectionState::Error

Out-of-range values fall back to OriginType::Custom and ConnectionState::Connected respectively.

Icon Resolution

The icon() override calls callStringMethod("icon") to get a string name, then passes it to BitmapFactory().iconFromTheme(name). The Python origin returns an icon name (e.g., "silo") rather than a QIcon object.

Required Python Interface

Methods the Python object must implement (called unconditionally):

Method Signature Purpose
id() → str Unique origin ID (cached on registration)
name() → str Display name
nickname() → str Short toolbar label
icon() → str Icon name for BitmapFactory
type() → int OriginType enum value
tracksExternally() → bool Whether origin syncs externally
requiresAuthentication() → bool Whether login is needed
ownsDocument(doc) → bool Ownership detection
documentIdentity(doc) → str Immutable tracking key
documentDisplayId(doc) → str Human-readable ID

Methods the Python object may implement (checked with hasAttr):

Method Signature Default
supportsRevisions() → bool False
supportsBOM() → bool False
supportsPartNumbers() → bool False
supportsAssemblies() → bool False
connectionState() → int 2 (Connected)
connect() → bool True
disconnect() → None no-op
syncProperties(doc) → bool True
newDocument(name) → Document None
openDocument(identity) → Document None
openDocumentInteractive() → Document None
saveDocument(doc) → bool False
saveDocumentAs(doc, id) → bool False
saveDocumentAsInteractive(doc) → bool False
commitDocument(doc) → bool False
pullDocument(doc) → bool False
pushDocument(doc) → bool False
showInfo(doc) → None no-op
showBOM(doc) → None no-op

Error Handling

All Python exceptions are caught and reported to the FreeCAD console. The bridge never propagates a Python exception into C++ — callers always receive a safe default value.

[Python exception in SiloOrigin.ownsDocument]
  → Base::PyException::reportException()
    → FreeCAD Console: "Python error: ..."
  → return false

This prevents a buggy Python origin from crashing the application.

Lifetime and Ownership

FreeCADGui.addOrigin(py_obj)
  │
  ├─ FileOriginPython(py_obj)       ← wraps, holds Py::Object ref
  │    └─ _inst = py_obj            ← prevents Python GC
  │
  ├─ _instances.push_back(wrapper)  ← static vector for findOrigin()
  │
  └─ OriginManager::registerOrigin(wrapper)
       └─ unique_ptr<FileOrigin>    ← OriginManager owns the wrapper
  • FileOriginPython holds a Py::Object reference to the Python instance, preventing garbage collection.
  • OriginManager owns the wrapper via std::unique_ptr.
  • The static _instances vector provides findOrigin() lookup but does not own the pointers.
  • On removeOrigin(): the wrapper is removed from _instances, then OriginManager::unregisterOrigin() deletes the wrapper, which releases the Py::Object reference.

See Also