Skip to content

Commit f335f42

Browse files
authored
Merge pull request #3446 from davidhewitt/relax-import-check
relax multiple-import check to only prevent subinterpreters
2 parents b9e9859 + 1a349c2 commit f335f42

File tree

4 files changed

+98
-23
lines changed

4 files changed

+98
-23
lines changed

guide/src/building_and_distribution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ If you encounter these or other complications when linking the interpreter stati
276276
277277
### Import your module when embedding the Python interpreter
278278
279-
When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_interpreter`, or `Python::with_gil` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.)
279+
When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_python_interpreter`, or `Python::with_gil` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.)
280280
281281
## Cross Compiling
282282

newsfragments/3446.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`#[pymodule]` will now return the same module object on repeated import by the same Python interpreter, on Python 3.9 and up.

pytests/tests/test_misc.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib
22
import platform
3+
import sys
34

45
import pyo3_pytests.misc
56
import pytest
@@ -10,15 +11,40 @@ def test_issue_219():
1011
pyo3_pytests.misc.issue_219()
1112

1213

14+
@pytest.mark.xfail(
15+
platform.python_implementation() == "CPython" and sys.version_info < (3, 9),
16+
reason="Cannot identify subinterpreters on Python older than 3.9",
17+
)
18+
def test_multiple_imports_same_interpreter_ok():
19+
spec = importlib.util.find_spec("pyo3_pytests.pyo3_pytests")
20+
21+
module = importlib.util.module_from_spec(spec)
22+
assert dir(module) == dir(pyo3_pytests.pyo3_pytests)
23+
24+
25+
@pytest.mark.xfail(
26+
platform.python_implementation() == "CPython" and sys.version_info < (3, 9),
27+
reason="Cannot identify subinterpreters on Python older than 3.9",
28+
)
1329
@pytest.mark.skipif(
1430
platform.python_implementation() == "PyPy",
15-
reason="PyPy does not reinitialize the module (appears to be some internal caching)",
31+
reason="PyPy does not support subinterpreters",
1632
)
17-
def test_second_module_import_fails():
18-
spec = importlib.util.find_spec("pyo3_pytests.pyo3_pytests")
33+
def test_import_in_subinterpreter_forbidden():
34+
import _xxsubinterpreters
1935

36+
if sys.version_info < (3, 12):
37+
expected_error = "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576"
38+
else:
39+
expected_error = "module pyo3_pytests.pyo3_pytests does not support loading in subinterpreters"
40+
41+
sub_interpreter = _xxsubinterpreters.create()
2042
with pytest.raises(
21-
ImportError,
22-
match="PyO3 modules may only be initialized once per interpreter process",
43+
_xxsubinterpreters.RunFailedError,
44+
match=expected_error,
2345
):
24-
importlib.util.module_from_spec(spec)
46+
_xxsubinterpreters.run_string(
47+
sub_interpreter, "import pyo3_pytests.pyo3_pytests"
48+
)
49+
50+
_xxsubinterpreters.destroy(sub_interpreter)

src/impl_/pymodule.rs

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
//! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code.
22
3-
use std::{
4-
cell::UnsafeCell,
5-
sync::atomic::{self, AtomicBool},
6-
};
3+
use std::cell::UnsafeCell;
74

8-
use crate::{exceptions::PyImportError, ffi, types::PyModule, Py, PyResult, Python};
5+
#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
6+
use std::sync::atomic::{AtomicI64, Ordering};
7+
8+
#[cfg(not(PyPy))]
9+
use crate::exceptions::PyImportError;
10+
use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python};
911

1012
/// `Sync` wrapper of `ffi::PyModuleDef`.
1113
pub struct ModuleDef {
1214
// wrapped in UnsafeCell so that Rust compiler treats this as interior mutability
1315
ffi_def: UnsafeCell<ffi::PyModuleDef>,
1416
initializer: ModuleInitializer,
15-
initialized: AtomicBool,
17+
/// Interpreter ID where module was initialized (not applicable on PyPy).
18+
#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
19+
interpreter: AtomicI64,
20+
/// Initialized module object, cached to avoid reinitialization.
21+
module: GILOnceCell<Py<PyModule>>,
1622
}
1723

1824
/// Wrapper to enable initializer to be used in const fns.
@@ -51,7 +57,10 @@ impl ModuleDef {
5157
ModuleDef {
5258
ffi_def,
5359
initializer,
54-
initialized: AtomicBool::new(false),
60+
// -1 is never expected to be a valid interpreter ID
61+
#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
62+
interpreter: AtomicI64::new(-1),
63+
module: GILOnceCell::new(),
5564
}
5665
}
5766
/// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule].
@@ -71,16 +80,55 @@ impl ModuleDef {
7180
))?;
7281
}
7382
}
74-
let module = unsafe {
75-
Py::<PyModule>::from_owned_ptr_or_err(py, ffi::PyModule_Create(self.ffi_def.get()))?
76-
};
77-
if self.initialized.swap(true, atomic::Ordering::SeqCst) {
78-
return Err(PyImportError::new_err(
79-
"PyO3 modules may only be initialized once per interpreter process",
80-
));
83+
// Check the interpreter ID has not changed, since we currently have no way to guarantee
84+
// that static data is not reused across interpreters.
85+
//
86+
// PyPy does not have subinterpreters, so no need to check interpreter ID.
87+
#[cfg(not(PyPy))]
88+
{
89+
// PyInterpreterState_Get is only available on 3.9 and later, but is missing
90+
// from python3.dll for Windows stable API on 3.9
91+
#[cfg(all(Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
92+
{
93+
let current_interpreter =
94+
unsafe { ffi::PyInterpreterState_GetID(ffi::PyInterpreterState_Get()) };
95+
crate::err::error_on_minusone(py, current_interpreter)?;
96+
if let Err(initialized_interpreter) = self.interpreter.compare_exchange(
97+
-1,
98+
current_interpreter,
99+
Ordering::SeqCst,
100+
Ordering::SeqCst,
101+
) {
102+
if initialized_interpreter != current_interpreter {
103+
return Err(PyImportError::new_err(
104+
"PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576",
105+
));
106+
}
107+
}
108+
}
109+
#[cfg(not(all(Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10))))))]
110+
{
111+
// CPython before 3.9 does not have APIs to check the interpreter ID, so best that can be
112+
// done to guard against subinterpreters is fail if the module is initialized twice
113+
if self.module.get(py).is_some() {
114+
return Err(PyImportError::new_err(
115+
"PyO3 modules compiled for CPython 3.8 or older may only be initialized once per interpreter process"
116+
));
117+
}
118+
}
81119
}
82-
(self.initializer.0)(py, module.as_ref(py))?;
83-
Ok(module)
120+
self.module
121+
.get_or_try_init(py, || {
122+
let module = unsafe {
123+
Py::<PyModule>::from_owned_ptr_or_err(
124+
py,
125+
ffi::PyModule_Create(self.ffi_def.get()),
126+
)?
127+
};
128+
(self.initializer.0)(py, module.as_ref(py))?;
129+
Ok(module)
130+
})
131+
.map(|py_module| py_module.clone_ref(py))
84132
}
85133
}
86134

0 commit comments

Comments
 (0)