diff --git a/Cargo.lock b/Cargo.lock index e3abaf6..311a8c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,7 @@ dependencies = [ "pyo3", "pyo3-stub-gen-derive", "serde", + "test-case", "toml", ] @@ -707,6 +708,39 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "test-case-core", +] + [[package]] name = "toml" version = "0.8.19" diff --git a/Cargo.toml b/Cargo.toml index a0e2415..3d909de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,4 @@ quote = "1.0.38" serde = { version = "1.0.217", features = ["derive"] } syn = "2.0.96" toml = "0.8.19" +test-case = "3.3.1" diff --git a/examples/mixed/python/mixed/main_mod.pyi b/examples/mixed/python/mixed/main_mod.pyi index 04a14ca..3c9eb5e 100644 --- a/examples/mixed/python/mixed/main_mod.pyi +++ b/examples/mixed/python/mixed/main_mod.pyi @@ -1,6 +1,7 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 +import builtins class A: def show_x(self) -> None: @@ -12,9 +13,9 @@ class B: ... -def create_a(x:int) -> A: +def create_a(x:builtins.int) -> A: ... -def create_b(x:int) -> B: +def create_b(x:builtins.int) -> B: ... diff --git a/examples/mixed_sub/python/mixed_sub/main_mod/__init__.pyi b/examples/mixed_sub/python/mixed_sub/main_mod/__init__.pyi index 87bfbed..141b654 100644 --- a/examples/mixed_sub/python/mixed_sub/main_mod/__init__.pyi +++ b/examples/mixed_sub/python/mixed_sub/main_mod/__init__.pyi @@ -1,6 +1,8 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 +import builtins +from . import int from . import sub_mod class A: @@ -13,9 +15,9 @@ class B: ... -def create_a(x:int) -> A: +def create_a(x:builtins.int) -> A: ... -def create_b(x:int) -> B: +def create_b(x:builtins.int) -> B: ... diff --git a/examples/mixed_sub/python/mixed_sub/main_mod/int.pyi b/examples/mixed_sub/python/mixed_sub/main_mod/int.pyi new file mode 100644 index 0000000..e00ed50 --- /dev/null +++ b/examples/mixed_sub/python/mixed_sub/main_mod/int.pyi @@ -0,0 +1,8 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import builtins + +def dummy_int_fun(x:builtins.int) -> builtins.int: + ... + diff --git a/examples/mixed_sub/python/mixed_sub/main_mod/sub_mod.pyi b/examples/mixed_sub/python/mixed_sub/main_mod/sub_mod.pyi index 260c093..9be58b3 100644 --- a/examples/mixed_sub/python/mixed_sub/main_mod/sub_mod.pyi +++ b/examples/mixed_sub/python/mixed_sub/main_mod/sub_mod.pyi @@ -1,12 +1,13 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 +import builtins class C: def show_x(self) -> None: ... -def create_c(x:int) -> C: +def create_c(x:builtins.int) -> C: ... diff --git a/examples/mixed_sub/src/lib.rs b/examples/mixed_sub/src/lib.rs index 2d77163..3676b91 100644 --- a/examples/mixed_sub/src/lib.rs +++ b/examples/mixed_sub/src/lib.rs @@ -68,6 +68,12 @@ fn create_c(x: usize) -> C { C { x } } +#[gen_stub_pyfunction(module = "mixed_sub.main_mod.int")] +#[pyfunction] +fn dummy_int_fun(x: usize) -> usize { + x +} + #[pymodule] fn main_mod(m: &Bound) -> PyResult<()> { m.add_class::()?; @@ -75,6 +81,7 @@ fn main_mod(m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(create_a, m)?)?; m.add_function(wrap_pyfunction!(create_b, m)?)?; sub_mod(m)?; + int_mod(m)?; Ok(()) } @@ -87,6 +94,15 @@ fn sub_mod(parent: &Bound) -> PyResult<()> { Ok(()) } +/// A dummy module to pollute namespace with unqualified `int` +fn int_mod(parent: &Bound) -> PyResult<()> { + let py = parent.py(); + let sub = PyModule::new(py, "int")?; + sub.add_function(wrap_pyfunction!(dummy_int_fun, &sub)?)?; + parent.add_submodule(&sub)?; + Ok(()) +} + define_stub_info_gatherer!(stub_info); /// Test of unit test for testing link problem diff --git a/examples/pure/pure.pyi b/examples/pure/pure.pyi index d8ea01d..e75abab 100644 --- a/examples/pure/pure.pyi +++ b/examples/pure/pure.pyi @@ -1,15 +1,16 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 +import builtins import os import pathlib import typing from enum import Enum, auto -MY_CONSTANT: int +MY_CONSTANT: builtins.int class A: - x: int - def __new__(cls,x:int): ... + x: builtins.int + def __new__(cls,x:builtins.int): ... def show_x(self) -> None: ... @@ -21,31 +22,31 @@ class Number(Enum): FLOAT = auto() INTEGER = auto() -def ahash_dict() -> dict[str, int]: +def ahash_dict() -> builtins.dict[builtins.str, builtins.int]: ... -def create_a(x:int=2) -> A: +def create_a(x:builtins.int=2) -> A: ... -def create_dict(n:int) -> dict[int, list[int]]: +def create_dict(n:builtins.int) -> builtins.dict[builtins.int, builtins.list[builtins.int]]: ... def default_value(num:Number=...) -> Number: ... -def echo_path(path:str | os.PathLike | pathlib.Path) -> str: +def echo_path(path:builtins.str | os.PathLike | pathlib.Path) -> builtins.str: ... -def read_dict(dict:typing.Mapping[int, typing.Mapping[int, int]]) -> None: +def read_dict(dict:typing.Mapping[builtins.int, typing.Mapping[builtins.int, builtins.int]]) -> None: ... -def str_len(x:str) -> int: +def str_len(x:builtins.str) -> builtins.int: r""" Returns the length of the string. """ ... -def sum(v:typing.Sequence[int]) -> int: +def sum(v:typing.Sequence[builtins.int]) -> builtins.int: r""" Returns the sum of two numbers as a string. """ diff --git a/pyo3-stub-gen/Cargo.toml b/pyo3-stub-gen/Cargo.toml index 7268672..56157d2 100644 --- a/pyo3-stub-gen/Cargo.toml +++ b/pyo3-stub-gen/Cargo.toml @@ -25,6 +25,9 @@ toml.workspace = true version = "0.7.0" path = "../pyo3-stub-gen-derive" +[dev-dependencies] +test-case.workspace = true + [features] default = ["numpy"] numpy = ["dep:numpy"] diff --git a/pyo3-stub-gen/src/lib.rs b/pyo3-stub-gen/src/lib.rs index c867c24..2b73661 100644 --- a/pyo3-stub-gen/src/lib.rs +++ b/pyo3-stub-gen/src/lib.rs @@ -127,7 +127,7 @@ //! assert_eq!( //! method.to_string().trim(), //! r#" -//! def foo(self, x:int) -> int: +//! def foo(self, x:builtins.int) -> builtins.int: //! r""" //! This is a foo method. //! """ diff --git a/pyo3-stub-gen/src/stub_type.rs b/pyo3-stub-gen/src/stub_type.rs index d815b9f..51648ae 100644 --- a/pyo3-stub-gen/src/stub_type.rs +++ b/pyo3-stub-gen/src/stub_type.rs @@ -61,6 +61,8 @@ impl fmt::Display for TypeInfo { impl TypeInfo { /// A `None` type annotation. pub fn none() -> Self { + // NOTE: since 3.10, NoneType is provided from types module, + // but there is no corresponding definitions prior to 3.10. Self { name: "None".to_string(), import: HashSet::new(), @@ -75,11 +77,57 @@ impl TypeInfo { } } - /// A type annotation of a built-in type, such as `int`, `str`, or `float`. Generic builtin types are also possible, such as `dict[str, str]`. + /// A `list[Type]` type annotation. + pub fn list_of() -> Self { + let TypeInfo { name, mut import } = T::type_output(); + import.insert("builtins".into()); + TypeInfo { + name: format!("builtins.list[{}]", name), + import, + } + } + + /// A `set[Type]` type annotation. + pub fn set_of() -> Self { + let TypeInfo { name, mut import } = T::type_output(); + import.insert("builtins".into()); + TypeInfo { + name: format!("builtins.set[{}]", name), + import, + } + } + + /// A `dict[Type]` type annotation. + pub fn dict_of() -> Self { + let TypeInfo { + name: name_k, + mut import, + } = K::type_output(); + let TypeInfo { + name: name_v, + import: import_v, + } = V::type_output(); + import.extend(import_v); + import.insert("builtins".into()); + TypeInfo { + name: format!("builtins.set[{}, {}]", name_k, name_v), + import, + } + } + + /// A type annotation of a built-in type provided from `builtins` module, such as `int`, `str`, or `float`. Generic builtin types are also possible, such as `dict[str, str]`. pub fn builtin(name: &str) -> Self { + Self { + name: format!("builtins.{name}"), + import: hashset! { "builtins".into() }, + } + } + + /// Unqualified type. + pub fn unqualified(name: &str) -> Self { Self { name: name.to_string(), - import: HashSet::new(), + import: hashset! {}, } } @@ -182,49 +230,23 @@ mod test { use super::*; use maplit::hashset; use std::collections::HashMap; - - #[test] - fn test() { - assert_eq!(bool::type_input().name, "bool"); - assert!(bool::type_input().import.is_empty()); - - assert_eq!(<&str>::type_input().name, "str"); - assert!(<&str>::type_input().import.is_empty()); - - assert_eq!(Vec::::type_input().name, "typing.Sequence[int]"); - assert_eq!( - Vec::::type_input().import, - hashset! { "typing".into() } - ); - - assert_eq!(Vec::::type_output().name, "list[int]"); - assert!(Vec::::type_output().import.is_empty()); - - assert_eq!( - HashMap::::type_input().name, - "typing.Mapping[int, str]" - ); - assert_eq!( - HashMap::::type_input().import, - hashset! { "typing".into() } - ); - - assert_eq!(HashMap::::type_output().name, "dict[int, str]"); - assert!(HashMap::::type_output().import.is_empty()); - - assert_eq!( - HashMap::>::type_input().name, - "typing.Mapping[int, typing.Sequence[int]]" - ); - assert_eq!( - HashMap::>::type_input().import, - hashset! { "typing".into() } - ); - - assert_eq!( - HashMap::>::type_output().name, - "dict[int, list[int]]" - ); - assert!(HashMap::>::type_output().import.is_empty()); + use test_case::test_case; + + #[test_case(bool::type_input(), "builtins.bool", hashset! { "builtins".into() } ; "bool_input")] + #[test_case(<&str>::type_input(), "builtins.str", hashset! { "builtins".into() } ; "str_input")] + #[test_case(Vec::::type_input(), "typing.Sequence[builtins.int]", hashset! { "typing".into(), "builtins".into() } ; "Vec_u32_input")] + #[test_case(Vec::::type_output(), "builtins.list[builtins.int]", hashset! { "builtins".into() } ; "Vec_u32_output")] + #[test_case(HashMap::::type_input(), "typing.Mapping[builtins.int, builtins.str]", hashset! { "typing".into(), "builtins".into() } ; "HashMap_u32_String_input")] + #[test_case(HashMap::::type_output(), "builtins.dict[builtins.int, builtins.str]", hashset! { "builtins".into() } ; "HashMap_u32_String_output")] + #[test_case(HashMap::>::type_input(), "typing.Mapping[builtins.int, typing.Sequence[builtins.int]]", hashset! { "builtins".into(), "typing".into() } ; "HashMap_u32_Vec_u32_input")] + #[test_case(HashMap::>::type_output(), "builtins.dict[builtins.int, builtins.list[builtins.int]]", hashset! { "builtins".into() } ; "HashMap_u32_Vec_u32_output")] + #[test_case(HashSet::::type_input(), "builtins.set[builtins.int]", hashset! { "builtins".into() } ; "HashSet_u32_input")] + fn test(tinfo: TypeInfo, name: &str, import: HashSet) { + assert_eq!(tinfo.name, name); + if import.is_empty() { + assert!(tinfo.import.is_empty()); + } else { + assert_eq!(tinfo.import, import); + } } } diff --git a/pyo3-stub-gen/src/stub_type/builtins.rs b/pyo3-stub-gen/src/stub_type/builtins.rs index 991ee28..793ccb6 100644 --- a/pyo3-stub-gen/src/stub_type/builtins.rs +++ b/pyo3-stub-gen/src/stub_type/builtins.rs @@ -14,10 +14,7 @@ macro_rules! impl_builtin { ($ty:ty, $pytype:expr) => { impl PyStubType for $ty { fn type_output() -> TypeInfo { - TypeInfo { - name: $pytype.to_string(), - import: HashSet::new(), - } + TypeInfo::builtin($pytype) } } }; @@ -33,7 +30,12 @@ macro_rules! impl_with_module { }; } -impl_builtin!((), "None"); +// NOTE: +impl PyStubType for () { + fn type_output() -> TypeInfo { + TypeInfo::none() + } +} impl_builtin!(bool, "bool"); impl_builtin!(u8, "int"); impl_builtin!(u16, "int"); diff --git a/pyo3-stub-gen/src/stub_type/collections.rs b/pyo3-stub-gen/src/stub_type/collections.rs index 209025e..ed88536 100644 --- a/pyo3-stub-gen/src/stub_type/collections.rs +++ b/pyo3-stub-gen/src/stub_type/collections.rs @@ -57,11 +57,7 @@ impl PyStubType for Vec { } } fn type_output() -> TypeInfo { - let TypeInfo { name, import } = T::type_output(); - TypeInfo { - name: format!("list[{}]", name), - import, - } + TypeInfo::list_of::() } } @@ -75,31 +71,19 @@ impl PyStubType for [T; N] { } } fn type_output() -> TypeInfo { - let TypeInfo { name, import } = T::type_output(); - TypeInfo { - name: format!("list[{}]", name), - import, - } + TypeInfo::list_of::() } } impl PyStubType for HashSet { fn type_output() -> TypeInfo { - let TypeInfo { name, import } = T::type_output(); - TypeInfo { - name: format!("set[{}]", name), - import, - } + TypeInfo::set_of::() } } impl PyStubType for BTreeSet { fn type_output() -> TypeInfo { - let TypeInfo { name, import } = T::type_output(); - TypeInfo { - name: format!("set[{}]", name), - import, - } + TypeInfo::set_of::() } } @@ -131,8 +115,9 @@ macro_rules! impl_map_inner { import: value_import, } = Value::type_output(); import.extend(value_import); + import.insert("builtins".into()); TypeInfo { - name: format!("dict[{}, {}]", key_name, value_name), + name: format!("builtins.dict[{}, {}]", key_name, value_name), import, } }