sizmailov / pybind11-stubgen

Generate stubs for python modules
Other
228 stars 45 forks source link

Equivalent of GLOBAL_CLASSNAME_REPLACEMENTS (v0.16.0) in newer versions? #151

Closed TimSchneider42 closed 10 months ago

TimSchneider42 commented 11 months ago

Hi,

in my code I am using pybind11::implicitly_convertible to indicate that a class A can be implicitly converted to B. Now whenever a function expects an argument of type B, I would like the type hint to be Union[A, B].

In version 0.16.0, I could achieve this with the following code:

import pybind11_stubgen
import re

def replace_union(match):
    return "typing.Union[A, B]"

if __name__ == '__main__':
    pybind11_stubgen.StubsGenerator.GLOBAL_CLASSNAME_REPLACEMENTS[re.compile("(B)"] = replace_union
    pybind11_stubgen.main()

However, in the latest version, this code does not work anymore. What would be the recommended way of solving this issue?

Best, Tim

battleguard commented 11 months ago

This should give you a good place to start. It has issues from what I tested on if you use a param that is an std::variant as can be seen with example1

from pybind11_stubgen import *
from pybind11_stubgen.structs import *

class FixImplicitConversions(IParser):
    def parse_annotation_str(
        self, annotation_str: str
    ) -> ResolvedType | InvalidExpression | Value:
        result = super().parse_annotation_str(annotation_str)

        if isinstance(result, ResolvedType):
            result: ResolvedType
            if len(result.name) == 1 and result.parameters is None and result.name[0] == 'UtStringId':
                result.name= QualifiedName.from_str('typing.Union') 
                result.parameters=[
                    ResolvedType(QualifiedName.from_str('str')), 
                    ResolvedType(QualifiedName.from_str('UtStringId'))]
                print(f'{result.__str__()} {annotation_str=}')

        return result

def main():
    logging.basicConfig(
        level=logging.INFO,
        format="%(name)s - [%(levelname)7s] %(message)s",
    )
    args = arg_parser().parse_args()

    parser = stub_parser_from_args(args)

    cls = parser.__class__
    parser.__class__ = cls.__class__(cls.__name__ + "WithExtraBase", (FixImplicitConversions, cls), {})

    printer = Printer(invalid_expr_as_ellipses=not args.print_invalid_expressions_as_is)

    out_dir, sub_dir = to_output_and_subdir(
        output_dir=args.output_dir,
        module_name=args.module_name,
        root_suffix=args.root_suffix,
    )

    run(
        parser,
        printer,
        args.module_name,
        out_dir,
        sub_dir=sub_dir,
        dry_run=args.dry_run,
        writer=Writer(stub_ext=args.stub_extension),
    )

if __name__ == '__main__':
    main()
   m.def("Example1", [](std::variant<std::string, UtStringId> value)
      {

      });
   m.def("Example2", [](UtStringId& value)
      {

      });
   m.def("Example3", [](std::vector<UtStringId> value)
      {

      });
   m.def("Example4", [](std::vector<UtStringId> value)
      {
         return UtStringId();
      });
   m.def("Example5", [](WsfStringId value)
      {
         return WsfStringId();
      })
def Example1(arg0: str | str | UtStringId) -> None:
    ...
def Example2(arg0: str | UtStringId) -> None:
    ...
def Example3(arg0: list[str | UtStringId]) -> None:
    ...
def Example4(arg0: list[str | UtStringId]) -> str | UtStringId:
    ...
def Example5(arg0: str | UtStringId) -> str | UtStringId:
    ...
battleguard commented 11 months ago

@sizmailov it would be useful to have a way to dynamically add custom IParser impls to the default Parser similar to how you are dynamically adding the top and bottom error handlers. Also what is the reason behind the main Parser using inheritance instead of just being a list of parsers underneath the hood. multi inheritance seems like a much more rigid way of doing things.

TimSchneider42 commented 11 months ago

@battleguard, thanks a lot! I will try it out once I find some time. I agree that providing a simpler way of adding custom IParser implementations would be useful.

TimSchneider42 commented 11 months ago

Hey, one thing I just noticed is that the above solution also modifies the return types of functions. However, this I do not want. Is there any way I can ensure that only function input parameters are modified?

Edit: to be clear, I think my previous solution also suffered from this issue. I just didn't realize it until now.

sizmailov commented 10 months ago

@TimSchneider42 The solution to your problem could be close to what @battleguard suggested. Alternatively, you can manually transverse the resulting structure and patch it after the parsing stage.

@battleguard I agree. Currently, customization is far from elegant and requires repetition of __init__.py. I suggest copying the whole __init__.py to be safe from sudden changes since I didn't make __init__.py robust to changes.

I've created #156 to address a more generic question. So I'll close this one.