diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 849409f..61a8151 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -10,11 +10,10 @@ static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) Py_RETURN_NONE; PyObject* ptr = self->data->click_callable->borrow(); - if (ptr && ptr != Py_None) { - Py_INCREF(ptr); // Return new reference, not borrowed + if (ptr && ptr != Py_None) return ptr; - } - Py_RETURN_NONE; + else + Py_RETURN_NONE; } // Click property setter diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 7c1bbc3..c4cef36 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -6,9 +6,6 @@ #include "UIGrid.h" #include "McRFPy_Automation.h" // #111 - For simulated mouse position #include "PythonObjectCache.h" // #184 - For subclass callback support -#include "McRFPy_API.h" // For Vector type access -#include "PyMouseButton.h" // For MouseButton enum -#include "PyInputState.h" // For InputState enum #include #include @@ -18,7 +15,6 @@ // Try to call a Python method on a UIDrawable subclass // Returns true if a method was found and called, false otherwise -// Signature matches property callbacks: (Vector, MouseButton, InputState) static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name, sf::Vector2f mousepos, const char* button, const char* action) { if (!drawable->is_python_subclass) return false; @@ -49,69 +45,14 @@ static bool tryCallPythonMethod(UIDrawable* drawable, const char* method_name, return false; } - // Get the method + // Get and call the method PyObject* method = PyObject_GetAttrString(pyObj, method_name); bool called = false; if (method && PyCallable_Check(method) && method != Py_None) { - // Create Vector object for position (matches property callback signature) - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) { - PyErr_Print(); - PyErr_Clear(); - Py_XDECREF(method); - Py_DECREF(pyObj); - return false; - } - PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y); - Py_DECREF(vector_type); - if (!pos) { - PyErr_Print(); - PyErr_Clear(); - Py_XDECREF(method); - Py_DECREF(pyObj); - return false; - } - - // Convert button string to MouseButton enum - int button_val = 0; - if (strcmp(button, "left") == 0) button_val = 0; - else if (strcmp(button, "right") == 0) button_val = 1; - else if (strcmp(button, "middle") == 0) button_val = 2; - else if (strcmp(button, "x1") == 0) button_val = 3; - else if (strcmp(button, "x2") == 0) button_val = 4; - // For hover events, button might be "enter", "exit", "move" - use LEFT as default - - PyObject* button_enum = nullptr; - if (PyMouseButton::mouse_button_enum_class) { - button_enum = PyObject_CallFunction(PyMouseButton::mouse_button_enum_class, "i", button_val); - } - if (!button_enum) { - PyErr_Clear(); - button_enum = PyLong_FromLong(button_val); // Fallback to int - } - - // Convert action string to InputState enum - int action_val = (strcmp(action, "start") == 0) ? 0 : 1; // PRESSED=0, RELEASED=1 - - PyObject* action_enum = nullptr; - if (PyInputState::input_state_enum_class) { - action_enum = PyObject_CallFunction(PyInputState::input_state_enum_class, "i", action_val); - } - if (!action_enum) { - PyErr_Clear(); - action_enum = PyLong_FromLong(action_val); // Fallback to int - } - - // Call with (Vector, MouseButton, InputState) signature - PyObject* args = Py_BuildValue("(OOO)", pos, button_enum, action_enum); - Py_DECREF(pos); - Py_DECREF(button_enum); - Py_DECREF(action_enum); - - PyObject* result = PyObject_Call(method, args, NULL); - Py_DECREF(args); - + // Call with (x, y, button, action) signature + PyObject* result = PyObject_CallFunction(method, "ffss", + mousepos.x, mousepos.y, button, action); if (result) { Py_DECREF(result); called = true; diff --git a/src/UIArc.h b/src/UIArc.h index 74019cb..888b8ab 100644 --- a/src/UIArc.h +++ b/src/UIArc.h @@ -118,21 +118,14 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_dealloc = (destructor)[](PyObject* self) { PyUIArcObject* obj = (PyUIArcObject*)self; - PyObject_GC_UnTrack(self); if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)UIArc::repr, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR( "Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)\n\n" "An arc UI element for drawing curved line segments.\n\n" @@ -169,38 +162,6 @@ namespace mcrfpydef { " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override\n" ), - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUIArcObject* obj = (PyUIArcObject*)self; - if (obj->data) { - if (obj->data->click_callable) { - PyObject* cb = obj->data->click_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_enter_callable) { - PyObject* cb = obj->data->on_enter_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_exit_callable) { - PyObject* cb = obj->data->on_exit_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_move_callable) { - PyObject* cb = obj->data->on_move_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - } - return 0; - }, - .tp_clear = [](PyObject* self) -> int { - PyUIArcObject* obj = (PyUIArcObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UIArc_methods, .tp_getset = UIArc::getsetters, .tp_base = &mcrfpydef::PyDrawableType, diff --git a/src/UICaption.h b/src/UICaption.h index b66c847..da4fe5c 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -59,21 +59,13 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUICaptionObject* obj = (PyUICaptionObject*)self; - // Untrack from GC before destroying - PyObject_GC_UnTrack(self); // Clear weak references if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - // Clear Python references to break cycles - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - // Release reference to font object - Py_CLEAR(obj->font); + // TODO - reevaluate with PyFont usage; UICaption does not own the font + // release reference to font object + if (obj->font) Py_DECREF(obj->font); obj->data.reset(); Py_TYPE(self)->tp_free(self); }, @@ -81,7 +73,7 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n" "A text display UI element with customizable font and styling.\n\n" "Args:\n" @@ -122,42 +114,6 @@ namespace mcrfpydef { " margin (float): General margin for alignment\n" " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override"), - // tp_traverse visits Python object references for GC cycle detection - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUICaptionObject* obj = (PyUICaptionObject*)self; - Py_VISIT(obj->font); - if (obj->data) { - if (obj->data->click_callable) { - PyObject* callback = obj->data->click_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_enter_callable) { - PyObject* callback = obj->data->on_enter_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_exit_callable) { - PyObject* callback = obj->data->on_exit_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_move_callable) { - PyObject* callback = obj->data->on_move_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - } - return 0; - }, - // tp_clear breaks reference cycles by clearing Python references - .tp_clear = [](PyObject* self) -> int { - PyUICaptionObject* obj = (PyUICaptionObject*)self; - Py_CLEAR(obj->font); - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UICaption_methods, //.tp_members = PyUIFrame_members, .tp_getset = UICaption::getsetters, diff --git a/src/UICircle.h b/src/UICircle.h index 966b6f3..5928808 100644 --- a/src/UICircle.h +++ b/src/UICircle.h @@ -107,21 +107,14 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_dealloc = (destructor)[](PyObject* self) { PyUICircleObject* obj = (PyUICircleObject*)self; - PyObject_GC_UnTrack(self); if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)UICircle::repr, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR( "Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)\n\n" "A circle UI element for drawing filled or outlined circles.\n\n" @@ -156,38 +149,6 @@ namespace mcrfpydef { " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override\n" ), - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUICircleObject* obj = (PyUICircleObject*)self; - if (obj->data) { - if (obj->data->click_callable) { - PyObject* cb = obj->data->click_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_enter_callable) { - PyObject* cb = obj->data->on_enter_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_exit_callable) { - PyObject* cb = obj->data->on_exit_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_move_callable) { - PyObject* cb = obj->data->on_move_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - } - return 0; - }, - .tp_clear = [](PyObject* self) -> int { - PyUICircleObject* obj = (PyUICircleObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UICircle_methods, .tp_getset = UICircle::getsetters, .tp_base = &mcrfpydef::PyDrawableType, diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index a3aff1a..a1fe1cd 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -259,11 +259,10 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) { PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click"); return NULL; } - if (ptr && ptr != Py_None) { - Py_INCREF(ptr); // Return new reference, not borrowed + if (ptr && ptr != Py_None) return ptr; - } - Py_RETURN_NONE; + else + return Py_None; } int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) { @@ -1616,11 +1615,10 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) { PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter"); return NULL; } - if (ptr && ptr != Py_None) { - Py_INCREF(ptr); // Return new reference, not borrowed + if (ptr && ptr != Py_None) return ptr; - } - Py_RETURN_NONE; + else + Py_RETURN_NONE; } int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) { @@ -1700,11 +1698,10 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) { PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit"); return NULL; } - if (ptr && ptr != Py_None) { - Py_INCREF(ptr); // Return new reference, not borrowed + if (ptr && ptr != Py_None) return ptr; - } - Py_RETURN_NONE; + else + Py_RETURN_NONE; } int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) { @@ -1793,11 +1790,10 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) { PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move"); return NULL; } - if (ptr && ptr != Py_None) { - Py_INCREF(ptr); // Return new reference, not borrowed + if (ptr && ptr != Py_None) return ptr; - } - Py_RETURN_NONE; + else + Py_RETURN_NONE; } int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) { diff --git a/src/UIFrame.h b/src/UIFrame.h index 1e753b4..75e5d48 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -84,19 +84,10 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUIFrameObject* obj = (PyUIFrameObject*)self; - // Untrack from GC before destroying - PyObject_GC_UnTrack(self); // Clear weak references if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - // Clear Python references to break cycles - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, @@ -104,7 +95,7 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n" "A rectangular frame UI element that can contain other drawable elements.\n\n" "Args:\n" @@ -148,49 +139,6 @@ namespace mcrfpydef { " margin (float): General margin for alignment\n" " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override"), - // tp_traverse visits Python object references for GC cycle detection - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUIFrameObject* obj = (PyUIFrameObject*)self; - if (obj->data) { - // Visit callback references - if (obj->data->click_callable) { - PyObject* callback = obj->data->click_callable->borrow(); - if (callback && callback != Py_None) { - Py_VISIT(callback); - } - } - if (obj->data->on_enter_callable) { - PyObject* callback = obj->data->on_enter_callable->borrow(); - if (callback && callback != Py_None) { - Py_VISIT(callback); - } - } - if (obj->data->on_exit_callable) { - PyObject* callback = obj->data->on_exit_callable->borrow(); - if (callback && callback != Py_None) { - Py_VISIT(callback); - } - } - if (obj->data->on_move_callable) { - PyObject* callback = obj->data->on_move_callable->borrow(); - if (callback && callback != Py_None) { - Py_VISIT(callback); - } - } - } - return 0; - }, - // tp_clear breaks reference cycles by clearing Python references - .tp_clear = [](PyObject* self) -> int { - PyUIFrameObject* obj = (PyUIFrameObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UIFrame::getsetters, @@ -202,9 +150,6 @@ namespace mcrfpydef { if (self) { self->data = std::make_shared(); self->weakreflist = nullptr; - // Note: For GC types, tracking happens automatically via tp_alloc - // when Py_TPFLAGS_HAVE_GC is set. Do NOT call PyObject_GC_Track here - // as it would double-track and cause corruption. } return (PyObject*)self; } diff --git a/src/UIGrid.h b/src/UIGrid.h index 18b367d..e2ab942 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -235,29 +235,16 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUIGridObject* obj = (PyUIGridObject*)self; - // Untrack from GC before destroying - PyObject_GC_UnTrack(self); // Clear weak references if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - // Clear Python references to break cycles - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - // Grid-specific cell callbacks - obj->data->on_cell_enter_callable.reset(); - obj->data->on_cell_exit_callable.reset(); - obj->data->on_cell_click_callable.reset(); - } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)UIGrid::repr, .tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n" "A grid-based UI element for tile-based rendering and entity management.\n\n" "Args:\n" @@ -309,57 +296,6 @@ namespace mcrfpydef { " margin (float): General margin for alignment\n" " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override"), - // tp_traverse visits Python object references for GC cycle detection - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUIGridObject* obj = (PyUIGridObject*)self; - if (obj->data) { - // Base class callbacks - if (obj->data->click_callable) { - PyObject* callback = obj->data->click_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_enter_callable) { - PyObject* callback = obj->data->on_enter_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_exit_callable) { - PyObject* callback = obj->data->on_exit_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_move_callable) { - PyObject* callback = obj->data->on_move_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - // Grid-specific cell callbacks - if (obj->data->on_cell_enter_callable) { - PyObject* callback = obj->data->on_cell_enter_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_cell_exit_callable) { - PyObject* callback = obj->data->on_cell_exit_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_cell_click_callable) { - PyObject* callback = obj->data->on_cell_click_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - } - return 0; - }, - // tp_clear breaks reference cycles by clearing Python references - .tp_clear = [](PyObject* self) -> int { - PyUIGridObject* obj = (PyUIGridObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - obj->data->on_cell_enter_callable.reset(); - obj->data->on_cell_exit_callable.reset(); - obj->data->on_cell_click_callable.reset(); - } - return 0; - }, .tp_methods = UIGrid_all_methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, diff --git a/src/UILine.h b/src/UILine.h index af85b6c..2912f93 100644 --- a/src/UILine.h +++ b/src/UILine.h @@ -104,21 +104,14 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_dealloc = (destructor)[](PyObject* self) { PyUILineObject* obj = (PyUILineObject*)self; - PyObject_GC_UnTrack(self); if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)UILine::repr, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR( "Line(start=None, end=None, thickness=1.0, color=None, **kwargs)\n\n" "A line UI element for drawing straight lines between two points.\n\n" @@ -151,38 +144,6 @@ namespace mcrfpydef { " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override\n" ), - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUILineObject* obj = (PyUILineObject*)self; - if (obj->data) { - if (obj->data->click_callable) { - PyObject* cb = obj->data->click_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_enter_callable) { - PyObject* cb = obj->data->on_enter_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_exit_callable) { - PyObject* cb = obj->data->on_exit_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - if (obj->data->on_move_callable) { - PyObject* cb = obj->data->on_move_callable->borrow(); - if (cb && cb != Py_None) Py_VISIT(cb); - } - } - return 0; - }, - .tp_clear = [](PyObject* self) -> int { - PyUILineObject* obj = (PyUILineObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UILine_methods, .tp_getset = UILine::getsetters, .tp_base = &mcrfpydef::PyDrawableType, diff --git a/src/UISprite.h b/src/UISprite.h index f409a23..d3ddb12 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -91,19 +91,12 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUISpriteObject* obj = (PyUISpriteObject*)self; - // Untrack from GC before destroying - PyObject_GC_UnTrack(self); // Clear weak references if (obj->weakreflist != NULL) { PyObject_ClearWeakRefs(self); } - // Clear Python references to break cycles - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } + // release reference to font object + //if (obj->texture) Py_DECREF(obj->texture); obj->data.reset(); Py_TYPE(self)->tp_free(self); }, @@ -111,7 +104,7 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n" "A sprite UI element that displays a texture or portion of a texture atlas.\n\n" "Args:\n" @@ -150,40 +143,6 @@ namespace mcrfpydef { " margin (float): General margin for alignment\n" " horiz_margin (float): Horizontal margin override\n" " vert_margin (float): Vertical margin override"), - // tp_traverse visits Python object references for GC cycle detection - .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { - PyUISpriteObject* obj = (PyUISpriteObject*)self; - if (obj->data) { - if (obj->data->click_callable) { - PyObject* callback = obj->data->click_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_enter_callable) { - PyObject* callback = obj->data->on_enter_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_exit_callable) { - PyObject* callback = obj->data->on_exit_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - if (obj->data->on_move_callable) { - PyObject* callback = obj->data->on_move_callable->borrow(); - if (callback && callback != Py_None) Py_VISIT(callback); - } - } - return 0; - }, - // tp_clear breaks reference cycles by clearing Python references - .tp_clear = [](PyObject* self) -> int { - PyUISpriteObject* obj = (PyUISpriteObject*)self; - if (obj->data) { - obj->data->click_unregister(); - obj->data->on_enter_unregister(); - obj->data->on_exit_unregister(); - obj->data->on_move_unregister(); - } - return 0; - }, .tp_methods = UISprite_methods, //.tp_members = PyUIFrame_members, .tp_getset = UISprite::getsetters, diff --git a/tests/regression/issue_callback_refcount_test.py b/tests/regression/issue_callback_refcount_test.py deleted file mode 100644 index da4b563..0000000 --- a/tests/regression/issue_callback_refcount_test.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -"""Test for callback property reference counting fix. - -This test verifies that accessing callback properties (on_click, on_enter, etc.) -returns correctly reference-counted objects, preventing use-after-free bugs. - -The bug: Callback getters were returning borrowed references instead of new -references, causing objects to be freed prematurely when Python DECREFs them. -""" -import mcrfpy -import sys -import gc - -def test_callback_refcount(): - """Test that callback getters return new references.""" - errors = [] - - # Create a scene - scene = mcrfpy.Scene("test_callback_refcount") - - # Test Frame - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - - # Set a callback - def my_callback(pos, button, action): - pass - - frame.on_click = my_callback - - # Read the callback back multiple times - # If borrowing incorrectly, this could cause use-after-free - for i in range(10): - cb = frame.on_click - if cb is None: - errors.append(f"on_click returned None on iteration {i}") - break - if not callable(cb): - errors.append(f"on_click returned non-callable on iteration {i}: {type(cb)}") - break - # Explicitly delete to trigger any refcount issues - del cb - gc.collect() - - # Final check - should still return the callback - final_cb = frame.on_click - if final_cb is None: - errors.append("on_click returned None after repeated access") - elif not callable(final_cb): - errors.append(f"on_click returned non-callable after repeated access: {type(final_cb)}") - - # Test on_enter, on_exit, on_move - frame.on_enter = lambda pos, button, action: None - frame.on_exit = lambda pos, button, action: None - frame.on_move = lambda pos, button, action: None - - for name in ['on_enter', 'on_exit', 'on_move']: - for i in range(5): - cb = getattr(frame, name) - if cb is None: - errors.append(f"{name} returned None on iteration {i}") - break - del cb - gc.collect() - - return errors - - -def test_grid_cell_callbacks(): - """Test Grid cell callback getters (these were already correct).""" - errors = [] - - grid = mcrfpy.Grid(pos=(0, 0), size=(100, 100), grid_size=(5, 5), - texture=mcrfpy.default_texture, zoom=1.0) - - grid.on_cell_enter = lambda pos: None - grid.on_cell_exit = lambda pos: None - grid.on_cell_click = lambda pos: None - - for name in ['on_cell_enter', 'on_cell_exit', 'on_cell_click']: - for i in range(5): - cb = getattr(grid, name) - if cb is None: - errors.append(f"{name} returned None on iteration {i}") - break - del cb - gc.collect() - - return errors - - -def test_subclass_callback(): - """Test callback access on Python subclasses.""" - errors = [] - - class MyFrame(mcrfpy.Frame): - pass - - obj = MyFrame(pos=(0, 0), size=(100, 100)) - - # Set callback via property - obj.on_click = lambda pos, button, action: print("clicked") - - # Read back multiple times - for i in range(5): - cb = obj.on_click - if cb is None: - errors.append(f"Subclass on_click returned None on iteration {i}") - break - if not callable(cb): - errors.append(f"Subclass on_click returned non-callable: {type(cb)}") - break - del cb - gc.collect() - - return errors - - -def run_tests(): - """Run all callback refcount tests.""" - all_errors = [] - - print("Testing callback property refcount...") - errors = test_callback_refcount() - if errors: - all_errors.extend(errors) - print(f" FAIL: {len(errors)} errors") - else: - print(" PASS: on_click, on_enter, on_exit, on_move") - - print("Testing Grid cell callbacks...") - errors = test_grid_cell_callbacks() - if errors: - all_errors.extend(errors) - print(f" FAIL: {len(errors)} errors") - else: - print(" PASS: on_cell_enter, on_cell_exit, on_cell_click") - - print("Testing subclass callbacks...") - errors = test_subclass_callback() - if errors: - all_errors.extend(errors) - print(f" FAIL: {len(errors)} errors") - else: - print(" PASS: MyFrame(Frame) subclass") - - if all_errors: - print(f"\nFAILED with {len(all_errors)} errors:") - for e in all_errors: - print(f" - {e}") - sys.exit(1) - else: - print("\nAll callback refcount tests PASSED") - sys.exit(0) - - -if __name__ == "__main__": - run_tests() diff --git a/tests/regression/subclass_callback_segfault_test.py b/tests/regression/subclass_callback_segfault_test.py deleted file mode 100644 index 19bed76..0000000 --- a/tests/regression/subclass_callback_segfault_test.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -"""Minimal reproduction of segfault when calling subclass method callback. - -The issue: When a Frame subclass assigns self.on_click = self._on_click, -reading it back works but there's a segfault during cleanup. -""" -import mcrfpy -import sys -import gc - -class MyFrame(mcrfpy.Frame): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.on_click = self._on_click - - def _on_click(self, pos, button, action): - print(f"Clicked at {pos}, button={button}, action={action}") - - -def test_minimal(): - """Minimal test case.""" - print("Creating MyFrame...") - obj = MyFrame(pos=(100, 100), size=(100, 100)) - - print(f"Reading on_click: {obj.on_click}") - print(f"Type: {type(obj.on_click)}") - - print("Attempting to call on_click...") - try: - obj.on_click((50, 50), "left", "start") - print("Call succeeded!") - except Exception as e: - print(f"Exception: {type(e).__name__}: {e}") - - print("Clearing callback...") - obj.on_click = None - - print("Deleting object...") - del obj - - print("Running GC...") - gc.collect() - - print("About to exit...") - sys.exit(0) - - -def test_without_callback_clear(): - """Test without clearing callback first.""" - print("Creating MyFrame...") - obj = MyFrame(pos=(100, 100), size=(100, 100)) - - print("Calling...") - obj.on_click((50, 50), "left", "start") - - print("Deleting without clearing callback...") - del obj - gc.collect() - - print("About to exit...") - sys.exit(0) - - -def test_added_to_scene(): - """Test when added to scene.""" - print("Creating scene and MyFrame...") - scene = mcrfpy.Scene("test") - obj = MyFrame(pos=(100, 100), size=(100, 100)) - scene.children.append(obj) - - print("Calling via scene.children[0]...") - scene.children[0].on_click((50, 50), "left", "start") - - print("About to exit...") - sys.exit(0) - - -if __name__ == "__main__": - # Try different scenarios - import sys - if len(sys.argv) > 1: - if sys.argv[1] == "2": - test_without_callback_clear() - elif sys.argv[1] == "3": - test_added_to_scene() - else: - test_minimal() diff --git a/tests/unit/test_uidrawable_monkeypatch.py b/tests/unit/test_uidrawable_monkeypatch.py index 6bdb540..f9eae08 100644 --- a/tests/unit/test_uidrawable_monkeypatch.py +++ b/tests/unit/test_uidrawable_monkeypatch.py @@ -5,9 +5,6 @@ Test monkey-patching support for UIDrawable subclass callbacks (#184) This tests that users can dynamically add callback methods at runtime (monkey-patching) and have them work correctly with the callback cache invalidation system. - -Callback signature: (pos: Vector, button: MouseButton, action: InputState) -This matches property callbacks for consistency. """ import mcrfpy import sys @@ -24,12 +21,6 @@ def test_failed(name, error): print(f" FAIL: {name}: {error}") -# Helper to create typed callback arguments -def make_click_args(x=0.0, y=0.0): - """Create properly typed callback arguments for testing.""" - return (mcrfpy.Vector(x, y), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED) - - # ============================================================================== # Test Classes # ============================================================================== @@ -47,8 +38,8 @@ class PartialFrame(mcrfpy.Frame): super().__init__(*args, **kwargs) self.call_log = [] - def on_click(self, pos, button, action): - self.call_log.append(('click', pos.x, pos.y)) + def on_click(self, x, y, button, action): + self.call_log.append(('click', x, y)) # ============================================================================== @@ -68,8 +59,8 @@ try: "EmptyFrame should not have on_click in its own __dict__ initially" # Add on_click method to class dynamically - def dynamic_on_click(self, pos, button, action): - self.call_log.append(('dynamic_click', pos.x, pos.y)) + def dynamic_on_click(self, x, y, button, action): + self.call_log.append(('dynamic_click', x, y)) EmptyFrame.on_click = dynamic_on_click @@ -77,13 +68,13 @@ try: assert 'on_click' in EmptyFrame.__dict__, "EmptyFrame should now have on_click in __dict__" # Test calling the method directly - frame1.on_click(*make_click_args(10.0, 20.0)) + frame1.on_click(10.0, 20.0, "left", "start") assert ('dynamic_click', 10.0, 20.0) in frame1.call_log, \ f"Dynamic method should have been called, log: {frame1.call_log}" # Create new instance - should also have the method frame2 = EmptyFrame(pos=(0, 0), size=(100, 100)) - frame2.on_click(*make_click_args(30.0, 40.0)) + frame2.on_click(30.0, 40.0, "left", "start") assert ('dynamic_click', 30.0, 40.0) in frame2.call_log, \ f"New instance should have dynamic method, log: {frame2.call_log}" @@ -96,20 +87,20 @@ try: frame = PartialFrame(pos=(0, 0), size=(100, 100)) # Call original method - frame.on_click(*make_click_args(1.0, 2.0)) + frame.on_click(1.0, 2.0, "left", "start") assert ('click', 1.0, 2.0) in frame.call_log, \ f"Original method should work, log: {frame.call_log}" frame.call_log.clear() # Replace the method - def new_on_click(self, pos, button, action): - self.call_log.append(('replaced_click', pos.x, pos.y)) + def new_on_click(self, x, y, button, action): + self.call_log.append(('replaced_click', x, y)) PartialFrame.on_click = new_on_click # Call again - should use new method - frame.on_click(*make_click_args(3.0, 4.0)) + frame.on_click(3.0, 4.0, "left", "start") assert ('replaced_click', 3.0, 4.0) in frame.call_log, \ f"Replaced method should work, log: {frame.call_log}" @@ -128,17 +119,20 @@ try: frame_a = FreshFrame(pos=(0, 0), size=(100, 100)) frame_b = FreshFrame(pos=(0, 0), size=(100, 100)) - # Add method to instance only (property callback style - no self) - def instance_on_click(pos, button, action): - frame_a.instance_log.append(('instance_click', pos.x, pos.y)) + # Add method to instance only + def instance_on_click(x, y, button, action): + frame_a.instance_log.append(('instance_click', x, y)) frame_a.on_click = instance_on_click # frame_a should have the method assert hasattr(frame_a, 'on_click'), "frame_a should have on_click" + # frame_b should NOT have the method (unless inherited from class) + # Actually, both will have on_click now since it's an instance attribute + # Verify instance method works - frame_a.on_click(*make_click_args(5.0, 6.0)) + frame_a.on_click(5.0, 6.0, "left", "start") assert ('instance_click', 5.0, 6.0) in frame_a.instance_log, \ f"Instance method should work, log: {frame_a.instance_log}" @@ -156,7 +150,7 @@ try: initial_gen = getattr(TrackedFrame, '_mcrf_callback_gen', 0) # Add a callback method - def tracked_on_enter(self, pos, button, action): + def tracked_on_enter(self, x, y, button, action): pass TrackedFrame.on_enter = tracked_on_enter @@ -180,30 +174,30 @@ try: frame = MultiCallbackFrame(pos=(0, 0), size=(100, 100)) # Add on_click - def multi_on_click(self, pos, button, action): + def multi_on_click(self, x, y, button, action): self.events.append('click') MultiCallbackFrame.on_click = multi_on_click # Add on_enter - def multi_on_enter(self, pos, button, action): + def multi_on_enter(self, x, y, button, action): self.events.append('enter') MultiCallbackFrame.on_enter = multi_on_enter # Add on_exit - def multi_on_exit(self, pos, button, action): + def multi_on_exit(self, x, y, button, action): self.events.append('exit') MultiCallbackFrame.on_exit = multi_on_exit # Add on_move - def multi_on_move(self, pos, button, action): + def multi_on_move(self, x, y, button, action): self.events.append('move') MultiCallbackFrame.on_move = multi_on_move # Call all methods - frame.on_click(*make_click_args()) - frame.on_enter(*make_click_args()) - frame.on_exit(*make_click_args()) - frame.on_move(*make_click_args()) + frame.on_click(0, 0, "left", "start") + frame.on_enter(0, 0, "enter", "start") + frame.on_exit(0, 0, "exit", "start") + frame.on_move(0, 0, "move", "start") assert frame.events == ['click', 'enter', 'exit', 'move'], \ f"All callbacks should fire, got: {frame.events}" @@ -219,7 +213,7 @@ try: super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True frame = DeletableFrame(pos=(0, 0), size=(100, 100)) @@ -228,7 +222,7 @@ try: assert 'on_click' in DeletableFrame.__dict__, "Should have on_click in __dict__ initially" # Call it - frame.on_click(*make_click_args()) + frame.on_click(0, 0, "left", "start") assert frame.clicked, "Method should work" # Delete the method from subclass @@ -252,13 +246,13 @@ try: self.method_called = False self.property_called = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.method_called = True frame = MixedFrame(pos=(0, 0), size=(100, 100)) - # Set property callback (no self parameter) - def prop_callback(pos, button, action): + # Set property callback + def prop_callback(x, y, button, action): frame.property_called = True frame.click = prop_callback @@ -270,11 +264,11 @@ try: assert hasattr(frame, 'on_click'), "on_click method should exist" # Can call method directly - frame.on_click(*make_click_args()) + frame.on_click(0, 0, "left", "start") assert frame.method_called, "Method should be callable directly" # Can call property callback - frame.click(*make_click_args()) + frame.click(0, 0, "left", "start") assert frame.property_called, "Property callback should be callable" test_passed("Property callback and method coexist") @@ -288,16 +282,16 @@ try: super().__init__(*args, **kwargs) self.clicks = [] - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicks.append('base') class DerivedClickable(BaseClickable): - def on_click(self, pos, button, action): - super().on_click(pos, button, action) + def on_click(self, x, y, button, action): + super().on_click(x, y, button, action) self.clicks.append('derived') frame = DerivedClickable(pos=(0, 0), size=(100, 100)) - frame.on_click(*make_click_args()) + frame.on_click(0, 0, "left", "start") assert frame.clicks == ['base', 'derived'], \ f"Inheritance chain should work, got: {frame.clicks}" diff --git a/tests/unit/test_uidrawable_subclass_callbacks.py b/tests/unit/test_uidrawable_subclass_callbacks.py index c4b29e4..05e0b15 100644 --- a/tests/unit/test_uidrawable_subclass_callbacks.py +++ b/tests/unit/test_uidrawable_subclass_callbacks.py @@ -5,9 +5,6 @@ Test UIDrawable subclass callback methods (#184) This tests the ability to define callback methods (on_click, on_enter, on_exit, on_move) directly in Python subclasses of UIDrawable types (Frame, Caption, Sprite, Grid, Line, Circle, Arc). - -Callback signature: (pos: Vector, button: MouseButton, action: InputState) -This matches property callbacks for consistency. """ import mcrfpy import sys @@ -34,9 +31,9 @@ class ClickableFrame(mcrfpy.Frame): self.click_count = 0 self.last_click_args = None - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.click_count += 1 - self.last_click_args = (pos, button, action) + self.last_click_args = (x, y, button, action) # ============================================================================== @@ -48,14 +45,14 @@ class HoverFrame(mcrfpy.Frame): super().__init__(*args, **kwargs) self.events = [] - def on_enter(self, pos, button, action): - self.events.append(('enter', pos.x, pos.y)) + def on_enter(self, x, y, button, action): + self.events.append(('enter', x, y)) - def on_exit(self, pos, button, action): - self.events.append(('exit', pos.x, pos.y)) + def on_exit(self, x, y, button, action): + self.events.append(('exit', x, y)) - def on_move(self, pos, button, action): - self.events.append(('move', pos.x, pos.y)) + def on_move(self, x, y, button, action): + self.events.append(('move', x, y)) # ============================================================================== @@ -67,7 +64,7 @@ class ClickableCaption(mcrfpy.Caption): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -80,7 +77,7 @@ class ClickableSprite(mcrfpy.Sprite): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -93,7 +90,7 @@ class ClickableGrid(mcrfpy.Grid): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -106,7 +103,7 @@ class ClickableCircle(mcrfpy.Circle): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -119,7 +116,7 @@ class ClickableLine(mcrfpy.Line): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -132,7 +129,7 @@ class ClickableArc(mcrfpy.Arc): super().__init__(*args, **kwargs) self.clicked = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.clicked = True @@ -146,7 +143,7 @@ class FrameWithBoth(mcrfpy.Frame): self.method_called = False self.property_called = False - def on_click(self, pos, button, action): + def on_click(self, x, y, button, action): self.method_called = True @@ -243,32 +240,26 @@ try: except Exception as e: test_failed("Base types are NOT marked as subclasses", e) -# Test 10: Verify subclass methods are callable with typed arguments +# Test 10: Verify subclass methods are callable try: frame = ClickableFrame(pos=(100, 100), size=(100, 100)) # Verify method exists and is callable assert hasattr(frame, 'on_click'), "ClickableFrame should have on_click method" assert callable(frame.on_click), "on_click should be callable" - # Manually call with proper typed objects to verify it works - pos = mcrfpy.Vector(50.0, 50.0) - button = mcrfpy.MouseButton.LEFT - action = mcrfpy.InputState.PRESSED - frame.on_click(pos, button, action) + # Manually call to verify it works + frame.on_click(50.0, 50.0, "left", "start") assert frame.click_count == 1, f"click_count should be 1, got {frame.click_count}" - assert frame.last_click_args[0].x == 50.0, f"pos.x mismatch: {frame.last_click_args[0].x}" - assert frame.last_click_args[0].y == 50.0, f"pos.y mismatch: {frame.last_click_args[0].y}" - assert frame.last_click_args[1] == mcrfpy.MouseButton.LEFT, f"button mismatch: {frame.last_click_args[1]}" - assert frame.last_click_args[2] == mcrfpy.InputState.PRESSED, f"action mismatch: {frame.last_click_args[2]}" + assert frame.last_click_args == (50.0, 50.0, "left", "start"), f"last_click_args mismatch: {frame.last_click_args}" test_passed("Subclass methods are callable and work") except Exception as e: test_failed("Subclass methods are callable and work", e) -# Test 11: Verify HoverFrame methods work with typed arguments +# Test 11: Verify HoverFrame methods work try: hover = HoverFrame(pos=(250, 100), size=(100, 100)) - hover.on_enter(mcrfpy.Vector(10.0, 20.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED) - hover.on_exit(mcrfpy.Vector(30.0, 40.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED) - hover.on_move(mcrfpy.Vector(50.0, 60.0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED) + hover.on_enter(10.0, 20.0, "enter", "start") + hover.on_exit(30.0, 40.0, "exit", "start") + hover.on_move(50.0, 60.0, "move", "start") assert len(hover.events) == 3, f"Should have 3 events, got {len(hover.events)}" assert hover.events[0] == ('enter', 10.0, 20.0), f"Event mismatch: {hover.events[0]}" assert hover.events[1] == ('exit', 30.0, 40.0), f"Event mismatch: {hover.events[1]}" @@ -281,7 +272,7 @@ except Exception as e: try: both = FrameWithBoth(pos=(400, 250), size=(100, 100)) property_was_called = [False] - def property_callback(pos, btn, action): + def property_callback(x, y, btn, action): property_was_called[0] = True both.click = property_callback # Assign to property # Property callback should be set @@ -297,7 +288,7 @@ try: frame = ClickableFrame(pos=(100, 100), size=(100, 100)) frame.custom_attr = "test_value" assert frame.custom_attr == "test_value", "Custom attribute should persist" - frame.on_click(mcrfpy.Vector(0, 0), mcrfpy.MouseButton.LEFT, mcrfpy.InputState.PRESSED) + frame.on_click(0, 0, "left", "start") assert frame.click_count == 1, "Click count should be 1" # Verify frame is still usable after attribute access assert frame.x == 100, f"Frame x should be 100, got {frame.x}"