Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
TimSchneider42 opened this issue Sep 25, 2023 · 5 comments
Labels
question Further information is requested

Comments

@TimSchneider42
Copy link

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

@sizmailov sizmailov added the question Further information is requested label Sep 25, 2023
@battleguard
Copy link

battleguard commented Sep 27, 2023

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
Copy link

@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
Copy link
Author

@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
Copy link
Author

TimSchneider42 commented Sep 27, 2023

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
Copy link
Owner

@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.

@sizmailov sizmailov closed this as not planned Won't fix, can't repro, duplicate, stale Sep 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants