Repository: numpy/numpy

Issue:
Issue with f2py in Numpy 1.13 when character arrays appear in a callback function
There is an issue with f2py in Numpy 1.13 when character arrays appear in a callback function.  The following example works with Numpy 1.12 but not in 1.13:

```
#! /usr/bin/env python

from __future__ import print_function
import numpy
from subprocess import call

#
# build extension from FORTRAN source
#
source = """
       subroutine func_with_callback(callBack, cu, lencu)

       implicit none

       integer :: lencu
       character*8 cu(lencu)
       external callBack

       print *,'Lencu:', lencu

       print *,'Calling callback...'
       call callBack(cu, lencu)
       print *,'Back in fortran...'

       end subroutine func_with_callback
"""

with open("callback.f", "w") as src_file:
    src_file.write(source)

rc = call("f2py callback.f -m callback -h callback.pyf".split())
if rc: quit()

rc = call("f2py -c callback.pyf callback.f".split())
if rc: quit()


#
# define callback function
#
def callBack(cu, lencu):
    print ('In callback:', cu)

#
# call library function with callback
#
from callback import func_with_callback
print(func_with_callback.__doc__)

print("\nNumpy Version:", numpy.__version__, "\n")

cu = numpy.zeros((1, 8), 'c')

func_with_callback(callBack, cu)
```

================

Numpy Version: 1.12.1

 Lencu:           1
 Calling callback...
In callback: [['' '' '' '' '' '' '' '']]
 Back in fortran...

================

Numpy Version: 1.13.3

 Lencu:           1
 Calling callback...
Call-back cb_callback_in_func_with_callback__user__routines failed.
Traceback (most recent call last):
  File "test_callback.py", line 57, in <module>
    func_with_callback(callBack, cu)
ValueError: data type must provide an itemsize



Discussion:
The problem came in with a618b4e1, #8948.
The proximate cause is this code in the generated source:
```
  if (cb_callBack_in_func_with_callback__user__routines_nofargs>capi_i) {
    PyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,2,cu_Dims,NPY_STRING,NULL,(char*)cu,0,NPY_ARRAY_CARRAY,NULL); /*XXX: Hmm, what will destroy this array??? */
```

Resolving patch:
diff --git a/numpy/f2py/cb_rules.py b/numpy/f2py/cb_rules.py
--- a/numpy/f2py/cb_rules.py
+++ b/numpy/f2py/cb_rules.py
@@ -367,11 +367,15 @@
         'pyobjfrom': [{debugcapi: '\tfprintf(stderr,"debug-capi:cb:#varname#\\n");'},
                       {isintent_c: """\
 \tif (#name#_nofargs>capi_i) {
-\t\tPyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,0,NPY_ARRAY_CARRAY,NULL); /*XXX: Hmm, what will destroy this array??? */
+\t\tint itemsize_ = #atype# == NPY_STRING ? 1 : 0;
+\t\t/*XXX: Hmm, what will destroy this array??? */
+\t\tPyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,itemsize_,NPY_ARRAY_CARRAY,NULL);
 """,
                        l_not(isintent_c): """\
 \tif (#name#_nofargs>capi_i) {
-\t\tPyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,0,NPY_ARRAY_FARRAY,NULL); /*XXX: Hmm, what will destroy this array??? */
+\t\tint itemsize_ = #atype# == NPY_STRING ? 1 : 0;
+\t\t/*XXX: Hmm, what will destroy this array??? */
+\t\tPyArrayObject *tmp_arr = (PyArrayObject *)PyArray_New(&PyArray_Type,#rank#,#varname_i#_Dims,#atype#,NULL,(char*)#varname_i#,itemsize_,NPY_ARRAY_FARRAY,NULL);
 """,
                        },
                       """


Test patch:
diff --git a/numpy/f2py/tests/test_callback.py b/numpy/f2py/tests/test_callback.py
--- a/numpy/f2py/tests/test_callback.py
+++ b/numpy/f2py/tests/test_callback.py
@@ -4,7 +4,7 @@
 import textwrap
 import sys
 
-from numpy import array
+import numpy as np
 from numpy.testing import run_module_suite, assert_, assert_equal, dec
 from . import util
 
@@ -48,6 +48,16 @@ class TestF77Callback(util.F2PyTest):
        a = callback(r)
        end
 
+       subroutine string_callback_array(callback, cu, lencu, a)
+       external callback
+       integer callback
+       integer lencu
+       character*8 cu(lencu)
+       integer a
+cf2py  intent(out) a
+
+       a = callback(cu, lencu)
+       end
     """
 
     @dec.slow
@@ -120,7 +130,8 @@ def mth(self):
         r = t(a.mth)
         assert_(r == 9, repr(r))
 
-    @dec.knownfailureif(sys.platform=='win32', msg='Fails with MinGW64 Gfortran (Issue #9673)')
+    @dec.knownfailureif(sys.platform=='win32',
+                        msg='Fails with MinGW64 Gfortran (Issue #9673)')
     def test_string_callback(self):
 
         def callback(code):
@@ -133,6 +144,25 @@ def callback(code):
         r = f(callback)
         assert_(r == 0, repr(r))
 
+    @dec.knownfailureif(sys.platform=='win32',
+                        msg='Fails with MinGW64 Gfortran (Issue #9673)')
+    def test_string_callback_array(self):
+        # See gh-10027
+        cu = np.zeros((1, 8), 'S1')
+
+        def callback(cu, lencu):
+            if cu.shape != (lencu, 8):
+                return 1
+            if cu.dtype != 'S1':
+                return 2
+            if not np.all(cu == b''):
+                return 3
+            return 0
+
+        f = getattr(self.module, 'string_callback_array')
+        res = f(callback, cu, len(cu))
+        assert_(res == 0, repr(res))
+
 
 if __name__ == "__main__":
     run_module_suite()