PyAPI: step support for mathutils slicing

Extend slice get/set on Vector, Color, Euler, Quaternion, Matrix and
MatrixAccess to support extended slices with arbitrary step
(`obj[::2]`, `obj[::-1]`, etc.), matching Python list semantics,
with the exception that resizing isn't supported.

This also add support for MatrixAccess slice assignment
(mat.row[i:j] = ..., mat.col[i:j] = ...) previously raised TypeError.

Tests extended with checks that steps work as intended.

Ref !158183
This commit is contained in:
Campbell Barton 2026-05-06 04:24:38 +00:00
parent 4a741dfa2e
commit 141af3d117
7 changed files with 492 additions and 329 deletions

View file

@ -235,6 +235,21 @@ void _BaseMathObject_RaiseNotFrozenExc(const BaseMathObject *self);
PyObject *value,
const char *error_prefix);
/**
* Returns true when a slice does *not* address every element of an `array_num`.
*/
[[nodiscard]] inline bool mathutils_slice_is_subset(Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length,
Py_ssize_t array_num)
{
return !((slice_length == array_num) &&
/* All forward `[:]`. */
((start == 0 && step == 1) ||
/* All reverse `[::-1]`. */
(start == array_num - 1 && step == -1)));
}
/**
* helper function that returns a Python `__hash__`.
*

View file

@ -570,63 +570,57 @@ static int Color_ass_item(ColorObject *self, Py_ssize_t i, PyObject *value)
return 0;
}
/** Sequence slice accessor (get): `x = object[i:j]`. */
static PyObject *Color_slice(ColorObject *self, int begin, int end)
/** Sequence slice accessor (get): `x = object[i:j]` / `object[i:j:step]`. */
static PyObject *Color_slice(ColorObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
int count;
if (BaseMath_ReadCallback(self) == -1) {
return nullptr;
}
CLAMP(begin, 0, COLOR_SIZE);
if (end < 0) {
end = (COLOR_SIZE + 1) + end;
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < COLOR_SIZE);
PyTuple_SET_ITEM(tuple, i, PyFloat_FromDouble(self->col[index]));
}
CLAMP(end, 0, COLOR_SIZE);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyTuple_SET_ITEM(tuple, count - begin, PyFloat_FromDouble(self->col[count]));
}
return tuple;
}
/** Sequence slice accessor (set): `object[i:j] = x`. */
static int Color_ass_slice(ColorObject *self, int begin, int end, PyObject *seq)
/**
* Sequence slice accessor (set): `object[i:j] = x` / `object[i:j:step] = x`.
* Length of `seq` must equal `slice_length`
* (Python list semantics: extended slice assignment cannot resize).
*/
static int Color_ass_slice(
ColorObject *self, Py_ssize_t start, Py_ssize_t step, Py_ssize_t slice_length, PyObject *seq)
{
int i, size;
float col[COLOR_SIZE];
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, COLOR_SIZE)) {
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(self) == -1) {
return -1;
}
}
CLAMP(begin, 0, COLOR_SIZE);
if (end < 0) {
end = (COLOR_SIZE + 1) + end;
}
CLAMP(end, 0, COLOR_SIZE);
begin = std::min(begin, end);
if ((size = mathutils_array_parse(col, 0, COLOR_SIZE, seq, "mathutils.Color[begin:end] = []")) ==
-1)
if (mathutils_array_parse(
col, slice_length, slice_length, seq, "mathutils.Color[slice] = seq") == -1)
{
return -1;
}
if (size != (end - begin)) {
PyErr_SetString(PyExc_ValueError,
"color[begin:end] = []: "
"size mismatch in slice assignment");
return -1;
}
for (i = 0; i < size; i++) {
self->col[begin + i] = col[i];
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < COLOR_SIZE);
self->col[index] = col[i];
}
(void)BaseMath_WriteCallback(self);
@ -648,21 +642,13 @@ static PyObject *Color_subscript(ColorObject *self, PyObject *item)
return Color_item(self, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, COLOR_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, COLOR_SIZE, &start, &stop, &step, &slice_length) < 0) {
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return Color_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with color");
return nullptr;
return Color_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -684,18 +670,13 @@ static int Color_ass_subscript(ColorObject *self, PyObject *item, PyObject *valu
return Color_ass_item(self, i, value);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, COLOR_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, COLOR_SIZE, &start, &stop, &step, &slice_length) < 0) {
return -1;
}
if (step == 1) {
return Color_ass_slice(self, start, stop, value);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with color");
return -1;
return Color_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(

View file

@ -598,63 +598,57 @@ static int Euler_ass_item(EulerObject *self, Py_ssize_t i, PyObject *value)
return 0;
}
/** Sequence slice accessor (get): `x = object[i:j]`. */
static PyObject *Euler_slice(EulerObject *self, int begin, int end)
/** Sequence slice accessor (get): `x = object[i:j]` / `object[i:j:step]`. */
static PyObject *Euler_slice(EulerObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
int count;
if (BaseMath_ReadCallback(self) == -1) {
return nullptr;
}
CLAMP(begin, 0, EULER_SIZE);
if (end < 0) {
end = (EULER_SIZE + 1) + end;
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < EULER_SIZE);
PyTuple_SET_ITEM(tuple, i, PyFloat_FromDouble(self->eul[index]));
}
CLAMP(end, 0, EULER_SIZE);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyTuple_SET_ITEM(tuple, count - begin, PyFloat_FromDouble(self->eul[count]));
}
return tuple;
}
/** Sequence slice accessor (set): `object[i:j] = x`. */
static int Euler_ass_slice(EulerObject *self, int begin, int end, PyObject *seq)
/**
* Sequence slice accessor (set): `object[i:j] = x` / `object[i:j:step] = x`.
* Length of `seq` must equal `slice_length`
* (Python list semantics: extended slice assignment cannot resize).
*/
static int Euler_ass_slice(
EulerObject *self, Py_ssize_t start, Py_ssize_t step, Py_ssize_t slice_length, PyObject *seq)
{
int i, size;
float eul[EULER_SIZE];
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, EULER_SIZE)) {
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(self) == -1) {
return -1;
}
}
CLAMP(begin, 0, EULER_SIZE);
if (end < 0) {
end = (EULER_SIZE + 1) + end;
}
CLAMP(end, 0, EULER_SIZE);
begin = std::min(begin, end);
if ((size = mathutils_array_parse(eul, 0, EULER_SIZE, seq, "mathutils.Euler[begin:end] = []")) ==
-1)
if (mathutils_array_parse(
eul, slice_length, slice_length, seq, "mathutils.Euler[slice] = seq") == -1)
{
return -1;
}
if (size != (end - begin)) {
PyErr_SetString(PyExc_ValueError,
"euler[begin:end] = []: "
"size mismatch in slice assignment");
return -1;
}
for (i = 0; i < size; i++) {
self->eul[begin + i] = eul[i];
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < EULER_SIZE);
self->eul[index] = eul[i];
}
(void)BaseMath_WriteCallback(self);
@ -676,21 +670,13 @@ static PyObject *Euler_subscript(EulerObject *self, PyObject *item)
return Euler_item(self, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, EULER_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, EULER_SIZE, &start, &stop, &step, &slice_length) < 0) {
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return Euler_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with eulers");
return nullptr;
return Euler_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -712,18 +698,13 @@ static int Euler_ass_subscript(EulerObject *self, PyObject *item, PyObject *valu
return Euler_ass_item(self, i, value);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, EULER_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, EULER_SIZE, &start, &stop, &step, &slice_length) < 0) {
return -1;
}
if (step == 1) {
return Euler_ass_slice(self, start, stop, value);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with euler");
return -1;
return Euler_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(

View file

@ -35,7 +35,11 @@ enum eMatrixAccess_t {
static PyObject *Matrix_copy_notest(MatrixObject *self, const float *matrix);
static PyObject *Matrix_copy(MatrixObject *self);
static PyObject *Matrix_deepcopy(MatrixObject *self, PyObject *args);
static int Matrix_ass_slice(MatrixObject *self, int begin, int end, PyObject *value);
static int Matrix_ass_slice(MatrixObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length,
PyObject *value);
static PyObject *matrix__apply_to_copy(PyObject *(*matrix_func)(MatrixObject *),
MatrixObject *self);
static PyObject *MatrixAccess_CreatePyObject(MatrixObject *matrix, const eMatrixAccess_t type);
@ -634,7 +638,8 @@ static PyObject *Matrix_vectorcall(PyObject *type,
/* Sane row & col size, new matrix and assign as slice. */
PyObject *matrix = Matrix_CreatePyObject(
nullptr, col_num, row_num, reinterpret_cast<PyTypeObject *>(type));
if (Matrix_ass_slice(reinterpret_cast<MatrixObject *>(matrix), 0, INT_MAX, arg) == 0) {
if (Matrix_ass_slice(reinterpret_cast<MatrixObject *>(matrix), 0, 1, row_num, arg) == 0)
{
return matrix;
}
/* matrix ok, slice assignment not */
@ -2680,88 +2685,98 @@ static int Matrix_ass_item_col(MatrixObject *self, int col, PyObject *value)
return 0;
}
/** Sequence slice accessor (get): `x = object[i:j]`. */
static PyObject *Matrix_slice(MatrixObject *self, int begin, int end)
/**
* Sequence slice accessor (get): `x = object[i:j]` / `object[i:j:step]`.
* \return a tuple of `Vector` row wrappers.
*/
static PyObject *Matrix_slice(MatrixObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
int count;
if (BaseMath_ReadCallback(self) == -1) {
return nullptr;
}
CLAMP(begin, 0, self->row_num);
CLAMP(end, 0, self->row_num);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < self->row_num);
PyTuple_SET_ITEM(tuple,
count - begin,
i,
Vector_CreatePyObject_cb(
(PyObject *)self, self->col_num, mathutils_matrix_row_cb_index, count));
(PyObject *)self, self->col_num, mathutils_matrix_row_cb_index, index));
}
return tuple;
}
/** Sequence slice accessor (set): `object[i:j] = x`. */
static int Matrix_ass_slice(MatrixObject *self, int begin, int end, PyObject *value)
/**
* Sequence slice accessor (set): `object[i:j] = x` / `object[i:j:step] = x`.
* `value` must be a sequence of `slice_length` row-sequences, each with
* `col_num` floats (Python list semantics: extended slice cannot resize).
*/
static int Matrix_ass_slice(MatrixObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length,
PyObject *value)
{
PyObject *value_fast;
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, self->row_num)) {
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(self) == -1) {
return -1;
}
}
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
PyObject *value_fast = PySequence_Fast(value, "matrix[slice] = value");
if (value_fast == nullptr) {
return -1;
}
CLAMP(begin, 0, self->row_num);
CLAMP(end, 0, self->row_num);
begin = std::min(begin, end);
/* non list/tuple cases */
if (!(value_fast = PySequence_Fast(value, "matrix[begin:end] = value"))) {
/* PySequence_Fast sets the error */
return -1;
}
PyObject **value_fast_items = PySequence_Fast_ITEMS(value_fast);
const int size = end - begin;
int row, col;
float mat[MATRIX_MAX_DIM * MATRIX_MAX_DIM];
float vec[4];
if (PySequence_Fast_GET_SIZE(value_fast) != size) {
const Py_ssize_t value_num = PySequence_Fast_GET_SIZE(value_fast);
if (value_num != slice_length) {
Py_DECREF(value_fast);
PyErr_SetString(PyExc_ValueError,
"matrix[begin:end] = []: "
"size mismatch in slice assignment");
PyErr_Format(PyExc_ValueError,
"matrix[slice] = value: sequence size is %zd, expected %zd",
value_num,
slice_length);
return -1;
}
memcpy(mat, self->matrix, self->col_num * self->row_num * sizeof(float));
/* Stage parsed rows so a mid-sequence parse failure cannot leave the matrix
* half-written: only commit after every row has parsed successfully. */
BLI_assert(slice_length <= MATRIX_MAX_DIM);
BLI_assert(self->col_num <= MATRIX_MAX_DIM);
float staging[MATRIX_MAX_DIM][MATRIX_MAX_DIM];
PyObject **value_fast_items = PySequence_Fast_ITEMS(value_fast);
/* parse sub items */
for (row = begin; row < end; row++) {
/* parse each sub sequence */
PyObject *item = value_fast_items[row - begin];
if (mathutils_array_parse(
vec, self->col_num, self->col_num, item, "matrix[begin:end] = value assignment") == -1)
for (Py_ssize_t i = 0; i < slice_length; i++) {
if (mathutils_array_parse(staging[i],
self->col_num,
self->col_num,
value_fast_items[i],
"matrix[slice] = value") == -1)
{
Py_DECREF(value_fast);
return -1;
}
for (col = 0; col < self->col_num; col++) {
mat[col * self->row_num + row] = vec[col];
}
}
Py_DECREF(value_fast);
/* Parsed well - now set in matrix. */
memcpy(self->matrix, mat, self->col_num * self->row_num * sizeof(float));
/* Commit. */
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < self->row_num);
for (int col = 0; col < self->col_num; col++) {
MATRIX_ITEM(self, index, col) = staging[i][col];
}
}
(void)BaseMath_WriteCallback(self);
return 0;
@ -2782,21 +2797,13 @@ static PyObject *Matrix_subscript(MatrixObject *self, PyObject *item)
return Matrix_item_row(self, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, self->row_num, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, self->row_num, &start, &stop, &step, &slice_length) < 0) {
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return Matrix_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with matrices");
return nullptr;
return Matrix_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -2818,18 +2825,13 @@ static int Matrix_ass_subscript(MatrixObject *self, PyObject *item, PyObject *va
return Matrix_ass_item_row(self, i, value);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, self->row_num, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, self->row_num, &start, &stop, &step, &slice_length) < 0) {
return -1;
}
if (step == 1) {
return Matrix_ass_slice(self, start, stop, value);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with matrices");
return -1;
return Matrix_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(
@ -3974,12 +3976,15 @@ static Py_ssize_t MatrixAccess_len(MatrixAccessObject *self)
return (self->type == MAT_ACCESS_ROW) ? self->matrix_user->row_num : self->matrix_user->col_num;
}
static PyObject *MatrixAccess_slice(MatrixAccessObject *self, Py_ssize_t begin, Py_ssize_t end)
/**
* Slice get: `mat.row[i:j]` / `mat.row[i:j:step]` (or `.col`).
* \return a tuple of `Vector` row/column wrappers.
*/
static PyObject *MatrixAccess_slice(MatrixAccessObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
Py_ssize_t count;
/* row/col access */
MatrixObject *matrix_user = self->matrix_user;
int matrix_access_len;
PyObject *(*Matrix_item_new)(MatrixObject *, Py_ssize_t);
@ -3993,21 +3998,111 @@ static PyObject *MatrixAccess_slice(MatrixAccessObject *self, Py_ssize_t begin,
Matrix_item_new = Matrix_item_col;
}
CLAMP(begin, 0, matrix_access_len);
if (end < 0) {
end = (matrix_access_len + 1) + end;
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < matrix_access_len);
PyTuple_SET_ITEM(tuple, i, Matrix_item_new(matrix_user, index));
}
CLAMP(end, 0, matrix_access_len);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyTuple_SET_ITEM(tuple, count - begin, Matrix_item_new(matrix_user, count));
}
return tuple;
}
/**
* Slice set: `mat.row[i:j] = (vec, ...)` / `mat.row[i:j:step] = (...)` (or `.col`).
* Length of `value` must equal `slice_length` and each item must be a sequence
* of `axis_size` floats (Python list semantics: extended slice cannot resize).
*/
static int MatrixAccess_ass_slice(MatrixAccessObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length,
PyObject *value)
{
MatrixObject *matrix_user = self->matrix_user;
int matrix_access_len;
int axis_size;
if (self->type == MAT_ACCESS_ROW) {
matrix_access_len = matrix_user->row_num;
axis_size = matrix_user->col_num;
}
else { /* MAT_ACCESS_COL */
matrix_access_len = matrix_user->col_num;
axis_size = matrix_user->row_num;
}
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, matrix_access_len)) {
if (BaseMath_ReadCallback_ForWrite(matrix_user) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(matrix_user) == -1) {
return -1;
}
}
PyObject *value_fast = PySequence_Fast(value, "matrix.row/col[slice] = value");
if (value_fast == nullptr) {
return -1;
}
const Py_ssize_t value_num = PySequence_Fast_GET_SIZE(value_fast);
if (value_num != slice_length) {
Py_DECREF(value_fast);
PyErr_Format(PyExc_ValueError,
"matrix.%s[slice] = value: sequence size is %zd, expected %zd",
(self->type == MAT_ACCESS_ROW) ? "row" : "col",
value_num,
slice_length);
return -1;
}
/* Stage parsed rows/cols so a mid-sequence parse failure cannot leave the
* matrix half-written: only commit after every item has parsed successfully. */
BLI_assert(slice_length <= MATRIX_MAX_DIM);
BLI_assert(axis_size <= MATRIX_MAX_DIM);
float staging[MATRIX_MAX_DIM][MATRIX_MAX_DIM];
PyObject **value_fast_items = PySequence_Fast_ITEMS(value_fast);
for (Py_ssize_t i = 0; i < slice_length; i++) {
if (mathutils_array_parse(staging[i],
axis_size,
axis_size,
value_fast_items[i],
"matrix.row/col[slice] = value") == -1)
{
Py_DECREF(value_fast);
return -1;
}
}
Py_DECREF(value_fast);
/* Commit. */
Py_ssize_t index = start;
if (self->type == MAT_ACCESS_ROW) {
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < matrix_access_len);
for (int col = 0; col < axis_size; col++) {
MATRIX_ITEM(matrix_user, index, col) = staging[i][col];
}
}
}
else { /* MAT_ACCESS_COL */
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < matrix_access_len);
for (int row = 0; row < axis_size; row++) {
MATRIX_ITEM(matrix_user, row, index) = staging[i][row];
}
}
}
(void)BaseMath_WriteCallback(matrix_user);
return 0;
}
static PyObject *MatrixAccess_subscript(MatrixAccessObject *self, PyObject *item)
{
MatrixObject *matrix_user = self->matrix_user;
@ -4031,22 +4126,15 @@ static PyObject *MatrixAccess_subscript(MatrixAccessObject *self, PyObject *item
return Matrix_item_col(matrix_user, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, MatrixAccess_len(self), &start, &stop, &step, &slicelength) < 0)
if (PySlice_GetIndicesEx(item, MatrixAccess_len(self), &start, &stop, &step, &slice_length) <
0)
{
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return MatrixAccess_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with matrix accessors");
return nullptr;
return MatrixAccess_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -4076,7 +4164,17 @@ static int MatrixAccess_ass_subscript(MatrixAccessObject *self, PyObject *item,
}
return Matrix_ass_item_col(matrix_user, i, value);
}
/* TODO: slice. */
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, MatrixAccess_len(self), &start, &stop, &step, &slice_length) <
0)
{
return -1;
}
return MatrixAccess_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(
PyExc_TypeError, "matrix indices must be integers, not %.200s", Py_TYPE(item)->tp_name);
@ -4088,7 +4186,7 @@ static PyObject *MatrixAccess_iter(MatrixAccessObject *self)
/* Try get values from a collection. */
PyObject *ret;
PyObject *iter = nullptr;
ret = MatrixAccess_slice(self, 0, MATRIX_MAX_DIM);
ret = MatrixAccess_slice(self, 0, 1, MatrixAccess_len(self));
/* We know this is a tuple so no need to #PyIter_Check
* otherwise it could be nullptr (unlikely) if conversion failed. */

