]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-78959: Add order parameter to memoryview.cast() (GH-153663)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 3 Aug 2026 19:06:00 +0000 (22:06 +0300)
committerGitHub <noreply@github.com>
Mon, 3 Aug 2026 19:06:00 +0000 (22:06 +0300)
memoryview.cast() now accepts a keyword-only order parameter.  With
order='F' it returns a zero-copy Fortran-contiguous (column-major) view
of a flat buffer, mirroring the order argument of memoryview.tobytes().
Only 'C' (the default) and 'F' are accepted; the buffer is not copied.

Doc/library/stdtypes.rst
Doc/whatsnew/3.16.rst
Lib/test/test_memoryview.py
Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst [new file with mode: 0644]
Objects/clinic/memoryobject.c.h
Objects/memoryobject.c

index 0b6a1b488fb64f9ea6d2e47c5ecbc57db4e3c2a5..2c11d0edbc2cbfdc9db3baa1157f5b1ee9c264bf 100644 (file)
@@ -4871,7 +4871,7 @@ copying.
       .. versionadded:: 3.2
 
    .. method:: cast(format, /)
-               cast(format, shape, /)
+               cast(format, shape, /, *, order='C')
 
       Cast a memoryview to a new format or shape. *shape* defaults to
       ``[byte_length//new_itemsize]``, which means that the result view
@@ -4880,6 +4880,12 @@ copying.
       1D -> C-:term:`contiguous`, C-contiguous -> 1D, and
       F-contiguous -> 1D.
 
+      With a multidimensional *shape*, *order* selects the memory layout of
+      the result: ``'C'`` for C-contiguous (row-major, the default) or ``'F'``
+      for Fortran-contiguous (column-major).  The buffer is still not copied,
+      so ``order='F'`` gives a zero-copy view over a buffer holding
+      column-major data.
+
       The destination format is restricted to a single element native format in
       :mod:`struct` syntax. One of the formats must be a byte format
       ('B', 'b' or 'c'). The byte length of the result must be the same
@@ -5031,8 +5037,20 @@ copying.
          >>> y.nbytes
          96
 
+      Interpret a flat buffer as a Fortran-contiguous (column-major) array::
+
+         >>> buf = bytes(range(6))
+         >>> y = memoryview(buf).cast('B', shape=[3, 2], order='F')
+         >>> y.f_contiguous
+         True
+         >>> y.tolist()
+         [[0, 3], [1, 4], [2, 5]]
+
       .. versionadded:: 3.3
 
+      .. versionchanged:: next
+         Added the *order* parameter.
+
    .. attribute:: readonly
 
       A bool indicating whether the memory is read only.
index c607e3c620572fee0121f73526bf7d7587d6bccb..90ab6758a264cd8d70c049d0fa4b147bdb68311e 100644 (file)
@@ -79,6 +79,12 @@ Other language changes
   F-contiguous view to a one-dimensional view.
   (Contributed by Jaemin Park in :gh:`91484`.)
 
+* :meth:`memoryview.cast` now accepts an *order* parameter.  With
+  ``order='F'`` it returns a zero-copy Fortran-contiguous (column-major)
+  view of a flat buffer, which is useful for interfacing with column-major
+  libraries.
+  (Contributed by Serhiy Storchaka in :gh:`78959`.)
+
 * :ref:`Frame objects <frame-objects>` now support :mod:`weak references
   <weakref>`.  This allows associating extra data with active frames,
   for example in debuggers, without keeping the frames (and everything
index df5ab9f2c879ca30c32b1e18698c164ca91b1030..3cb8a104faee5e60a77ea0d10224616a9e85dd19 100644 (file)
@@ -689,6 +689,54 @@ class ArrayMemorySliceSliceTest(unittest.TestCase,
 
 
 class OtherTest(unittest.TestCase):
+    def test_cast_order(self):
+        # gh-78959: cast(order=...) selects the result memory layout.
+        b = bytearray(range(6))
+        mv = memoryview(b)
+
+        c = mv.cast('B', shape=(3, 2))                # default: C-contiguous
+        self.assertTrue(c.c_contiguous)
+        self.assertFalse(c.f_contiguous)
+        self.assertEqual(c.strides, (2, 1))
+        self.assertEqual(c.tolist(), [[0, 1], [2, 3], [4, 5]])
+
+        f = mv.cast('B', shape=(3, 2), order='F')     # Fortran-contiguous
+        self.assertTrue(f.f_contiguous)
+        self.assertFalse(f.c_contiguous)
+        self.assertEqual(f.strides, (1, 3))
+        self.assertEqual(f.tolist(), [[0, 3], [1, 4], [2, 5]])
+        # zero-copy view: the flat physical bytes are unchanged
+        self.assertEqual(f.tobytes('A'), bytes(range(6)))
+
+        # explicit 'C' is the default
+        self.assertTrue(mv.cast('B', shape=(3, 2), order='C').c_contiguous)
+
+        # a wider format works too (strides scale by itemsize)
+        m = memoryview(struct.pack('6i', *range(6))).cast('i', shape=(3, 2),
+                                                          order='F')
+        self.assertTrue(m.f_contiguous)
+        self.assertEqual(m.strides, (4, 12))
+        self.assertEqual(m.tolist(), [[0, 3], [1, 4], [2, 5]])
+
+        # more than two dimensions
+        m3 = memoryview(bytearray(range(24))).cast('B', shape=(2, 3, 4),
+                                                   order='F')
+        self.assertTrue(m3.f_contiguous)
+        self.assertEqual(m3.strides, (1, 2, 6))
+
+        # order is keyword-only and only the strings 'C'/'F' are accepted
+        self.assertRaises(TypeError, mv.cast, 'B', (3, 2), 'F')
+        self.assertRaises(TypeError, mv.cast, 'B', shape=(3, 2), order=None)
+        self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='X')
+        self.assertRaises(ValueError, mv.cast, 'B', shape=(3, 2), order='A')
+
+    def test_cast_order_writable(self):
+        # An F-contiguous cast shares memory with the original buffer.
+        b = bytearray(6)
+        f = memoryview(b).cast('B', shape=(3, 2), order='F')
+        f[0, 1] = 9                    # column-major: physical offset 3
+        self.assertEqual(b[3], 9)
+
     def test_ctypes_cast(self):
         # Issue 15944: Allow all source formats when casting to bytes.
         ctypes = import_helper.import_module("ctypes")
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-13-13-05-00.gh-issue-78959.Xy4a2B.rst
new file mode 100644 (file)
index 0000000..c13ae14
--- /dev/null
@@ -0,0 +1,4 @@
+Add the *order* parameter to :meth:`memoryview.cast`.
+With ``order='F'`` it creates a zero-copy Fortran-contiguous (column-major)
+view of a flat buffer, mirroring the *order* argument of
+:meth:`memoryview.tobytes`.
index a0cf3243edc08a0f3ea524b58a21dce57bb21b75..d55e7c4ac475766a3cdc992c97bf7338c22a7d33 100644 (file)
@@ -148,17 +148,21 @@ memoryview_release(PyObject *self, PyObject *Py_UNUSED(ignored))
 }
 
 PyDoc_STRVAR(memoryview_cast__doc__,
-"cast($self, /, format, shape=<unrepresentable>)\n"
+"cast($self, /, format, shape=<unrepresentable>, *, order=\'C\')\n"
 "--\n"
 "\n"
-"Cast a memoryview to a new format or shape.");
+"Cast a memoryview to a new format or shape.\n"
+"\n"
+"With a multidimensional *shape*, *order* selects the result\n"
+"layout: \'C\' for C-contiguous (row-major, the default) or \'F\'\n"
+"for Fortran-contiguous (column-major).");
 
 #define MEMORYVIEW_CAST_METHODDEF    \
     {"cast", _PyCFunction_CAST(memoryview_cast), METH_FASTCALL|METH_KEYWORDS, memoryview_cast__doc__},
 
 static PyObject *
 memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
