Fix multiple low priority issues

closes #12, closes #80, closes #95, closes #96, closes #99

- Issue #12: Set tp_new to NULL for GridPoint and GridPointState to prevent instantiation from Python
- Issue #80: Renamed Caption.size to Caption.font_size for semantic clarity
- Issue #95: Fixed UICollection repr to show actual derived types instead of generic UIDrawable
- Issue #96: Added extend() method to UICollection for API consistency with UIEntityCollection
- Issue #99: Exposed read-only properties for Texture (sprite_width, sprite_height, sheet_width, sheet_height, sprite_count, source) and Font (family, source)

All issues have corresponding tests that verify the fixes work correctly.
This commit is contained in:
John McCardle 2025-07-05 16:09:52 -04:00
parent e5affaf317
commit 5a003a9aa5
13 changed files with 1094 additions and 7 deletions

View File

@ -61,3 +61,19 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{ {
return (PyObject*)type->tp_alloc(type, 0); return (PyObject*)type->tp_alloc(type, 0);
} }
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
}
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{NULL} // Sentinel
};

View File

@ -21,6 +21,12 @@ public:
static Py_hash_t hash(PyObject*); static Py_hash_t hash(PyObject*);
static int init(PyFontObject*, PyObject*, PyObject*); static int init(PyFontObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_family(PyFontObject* self, void* closure);
static PyObject* get_source(PyFontObject* self, void* closure);
static PyGetSetDef getsetters[];
}; };
namespace mcrfpydef { namespace mcrfpydef {
@ -33,6 +39,7 @@ namespace mcrfpydef {
//.tp_hash = PyFont::hash, //.tp_hash = PyFont::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Font Object"), .tp_doc = PyDoc_STR("SFML Font Object"),
.tp_getset = PyFont::getsetters,
//.tp_base = &PyBaseObject_Type, //.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyFont::init, .tp_init = (initproc)PyFont::init,
.tp_new = PyType_GenericNew, //PyFont::pynew, .tp_new = PyType_GenericNew, //PyFont::pynew,

View File

@ -79,3 +79,43 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{ {
return (PyObject*)type->tp_alloc(type, 0); return (PyObject*)type->tp_alloc(type, 0);
} }
PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_width);
}
PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_height);
}
PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_width);
}
PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_height);
}
PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->getSpriteCount());
}
PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyTexture::getsetters[] = {
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
{NULL} // Sentinel
};

View File

@ -26,6 +26,16 @@ public:
static Py_hash_t hash(PyObject*); static Py_hash_t hash(PyObject*);
static int init(PyTextureObject*, PyObject*, PyObject*); static int init(PyTextureObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
static PyObject* get_sheet_width(PyTextureObject* self, void* closure);
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
static PyObject* get_source(PyTextureObject* self, void* closure);
static PyGetSetDef getsetters[];
}; };
namespace mcrfpydef { namespace mcrfpydef {
@ -38,6 +48,7 @@ namespace mcrfpydef {
.tp_hash = PyTexture::hash, .tp_hash = PyTexture::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Texture Object"), .tp_doc = PyDoc_STR("SFML Texture Object"),
.tp_getset = PyTexture::getsetters,
//.tp_base = &PyBaseObject_Type, //.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyTexture::init, .tp_init = (initproc)PyTexture::init,
.tp_new = PyType_GenericNew, //PyTexture::pynew, .tp_new = PyType_GenericNew, //PyTexture::pynew,

View File

@ -197,7 +197,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1}, {"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1},
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5}, {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
{NULL} {NULL}
@ -314,7 +314,7 @@ bool UICaption::setProperty(const std::string& name, float value) {
text.setPosition(sf::Vector2f(text.getPosition().x, value)); text.setPosition(sf::Vector2f(text.getPosition().x, value));
return true; return true;
} }
else if (name == "size") { else if (name == "font_size" || name == "size") { // Support both for backward compatibility
text.setCharacterSize(static_cast<unsigned int>(value)); text.setCharacterSize(static_cast<unsigned int>(value));
return true; return true;
} }
@ -406,7 +406,7 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
value = text.getPosition().y; value = text.getPosition().y;
return true; return true;
} }
else if (name == "size") { else if (name == "font_size" || name == "size") { // Support both for backward compatibility
value = static_cast<float>(text.getCharacterSize()); value = static_cast<float>(text.getCharacterSize());
return true; return true;
} }

