Merge branch 'constructor-surface-1.0': pre-1.0 constructor signature freeze

Group A of the API freeze pass. No backward-compat shims.

Added parent= kwarg to Frame, Caption, Sprite, Line, Circle, Arc:
  mcrfpy.Frame(pos=(0,0), size=(100,100), parent=scene)
Validates against Frame/Scene/Grid/GridView and auto-appends to
parent.children. New UIDRAWABLE_ATTACH_TO_PARENT macro in UIBase.h
keeps the six call sites one-liners.

Added grid= kwarg to ColorLayer, TileLayer:
  mcrfpy.ColorLayer(name='fog', grid=g)
Routes through grid.add_layer(self) and matches the existing
Entity(grid=...) attachment pattern.

Reordered:
  Circle: (center, radius, ...) -- was (radius, center, ...)
  ColorLayer / TileLayer: (name, z_index, ...) -- was (z_index, name, ...)

Caption font is now keyword-only via PyArg_ParseTupleAndKeywords '$'
separator. Positional order is now (pos, text); font, fill_color,
outline_color, etc. must be passed as kwargs.

Test impact: test_constructor_comprehensive.py:77 uses positional
Caption(pos, None, text), which now fails. Other test sites use
kwargs and survive.
This commit is contained in:
John McCardle 2026-04-18 13:29:58 -04:00
commit 157ba9d011
8 changed files with 151 additions and 28 deletions

View file

@ -814,14 +814,24 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
};
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "name", "grid_size", NULL};
// 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "grid_size", "grid", NULL};
int z_index = -1;
const char* name_str = nullptr;
PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast<char**>(kwlist),
&z_index, &name_str, &grid_size_obj)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOO", const_cast<char**>(kwlist),
&name_str, &z_index, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
@ -851,6 +861,15 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
}
self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0;
}
@ -1928,15 +1947,25 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
};
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL};
// 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "texture", "grid_size", "grid", NULL};
int z_index = -1;
const char* name_str = nullptr;
PyObject* texture_obj = nullptr;
PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast<char**>(kwlist),
&z_index, &name_str, &texture_obj, &grid_size_obj)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOOO", const_cast<char**>(kwlist),
&name_str, &z_index, &texture_obj, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
@ -1987,6 +2016,15 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
}
self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0;
}

View file

@ -2,6 +2,10 @@
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
#include <sstream>
@ -556,19 +560,22 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = {
"center", "radius", "start_angle", "end_angle", "color", "thickness",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfffO", const_cast<char**>(kwlist),
&center_obj, &radius, &start_angle, &end_angle,
&color_obj, &thickness,
&click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -639,5 +646,8 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -158,6 +158,33 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
} \
} while (0)
// Macro for auto-attaching a newly constructed UI drawable to a parent's children.
// Usage: UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
// parent_obj must be a Frame, Scene, or Grid (anything with a .children UICollection).
// Returns -1 on error (suitable for use in tp_init functions). No-op when parent_obj is null/None.
#define UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self) \
do { \
if ((parent_obj) && (parent_obj) != Py_None) { \
if (!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIFrameType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PySceneType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridViewType)) { \
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Scene, or Grid"); \
return -1; \
} \
PyObject* _children = PyObject_GetAttrString((parent_obj), "children"); \
if (!_children) { \
return -1; \
} \
PyObject* _result = PyObject_CallMethod(_children, "append", "O", (PyObject*)(self)); \
Py_DECREF(_children); \
if (!_result) { \
return -1; \
} \
Py_DECREF(_result); \
} \
} while (0)
// Property getters/setters for visible and opacity
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)

View file

@ -7,6 +7,9 @@
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
// UIDrawable methods now in UIBase.h
#include <algorithm>
@ -448,23 +451,27 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args
// Keywords list: pos and text are positional-or-keyword. Everything after
// the '$' separator (font and friends) is keyword-only.
static const char* kwlist[] = {
"pos", "font", "text", // Positional args (as per spec)
// Keyword-only args
"fill_color", "outline_color", "outline", "font_size", "on_click",
"pos", "text",
// Keyword-only args follow:
"font", "fill_color", "outline_color", "outline", "font_size", "on_click",
"visible", "opacity", "z_index", "name", "x", "y",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast<char**>(kwlist),
&pos_obj, &font, &text, // Positional
&fill_color, &outline_color, &outline, &font_size, &click_handler,
// '$' marker makes all following args keyword-only (Python 3.3+ format extension).
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz$OOOffOifizffOfffO", const_cast<char**>(kwlist),
&pos_obj, &text, // pos+text are positional-or-keyword
&font, &fill_color, &outline_color, &outline, &font_size, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -597,6 +604,9 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -5,6 +5,10 @@
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
UICircle::UICircle()
@ -479,10 +483,13 @@ PyObject* UICircle::repr(PyUICircleObject* self) {
}
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// 1.0 API freeze: positional order is now (center, radius, ...). The old
// (radius, center) ordering is no longer supported.
static const char* kwlist[] = {
"radius", "center", "fill_color", "outline_color", "outline",
"center", "radius", "fill_color", "outline_color", "outline",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin", NULL
"align", "margin", "horiz_margin", "vert_margin",
"parent", NULL
};
float radius = 10.0f;
@ -501,11 +508,13 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = NULL; // Auto-attach parent (Frame, Scene, or Grid)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist,
&radius, &center_obj, &fill_color_obj, &outline_color_obj, &outline,
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfOOfOpfisOfffO", (char**)kwlist,
&center_obj, &radius, &fill_color_obj, &outline_color_obj, &outline,
&click_obj, &visible, &opacity_val, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -597,5 +606,8 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -5,6 +5,7 @@
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
@ -601,6 +602,7 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
@ -609,15 +611,17 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
"fill_color", "outline_color", "outline", "children", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfffO", const_cast<char**>(kwlist),
&pos_obj, &size_obj, // Positional
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -798,6 +802,9 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -5,6 +5,10 @@
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
UILine::UILine()
@ -565,18 +569,21 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = {
"start", "end", "thickness", "color",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfffO", const_cast<char**>(kwlist),
&start_obj, &end_obj, &thickness, &color_obj,
&click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -663,5 +670,8 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -3,6 +3,9 @@
#include "PyVector.h"
#include "PythonObjectCache.h"
#include "UIFrame.h" // #144: For snapshot= parameter
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
@ -476,6 +479,7 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
@ -484,15 +488,17 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
"scale", "scale_x", "scale_y", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "snapshot",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfffO", const_cast<char**>(kwlist),
&pos_obj, &texture, &sprite_index, // Positional
&scale, &scale_x, &scale_y, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &snapshot,
&align_obj, &margin, &horiz_margin, &vert_margin)) {
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1;
}
@ -627,6 +633,9 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}