test_f2py2e.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964
  1. import re
  2. import shlex
  3. import subprocess
  4. import sys
  5. import textwrap
  6. from pathlib import Path
  7. from collections import namedtuple
  8. import platform
  9. import pytest
  10. from . import util
  11. from numpy.f2py.f2py2e import main as f2pycli
  12. from numpy.testing._private.utils import NOGIL_BUILD
  13. #######################
  14. # F2PY Test utilities #
  15. ######################
  16. # Tests for CLI commands which call meson will fail if no compilers are present, these are to be skipped
  17. def compiler_check_f2pycli():
  18. if not util.has_fortran_compiler():
  19. pytest.skip("CLI command needs a Fortran compiler")
  20. else:
  21. f2pycli()
  22. #########################
  23. # CLI utils and classes #
  24. #########################
  25. PPaths = namedtuple("PPaths", "finp, f90inp, pyf, wrap77, wrap90, cmodf")
  26. def get_io_paths(fname_inp, mname="untitled"):
  27. """Takes in a temporary file for testing and returns the expected output and input paths
  28. Here expected output is essentially one of any of the possible generated
  29. files.
  30. ..note::
  31. Since this does not actually run f2py, none of these are guaranteed to
  32. exist, and module names are typically incorrect
  33. Parameters
  34. ----------
  35. fname_inp : str
  36. The input filename
  37. mname : str, optional
  38. The name of the module, untitled by default
  39. Returns
  40. -------
  41. genp : NamedTuple PPaths
  42. The possible paths which are generated, not all of which exist
  43. """
  44. bpath = Path(fname_inp)
  45. return PPaths(
  46. finp=bpath.with_suffix(".f"),
  47. f90inp=bpath.with_suffix(".f90"),
  48. pyf=bpath.with_suffix(".pyf"),
  49. wrap77=bpath.with_name(f"{mname}-f2pywrappers.f"),
  50. wrap90=bpath.with_name(f"{mname}-f2pywrappers2.f90"),
  51. cmodf=bpath.with_name(f"{mname}module.c"),
  52. )
  53. ################
  54. # CLI Fixtures #
  55. ################
  56. @pytest.fixture(scope="session")
  57. def hello_world_f90(tmpdir_factory):
  58. """Generates a single f90 file for testing"""
  59. fdat = util.getpath("tests", "src", "cli", "hiworld.f90").read_text()
  60. fn = tmpdir_factory.getbasetemp() / "hello.f90"
  61. fn.write_text(fdat, encoding="ascii")
  62. return fn
  63. @pytest.fixture(scope="session")
  64. def gh23598_warn(tmpdir_factory):
  65. """F90 file for testing warnings in gh23598"""
  66. fdat = util.getpath("tests", "src", "crackfortran", "gh23598Warn.f90").read_text()
  67. fn = tmpdir_factory.getbasetemp() / "gh23598Warn.f90"
  68. fn.write_text(fdat, encoding="ascii")
  69. return fn
  70. @pytest.fixture(scope="session")
  71. def gh22819_cli(tmpdir_factory):
  72. """F90 file for testing disallowed CLI arguments in ghff819"""
  73. fdat = util.getpath("tests", "src", "cli", "gh_22819.pyf").read_text()
  74. fn = tmpdir_factory.getbasetemp() / "gh_22819.pyf"
  75. fn.write_text(fdat, encoding="ascii")
  76. return fn
  77. @pytest.fixture(scope="session")
  78. def hello_world_f77(tmpdir_factory):
  79. """Generates a single f77 file for testing"""
  80. fdat = util.getpath("tests", "src", "cli", "hi77.f").read_text()
  81. fn = tmpdir_factory.getbasetemp() / "hello.f"
  82. fn.write_text(fdat, encoding="ascii")
  83. return fn
  84. @pytest.fixture(scope="session")
  85. def retreal_f77(tmpdir_factory):
  86. """Generates a single f77 file for testing"""
  87. fdat = util.getpath("tests", "src", "return_real", "foo77.f").read_text()
  88. fn = tmpdir_factory.getbasetemp() / "foo.f"
  89. fn.write_text(fdat, encoding="ascii")
  90. return fn
  91. @pytest.fixture(scope="session")
  92. def f2cmap_f90(tmpdir_factory):
  93. """Generates a single f90 file for testing"""
  94. fdat = util.getpath("tests", "src", "f2cmap", "isoFortranEnvMap.f90").read_text()
  95. f2cmap = util.getpath("tests", "src", "f2cmap", ".f2py_f2cmap").read_text()
  96. fn = tmpdir_factory.getbasetemp() / "f2cmap.f90"
  97. fmap = tmpdir_factory.getbasetemp() / "mapfile"
  98. fn.write_text(fdat, encoding="ascii")
  99. fmap.write_text(f2cmap, encoding="ascii")
  100. return fn
  101. #########
  102. # Tests #
  103. #########
  104. def test_gh22819_cli(capfd, gh22819_cli, monkeypatch):
  105. """Check that module names are handled correctly
  106. gh-22819
  107. Essentially, the -m name cannot be used to import the module, so the module
  108. named in the .pyf needs to be used instead
  109. CLI :: -m and a .pyf file
  110. """
  111. ipath = Path(gh22819_cli)
  112. monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath}".split())
  113. with util.switchdir(ipath.parent):
  114. f2pycli()
  115. gen_paths = [item.name for item in ipath.parent.rglob("*") if item.is_file()]
  116. assert "blahmodule.c" not in gen_paths # shouldn't be generated
  117. assert "blah-f2pywrappers.f" not in gen_paths
  118. assert "test_22819-f2pywrappers.f" in gen_paths
  119. assert "test_22819module.c" in gen_paths
  120. assert "Ignoring blah"
  121. def test_gh22819_many_pyf(capfd, gh22819_cli, monkeypatch):
  122. """Only one .pyf file allowed
  123. gh-22819
  124. CLI :: .pyf files
  125. """
  126. ipath = Path(gh22819_cli)
  127. monkeypatch.setattr(sys, "argv", f"f2py -m blah {ipath} hello.pyf".split())
  128. with util.switchdir(ipath.parent):
  129. with pytest.raises(ValueError, match="Only one .pyf file per call"):
  130. f2pycli()
  131. def test_gh23598_warn(capfd, gh23598_warn, monkeypatch):
  132. foutl = get_io_paths(gh23598_warn, mname="test")
  133. ipath = foutl.f90inp
  134. monkeypatch.setattr(
  135. sys, "argv",
  136. f'f2py {ipath} -m test'.split())
  137. with util.switchdir(ipath.parent):
  138. f2pycli() # Generate files
  139. wrapper = foutl.wrap90.read_text()
  140. assert "intproductf2pywrap, intpr" not in wrapper
  141. def test_gen_pyf(capfd, hello_world_f90, monkeypatch):
  142. """Ensures that a signature file is generated via the CLI
  143. CLI :: -h
  144. """
  145. ipath = Path(hello_world_f90)
  146. opath = Path(hello_world_f90).stem + ".pyf"
  147. monkeypatch.setattr(sys, "argv", f'f2py -h {opath} {ipath}'.split())
  148. with util.switchdir(ipath.parent):
  149. f2pycli() # Generate wrappers
  150. out, _ = capfd.readouterr()
  151. assert "Saving signatures to file" in out
  152. assert Path(f'{opath}').exists()
  153. def test_gen_pyf_stdout(capfd, hello_world_f90, monkeypatch):
  154. """Ensures that a signature file can be dumped to stdout
  155. CLI :: -h
  156. """
  157. ipath = Path(hello_world_f90)
  158. monkeypatch.setattr(sys, "argv", f'f2py -h stdout {ipath}'.split())
  159. with util.switchdir(ipath.parent):
  160. f2pycli()
  161. out, _ = capfd.readouterr()
  162. assert "Saving signatures to file" in out
  163. assert "function hi() ! in " in out
  164. def test_gen_pyf_no_overwrite(capfd, hello_world_f90, monkeypatch):
  165. """Ensures that the CLI refuses to overwrite signature files
  166. CLI :: -h without --overwrite-signature
  167. """
  168. ipath = Path(hello_world_f90)
  169. monkeypatch.setattr(sys, "argv", f'f2py -h faker.pyf {ipath}'.split())
  170. with util.switchdir(ipath.parent):
  171. Path("faker.pyf").write_text("Fake news", encoding="ascii")
  172. with pytest.raises(SystemExit):
  173. f2pycli() # Refuse to overwrite
  174. _, err = capfd.readouterr()
  175. assert "Use --overwrite-signature to overwrite" in err
  176. @pytest.mark.skipif(sys.version_info <= (3, 12), reason="Python 3.12 required")
  177. def test_untitled_cli(capfd, hello_world_f90, monkeypatch):
  178. """Check that modules are named correctly
  179. CLI :: defaults
  180. """
  181. ipath = Path(hello_world_f90)
  182. monkeypatch.setattr(sys, "argv", f"f2py --backend meson -c {ipath}".split())
  183. with util.switchdir(ipath.parent):
  184. compiler_check_f2pycli()
  185. out, _ = capfd.readouterr()
  186. assert "untitledmodule.c" in out
  187. @pytest.mark.skipif((platform.system() != 'Linux') or (sys.version_info <= (3, 12)), reason='Compiler and 3.12 required')
  188. def test_no_py312_distutils_fcompiler(capfd, hello_world_f90, monkeypatch):
  189. """Check that no distutils imports are performed on 3.12
  190. CLI :: --fcompiler --help-link --backend distutils
  191. """
  192. MNAME = "hi"
  193. foutl = get_io_paths(hello_world_f90, mname=MNAME)
  194. ipath = foutl.f90inp
  195. monkeypatch.setattr(
  196. sys, "argv", f"f2py {ipath} -c --fcompiler=gfortran -m {MNAME}".split()
  197. )
  198. with util.switchdir(ipath.parent):
  199. compiler_check_f2pycli()
  200. out, _ = capfd.readouterr()
  201. assert "--fcompiler cannot be used with meson" in out
  202. monkeypatch.setattr(
  203. sys, "argv", "f2py --help-link".split()
  204. )
  205. with util.switchdir(ipath.parent):
  206. f2pycli()
  207. out, _ = capfd.readouterr()
  208. assert "Use --dep for meson builds" in out
  209. MNAME = "hi2" # Needs to be different for a new -c
  210. monkeypatch.setattr(
  211. sys, "argv", f"f2py {ipath} -c -m {MNAME} --backend distutils".split()
  212. )
  213. with util.switchdir(ipath.parent):
  214. f2pycli()
  215. out, _ = capfd.readouterr()
  216. assert "Cannot use distutils backend with Python>=3.12" in out
  217. @pytest.mark.xfail
  218. def test_f2py_skip(capfd, retreal_f77, monkeypatch):
  219. """Tests that functions can be skipped
  220. CLI :: skip:
  221. """
  222. foutl = get_io_paths(retreal_f77, mname="test")
  223. ipath = foutl.finp
  224. toskip = "t0 t4 t8 sd s8 s4"
  225. remaining = "td s0"
  226. monkeypatch.setattr(
  227. sys, "argv",
  228. f'f2py {ipath} -m test skip: {toskip}'.split())
  229. with util.switchdir(ipath.parent):
  230. f2pycli()
  231. out, err = capfd.readouterr()
  232. for skey in toskip.split():
  233. assert (
  234. f'buildmodule: Could not found the body of interfaced routine "{skey}". Skipping.'
  235. in err)
  236. for rkey in remaining.split():
  237. assert f'Constructing wrapper function "{rkey}"' in out
  238. def test_f2py_only(capfd, retreal_f77, monkeypatch):
  239. """Test that functions can be kept by only:
  240. CLI :: only:
  241. """
  242. foutl = get_io_paths(retreal_f77, mname="test")
  243. ipath = foutl.finp
  244. toskip = "t0 t4 t8 sd s8 s4"
  245. tokeep = "td s0"
  246. monkeypatch.setattr(
  247. sys, "argv",
  248. f'f2py {ipath} -m test only: {tokeep}'.split())
  249. with util.switchdir(ipath.parent):
  250. f2pycli()
  251. out, err = capfd.readouterr()
  252. for skey in toskip.split():
  253. assert (
  254. f'buildmodule: Could not find the body of interfaced routine "{skey}". Skipping.'
  255. in err)
  256. for rkey in tokeep.split():
  257. assert f'Constructing wrapper function "{rkey}"' in out
  258. def test_file_processing_switch(capfd, hello_world_f90, retreal_f77,
  259. monkeypatch):
  260. """Tests that it is possible to return to file processing mode
  261. CLI :: :
  262. BUG: numpy-gh #20520
  263. """
  264. foutl = get_io_paths(retreal_f77, mname="test")
  265. ipath = foutl.finp
  266. toskip = "t0 t4 t8 sd s8 s4"
  267. ipath2 = Path(hello_world_f90)
  268. tokeep = "td s0 hi" # hi is in ipath2
  269. mname = "blah"
  270. monkeypatch.setattr(
  271. sys,
  272. "argv",
  273. f'f2py {ipath} -m {mname} only: {tokeep} : {ipath2}'.split(
  274. ),
  275. )
  276. with util.switchdir(ipath.parent):
  277. f2pycli()
  278. out, err = capfd.readouterr()
  279. for skey in toskip.split():
  280. assert (
  281. f'buildmodule: Could not find the body of interfaced routine "{skey}". Skipping.'
  282. in err)
  283. for rkey in tokeep.split():
  284. assert f'Constructing wrapper function "{rkey}"' in out
  285. def test_mod_gen_f77(capfd, hello_world_f90, monkeypatch):
  286. """Checks the generation of files based on a module name
  287. CLI :: -m
  288. """
  289. MNAME = "hi"
  290. foutl = get_io_paths(hello_world_f90, mname=MNAME)
  291. ipath = foutl.f90inp
  292. monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m {MNAME}'.split())
  293. with util.switchdir(ipath.parent):
  294. f2pycli()
  295. # Always generate C module
  296. assert Path.exists(foutl.cmodf)
  297. # File contains a function, check for F77 wrappers
  298. assert Path.exists(foutl.wrap77)
  299. def test_mod_gen_gh25263(capfd, hello_world_f77, monkeypatch):
  300. """Check that pyf files are correctly generated with module structure
  301. CLI :: -m <name> -h pyf_file
  302. BUG: numpy-gh #20520
  303. """
  304. MNAME = "hi"
  305. foutl = get_io_paths(hello_world_f77, mname=MNAME)
  306. ipath = foutl.finp
  307. monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m {MNAME} -h hi.pyf'.split())
  308. with util.switchdir(ipath.parent):
  309. f2pycli()
  310. with Path('hi.pyf').open() as hipyf:
  311. pyfdat = hipyf.read()
  312. assert "python module hi" in pyfdat
  313. def test_lower_cmod(capfd, hello_world_f77, monkeypatch):
  314. """Lowers cases by flag or when -h is present
  315. CLI :: --[no-]lower
  316. """
  317. foutl = get_io_paths(hello_world_f77, mname="test")
  318. ipath = foutl.finp
  319. capshi = re.compile(r"HI\(\)")
  320. capslo = re.compile(r"hi\(\)")
  321. # Case I: --lower is passed
  322. monkeypatch.setattr(sys, "argv", f'f2py {ipath} -m test --lower'.split())
  323. with util.switchdir(ipath.parent):
  324. f2pycli()
  325. out, _ = capfd.readouterr()
  326. assert capslo.search(out) is not None
  327. assert capshi.search(out) is None
  328. # Case II: --no-lower is passed
  329. monkeypatch.setattr(sys, "argv",
  330. f'f2py {ipath} -m test --no-lower'.split())
  331. with util.switchdir(ipath.parent):
  332. f2pycli()
  333. out, _ = capfd.readouterr()
  334. assert capslo.search(out) is None
  335. assert capshi.search(out) is not None
  336. def test_lower_sig(capfd, hello_world_f77, monkeypatch):
  337. """Lowers cases in signature files by flag or when -h is present
  338. CLI :: --[no-]lower -h
  339. """
  340. foutl = get_io_paths(hello_world_f77, mname="test")
  341. ipath = foutl.finp
  342. # Signature files
  343. capshi = re.compile(r"Block: HI")
  344. capslo = re.compile(r"Block: hi")
  345. # Case I: --lower is implied by -h
  346. # TODO: Clean up to prevent passing --overwrite-signature
  347. monkeypatch.setattr(
  348. sys,
  349. "argv",
  350. f'f2py {ipath} -h {foutl.pyf} -m test --overwrite-signature'.split(),
  351. )
  352. with util.switchdir(ipath.parent):
  353. f2pycli()
  354. out, _ = capfd.readouterr()
  355. assert capslo.search(out) is not None
  356. assert capshi.search(out) is None
  357. # Case II: --no-lower overrides -h
  358. monkeypatch.setattr(
  359. sys,
  360. "argv",
  361. f'f2py {ipath} -h {foutl.pyf} -m test --overwrite-signature --no-lower'
  362. .split(),
  363. )
  364. with util.switchdir(ipath.parent):
  365. f2pycli()
  366. out, _ = capfd.readouterr()
  367. assert capslo.search(out) is None
  368. assert capshi.search(out) is not None
  369. def test_build_dir(capfd, hello_world_f90, monkeypatch):
  370. """Ensures that the build directory can be specified
  371. CLI :: --build-dir
  372. """
  373. ipath = Path(hello_world_f90)
  374. mname = "blah"
  375. odir = "tttmp"
  376. monkeypatch.setattr(sys, "argv",
  377. f'f2py -m {mname} {ipath} --build-dir {odir}'.split())
  378. with util.switchdir(ipath.parent):
  379. f2pycli()
  380. out, _ = capfd.readouterr()
  381. assert f"Wrote C/API module \"{mname}\"" in out
  382. def test_overwrite(capfd, hello_world_f90, monkeypatch):
  383. """Ensures that the build directory can be specified
  384. CLI :: --overwrite-signature
  385. """
  386. ipath = Path(hello_world_f90)
  387. monkeypatch.setattr(
  388. sys, "argv",
  389. f'f2py -h faker.pyf {ipath} --overwrite-signature'.split())
  390. with util.switchdir(ipath.parent):
  391. Path("faker.pyf").write_text("Fake news", encoding="ascii")
  392. f2pycli()
  393. out, _ = capfd.readouterr()
  394. assert "Saving signatures to file" in out
  395. def test_latexdoc(capfd, hello_world_f90, monkeypatch):
  396. """Ensures that TeX documentation is written out
  397. CLI :: --latex-doc
  398. """
  399. ipath = Path(hello_world_f90)
  400. mname = "blah"
  401. monkeypatch.setattr(sys, "argv",
  402. f'f2py -m {mname} {ipath} --latex-doc'.split())
  403. with util.switchdir(ipath.parent):
  404. f2pycli()
  405. out, _ = capfd.readouterr()
  406. assert "Documentation is saved to file" in out
  407. with Path(f"{mname}module.tex").open() as otex:
  408. assert "\\documentclass" in otex.read()
  409. def test_nolatexdoc(capfd, hello_world_f90, monkeypatch):
  410. """Ensures that TeX documentation is written out
  411. CLI :: --no-latex-doc
  412. """
  413. ipath = Path(hello_world_f90)
  414. mname = "blah"
  415. monkeypatch.setattr(sys, "argv",
  416. f'f2py -m {mname} {ipath} --no-latex-doc'.split())
  417. with util.switchdir(ipath.parent):
  418. f2pycli()
  419. out, _ = capfd.readouterr()
  420. assert "Documentation is saved to file" not in out
  421. def test_shortlatex(capfd, hello_world_f90, monkeypatch):
  422. """Ensures that truncated documentation is written out
  423. TODO: Test to ensure this has no effect without --latex-doc
  424. CLI :: --latex-doc --short-latex
  425. """
  426. ipath = Path(hello_world_f90)
  427. mname = "blah"
  428. monkeypatch.setattr(
  429. sys,
  430. "argv",
  431. f'f2py -m {mname} {ipath} --latex-doc --short-latex'.split(),
  432. )
  433. with util.switchdir(ipath.parent):
  434. f2pycli()
  435. out, _ = capfd.readouterr()
  436. assert "Documentation is saved to file" in out
  437. with Path(f"./{mname}module.tex").open() as otex:
  438. assert "\\documentclass" not in otex.read()
  439. def test_restdoc(capfd, hello_world_f90, monkeypatch):
  440. """Ensures that RsT documentation is written out
  441. CLI :: --rest-doc
  442. """
  443. ipath = Path(hello_world_f90)
  444. mname = "blah"
  445. monkeypatch.setattr(sys, "argv",
  446. f'f2py -m {mname} {ipath} --rest-doc'.split())
  447. with util.switchdir(ipath.parent):
  448. f2pycli()
  449. out, _ = capfd.readouterr()
  450. assert "ReST Documentation is saved to file" in out
  451. with Path(f"./{mname}module.rest").open() as orst:
  452. assert r".. -*- rest -*-" in orst.read()
  453. def test_norestexdoc(capfd, hello_world_f90, monkeypatch):
  454. """Ensures that TeX documentation is written out
  455. CLI :: --no-rest-doc
  456. """
  457. ipath = Path(hello_world_f90)
  458. mname = "blah"
  459. monkeypatch.setattr(sys, "argv",
  460. f'f2py -m {mname} {ipath} --no-rest-doc'.split())
  461. with util.switchdir(ipath.parent):
  462. f2pycli()
  463. out, _ = capfd.readouterr()
  464. assert "ReST Documentation is saved to file" not in out
  465. def test_debugcapi(capfd, hello_world_f90, monkeypatch):
  466. """Ensures that debugging wrappers are written
  467. CLI :: --debug-capi
  468. """
  469. ipath = Path(hello_world_f90)
  470. mname = "blah"
  471. monkeypatch.setattr(sys, "argv",
  472. f'f2py -m {mname} {ipath} --debug-capi'.split())
  473. with util.switchdir(ipath.parent):
  474. f2pycli()
  475. with Path(f"./{mname}module.c").open() as ocmod:
  476. assert r"#define DEBUGCFUNCS" in ocmod.read()
  477. @pytest.mark.skip(reason="Consistently fails on CI; noisy so skip not xfail.")
  478. def test_debugcapi_bld(hello_world_f90, monkeypatch):
  479. """Ensures that debugging wrappers work
  480. CLI :: --debug-capi -c
  481. """
  482. ipath = Path(hello_world_f90)
  483. mname = "blah"
  484. monkeypatch.setattr(sys, "argv",
  485. f'f2py -m {mname} {ipath} -c --debug-capi'.split())
  486. with util.switchdir(ipath.parent):
  487. f2pycli()
  488. cmd_run = shlex.split(f"{sys.executable} -c \"import blah; blah.hi()\"")
  489. rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
  490. eout = ' Hello World\n'
  491. eerr = textwrap.dedent("""\
  492. debug-capi:Python C/API function blah.hi()
  493. debug-capi:float hi=:output,hidden,scalar
  494. debug-capi:hi=0
  495. debug-capi:Fortran subroutine `f2pywraphi(&hi)'
  496. debug-capi:hi=0
  497. debug-capi:Building return value.
  498. debug-capi:Python C/API function blah.hi: successful.
  499. debug-capi:Freeing memory.
  500. """)
  501. assert rout.stdout == eout
  502. assert rout.stderr == eerr
  503. def test_wrapfunc_def(capfd, hello_world_f90, monkeypatch):
  504. """Ensures that fortran subroutine wrappers for F77 are included by default
  505. CLI :: --[no]-wrap-functions
  506. """
  507. # Implied
  508. ipath = Path(hello_world_f90)
  509. mname = "blah"
  510. monkeypatch.setattr(sys, "argv", f'f2py -m {mname} {ipath}'.split())
  511. with util.switchdir(ipath.parent):
  512. f2pycli()
  513. out, _ = capfd.readouterr()
  514. assert r"Fortran 77 wrappers are saved to" in out
  515. # Explicit
  516. monkeypatch.setattr(sys, "argv",
  517. f'f2py -m {mname} {ipath} --wrap-functions'.split())
  518. with util.switchdir(ipath.parent):
  519. f2pycli()
  520. out, _ = capfd.readouterr()
  521. assert r"Fortran 77 wrappers are saved to" in out
  522. def test_nowrapfunc(capfd, hello_world_f90, monkeypatch):
  523. """Ensures that fortran subroutine wrappers for F77 can be disabled
  524. CLI :: --no-wrap-functions
  525. """
  526. ipath = Path(hello_world_f90)
  527. mname = "blah"
  528. monkeypatch.setattr(sys, "argv",
  529. f'f2py -m {mname} {ipath} --no-wrap-functions'.split())
  530. with util.switchdir(ipath.parent):
  531. f2pycli()
  532. out, _ = capfd.readouterr()
  533. assert r"Fortran 77 wrappers are saved to" not in out
  534. def test_inclheader(capfd, hello_world_f90, monkeypatch):
  535. """Add to the include directories
  536. CLI :: -include
  537. TODO: Document this in the help string
  538. """
  539. ipath = Path(hello_world_f90)
  540. mname = "blah"
  541. monkeypatch.setattr(
  542. sys,
  543. "argv",
  544. f'f2py -m {mname} {ipath} -include<stdbool.h> -include<stdio.h> '.
  545. split(),
  546. )
  547. with util.switchdir(ipath.parent):
  548. f2pycli()
  549. with Path(f"./{mname}module.c").open() as ocmod:
  550. ocmr = ocmod.read()
  551. assert "#include <stdbool.h>" in ocmr
  552. assert "#include <stdio.h>" in ocmr
  553. def test_inclpath():
  554. """Add to the include directories
  555. CLI :: --include-paths
  556. """
  557. # TODO: populate
  558. pass
  559. def test_hlink():
  560. """Add to the include directories
  561. CLI :: --help-link
  562. """
  563. # TODO: populate
  564. pass
  565. def test_f2cmap(capfd, f2cmap_f90, monkeypatch):
  566. """Check that Fortran-to-Python KIND specs can be passed
  567. CLI :: --f2cmap
  568. """
  569. ipath = Path(f2cmap_f90)
  570. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --f2cmap mapfile'.split())
  571. with util.switchdir(ipath.parent):
  572. f2pycli()
  573. out, _ = capfd.readouterr()
  574. assert "Reading f2cmap from 'mapfile' ..." in out
  575. assert "Mapping \"real(kind=real32)\" to \"float\"" in out
  576. assert "Mapping \"real(kind=real64)\" to \"double\"" in out
  577. assert "Mapping \"integer(kind=int64)\" to \"long_long\"" in out
  578. assert "Successfully applied user defined f2cmap changes" in out
  579. def test_quiet(capfd, hello_world_f90, monkeypatch):
  580. """Reduce verbosity
  581. CLI :: --quiet
  582. """
  583. ipath = Path(hello_world_f90)
  584. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --quiet'.split())
  585. with util.switchdir(ipath.parent):
  586. f2pycli()
  587. out, _ = capfd.readouterr()
  588. assert len(out) == 0
  589. def test_verbose(capfd, hello_world_f90, monkeypatch):
  590. """Increase verbosity
  591. CLI :: --verbose
  592. """
  593. ipath = Path(hello_world_f90)
  594. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} --verbose'.split())
  595. with util.switchdir(ipath.parent):
  596. f2pycli()
  597. out, _ = capfd.readouterr()
  598. assert "analyzeline" in out
  599. def test_version(capfd, monkeypatch):
  600. """Ensure version
  601. CLI :: -v
  602. """
  603. monkeypatch.setattr(sys, "argv", 'f2py -v'.split())
  604. # TODO: f2py2e should not call sys.exit() after printing the version
  605. with pytest.raises(SystemExit):
  606. f2pycli()
  607. out, _ = capfd.readouterr()
  608. import numpy as np
  609. assert np.__version__ == out.strip()
  610. @pytest.mark.skip(reason="Consistently fails on CI; noisy so skip not xfail.")
  611. def test_npdistop(hello_world_f90, monkeypatch):
  612. """
  613. CLI :: -c
  614. """
  615. ipath = Path(hello_world_f90)
  616. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c'.split())
  617. with util.switchdir(ipath.parent):
  618. f2pycli()
  619. cmd_run = shlex.split(f"{sys.executable} -c \"import blah; blah.hi()\"")
  620. rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
  621. eout = ' Hello World\n'
  622. assert rout.stdout == eout
  623. @pytest.mark.skipif((platform.system() != 'Linux') or sys.version_info <= (3, 12),
  624. reason='Compiler and Python 3.12 or newer required')
  625. def test_no_freethreading_compatible(hello_world_f90, monkeypatch):
  626. """
  627. CLI :: --no-freethreading-compatible
  628. """
  629. ipath = Path(hello_world_f90)
  630. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --no-freethreading-compatible'.split())
  631. with util.switchdir(ipath.parent):
  632. compiler_check_f2pycli()
  633. cmd = f"{sys.executable} -c \"import blah; blah.hi();"
  634. if NOGIL_BUILD:
  635. cmd += "import sys; assert sys._is_gil_enabled() is True\""
  636. else:
  637. cmd += "\""
  638. cmd_run = shlex.split(cmd)
  639. rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
  640. eout = ' Hello World\n'
  641. assert rout.stdout == eout
  642. if NOGIL_BUILD:
  643. assert "The global interpreter lock (GIL) has been enabled to load module 'blah'" in rout.stderr
  644. assert rout.returncode == 0
  645. @pytest.mark.skipif((platform.system() != 'Linux') or sys.version_info <= (3, 12),
  646. reason='Compiler and Python 3.12 or newer required')
  647. def test_freethreading_compatible(hello_world_f90, monkeypatch):
  648. """
  649. CLI :: --freethreading_compatible
  650. """
  651. ipath = Path(hello_world_f90)
  652. monkeypatch.setattr(sys, "argv", f'f2py -m blah {ipath} -c --freethreading-compatible'.split())
  653. with util.switchdir(ipath.parent):
  654. compiler_check_f2pycli()
  655. cmd = f"{sys.executable} -c \"import blah; blah.hi();"
  656. if NOGIL_BUILD:
  657. cmd += "import sys; assert sys._is_gil_enabled() is False\""
  658. else:
  659. cmd += "\""
  660. cmd_run = shlex.split(cmd)
  661. rout = subprocess.run(cmd_run, capture_output=True, encoding='UTF-8')
  662. eout = ' Hello World\n'
  663. assert rout.stdout == eout
  664. assert rout.stderr == ""
  665. assert rout.returncode == 0
  666. # Numpy distutils flags
  667. # TODO: These should be tested separately
  668. def test_npd_fcompiler():
  669. """
  670. CLI :: -c --fcompiler
  671. """
  672. # TODO: populate
  673. pass
  674. def test_npd_compiler():
  675. """
  676. CLI :: -c --compiler
  677. """
  678. # TODO: populate
  679. pass
  680. def test_npd_help_fcompiler():
  681. """
  682. CLI :: -c --help-fcompiler
  683. """
  684. # TODO: populate
  685. pass
  686. def test_npd_f77exec():
  687. """
  688. CLI :: -c --f77exec
  689. """
  690. # TODO: populate
  691. pass
  692. def test_npd_f90exec():
  693. """
  694. CLI :: -c --f90exec
  695. """
  696. # TODO: populate
  697. pass
  698. def test_npd_f77flags():
  699. """
  700. CLI :: -c --f77flags
  701. """
  702. # TODO: populate
  703. pass
  704. def test_npd_f90flags():
  705. """
  706. CLI :: -c --f90flags
  707. """
  708. # TODO: populate
  709. pass
  710. def test_npd_opt():
  711. """
  712. CLI :: -c --opt
  713. """
  714. # TODO: populate
  715. pass
  716. def test_npd_arch():
  717. """
  718. CLI :: -c --arch
  719. """
  720. # TODO: populate
  721. pass
  722. def test_npd_noopt():
  723. """
  724. CLI :: -c --noopt
  725. """
  726. # TODO: populate
  727. pass
  728. def test_npd_noarch():
  729. """
  730. CLI :: -c --noarch
  731. """
  732. # TODO: populate
  733. pass
  734. def test_npd_debug():
  735. """
  736. CLI :: -c --debug
  737. """
  738. # TODO: populate
  739. pass
  740. def test_npd_link_auto():
  741. """
  742. CLI :: -c --link-<resource>
  743. """
  744. # TODO: populate
  745. pass
  746. def test_npd_lib():
  747. """
  748. CLI :: -c -L/path/to/lib/ -l<libname>
  749. """
  750. # TODO: populate
  751. pass
  752. def test_npd_define():
  753. """
  754. CLI :: -D<define>
  755. """
  756. # TODO: populate
  757. pass
  758. def test_npd_undefine():
  759. """
  760. CLI :: -U<name>
  761. """
  762. # TODO: populate
  763. pass
  764. def test_npd_incl():
  765. """
  766. CLI :: -I/path/to/include/
  767. """
  768. # TODO: populate
  769. pass
  770. def test_npd_linker():
  771. """
  772. CLI :: <filename>.o <filename>.so <filename>.a
  773. """
  774. # TODO: populate
  775. pass