Repository: cython/cython

Issue:
Support __len__ for memoryview slices
Currently we are forced to use `arr.shape[0]` rather than `len(arr)` in code such as:
```cython
cdef int f(double[::1] arr) nogil:
    return len(arr)
```
Supporting `__len__` would make it more convenient to convert existing Python code and would make the code more compact.


Discussion:
`len()` generally works here, but it's a Python thing, i.e. it requires the GIL. It could probably be 'optimised' into something that does not require the GIL, though.
One more thing to keep in mind: memory view arguments are allowed to be `None`, which means that `len(arr)` might need to raise a `TypeError`. That also requires the GIL. The transformation into `arr.shape[0]` could at least be done if the `arr` argument is declared as `not None`, but that further restricts the applicability.

Resolving patch:
diff --git a/Cython/Compiler/Optimize.py b/Cython/Compiler/Optimize.py
--- a/Cython/Compiler/Optimize.py
+++ b/Cython/Compiler/Optimize.py
@@ -2429,6 +2429,14 @@ def _handle_simple_function_len(self, node, function, pos_args):
                 node.pos, "__Pyx_Py_UNICODE_strlen", self.Pyx_Py_UNICODE_strlen_func_type,
                 args = [arg],
                 is_temp = node.is_temp)
+        elif arg.type.is_memoryviewslice:
+            func_type = PyrexTypes.CFuncType(
+                PyrexTypes.c_size_t_type, [
+                    PyrexTypes.CFuncTypeArg("memoryviewslice", arg.type, None)
+                ], nogil=True)
+            new_node = ExprNodes.PythonCapiCallNode(
+                node.pos, "__Pyx_MemoryView_Len", func_type,
+                args=[arg], is_temp=node.is_temp)
         elif arg.type.is_pyobject:
             cfunc_name = self._map_to_capi_len_function(arg.type)
             if cfunc_name is None:
@@ -2442,8 +2450,7 @@ def _handle_simple_function_len(self, node, function, pos_args):
                 "object of type 'NoneType' has no len()")
             new_node = ExprNodes.PythonCapiCallNode(
                 node.pos, cfunc_name, self.PyObject_Size_func_type,
-                args = [arg],
-                is_temp = node.is_temp)
+                args=[arg], is_temp=node.is_temp)
         elif arg.type.is_unicode_char:
             return ExprNodes.IntNode(node.pos, value='1', constant_result=1,
                                      type=node.type)


Test patch:
diff --git a/tests/memoryview/memoryviewattrs.pyx b/tests/memoryview/memoryviewattrs.pyx
--- a/tests/memoryview/memoryviewattrs.pyx
+++ b/tests/memoryview/memoryviewattrs.pyx
@@ -279,6 +279,13 @@ def two_dee():
 
     assert len(arr) == 2
 
+    try:
+        _ = len(mv1)
+    except UnboundLocalError:
+        pass
+    else:
+        assert False, "UnboundLocalError not raised for uninitialised memory view"
+
     cdef long *arr_data
     arr_data = <long*>arr.data
 
diff --git a/tests/memoryview/memslice.pyx b/tests/memoryview/memslice.pyx
--- a/tests/memoryview/memslice.pyx
+++ b/tests/memoryview/memslice.pyx
@@ -1677,7 +1677,7 @@ cdef int cdef_nogil(int[:, :] a) nogil except 0:
         for j in range(b.shape[1]):
             b[i, j] = -b[i, j]
 
-    return 1
+    return len(a)
 
 @testcase
 def test_nogil():
@@ -1690,10 +1690,15 @@ def test_nogil():
     released A
     """
     _a = IntMockBuffer("A", range(4 * 9), shape=(4, 9))
-    cdef_nogil(_a)
+    assert cdef_nogil(_a) == 4
     cdef int[:, :] a = _a
     print a[2, 7]
 
+    cdef int length
+    with nogil:
+        length = cdef_nogil(a)
+    assert length == 4
+
 @testcase
 def test_convert_slicenode_to_indexnode():
     """