Skip to content

Commit cf27205

Browse files
committed
feat: first version
Signed-off-by: Henry Schreiner <[email protected]>
1 parent d811787 commit cf27205

File tree

12 files changed

+320
-31
lines changed

12 files changed

+320
-31
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ repos:
5454
args: []
5555
additional_dependencies:
5656
- pytest
57+
- scikit-build-core
5758

5859
- repo: https://github.com/codespell-project/codespell
5960
rev: "v2.3.0"

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,72 @@
11
# f2py-cmake
22

33
[![Actions Status][actions-badge]][actions-link]
4+
5+
<!--
46
[![Documentation Status][rtd-badge]][rtd-link]
7+
-->
58

69
[![PyPI version][pypi-version]][pypi-link]
7-
[![Conda-Forge][conda-badge]][conda-link]
810
[![PyPI platforms][pypi-platforms]][pypi-link]
911

12+
<!--
1013
[![GitHub Discussion][github-discussions-badge]][github-discussions-link]
14+
-->
1115

1216
<!-- SPHINX-START -->
1317

18+
This provides helpers for using F2Py. Use:
19+
20+
```cmake
21+
include(UseF2Py)
22+
```
23+
24+
You must have found a Python interpreter beforehand. This will define a
25+
`F2Py::F2Py` target (along with a matching `F2PY_EXECUTABLE` variable). It will
26+
also provide the following helper function:
27+
28+
```cmake
29+
f2py_generate_module(<module> <files>...
30+
[F2PY_ARGS <args> ...]
31+
[F77 | F90]
32+
[NOLOWER]
33+
[OUTPUT_DIR <OutputDir>]
34+
[OUTPUT_VARIABLE <OutputVariable>]
35+
)
36+
```
37+
38+
## Example
39+
40+
```cmake
41+
find_package(
42+
Python
43+
COMPONENTS Interpreter Development.Module NumPy
44+
REQUIRED)
45+
46+
include(UseF2Py)
47+
48+
f2py_object_library(f2py_object OBJECT)
49+
50+
f2py_generate_module(fibby fib1.f OUTPUT_VARIABLE fibby_files)
51+
52+
python_add_library(fibby MODULE "${fibby_files}" WITH_SOABI)
53+
target_link_library(fibby PRIVATE f2py_object)
54+
```
55+
56+
## scikit-build-core
57+
58+
To use this package with scikit-build-core, you need to include it in your build
59+
requirements:
60+
61+
```toml
62+
[build-system]
63+
requires = ["scikit-build-core", "numpy", "f2py-cmake"]
64+
build-backend = "scikit_build_core.build"
65+
```
66+
1467
<!-- prettier-ignore-start -->
1568
[actions-badge]: https://github.com/scikit-build/f2py-cmake/workflows/CI/badge.svg
1669
[actions-link]: https://github.com/scikit-build/f2py-cmake/actions
17-
[conda-badge]: https://img.shields.io/conda/vn/conda-forge/f2py-cmake
18-
[conda-link]: https://github.com/conda-forge/f2py-cmake-feedstock
1970
[github-discussions-badge]: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github
2071
[github-discussions-link]: https://github.com/scikit-build/f2py-cmake/discussions
2172
[pypi-link]: https://pypi.org/project/f2py-cmake/

noxfile.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from __future__ import annotations
22

33
import argparse
4-
import shutil
54
from pathlib import Path
65

76
import nox
87

98
DIR = Path(__file__).parent.resolve()
109

1110
nox.needs_version = ">=2024.3.2"
12-
nox.options.sessions = ["lint", "pylint", "tests"]
1311
nox.options.default_venv_backend = "uv|virtualenv"
12+
nox.options.sessions = ["lint", "pylint", "tests"]
1413

1514

