feat(Grid): flexible at() method arguments
- Support tuple argument: grid.at((x, y)) - Support keyword arguments: grid.at(x=5, y=3) - Support pos keyword: grid.at(pos=(2, 8)) - Maintain backward compatibility with grid.at(x, y) - Add comprehensive error handling for invalid arguments Improves API ergonomics and Python-like flexibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cd0bd5468b
commit
5b6b0cc8ff
34 changed files with 1930 additions and 184 deletions
250
src/UIGrid.cpp
250
src/UIGrid.cpp
|
|
@ -475,13 +475,75 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
|||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
|
||||
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
|
||||
PyErr_SetString(PyExc_TypeError, "UIGrid.at requires two integer arguments: (x, y)");
|
||||
static const char* keywords[] = { "x", "y", "pos", nullptr };
|
||||
int x = -1, y = -1;
|
||||
PyObject* pos = nullptr;
|
||||
|
||||
// Try to parse with keywords first
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast<char**>(keywords), &x, &y, &pos)) {
|
||||
PyErr_Clear(); // Clear the error and try different parsing
|
||||
|
||||
// Check if we have a single tuple argument (x, y)
|
||||
if (PyTuple_Size(args) == 1 && kwds == nullptr) {
|
||||
PyObject* arg = PyTuple_GetItem(args, 0);
|
||||
if (PyTuple_Check(arg) && PyTuple_Size(arg) == 2) {
|
||||
// It's a tuple, extract x and y
|
||||
PyObject* x_obj = PyTuple_GetItem(arg, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(arg, 1);
|
||||
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
||||
x = PyLong_AsLong(x_obj);
|
||||
y = PyLong_AsLong(y_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Tuple elements must be integers");
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)");
|
||||
return NULL;
|
||||
}
|
||||
} else if (PyTuple_Size(args) == 2 && kwds == nullptr) {
|
||||
// Two positional arguments
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Arguments must be integers");
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pos keyword argument
|
||||
if (pos != nullptr) {
|
||||
if (x != -1 || y != -1) {
|
||||
PyErr_SetString(PyExc_TypeError, "Cannot specify both pos and x/y arguments");
|
||||
return NULL;
|
||||
}
|
||||
if (PyTuple_Check(pos) && PyTuple_Size(pos) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(pos, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(pos, 1);
|
||||
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
||||
x = PyLong_AsLong(x_obj);
|
||||
y = PyLong_AsLong(y_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple elements must be integers");
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two integers");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate we have both x and y
|
||||
if (x == -1 || y == -1) {
|
||||
PyErr_SetString(PyExc_TypeError, "UIGrid.at requires both x and y coordinates");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Range validation
|
||||
if (x < 0 || x >= self->data->grid_x) {
|
||||
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");
|
||||
return NULL;
|
||||
|
|
@ -501,7 +563,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
|
|||
}
|
||||
|
||||
PyMethodDef UIGrid::methods[] = {
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS},
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
|
@ -840,184 +902,6 @@ PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, P
|
|||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {
|
||||
auto list = self->data.get();
|
||||
if (!list) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += list->size();
|
||||
|
||||
// Bounds check
|
||||
if (index >= list->size()) {
|
||||
PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get iterator to the target position
|
||||
auto it = list->begin();
|
||||
std::advance(it, index);
|
||||
|
||||
// Handle deletion
|
||||
if (value == NULL) {
|
||||
// Clear grid reference from the entity being removed
|
||||
(*it)->grid = nullptr;
|
||||
list->erase(it);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Type checking - must be an Entity
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
||||
if (!entity->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clear grid reference from the old entity
|
||||
(*it)->grid = nullptr;
|
||||
|
||||
// Replace the element and set grid reference
|
||||
*it = entity->data;
|
||||
entity->data->grid = self->grid;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) {
|
||||
auto list = self->data.get();
|
||||
if (!list) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Type checking - must be an Entity
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
||||
// Not an Entity, so it can't be in the collection
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
||||
if (!entity->data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Search for the object by comparing C++ pointers
|
||||
for (const auto& ent : *list) {
|
||||
if (ent.get() == entity->data.get()) {
|
||||
return 1; // Found
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Not found
|
||||
}
|
||||
|
||||
PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) {
|
||||
// Create a new Python list containing elements from both collections
|
||||
if (!PySequence_Check(other)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_ssize_t self_len = self->data->size();
|
||||
Py_ssize_t other_len = PySequence_Length(other);
|
||||
if (other_len == -1) {
|
||||
return NULL; // Error already set
|
||||
}
|
||||
|
||||
PyObject* result_list = PyList_New(self_len + other_len);
|
||||
if (!result_list) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add all elements from self
|
||||
Py_ssize_t idx = 0;
|
||||
for (const auto& entity : *self->data) {
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||
if (obj) {
|
||||
obj->data = entity;
|
||||
PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference
|
||||
} else {
|
||||
Py_DECREF(result_list);
|
||||
Py_DECREF(type);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(type);
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Add all elements from other
|
||||
for (Py_ssize_t i = 0; i < other_len; i++) {
|
||||
PyObject* item = PySequence_GetItem(other, i);
|
||||
if (!item) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference
|
||||
}
|
||||
|
||||
return result_list;
|
||||
}
|
||||
|
||||
PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) {
|
||||
if (!PySequence_Check(other)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// First, validate ALL items in the sequence before modifying anything
|
||||
Py_ssize_t other_len = PySequence_Length(other);
|
||||
if (other_len == -1) {
|
||||
return NULL; // Error already set
|
||||
}
|
||||
|
||||
// Validate all items first
|
||||
for (Py_ssize_t i = 0; i < other_len; i++) {
|
||||
PyObject* item = PySequence_GetItem(other, i);
|
||||
if (!item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type check
|
||||
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
||||
Py_DECREF(item);
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"EntityCollection can only contain Entity objects; "
|
||||
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(item);
|
||||
}
|
||||
|
||||
// All items validated, now we can safely add them
|
||||
for (Py_ssize_t i = 0; i < other_len; i++) {
|
||||
PyObject* item = PySequence_GetItem(other, i);
|
||||
if (!item) {
|
||||
return NULL; // Shouldn't happen, but be safe
|
||||
}
|
||||
|
||||
// Use the existing append method which handles grid references
|
||||
PyObject* result = append(self, item);
|
||||
Py_DECREF(item);
|
||||
|
||||
if (!result) {
|
||||
return NULL; // append() failed
|
||||
}
|
||||
Py_DECREF(result); // append returns Py_None
|
||||
}
|
||||
|
||||
Py_INCREF(self);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
PySequenceMethods UIEntityCollection::sqmethods = {
|
||||
.sq_length = (lenfunc)UIEntityCollection::len,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public:
|
|||
static PyObject* get_float_member(PyUIGridObject* self, void* closure);
|
||||
static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_texture(PyUIGridObject* self, void* closure);
|
||||
static PyObject* py_at(PyUIGridObject* self, PyObject* o);
|
||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* get_children(PyUIGridObject* self, void* closure);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue