Update Adding Python Bindings
parent
3be2fec45c
commit
d65242a074
1 changed files with 378 additions and 378 deletions
|
|
@ -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<MyType> 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<MyType>();
|
||||
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<MyType> 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<MyType>();
|
||||
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/)
|
||||
Loading…
Add table
Add a link
Reference in a new issue