pyapp-kit / magicgui

build GUIs from type annotations
https://pyapp-kit.github.io/magicgui/
MIT License
362 stars 49 forks source link

chore: preserve magicgui-decorated function parameter hints with ParamSpec #600

Closed tlambert03 closed 11 months ago

tlambert03 commented 11 months ago

This PR uses ParamSpec to preserve function type hints when decorated with @magicgui

before this PR:

Screen Shot 2023-10-10 at 7 24 23 PM

after:

Screen Shot 2023-10-10 at 7 24 06 PM
codecov[bot] commented 11 months ago

Codecov Report

All modified lines are covered by tests :white_check_mark:

Comparison is base (3aa4b81) 87.85% compared to head (a322599) 87.86%.

Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #600 +/- ## ========================================== + Coverage 87.85% 87.86% +0.01% ========================================== Files 39 39 Lines 4561 4565 +4 ========================================== + Hits 4007 4011 +4 Misses 554 554 ``` | [Files](https://app.codecov.io/gh/pyapp-kit/magicgui/pull/600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyapp-kit) | Coverage Δ | | |---|---|---| | [src/magicgui/type\_map/\_magicgui.py](https://app.codecov.io/gh/pyapp-kit/magicgui/pull/600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyapp-kit#diff-c3JjL21hZ2ljZ3VpL3R5cGVfbWFwL19tYWdpY2d1aS5weQ==) | `96.22% <100.00%> (+0.07%)` | :arrow_up: | | [src/magicgui/types.py](https://app.codecov.io/gh/pyapp-kit/magicgui/pull/600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyapp-kit#diff-c3JjL21hZ2ljZ3VpL3R5cGVzLnB5) | `89.47% <100.00%> (ø)` | | | [src/magicgui/widgets/\_function\_gui.py](https://app.codecov.io/gh/pyapp-kit/magicgui/pull/600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyapp-kit#diff-c3JjL21hZ2ljZ3VpL3dpZGdldHMvX2Z1bmN0aW9uX2d1aS5weQ==) | `93.69% <100.00%> (+0.08%)` | :arrow_up: |

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

multimeric commented 11 months ago

Hmm, I think this may have broken some downstream things. I believe adding an extra generic parameter to your classes changes the inheritance interface, because inheriting classes now have to add the corresponding parameter or it will be a runtime error: TypeError: Too few arguments for <class '__main__.Foo'>; actual 1, expected 2. Happy to be convinced otherwise.

The commit in question is: https://github.com/pyapp-kit/magicgui/commit/05c92921a5a6e7dad3757557e73d09c2f559ac01

tlambert03 commented 11 months ago

It would be helpful if you could include the code you ran with Foo in it so I can see exactly what you're trying to do/inherit.

edit: are you using FunctionGui[...] as a type hint? if so, I'd recommend just putting it in quotes or using __future__.annotations ... but show me your code and I can try to help further

multimeric commented 11 months ago

This was happening with a class that inherited from FunctionGui, but assuming it only had 1 type parameter. However I can't replicate this anymore. Even if I cram in far too many type parameters it doesn't throw an exception:

from magicgui.widgets import FunctionGui

class MyFunctionGui(FunctionGui[int, int, int, int, int, int]):
    pass
tlambert03 commented 11 months ago

ok... this is a complicated issue to be sure and I'm still trying to determine where you will and won't get the error:

But to provide some general high level hints:

you can always use FunctionGui without parametrizing the generic. It's optional, and only necessary for static type hinting... though it does have implications for runtime as well:

class MyFunctionGui(FunctionGui): ...

Since this is only necessary for static type hinting, and not part of the actual subclass inheritance requirements, you can also guard your subclass behind TYPE_CHECKING

from typing import TYPE_CHECKING, Generic, TypeVar

T = TypeVar("T")

class MyType(Generic[T]):
    ...

if TYPE_CHECKING:
    mybase = MyType[int]
else:
    mybase = MyType

class MySub(mybase):
    ...

in the weeds... I admit I'm a bit amused/confused that `FunctionGui[int, int, int, int, int, int]` isn't throwing a TypeError ... it has to do with the inheritance of FunctionGui itself. Note that when you call `SomeClass[]` you're using the `__class_getitem__` method: and that will hold the key as to why you sometimes do/don't see that error. This script kinda helps to show you why: ```python from typing import Generic, MutableSequence, TypeVar from magicgui.widgets import FunctionGui T = TypeVar("T") class MyType(Generic[T]): ... fgui_cgi = FunctionGui.__class_getitem__ mt_cgi = MyType.__class_getitem__ print("FunctionGui", fgui_cgi, fgui_cgi.__module__) print("MyType", mt_cgi, mt_cgi.__module__) class MyType2(MutableSequence[T]): ... print("MyType2", MyType2.__class_getitem__) ``` prints: ``` FunctionGui > types MyType None MyType2 > ``` So simply subclassing from `MutableSequence[T]`, and probably many other things, gives you a `__class_getitem__` from the `types` module that doesn't care how many parameters it gets (unlike the default `__class_getitem__` from the typing module, which gives you the error you saw: https://github.com/python/cpython/blob/main/Lib/typing.py#L291

bringing it back to something concrete. Why don't you tell me more specifically where you're running into this error. link to a repo if you have one... and I'll try to help you come up with a static typing solution you're happy with, but for runtime, none of this is necessary. magicgui does need to be able to update its static typing interface, and there's really no way to deprecate that. So this change isn't going to be reverted... but there are ways that you can support both old/new magicgui at runtime and achieve any static typing you want to (though for that you'll need to run mypy assuming certain versions of magicgui).

thanks!