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