-                     PyObject *shape);
+                     PyObject *shape, int order);
 
 static PyObject *
 memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -166,7 +170,7 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec
     PyObject *return_value = NULL;
     #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
 
-    #define NUM_KEYWORDS 2
+    #define NUM_KEYWORDS 3
     static struct {
         PyGC_Head _this_is_not_used;
         PyObject_VAR_HEAD
@@ -175,7 +179,7 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec
     } _kwtuple = {
         .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
         .ob_hash = -1,
-        .ob_item = { &_Py_ID(format), &_Py_ID(shape), },
+        .ob_item = { &_Py_ID(format), &_Py_ID(shape), &_Py_ID(order), },
     };
     #undef NUM_KEYWORDS
     #define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -184,17 +188,18 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec
     #  define KWTUPLE NULL
     #endif  // !Py_BUILD_CORE
 
-    static const char * const _keywords[] = {"format", "shape", NULL};
+    static const char * const _keywords[] = {"format", "shape", "order", NULL};
     static _PyArg_Parser _parser = {
         .keywords = _keywords,
         .fname = "cast",
         .kwtuple = KWTUPLE,
     };
     #undef KWTUPLE
-    PyObject *argsbuf[2];
+    PyObject *argsbuf[3];
     Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
     PyObject *format;
     PyObject *shape = NULL;
