Skip to content

Instantly share code, notes, and snippets.

@kaofelix
Last active November 21, 2024 21:13
Show Gist options
  • Save kaofelix/2b5cba9d6a4a6c61db0e513247950244 to your computer and use it in GitHub Desktop.
Save kaofelix/2b5cba9d6a4a6c61db0e513247950244 to your computer and use it in GitHub Desktop.
A descriptor to create bindable properties for PySide. They can bind to any setter and signal (e.g. setValue and valueChanged on a QSlider)
from typing import Any
from PySide6.QtCore import QObject, Signal
NOT_SET: Any = object()
class BindableProperty[T]:
class Notifier(QObject):
changed = Signal(object)
def __init__(self, t: type[T], *, default: T = NOT_SET):
super().__init__()
print(t)
self._value = t() if default is NOT_SET else default
self._notifier = self.Notifier()
@property
def changed(self):
return self._notifier.changed
@property
def value(self) -> T:
return self._value
@value.setter
def value(self, value: T):
if self._value == value:
return
self._value = value
self.changed.emit(value)
def bind(self, setter, signal):
setter(self.value)
self.changed.connect(setter)
signal.connect(lambda v: setattr(self, "value", v))
class bindable[T]:
def __init__(self, t: type[T], *, default: T = NOT_SET):
self._default = t() if default is NOT_SET else default
self._type = t
def __set_name__(self, owner, name):
self._name = "_" + name
def __get__(self, obj, owner) -> BindableProperty[T]:
if not hasattr(obj, self._name):
setattr(
obj, self._name, BindableProperty(self._type, default=self._default)
)
return getattr(obj, self._name)
def __set__(self, obj, value: T):
raise AttributeError("Use the .value property to set the value")
from enum import Enum, auto
from unittest.mock import Mock
from PySide6.QtWidgets import QSlider
from utils.bindings import bindable
class TestBindableProperties:
def test_default_value(self):
class Model:
prop = bindable(int, default=10)
instance = Model()
assert instance.prop.value == 10
def test_signal_on_change_property(self):
class Model:
prop = bindable(int)
instance = Model()
slot = Mock()
instance.prop.changed.connect(slot)
instance.prop.value = 2
assert instance.prop.value == 2
slot.assert_called_once_with(2)
def test_bind_property(self, qtbot):
class Model:
prop = bindable(int)
qtbot.addWidget(slider := QSlider())
qtbot.addWidget(another_slider := QSlider())
m = Model()
m.prop.value = 10
m.prop.bind(slider.setValue, slider.valueChanged)
assert slider.value() == 10
m.prop.value = 20
assert slider.value() == 20
m.prop.bind(another_slider.setValue, another_slider.valueChanged)
assert another_slider.value() == 20
slider.setSliderDown(True)
slider.setSliderPosition(30)
slider.setSliderDown(False)
assert m.prop.value == 30
assert another_slider.value() == 30
def test_enum_value(self):
class AnEnum(Enum):
FIRST = auto()
SECOND = auto()
class Model:
int_prop = bindable(int)
str_prop = bindable(str)
enum_prop = bindable(AnEnum, default=AnEnum.FIRST)
instance = Model()
assert instance.int_prop.value == 0
assert instance.str_prop.value == ""
assert instance.enum_prop.value == AnEnum.FIRST
instance.int_prop.changed.connect(int_slot := Mock())
instance.str_prop.changed.connect(str_slot := Mock())
instance.enum_prop.changed.connect(enum_slot := Mock())
instance.int_prop.value = 10
instance.str_prop.value = "new"
instance.enum_prop.value = AnEnum.SECOND
int_slot.assert_called_once_with(10)
str_slot.assert_called_once_with("new")
enum_slot.assert_called_once_with(AnEnum.SECOND)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment