This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new a39cddaa Improve typing of overloaded methods in stubgen (#678)
a39cddaa is described below

commit a39cddaace892987787449632a9c9b39725c1bfa
Author: Matthew Brookhart <[email protected]>
AuthorDate: Mon Jul 20 18:01:46 2026 -0600

    Improve typing of overloaded methods in stubgen (#678)
---
 include/tvm/ffi/reflection/overload.h           | 30 +++++++++++-----
 python/tvm_ffi/stub/python_generator/codegen.py |  2 ++
 python/tvm_ffi/stub/python_generator/utils.py   | 47 ++++++++++++++-----------
 python/tvm_ffi/stub/utils.py                    | 10 ++++++
 tests/cpp/test_overload.cc                      | 15 ++++++++
 tests/python/test_stubgen.py                    | 39 +++++++++++++++++---
 6 files changed, 110 insertions(+), 33 deletions(-)

diff --git a/include/tvm/ffi/reflection/overload.h 
b/include/tvm/ffi/reflection/overload.h
index 04c21fb5..c9193c40 100644
--- a/include/tvm/ffi/reflection/overload.h
+++ b/include/tvm/ffi/reflection/overload.h
@@ -493,18 +493,25 @@ class OverloadObjectDef : private ObjectDef<Class> {
 
     auto method_name = std::string(type_key_) + "." + name;
 
-    // if an overload method exists, register to existing overload function
+    // If an overload method exists, add the callable to the existing runtime
+    // dispatcher but still publish a TypeMethod entry for this signature.
+    // Stub generators consume TypeInfo::methods, so omitting later entries
+    // made an overloaded C++ method look like a single Python signature.
+    Function method;
     if (const auto overload_it = registered_fields_.find(name);
         overload_it != registered_fields_.end()) {
-      ::tvm::ffi::details::OverloadBase* overload_ptr = overload_it->second;
-      return overload_ptr->Register(NewOverload(std::move(method_name), 
std::forward<Func>(func)));
+      overload_it->second.overload->Register(
+          NewOverload(std::move(method_name), std::forward<Func>(func)));
+      method = overload_it->second.method;
+    } else {
+      // First registration creates the runtime overload dispatcher. Keep the
+      // Function alive so every reflected signature can reference it.
+      auto [new_method, overload_ptr] =
+          GetOverloadMethod(std::move(method_name), std::forward<Func>(func));
+      method = std::move(new_method);
+      registered_fields_.try_emplace(name, RegisteredMethod{overload_ptr, 
method});
     }
 
-    // first time registering overload method
-    auto [method, overload_ptr] =
-        GetOverloadMethod(std::move(method_name), std::forward<Func>(func));
-    registered_fields_.try_emplace(name, overload_ptr);
-
     info.method = AnyView(method).CopyToTVMFFIAny();
     info.metadata_.emplace_back("type_schema", FuncInfo::TypeSchema());
     // apply method info traits
@@ -514,7 +521,12 @@ class OverloadObjectDef : private ObjectDef<Class> {
     TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterMethod(type_index_, &info));
   }
 
-  std::unordered_map<std::string, ::tvm::ffi::details::OverloadBase*> 
registered_fields_;
+  struct RegisteredMethod {
+    ::tvm::ffi::details::OverloadBase* overload;
+    Function method;
+  };
+
+  std::unordered_map<std::string, RegisteredMethod> registered_fields_;
 };
 
 }  // namespace reflection
diff --git a/python/tvm_ffi/stub/python_generator/codegen.py 
b/python/tvm_ffi/stub/python_generator/codegen.py
index 0f813464..2669231d 100644
--- a/python/tvm_ffi/stub/python_generator/codegen.py
+++ b/python/tvm_ffi/stub/python_generator/codegen.py
@@ -183,6 +183,8 @@ def generate_python_object(
     info = obj_info
     method_names = {m.schema.name.rsplit(".", 1)[-1] for m in info.methods}
     fn_ty_map = _type_suffix_and_record(ty_map, imports, 
func_names=method_names)
+    if info.has_overloaded_methods():
+        imports.append(ImportItem("typing.overload", type_checking_only=True))
     input_fn_ty_map = _type_suffix_and_record(
         _make_input_ty_map(ty_map), imports, func_names=method_names
     )
diff --git a/python/tvm_ffi/stub/python_generator/utils.py 
b/python/tvm_ffi/stub/python_generator/utils.py
index 37eff8f1..bf19b367 100644
--- a/python/tvm_ffi/stub/python_generator/utils.py
+++ b/python/tvm_ffi/stub/python_generator/utils.py
@@ -155,16 +155,29 @@ def render_object_methods(
         input_ty_map = ty_map
     indent_str = " " * indent
     ret = []
+    groups: list[list[FuncInfo]] = []
+    seen: dict[tuple[str, bool], list[FuncInfo]] = {}
     for method in info.methods:
-        func_name = method.schema.name.rsplit(".", 1)[-1]
-        if func_name == "__ffi_init__":
-            # __ffi_init__ is installed as an instance method (self, *args, 
**kwargs) -> None
-            # by _install_ffi_init_attr, regardless of the C++ static 
registration.
-            ret.append(_render_ffi_init_from_method(method, ty_map, indent, 
input_ty_map))
-            continue
-        if not method.is_member:
-            ret.append(f"{indent_str}@staticmethod")
-        ret.append(render_func_signature(method, ty_map, indent, input_ty_map))
+        key = (method.schema.name, method.is_member)
+        if key not in seen:
+            group: list[FuncInfo] = []
+            groups.append(group)
+            seen[key] = group
+        seen[key].append(method)
+
+    for group in groups:
+        for candidate in group:
+            func_name = candidate.schema.name.rsplit(".", 1)[-1]
+            if len(group) > 1:
+                ret.append(f"{indent_str}@overload")
+            if func_name == "__ffi_init__":
+                # __ffi_init__ is installed as an instance method (self, 
*args, **kwargs) -> None
+                # by _install_ffi_init_attr, regardless of the C++ static 
registration.
+                ret.append(_render_ffi_init_from_method(candidate, ty_map, 
indent, input_ty_map))
+                continue
+            if not candidate.is_member:
+                ret.append(f"{indent_str}@staticmethod")
+            ret.append(render_func_signature(candidate, ty_map, indent, 
input_ty_map))
     return ret
 
 
