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 7b0cb6d..a30e5ea 100644 --- a/Adding-Python-Bindings.-.md +++ b/Adding-Python-Bindings.md @@ -1,379 +1,379 @@ -# 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 C++ class or function to expose -- McRogueFace builds from project root with `make` - -## Quick Reference - -**Related Systems:** [[Python-Binding-Layer]], [[UI-Component-Hierarchy]] - -**Key Files:** -- `src/McRFPy_API.cpp` - Module definition and type registration -- `src/McRFPy_Doc.h` - Documentation macros (MCRF_METHOD, MCRF_PROPERTY, etc.) -- `src/PyObjectUtils.h` - Helper utilities -- Individual `src/Py*.cpp` files - Type bindings - ---- - -## Workflow 1: Adding a Property to Existing Class - -### Step 1: Add to PyGetSetDef Array - -Find the class's `getsetters` array: - -```cpp -PyGetSetDef UISprite::getsetters[] = { - // Existing properties... - - // Add new property with doc macro - {"rotation", (getter)UISprite::get_rotation, (setter)UISprite::set_rotation, - MCRF_PROPERTY(rotation, - "Sprite rotation angle in degrees. Range: 0-360." - ), NULL}, - - {NULL} // Sentinel - always last! -}; -``` - -### Step 2: Implement Getter Function - -```cpp -PyObject* UISprite::get_rotation(PyUISpriteObject* 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 UISprite::set_rotation(PyUISpriteObject* 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; -} -``` - -### Step 4: Rebuild and Test - -```bash -# From project root -make clean && make - -cd build -./mcrogueface --headless -c " -import mcrfpy, sys -s = mcrfpy.Sprite(x=0, y=0, sprite_index=0) -s.rotation = 45 -assert s.rotation == 45 -print('PASS') -sys.exit(0) -" -``` - ---- - -## Workflow 2: Adding a Method to Existing Class - -### Step 1: Add to PyMethodDef Array - -```cpp -PyMethodDef UIGrid::methods[] = { - // Existing methods... - - {"fill_walkable", (PyCFunction)UIGrid::fill_walkable, METH_VARARGS | METH_KEYWORDS, - MCRF_METHOD(Grid, fill_walkable, - MCRF_SIG("(walkable: bool)", "None"), - MCRF_DESC("Set walkable property for all cells."), - MCRF_ARGS_START - MCRF_ARG("walkable", "Whether cells should be walkable") - )}, - - {NULL} // Sentinel -}; -``` - -### Step 2: Implement Method - -```cpp -PyObject* UIGrid::fill_walkable(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Grid data is null"); - return NULL; - } - - int walkable; - static char* kwlist[] = {"walkable", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "p", kwlist, &walkable)) { - return NULL; - } - - for (int x = 0; x < self->data->grid_x; x++) { - for (int y = 0; y < self->data->grid_y; y++) { - self->data->at(x, y).walkable = walkable; - } - } - - Py_RETURN_NONE; -} -``` - -### Step 3: Test - -```python -import mcrfpy -import sys - -grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) -grid.fill_walkable(True) -assert grid.at(5, 5).walkable == True -print("PASS") -sys.exit(0) -``` - ---- - -## Workflow 3: Creating New Python Type - -### Step 1: Define Type Structure - -In new header file (e.g., `src/PyMyType.h`): - -```cpp -typedef struct { - PyObject_HEAD - std::shared_ptr data; -} PyMyTypeObject; - -namespace mcrfpydef { - static PyTypeObject PyMyTypeType = { - .tp_name = "mcrfpy.MyType", - .tp_basicsize = sizeof(PyMyTypeObject), - // ... filled in during init - }; -} - -class PyMyType { -public: - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; - - static int init(PyMyTypeObject* self, PyObject* args, PyObject* kwds); - static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); - static void dealloc(PyMyTypeObject* self); -}; -``` - -### Step 2: Implement (in `src/PyMyType.cpp`) - -```cpp -#include "PyMyType.h" -#include "McRFPy_Doc.h" - -int PyMyType::init(PyMyTypeObject* self, PyObject* args, PyObject* kwds) { - // Parse arguments, initialize self->data - self->data = std::make_shared(); - return 0; -} - -PyObject* PyMyType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { - auto* self = (PyMyTypeObject*)type->tp_alloc(type, 0); - if (self) self->data = nullptr; - return (PyObject*)self; -} - -void PyMyType::dealloc(PyMyTypeObject* self) { - self->data.reset(); - Py_TYPE(self)->tp_free((PyObject*)self); -} - -PyGetSetDef PyMyType::getsetters[] = { - {"name", (getter)get_name, (setter)set_name, - MCRF_PROPERTY(name, "Object name."), NULL}, - {NULL} -}; - -PyMethodDef PyMyType::methods[] = { - {NULL} -}; -``` - -### Step 3: Register in Module - -In `src/McRFPy_API.cpp`: - -```cpp -#include "PyMyType.h" - -// In the exported_types or internal_types array: -static PyTypeObject* exported_types[] = { - // ... existing types ... - &mcrfpydef::PyMyTypeType, -}; - -// Before PyType_Ready calls: -mcrfpydef::PyMyTypeType.tp_methods = PyMyType::methods; -mcrfpydef::PyMyTypeType.tp_getset = PyMyType::getsetters; -mcrfpydef::PyMyTypeType.tp_init = (initproc)PyMyType::init; -mcrfpydef::PyMyTypeType.tp_new = PyMyType::pynew; -mcrfpydef::PyMyTypeType.tp_dealloc = (destructor)PyMyType::dealloc; -``` - -CMake auto-discovers new `.cpp` files via GLOB_RECURSE, so no CMakeLists.txt changes needed. - ---- - -## Documentation Macros - -Use macros from `src/McRFPy_Doc.h` for all Python-facing documentation: - -| Macro | Purpose | -|-------|---------| -| `MCRF_METHOD(cls, name, ...)` | Method docstring | -| `MCRF_PROPERTY(name, desc)` | Property docstring | -| `MCRF_SIG(params, ret)` | Method signature | -| `MCRF_DESC(text)` | Description paragraph | -| `MCRF_ARGS_START` | Begin arguments section | -| `MCRF_ARG(name, desc)` | Individual argument | -| `MCRF_RETURNS(text)` | Return value description | -| `MCRF_RAISES(exc, cond)` | Exception documentation | -| `MCRF_NOTE(text)` | Important notes | -| `MCRF_LINK(path, text)` | External documentation link | - ---- - -## Testing Your Bindings - -### Direct Execution Test - -```python -import mcrfpy -import sys - -# Test property -s = mcrfpy.Sprite(x=0, y=0, sprite_index=0) -s.rotation = 45 -assert s.rotation == 45 - -# Test method -grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) -grid.fill_walkable(True) -assert grid.at(5, 5).walkable == True - -print("PASS") -sys.exit(0) -``` - -### Run - -```bash -cd build -./mcrogueface --headless --exec ../tests/unit/my_binding_test.py -``` - ---- - -## Regenerating Documentation - -After adding bindings: - -```bash -# From project root -make - -# Generate all docs (recommended) -./tools/generate_all_docs.sh - -# Or individually: -cd build -./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py -./mcrogueface --headless --exec ../tools/generate_stubs_v2.py -``` - ---- - -## Common Pitfalls - -### Pitfall 1: Forgetting NULL Sentinel - -```cpp -// WRONG - missing sentinel causes crash -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 - returns base UIDrawable wrapper -return generic_wrap(item->data); - -// CORRECT - preserves derived type (Frame, Caption, etc.) -RET_PY_INSTANCE(item->data); -``` - -### Pitfall 3: Missing Error Checks - -```cpp -// WRONG - no type check -double value = PyFloat_AsDouble(obj); - -// CORRECT -if (!PyFloat_Check(obj) && !PyLong_Check(obj)) { - PyErr_SetString(PyExc_TypeError, "Expected number"); - return NULL; -} -double value = PyFloat_AsDouble(obj); -``` - -### Pitfall 4: Not Setting tp_methods/tp_getset Before PyType_Ready - -```cpp -// WRONG - setting after PyType_Ready has no effect -PyType_Ready(&MyType); -MyType.tp_methods = methods; - -// CORRECT - set before PyType_Ready -MyType.tp_methods = methods; -MyType.tp_getset = getsetters; -PyType_Ready(&MyType); -``` - ---- - -## Related Documentation - -- [[Python-Binding-Layer]] - System architecture -- [[UI-Component-Hierarchy]] - Classes exposed to Python -- `src/McRFPy_Doc.h` - Documentation macro reference +# 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 C++ class or function to expose +- McRogueFace builds from project root with `make` + +## Quick Reference + +**Related Systems:** [[Python-Binding-Layer]], [[UI-Component-Hierarchy]] + +**Key Files:** +- `src/McRFPy_API.cpp` - Module definition and type registration +- `src/McRFPy_Doc.h` - Documentation macros (MCRF_METHOD, MCRF_PROPERTY, etc.) +- `src/PyObjectUtils.h` - Helper utilities +- Individual `src/Py*.cpp` files - Type bindings + +--- + +## Workflow 1: Adding a Property to Existing Class + +### Step 1: Add to PyGetSetDef Array + +Find the class's `getsetters` array: + +```cpp +PyGetSetDef UISprite::getsetters[] = { + // Existing properties... + + // Add new property with doc macro + {"rotation", (getter)UISprite::get_rotation, (setter)UISprite::set_rotation, + MCRF_PROPERTY(rotation, + "Sprite rotation angle in degrees. Range: 0-360." + ), NULL}, + + {NULL} // Sentinel - always last! +}; +``` + +### Step 2: Implement Getter Function + +```cpp +PyObject* UISprite::get_rotation(PyUISpriteObject* 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 UISprite::set_rotation(PyUISpriteObject* 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; +} +``` + +### Step 4: Rebuild and Test + +```bash +# From project root +make clean && make + +cd build +./mcrogueface --headless -c " +import mcrfpy, sys +s = mcrfpy.Sprite(x=0, y=0, sprite_index=0) +s.rotation = 45 +assert s.rotation == 45 +print('PASS') +sys.exit(0) +" +``` + +--- + +## Workflow 2: Adding a Method to Existing Class + +### Step 1: Add to PyMethodDef Array + +```cpp +PyMethodDef UIGrid::methods[] = { + // Existing methods... + + {"fill_walkable", (PyCFunction)UIGrid::fill_walkable, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(Grid, fill_walkable, + MCRF_SIG("(walkable: bool)", "None"), + MCRF_DESC("Set walkable property for all cells."), + MCRF_ARGS_START + MCRF_ARG("walkable", "Whether cells should be walkable") + )}, + + {NULL} // Sentinel +}; +``` + +### Step 2: Implement Method + +```cpp +PyObject* UIGrid::fill_walkable(PyUIGridObject* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Grid data is null"); + return NULL; + } + + int walkable; + static char* kwlist[] = {"walkable", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "p", kwlist, &walkable)) { + return NULL; + } + + for (int x = 0; x < self->data->grid_x; x++) { + for (int y = 0; y < self->data->grid_y; y++) { + self->data->at(x, y).walkable = walkable; + } + } + + Py_RETURN_NONE; +} +``` + +### Step 3: Test + +```python +import mcrfpy +import sys + +grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) +grid.fill_walkable(True) +assert grid.at(5, 5).walkable == True +print("PASS") +sys.exit(0) +``` + +--- + +## Workflow 3: Creating New Python Type + +### Step 1: Define Type Structure + +In new header file (e.g., `src/PyMyType.h`): + +```cpp +typedef struct { + PyObject_HEAD + std::shared_ptr data; +} PyMyTypeObject; + +namespace mcrfpydef { + static PyTypeObject PyMyTypeType = { + .tp_name = "mcrfpy.MyType", + .tp_basicsize = sizeof(PyMyTypeObject), + // ... filled in during init + }; +} + +class PyMyType { +public: + static PyGetSetDef getsetters[]; + static PyMethodDef methods[]; + + static int init(PyMyTypeObject* self, PyObject* args, PyObject* kwds); + static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); + static void dealloc(PyMyTypeObject* self); +}; +``` + +### Step 2: Implement (in `src/PyMyType.cpp`) + +```cpp +#include "PyMyType.h" +#include "McRFPy_Doc.h" + +int PyMyType::init(PyMyTypeObject* self, PyObject* args, PyObject* kwds) { + // Parse arguments, initialize self->data + self->data = std::make_shared(); + return 0; +} + +PyObject* PyMyType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { + auto* self = (PyMyTypeObject*)type->tp_alloc(type, 0); + if (self) self->data = nullptr; + return (PyObject*)self; +} + +void PyMyType::dealloc(PyMyTypeObject* self) { + self->data.reset(); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyGetSetDef PyMyType::getsetters[] = { + {"name", (getter)get_name, (setter)set_name, + MCRF_PROPERTY(name, "Object name."), NULL}, + {NULL} +}; + +PyMethodDef PyMyType::methods[] = { + {NULL} +}; +``` + +### Step 3: Register in Module + +In `src/McRFPy_API.cpp`: + +```cpp +#include "PyMyType.h" + +// In the exported_types or internal_types array: +static PyTypeObject* exported_types[] = { + // ... existing types ... + &mcrfpydef::PyMyTypeType, +}; + +// Before PyType_Ready calls: +mcrfpydef::PyMyTypeType.tp_methods = PyMyType::methods; +mcrfpydef::PyMyTypeType.tp_getset = PyMyType::getsetters; +mcrfpydef::PyMyTypeType.tp_init = (initproc)PyMyType::init; +mcrfpydef::PyMyTypeType.tp_new = PyMyType::pynew; +mcrfpydef::PyMyTypeType.tp_dealloc = (destructor)PyMyType::dealloc; +``` + +CMake auto-discovers new `.cpp` files via GLOB_RECURSE, so no CMakeLists.txt changes needed. + +--- + +## Documentation Macros + +Use macros from `src/McRFPy_Doc.h` for all Python-facing documentation: + +| Macro | Purpose | +|-------|---------| +| `MCRF_METHOD(cls, name, ...)` | Method docstring | +| `MCRF_PROPERTY(name, desc)` | Property docstring | +| `MCRF_SIG(params, ret)` | Method signature | +| `MCRF_DESC(text)` | Description paragraph | +| `MCRF_ARGS_START` | Begin arguments section | +| `MCRF_ARG(name, desc)` | Individual argument | +| `MCRF_RETURNS(text)` | Return value description | +| `MCRF_RAISES(exc, cond)` | Exception documentation | +| `MCRF_NOTE(text)` | Important notes | +| `MCRF_LINK(path, text)` | External documentation link | + +--- + +## Testing Your Bindings + +### Direct Execution Test + +```python +import mcrfpy +import sys + +# Test property +s = mcrfpy.Sprite(x=0, y=0, sprite_index=0) +s.rotation = 45 +assert s.rotation == 45 + +# Test method +grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) +grid.fill_walkable(True) +assert grid.at(5, 5).walkable == True + +print("PASS") +sys.exit(0) +``` + +### Run + +```bash +cd build +./mcrogueface --headless --exec ../tests/unit/my_binding_test.py +``` + +--- + +## Regenerating Documentation + +After adding bindings: + +```bash +# From project root +make + +# Generate all docs (recommended) +./tools/generate_all_docs.sh + +# Or individually: +cd build +./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py +./mcrogueface --headless --exec ../tools/generate_stubs_v2.py +``` + +--- + +## Common Pitfalls + +### Pitfall 1: Forgetting NULL Sentinel + +```cpp +// WRONG - missing sentinel causes crash +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 - returns base UIDrawable wrapper +return generic_wrap(item->data); + +// CORRECT - preserves derived type (Frame, Caption, etc.) +RET_PY_INSTANCE(item->data); +``` + +### Pitfall 3: Missing Error Checks + +```cpp +// WRONG - no type check +double value = PyFloat_AsDouble(obj); + +// CORRECT +if (!PyFloat_Check(obj) && !PyLong_Check(obj)) { + PyErr_SetString(PyExc_TypeError, "Expected number"); + return NULL; +} +double value = PyFloat_AsDouble(obj); +``` + +### Pitfall 4: Not Setting tp_methods/tp_getset Before PyType_Ready + +```cpp +// WRONG - setting after PyType_Ready has no effect +PyType_Ready(&MyType); +MyType.tp_methods = methods; + +// CORRECT - set before PyType_Ready +MyType.tp_methods = methods; +MyType.tp_getset = getsetters; +PyType_Ready(&MyType); +``` + +--- + +## Related Documentation + +- [[Python-Binding-Layer]] - System architecture +- [[UI-Component-Hierarchy]] - Classes exposed to Python +- `src/McRFPy_Doc.h` - Documentation macro reference - [Python C API Reference](https://docs.python.org/3/c-api/) \ No newline at end of file