Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/magicgui/type_map/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
17 changes: 17 additions & 0 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading