Update Adding-Python-Bindings: modern constructors, MCRF_* doc macros, exported_types[] pattern, generate_stubs_v2.py
parent
421f74639a
commit
35d5c12b68
2 changed files with 379 additions and 435 deletions
379
Adding-Python-Bindings.-.md
Normal file
379
Adding-Python-Bindings.-.md
Normal file
|
|
@ -0,0 +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
|
||||||
|
- [Python C API Reference](https://docs.python.org/3/c-api/)
|
||||||
|
|
@ -1,435 +0,0 @@
|
||||||
# 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<UIButton> 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<UIButton>(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
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue