From 83c6ab5e7bbdea275ae6320b12a4edf5a9d31119 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:53:47 -0700 Subject: [PATCH] fix: make MagicFactory work as a method descriptor magic_factory used on a method did not bind the instance: functools.partial had no __get__ before Python 3.13, so self was never passed, and from 3.13 on partial.__get__ binds the instance as the first positional argument of the factory (which only accepts keyword arguments). Either way calling the resulting widget raised TypeError: missing a required argument: 'self'. MagicFactory now implements __get__ itself, returning a factory whose first parameter is bound to the instance, mirroring FunctionGui.__get__. Adds tests/test_factory.py::test_magic_factory_as_method. --- src/magicgui/type_map/_magicgui.py | 31 ++++++++++++++++++++++++++++++ tests/test_factory.py | 17 ++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index 4007df0bb..f23ac255f 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 04f8d6118..8cfed64a1 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