diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..76781ef --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,37 @@ +name: Python + +on: + push: + branches: + - "main" + pull_request: + workflow_dispatch: + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + + - name: Build and install + run: | + pip install -v ./pyo3-stub-gen-testing[test] + + - name: Python Test + run: pytest diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e26789e..39f2c66 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -92,3 +92,25 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + + stub-gen: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Generate stub file + uses: actions-rs/cargo@v1 + with: + command: run + args: --bin stub_gen --features stub_gen diff --git a/.gitignore b/.gitignore index 6985cf1..b53f887 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,10 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +# PyO3 development +.venv*/ +__pycache__/ +*.so +*.pyc +dist/ diff --git a/Cargo.toml b/Cargo.toml index 5952362..5ea8c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -members = ["pyo3-stub-gen", "pyo3-stub-gen-derive"] +members = [ + "pyo3-stub-gen", + "pyo3-stub-gen-derive", + "pyo3-stub-gen-testing" +] resolver = "2" [workspace.dependencies] @@ -12,6 +16,6 @@ inventory = "0.3.14" itertools = "0.12.0" prettyplease = "0.2.16" proc-macro2 = "1.0.74" -pyo3 = "0.20.1" +pyo3 = { version = "0.20.1", features = ["experimental-inspect"] } quote = "1.0.35" syn = "2.0.46" diff --git a/pyo3-stub-gen-derive/Cargo.toml b/pyo3-stub-gen-derive/Cargo.toml index 5b10ef4..1228f28 100644 --- a/pyo3-stub-gen-derive/Cargo.toml +++ b/pyo3-stub-gen-derive/Cargo.toml @@ -17,4 +17,4 @@ pyo3-stub-gen.workspace = true insta.workspace = true inventory.workspace = true prettyplease.workspace = true -pyo3 = { workspace = true, features = ["experimental-inspect"] } +pyo3.workspace = true diff --git a/pyo3-stub-gen-derive/src/gen_stub.rs b/pyo3-stub-gen-derive/src/gen_stub.rs index d819716..06d069a 100644 --- a/pyo3-stub-gen-derive/src/gen_stub.rs +++ b/pyo3-stub-gen-derive/src/gen_stub.rs @@ -91,7 +91,6 @@ pub fn pyclass(item: TokenStream2) -> Result { let inner = PyClassInfo::try_from(parse2::(item.clone())?)?; Ok(quote! { #item - #[cfg(feature = "gen_stub")] inventory::submit! { #inner } @@ -102,7 +101,6 @@ pub fn pyclass_enum(item: TokenStream2) -> Result { let inner = PyEnumInfo::try_from(parse2::(item.clone())?)?; Ok(quote! { #item - #[cfg(feature = "gen_stub")] inventory::submit! { #inner } @@ -113,7 +111,6 @@ pub fn pymethods(item: TokenStream2) -> Result { let inner = PyMethodsInfo::try_from(parse2::(item.clone())?)?; Ok(quote! { #item - #[cfg(feature = "gen_stub")] inventory::submit! { #inner } @@ -125,7 +122,6 @@ pub fn pyfunction(attr: TokenStream2, item: TokenStream2) -> Result TokenStream2 { // PyO3 reference: https://docs.rs/pyo3/latest/pyo3/pyclass/enum.CompareOp.html // PEP: https://peps.python.org/pep-0207/ if last_seg.ident == "CompareOp" { - quote! { crate::stub::compare_op_type_input } + quote! { ::pyo3_stub_gen::type_info::compare_op_type_input } } else { quote! { <#ty as FromPyObject>::type_input } } @@ -105,7 +105,7 @@ impl ToTokens for ArgInfo { let Self { name, r#type: ty } = self; let type_tt = type_to_token(ty); tokens.append_all(quote! { - crate::stub::ArgInfo { name: #name, r#type: #type_tt } + ::pyo3_stub_gen::type_info::ArgInfo { name: #name, r#type: #type_tt } }); } } diff --git a/pyo3-stub-gen-derive/src/gen_stub/member.rs b/pyo3-stub-gen-derive/src/gen_stub/member.rs index 5db9f4d..6f7f628 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/member.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/member.rs @@ -66,7 +66,7 @@ impl ToTokens for MemberInfo { let Self { name, r#type: ty } = self; let name = name.strip_prefix("get_").unwrap_or(name); tokens.append_all(quote! { - crate::stub::MemberInfo { + ::pyo3_stub_gen::type_info::MemberInfo { name: #name, r#type: <#ty as IntoPy>::type_output } diff --git a/pyo3-stub-gen-derive/src/gen_stub/method.rs b/pyo3-stub-gen-derive/src/gen_stub/method.rs index 45901d0..548b5e7 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/method.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/method.rs @@ -102,10 +102,10 @@ impl ToTokens for MethodInfo { let ret_tt = if let Some(ret) = ret { quote! { <#ret as IntoPy<::pyo3::PyObject>>::type_output } } else { - quote! { crate::stub::no_return_type_output } + quote! { ::pyo3_stub_gen::type_info::no_return_type_output } }; tokens.append_all(quote! { - crate::stub::MethodInfo { + ::pyo3_stub_gen::type_info::MethodInfo { name: #name, args: &[ #(#args),* ], r#return: #ret_tt, diff --git a/pyo3-stub-gen-derive/src/gen_stub/new.rs b/pyo3-stub-gen-derive/src/gen_stub/new.rs index d136a59..8d01638 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/new.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/new.rs @@ -41,7 +41,7 @@ impl ToTokens for NewInfo { let Self { args, sig } = self; let sig_tt = quote_option(sig); tokens.append_all(quote! { - crate::stub::NewInfo { + ::pyo3_stub_gen::type_info::NewInfo { args: &[ #(#args),* ], signature: #sig_tt, } diff --git a/pyo3-stub-gen-derive/src/gen_stub/pyclass.rs b/pyo3-stub-gen-derive/src/gen_stub/pyclass.rs index 02d5bcc..5f70d02 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/pyclass.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/pyclass.rs @@ -64,7 +64,7 @@ impl ToTokens for PyClassInfo { } = self; let module = quote_option(module); tokens.append_all(quote! { - crate::stub::PyClassInfo { + ::pyo3_stub_gen::type_info::PyClassInfo { pyclass_name: #pyclass_name, struct_id: std::any::TypeId::of::<#struct_type>, members: &[ #( #members),* ], @@ -101,19 +101,19 @@ mod test { )?; let out = PyClassInfo::try_from(input)?.to_token_stream(); insta::assert_snapshot!(format_as_value(out), @r###" - crate::stub::PyClassInfo { + ::pyo3_stub_gen::type_info::PyClassInfo { pyclass_name: "Placeholder", struct_id: std::any::TypeId::of::, members: &[ - crate::stub::MemberInfo { + ::pyo3_stub_gen::type_info::MemberInfo { name: "name", r#type: >::type_output, }, - crate::stub::MemberInfo { + ::pyo3_stub_gen::type_info::MemberInfo { name: "ndim", r#type: >::type_output, }, - crate::stub::MemberInfo { + ::pyo3_stub_gen::type_info::MemberInfo { name: "description", r#type: as IntoPy>::type_output, }, diff --git a/pyo3-stub-gen-derive/src/gen_stub/pyclass_enum.rs b/pyo3-stub-gen-derive/src/gen_stub/pyclass_enum.rs index d11269a..ec2cdce 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/pyclass_enum.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/pyclass_enum.rs @@ -67,7 +67,7 @@ impl ToTokens for PyEnumInfo { } = self; let module = quote_option(module); tokens.append_all(quote! { - crate::stub::PyEnumInfo { + ::pyo3_stub_gen::type_info::PyEnumInfo { pyclass_name: #pyclass_name, enum_id: std::any::TypeId::of::<#enum_type>, variants: &[ #(#variants),* ], diff --git a/pyo3-stub-gen-derive/src/gen_stub/pyfunction.rs b/pyo3-stub-gen-derive/src/gen_stub/pyfunction.rs index 179c644..ffbf1fa 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/pyfunction.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/pyfunction.rs @@ -86,12 +86,12 @@ impl ToTokens for PyFunctionInfo { let ret_tt = if let Some(ret) = ret { quote! { <#ret as IntoPy<::pyo3::PyObject>>::type_output } } else { - quote! { crate::stub::no_return_type_output } + quote! { ::pyo3_stub_gen::type_info::no_return_type_output } }; let sig_tt = quote_option(sig); let module_tt = quote_option(module); tokens.append_all(quote! { - crate::stub::PyFunctionInfo { + ::pyo3_stub_gen::type_info::PyFunctionInfo { name: #name, args: &[ #(#args),* ], r#return: #ret_tt, diff --git a/pyo3-stub-gen-derive/src/gen_stub/pymethods.rs b/pyo3-stub-gen-derive/src/gen_stub/pymethods.rs index a26a14b..c35e7cc 100644 --- a/pyo3-stub-gen-derive/src/gen_stub/pymethods.rs +++ b/pyo3-stub-gen-derive/src/gen_stub/pymethods.rs @@ -51,7 +51,7 @@ impl ToTokens for PyMethodsInfo { } = self; let new_tt = quote_option(new); tokens.append_all(quote! { - crate::stub::PyMethodsInfo { + ::pyo3_stub_gen::type_info::PyMethodsInfo { struct_id: std::any::TypeId::of::<#struct_id>, new: #new_tt, getters: &[ #(#getters),* ], diff --git a/pyo3-stub-gen-testing/Cargo.toml b/pyo3-stub-gen-testing/Cargo.toml new file mode 100644 index 0000000..ab82c4e --- /dev/null +++ b/pyo3-stub-gen-testing/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pyo3-stub-gen-testing" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +inventory.workspace = true +pyo3-stub-gen = { workspace = true, optional = true } +pyo3.workspace = true + +[features] +stub_gen = ["pyo3-stub-gen"] + +[[bin]] +name = "stub_gen" +path = "src/bin/stub_gen.rs" +required-features = ["stub_gen"] diff --git a/pyo3-stub-gen-testing/pyproject.toml b/pyo3-stub-gen-testing/pyproject.toml new file mode 100644 index 0000000..87e9626 --- /dev/null +++ b/pyo3-stub-gen-testing/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["maturin>=1.1,<2.0"] +build-backend = "maturin" + +[project] +name = "pyo3_stub_gen_testing" +requires-python = ">=3.9" + +[project.optional-dependencies] +test = ["pytest", "pyright"] diff --git a/pyo3-stub-gen-testing/src/bin/stub_gen.rs b/pyo3-stub-gen-testing/src/bin/stub_gen.rs new file mode 100644 index 0000000..3e30407 --- /dev/null +++ b/pyo3-stub-gen-testing/src/bin/stub_gen.rs @@ -0,0 +1,6 @@ +use pyo3_stub_gen::*; + +fn main() { + let modules = generate::gather().unwrap(); + dbg!(modules); +} diff --git a/pyo3-stub-gen-testing/src/lib.rs b/pyo3-stub-gen-testing/src/lib.rs new file mode 100644 index 0000000..fcdea7a --- /dev/null +++ b/pyo3-stub-gen-testing/src/lib.rs @@ -0,0 +1,31 @@ +use pyo3::prelude::*; + +#[cfg(feature = "stub_gen")] +use pyo3_stub_gen::derive::*; + +/// Returns the sum of two numbers as a string. +/// +/// Test of running doc-test +/// +/// ```rust +/// assert_eq!(2 + 2, 4); +/// ``` +#[cfg_attr(feature = "stub_gen", gen_stub_pyfunction)] +#[pyfunction] +fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) +} + +#[pymodule] +fn pyo3_stub_gen_testing(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; + Ok(()) +} + +#[cfg(test)] +mod test { + #[test] + fn test() { + assert_eq!(2 + 2, 4); + } +} diff --git a/pyo3-stub-gen-testing/tests/test_python.py b/pyo3-stub-gen-testing/tests/test_python.py new file mode 100644 index 0000000..6674708 --- /dev/null +++ b/pyo3-stub-gen-testing/tests/test_python.py @@ -0,0 +1,5 @@ +import pyo3_stub_gen_testing + + +def test_sum_as_string(): + assert pyo3_stub_gen_testing.sum_as_string(1, 2) == "3" diff --git a/pyo3-stub-gen/Cargo.toml b/pyo3-stub-gen/Cargo.toml index 5b7ac23..4f4b261 100644 --- a/pyo3-stub-gen/Cargo.toml +++ b/pyo3-stub-gen/Cargo.toml @@ -9,4 +9,4 @@ pyo3-stub-gen-derive.workspace = true anyhow.workspace = true itertools.workspace = true inventory.workspace = true -pyo3 = { workspace = true, features = ["experimental-inspect"]} +pyo3.workspace = true diff --git a/pyo3-stub-gen/src/generate.rs b/pyo3-stub-gen/src/generate.rs index faf2941..a4c6104 100644 --- a/pyo3-stub-gen/src/generate.rs +++ b/pyo3-stub-gen/src/generate.rs @@ -269,7 +269,7 @@ impl fmt::Display for FunctionDef { } #[derive(Debug, Clone, PartialEq, Default)] -struct Module { +pub struct Module { class: BTreeMap, enum_: BTreeMap, function: BTreeMap<&'static str, FunctionDef>, @@ -295,7 +295,7 @@ impl fmt::Display for Module { } /// Gather metadata generated by proc-macros -fn gather() -> Result> { +pub fn gather() -> Result> { let mut modules: BTreeMap = BTreeMap::new(); for info in inventory::iter:: {