Skip to content

Commit 19c2d4c

Browse files
authored
fix: modern PyO3 example, better compiled docs (#715)
* docs: mention names of modules Signed-off-by: Henry Schreiner <[email protected]> * fix: modernize Rust example, and render into docs Signed-off-by: Henry Schreiner <[email protected]> * fix: drop one unneeded line Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent a310c80 commit 19c2d4c

File tree

6 files changed

+147
-59
lines changed

6 files changed

+147
-59
lines changed

docs/_includes/pyproject.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ Changelog = "https://github.com/org/package/releases"
5353
<!-- prettier-ignore-end -->
5454
<!-- [[[end]]] -->
5555

56-
You can read more about each field, and all allowed fields, in
56+
In this example, `"package"` is the name of the thing you are working on. You
57+
can read more about each field, and all allowed fields, in
5758
[packaging.python.org][metadata],
5859
[Flit](https://flit.readthedocs.io/en/latest/pyproject_toml.html#new-style-metadata)
5960
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Only the

docs/pages/guides/packaging_compiled.md

Lines changed: 116 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ parent: Topical Guides
88

99
{% include toc.html %}
1010

11+
<!-- [[[cog
12+
from cog_helpers import code_fence, render_cookie, TOMLMatcher
13+
with render_cookie(backend="skbuild", vcs=False) as skbuild:
14+
skbuild_cmakelists_txt = skbuild.joinpath("CMakeLists.txt").read_text(encoding="utf-8").strip()
15+
skbuild_src_main_cpp = skbuild.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
16+
skbuild_pyproject = TOMLMatcher.from_file(skbuild / "pyproject.toml")
17+
with render_cookie(backend="mesonpy", vcs=False) as mesonpy:
18+
mesonpy_meson_build = mesonpy.joinpath("meson.build").read_text(encoding="utf-8").strip()
19+
mesonpy_src_main_cpp = mesonpy.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
20+
mesonpy_pyproject = TOMLMatcher.from_file(mesonpy / "pyproject.toml")
21+
assert "tool.meson-python" not in mesonpy_pyproject
22+
with render_cookie(backend="maturin", vcs=False) as maturin:
23+
maturin_cargo_toml = maturin.joinpath("Cargo.toml").read_text(encoding="utf-8").strip()
24+
maturin_src_lib_rs = maturin.joinpath("src/lib.rs").read_text(encoding="utf-8").strip()
25+
maturin_pyproject = TOMLMatcher.from_file(maturin / "pyproject.toml")
26+
]]] -->
27+
<!-- [[[end]]] -->
28+
1129
# Packaging Compiled Projects
1230

1331
There are a variety of ways to package compiled projects. In the past, the only
@@ -52,27 +70,48 @@ selects the backend:
5270

5371
{% tabs %} {% tab skbc Scikit-build-core %}
5472

73+
<!-- [[[cog
74+
with code_fence("toml"):
75+
print(skbuild_pyproject.get_source("build-system"))
76+
]]] -->
77+
<!-- prettier-ignore-start -->
5578
```toml
5679
[build-system]
57-
requires = ["scikit-build-core"]
80+
requires = ["pybind11", "scikit-build-core>=0.11"]
5881
build-backend = "scikit_build_core.build"
5982
```
83+
<!-- prettier-ignore-end -->
84+
<!-- [[[end]]] -->
6085

6186
{% endtab %} {% tab meson Meson-python %}
6287

88+
<!-- [[[cog
89+
with code_fence("toml"):
90+
print(mesonpy_pyproject.get_source("build-system"))
91+
]]] -->
92+
<!-- prettier-ignore-start -->
6393
```toml
6494
[build-system]
65-
requires = ["meson-python"]
95+
requires = ["meson-python>=0.18", "pybind11"]
6696
build-backend = "mesonpy"
6797
```
98+
<!-- prettier-ignore-end -->
99+
<!-- [[[end]]] -->
68100

69101
{% endtab %} {% tab maturin Maturin %}
70102

103+
<!-- [[[cog
104+
with code_fence("toml"):
105+
print(maturin_pyproject.get_source("build-system"))
106+
]]] -->
107+
<!-- prettier-ignore-start -->
71108
```toml
72109
[build-system]
73-
requires = ["maturin"]
110+
requires = ["maturin>=1.9,<2"]
74111
build-backend = "maturin"
75112
```
113+
<!-- prettier-ignore-end -->
114+
<!-- [[[end]]] -->
76115

77116
{% endtab %} {% endtabs %}
78117

@@ -83,20 +122,48 @@ build-backend = "maturin"
83122
These tools all read the project table. They also have extra configuration
84123
options in `tool.*` settings.
85124

125+
{% tabs %} {% tab skbc Scikit-build-core %}
126+
86127
<!-- [[[cog
87-
from cog_helpers import code_fence, render_cookie
88-
with render_cookie(backend="skbuild") as skbuild:
89-
skbuild_cmakelists_txt = skbuild.joinpath("CMakeLists.txt").read_text(encoding="utf-8").strip()
90-
skbuild_src_main_cpp = skbuild.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
91-
with render_cookie(backend="mesonpy") as mesonpy:
92-
mesonpy_meson_build = mesonpy.joinpath("meson.build").read_text(encoding="utf-8").strip()
93-
mesonpy_src_main_cpp = mesonpy.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
94-
with render_cookie(backend="maturin") as maturin:
95-
maturin_cargo_toml = maturin.joinpath("Cargo.toml").read_text(encoding="utf-8").strip()
96-
maturin_src_lib_rs = maturin.joinpath("src/lib.rs").read_text(encoding="utf-8").strip()
128+
with code_fence("toml"):
129+
print(skbuild_pyproject.get_source("tool.scikit-build"))
97130
]]] -->
131+
<!-- prettier-ignore-start -->
132+
```toml
133+
[tool.scikit-build]
134+
minimum-version = "build-system.requires"
135+
build-dir = "build/{wheel_tag}"
136+
```
137+
<!-- prettier-ignore-end -->
98138
<!-- [[[end]]] -->
99139

140+
These options are not required, but can improve your experience.
141+
142+
{% endtab %} {% tab meson Meson-python %}
143+
144+
No `tool.meson-python` configuration required for this example.
145+
146+
{% endtab %} {% tab maturin Maturin %}
147+
148+
<!-- [[[cog
149+
with code_fence("toml"):
150+
print(maturin_pyproject.get_source("tool.maturin"))
151+
]]] -->
152+
<!-- prettier-ignore-start -->
153+
```toml
154+
[tool.maturin]
155+
module-name = "package._core"
156+
python-source = "src"
157+
sdist-generator = "git" # default is cargo
158+
```
159+
<!-- prettier-ignore-end -->
160+
<!-- [[[end]]] -->
161+
162+
Maturin assumes you follow Rust's package structure, so we need a little bit of
163+
configuration here to follow the convention of the other tools here.
164+
165+
{% endtab %} {% endtabs %}
166+
100167
## Backend specific files
101168

102169
{% tabs %} {% tab skbc Scikit-build-core %}
@@ -180,18 +247,18 @@ with code_fence("toml"):
180247
[package]
181248
name = "package"
182249
version = "0.1.0"
183-
edition = "2018"
250+
edition = "2021"
184251

185252
[lib]
186253
name = "_core"
187254
# "cdylib" is necessary to produce a shared library for Python to import from.
188255
crate-type = ["cdylib"]
189256

190257
[dependencies]
191-
rand = "0.8.3"
258+
rand = "0.9.2"
192259

193260
[dependencies.pyo3]
194-
version = "0.19.1"
261+
version = "0.27.2"
195262
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
196263
# "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10
197264
features = ["extension-module", "abi3-py310"]
@@ -203,6 +270,11 @@ features = ["extension-module", "abi3-py310"]
203270

204271
## Example compiled file
205272

273+
This example will make a `_core` extension inside your package; this pattern
274+
allows you to easily provide both Python files and compiled extensions, and
275+
keeping the details of your compiled extension private. You can select whatever
276+
name you wish, though, or even make your compiled extension a top level module.
277+
206278
{% tabs %} {% tab skbc Scikit-build-core %}
207279

208280
Example `src/main.cpp` file:
@@ -297,26 +369,29 @@ with code_fence("rs"):
297369
```rs
298370
use pyo3::prelude::*;
299371

300-
#[pyfunction]
301-
fn add(x: i64, y: i64) -> i64 {
302-
x + y
303-
}
304-
305-
#[pyfunction]
306-
fn subtract(x: i64, y: i64) -> i64 {
307-
x - y
308-
}
309-
310372
/// A Python module implemented in Rust. The name of this function must match
311373
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
312374
/// import the module.
313375
#[pymodule]
314-
fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
315-
m.add_function(wrap_pyfunction!(add, m)?)?;
316-
m.add_function(wrap_pyfunction!(subtract, m)?)?;
317-
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
376+
mod _core {
377+
use super::*;
378+
379+
#[pyfunction]
380+
fn add(x: i64, y: i64) -> i64 {
381+
x + y
382+
}
383+
384+
#[pyfunction]
385+
fn subtract(x: i64, y: i64) -> i64 {
386+
x - y
387+
}
388+
318389

319-
Ok(())
390+
#[pymodule_init]
391+
fn pymodule_init(m: &Bound<'_, PyModule>) -> PyResult<()> {
392+
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
393+
Ok(())
394+
}
320395
}
321396
```
322397
<!-- prettier-ignore-end -->
@@ -327,7 +402,8 @@ fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
327402
## Package structure
328403

329404
The recommendation (followed above) is to have source code in `/src`, and the
330-
Python package files in `/src/<package>`.
405+
Python package files in `/src/<package>`. The compiled files also can go in
406+
`/src`.
331407

332408
## Versioning
333409

@@ -343,25 +419,26 @@ though the defaults are reasonable.
343419

344420
Unlike pure Python, you'll need to build redistributable wheels for each
345421
platform and supported Python version if you want to avoid compilation on the
346-
user's system. See [the CI page on wheels][gha_wheels] for a suggested workflow.
422+
user's system using cibuildwheel. See [the CI page on wheels][gha_wheels] for a
423+
suggested workflow.
347424

348425
## Special considerations
349426

350427
### NumPy
351428

352429
Modern versions of NumPy (1.25+) allow you to target older versions when
353-
building, which is _highly_ recommended, and this will become required in NumPy
354-
2.0. Now you add:
430+
building, which is _highly_ recommended, and this became required in NumPy 2.0.
431+
Now you add:
355432

356433
```cpp
357434
#define NPY_TARGET_VERSION NPY_1_22_API_VERSION
358435
```
359436
360437
(Where that number is whatever version you support as a minimum) then make sure
361-
you build with NumPy 1.25+ (or 2.0+ when it comes out). Before 1.25, it was
362-
necessary to actually pin the oldest NumPy you supported (the
363-
`oldest-supported-numpy` package is the easiest method). If you support Python <
364-
3.9, you'll have to use the old method for those versions.
438+
you build with NumPy 1.25+ (or 2.0+). Before 1.25, it was necessary to actually
439+
pin the oldest NumPy you supported (the `oldest-supported-numpy` package is the
440+
easiest method). If you support Python < 3.9, you'll have to use the old method
441+
for those versions.
365442
366443
If using pybind11, you don't need NumPy at build-time in the first place.
367444

helpers/cog_helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ def get_source(self, dotted_name: str, /) -> str:
7373
)
7474
return tomlkit.dumps(toml).strip()
7575

76+
def __contains__(self, dotted_name: str, /) -> bool:
77+
names = dotted_name.split(".")
78+
try:
79+
functools.reduce(lambda d, k: d[k], names, self.toml)
80+
except KeyError:
81+
return False
82+
return True
83+
7684

7785
@contextlib.contextmanager
7886
def code_fence(lang: str, /, *, width: int = 3) -> Generator[None, None, None]:

{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ sdist.include = ["src/{{ cookiecutter.__project_slug }}/_version.py"]
162162

163163
[tool.maturin]
164164
module-name = "{{ cookiecutter.__project_slug }}._core"
165-
python-packages = ["{{ cookiecutter.__project_slug }}"]
166165
python-source = "src"
167166
sdist-generator = "git" # default is cargo
168167

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
use pyo3::prelude::*;
22

3-
#[pyfunction]
4-
fn add(x: i64, y: i64) -> i64 {
5-
x + y
6-
}
7-
8-
#[pyfunction]
9-
fn subtract(x: i64, y: i64) -> i64 {
10-
x - y
11-
}
12-
133
/// A Python module implemented in Rust. The name of this function must match
144
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
155
/// import the module.
166
#[pymodule]
17-
fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
18-
m.add_function(wrap_pyfunction!(add, m)?)?;
19-
m.add_function(wrap_pyfunction!(subtract, m)?)?;
20-
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
7+
mod _core {
8+
use super::*;
9+
10+
#[pyfunction]
11+
fn add(x: i64, y: i64) -> i64 {
12+
x + y
13+
}
14+
15+
#[pyfunction]
16+
fn subtract(x: i64, y: i64) -> i64 {
17+
x - y
18+
}
19+
2120

22-
Ok(())
21+
#[pymodule_init]
22+
fn pymodule_init(m: &Bound<'_, PyModule>) -> PyResult<()> {
23+
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
24+
Ok(())
25+
}
2326
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
[package]
22
name = "{{ cookiecutter.__project_slug }}"
33
version = "0.1.0"
4-
edition = "2018"
4+
edition = "2021"
55

66
[lib]
77
name = "_core"
88
# "cdylib" is necessary to produce a shared library for Python to import from.
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12-
rand = "0.8.3"
12+
rand = "0.9.2"
1313

1414
[dependencies.pyo3]
15-
version = "0.19.1"
15+
version = "0.27.2"
1616
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
1717
# "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10
1818
features = ["extension-module", "abi3-py310"]

0 commit comments

Comments
 (0)