diff --git a/noxfile.py b/noxfile.py index dcea53ff427..4bc5307e9f6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1485,6 +1485,37 @@ def _ensure_directory_equals(expected_dir: Path, actual_dir: Path): ) +@nox.session(name="test-introspection-backward-compatibility") +def test_introspection_backward_compatibility(session: nox.Session): + session.install("maturin") + session.install("ruff") + for crate in Path("pytests/backward-compatibility").iterdir(): + if not crate.is_dir(): + continue + with tempfile.TemporaryDirectory() as stub_dir: + cargo_toml_file = crate / "Cargo.toml" + _run(session, "maturin", "develop", "-m", str(cargo_toml_file)) + package_name = toml.loads(cargo_toml_file.read_text())["lib"]["name"] + lib_file = session.run( + "python", + "-c", + f"import {package_name}; print({package_name}.{package_name}.__file__)", + silent=True, + ).strip() + _run_cargo( + session, + "run", + "-p", + "pyo3-introspection", + "--", + lib_file, + package_name, + stub_dir, + ) + _run(session, "ruff", "format", stub_dir) + _ensure_directory_equals(Path(stub_dir), crate / "stubs") + + @lru_cache() def _get_rust_info() -> Tuple[str, ...]: output = _get_output("rustc", "-vV") diff --git a/pytests/backward-compatibility/0.28/Cargo.toml b/pytests/backward-compatibility/0.28/Cargo.toml new file mode 100644 index 00000000000..d01d671edad --- /dev/null +++ b/pytests/backward-compatibility/0.28/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] + +[package] +name = "pyo3-backward-compatibility-028" +version = "0.0.0" +description = "Backward compatibility tests for pyo3-introspection" +edition = "2021" +publish = false +rust-version = "1.83" + +[dependencies] +pyo3 = { version = "0.28", features = ["experimental-inspect"] } + +[lib] +name = "pyo3_backward_compatibility_028" +crate-type = ["cdylib"] diff --git a/pytests/backward-compatibility/0.28/src/lib.rs b/pytests/backward-compatibility/0.28/src/lib.rs new file mode 100644 index 00000000000..298d9da62dc --- /dev/null +++ b/pytests/backward-compatibility/0.28/src/lib.rs @@ -0,0 +1,87 @@ +use pyo3::prelude::*; + +/// Some module +#[pymodule] +mod pyo3_backward_compatibility_028 { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyTuple, PyType}; + use std::collections::HashMap; + use std::path::PathBuf; + + /// Some const + #[pymodule_export] + const CONST: usize = 0; + + /// Some function + #[pyfunction] + #[pyo3(signature = (_arg1, /, _arg2: "int", *_args, _foo = None, **_kwargs))] + fn some_fn( + _arg1: (usize, Vec, HashMap), + _arg2: Bound<'_, PyAny>, + _args: Bound<'_, PyTuple>, + _foo: Option<&str>, + _kwargs: Option>, + ) -> PyResult<()> { + Ok(()) + } + + /// Some class + #[pyclass(eq, ord, extends = PyDict)] + #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + struct MyClass { + // TODO: parent class + value: usize, + } + + #[pymethods] + impl MyClass { + #[expect(dead_code)] + const PI: Self = Self { value: 3 }; + + #[new] + fn new(value: usize) -> Self { + Self { value } + } + + #[getter] + fn value(&self) -> usize { + self.value + } + + #[setter] + fn set_value(&mut self, value: usize) { + self.value = value; + } + + #[deleter] + fn delete_value(&self) {} + + #[staticmethod] + fn static_method() -> bool { + true + } + + #[classmethod] + fn class_method(_cls: Bound<'_, PyType>) -> &'static str { + "foo" + } + + #[classattr] + fn class_attr() -> f32 { + 0. + } + } + + #[pymodule] + mod submodule { + use super::*; + + #[pyclass(subclass)] + struct Class2 {} + } + + #[pymodule_init] + fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) + } +} diff --git a/pytests/backward-compatibility/0.28/stubs/__init__.pyi b/pytests/backward-compatibility/0.28/stubs/__init__.pyi new file mode 100644 index 00000000000..977b4912c48 --- /dev/null +++ b/pytests/backward-compatibility/0.28/stubs/__init__.pyi @@ -0,0 +1,39 @@ +from _typeshed import Incomplete +from collections.abc import Sequence +from os import PathLike +from typing import Final, final + +CONST: Final = 0 + +@final +class MyClass(dict): + def __eq__(self, /, other: MyClass) -> bool: ... + def __ge__(self, /, other: MyClass) -> bool: ... + def __gt__(self, /, other: MyClass) -> bool: ... + def __le__(self, /, other: MyClass) -> bool: ... + def __lt__(self, /, other: MyClass) -> bool: ... + def __ne__(self, /, other: MyClass) -> bool: ... + def __new__(cls, /, value: int) -> MyClass: ... + @classmethod + @property + def class_attr(cls, /) -> float: ... + @classmethod + def class_method(cls, /) -> str: ... + @staticmethod + def static_method() -> bool: ... + @property + def value(self, /) -> int: ... + @value.deleter + def value(self, /) -> None: ... + @value.setter + def value(self, /, value: int) -> None: ... + +def some_fn( + _arg1: tuple[int, Sequence[str | PathLike], dict[str, int]], + /, + _arg2: "int", + *_args, + _foo: str | None = None, + **_kwargs, +) -> None: ... +def __getattr__(name: str) -> Incomplete: ... diff --git a/pytests/backward-compatibility/0.28/stubs/submodule.pyi b/pytests/backward-compatibility/0.28/stubs/submodule.pyi new file mode 100644 index 00000000000..7db969c901a --- /dev/null +++ b/pytests/backward-compatibility/0.28/stubs/submodule.pyi @@ -0,0 +1 @@ +class Class2: ... diff --git a/pytests/backward-compatibility/0.29/Cargo.toml b/pytests/backward-compatibility/0.29/Cargo.toml new file mode 100644 index 00000000000..59dc33b90c7 --- /dev/null +++ b/pytests/backward-compatibility/0.29/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] + +[package] +name = "pyo3-backward-compatibility-029" +version = "0.0.0" +description = "Backward compatibility tests for pyo3-introspection" +edition = "2021" +publish = false +rust-version = "1.83" + +[dependencies] +pyo3 = { path = "../../..", features = ["experimental-inspect"] } + +[lib] +name = "pyo3_backward_compatibility_029" +crate-type = ["cdylib"] diff --git a/pytests/backward-compatibility/0.29/src/lib.rs b/pytests/backward-compatibility/0.29/src/lib.rs new file mode 100644 index 00000000000..5b6986da079 --- /dev/null +++ b/pytests/backward-compatibility/0.29/src/lib.rs @@ -0,0 +1,87 @@ +use pyo3::prelude::*; + +/// Some module +#[pymodule] +mod pyo3_backward_compatibility_029 { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyTuple, PyType}; + use std::collections::HashMap; + use std::path::PathBuf; + + /// Some const + #[pymodule_export] + const CONST: usize = 0; + + /// Some function + #[pyfunction] + #[pyo3(signature = (_arg1, /, _arg2: "int", *_args, _foo = None, **_kwargs))] + fn some_fn( + _arg1: (usize, Vec, HashMap), + _arg2: Bound<'_, PyAny>, + _args: Bound<'_, PyTuple>, + _foo: Option<&str>, + _kwargs: Option>, + ) -> PyResult<()> { + Ok(()) + } + + /// Some class + #[pyclass(eq, ord, extends = PyDict)] + #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + struct MyClass { + // TODO: parent class + value: usize, + } + + #[pymethods] + impl MyClass { + #[expect(dead_code)] + const PI: Self = Self { value: 3 }; + + #[new] + fn new(value: usize) -> Self { + Self { value } + } + + #[getter] + fn value(&self) -> usize { + self.value + } + + #[setter] + fn set_value(&mut self, value: usize) { + self.value = value; + } + + #[deleter] + fn delete_value(&self) {} + + #[staticmethod] + fn static_method() -> bool { + true + } + + #[classmethod] + fn class_method(_cls: Bound<'_, PyType>) -> &'static str { + "foo" + } + + #[classattr] + fn class_attr() -> f32 { + 0. + } + } + + #[pymodule] + mod submodule { + use super::*; + + #[pyclass(subclass)] + struct Class2 {} + } + + #[pymodule_init] + fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) + } +} diff --git a/pytests/backward-compatibility/0.29/stubs/__init__.pyi b/pytests/backward-compatibility/0.29/stubs/__init__.pyi new file mode 100644 index 00000000000..92daca25f93 --- /dev/null +++ b/pytests/backward-compatibility/0.29/stubs/__init__.pyi @@ -0,0 +1,52 @@ +""" +Some module +""" + +from _typeshed import Incomplete +from collections.abc import Sequence +from os import PathLike +from typing import Final, final + +CONST: Final = 0 +""" +Some const +""" + +@final +class MyClass(dict): + """ + Some class + """ + + class_attr: Final[float] + def __eq__(self, /, other: object) -> bool: ... + def __ge__(self, /, other: object) -> bool: ... + def __gt__(self, /, other: object) -> bool: ... + def __le__(self, /, other: object) -> bool: ... + def __lt__(self, /, other: object) -> bool: ... + def __ne__(self, /, other: object) -> bool: ... + def __new__(cls, /, value: int) -> MyClass: ... + @classmethod + def class_method(cls, /) -> str: ... + @staticmethod + def static_method() -> bool: ... + @property + def value(self, /) -> int: ... + @value.deleter + def value(self, /) -> None: ... + @value.setter + def value(self, /, value: int) -> None: ... + +def some_fn( + _arg1: tuple[int, Sequence[str | PathLike[str]], dict[str, int]], + /, + _arg2: "int", + *_args, + _foo: str | None = None, + **_kwargs, +) -> None: + """ + Some function + """ + +def __getattr__(name: str) -> Incomplete: ... diff --git a/pytests/backward-compatibility/0.29/stubs/submodule.pyi b/pytests/backward-compatibility/0.29/stubs/submodule.pyi new file mode 100644 index 00000000000..7db969c901a --- /dev/null +++ b/pytests/backward-compatibility/0.29/stubs/submodule.pyi @@ -0,0 +1 @@ +class Class2: ... diff --git a/pytests/backward-compatibility/README.md b/pytests/backward-compatibility/README.md new file mode 100644 index 00000000000..325315270e7 --- /dev/null +++ b/pytests/backward-compatibility/README.md @@ -0,0 +1 @@ +A set of crates to test `pyo3-introspection` backward compatibility support. \ No newline at end of file