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:
John McCardle 2025-07-05 20:35:33 -04:00
commit 5b6b0cc8ff
34 changed files with 1930 additions and 184 deletions

View file

@ -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,

View file

@ -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);