From 4c86db60671922a02eae30dabc740705bdcf90be Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 2 Dec 2025 03:09:30 +0000 Subject: [PATCH] Update Adding Python Bindings --- ...Bindings.-.md => Adding-Python-Bindings.md | 868 +++++++++--------- 1 file changed, 434 insertions(+), 434 deletions(-) rename Adding-Python-Bindings.-.md => Adding-Python-Bindings.md (95%) diff --git a/Adding-Python-Bindings.-.md b/Adding-Python-Bindings.md similarity index 95% rename from Adding-Python-Bindings.-.md rename to Adding-Python-Bindings.md index 7d9fc9c..6838458 100644 --- a/Adding-Python-Bindings.-.md +++ b/Adding-Python-Bindings.md @@ -1,435 +1,435 @@ -# Adding Python Bindings - -Step-by-step guide for exposing C++ functionality to Python in McRogueFace. - -## Prerequisites - -- Understanding of [[Python-Binding-Layer]] system architecture -- Familiarity with `PYTHON_BINDING_PATTERNS.md` (repository root) -- C++ class or function to expose - -## Quick Reference - -**Related Systems:** [[Python-Binding-Layer]], [[UI-Component-Hierarchy]] - -**Key Files:** -- `src/McRFPy_API.cpp` - Module-level functions -- `src/PyObjectUtils.h` - Helper utilities -- Individual `src/UI*.cpp` files - Type bindings - -**Documentation Format:** See CLAUDE.md "Inline C++ Documentation Format" - ---- - -## Workflow 1: Adding a Property to Existing Class - -### Step 1: Add to PyGetSetDef Array - -Find the class's `getsetters` array (e.g., `PyUISprite::getsetters`): - -```cpp -PyGetSetDef PyUISprite::getsetters[] = { - // Existing properties... - - // Add new property - {"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, - "Sprite rotation angle in degrees. Range: 0-360.", NULL}, - - {NULL} // Sentinel - always last! -}; -``` - -### Step 2: Implement Getter Function - -```cpp -PyObject* PyUISprite::get_rotation(PyUISprite* self, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "UISprite data is null"); - return NULL; - } - - return PyFloat_FromDouble(self->data->rotation); -} -``` - -### Step 3: Implement Setter Function - -```cpp -int PyUISprite::set_rotation(PyUISprite* self, PyObject* value, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "UISprite data is null"); - return -1; - } - - if (!PyFloat_Check(value) && !PyLong_Check(value)) { - PyErr_SetString(PyExc_TypeError, "rotation must be a number"); - return -1; - } - - double rotation = PyFloat_AsDouble(value); - if (rotation < 0 || rotation > 360) { - PyErr_SetString(PyExc_ValueError, "rotation must be 0-360"); - return -1; - } - - self->data->rotation = rotation; - return 0; // Success -} -``` - -### Step 4: Add Documentation - -Update the docstring in PyGetSetDef: - -```cpp -{"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, - "Sprite rotation angle in degrees.\n\n" - "Range: 0-360 degrees. 0 is upright, increases clockwise.\n\n" - "Example:\n" - " sprite.rotation = 90 # Rotate 90 degrees clockwise\n\n" - "Note:\n" - " Rotation is applied during rendering, not to position.", - NULL}, -``` - -### Step 5: Rebuild and Test - -```bash -make clean && make - -cd build -./mcrogueface --exec test_rotation.py -``` - ---- - -## Workflow 2: Adding a Method to Existing Class - -### Step 1: Add to PyMethodDef Array - -Find the class's `methods` array: - -```cpp -PyMethodDef PyUIGrid::methods[] = { - // Existing methods... - - {"fill_rect", (PyCFunction)PyUIGrid::fill_rect, METH_VARARGS | METH_KEYWORDS, - "fill_rect(x: int, y: int, w: int, h: int, tile: int) -> None\n\n" - "Fill rectangular area with tile index.\n\n" - "Args:\n" - " x: Top-left X coordinate\n" - " y: Top-left Y coordinate\n" - " w: Width in tiles\n" - " h: Height in tiles\n" - " tile: Tile sprite index\n\n" - "Example:\n" - " grid.fill_rect(5, 5, 10, 10, 42) # Fill 10x10 area with tile 42"}, - - {NULL} // Sentinel -}; -``` - -### Step 2: Implement Method Function - -```cpp -PyObject* PyUIGrid::fill_rect(PyUIGrid* self, PyObject* args, PyObject* kwds) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Grid data is null"); - return NULL; - } - - int x, y, w, h, tile; - static char* kwlist[] = {"x", "y", "w", "h", "tile", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiiii", kwlist, - &x, &y, &w, &h, &tile)) { - return NULL; // PyArg functions set error automatically - } - - // Bounds checking - if (x < 0 || y < 0 || x + w > self->data->grid_x || y + h > self->data->grid_y) { - PyErr_SetString(PyExc_ValueError, "Rectangle out of grid bounds"); - return NULL; - } - - // Fill the rectangle - for (int dx = 0; dx < w; dx++) { - for (int dy = 0; dy < h; dy++) { - self->data->at(x + dx, y + dy).tilesprite = tile; - } - } - - Py_RETURN_NONE; -} -``` - -### Step 3: Test - -```python -import mcrfpy - -grid = mcrfpy.Grid(50, 50, 16, 16) -grid.texture = mcrfpy.createTexture("tiles.png") - -# Test new method -grid.fill_rect(10, 10, 5, 5, 42) - -# Verify -assert grid.at((10, 10)).tilesprite == 42 -print("Test passed!") -``` - ---- - -## Workflow 3: Creating New Python Type - -### Step 1: Define Python Type Structure - -In new header file (e.g., `src/UIButton.h`): - -```cpp -// Python object wrapper -typedef struct { - PyObject_HEAD - std::shared_ptr data; -} PyUIButtonObject; - -// Python type object -class PyUIButton { -public: - static PyTypeObject Type; - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; - - // Lifecycle - static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); - static int pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds); - static void dealloc(PyUIButtonObject* self); - - // Properties - static PyObject* get_text(PyUIButtonObject* self, void* closure); - static int set_text(PyUIButtonObject* self, PyObject* value, void* closure); - // ... more properties -}; -``` - -### Step 2: Implement Type Object - -In `src/UIButton.cpp`: - -```cpp -PyTypeObject PyUIButton::Type = { - PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "mcrfpy.Button", - .tp_basicsize = sizeof(PyUIButtonObject), - .tp_dealloc = (destructor)PyUIButton::dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = "Button UI element with click handling", - .tp_methods = PyUIButton::methods, - .tp_getset = PyUIButton::getsetters, - .tp_new = PyUIButton::pynew, - .tp_init = (initproc)PyUIButton::pyinit, -}; -``` - -### Step 3: Register in Module - -In `src/McRFPy_API.cpp::PyInit_mcrfpy()`: - -```cpp -// After other type registrations -if (PyType_Ready(&PyUIButton::Type) < 0) { - return NULL; -} - -Py_INCREF(&PyUIButton::Type); -if (PyModule_AddObject(m, "Button", (PyObject*)&PyUIButton::Type) < 0) { - Py_DECREF(&PyUIButton::Type); - Py_DECREF(m); - return NULL; -} -``` - -### Step 4: Implement Constructor - -```cpp -PyObject* PyUIButton::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { - PyUIButtonObject* self = (PyUIButtonObject*)type->tp_alloc(type, 0); - if (self != NULL) { - self->data = nullptr; // Initialize in __init__ - } - return (PyObject*)self; -} - -int PyUIButton::pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds) { - int x = 0, y = 0, w = 100, h = 30; - const char* text = ""; - - static char* kwlist[] = {"x", "y", "w", "h", "text", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiiis", kwlist, - &x, &y, &w, &h, &text)) { - return -1; - } - - self->data = std::make_shared(x, y, w, h, text); - return 0; -} -``` - ---- - -## Common Patterns & Helpers - -### PyArgHelpers for Position/Size - -Use standardized helpers for tuple support: - -```cpp -#include "PyArgHelpers.h" - -// Accept (x, y) tuple OR separate x, y args -int x, y; -if (!PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y")) { - return -1; -} -``` - -**See:** `src/PyArgHelpers.h` for complete helper reference - -### Closure Parameter Encoding - -For UIDrawable-derived types: - -```cpp -// Encode type and member index -(void*)((intptr_t)PyObjectsEnum::BUTTON << 8 | BUTTON_MEMBER_TEXT) -``` - -**See:** `PYTHON_BINDING_PATTERNS.md` for complete encoding scheme - -### Error Handling - -**Always check for NULL:** -```cpp -if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Object data is null"); - return NULL; // Or -1 for setters -} -``` - -**Set descriptive errors:** -```cpp -PyErr_Format(PyExc_ValueError, "Index %d out of range (0-%d)", idx, max_idx); -``` - ---- - -## Testing Your Bindings - -### Manual Testing - -```python -#!/usr/bin/env python3 -import mcrfpy - -# Test property -sprite = mcrfpy.Sprite("test.png", 0, 0) -sprite.rotation = 45 -assert sprite.rotation == 45 - -# Test method -grid = mcrfpy.Grid(10, 10, 16, 16) -grid.fill_rect(0, 0, 5, 5, 42) -assert grid.at((0, 0)).tilesprite == 42 - -print("All tests passed!") -``` - -### Automated Testing - -Create test in `tests/test_new_binding.py`: - -```python -import mcrfpy -import sys - -def test_binding(): - # Your tests here - pass - -if __name__ == "__main__": - try: - test_binding() - print("PASS") - sys.exit(0) - except Exception as e: - print(f"FAIL: {e}") - sys.exit(1) -``` - -Run: `./build/mcrogueface --headless --exec tests/test_new_binding.py` - ---- - -## Common Pitfalls - -### Pitfall 1: Forgetting NULL Sentinel - -```cpp -// WRONG - missing sentinel -PyGetSetDef getsetters[] = { - {"x", get_x, set_x, "X position", NULL} -}; - -// CORRECT -PyGetSetDef getsetters[] = { - {"x", get_x, set_x, "X position", NULL}, - {NULL} // Must have this! -}; -``` - -### Pitfall 2: Type Preservation in Collections - -When returning from collections, use `RET_PY_INSTANCE`: - -```cpp -// WRONG - loses derived type -return (PyObject*)item->data.get(); - -// CORRECT - preserves type -RET_PY_INSTANCE(item->data); -``` - -### Pitfall 3: Missing Error Checks - -```cpp -// WRONG - no error check -double value = PyFloat_AsDouble(obj); - -// CORRECT -if (!PyFloat_Check(obj)) { - PyErr_SetString(PyExc_TypeError, "Expected float"); - return NULL; -} -double value = PyFloat_AsDouble(obj); -``` - ---- - -## Related Documentation - -- [[Python-Binding-Layer]] - System architecture -- `PYTHON_BINDING_PATTERNS.md` - Complete pattern reference -- CLAUDE.md - Inline documentation format -- [Python C API Reference](https://docs.python.org/3/c-api/) - -## Next Steps - -After adding bindings: -1. Rebuild: `make clean && make` -2. Test manually -3. Add automated test in `tests/` -4. Regenerate stub files: `./build/mcrogueface --exec tools/generate_stubs.py` -5. Update API docs: `./build/mcrogueface --exec tools/generate_dynamic_docs.py` +# Adding Python Bindings + +Step-by-step guide for exposing C++ functionality to Python in McRogueFace. + +## Prerequisites + +- Understanding of [[Python-Binding-Layer]] system architecture +- Familiarity with `PYTHON_BINDING_PATTERNS.md` (repository root) +- C++ class or function to expose + +## Quick Reference + +**Related Systems:** [[Python-Binding-Layer]], [[UI-Component-Hierarchy]] + +**Key Files:** +- `src/McRFPy_API.cpp` - Module-level functions +- `src/PyObjectUtils.h` - Helper utilities +- Individual `src/UI*.cpp` files - Type bindings + +**Documentation Format:** See CLAUDE.md "Inline C++ Documentation Format" + +--- + +## Workflow 1: Adding a Property to Existing Class + +### Step 1: Add to PyGetSetDef Array + +Find the class's `getsetters` array (e.g., `PyUISprite::getsetters`): + +```cpp +PyGetSetDef PyUISprite::getsetters[] = { + // Existing properties... + + // Add new property + {"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, + "Sprite rotation angle in degrees. Range: 0-360.", NULL}, + + {NULL} // Sentinel - always last! +}; +``` + +### Step 2: Implement Getter Function + +```cpp +PyObject* PyUISprite::get_rotation(PyUISprite* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "UISprite data is null"); + return NULL; + } + + return PyFloat_FromDouble(self->data->rotation); +} +``` + +### Step 3: Implement Setter Function + +```cpp +int PyUISprite::set_rotation(PyUISprite* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "UISprite data is null"); + return -1; + } + + if (!PyFloat_Check(value) && !PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "rotation must be a number"); + return -1; + } + + double rotation = PyFloat_AsDouble(value); + if (rotation < 0 || rotation > 360) { + PyErr_SetString(PyExc_ValueError, "rotation must be 0-360"); + return -1; + } + + self->data->rotation = rotation; + return 0; // Success +} +``` + +### Step 4: Add Documentation + +Update the docstring in PyGetSetDef: + +```cpp +{"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, + "Sprite rotation angle in degrees.\n\n" + "Range: 0-360 degrees. 0 is upright, increases clockwise.\n\n" + "Example:\n" + " sprite.rotation = 90 # Rotate 90 degrees clockwise\n\n" + "Note:\n" + " Rotation is applied during rendering, not to position.", + NULL}, +``` + +### Step 5: Rebuild and Test + +```bash +make clean && make + +cd build +./mcrogueface --exec test_rotation.py +``` + +--- + +## Workflow 2: Adding a Method to Existing Class + +### Step 1: Add to PyMethodDef Array + +Find the class's `methods` array: + +```cpp +PyMethodDef PyUIGrid::methods[] = { + // Existing methods... + + {"fill_rect", (PyCFunction)PyUIGrid::fill_rect, METH_VARARGS | METH_KEYWORDS, + "fill_rect(x: int, y: int, w: int, h: int, tile: int) -> None\n\n" + "Fill rectangular area with tile index.\n\n" + "Args:\n" + " x: Top-left X coordinate\n" + " y: Top-left Y coordinate\n" + " w: Width in tiles\n" + " h: Height in tiles\n" + " tile: Tile sprite index\n\n" + "Example:\n" + " grid.fill_rect(5, 5, 10, 10, 42) # Fill 10x10 area with tile 42"}, + + {NULL} // Sentinel +}; +``` + +### Step 2: Implement Method Function + +```cpp +PyObject* PyUIGrid::fill_rect(PyUIGrid* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Grid data is null"); + return NULL; + } + + int x, y, w, h, tile; + static char* kwlist[] = {"x", "y", "w", "h", "tile", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiiii", kwlist, + &x, &y, &w, &h, &tile)) { + return NULL; // PyArg functions set error automatically + } + + // Bounds checking + if (x < 0 || y < 0 || x + w > self->data->grid_x || y + h > self->data->grid_y) { + PyErr_SetString(PyExc_ValueError, "Rectangle out of grid bounds"); + return NULL; + } + + // Fill the rectangle + for (int dx = 0; dx < w; dx++) { + for (int dy = 0; dy < h; dy++) { + self->data->at(x + dx, y + dy).tilesprite = tile; + } + } + + Py_RETURN_NONE; +} +``` + +### Step 3: Test + +```python +import mcrfpy + +grid = mcrfpy.Grid(50, 50, 16, 16) +grid.texture = mcrfpy.createTexture("tiles.png") + +# Test new method +grid.fill_rect(10, 10, 5, 5, 42) + +# Verify +assert grid.at((10, 10)).tilesprite == 42 +print("Test passed!") +``` + +--- + +## Workflow 3: Creating New Python Type + +### Step 1: Define Python Type Structure + +In new header file (e.g., `src/UIButton.h`): + +```cpp +// Python object wrapper +typedef struct { + PyObject_HEAD + std::shared_ptr data; +} PyUIButtonObject; + +// Python type object +class PyUIButton { +public: + static PyTypeObject Type; + static PyGetSetDef getsetters[]; + static PyMethodDef methods[]; + + // Lifecycle + static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); + static int pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds); + static void dealloc(PyUIButtonObject* self); + + // Properties + static PyObject* get_text(PyUIButtonObject* self, void* closure); + static int set_text(PyUIButtonObject* self, PyObject* value, void* closure); + // ... more properties +}; +``` + +### Step 2: Implement Type Object + +In `src/UIButton.cpp`: + +```cpp +PyTypeObject PyUIButton::Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mcrfpy.Button", + .tp_basicsize = sizeof(PyUIButtonObject), + .tp_dealloc = (destructor)PyUIButton::dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "Button UI element with click handling", + .tp_methods = PyUIButton::methods, + .tp_getset = PyUIButton::getsetters, + .tp_new = PyUIButton::pynew, + .tp_init = (initproc)PyUIButton::pyinit, +}; +``` + +### Step 3: Register in Module + +In `src/McRFPy_API.cpp::PyInit_mcrfpy()`: + +```cpp +// After other type registrations +if (PyType_Ready(&PyUIButton::Type) < 0) { + return NULL; +} + +Py_INCREF(&PyUIButton::Type); +if (PyModule_AddObject(m, "Button", (PyObject*)&PyUIButton::Type) < 0) { + Py_DECREF(&PyUIButton::Type); + Py_DECREF(m); + return NULL; +} +``` + +### Step 4: Implement Constructor + +```cpp +PyObject* PyUIButton::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { + PyUIButtonObject* self = (PyUIButtonObject*)type->tp_alloc(type, 0); + if (self != NULL) { + self->data = nullptr; // Initialize in __init__ + } + return (PyObject*)self; +} + +int PyUIButton::pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds) { + int x = 0, y = 0, w = 100, h = 30; + const char* text = ""; + + static char* kwlist[] = {"x", "y", "w", "h", "text", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiiis", kwlist, + &x, &y, &w, &h, &text)) { + return -1; + } + + self->data = std::make_shared(x, y, w, h, text); + return 0; +} +``` + +--- + +## Common Patterns & Helpers + +### PyArgHelpers for Position/Size + +Use standardized helpers for tuple support: + +```cpp +#include "PyArgHelpers.h" + +// Accept (x, y) tuple OR separate x, y args +int x, y; +if (!PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y")) { + return -1; +} +``` + +**See:** `src/PyArgHelpers.h` for complete helper reference + +### Closure Parameter Encoding + +For UIDrawable-derived types: + +```cpp +// Encode type and member index +(void*)((intptr_t)PyObjectsEnum::BUTTON << 8 | BUTTON_MEMBER_TEXT) +``` + +**See:** `PYTHON_BINDING_PATTERNS.md` for complete encoding scheme + +### Error Handling + +**Always check for NULL:** +```cpp +if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Object data is null"); + return NULL; // Or -1 for setters +} +``` + +**Set descriptive errors:** +```cpp +PyErr_Format(PyExc_ValueError, "Index %d out of range (0-%d)", idx, max_idx); +``` + +--- + +## Testing Your Bindings + +### Manual Testing + +```python +#!/usr/bin/env python3 +import mcrfpy + +# Test property +sprite = mcrfpy.Sprite("test.png", 0, 0) +sprite.rotation = 45 +assert sprite.rotation == 45 + +# Test method +grid = mcrfpy.Grid(10, 10, 16, 16) +grid.fill_rect(0, 0, 5, 5, 42) +assert grid.at((0, 0)).tilesprite == 42 + +print("All tests passed!") +``` + +### Automated Testing + +Create test in `tests/test_new_binding.py`: + +```python +import mcrfpy +import sys + +def test_binding(): + # Your tests here + pass + +if __name__ == "__main__": + try: + test_binding() + print("PASS") + sys.exit(0) + except Exception as e: + print(f"FAIL: {e}") + sys.exit(1) +``` + +Run: `./build/mcrogueface --headless --exec tests/test_new_binding.py` + +--- + +## Common Pitfalls + +### Pitfall 1: Forgetting NULL Sentinel + +```cpp +// WRONG - missing sentinel +PyGetSetDef getsetters[] = { + {"x", get_x, set_x, "X position", NULL} +}; + +// CORRECT +PyGetSetDef getsetters[] = { + {"x", get_x, set_x, "X position", NULL}, + {NULL} // Must have this! +}; +``` + +### Pitfall 2: Type Preservation in Collections + +When returning from collections, use `RET_PY_INSTANCE`: + +```cpp +// WRONG - loses derived type +return (PyObject*)item->data.get(); + +// CORRECT - preserves type +RET_PY_INSTANCE(item->data); +``` + +### Pitfall 3: Missing Error Checks + +```cpp +// WRONG - no error check +double value = PyFloat_AsDouble(obj); + +// CORRECT +if (!PyFloat_Check(obj)) { + PyErr_SetString(PyExc_TypeError, "Expected float"); + return NULL; +} +double value = PyFloat_AsDouble(obj); +``` + +--- + +## Related Documentation + +- [[Python-Binding-Layer]] - System architecture +- `PYTHON_BINDING_PATTERNS.md` - Complete pattern reference +- CLAUDE.md - Inline documentation format +- [Python C API Reference](https://docs.python.org/3/c-api/) + +## Next Steps + +After adding bindings: +1. Rebuild: `make clean && make` +2. Test manually +3. Add automated test in `tests/` +4. Regenerate stub files: `./build/mcrogueface --exec tools/generate_stubs.py` +5. Update API docs: `./build/mcrogueface --exec tools/generate_dynamic_docs.py` 6. Document in wiki if new system \ No newline at end of file