From c9f6f9e02b077ccf5671f64f49b392ac42c2de08 Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Tue, 10 Feb 2026 08:02:02 -0600 Subject: [PATCH] =?UTF-8?q?docs(c++):=20FileOriginPython=20bridge=20?= =?UTF-8?q?=E2=80=94=20Python-to-C++=20origin=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the FileOriginPython bridge: registration via FreeCADGui.addOrigin()/removeOrigin(), method dispatch pattern with GIL management, callStringMethod/callBoolMethod/callMethod helpers, document argument marshalling (App::Document* <-> App.Document), enum mapping tables for OriginType and ConnectionState, required vs optional Python interface methods with defaults, error handling (never propagates Python exceptions into C++), icon resolution via BitmapFactory, and lifetime/ ownership model between Py::Object, _instances vector, and OriginManager unique_ptr. Closes #135 --- docs/src/SUMMARY.md | 1 + docs/src/reference/cpp-file-origin-python.md | 257 +++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 docs/src/reference/cpp-file-origin-python.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a65e44ad90..467f53a381 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -52,3 +52,4 @@ - [FileOrigin Interface](./reference/cpp-file-origin.md) - [OriginManager](./reference/cpp-origin-manager.md) - [CommandOrigin](./reference/cpp-command-origin.md) +- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md) diff --git a/docs/src/reference/cpp-file-origin-python.md b/docs/src/reference/cpp-file-origin-python.md new file mode 100644 index 0000000000..14941a0c67 --- /dev/null +++ b/docs/src/reference/cpp-file-origin-python.md @@ -0,0 +1,257 @@ +# 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](./cpp-file-origin.md). This is how Python +addons (like SiloOrigin) integrate with the C++ +[OriginManager](./cpp-origin-manager.md) without compiling any C++ code. + +## Registration from Python + +The bridge is exposed to Python through three `FreeCADGui` module +functions: + +```python +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) + +```python +# 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: + +```cpp +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: + +```cpp +if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) { + return static_cast(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 ← 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 + +- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface + being adapted +- [OriginManager](./cpp-origin-manager.md) — where the wrapped origin + gets registered +- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch + through this bridge +- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md) + — step-by-step guide using this bridge