1615
@nox.session
@@ -31,7 +30,7 @@ def pylint(session: nox.Session) -> None:
3130
"""
3231
# This needs to be installed into the package environment, and is slower
3332
# than a pre-commit check
34-
session.install(".", "pylint>=3.2")
33+
session.install(".", "pylint")
3534
session.run("pylint", "f2py_cmake", *session.posargs)
3635

3736

@@ -40,37 +39,48 @@ def tests(session: nox.Session) -> None:
4039
"""
4140
Run the unit and regular tests.
4241
"""
43-
session.install(".[test]")
42+
session.install("-e.[test]")
4443
session.run("pytest", *session.posargs)
4544

4645

4746
@nox.session(reuse_venv=True)
4847
def docs(session: nox.Session) -> None:
4948
"""
50-
Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory.
49+
Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links.
5150
"""
5251

5352
parser = argparse.ArgumentParser()
53+
parser.add_argument("--serve", action="store_true", help="Serve after building")
5454
parser.add_argument(
5555
"-b", dest="builder", default="html", help="Build target (default: html)"
5656
)
57-
parser.add_argument("output", nargs="?", help="Output directory")
5857
args, posargs = parser.parse_known_args(session.posargs)
59-
serve = args.builder == "html" and session.interactive
6058

61-
session.install("-e.[docs]", "sphinx-autobuild")
59+
if args.builder != "html" and args.serve:
60+
session.error("Must not specify non-HTML builder with --serve")
61+
62+
extra_installs = ["sphinx-autobuild"] if args.serve else []
63+
64+
session.install("-e.[docs]", *extra_installs)
65+
session.chdir("docs")
66+
67+
if args.builder == "linkcheck":
68+
session.run(
69+
"sphinx-build", "-b", "linkcheck", ".", "_build/linkcheck", *posargs
70+
)
71+
return
6272

6373
shared_args = (
6474
"-n", # nitpicky mode
6575
"-T", # full tracebacks
6676
f"-b={args.builder}",
67-
"docs",
68-
args.output or f"docs/_build/{args.builder}",
77+
".",
78+
f"_build/{args.builder}",
6979
*posargs,
7080
)
7181

72-
if serve:
73-
session.run("sphinx-autobuild", "--open-browser", *shared_args)
82+
if args.serve:
83+
session.run("sphinx-autobuild", *shared_args)
7484
else:
7585
session.run("sphinx-build", "--keep-going", *shared_args)
7686

@@ -82,14 +92,15 @@ def build_api_docs(session: nox.Session) -> None:
8292
"""
8393

8494
session.install("sphinx")
95+
session.chdir("docs")
8596
session.run(
8697
"sphinx-apidoc",
8798
"-o",
88-
"docs/api/",
99+
"api/",
89100
"--module-first",
90101
"--no-toc",
91102
"--force",
92-
"src/f2py_cmake",
103+
"../src/f2py_cmake",
93104
)
94105

95106

@@ -99,9 +110,5 @@ def build(session: nox.Session) -> None:
99110
Build an SDist and wheel.
100111
"""
101112

102-
build_path = DIR.joinpath("build")
103-
if build_path.exists():
104-
shutil.rmtree(build_path)
105-
106113
session.install("build")
107114
session.run("python", "-m", "build")

pyproject.toml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "f2py-cmake"
88
authors = [
99
{ name = "Henry Schreiner", email = "[email protected]" },
1010
]
11-
description = "Helper for F2Py and CMake"
11+
description = "CMake helpers for building F2Py modules"
1212
readme = "README.md"
1313
license.file = "LICENSE"
1414
requires-python = ">=3.8"
@@ -32,14 +32,14 @@ classifiers = [
3232
dynamic = ["version"]
3333
dependencies = []
3434

35+
[project.entry-points."cmake.module"]
36+
any = "f2py_cmake.cmake"
37+
3538
[project.optional-dependencies]
3639
test = [
3740
"pytest >=6",
38-
"pytest-cov >=3",
39-
]
40-
dev = [
41-
"pytest >=6",
42-
"pytest-cov >=3",
41+
"scikit-build-core",
42+
"numpy",
4343
]
4444
docs = [
4545
"sphinx>=7.0",
@@ -61,9 +61,11 @@ version.source = "vcs"
6161
build.hooks.vcs.version-file = "src/f2py_cmake/_version.py"
6262

6363
[tool.hatch.envs.default]
64-
features = ["test"]
65-
scripts.test = "pytest {args}"
64+
installer = "uv"
6665

66+
[tool.hatch.envs.hatch-test]
67+
features = ["test"]
68+
extra-dependencies = ["cmake", "ninja"]
6769

6870
[tool.pytest.ini_options]
6971
minversion = "6.0"
@@ -152,6 +154,5 @@ messages_control.disable = [
152154
"fixme",
153155
"line-too-long",
154156
"missing-module-docstring",
155-
"missing-function-docstring",
156157
"wrong-import-position",
157158
]

src/f2py_cmake/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Copyright (c) 2024 Henry Schreiner. All rights reserved.
33
4-
f2py-cmake: Helper for F2Py and CMake
4+
f2py-cmake: CMake helpers for building F2Py modules
55
"""
66

77
from __future__ import annotations

