diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index 4007df0b..f23ac255 100644 --- a/src/magicgui/type_map/_magicgui.py +++ b/src/magicgui/type_map/_magicgui.py @@ -104,6 +104,37 @@ def __call__(self, *args: Any, **kwargs: Any) -> _FGuiVar: self._widget_init(widget) return widget + def __get__(self, obj: object, objtype: type | None = None) -> MagicFactory: + """Provide descriptor protocol. + + This allows the `@magic_factory` decorator to work on a method as well as + on a plain function. Accessing the attribute on an instance returns a + factory whose widgets bind the first parameter of the function to that + instance, mirroring `FunctionGui.__get__`. + + Without this, `functools.partial.__get__` (added in Python 3.13) would + bind the instance as the first *positional* argument of the factory, + while on older versions the instance was simply never passed at all. + """ + if obj is None: + return self + function = self.keywords.get("function") + if function is None: # pragma: no cover + return self + p0 = next(iter(inspect.signature(function).parameters), None) + if p0 is None: # pragma: no cover + return self + keywords = self.keywords.copy() + param_options = dict(keywords.pop("param_options", None) or {}) + param_options.setdefault(p0, {"bind": obj}) + return type(self)( + magic_class=self.func, # type: ignore + widget_init=self._widget_init, + type_map=self._type_map, + param_options=param_options, + **keywords, + ) + def __getattr__(self, name: str) -> Any: """Allow accessing FunctionGui attributes without mypy error.""" diff --git a/tests/test_factory.py b/tests/test_factory.py index 04f8d611..8cfed64a 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -145,3 +145,20 @@ def factory(arg=None): return 1 assert factory()() == 1 + + +def test_magic_factory_as_method(): + """A magic_factory used on a method should bind the instance to `self`.""" + + class MyClass: + def __init__(self, x: int): + self.x = x + + @magic_factory + def factory(self): + return self.x + + obj = MyClass(x=5) + widget = obj.factory() + assert isinstance(widget, FunctionGui) + assert widget() == 5