@@ -179,20 +192,17 @@ def _render_ffi_init_from_method(
         input_ty_map = ty_map
     indent_str = " " * indent
     schema = method.schema
-    # Subclass __ffi_init__ signatures legitimately differ from the parent
-    # (different fields -> different constructor params), so suppress LSP.
-    ignore = "  # ty: ignore[invalid-method-override]"
     if schema.origin != "Callable" or not schema.args:
         ty_map("Any")
-        return f"{indent_str}def __ffi_init__(self, *args: Any) -> None: 
...{ignore}"
+        return f"{indent_str}def __ffi_init__(self, *args: Any) -> None: ..."
     # schema.args[0] is return type, schema.args[1:] are param types.
     parts: list[str] = []
     for i, arg in enumerate(schema.args[1:]):
         parts.append(f"_{i}: {arg.input_repr(input_ty_map)}")
     if parts:
         params = ", ".join(parts)
-        return f"{indent_str}def __ffi_init__(self, {params}, /) -> None: 
...{ignore}"
-    return f"{indent_str}def __ffi_init__(self) -> None: ...{ignore}"
+        return f"{indent_str}def __ffi_init__(self, {params}, /) -> None: ..."
+    return f"{indent_str}def __ffi_init__(self) -> None: ..."
 
 
 def render_object_ffi_init(
@@ -281,10 +291,7 @@ def _render_ffi_init_from_fields(
 ) -> list[str]:
     """Render ``__ffi_init__`` stub from field metadata for auto-generated 
init."""
     indent_str = " " * indent
-    # Subclass __ffi_init__ signatures legitimately differ from the parent
-    # (different fields -> different constructor params), so suppress LSP.
-    ignore = "  # ty: ignore[invalid-method-override]"
     params = _format_field_params(info, ty_map, input_ty_map)
     if params:
-        return [f"{indent_str}def __ffi_init__(self, {params}) -> None: 
...{ignore}"]
-    return [f"{indent_str}def __ffi_init__(self) -> None: ...{ignore}"]
+        return [f"{indent_str}def __ffi_init__(self, {params}) -> None: ..."]
+    return [f"{indent_str}def __ffi_init__(self) -> None: ..."]
diff --git a/python/tvm_ffi/stub/utils.py b/python/tvm_ffi/stub/utils.py
index 172fac33..07f40f8d 100644
--- a/python/tvm_ffi/stub/utils.py
+++ b/python/tvm_ffi/stub/utils.py
@@ -130,6 +130,16 @@ class ObjectInfo:
     init_fields: list[InitFieldInfo] = dataclasses.field(default_factory=list)
     has_init: bool = False
 
+    def has_overloaded_methods(self) -> bool:
+        """Return whether reflection exposed multiple signatures for a 
method."""
+        seen: set[tuple[str, bool]] = set()
+        for method in self.methods:
+            key = (method.schema.name, method.is_member)
+            if key in seen:
+                return True
+            seen.add(key)
+        return False
+
     @staticmethod
     def from_type_info(type_info: TypeInfo) -> ObjectInfo:
         """Construct an `ObjectInfo` from a `TypeInfo` instance."""
diff --git a/tests/cpp/test_overload.cc b/tests/cpp/test_overload.cc
index 7dfb9c70..7c2f62c7 100644
--- a/tests/cpp/test_overload.cc
+++ b/tests/cpp/test_overload.cc
@@ -26,6 +26,8 @@
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ffi/string.h>
 
+#include <string>
+
 namespace {
 
 using namespace tvm::ffi;
@@ -92,4 +94,17 @@ TEST(Reflection, CallOverloadedStaticMethod) {
   EXPECT_EQ(res_b.as<float>(), 2.0f);
 }
 
+TEST(Reflection, RecordsEveryOverloadedMethodSignature) {
+  const TypeInfo* info = 
TVMFFIGetTypeInfo(TestOverloadObj::RuntimeTypeIndex());
+  int static_overload_count = 0;
+  for (int32_t index = 0; index < info->num_methods; ++index) {
+    const TVMFFIMethodInfo& method = info->methods[index];
+    if (std::string(method.name.data, method.name.size) == "add_one_static") {
+      ++static_overload_count;
+      EXPECT_GT(method.metadata.size, 0);
+    }
+  }
+  EXPECT_EQ(static_overload_count, 2);
+}
+
 }  // namespace
diff --git a/tests/python/test_stubgen.py b/tests/python/test_stubgen.py
index 277c725d..f94d2418 100644
--- a/tests/python/test_stubgen.py
+++ b/tests/python/test_stubgen.py
@@ -257,6 +257,40 @@ def test_objectinfo_gen_fields_and_methods() -> None:
     ]
 
 
+def test_objectinfo_gen_overloaded_static_methods() -> None:
+    info = ObjectInfo(
+        fields=[],
+        methods=[
+            FuncInfo.from_schema(
+                "demo.Factory.create",
+                TypeSchema("Callable", (TypeSchema("demo.Factory"), 
TypeSchema("int"))),
+                is_member=False,
+            ),
+            FuncInfo.from_schema(
+                "demo.Factory.reset",
+                TypeSchema("Callable", (TypeSchema("None"),)),
+                is_member=False,
+            ),
+            FuncInfo.from_schema(
+                "demo.Factory.create",
+                TypeSchema("Callable", (TypeSchema("demo.Factory"), 
TypeSchema("str"))),
+                is_member=False,
+            ),
+        ],
+    )
+    assert info.has_overloaded_methods()
+    assert render_object_methods(info, _identity_ty_map, indent=2) == [
+        "  @overload",
+        "  @staticmethod",
+        "  def create(_0: int, /) -> demo.Factory: ...",
+        "  @overload",
+        "  @staticmethod",
+        "  def create(_0: str, /) -> demo.Factory: ...",
+        "  @staticmethod",
+        "  def reset() -> None: ...",
+    ]
+
+
 def test_type_schema_container_origins() -> None:
     """Test that Array/List/Map/Dict origins are distinct and validated 
correctly."""
     # Array and List: 0 or 1 arg, default to (Any,)
@@ -400,10 +434,7 @@ def test_objectinfo_gen_init_uses_input_annotations() -> 
None:
     ]
     assert render_object_ffi_init(
         info, _type_suffix, indent=0, input_ty_map=_input_type_suffix
-    ) == [
-        "def __ffi_init__(self, items: Sequence[int]) -> None: ...  # ty: "
-        "ignore[invalid-method-override]"
-    ]
+    ) == ["def __ffi_init__(self, items: Sequence[int]) -> None: ..."]
 
 
 def test_py_class_method_metadata_renders_stub_signature() -> None:

Reply via email to