fix(bindings): route every drawable wrapper through PythonObjectCache

`.parent` allocated a brand-new Python wrapper on every read, so object
identity never held: `kid.parent is parent` was False, and so was
`kid.parent is kid.parent`. A Frame/Grid subclass reached through .parent
came back as the base type with its Python attributes gone, and `.parent`
could not be used as a dict key or set member.

The cache-aware converter already existed -- convertDrawableToPython in
UICollection.cpp -- but was file-local, so three other paths hand-rolled
their own tp_alloc switch and skipped the cache:

  - UIDrawable::get_parent      (the filed bug)
  - find_in_collection          (mcrfpy.find / find_all)
  - UIEntityCollection concat and slice __getitem__

The entity sites were the worst of them: a duplicate wrapper's tp_dealloc
unconditionally clears UIEntity::pyobject, destroying the #266 subclass
identity ref held by the *original* wrapper.

Promote the converter to UIDrawable::pyobject_for and route all sites
through it; add the matching convertEntityToPython for entities. This also
gives .parent the Line/Circle/Arc/Viewport3D arms it never had (they
previously fell through to None).

Residual gap, deliberately not closed here: the cache holds weakrefs, so a
subclass wrapper GC'd while only C++ holds the object still resurrects as
the base type. Closing that needs an owner-held strong ref (the #348
pattern) and is a lifetime design decision; filed separately.

Verified A/B: 17 of the new test's 22 checks fail pre-fix, all pass after.
Suite 330/330. Also corrects CLAUDE.md, which documented the long-deleted
RET_PY_INSTANCE macro and recommended bare tp_alloc as the "most common"
pattern -- i.e. taught this exact bug.

closes #369
This commit is contained in:
John McCardle 2026-07-13 23:37:01 -04:00
commit ce15469ffc
9 changed files with 346 additions and 324 deletions

View file

