test_f2py2e.py 28 KB

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