+    int order = 'C';
 
     args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
             /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -209,9 +214,30 @@ memoryview_cast(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObjec
     if (!noptargs) {
         goto skip_optional_pos;
     }
-    shape = args[1];
+    if (args[1]) {
+        shape = args[1];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
 skip_optional_pos:
-    return_value = memoryview_cast_impl((PyMemoryViewObject *)self, format, shape);
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (!PyUnicode_Check(args[2])) {
+        _PyArg_BadArgument("cast", "argument 'order'", "a unicode character", args[2]);
+        goto exit;
+    }
+    if (PyUnicode_GET_LENGTH(args[2]) != 1) {
+        PyErr_Format(PyExc_TypeError,
+            "cast(): argument 'order' must be a unicode character, "
+            "not a string of length %zd",
+            PyUnicode_GET_LENGTH(args[2]));
+        goto exit;
+    }
+    order = PyUnicode_READ_CHAR(args[2], 0);
+skip_optional_kwonly:
+    return_value = memoryview_cast_impl((PyMemoryViewObject *)self, format, shape, order);
 
 exit:
     return return_value;
@@ -506,4 +532,4 @@ skip_optional:
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=3abf9c80cd49229a input=a9049054013a1b77]*/
+/*[clinic end generated code: output=17a403895f4f778c input=a9049054013a1b77]*/
index f6ffa337c632a0b630290ebe81ad874fbca5a45e..0bcd0b6596be77662124c913a64455b3d98ce5d7 100644 (file)
@@ -1400,11 +1400,12 @@ copy_shape(Py_ssize_t *shape, const PyObject *seq, Py_ssize_t ndim,
     return len;
 }
 
-/* Cast a 1-D array to a new shape. The result array will be C-contiguous.
-   If the result array does not have exactly the same byte length as the
-   input array, raise ValueError. */
+/* Cast a 1-D array to a new shape. The result array will be C-contiguous
+   ('C') or Fortran-contiguous ('F') according to 'order'.  If the result
+   array does not have exactly the same byte length as the input array, raise
+   TypeError. */
 static int
-cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim)
+cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim, char order)
 {
     Py_buffer *view = &mv->view;
     Py_ssize_t len;
@@ -1425,7 +1426,10 @@ cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim)
         len = copy_shape(view->shape, shape, ndim, view->itemsize);
         if (len < 0)
             return -1;
-        init_strides_from_shape(view);
+        if (order == 'F')
+            init_fortran_strides_from_shape(view);
+        else
+            init_strides_from_shape(view);
     }
 
     if (view->len != len) {
@@ -1469,14 +1473,20 @@ memoryview.cast
 
     format: unicode
     shape: object = NULL
+    *
+    order: int(accept={str}) = 'C'
 
 Cast a memoryview to a new format or shape.
+
+With a multidimensional *shape*, *order* selects the result
+layout: 'C' for C-contiguous (row-major, the default) or 'F'
+for Fortran-contiguous (column-major).
 [clinic start generated code]*/
 
 static PyObject *
 memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
-                     PyObject *shape)
-/*[clinic end generated code: output=bae520b3a389cbab input=138936cc9041b1a3]*/
+                     PyObject *shape, int order)
+/*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/
 {
     PyMemoryViewObject *mv = NULL;
     Py_ssize_t ndim = 1;
@@ -1484,6 +1494,11 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
     CHECK_RELEASED(self);
     CHECK_RESTRICTED(self);
 
+    if (order != 'C' && order != 'F') {
+        PyErr_SetString(PyExc_ValueError, "order must be 'C' or 'F'");
+        return NULL;
+    }
+
     if (!MV_C_CONTIGUOUS(self->flags)) {
         if (shape || !MV_F_CONTIGUOUS(self->flags)) {
             PyErr_SetString(PyExc_TypeError,
@@ -1519,7 +1534,7 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format,
 
     if (cast_to_1D(mv, format) < 0)
         goto error;
-    if (shape && cast_to_ND(mv, shape, (int)ndim) < 0)
+    if (shape && cast_to_ND(mv, shape, (int)ndim, (char)order) < 0)
         goto error;
 
     return (PyObject *)mv;