From 9fb93ea0fc28315cab9f75bd39e22c9aa9bbedee Mon Sep 17 00:00:00 2001 From: "Joseph.Demarest" Date: Sun, 26 Jul 2026 19:35:10 -0400 Subject: [PATCH] fix: bind magic factories as method descriptors --- src/magicgui/type_map/_magicgui.py | 24 ++++++++++++++- tests/test_factory.py | 47 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index 4007df0bb..3f0511d85 100644 --- a/src/magicgui/type_map/_magicgui.py +++ b/src/magicgui/type_map/_magicgui.py @@ -2,7 +2,7 @@ import inspect from functools import partial -from typing import Any, Callable, Generic, TypeVar +from typing import Any, Callable, Generic, TypeVar, cast from magicgui.type_map._type_map import TypeMap from magicgui.widgets import FunctionGui @@ -83,6 +83,28 @@ def __repr__(self) -> str: ] return f"MagicFactory({', '.join(args)})" + def __get__(self, obj: Any, objtype: type | None = None) -> MagicFactory[_FGuiVar]: + """Bind the factory's function when accessed as a method descriptor.""" + if obj is None: + return self + + factory_kwargs = self.keywords.copy() + function = factory_kwargs.pop("function") + function_get = getattr(function, "__get__", None) + bound_function = ( + function_get(obj, objtype) + if function_get is not None + else partial(function, obj) + ) + return type(self)( + bound_function, + *self.args, + magic_class=cast("type[_FGuiVar]", self.func), + widget_init=self._widget_init, + type_map=self._type_map, + **factory_kwargs, + ) + # TODO: annotate args and kwargs here so that # calling a MagicFactory instance gives proper mypy hints def __call__(self, *args: Any, **kwargs: Any) -> _FGuiVar: diff --git a/tests/test_factory.py b/tests/test_factory.py index 04f8d6118..32a02968a 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,3 +1,6 @@ +import gc +import weakref + import pytest from psygnal import EmitLoopError @@ -48,6 +51,50 @@ def factory(x="a"): assert isinstance(widget_b.x, ComboBox) +def test_magic_factory_method_descriptor(): + initialized = [] + + def widget_init(widget): + initialized.append(widget) + + class Example: + def __init__(self, value): + self.value = value + + @magic_factory(call_button=True, widget_init=widget_init) + def factory(self, suffix: str = ""): + return f"{self.value}{suffix}" + + first = Example("first") + second = Example("second") + first_widget = first.factory() + second_widget = second.factory() + + assert Example.factory is Example.__dict__["factory"] + assert first_widget.call_button + assert first_widget() == "first" + assert second_widget(suffix="!") == "second!" + assert initialized == [first_widget, second_widget] + + +def test_magic_factory_method_descriptor_does_not_retain_instance(): + class Example: + @magic_factory + def factory(self): + return self + + instance = Example() + instance_ref = weakref.ref(instance) + bound_factory = instance.factory + + del instance + assert instance_ref() is not None + + del bound_factory + gc.collect() + assert instance_ref() is None + + def test_magic_factory_repr(): """Test basic magic_factory behavior."""