forked from M-Labs/thermostat
127 lines
3.6 KiB
Python
127 lines
3.6 KiB
Python
|
# A Custom Class that allows defining a QObject Property Dynamically
|
||
|
# Adapted from: https://stackoverflow.com/questions/48425316/how-to-create-pyqt-properties-dynamically
|
||
|
|
||
|
from functools import wraps
|
||
|
|
||
|
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
|
||
|
|
||
|
|
||
|
class PropertyMeta(type(QObject)):
|
||
|
"""Lets a class succinctly define Qt properties."""
|
||
|
|
||
|
def __new__(cls, name, bases, attrs):
|
||
|
for key in list(attrs.keys()):
|
||
|
attr = attrs[key]
|
||
|
if not isinstance(attr, Property):
|
||
|
continue
|
||
|
|
||
|
types = {list: "QVariantList", dict: "QVariantMap"}
|
||
|
type_ = types.get(attr.type_, attr.type_)
|
||
|
|
||
|
notifier = pyqtSignal(type_)
|
||
|
attrs[f"{key}_update"] = notifier
|
||
|
attrs[key] = PropertyImpl(type_=type_, name=key, notify=notifier)
|
||
|
|
||
|
return super().__new__(cls, name, bases, attrs)
|
||
|
|
||
|
|
||
|
class Property:
|
||
|
"""Property definition.
|
||
|
|
||
|
Instances of this class will be replaced with their full
|
||
|
implementation by the PropertyMeta metaclass.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, type_):
|
||
|
self.type_ = type_
|
||
|
|
||
|
|
||
|
class PropertyImpl(pyqtProperty):
|
||
|
"""Property implementation: gets, sets, and notifies of change."""
|
||
|
|
||
|
def __init__(self, type_, name, notify):
|
||
|
super().__init__(type_, self.getter, self.setter, notify=notify)
|
||
|
self.name = name
|
||
|
|
||
|
def getter(self, instance):
|
||
|
return getattr(instance, f"_{self.name}")
|
||
|
|
||
|
def setter(self, instance, value):
|
||
|
signal = getattr(instance, f"{self.name}_update")
|
||
|
|
||
|
if type(value) in {list, dict}:
|
||
|
value = make_notified(value, signal)
|
||
|
|
||
|
setattr(instance, f"_{self.name}", value)
|
||
|
signal.emit(value)
|
||
|
|
||
|
|
||
|
class MakeNotified:
|
||
|
"""Adds notifying signals to lists and dictionaries.
|
||
|
|
||
|
Creates the modified classes just once, on initialization.
|
||
|
"""
|
||
|
|
||
|
change_methods = {
|
||
|
list: [
|
||
|
"__delitem__",
|
||
|
"__iadd__",
|
||
|
"__imul__",
|
||
|
"__setitem__",
|
||
|
"append",
|
||
|
"extend",
|
||
|
"insert",
|
||
|
"pop",
|
||
|
"remove",
|
||
|
"reverse",
|
||
|
"sort",
|
||
|
],
|
||
|
dict: [
|
||
|
"__delitem__",
|
||
|
"__ior__",
|
||
|
"__setitem__",
|
||
|
"clear",
|
||
|
"pop",
|
||
|
"popitem",
|
||
|
"setdefault",
|
||
|
"update",
|
||
|
],
|
||
|
}
|
||
|
|
||
|
def __init__(self):
|
||
|
if not hasattr(dict, "__ior__"):
|
||
|
# Dictionaries don't have | operator in Python < 3.9.
|
||
|
self.change_methods[dict].remove("__ior__")
|
||
|
self.notified_class = {
|
||
|
type_: self.make_notified_class(type_) for type_ in [list, dict]
|
||
|
}
|
||
|
|
||
|
def __call__(self, seq, signal):
|
||
|
"""Returns a notifying version of the supplied list or dict."""
|
||
|
notified_class = self.notified_class[type(seq)]
|
||
|
notified_seq = notified_class(seq)
|
||
|
notified_seq.signal = signal
|
||
|
return notified_seq
|
||
|
|
||
|
@classmethod
|
||
|
def make_notified_class(cls, parent):
|
||
|
notified_class = type(f"notified_{parent.__name__}", (parent,), {})
|
||
|
for method_name in cls.change_methods[parent]:
|
||
|
original = getattr(notified_class, method_name)
|
||
|
notified_method = cls.make_notified_method(original, parent)
|
||
|
setattr(notified_class, method_name, notified_method)
|
||
|
return notified_class
|
||
|
|
||
|
@staticmethod
|
||
|
def make_notified_method(method, parent):
|
||
|
@wraps(method)
|
||
|
def notified_method(self, *args, **kwargs):
|
||
|
result = getattr(parent, method.__name__)(self, *args, **kwargs)
|
||
|
self.signal.emit(self)
|
||
|
return result
|
||
|
|
||
|
return notified_method
|
||
|
|
||
|
|
||
|
make_notified = MakeNotified()
|