src/f2py_cmake/cmake/UseF2Py.cmake

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
if(CMAKE_VERSION VERSION_LESS 3.17)
2+
message(FATAL_ERROR "CMake 3.17+ required")
3+
endif()
4+
5+
include_guard(GLOBAL)
6+
7+
if(TARGET Python::NumPy)
8+
set(_Python Python)
9+
elseif(TARGET Python3::NumPy)
10+
set(_Python Python3)
11+
else()
12+
message(FATAL_ERROR "You must find Python or Python3 with the NumPy component before including F2PY!")
13+
endif()
14+
15+
execute_process(
16+
COMMAND "${${_Python}_EXECUTABLE}" -c
17+
"import numpy.f2py; print(numpy.f2py.get_include())"
18+
OUTPUT_VARIABLE F2PY_inc_output
19+
ERROR_VARIABLE F2PY_inc_error
20+
RESULT_VARIABLE F2PY_inc_result
21+
OUTPUT_STRIP_TRAILING_WHITESPACE
22+
ERROR_STRIP_TRAILING_WHITESPACE)
23+
24+
if(NOT F2PY_inc_result EQUAL 0)
25+
message(FATAL_ERROR "Can't find f2py, got ${F2PY_inc_output} ${F2PY_inc_error}")
26+
endif()
27+
28+
set(F2PY_INCLUDE_DIR "${F2PY_inc_output}" CACHE STRING "" FORCE)
29+
set(F2PY_OBJECT_FILES "${F2PY_inc_output}/fortranobject.c;${F2PY_inc_output}/fortranobject.h" CACHE STRING "" FORCE)
30+
mark_as_advanced(F2PY_INCLUDE_DIR F2PY_OBJECT_FILES)
31+
32+
add_library(F2Py::Headers IMPORTED INTERFACE)
33+
target_include_directories(F2Py::Headers INTERFACE "${F2PY_INCLUDE_DIR}")
34+
35+
function(f2py_object_library NAME TYPE)
36+
add_library(${NAME} ${TYPE} "${F2PY_INCLUDE_DIR}/fortranobject.c")
37+
target_link_libraries(${NAME} PUBLIC ${_Python}::NumPy F2Py::Headers)
38+
if("${TYPE}" STREQUAL "OBJECT")
39+
set_property(TARGET ${NAME} PROPERTY POSITION_INDEPENDENT_CODE ON)
40+
endif()
41+
endfunction()
42+
43+
function(f2py_generate_module NAME)
44+
cmake_parse_arguments(
45+
PARSE_ARGV 1
46+
F2PY
47+
"NOLOWER;F77;F90"
48+
"OUTPUT_DIR;OUTPUT_VARIABLE"
49+
"F2PY_ARGS"
50+
)
51+
set(ALL_FILES ${F2PY_UNPARSED_ARGUMENTS})
52+
53+
if(NOT ALL_FILES)
54+
message(FATAL_ERROR "One or more input files must be specified")
55+
endif()
56+
57+
if(NOT F2PY_OUTPUT_DIR)
58+
set(F2PY_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
59+
endif()
60+
61+
if(NAME MATCHES "\\.pyf$")
62+
set(_file_arg "${NAME}")
63+
get_filename_component(NAME "${NAME}" NAME_WE)
64+
else()
65+
set(_file_arg "-m ${NAME}")
66+
endif()
67+
68+
if(F2PY_F77 AND F2PY_F90)
69+
message(FATAL_ERROR "Can't specify F77 and F90")
70+
elseif(NOT F2PY_F77 AND NOT F2PY_F90)
71+
set(HAS_F90_FILE FALSE)
72+
73+
foreach(file IN LISTS ALL_FILES)
74+
if("${file}" MATCHES "\\.f90$")
75+
set(HAS_F90_FILE TRUE)
76+
break()
77+
endif()
78+
endforeach()
79+
80+
if(HAS_F90_FILE)
81+
set(F2PY_F90 ON)
82+
else()
83+
set(F2PY_F77 ON)
84+
endif()
85+
endif()
86+
87+
if(F2PY_F77)
88+
set(wrapper_files ${NAME}-f2pywrappers.f)
89+
else()
90+
set(wrapper_files ${NAME}-f2pywrappers.f ${NAME}-f2pywrappers2.f90)
91+
endif()
92+
93+
if(F2PY_NOLOWER)
94+
set(lower "--no-lower")
95+
else()
96+
set(lower "--lower")
97+
endif()
98+
99+
set(abs_all_files)
100+
foreach(file IN LISTS ALL_FILES)
101+
if(IS_ABSOLUTE "${file}")
102+
list(APPEND abs_all_files "${file}")
103+
else()
104+
list(APPEND abs_all_files "${CMAKE_CURRENT_SOURCE_DIR}/${file}")
105+
endif()
106+
endforeach()
107+
108+
add_custom_command(
109+
OUTPUT ${NAME}module.c ${wrapper_files}
110+
DEPENDS ${ALL_FILES}
111+
VERBATIM
112+
COMMAND
113+
"${${_Python}_EXECUTABLE}" -m numpy.f2py
114+
"${abs_all_files}" ${_file_arg} ${lower} ${F2PY_F2PY_ARGS}
115+
COMMAND
116+
"${CMAKE_COMMAND}" -E touch ${wrapper_files}
117+
WORKING_DIRECTORY "${F2PY_OUTPUT_DIR}"
118+
COMMENT
119+
"F2PY making ${NAME} wrappers"
120+
)
121+
122+
if(F2PY_OUTPUT_VARIABLE)
123+
set(${F2PY_OUTPUT_VARIABLE} ${NAME}module.c ${wrapper_files} PARENT_SCOPE)
124+
endif()
125+
endfunction()

src/f2py_cmake/cmake/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)