Skip to content

Commit f833f33

Browse files
authored
Merge pull request numpy#27728 from HaoZeke/gh27697_lower_callstatement
BUG: Handle `--lower` for F2PY directives and callbacks
2 parents d6b0387 + adef3a0 commit f833f33

File tree

6 files changed

+73
-1
lines changed

6 files changed

+73
-1
lines changed

doc/source/f2py/python-usage.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ In Python:
243243
.. literalinclude:: ./code/results/extcallback_session.dat
244244
:language: python
245245

246+
.. note::
247+
248+
When using modified Fortran code via ``callstatement`` or other directives,
249+
the wrapped Python function must be called as a callback, otherwise only the
250+
bare Fortran routine will be used. For more details, see
251+
https://github.com/numpy/numpy/issues/26681#issuecomment-2466460943
252+
246253
Resolving arguments to call-back functions
247254
------------------------------------------
248255

numpy/f2py/crackfortran.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,14 @@ def readfortrancode(ffile, dowithline=show, istop=1):
424424
if l[-1] not in "\n\r\f":
425425
break
426426
l = l[:-1]
427+
# Do not lower for directives, gh-2547, gh-27697, gh-26681
428+
is_f2py_directive = False
427429
# Unconditionally remove comments
428430
(l, rl) = split_by_unquoted(l, '!')
429431
l += ' '
430432
if rl[:5].lower() == '!f2py': # f2py directive
431433
l, _ = split_by_unquoted(l + 4 * ' ' + rl[5:], '!')
434+
is_f2py_directive = True
432435
if l.strip() == '': # Skip empty line
433436
if sourcecodeform == 'free':
434437
# In free form, a statement continues in the next line
@@ -448,8 +451,10 @@ def readfortrancode(ffile, dowithline=show, istop=1):
448451
if l[0] in ['*', 'c', '!', 'C', '#']:
449452
if l[1:5].lower() == 'f2py': # f2py directive
450453
l = ' ' + l[5:]
454+
is_f2py_directive = True
451455
else: # Skip comment line
452456
cont = False
457+
is_f2py_directive = False
453458
continue
454459
elif strictf77:
455460
if len(l) > 72:
@@ -475,6 +480,7 @@ def readfortrancode(ffile, dowithline=show, istop=1):
475480
else:
476481
# clean up line beginning from possible digits.
477482
l = ' ' + l[5:]
483+
# f2py directives are already stripped by this point
478484
if localdolowercase:
479485
finalline = ll.lower()
480486
else:
@@ -504,7 +510,11 @@ def readfortrancode(ffile, dowithline=show, istop=1):
504510
origfinalline = ''
505511
else:
506512
if localdolowercase:
507-
finalline = ll.lower()
513+
# lines with intent() should be lowered otherwise
514+
# TestString::test_char fails due to mixed case
515+
# f2py directives without intent() should be left untouched
516+
# gh-2547, gh-27697, gh-26681
517+
finalline = ll.lower() if "intent" in ll.lower() or not is_f2py_directive else ll
508518
else:
509519
finalline = ll
510520
origfinalline = ll
@@ -536,6 +546,7 @@ def readfortrancode(ffile, dowithline=show, istop=1):
536546
else:
537547
dowithline(finalline)
538548
l1 = ll
549+
# Last line should never have an f2py directive anyway
539550
if localdolowercase:
540551
finalline = ll.lower()
541552
else:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module utils
2+
implicit none
3+
contains
4+
subroutine my_abort(message)
5+
implicit none
6+
character(len=*), intent(in) :: message
7+
!f2py callstatement PyErr_SetString(PyExc_ValueError, message);f2py_success = 0;
8+
!f2py callprotoargument char*
9+
write(0,*) "THIS SHOULD NOT APPEAR"
10+
stop 1
11+
end subroutine my_abort
12+
13+
subroutine do_something(message)
14+
!f2py intent(callback, hide) mypy_abort
15+
character(len=*), intent(in) :: message
16+
call mypy_abort(message)
17+
end subroutine do_something
18+
end module utils
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module utils
2+
implicit none
3+
contains
4+
subroutine my_abort(message)
5+
implicit none
6+
character(len=*), intent(in) :: message
7+
!f2py callstatement PyErr_SetString(PyExc_ValueError, message);f2py_success = 0;
8+
!f2py callprotoargument char*
9+
write(0,*) "THIS SHOULD NOT APPEAR"
10+
stop 1
11+
end subroutine my_abort
12+
end module utils

numpy/f2py/tests/test_callback.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import threading
66
import traceback
77
import time
8+
import platform
89

910
import numpy as np
1011
from numpy.testing import IS_PYPY
@@ -244,3 +245,17 @@ def bar(x):
244245

245246
res = self.module.foo(bar)
246247
assert res == 110
248+
249+
250+
@pytest.mark.slow
251+
@pytest.mark.xfail(condition=(platform.system().lower() == 'darwin'),
252+
run=False,
253+
reason="Callback aborts cause CI failures on macOS")
254+
class TestCBFortranCallstatement(util.F2PyTest):
255+
sources = [util.getpath("tests", "src", "callback", "gh26681.f90")]
256+
options = ['--lower']
257+
258+
def test_callstatement_fortran(self):
259+
with pytest.raises(ValueError, match='helpme') as exc:
260+
self.module.mypy_abort = self.module.utils.my_abort
261+
self.module.utils.do_something('helpme')

numpy/f2py/tests/test_crackfortran.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,12 @@ def test_param_eval_too_many_dims(self):
403403
dimspec = '(0:4, 3:12, 5)'
404404
pytest.raises(ValueError, crackfortran.param_eval, v, g_params, params,
405405
dimspec=dimspec)
406+
407+
@pytest.mark.slow
408+
class TestLowerF2PYDirective(util.F2PyTest):
409+
sources = [util.getpath("tests", "src", "crackfortran", "gh27697.f90")]
410+
options = ['--lower']
411+
412+
def test_no_lower_fail(self):
413+
with pytest.raises(ValueError, match='aborting directly') as exc:
414+
self.module.utils.my_abort('aborting directly')

0 commit comments

Comments
 (0)