@ -609,34 +609,29 @@ echo 'import mcrfpy; print("Test"); scene = mcrfpy.Scene("test"); scene.activate
### Understanding Key Macros and Patterns ### Understanding Key Macros and Patterns
#### RET_PY_INSTANCE Macro (UIDrawable.h) #### Returning a C++ object to Python (#369)
This macro handles converting C++ UI objects to their Python equivalents:
**Never `tp_alloc` a wrapper for a C++ object that might already have one.** A fresh
wrapper breaks object identity (`x.parent is x.parent` -> False) and silently downgrades
a Python subclass to its base type. `RET_PY_INSTANCE`, which older docs described here,
no longer exists; use the cache-aware converters instead:
```cpp ```cpp
RET_PY_INSTANCE(target); // Drawables (Frame, Caption, Sprite, GridView, Line, Circle, Arc, Viewport3D)
// Expands to a switch on target->derived_type() that: PyObject* obj = UIDrawable::pyobject_for(drawable_shared_ptr); // UIDrawable.h
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
// 2. Sets the shared_ptr data member // Entities -- convertEntityToPython(), file-local to UIEntityCollection.cpp
// 3. Returns the PyObject* // GridData -- PyGridData::pyobject_for()
``` ```
Each looks the object up in `PythonObjectCache` by `serial_number` and returns the live
wrapper if one exists, only allocating (and re-registering) on a miss. `tp_alloc` is
correct **only** in a type's own `tp_init`, where the object is genuinely new.
#### Collection Patterns #### Collection Patterns
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>` - `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>` - `UIEntityCollection` wraps `std::vector<std::shared_ptr<UIEntity>>` (#329: was a
- Different containers require different iteration code (vector vs list) `std::list`; indexing is O(1) now, so don't reintroduce `std::advance`)
#### Python Object Creation Patterns
```cpp
// Pattern 1: Using tp_alloc (most common)
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
o->data = std::make_shared<UIFrame>();
// Pattern 2: Getting type from module
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
// Pattern 3: Direct shared_ptr assignment
iterObj->data = self->data; // Shares the C++ object
```
### Working Directory Structure ### Working Directory Structure
``` ```

File diff suppressed because one or more lines are too long

View file

@ -1609,52 +1609,14 @@ static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collect
// Check this element's name // Check this element's name
if (name_matches_pattern(drawable->name, pattern)) { if (name_matches_pattern(drawable->name, pattern)) {
// Convert to Python object using RET_PY_INSTANCE logic // #369: go through the cache-aware converter so find() returns the caller's
PyObject* py_obj = nullptr; // existing wrapper (and Python subclass), not a fresh identity-less one.
PyObject* py_obj = UIDrawable::pyobject_for(drawable);
switch (drawable->derived_type()) { if (py_obj == Py_None) {
case PyObjectsEnum::UIFRAME: { Py_DECREF(py_obj);
auto frame = std::static_pointer_cast<UIFrame>(drawable); py_obj = nullptr;
auto type = &mcrfpydef::PyUIFrameType; } else if (!py_obj) {
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0); PyErr_Clear(); // unknown derived type: skip, as the old switch did
if (o) {
o->data = frame;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UICAPTION: {
auto caption = std::static_pointer_cast<UICaption>(drawable);
auto type = &mcrfpydef::PyUICaptionType;
auto o = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (o) {
o->data = caption;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UISPRITE: {
auto sprite = std::static_pointer_cast<UISprite>(drawable);
auto type = &mcrfpydef::PyUISpriteType;
auto o = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (o) {
o->data = sprite;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UIGRIDVIEW: {
auto gridview = std::static_pointer_cast<UIGridView>(drawable);
auto type = &mcrfpydef::PyUIGridViewType;
auto o = (PyUIGridViewObject*)type->tp_alloc(type, 0);
if (o) {
o->data = gridview;
py_obj = (PyObject*)o;
}
break;
}
default:
break;
} }
if (py_obj) { if (py_obj) {

View file

@ -17,130 +17,10 @@
using namespace mcrfpydef; using namespace mcrfpydef;
// Local helper function to convert UIDrawable to appropriate Python object // #369: promoted to UIDrawable::pyobject_for so .parent, find(), and collection
// indexing all hand back the same wrapper for the same C++ object.
static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) { static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
if (!drawable) { return UIDrawable::pyobject_for(drawable);
Py_RETURN_NONE;
}
// Check cache first
if (drawable->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
PyTypeObject* type = nullptr;
PyObject* obj = nullptr;
switch (drawable->derived_type()) {
case PyObjectsEnum::UIFRAME:
{
type = &PyUIFrameType;
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICAPTION:
{
type = &PyUICaptionType;
auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
pyObj->font = nullptr;
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UISPRITE:
{
type = &PyUISpriteType;
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIGRIDVIEW:
{
type = &PyUIGridViewType;
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGridView>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UILINE:
{
type = &PyUILineType;
auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UILine>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICIRCLE:
{
type = &PyUICircleType;
auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICircle>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIARC:
{
type = &PyUIArcType;
auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIArc>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIVIEWPORT3D:
{
type = &PyViewport3DType;
auto pyObj = (PyViewport3DObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<mcrf::Viewport3D>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
default:
PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type");
return nullptr;
}
// Re-register in cache if the object has a serial number
// This handles the case where the original Python wrapper was GC'd
// but the C++ object persists (e.g., inline-created objects added to collections)
if (obj && drawable->serial_number != 0) {
PyObject* weakref = PyWeakref_NewRef(obj, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(drawable->serial_number, weakref);
Py_DECREF(weakref);
}
}
return obj;
} }
// Helper to extract shared_ptr<UIDrawable> from any UIDrawable Python subclass. // Helper to extract shared_ptr<UIDrawable> from any UIDrawable Python subclass.

View file

@ -8,6 +8,7 @@
#include "UILine.h" #include "UILine.h"
#include "UICircle.h" #include "UICircle.h"
#include "UIArc.h" #include "UIArc.h"
#include "3d/Viewport3D.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
@ -1284,6 +1285,133 @@ PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) {
return (PyObject*)collection; return (PyObject*)collection;
} }
// #369: single cache-aware conversion from a C++ drawable to its Python wrapper.
// Every path that hands a drawable back to Python must go through here, or object
// identity breaks (`x.parent is x.parent` -> False) and Python subclasses are lost.
PyObject* UIDrawable::pyobject_for(std::shared_ptr<UIDrawable> drawable) {
if (!drawable) {
Py_RETURN_NONE;
}
// A live wrapper wins: it carries the caller's subclass and identity.
if (drawable->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
if (cached) {
return cached; // lookup() already INCREF'd
}
}
PyTypeObject* type = nullptr;
PyObject* obj = nullptr;
switch (drawable->derived_type()) {
case PyObjectsEnum::UIFRAME:
{
type = &mcrfpydef::PyUIFrameType;
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICAPTION:
{
type = &mcrfpydef::PyUICaptionType;
auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
pyObj->font = nullptr;
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UISPRITE:
{
type = &mcrfpydef::PyUISpriteType;
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIGRIDVIEW:
{
type = &mcrfpydef::PyUIGridViewType;
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGridView>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UILINE:
{
type = &mcrfpydef::PyUILineType;
auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UILine>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICIRCLE:
{
type = &mcrfpydef::PyUICircleType;
auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICircle>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIARC:
{
type = &mcrfpydef::PyUIArcType;
auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIArc>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIVIEWPORT3D:
{
type = &mcrfpydef::PyViewport3DType;
auto pyObj = (PyViewport3DObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<mcrf::Viewport3D>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
default:
PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type");
return nullptr;
}
// Register the fresh wrapper so the next lookup returns this same object.
// Covers C++-created drawables whose original wrapper was GC'd.
if (obj && drawable->serial_number != 0) {
PyObject* weakref = PyWeakref_NewRef(obj, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(drawable->serial_number, weakref);
Py_DECREF(weakref);
}
}
return obj;
}
// Python API - get parent drawable // Python API - get parent drawable
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) { PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
@ -1299,67 +1427,7 @@ PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
// Scene not found in python_scenes (shouldn't happen, but fall through to None) // Scene not found in python_scenes (shouldn't happen, but fall through to None)
} }
auto parent_ptr = drawable->getParent(); return UIDrawable::pyobject_for(drawable->getParent());
if (!parent_ptr) {
Py_RETURN_NONE;
}
// Convert parent to Python object using the cache/conversion system
// Re-use the pattern from UICollection
PyTypeObject* type = nullptr;
PyObject* obj = nullptr;
switch (parent_ptr->derived_type()) {
case PyObjectsEnum::UIFRAME:
{
type = &mcrfpydef::PyUIFrameType;
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UICAPTION:
{
type = &mcrfpydef::PyUICaptionType;
auto pyObj = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(parent_ptr);
pyObj->font = nullptr;
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UISPRITE:
{
type = &mcrfpydef::PyUISpriteType;
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
case PyObjectsEnum::UIGRIDVIEW:
{
type = &mcrfpydef::PyUIGridViewType;
auto pyObj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGridView>(parent_ptr);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
}
default:
Py_RETURN_NONE;
}
return obj;
} }
// Python API - set parent drawable (or None to remove from parent) // Python API - set parent drawable (or None to remove from parent)

View file

@ -180,6 +180,12 @@ public:
// Get the global (screen) position by walking up the parent chain (#102) // Get the global (screen) position by walking up the parent chain (#102)
sf::Vector2f get_global_position() const; sf::Vector2f get_global_position() const;
// #369: the single cache-aware C++ -> Python wrapper conversion for drawables.
// Returns the live wrapper for this drawable if one exists (preserving object
// identity and any Python subclass), otherwise allocates one and registers it.
// Returns a new reference, or nullptr with an exception set.
static PyObject* pyobject_for(std::shared_ptr<UIDrawable> drawable);
// Python API for parent/global_position // Python API for parent/global_position
static PyObject* get_parent(PyObject* self, void* closure); static PyObject* get_parent(PyObject* self, void* closure);
static int set_parent(PyObject* self, PyObject* value, void* closure); static int set_parent(PyObject* self, PyObject* value, void* closure);

View file

@ -12,6 +12,40 @@
#include <sstream> #include <sstream>
#include <algorithm> #include <algorithm>
// #369: single cache-aware conversion from a C++ entity to its Python wrapper.
// Raw tp_alloc here is not merely an identity bug as it is for drawables: a duplicate
// wrapper's tp_dealloc unconditionally clears UIEntity::pyobject, destroying the #266
// subclass-identity ref held by the *original* wrapper. Every site must use this.
static PyObject* convertEntityToPython(std::shared_ptr<UIEntity> entity) {
if (!entity) {
Py_RETURN_NONE;
}
if (entity->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(entity->serial_number);
if (cached) {
return cached; // lookup() already INCREF'd
}
}
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
auto o = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
if (!o) return NULL;
o->data = entity;
o->weakreflist = NULL;
if (entity->serial_number != 0) {
PyObject* weakref = PyWeakref_NewRef((PyObject*)o, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(entity->serial_number, weakref);
Py_DECREF(weakref);
}
}
return (PyObject*)o;
}
// ============================================================================ // ============================================================================
// UIEntityCollectionIter implementation // UIEntityCollectionIter implementation
// ============================================================================ // ============================================================================
@ -44,23 +78,7 @@ PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
auto target = (*self->data)[self->index]; auto target = (*self->data)[self->index];
++self->index; ++self->index;
// Check cache first to preserve derived class identity return convertEntityToPython(std::static_pointer_cast<UIEntity>(target));
if (target->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
// Otherwise create and return a new Python Entity object
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
auto o = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
if (!o) return NULL;
o->data = std::static_pointer_cast<UIEntity>(target);
o->weakreflist = NULL;
return (PyObject*)o;
} }
PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self) PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self)
@ -102,35 +120,7 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
// #329 - O(1) random access (was std::advance over a std::list, O(n)) // #329 - O(1) random access (was std::advance over a std::list, O(n))
auto target = (*vec)[index]; auto target = (*vec)[index];
// Check cache first to preserve derived class return convertEntityToPython(std::static_pointer_cast<UIEntity>(target));
if (target->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
// Create a new base Entity object
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
auto o = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
if (!o) return NULL;
o->data = std::static_pointer_cast<UIEntity>(target);
o->weakreflist = NULL;
// Re-register in cache if the entity has a serial number
// This handles the case where the original Python wrapper was GC'd
// but the C++ object persists (e.g., inline-created objects added to collections)
if (target->serial_number != 0) {
PyObject* weakref = PyWeakref_NewRef((PyObject*)o, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(target->serial_number, weakref);
Py_DECREF(weakref);
}
}
return (PyObject*)o;
} }
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) { int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {
@ -244,19 +234,15 @@ PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject*
return NULL; return NULL;
} }
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
// Add all elements from self // Add all elements from self
Py_ssize_t idx = 0; Py_ssize_t idx = 0;
for (const auto& entity : *self->data) { for (const auto& entity : *self->data) {
auto obj = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); PyObject* obj = convertEntityToPython(entity);
if (!obj) { if (!obj) {
Py_DECREF(result_list); Py_DECREF(result_list);
return NULL; return NULL;
} }
obj->data = entity; PyList_SET_ITEM(result_list, idx++, obj);
obj->weakreflist = NULL;
PyList_SET_ITEM(result_list, idx++, (PyObject*)obj);
} }
// Add all elements from other // Add all elements from other
@ -358,21 +344,13 @@ PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* self, PyObje
return NULL; return NULL;
} }
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
auto it = self->data->begin();
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
auto cur_it = it; PyObject* obj = convertEntityToPython((*self->data)[cur]);
std::advance(cur_it, cur);
auto obj = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
if (!obj) { if (!obj) {
Py_DECREF(result_list); Py_DECREF(result_list);
return NULL; return NULL;
} }
obj->data = *cur_it; PyList_SET_ITEM(result_list, i, obj);
obj->weakreflist = NULL;
PyList_SET_ITEM(result_list, i, (PyObject*)obj);
} }
return result_list; return result_list;

View file

@ -44,17 +44,14 @@ def shot():
def case_child_parent_is_the_view(): def case_child_parent_is_the_view():
"""The child's parent is the Grid (UIGridView), not the internal _GridData. """The child's parent is the Grid (UIGridView), not the internal _GridData.
Identity (`child.parent is grid`) is NOT asserted: `.parent` allocates a fresh Identity is asserted directly now that #369 routes .parent through the object
wrapper on every read, so `is` fails for Frames too -- even `kid.parent is cache; this originally had to settle for comparing .name.
kid.parent` is False. That is a pre-existing engine-wide gap, not this bug, and
it is filed separately. Here we assert the parent's TYPE and identify the view by
name, which is what #364 actually turns on.
""" """
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(200, 200), name="the_view") grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(200, 200), name="the_view")
bubble = mcrfpy.Frame(pos=(32, 32), size=(20, 20)) bubble = mcrfpy.Frame(pos=(32, 32), size=(20, 20))
grid.children.append(bubble) grid.children.append(bubble)
check("1a: child.parent is the grid view", bubble.parent.name == "the_view") check("1a: child.parent is the grid view", bubble.parent is grid)
# #361: mcrfpy.Grid IS mcrfpy.GridView (one type object, two names), and the # #361: mcrfpy.Grid IS mcrfpy.GridView (one type object, two names), and the
# canonical tp_name is GridView. # canonical tp_name is GridView.
check("1b: child.parent is a Grid, not a GridData", check("1b: child.parent is a Grid, not a GridData",
@ -72,7 +69,7 @@ def case_children_are_per_view_entities_are_shared():
v1.children.append(marker) v1.children.append(marker)
check("2b: child appended to v1 is in v1.children", len(v1.children) == 1) check("2b: child appended to v1 is in v1.children", len(v1.children) == 1)
check("2c: child appended to v1 is NOT in v2.children", len(v2.children) == 0) check("2c: child appended to v1 is NOT in v2.children", len(v2.children) == 0)
check("2d: the child's parent is v1, not v2", marker.parent.name == "v1") check("2d: the child's parent is v1, not v2", marker.parent is v1)
v2.children.append(mcrfpy.Frame(pos=(64, 64), size=(20, 20))) v2.children.append(mcrfpy.Frame(pos=(64, 64), size=(20, 20)))
check("2e: v2 keeps its own children", len(v2.children) == 1 and len(v1.children) == 1) check("2e: v2 keeps its own children", len(v2.children) == 1 and len(v1.children) == 1)

View file

@ -0,0 +1,136 @@
"""
Regression test for issue #369.
Every path that hands a C++ drawable back to Python must return the *same* wrapper
object, or `child.parent is parent` silently answers False and Python subclasses
reached through those paths lose their identity.
Broken paths this locks down:
- UIDrawable::get_parent (.parent)
- find_in_collection (mcrfpy.find / find_all)
- UIEntityCollection concat/slice (entities + [...], entities[a:b])
"""
import mcrfpy
import sys
failures = []
def check(label, condition):
if condition:
print(f" PASS: {label}")
else:
print(f" FAIL: {label}")
failures.append(label)
scene = mcrfpy.Scene("issue369")
ui = scene.children
# ---------------------------------------------------------------- .parent identity
print("1. .parent returns a stable, identical wrapper")
parent = mcrfpy.Frame(pos=(0, 0), size=(100, 100), name="parent_frame")
kid = mcrfpy.Frame(pos=(1, 1), size=(10, 10), name="kid_frame")
ui.append(parent)
parent.children.append(kid)
check("kid.parent is parent", kid.parent is parent)
check("repeated reads are identical", kid.parent is kid.parent)
check("id() is stable across reads", id(kid.parent) == id(kid.parent))
# .parent must be usable as a dict key / set member
seen = {kid.parent: "found"}
check(".parent works as a dict key", seen.get(parent) == "found")
check(".parent works in a set", len({kid.parent, kid.parent, parent}) == 1)
# ------------------------------------------------------- subclass survives .parent
print("2. Python subclass identity survives .parent")
class MyFrame(mcrfpy.Frame):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.custom_state = "preserved"
sub_parent = MyFrame(pos=(0, 0), size=(200, 200), name="sub_parent")
sub_kid = mcrfpy.Caption(pos=(5, 5), text="inner", name="sub_kid")
ui.append(sub_parent)
sub_parent.children.append(sub_kid)
check("sub_kid.parent is sub_parent", sub_kid.parent is sub_parent)
check("subclass type preserved", isinstance(sub_kid.parent, MyFrame))
check("subclass attributes preserved",
getattr(sub_kid.parent, "custom_state", None) == "preserved")
# ------------------------------------------------- .parent covers all drawable types
print("3. .parent resolves non-Frame/Caption/Sprite/Grid parents (was None)")
# Line/Circle/Arc/Viewport3D had no switch arm and fell through to Py_RETURN_NONE.
# They aren't containers, so verify the *child-of-grid* case instead: a GridView
# parent must come back identical, and every type must at least round-trip via find().
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(100, 100), name="the_grid")
ui.append(grid)
overlay = mcrfpy.Frame(pos=(0, 0), size=(10, 10), name="overlay_child")
grid.children.append(overlay)
check("overlay.parent is grid", overlay.parent is grid)
# ------------------------------------------------------------- find() identity
print("4. find() / find_all() return existing wrappers")
found = mcrfpy.find("parent_frame", "issue369")
check("find() returns the same object", found is parent)
found_sub = mcrfpy.find("sub_parent", "issue369")
check("find() preserves subclass", found_sub is sub_parent)
check("find() subclass attrs intact",
getattr(found_sub, "custom_state", None) == "preserved")
all_found = mcrfpy.find_all("*_frame", "issue369")
by_id = {id(o) for o in all_found}
check("find_all() returns live wrappers",
id(parent) in by_id and id(kid) in by_id)
# ---------------------------------------------------------- entity collection paths
print("5. EntityCollection slice / concat preserve entity identity")
class MyEntity(mcrfpy.Entity):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.tag = "mine"
e0 = MyEntity(grid_pos=(1, 1))
e1 = mcrfpy.Entity(grid_pos=(2, 2))
e2 = mcrfpy.Entity(grid_pos=(3, 3))
for e in (e0, e1, e2):
grid.entities.append(e)
check("entities[0] is e0", grid.entities[0] is e0)
sliced = grid.entities[0:2]
check("slice returns the same wrappers", sliced[0] is e0 and sliced[1] is e1)
check("slice preserves subclass", isinstance(sliced[0], MyEntity))
check("slice preserves subclass attrs", getattr(sliced[0], "tag", None) == "mine")
concat = grid.entities + []
check("concat returns the same wrappers", concat[0] is e0)
check("concat preserves subclass", isinstance(concat[0], MyEntity))
# The #266 identity ref must survive a duplicate-wrapper round trip: before the fix,
# the temporary wrapper's dealloc cleared UIEntity::pyobject on the shared C++ object.
del sliced, concat
import gc
gc.collect()
check("entity identity survives temp wrappers", grid.entities[0] is e0)
check("subclass attrs survive temp wrappers",
getattr(grid.entities[0], "tag", None) == "mine")
print()
if failures:
print(f"FAIL - {len(failures)} check(s) failed:")
for f in failures:
print(f" - {f}")
sys.exit(1)
print("PASS")
sys.exit(0)