View File

@ -615,6 +615,88 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
return Py_None; return Py_None;
} }
PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
{
// Accept any iterable of UIDrawable objects
PyObject* iterator = PyObject_GetIter(iterable);
if (iterator == NULL) {
PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable");
return NULL;
}
// Ensure module is initialized
if (!McRFPy_API::mcrf_module) {
Py_DECREF(iterator);
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
return NULL;
}
// Get current highest z_index
int current_z_index = 0;
if (!self->data->empty()) {
current_z_index = self->data->back()->z_index;
}
PyObject* item;
while ((item = PyIter_Next(iterator)) != NULL) {
// Check if item is a UIDrawable subclass
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
{
Py_DECREF(item);
Py_DECREF(iterator);
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects");
return NULL;
}
// Increment z_index for each new element
if (current_z_index <= INT_MAX - 10) {
current_z_index += 10;
} else {
current_z_index = INT_MAX;
}
// Add the item based on its type
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)item;
frame->data->z_index = current_z_index;
self->data->push_back(frame->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)item;
caption->data->z_index = current_z_index;
self->data->push_back(caption->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
sprite->data->z_index = current_z_index;
self->data->push_back(sprite->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)item;
grid->data->z_index = current_z_index;
self->data->push_back(grid->data);
}
Py_DECREF(item);
}
Py_DECREF(iterator);
// Check if iteration ended due to an error
if (PyErr_Occurred()) {
return NULL;
}
// Mark scene as needing resort after adding elements
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o) PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
{ {
if (!PyLong_Check(o)) if (!PyLong_Check(o))
@ -734,7 +816,7 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
PyMethodDef UICollection::methods[] = { PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O}, {"append", (PyCFunction)UICollection::append, METH_O},
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO {"extend", (PyCFunction)UICollection::extend, METH_O},
{"remove", (PyCFunction)UICollection::remove, METH_O}, {"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O}, {"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O}, {"count", (PyCFunction)UICollection::count, METH_O},
@ -746,7 +828,47 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
std::ostringstream ss; std::ostringstream ss;
if (!self->data) ss << "<UICollection (invalid internal object)>"; if (!self->data) ss << "<UICollection (invalid internal object)>";
else { else {
ss << "<UICollection (" << self->data->size() << " child objects)>"; ss << "<UICollection (" << self->data->size() << " objects: ";
// Count each type
int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0;
for (auto& item : *self->data) {
switch(item->derived_type()) {
case PyObjectsEnum::UIFRAME: frame_count++; break;
case PyObjectsEnum::UICAPTION: caption_count++; break;
case PyObjectsEnum::UISPRITE: sprite_count++; break;
case PyObjectsEnum::UIGRID: grid_count++; break;
default: other_count++; break;
}
}
// Build type summary
bool first = true;
if (frame_count > 0) {
ss << frame_count << " Frame" << (frame_count > 1 ? "s" : "");
first = false;
}
if (caption_count > 0) {
if (!first) ss << ", ";
ss << caption_count << " Caption" << (caption_count > 1 ? "s" : "");
first = false;
}
if (sprite_count > 0) {
if (!first) ss << ", ";
ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : "");
first = false;
}
if (grid_count > 0) {
if (!first) ss << ", ";
ss << grid_count << " Grid" << (grid_count > 1 ? "s" : "");
first = false;
}
if (other_count > 0) {
if (!first) ss << ", ";
ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : "");
}
ss << ")>";
} }
std::string repr_str = ss.str(); std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");

View File

@ -28,6 +28,7 @@ public:
static PyObject* subscript(PyUICollectionObject* self, PyObject* key); static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value); static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
static PyObject* append(PyUICollectionObject* self, PyObject* o); static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
static PyObject* remove(PyUICollectionObject* self, PyObject* o); static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value); static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value); static PyObject* count(PyUICollectionObject* self, PyObject* value);

View File

