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
24 changes: 23 additions & 1 deletion src/magicgui/type_map/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_factory.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import gc
import weakref

import pytest
from psygnal import EmitLoopError

Expand Down Expand Up @@ -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."""

Expand Down
Loading