Various ListModel and ListItem improvements

Type hints, pyqt types, comments,
better __repr__ for ListItem, repr() for ListModel
This commit is contained in:
miruka 2019-05-01 17:31:02 -04:00
parent 92b3baa012
commit ada44cf6f7
3 changed files with 70 additions and 51 deletions

View File

@ -57,7 +57,7 @@ class HtmlFilter(QObject):
return str(result, "utf-8") return str(result, "utf-8")
@pyqtProperty("QVariant") @pyqtProperty("QVariantMap")
def sanitizer_settings(self) -> dict: def sanitizer_settings(self) -> dict:
# https://matrix.org/docs/spec/client_server/latest.html#m-room-message-msgtypes # https://matrix.org/docs/spec/client_server/latest.html#m-room-message-msgtypes
return { return {

View File

@ -1,15 +1,18 @@
from typing import Any, Dict, List, Mapping, Optional, Tuple from typing import Any, Dict, List, Mapping, Set, Tuple, Union
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
PyqtType = Union[str, type]
class _ListItemMeta(type(QObject)): # type: ignore class _ListItemMeta(type(QObject)): # type: ignore
__slots__ = () __slots__ = ()
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[str, Any]): def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[str, Any]):
def to_pyqt_type(type_): def to_pyqt_type(type_) -> PyqtType:
"Return an appropriate pyqtProperty type from an annotation."
try: try:
if issubclass(type_, (bool, int, float, str)): if issubclass(type_, (bool, int, float, str, type(None))):
return type_ return type_
if issubclass(type_, Mapping): if issubclass(type_, Mapping):
return "QVariantMap" return "QVariantMap"
@ -17,10 +20,14 @@ class _ListItemMeta(type(QObject)): # type: ignore
except TypeError: # e.g. None passed except TypeError: # e.g. None passed
return to_pyqt_type(type(type_)) return to_pyqt_type(type(type_))
special = {"_main_key", "_required_init_values", "_constant"} # These special attributes must not be processed like properties
constant = set(attrs.get("_constant") or set()) special = {"_main_key", "_required_init_values", "_constant"}
props = { # These properties won't be settable and will not have a notify signal
constant: Set[str] = set(attrs.get("_constant") or set())
# {property_name: (its_pyqt_type, its_default_value)}
props: Dict[str, Tuple[PyqtType, Any]] = {
name: (to_pyqt_type(attrs.get("__annotations__", {}).get(name)), name: (to_pyqt_type(attrs.get("__annotations__", {}).get(name)),
value) value)
for name, value in attrs.items() for name, value in attrs.items()
@ -29,23 +36,28 @@ class _ListItemMeta(type(QObject)): # type: ignore
name in special) name in special)
} }
signals = { # Signals for the pyqtProperty notify arguments
signals: Dict[str, pyqtSignal] = {
f"{name}Changed": pyqtSignal(type_) f"{name}Changed": pyqtSignal(type_)
for name, (type_, _) in props.items() if name not in constant for name, (type_, _) in props.items() if name not in constant
} }
# pyqtProperty() won't take None, so we make dicts of extra kwargs
# to pass for each property
pyqt_props_kwargs: Dict[str, Dict[str, Any]] = { pyqt_props_kwargs: Dict[str, Dict[str, Any]] = {
name: {"constant": True} if name in constant else name: {"constant": True} if name in constant else
{"notify": signals[f"{name}Changed"], {"notify": signals[f"{name}Changed"],
"fset": lambda self, value, n=name: ( "fset": lambda self, value, n=name: (
setattr(self, f"_{n}", value) or # type: ignore setattr(self, f"_{n}", value) or # type: ignore
getattr(self, f"{n}Changed").emit(value), getattr(self, f"{n}Changed").emit(value),
), )}
}
for name in props for name in props
} }
pyqt_props = { # The final pyqtProperty objects
pyqt_props: Dict[str, pyqtProperty] = {
name: pyqtProperty( name: pyqtProperty(
type_, type_,
fget=lambda self, n=name: getattr(self, f"_{n}"), fget=lambda self, n=name: getattr(self, f"_{n}"),
@ -55,9 +67,17 @@ class _ListItemMeta(type(QObject)): # type: ignore
} }
attrs = { attrs = {
**attrs, **signals, **pyqt_props, **attrs, # Original class attributes
**signals,
**pyqt_props,
# Set the internal _properties as slots for memory savings
"__slots__": tuple({f"_{prop}" for prop in props} & {"_main_key"}), "__slots__": tuple({f"_{prop}" for prop in props} & {"_main_key"}),
"_props": props,
"_props": props,
# The main key is either the attribute _main_key,
# or the first defined property
"_main_key": attrs.get("_main_key") or "_main_key": attrs.get("_main_key") or
list(props.keys())[0] if props else None, list(props.keys())[0] if props else None,
@ -71,25 +91,29 @@ class ListItem(QObject, metaclass=_ListItemMeta):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__() super().__init__()
method = "%s.__init__()" % type(self).__name__ method: str = "%s.__init__()" % type(self).__name__
already_set = set() already_set: Set[str] = set()
required = set(self._required_init_values) required: Set[str] = set(self._required_init_values)
required_num = len(required) + 1 # + 1 = self required_num: int = len(required) + 1 # + 1 = self
args_num = len(self._props) + 1
from_to = str(args_num) if required_num == args_num else \
f"from {required_num} to {args_num}"
args_num: int = len(self._props) + 1
from_to: str = str(args_num) if required_num == args_num else \
f"from {required_num} to {args_num}"
# Check that not too many positional arguments were passed
if len(args) > len(self._props): if len(args) > len(self._props):
raise TypeError( raise TypeError(
f"{method} takes {from_to} positional arguments but " f"{method} takes {from_to} positional arguments but "
f"{len(args) + 1} were given" f"{len(args) + 1} were given"
) )
# Set properties from provided positional arguments
for prop, value in zip(self._props, args): for prop, value in zip(self._props, args):
setattr(self, f"_{prop}", value) setattr(self, f"_{prop}", value)
already_set.add(prop) already_set.add(prop)
# Set properties from provided keyword arguments
for prop, value in kwargs.items(): for prop, value in kwargs.items():
if prop in already_set: if prop in already_set:
raise TypeError(f"{method} got multiple values for " raise TypeError(f"{method} got multiple values for "
@ -100,48 +124,38 @@ class ListItem(QObject, metaclass=_ListItemMeta):
setattr(self, f"_{prop}", value) setattr(self, f"_{prop}", value)
already_set.add(prop) already_set.add(prop)
missing = required - already_set # Check for required init arguments not provided
missing: Set[str] = required - already_set
if missing: if missing:
raise TypeError("%s missing %d required argument: %s" % ( raise TypeError("%s missing %d required argument: %s" % (
method, len(missing), ", ".join((repr(m) for m in missing)))) method, len(missing), ", ".join((repr(m) for m in missing))))
# Set default values for properties not provided in arguments
for prop in set(self._props) - already_set: for prop in set(self._props) - already_set:
# Set default values for properties not provided in arguments
setattr(self, f"_{prop}", self._props[prop][1]) setattr(self, f"_{prop}", self._props[prop][1])
def __repr__(self) -> str: def __repr__(self) -> str:
return "%s(main_key=%r, required_init_values=%r, constant=%r, %s)" % ( prop_strings = (
type(self).__name__, "\033[%dm%s\033[0m=%r" % (
self.mainKey, 1 if p == self.mainKey else 0, # 1 = term bold
self._required_init_values, p,
self._constant, getattr(self, p)
", ".join((("%s=%r" % (p, getattr(self, p))) for p in self._props)) ) for p in self._props
) )
return "%s(%s)" % (type(self).__name__, ", ".join(prop_strings))
@pyqtSlot(result=str) @pyqtSlot(result=str)
def repr(self) -> str: def repr(self) -> str:
return self.__repr() return self.__repr__()
@pyqtProperty(list) @pyqtProperty("QStringList", constant=True)
def roles(self) -> List[str]: def roles(self) -> List[str]:
return list(self._props.keys()) return list(self._props.keys())
@pyqtProperty(str) @pyqtProperty(str, constant=True)
def mainKey(self) -> str: def mainKey(self) -> str:
return self._main_key return self._main_key
class User(ListItem):
_required_init_values = {"name"}
_constant = {"name"}
name: str = ""
age: int = 0
likes: Tuple[str, ...] = ()
knows: Dict[str, str] = {}
photo: Optional[str] = None
other = None

View File

@ -35,15 +35,15 @@ class ListModel(QAbstractListModel):
return "%s(%r)" % (type(self).__name__, self._data) return "%s(%r)" % (type(self).__name__, self._data)
def __getitem__(self, index): def __getitem__(self, index: Index) -> ListItem:
return self.get(index) return self.get(index)
def __setitem__(self, index, value) -> None: def __setitem__(self, index: Index, value: NewItem) -> None:
self.set(index, value) self.set(index, value)
def __delitem__(self, index) -> None: def __delitem__(self, index: Index) -> None:
self.remove(index) self.remove(index)
@ -51,11 +51,16 @@ class ListModel(QAbstractListModel):
return len(self._data) return len(self._data)
def __iter__(self): def __iter__(self) -> Iterable[NewItem]:
return iter(self._data) return iter(self._data)
@pyqtProperty("QVariant", notify=rolesSet) @pyqtSlot(result=str)
def repr(self) -> str:
return self.__repr__()
@pyqtProperty("QStringList", notify=rolesSet)
def roles(self) -> Tuple[str, ...]: def roles(self) -> Tuple[str, ...]:
return self._data[0].roles if self._data else () # type: ignore return self._data[0].roles if self._data else () # type: ignore
@ -166,9 +171,9 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, "QVariantMap") @pyqtSlot(int, "QVariantMap")
@pyqtSlot(int, "QVariantMap", "QVariant") @pyqtSlot(int, "QVariantMap", "QStringList")
@pyqtSlot(str, "QVariantMap") @pyqtSlot(str, "QVariantMap")
@pyqtSlot(str, "QVariantMap", "QVariant") @pyqtSlot(str, "QVariantMap", "QStringList")
def update(self, def update(self,
index: Index, index: Index,
value: NewItem, value: NewItem,
@ -194,7 +199,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(str, "QVariantMap") @pyqtSlot(str, "QVariantMap")
@pyqtSlot(str, "QVariantMap", int) @pyqtSlot(str, "QVariantMap", int)
@pyqtSlot(str, "QVariantMap", int, list) @pyqtSlot(str, "QVariantMap", int, "QStringList")
def upsert(self, def upsert(self,
where_main_key_is_value: Any, where_main_key_is_value: Any,
update_with: NewItem, update_with: NewItem,