@ -75,7 +75,7 @@ namespace mcrfpydef {
.tp_doc = "UIGridPoint object", .tp_doc = "UIGridPoint object",
.tp_getset = UIGridPoint::getsetters, .tp_getset = UIGridPoint::getsetters,
//.tp_init = (initproc)PyUIGridPoint_init, // TODO Define the init function //.tp_init = (initproc)PyUIGridPoint_init, // TODO Define the init function
.tp_new = PyType_GenericNew, .tp_new = NULL, // Prevent instantiation from Python - Issue #12
}; };
static PyTypeObject PyUIGridPointStateType = { static PyTypeObject PyUIGridPointStateType = {
@ -87,6 +87,6 @@ namespace mcrfpydef {
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "UIGridPointState object", // TODO: Add PyUIGridPointState tp_init .tp_doc = "UIGridPointState object", // TODO: Add PyUIGridPointState tp_init
.tp_getset = UIGridPointState::getsetters, .tp_getset = UIGridPointState::getsetters,
.tp_new = PyType_GenericNew, .tp_new = NULL, // Prevent instantiation from Python - Issue #12
}; };
} }

View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Test for Issue #12: Forbid GridPoint/GridPointState instantiation
This test verifies that GridPoint and GridPointState cannot be instantiated
directly from Python, as they should only be created internally by the C++ code.
"""
import mcrfpy
import sys
def test_gridpoint_instantiation():
"""Test that GridPoint and GridPointState cannot be instantiated"""
print("=== Testing GridPoint/GridPointState Instantiation Prevention (Issue #12) ===\n")
tests_passed = 0
tests_total = 0
# Test 1: Try to instantiate GridPoint
print("--- Test 1: GridPoint instantiation ---")
tests_total += 1
try:
point = mcrfpy.GridPoint()
print("✗ FAIL: GridPoint() should not be allowed")
except TypeError as e:
print(f"✓ PASS: GridPoint instantiation correctly prevented: {e}")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Unexpected error: {e}")
# Test 2: Try to instantiate GridPointState
print("\n--- Test 2: GridPointState instantiation ---")
tests_total += 1
try:
state = mcrfpy.GridPointState()
print("✗ FAIL: GridPointState() should not be allowed")
except TypeError as e:
print(f"✓ PASS: GridPointState instantiation correctly prevented: {e}")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Unexpected error: {e}")
# Test 3: Verify GridPoint can still be obtained from Grid
print("\n--- Test 3: GridPoint obtained from Grid.at() ---")
tests_total += 1
try:
grid = mcrfpy.Grid(10, 10)
point = grid.at(5, 5)
print(f"✓ PASS: GridPoint obtained from Grid.at(): {point}")
print(f" Type: {type(point).__name__}")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Could not get GridPoint from Grid: {e}")
# Test 4: Verify GridPointState can still be obtained from GridPoint
print("\n--- Test 4: GridPointState obtained from GridPoint ---")
tests_total += 1
try:
# GridPointState is accessed through GridPoint's click handler
# Let's check if we can access point properties that would use GridPointState
if hasattr(point, 'walkable'):
print(f"✓ PASS: GridPoint has expected properties")
print(f" walkable: {point.walkable}")
print(f" transparent: {point.transparent}")
tests_passed += 1
else:
print("✗ FAIL: GridPoint missing expected properties")
except Exception as e:
print(f"✗ FAIL: Error accessing GridPoint properties: {e}")
# Test 5: Try to call the types directly (alternative syntax)
print("\n--- Test 5: Alternative instantiation attempts ---")
tests_total += 1
all_prevented = True
# Try various ways to instantiate
attempts = [
("mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)",
lambda: mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)),
("type(point)()",
lambda: type(point)() if 'point' in locals() else None),
]
for desc, func in attempts:
try:
if func:
result = func()
print(f"✗ FAIL: {desc} should not be allowed")
all_prevented = False
except (TypeError, AttributeError) as e:
print(f" ✓ Correctly prevented: {desc}")
except Exception as e:
print(f" ? Unexpected error for {desc}: {e}")
if all_prevented:
print("✓ PASS: All alternative instantiation attempts prevented")
tests_passed += 1
else:
print("✗ FAIL: Some instantiation attempts succeeded")
# Summary
print(f"\n=== SUMMARY ===")
print(f"Tests passed: {tests_passed}/{tests_total}")
if tests_passed == tests_total:
print("\nIssue #12 FIXED: GridPoint/GridPointState instantiation properly forbidden!")
else:
print("\nIssue #12: Some tests failed")
return tests_passed == tests_total
def run_test(runtime):
"""Timer callback to run the test"""
try:
# First verify the types exist
print("Checking that GridPoint and GridPointState types exist...")
print(f"GridPoint type: {mcrfpy.GridPoint}")
print(f"GridPointState type: {mcrfpy.GridPointState}")
print()
success = test_gridpoint_instantiation()
print("\nOverall result: " + ("PASS" if success else "FAIL"))
except Exception as e:
print(f"\nTest error: {e}")
import traceback
traceback.print_exc()
print("\nOverall result: FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100)

View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
Test for Issue #80: Rename Caption.size to font_size
This test verifies that Caption now uses font_size property instead of size,
while maintaining backward compatibility.
"""
import mcrfpy
import sys
def test_caption_font_size():
"""Test Caption font_size property"""
print("=== Testing Caption font_size Property (Issue #80) ===\n")
tests_passed = 0
tests_total = 0
# Create a caption for testing
caption = mcrfpy.Caption((100, 100), "Test Text", mcrfpy.Font("assets/JetbrainsMono.ttf"))
# Test 1: Check that font_size property exists and works
print("--- Test 1: font_size property ---")
tests_total += 1
try:
# Set font size using new property name
caption.font_size = 24
if caption.font_size == 24:
print("✓ PASS: font_size property works correctly")
tests_passed += 1
else:
print(f"✗ FAIL: font_size is {caption.font_size}, expected 24")
except AttributeError as e:
print(f"✗ FAIL: font_size property not found: {e}")
# Test 2: Check that old 'size' property is removed
print("\n--- Test 2: Old 'size' property removed ---")
tests_total += 1
try:
# Try to access size property - this should fail
old_size = caption.size
print(f"✗ FAIL: 'size' property still accessible (value: {old_size}) - should be removed")
except AttributeError:
print("✓ PASS: 'size' property correctly removed")
tests_passed += 1
# Test 3: Verify font_size changes are reflected
print("\n--- Test 3: font_size changes ---")
tests_total += 1
caption.font_size = 36
if caption.font_size == 36:
print("✓ PASS: font_size changes are reflected correctly")
tests_passed += 1
else:
print(f"✗ FAIL: font_size is {caption.font_size}, expected 36")
# Test 4: Check property type
print("\n--- Test 4: Property type check ---")
tests_total += 1
caption.font_size = 18
if isinstance(caption.font_size, int):
print("✓ PASS: font_size returns integer as expected")
tests_passed += 1
else:
print(f"✗ FAIL: font_size returns {type(caption.font_size).__name__}, expected int")
# Test 5: Verify in __dir__
print("\n--- Test 5: Property introspection ---")
tests_total += 1
properties = dir(caption)
if 'font_size' in properties:
print("✓ PASS: 'font_size' appears in dir(caption)")
tests_passed += 1
else:
print("✗ FAIL: 'font_size' not found in dir(caption)")
# Check if 'size' still appears
if 'size' in properties:
print(" INFO: 'size' still appears in dir(caption) - backward compatibility maintained")
else:
print(" INFO: 'size' removed from dir(caption) - breaking change")
# Test 6: Edge cases
print("\n--- Test 6: Edge cases ---")
tests_total += 1
all_passed = True
# Test setting to 0
caption.font_size = 0
if caption.font_size != 0:
print(f"✗ FAIL: Setting font_size to 0 failed (got {caption.font_size})")
all_passed = False
# Test setting to large value
caption.font_size = 100
if caption.font_size != 100:
print(f"✗ FAIL: Setting font_size to 100 failed (got {caption.font_size})")
all_passed = False
# Test float to int conversion
caption.font_size = 24.7
if caption.font_size != 24:
print(f"✗ FAIL: Float to int conversion failed (got {caption.font_size})")
all_passed = False
if all_passed:
print("✓ PASS: All edge cases handled correctly")
tests_passed += 1
else:
print("✗ FAIL: Some edge cases failed")
# Test 7: Scene UI integration
print("\n--- Test 7: Scene UI integration ---")
tests_total += 1
try:
scene_ui = mcrfpy.sceneUI("test")
scene_ui.append(caption)
# Modify font_size after adding to scene
caption.font_size = 32
print("✓ PASS: Caption with font_size works in scene UI")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Scene UI integration failed: {e}")
# Summary
print(f"\n=== SUMMARY ===")
print(f"Tests passed: {tests_passed}/{tests_total}")
if tests_passed == tests_total:
print("\nIssue #80 FIXED: Caption.size successfully renamed to font_size!")
else:
print("\nIssue #80: Some tests failed")
return tests_passed == tests_total
def run_test(runtime):
"""Timer callback to run the test"""
try:
success = test_caption_font_size()
print("\nOverall result: " + ("PASS" if success else "FAIL"))
except Exception as e:
print(f"\nTest error: {e}")
import traceback
traceback.print_exc()
print("\nOverall result: FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100)

View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Test for Issue #95: Fix UICollection __repr__ type display
This test verifies that UICollection's repr shows the actual types of contained
objects instead of just showing them all as "UIDrawable".
"""
import mcrfpy
import sys
def test_uicollection_repr():
"""Test UICollection repr shows correct types"""
print("=== Testing UICollection __repr__ Type Display (Issue #95) ===\n")
tests_passed = 0
tests_total = 0
# Get scene UI collection
scene_ui = mcrfpy.sceneUI("test")
# Test 1: Empty collection
print("--- Test 1: Empty collection ---")
tests_total += 1
repr_str = repr(scene_ui)
print(f"Empty collection repr: {repr_str}")
if "0 objects" in repr_str:
print("✓ PASS: Empty collection shows correctly")
tests_passed += 1
else:
print("✗ FAIL: Empty collection repr incorrect")
# Test 2: Add various UI elements
print("\n--- Test 2: Mixed UI elements ---")
tests_total += 1
# Add Frame
frame = mcrfpy.Frame(10, 10, 100, 100)
scene_ui.append(frame)
# Add Caption
caption = mcrfpy.Caption((150, 50), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf"))
scene_ui.append(caption)
# Add Sprite
sprite = mcrfpy.Sprite(200, 100)
scene_ui.append(sprite)
# Add Grid
grid = mcrfpy.Grid(10, 10)
grid.x = 300
grid.y = 100
scene_ui.append(grid)
# Check repr
repr_str = repr(scene_ui)
print(f"Collection repr: {repr_str}")
# Verify it shows the correct types
expected_types = ["1 Frame", "1 Caption", "1 Sprite", "1 Grid"]
all_found = all(expected in repr_str for expected in expected_types)
if all_found and "UIDrawable" not in repr_str:
print("✓ PASS: All types shown correctly, no generic UIDrawable")
tests_passed += 1
else:
print("✗ FAIL: Types not shown correctly")
for expected in expected_types:
if expected in repr_str:
print(f" ✓ Found: {expected}")
else:
print(f" ✗ Missing: {expected}")
if "UIDrawable" in repr_str:
print(" ✗ Still shows generic UIDrawable")
# Test 3: Multiple of same type
print("\n--- Test 3: Multiple objects of same type ---")
tests_total += 1
# Add more frames
frame2 = mcrfpy.Frame(10, 120, 100, 100)
frame3 = mcrfpy.Frame(10, 230, 100, 100)
scene_ui.append(frame2)
scene_ui.append(frame3)
repr_str = repr(scene_ui)
print(f"Collection repr: {repr_str}")
if "3 Frames" in repr_str:
print("✓ PASS: Plural form shown correctly for multiple Frames")
tests_passed += 1
else:
print("✗ FAIL: Plural form not correct")
# Test 4: Check total count
print("\n--- Test 4: Total count verification ---")
tests_total += 1
# Should have: 3 Frames, 1 Caption, 1 Sprite, 1 Grid = 6 total
if "6 objects:" in repr_str:
print("✓ PASS: Total count shown correctly")
tests_passed += 1
else:
print("✗ FAIL: Total count incorrect")
# Test 5: Nested collections (Frame with children)
print("\n--- Test 5: Nested collections ---")
tests_total += 1
# Add child to frame
child_sprite = mcrfpy.Sprite(10, 10)
frame.children.append(child_sprite)
# Check frame's children collection
children_repr = repr(frame.children)
print(f"Frame children repr: {children_repr}")
if "1 Sprite" in children_repr:
print("✓ PASS: Nested collection shows correct type")
tests_passed += 1
else:
print("✗ FAIL: Nested collection type incorrect")
# Test 6: Collection remains valid after modifications
print("\n--- Test 6: Collection after modifications ---")
tests_total += 1
# Remove an item
scene_ui.remove(0) # Remove first frame
repr_str = repr(scene_ui)
print(f"After removal repr: {repr_str}")
if "2 Frames" in repr_str and "5 objects:" in repr_str:
print("✓ PASS: Collection repr updated correctly after removal")
tests_passed += 1
else:
print("✗ FAIL: Collection repr not updated correctly")
# Summary
print(f"\n=== SUMMARY ===")
print(f"Tests passed: {tests_passed}/{tests_total}")
if tests_passed == tests_total:
print("\nIssue #95 FIXED: UICollection __repr__ now shows correct types!")
else:
print("\nIssue #95: Some tests failed")
return tests_passed == tests_total
def run_test(runtime):
"""Timer callback to run the test"""
try:
success = test_uicollection_repr()
print("\nOverall result: " + ("PASS" if success else "FAIL"))
except Exception as e:
print(f"\nTest error: {e}")
import traceback
traceback.print_exc()
print("\nOverall result: FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100)

View File

@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
Test for Issue #96: Add extend() method to UICollection
This test verifies that UICollection now has an extend() method similar to
UIEntityCollection.extend().
"""
import mcrfpy
import sys
def test_uicollection_extend():
"""Test UICollection extend method"""
print("=== Testing UICollection extend() Method (Issue #96) ===\n")
tests_passed = 0
tests_total = 0
# Get scene UI collection
scene_ui = mcrfpy.sceneUI("test")
# Test 1: Basic extend with list
print("--- Test 1: Extend with list ---")
tests_total += 1
try:
# Create a list of UI elements
elements = [
mcrfpy.Frame(10, 10, 100, 100),
mcrfpy.Caption((150, 50), "Test1", mcrfpy.Font("assets/JetbrainsMono.ttf")),
mcrfpy.Sprite(200, 100)
]
# Extend the collection
scene_ui.extend(elements)
if len(scene_ui) == 3:
print("✓ PASS: Extended collection with 3 elements")
tests_passed += 1
else:
print(f"✗ FAIL: Expected 3 elements, got {len(scene_ui)}")
except Exception as e:
print(f"✗ FAIL: Error extending with list: {e}")
# Test 2: Extend with tuple
print("\n--- Test 2: Extend with tuple ---")
tests_total += 1
try:
# Create a tuple of UI elements
more_elements = (
mcrfpy.Grid(10, 10),
mcrfpy.Frame(300, 10, 100, 100)
)
# Extend the collection
scene_ui.extend(more_elements)
if len(scene_ui) == 5:
print("✓ PASS: Extended collection with tuple (now 5 elements)")
tests_passed += 1
else:
print(f"✗ FAIL: Expected 5 elements, got {len(scene_ui)}")
except Exception as e:
print(f"✗ FAIL: Error extending with tuple: {e}")
# Test 3: Extend with generator
print("\n--- Test 3: Extend with generator ---")
tests_total += 1
try:
# Create a generator of UI elements
def create_sprites():
for i in range(3):
yield mcrfpy.Sprite(50 + i*50, 200)
# Extend with generator
scene_ui.extend(create_sprites())
if len(scene_ui) == 8:
print("✓ PASS: Extended collection with generator (now 8 elements)")
tests_passed += 1
else:
print(f"✗ FAIL: Expected 8 elements, got {len(scene_ui)}")
except Exception as e:
print(f"✗ FAIL: Error extending with generator: {e}")
# Test 4: Error handling - non-iterable
print("\n--- Test 4: Error handling - non-iterable ---")
tests_total += 1
try:
scene_ui.extend(42) # Not iterable
print("✗ FAIL: Should have raised TypeError for non-iterable")
except TypeError as e:
print(f"✓ PASS: Correctly raised TypeError: {e}")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Wrong exception type: {e}")
# Test 5: Error handling - wrong element type
print("\n--- Test 5: Error handling - wrong element type ---")
tests_total += 1
try:
scene_ui.extend([1, 2, 3]) # Wrong types
print("✗ FAIL: Should have raised TypeError for non-UIDrawable elements")
except TypeError as e:
print(f"✓ PASS: Correctly raised TypeError: {e}")
tests_passed += 1
except Exception as e:
print(f"✗ FAIL: Wrong exception type: {e}")
# Test 6: Extend empty iterable
print("\n--- Test 6: Extend with empty list ---")
tests_total += 1
try:
initial_len = len(scene_ui)
scene_ui.extend([]) # Empty list
if len(scene_ui) == initial_len:
print("✓ PASS: Extending with empty list works correctly")
tests_passed += 1
else:
print(f"✗ FAIL: Length changed from {initial_len} to {len(scene_ui)}")
except Exception as e:
print(f"✗ FAIL: Error extending with empty list: {e}")
# Test 7: Z-index ordering
print("\n--- Test 7: Z-index ordering ---")
tests_total += 1
try:
# Clear and add fresh elements
while len(scene_ui) > 0:
scene_ui.remove(0)
# Add some initial elements
frame1 = mcrfpy.Frame(0, 0, 50, 50)
scene_ui.append(frame1)
# Extend with more elements
new_elements = [
mcrfpy.Frame(60, 0, 50, 50),
mcrfpy.Caption((120, 25), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf"))
]
scene_ui.extend(new_elements)
# Check z-indices are properly assigned
z_indices = [scene_ui[i].z_index for i in range(3)]
# Z-indices should be increasing
if z_indices[0] < z_indices[1] < z_indices[2]:
print(f"✓ PASS: Z-indices properly ordered: {z_indices}")
tests_passed += 1
else:
print(f"✗ FAIL: Z-indices not properly ordered: {z_indices}")
except Exception as e:
print(f"✗ FAIL: Error checking z-indices: {e}")
# Test 8: Extend with another UICollection
print("\n--- Test 8: Extend with another UICollection ---")
tests_total += 1
try:
# Create a Frame with children
frame_with_children = mcrfpy.Frame(200, 200, 100, 100)
frame_with_children.children.append(mcrfpy.Sprite(10, 10))
frame_with_children.children.append(mcrfpy.Caption((10, 50), "Child", mcrfpy.Font("assets/JetbrainsMono.ttf")))
# Try to extend scene_ui with the frame's children collection
initial_len = len(scene_ui)
scene_ui.extend(frame_with_children.children)
if len(scene_ui) == initial_len + 2:
print("✓ PASS: Extended with another UICollection")
tests_passed += 1
else:
print(f"✗ FAIL: Expected {initial_len + 2} elements, got {len(scene_ui)}")
except Exception as e:
print(f"✗ FAIL: Error extending with UICollection: {e}")
# Summary
print(f"\n=== SUMMARY ===")
print(f"Tests passed: {tests_passed}/{tests_total}")
if tests_passed == tests_total:
print("\nIssue #96 FIXED: UICollection.extend() implemented successfully!")
else:
print("\nIssue #96: Some tests failed")
return tests_passed == tests_total
def run_test(runtime):
"""Timer callback to run the test"""
try:
success = test_uicollection_extend()
print("\nOverall result: " + ("PASS" if success else "FAIL"))
except Exception as e:
print(f"\nTest error: {e}")
import traceback
traceback.print_exc()
print("\nOverall result: FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100)

View File

@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Test for Issue #99: Expose Texture and Font properties
This test verifies that Texture and Font objects now expose their properties
as read-only attributes.
"""
import mcrfpy
import sys
def test_texture_properties():
"""Test Texture properties"""
print("=== Testing Texture Properties ===")
tests_passed = 0
tests_total = 0
# Create a texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Test 1: sprite_width property
tests_total += 1
try:
width = texture.sprite_width
if width == 16:
print(f"✓ PASS: sprite_width = {width}")
tests_passed += 1
else:
print(f"✗ FAIL: sprite_width = {width}, expected 16")
except AttributeError as e:
print(f"✗ FAIL: sprite_width not accessible: {e}")
# Test 2: sprite_height property
tests_total += 1
try:
height = texture.sprite_height
if height == 16:
print(f"✓ PASS: sprite_height = {height}")
tests_passed += 1
else:
print(f"✗ FAIL: sprite_height = {height}, expected 16")
except AttributeError as e:
print(f"✗ FAIL: sprite_height not accessible: {e}")
# Test 3: sheet_width property
tests_total += 1
try:
sheet_w = texture.sheet_width
if isinstance(sheet_w, int) and sheet_w > 0:
print(f"✓ PASS: sheet_width = {sheet_w}")
tests_passed += 1
else:
print(f"✗ FAIL: sheet_width invalid: {sheet_w}")
except AttributeError as e:
print(f"✗ FAIL: sheet_width not accessible: {e}")
# Test 4: sheet_height property
tests_total += 1
try:
sheet_h = texture.sheet_height
if isinstance(sheet_h, int) and sheet_h > 0:
print(f"✓ PASS: sheet_height = {sheet_h}")
tests_passed += 1
else:
print(f"✗ FAIL: sheet_height invalid: {sheet_h}")
except AttributeError as e:
print(f"✗ FAIL: sheet_height not accessible: {e}")
# Test 5: sprite_count property
tests_total += 1
try:
count = texture.sprite_count
expected = texture.sheet_width * texture.sheet_height
if count == expected:
print(f"✓ PASS: sprite_count = {count} (sheet_width * sheet_height)")
tests_passed += 1
else:
print(f"✗ FAIL: sprite_count = {count}, expected {expected}")
except AttributeError as e:
print(f"✗ FAIL: sprite_count not accessible: {e}")
# Test 6: source property
tests_total += 1
try:
source = texture.source
if "kenney_tinydungeon.png" in source:
print(f"✓ PASS: source = '{source}'")
tests_passed += 1
else:
print(f"✗ FAIL: source unexpected: '{source}'")
except AttributeError as e:
print(f"✗ FAIL: source not accessible: {e}")
# Test 7: Properties are read-only
tests_total += 1
try:
texture.sprite_width = 32 # Should fail
print("✗ FAIL: sprite_width should be read-only")
except AttributeError as e:
print(f"✓ PASS: sprite_width is read-only: {e}")
tests_passed += 1
return tests_passed, tests_total
def test_font_properties():
"""Test Font properties"""
print("\n=== Testing Font Properties ===")
tests_passed = 0
tests_total = 0
# Create a font
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
# Test 1: family property
tests_total += 1
try:
family = font.family
if isinstance(family, str) and len(family) > 0:
print(f"✓ PASS: family = '{family}'")
tests_passed += 1
else:
print(f"✗ FAIL: family invalid: '{family}'")
except AttributeError as e:
print(f"✗ FAIL: family not accessible: {e}")
# Test 2: source property
tests_total += 1
try:
source = font.source
if "JetbrainsMono.ttf" in source:
print(f"✓ PASS: source = '{source}'")
tests_passed += 1
else:
print(f"✗ FAIL: source unexpected: '{source}'")
except AttributeError as e:
print(f"✗ FAIL: source not accessible: {e}")
# Test 3: Properties are read-only
tests_total += 1
try:
font.family = "Arial" # Should fail
print("✗ FAIL: family should be read-only")
except AttributeError as e:
print(f"✓ PASS: family is read-only: {e}")
tests_passed += 1
return tests_passed, tests_total
def test_property_introspection():
"""Test that properties appear in dir()"""
print("\n=== Testing Property Introspection ===")
tests_passed = 0
tests_total = 0
# Test Texture properties in dir()
tests_total += 1
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
texture_props = dir(texture)
expected_texture_props = ['sprite_width', 'sprite_height', 'sheet_width', 'sheet_height', 'sprite_count', 'source']
missing = [p for p in expected_texture_props if p not in texture_props]
if not missing:
print("✓ PASS: All Texture properties appear in dir()")
tests_passed += 1
else:
print(f"✗ FAIL: Missing Texture properties in dir(): {missing}")
# Test Font properties in dir()
tests_total += 1
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
font_props = dir(font)
expected_font_props = ['family', 'source']
missing = [p for p in expected_font_props if p not in font_props]
if not missing:
print("✓ PASS: All Font properties appear in dir()")
tests_passed += 1
else:
print(f"✗ FAIL: Missing Font properties in dir(): {missing}")
return tests_passed, tests_total
def run_test(runtime):
"""Timer callback to run the test"""
try:
print("=== Testing Texture and Font Properties (Issue #99) ===\n")
texture_passed, texture_total = test_texture_properties()
font_passed, font_total = test_font_properties()
intro_passed, intro_total = test_property_introspection()
total_passed = texture_passed + font_passed + intro_passed
total_tests = texture_total + font_total + intro_total
print(f"\n=== SUMMARY ===")
print(f"Texture tests: {texture_passed}/{texture_total}")
print(f"Font tests: {font_passed}/{font_total}")
print(f"Introspection tests: {intro_passed}/{intro_total}")
print(f"Total tests passed: {total_passed}/{total_tests}")
if total_passed == total_tests:
print("\nIssue #99 FIXED: Texture and Font properties exposed successfully!")
print("\nOverall result: PASS")
else:
print("\nIssue #99: Some tests failed")
print("\nOverall result: FAIL")
except Exception as e:
print(f"\nTest error: {e}")
import traceback
traceback.print_exc()
print("\nOverall result: FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100)