From 2ae41b5ae99ac878a52dc1caa1c4ffaf7938c3f6 Mon Sep 17 00:00:00 2001 From: Bryn Lloyd <12702862+dyollb@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:30:56 +0200 Subject: [PATCH 1/2] Add optional concrete fallback definition after overload stubs --- pybind11_stubgen/__init__.py | 11 +++ pybind11_stubgen/printer.py | 122 +++++++++++++++++++++++++++++++- tests/test_overload_fallback.py | 61 ++++++++++++++++ 3 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 tests/test_overload_fallback.py diff --git a/pybind11_stubgen/__init__.py b/pybind11_stubgen/__init__.py index 02adaed..46643ec 100644 --- a/pybind11_stubgen/__init__.py +++ b/pybind11_stubgen/__init__.py @@ -77,6 +77,7 @@ class CLIArgs(Namespace): print_invalid_expressions_as_is: bool print_safe_value_reprs: re.Pattern | None print_value_comments: bool + print_overload_fallback: bool exit_code: bool dry_run: bool stub_extension: str @@ -202,6 +203,15 @@ def regex_colon_path(regex_path: str) -> tuple[re.Pattern, str]: "i.e., '... # value = '", ) + parser.add_argument( + "--print-overload-fallback", + default=False, + action="store_true", + dest="print_overload_fallback", + help="Emit a concrete fallback definition after overload sets. " + "This allows re-exports to resolve the symbol via tools like Griffe.", + ) + parser.add_argument( "--exit-code", action="store_true", @@ -325,6 +335,7 @@ def main(argv: Sequence[str] | None = None) -> None: printer = Printer( invalid_expr_as_ellipses=not args.print_invalid_expressions_as_is, print_value_comments=args.print_value_comments, + print_overload_fallback=args.print_overload_fallback, ) run( diff --git a/pybind11_stubgen/printer.py b/pybind11_stubgen/printer.py index 2361500..45ab17c 100644 --- a/pybind11_stubgen/printer.py +++ b/pybind11_stubgen/printer.py @@ -20,6 +20,7 @@ Modifier, Module, Property, + QualifiedName, ResolvedType, TypeVar_, Value, @@ -141,13 +142,124 @@ def __init__( self, invalid_expr_as_ellipses: bool, print_value_comments: bool = False, + print_overload_fallback: bool = False, ): self.invalid_expr_as_ellipses = invalid_expr_as_ellipses self.print_value_comments = print_value_comments + self.print_overload_fallback = print_overload_fallback def _order_classes(self, classes: list[Class]) -> list[Class]: return _topological_sort_classes(classes) + def _is_overload(self, func: Function) -> bool: + """Check if a function has the @overload decorator.""" + return any( + "overload" in str(decorator) for decorator in func.decorators + ) + + def _create_fallback_function(self, func: Function) -> Function: + """Create a fallback function with broad parameter types. + + Returns a new Function with: + - Same name + - All parameters accept Any type + - Return type is Any + - No @overload decorator + - No docstring + """ + fallback_args = [] + for arg in func.args: + # Keep the argument structure but replace annotation with Any + new_arg = Argument( + name=arg.name, + pos_only=arg.pos_only, + kw_only=arg.kw_only, + variadic=arg.variadic, + kw_variadic=arg.kw_variadic, + default=arg.default, + annotation=ResolvedType(QualifiedName.from_str("typing.Any")), + ) + fallback_args.append(new_arg) + + # Return type is Any + fallback_return = ResolvedType(QualifiedName.from_str("typing.Any")) + + fallback_func = Function( + name=func.name, + args=fallback_args, + returns=fallback_return, + doc=None, + decorators=[], # No @overload decorator + type_vars=func.type_vars, + ) + return fallback_func + + def _process_with_fallbacks( + self, + items: list, + get_key, + get_function, + create_wrapper, + ) -> list: + """Generic method to process items and insert fallback definitions after overload groups. + + Args: + items: List of items to process (Functions or Methods) + get_key: Callable to extract the grouping key from an item + get_function: Callable to extract the Function from an item + create_wrapper: Callable to wrap a Function back into the original type + (receives fallback_func and first original item) + """ + if not self.print_overload_fallback or not items: + return items + + # Group items by key while preserving order + groups: dict = {} + group_order: list = [] + + for item in items: + key = get_key(item) + if key not in groups: + groups[key] = [] + group_order.append(key) + groups[key].append(item) + + result = [] + for key in group_order: + group = groups[key] + result.extend(group) + + # Check if any item in the group has an overload + if any(self._is_overload(get_function(item)) for item in group): + fallback_func = self._create_fallback_function(get_function(group[0])) + result.append(create_wrapper(fallback_func, group[0])) + + return result + + def _process_functions_with_fallbacks( + self, functions: list[Function] + ) -> list[Function]: + """Process functions and insert fallback definitions after overload groups.""" + return self._process_with_fallbacks( + functions, + get_key=lambda f: f.name, + get_function=lambda f: f, + create_wrapper=lambda f, _: f, + ) + + def _process_methods_with_fallbacks( + self, methods: list[Method] + ) -> list[Method]: + """Process methods and insert fallback definitions after overload groups.""" + return self._process_with_fallbacks( + methods, + get_key=lambda m: (m.modifier, m.function.name), + get_function=lambda m: m.function, + create_wrapper=lambda f, orig_method: Method( + function=f, modifier=orig_method.modifier + ), + ) + def print_alias(self, alias: Alias) -> list[str]: return [f"{alias.name} = {alias.origin}"] @@ -224,9 +336,11 @@ def print_class_body(self, class_: Class) -> list[str]: for alias in sorted(class_.aliases, key=lambda a: a.name): result.extend(self.print_alias(alias)) - for method in sorted( + sorted_methods = sorted( class_.methods, key=lambda m: (modifier_order[m.modifier], m.function.name) - ): + ) + processed_methods = self._process_methods_with_fallbacks(sorted_methods) + for method in processed_methods: result.extend(self.print_method(method)) for prop in sorted(class_.properties, key=lambda p: p.name): @@ -353,7 +467,9 @@ def print_module(self, module: Module) -> list[str]: for class_ in self._order_classes(module.classes): result.extend(self.print_class(class_)) - for func in sorted(module.functions, key=lambda f: f.name): + sorted_functions = sorted(module.functions, key=lambda f: f.name) + processed_functions = self._process_functions_with_fallbacks(sorted_functions) + for func in processed_functions: result.extend(self.print_function(func)) for attr in sorted(module.attributes, key=lambda a: a.name): diff --git a/tests/test_overload_fallback.py b/tests/test_overload_fallback.py new file mode 100644 index 0000000..47ed52a --- /dev/null +++ b/tests/test_overload_fallback.py @@ -0,0 +1,61 @@ +"""Tests for overload fallback stub emission.""" + +from pybind11_stubgen.structs import ( + Argument, + Decorator, + Function, + Identifier, + QualifiedName, + ResolvedType, + Module, +) +from pybind11_stubgen.printer import Printer + + +def _make_overload_function(type_name: str) -> Function: + return Function( + name=Identifier("func"), + args=[ + Argument( + name=Identifier("x"), + annotation=ResolvedType(QualifiedName.from_str(type_name)), + ) + ], + returns=ResolvedType(QualifiedName.from_str(type_name)), + decorators=[Decorator("typing.overload")], + ) + + +def _render_module(*, print_overload_fallback: bool) -> str: + printer = Printer( + invalid_expr_as_ellipses=True, + print_value_comments=False, + print_overload_fallback=print_overload_fallback, + ) + module = Module( + name=Identifier("test_module"), + functions=[ + _make_overload_function("int"), + _make_overload_function("str"), + ], + ) + return "\n".join(printer.print_module(module)) + + +def test_overload_fallback_enabled() -> None: + output_str = _render_module(print_overload_fallback=True) + + assert "@typing.overload" in output_str + assert output_str.count("def func") == 3 + assert "typing.Any" in output_str + + lines = output_str.split("\n") + last_def_idx = max(i for i, line in enumerate(lines) if "def func" in line) + if last_def_idx > 0: + assert "@overload" not in lines[last_def_idx - 1].strip() + + +def test_overload_fallback_disabled() -> None: + output_str = _render_module(print_overload_fallback=False) + + assert output_str.count("def func") == 2 From 1f0874c6c045ea3cdb04b4bcd56ebea656b43d33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:38:02 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pybind11_stubgen/printer.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pybind11_stubgen/printer.py b/pybind11_stubgen/printer.py index 45ab17c..ea9d953 100644 --- a/pybind11_stubgen/printer.py +++ b/pybind11_stubgen/printer.py @@ -153,13 +153,11 @@ def _order_classes(self, classes: list[Class]) -> list[Class]: def _is_overload(self, func: Function) -> bool: """Check if a function has the @overload decorator.""" - return any( - "overload" in str(decorator) for decorator in func.decorators - ) + return any("overload" in str(decorator) for decorator in func.decorators) def _create_fallback_function(self, func: Function) -> Function: """Create a fallback function with broad parameter types. - + Returns a new Function with: - Same name - All parameters accept Any type @@ -180,10 +178,10 @@ def _create_fallback_function(self, func: Function) -> Function: annotation=ResolvedType(QualifiedName.from_str("typing.Any")), ) fallback_args.append(new_arg) - + # Return type is Any fallback_return = ResolvedType(QualifiedName.from_str("typing.Any")) - + fallback_func = Function( name=func.name, args=fallback_args, @@ -202,7 +200,7 @@ def _process_with_fallbacks( create_wrapper, ) -> list: """Generic method to process items and insert fallback definitions after overload groups. - + Args: items: List of items to process (Functions or Methods) get_key: Callable to extract the grouping key from an item @@ -216,7 +214,7 @@ def _process_with_fallbacks( # Group items by key while preserving order groups: dict = {} group_order: list = [] - + for item in items: key = get_key(item) if key not in groups: @@ -228,7 +226,7 @@ def _process_with_fallbacks( for key in group_order: group = groups[key] result.extend(group) - + # Check if any item in the group has an overload if any(self._is_overload(get_function(item)) for item in group): fallback_func = self._create_fallback_function(get_function(group[0])) @@ -247,9 +245,7 @@ def _process_functions_with_fallbacks( create_wrapper=lambda f, _: f, ) - def _process_methods_with_fallbacks( - self, methods: list[Method] - ) -> list[Method]: + def _process_methods_with_fallbacks(self, methods: list[Method]) -> list[Method]: """Process methods and insert fallback definitions after overload groups.""" return self._process_with_fallbacks( methods,