View file

@ -1084,64 +1084,60 @@ static int Quaternion_ass_item(QuaternionObject *self, Py_ssize_t i, PyObject *o
return 0;
}
/** Sequence slice accessor (get): `x = object[i:j]`. */
static PyObject *Quaternion_slice(QuaternionObject *self, int begin, int end)
/** Sequence slice accessor (get): `x = object[i:j]` / `object[i:j:step]`. */
static PyObject *Quaternion_slice(QuaternionObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
int count;
if (BaseMath_ReadCallback(self) == -1) {
return nullptr;
}
CLAMP(begin, 0, QUAT_SIZE);
if (end < 0) {
end = (QUAT_SIZE + 1) + end;
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < QUAT_SIZE);
PyTuple_SET_ITEM(tuple, i, PyFloat_FromDouble(self->quat[index]));
}
CLAMP(end, 0, QUAT_SIZE);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyTuple_SET_ITEM(tuple, count - begin, PyFloat_FromDouble(self->quat[count]));
}
return tuple;
}
/** Sequence slice accessor (set): `object[i:j] = x`. */
static int Quaternion_ass_slice(QuaternionObject *self, int begin, int end, PyObject *seq)
/**
* Sequence slice accessor (set): `object[i:j] = x` / `object[i:j:step] = x`.
* Length of `seq` must equal `slice_length`
* (Python list semantics: extended slice assignment cannot resize).
*/
static int Quaternion_ass_slice(QuaternionObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length,
PyObject *seq)
{
int i, size;
float quat[QUAT_SIZE];
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, QUAT_SIZE)) {
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(self) == -1) {
return -1;
}
}
CLAMP(begin, 0, QUAT_SIZE);
if (end < 0) {
end = (QUAT_SIZE + 1) + end;
}
CLAMP(end, 0, QUAT_SIZE);
begin = std::min(begin, end);
if ((size = mathutils_array_parse(
quat, 0, QUAT_SIZE, seq, "mathutils.Quaternion[begin:end] = []")) == -1)
if (mathutils_array_parse(
quat, slice_length, slice_length, seq, "mathutils.Quaternion[slice] = seq") == -1)
{
return -1;
}
if (size != (end - begin)) {
PyErr_SetString(PyExc_ValueError,
"quaternion[begin:end] = []: "
"size mismatch in slice assignment");
return -1;
}
/* Parsed well, now set in vector. */
for (i = 0; i < size; i++) {
self->quat[begin + i] = quat[i];
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < QUAT_SIZE);
self->quat[index] = quat[i];
}
(void)BaseMath_WriteCallback(self);
@ -1163,21 +1159,13 @@ static PyObject *Quaternion_subscript(QuaternionObject *self, PyObject *item)
return Quaternion_item(self, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, QUAT_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, QUAT_SIZE, &start, &stop, &step, &slice_length) < 0) {
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return Quaternion_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with quaternions");
return nullptr;
return Quaternion_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -1199,18 +1187,13 @@ static int Quaternion_ass_subscript(QuaternionObject *self, PyObject *item, PyOb
return Quaternion_ass_item(self, i, value);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, QUAT_SIZE, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, QUAT_SIZE, &start, &stop, &step, &slice_length) < 0) {
return -1;
}
if (step == 1) {
return Quaternion_ass_slice(self, start, stop, value);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with quaternion");
return -1;
return Quaternion_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(

View file

@ -1901,48 +1901,49 @@ static int Vector_ass_item(VectorObject *self, Py_ssize_t i, PyObject *value)
return vector_ass_item_internal(self, i, value, false);
}
/** Sequence slice accessor (get): `x = object[i:j]`. */
static PyObject *Vector_slice(VectorObject *self, int begin, int end)
/** Sequence slice accessor (get): `x = object[i:j]` / `object[i:j:step]`. */
static PyObject *Vector_slice(VectorObject *self,
Py_ssize_t start,
Py_ssize_t step,
Py_ssize_t slice_length)
{
PyObject *tuple;
int count;
if (BaseMath_ReadCallback(self) == -1) {
return nullptr;
}
CLAMP(begin, 0, self->vec_num);
if (end < 0) {
end = self->vec_num + end + 1;
PyObject *tuple = PyTuple_New(slice_length);
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < self->vec_num);
PyTuple_SET_ITEM(tuple, i, PyFloat_FromDouble(self->vec[index]));
}
CLAMP(end, 0, self->vec_num);
begin = std::min(begin, end);
tuple = PyTuple_New(end - begin);
for (count = begin; count < end; count++) {
PyTuple_SET_ITEM(tuple, count - begin, PyFloat_FromDouble(self->vec[count]));
}
return tuple;
}
/** Sequence slice accessor (set): `object[i:j] = x`. */
static int Vector_ass_slice(VectorObject *self, int begin, int end, PyObject *seq)
/**
* Sequence slice accessor (set): `object[i:j] = x` / `object[i:j:step] = x`.
* Length of `seq` must equal `slice_length`
* (Python list semantics: extended slice assignment cannot resize).
*/
static int Vector_ass_slice(
VectorObject *self, Py_ssize_t start, Py_ssize_t step, Py_ssize_t slice_length, PyObject *seq)
{
int vec_num = 0;
float *vec = nullptr;
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
/* Subset writes merge into existing values, so sync the source first. */
if (mathutils_slice_is_subset(start, step, slice_length, self->vec_num)) {
if (BaseMath_ReadCallback_ForWrite(self) == -1) {
return -1;
}
}
else {
if (BaseMath_Prepare_ForWrite(self) == -1) {
return -1;
}
}
CLAMP(begin, 0, self->vec_num);
CLAMP(end, 0, self->vec_num);
begin = std::min(begin, end);
vec_num = (end - begin);
const int parsed_size = mathutils_array_parse_alloc(
&vec, vec_num, seq, "vector[begin:end] = [...]");
&vec, int(slice_length), seq, "vector[slice] = seq");
if (parsed_size == -1) {
return -1;
}
@ -1951,17 +1952,20 @@ static int Vector_ass_slice(VectorObject *self, int begin, int end, PyObject *se
* Rely on explicit use of the `.resize()` method because (unlike lists),
* these are more typically fixed size collections.
* Resizing is more likely to be a mistake as it isn't a common operation. */
if (parsed_size != vec_num) {
if (parsed_size != slice_length) {
PyMem_Free(vec);
PyErr_Format(PyExc_ValueError,
"vector[begin:end] = [...]: sequence size is %d, expected %d",
"vector[slice] = seq: sequence size is %d, expected %d",
parsed_size,
vec_num);
int(slice_length));
return -1;
}
/* Parsed well - now set in vector. */
memcpy(self->vec + begin, vec, vec_num * sizeof(float));
Py_ssize_t index = start;
for (Py_ssize_t i = 0; i < slice_length; i++, index += step) {
BLI_assert(index >= 0 && index < self->vec_num);
self->vec[index] = vec[i];
}
PyMem_Free(vec);
@ -1987,21 +1991,13 @@ static PyObject *Vector_subscript(VectorObject *self, PyObject *item)
return Vector_item(self, i);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, self->vec_num, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, self->vec_num, &start, &stop, &step, &slice_length) < 0) {
return nullptr;
}
if (slicelength <= 0) {
return PyTuple_New(0);
}
if (step == 1) {
return Vector_slice(self, start, stop);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with vectors");
return nullptr;
return Vector_slice(self, start, step, slice_length);
}
PyErr_Format(
@ -2023,18 +2019,13 @@ static int Vector_ass_subscript(VectorObject *self, PyObject *item, PyObject *va
return Vector_ass_item(self, i, value);
}
if (PySlice_Check(item)) {
Py_ssize_t start, stop, step, slicelength;
Py_ssize_t start, stop, step, slice_length;
if (PySlice_GetIndicesEx(item, self->vec_num, &start, &stop, &step, &slicelength) < 0) {
if (PySlice_GetIndicesEx(item, self->vec_num, &start, &stop, &step, &slice_length) < 0) {
return -1;
}
if (step == 1) {
return Vector_ass_slice(self, start, stop, value);
}
PyErr_SetString(PyExc_IndexError, "slice steps not supported with vectors");
return -1;
return Vector_ass_slice(self, start, step, slice_length, value);
}
PyErr_Format(

View file

@ -88,11 +88,17 @@ class GenericSliceMixIn:
base = tuple(float(i + 1) for i in range(self.generic_len))
obj = self.generic_make(base)
self.assertIsInstance(obj[:], tuple)
# Step 1.
self.assertEqual(obj[:], base)
self.assertEqual(obj[1:], base[1:])
self.assertEqual(obj[:-1], base[:-1])
self.assertEqual(obj[-1:], base[-1:])
self.assertEqual(obj[1:1], ())
# Stepped.
self.assertEqual(obj[::2], base[::2])
self.assertEqual(obj[::-1], base[::-1])
self.assertEqual(obj[::-2], base[::-2])
self.assertEqual(obj[1:1:-1], ())
def test_slice_set(self):
base = tuple(float(i + 1) for i in range(self.generic_len))
@ -101,11 +107,23 @@ class GenericSliceMixIn:
new_full = tuple(10.0 * (i + 1) for i in range(self.generic_len))
obj[:] = new_full
self.assertEqual(tuple(obj), new_full)
# Reverse full overwrite (fast path).
obj[::-1] = base
self.assertEqual(tuple(obj), base[::-1])
# Partial step-1 (slow path).
obj = self.generic_make(base)
new_tail = tuple(50.0 * (i + 1) for i in range(self.generic_len - 1))
obj[1:] = new_tail
self.assertEqual(tuple(obj), (base[0],) + new_tail)
# Stepped partial (slow path).
obj = self.generic_make(base)
every_other = base[::2]
new_every_other = tuple(99.0 + i for i in range(len(every_other)))
obj[::2] = new_every_other
expected = list(base)
for i, v in zip(range(0, self.generic_len, 2), new_every_other):
expected[i] = v
self.assertEqual(tuple(obj), tuple(expected))
# Empty extended slice with empty seq is a no-op.
obj = self.generic_make(base)
obj[5:2:1] = ()
@ -116,6 +134,8 @@ class GenericSliceMixIn:
obj = self.generic_make(base)
with self.assertRaises(ValueError):
obj[:] = base[:-1]
with self.assertRaises(ValueError):
obj[::2] = base
with self.assertRaises(ValueError):
obj[5:2:1] = (1.0,)
self.assertEqual(tuple(obj), base)
@ -124,10 +144,22 @@ class GenericSliceMixIn:
base = tuple(float(i + 1) for i in range(self.generic_len))
obj = self.generic_make(base)
obj.freeze()
# Fast path (full overwrite, frozen check only).
with self.assertRaises(TypeError):
obj[:] = base
# Slow path (partial, frozen check inside `ReadCallback_ForWrite`).
with self.assertRaises(TypeError):
obj[::2] = base[::2]
self.assertEqual(tuple(obj), base)
def test_slice_set_self_aliased_reverse(self):
# `obj[::-1] = obj` must reverse in place. Exercises the parse-through-seq
# sync path that silently undoes the `is_subset=false` fast-path skip.
base = tuple(float(i + 1) for i in range(self.generic_len))
obj = self.generic_make(base)
obj[::-1] = obj
self.assertEqual(tuple(obj), base[::-1])
def test_slice_set_self_aliased_full(self):
# `obj[:] = obj` writes original values back; effectively a no-op.
base = tuple(float(i + 1) for i in range(self.generic_len))
@ -154,6 +186,8 @@ class GenericSliceMixIn:
original_id = id(obj)
obj[:] = base
self.assertEqual(id(obj), original_id)
obj[::2] = base[::2]
self.assertEqual(id(obj), original_id)
def test_slice_set_seq_longer_than_slice(self):
# Assigning a sequence longer than the slice must raise ValueError,
@ -494,11 +528,59 @@ class MatrixSliceMixIn:
base = tuple(self._make_value(i + 1) for i in range(n))
_mat, obj = self._make_obj(base)
self.assertIsInstance(obj[:], tuple)
# Step 1.
self.assertEqual(obj[:], base)
self.assertEqual(obj[1:], base[1:])
self.assertEqual(obj[:-1], base[:-1])
self.assertEqual(obj[-1:], base[-1:])
self.assertEqual(obj[1:1], ())
# Stepped.
self.assertEqual(obj[::2], base[::2])
self.assertEqual(obj[::-1], base[::-1])
self.assertEqual(obj[::-2], base[::-2])
self.assertEqual(obj[1:1:-1], ())
def test_slice_set(self):
n = self.matrix_size
base = tuple(self._make_value(i + 1) for i in range(n))
# Forward full overwrite (fast path).
_mat, obj = self._make_obj(base)
new_full = tuple(self._make_value(10 * (i + 1)) for i in range(n))
obj[:] = new_full
self.assertEqual(tuple(obj), new_full)
# Reverse full overwrite (fast path).
obj[::-1] = base
self.assertEqual(tuple(obj), base[::-1])
# Partial step-1 (slow path).
_mat, obj = self._make_obj(base)
new_tail = tuple(self._make_value(50 * (i + 1)) for i in range(n - 1))
obj[1:] = new_tail
self.assertEqual(tuple(obj), (base[0],) + new_tail)
# Stepped partial (slow path).
_mat, obj = self._make_obj(base)
every_other = base[::2]
new_every_other = tuple(self._make_value(99 + i) for i in range(len(every_other)))
obj[::2] = new_every_other
expected = list(base)
for i, v in zip(range(0, n, 2), new_every_other):
expected[i] = v
self.assertEqual(tuple(obj), tuple(expected))
# Empty extended slice with empty seq is a no-op.
_mat, obj = self._make_obj(base)
obj[5:2:1] = ()
self.assertEqual(tuple(obj), base)
def test_slice_set_length_mismatch(self):
n = self.matrix_size
base = tuple(self._make_value(i + 1) for i in range(n))
_mat, obj = self._make_obj(base)
with self.assertRaises(ValueError):
obj[:] = base[:-1]
with self.assertRaises(ValueError):
obj[::2] = base
with self.assertRaises(ValueError):
obj[5:2:1] = (self._make_value(1),)
self.assertEqual(tuple(obj), base)
def test_slice_set_frozen(self):
n = self.matrix_size
@ -506,10 +588,42 @@ class MatrixSliceMixIn:
mat, obj = self._make_obj(base)
# `MatrixAccess` has no `freeze()`; freezing the matrix is enough either way.
mat.freeze()
# Fast path (full overwrite, frozen check only).
with self.assertRaises(TypeError):
obj[:] = base
# Slow path (partial, frozen check inside ReadCallback_ForWrite).
with self.assertRaises(TypeError):
obj[::2] = base[::2]
self.assertEqual(tuple(obj), base)
def test_slice_set_self_aliased_reverse(self):
# `obj[::-1] = obj` reverses rows/cols in place.
# Atomicity comes from staging all parsed rows/cols before assigning.
n = self.matrix_size
base = tuple(self._make_value(i + 1) for i in range(n))
_mat, obj = self._make_obj(base)
obj[::-1] = obj
self.assertEqual(tuple(obj), base[::-1])
def test_slice_set_self_aliased_full(self):
# `obj[:] = obj` writes original values back; effectively a no-op.
n = self.matrix_size
base = tuple(self._make_value(i + 1) for i in range(n))
_mat, obj = self._make_obj(base)
obj[:] = obj
self.assertEqual(tuple(obj), base)
def test_slice_set_identity_preserved(self):
# Slice assignment must not replace the matrix or its `MatrixAccess` wrapper.
n = self.matrix_size
base = tuple(self._make_value(i + 1) for i in range(n))
_mat, obj = self._make_obj(base)
original_id = id(obj)
obj[:] = base
self.assertEqual(id(obj), original_id)
obj[::2] = base[::2]
self.assertEqual(id(obj), original_id)
class Matrix3x3TestingSlice(MatrixSliceMixIn, unittest.TestCase):
matrix_size = 3