Merge pull request #191 from nyanpasu64/gui-unit-suffix

Add unit suffixes to GUI spinboxes
pull/357/head
nyanpasu64 2019-02-10 23:34:37 -08:00 zatwierdzone przez GitHub
commit 3ccbcc6734
7 zmienionych plików z 117 dodań i 39 usunięć

Wyświetl plik

@ -24,6 +24,8 @@ __all__ = [
"copy_config", "copy_config",
"DumpableAttrs", "DumpableAttrs",
"KeywordAttrs", "KeywordAttrs",
"with_units",
"get_units",
"Alias", "Alias",
"Ignored", "Ignored",
"DumpEnumAsStr", "DumpEnumAsStr",
@ -222,6 +224,18 @@ class KeywordAttrs(DumpableAttrs):
super().__init_subclass__(kw_only=True, **kwargs) super().__init_subclass__(kw_only=True, **kwargs)
UNIT_SUFFIX = "suffix"
def with_units(unit, **kwargs):
metadata = {UNIT_SUFFIX: f" {unit}"}
return attr.ib(metadata=metadata, **kwargs)
def get_units(field: attr.Attribute) -> str:
return field.metadata.get(UNIT_SUFFIX, "")
@attr.dataclass @attr.dataclass
class Alias: class Alias:
""" """

Wyświetl plik

@ -12,7 +12,7 @@ import attr
from corrscope import outputs as outputs_ from corrscope import outputs as outputs_
from corrscope.channel import Channel, ChannelConfig from corrscope.channel import Channel, ChannelConfig
from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError, with_units
from corrscope.layout import LayoutConfig from corrscope.layout import LayoutConfig
from corrscope.renderer import MatplotlibRenderer, RendererConfig, Renderer from corrscope.renderer import MatplotlibRenderer, RendererConfig, Renderer
from corrscope.triggers import ( from corrscope.triggers import (
@ -51,13 +51,13 @@ class Config(
""" Default values indicate optional attributes. """ """ Default values indicate optional attributes. """
master_audio: Optional[str] master_audio: Optional[str]
begin_time: float = 0 begin_time: float = with_units("s", default=0)
end_time: Optional[float] = None end_time: Optional[float] = None
fps: int fps: int
trigger_ms: int trigger_ms: int = with_units("ms")
render_ms: int render_ms: int = with_units("ms")
# Performance # Performance
trigger_subsampling: int = 1 trigger_subsampling: int = 1

Wyświetl plik

@ -10,14 +10,16 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Union, Union,
Sequence, Sequence,
Tuple,
) )
import attr
from PyQt5 import QtWidgets as qw, QtCore as qc from PyQt5 import QtWidgets as qw, QtCore as qc
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QPalette, QColor from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from corrscope.config import CorrError, DumpableAttrs from corrscope.config import CorrError, DumpableAttrs, get_units
from corrscope.gui.util import color2hex from corrscope.gui.util import color2hex
from corrscope.triggers import lerp from corrscope.triggers import lerp
from corrscope.util import obj_name, perr from corrscope.util import obj_name, perr
@ -229,12 +231,25 @@ class BoundLineEdit(qw.QLineEdit, BoundWidget):
class BoundSpinBox(qw.QSpinBox, BoundWidget): class BoundSpinBox(qw.QSpinBox, BoundWidget):
def bind_widget(self, model: PresentationModel, path: str, *args, **kwargs) -> None:
BoundWidget.bind_widget(self, model, path, *args, **kwargs)
try:
parent, name = flatten_attr(model.cfg, path)
except AttributeError:
return
fields = attr.fields_dict(type(parent))
field = fields[name]
self.setSuffix(get_units(field))
set_gui = alias("setValue") set_gui = alias("setValue")
gui_changed = alias("valueChanged") gui_changed = alias("valueChanged")
set_model = model_setter(int) set_model = model_setter(int)
class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget): class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget):
bind_widget = BoundSpinBox.bind_widget
set_gui = alias("setValue") set_gui = alias("setValue")
gui_changed = alias("valueChanged") gui_changed = alias("valueChanged")
set_model = model_setter(float) set_model = model_setter(float)
@ -495,11 +510,13 @@ def rgetattr(obj: DumpableAttrs, dunder_delim_path: str, *default) -> Any:
:return: obj.attr1.attr2.etc :return: obj.attr1.attr2.etc
""" """
def _getattr(obj, attr):
return getattr(obj, attr, *default)
attrs: List[Any] = dunder_delim_path.split(DUNDER) attrs: List[Any] = dunder_delim_path.split(DUNDER)
return functools.reduce(_getattr, [obj] + attrs) try:
return functools.reduce(getattr, attrs, obj)
except AttributeError:
if default:
return default[0]
raise
def rhasattr(obj, dunder_delim_path: str): def rhasattr(obj, dunder_delim_path: str):
@ -510,6 +527,20 @@ def rhasattr(obj, dunder_delim_path: str):
return False return False
def flatten_attr(obj, dunder_delim_path: str) -> Tuple[Any, str]:
"""
:param obj: Object
:param dunder_delim_path: 'attr1__attr2__etc'
:return: (shallow_obj, name) such that
getattr(shallow_obj, name) == rgetattr(obj, dunder_delim_path).
"""
parent, _, name = dunder_delim_path.rpartition(DUNDER)
parent_obj = rgetattr(obj, parent) if parent else obj
return parent_obj, name
# https://stackoverflow.com/a/31174427/2683842 # https://stackoverflow.com/a/31174427/2683842
def rsetattr(obj, dunder_delim_path: str, val): def rsetattr(obj, dunder_delim_path: str, val):
""" """
@ -517,7 +548,5 @@ def rsetattr(obj, dunder_delim_path: str, val):
:param dunder_delim_path: 'attr1__attr2__etc' :param dunder_delim_path: 'attr1__attr2__etc'
:param val: obj.attr1.attr2.etc = val :param val: obj.attr1.attr2.etc = val
""" """
parent, _, name = dunder_delim_path.rpartition(DUNDER) parent_obj, name = flatten_attr(obj, dunder_delim_path)
parent_obj = rgetattr(obj, parent) if parent else obj
return setattr(parent_obj, name, val) return setattr(parent_obj, name, val)

Wyświetl plik

@ -54,7 +54,7 @@
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="trigger_msL"> <widget class="QLabel" name="trigger_msL">
<property name="text"> <property name="text">
<string>Trigger Width (ms)</string> <string>Trigger Width</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -71,7 +71,7 @@
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="render_msL"> <widget class="QLabel" name="render_msL">
<property name="text"> <property name="text">
<string>Render Width (ms)</string> <string>Render Width</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -102,7 +102,7 @@
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="begin_timeL"> <widget class="QLabel" name="begin_timeL">
<property name="text"> <property name="text">
<string>Begin Time (s)</string> <string>Begin Time</string>
</property> </property>
</widget> </widget>
</item> </item>

Wyświetl plik

@ -6,7 +6,7 @@ import attr
import matplotlib import matplotlib
import numpy as np import numpy as np
from corrscope.config import DumpableAttrs from corrscope.config import DumpableAttrs, with_units
from corrscope.layout import RendererLayout, LayoutConfig, EdgeFinder from corrscope.layout import RendererLayout, LayoutConfig, EdgeFinder
from corrscope.outputs import RGB_DEPTH, ByteBuffer from corrscope.outputs import RGB_DEPTH, ByteBuffer
from corrscope.util import coalesce from corrscope.util import coalesce
@ -54,7 +54,7 @@ def default_color() -> str:
class RendererConfig(DumpableAttrs, always_dump="*"): class RendererConfig(DumpableAttrs, always_dump="*"):
width: int width: int
height: int height: int
line_width: float = 1.5 line_width: float = with_units("px", default=1.5)
bg_color: str = "#000000" bg_color: str = "#000000"
init_line_color: str = default_color() init_line_color: str = default_color()

Wyświetl plik

@ -1,3 +1,4 @@
import attr
import pytest import pytest
from ruamel.yaml import yaml_object from ruamel.yaml import yaml_object
@ -9,12 +10,13 @@ from corrscope.config import (
Ignored, Ignored,
CorrError, CorrError,
CorrWarning, CorrWarning,
with_units,
get_units,
) )
# YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html # YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html
# Load/dump infrastructure testing # Load/dump infrastructure testing
import attr
def test_dumpable_attrs(): def test_dumpable_attrs():
@ -52,6 +54,26 @@ def test_yaml_object():
assert s == "!Bar {}\n" assert s == "!Bar {}\n"
# Test per-field unit suffixes (used by GUI)
def test_unit_suffix():
class Foo(DumpableAttrs):
xs: int = with_units("xs")
ys: int = with_units("ys", default=2)
no_unit: int = 3
# Assert class constructor works.
foo = Foo(1, 2, 3)
foo_default = Foo(1)
# Assert units work.
foo_fields = attr.fields(Foo)
assert get_units(foo_fields.xs) == " xs"
assert get_units(foo_fields.ys) == " ys"
assert get_units(foo_fields.no_unit) == ""
# Dataclass dump testing # Dataclass dump testing

Wyświetl plik

@ -1,6 +1,24 @@
import pytest import pytest
from corrscope.gui.data_bind import rgetattr, rsetattr, rhasattr from corrscope.gui.data_bind import rgetattr, rsetattr, rhasattr, flatten_attr
class Person(object):
def __init__(self):
self.pet = Pet()
self.residence = Residence()
class Pet(object):
def __init__(self, name="Fido", species="Dog"):
self.name = name
self.species = species
class Residence(object):
def __init__(self, type="House", sqft=None):
self.type = type
self.sqft = sqft
def test_rgetattr(): def test_rgetattr():
@ -9,22 +27,6 @@ def test_rgetattr():
https://stackoverflow__com/a/31174427/ https://stackoverflow__com/a/31174427/
""" """
class Person(object):
def __init__(self):
self.pet = Pet()
self.residence = Residence()
class Pet(object):
def __init__(self, name="Fido", species="Dog"):
self.name = name
self.species = species
class Residence(object):
def __init__(self, type="House", sqft=None):
self.type = type
self.sqft = sqft
p = Person() p = Person()
# Test rgetattr(present) # Test rgetattr(present)
@ -55,9 +57,20 @@ def test_rgetattr():
assert not rhasattr(p, "ghost__species") assert not rhasattr(p, "ghost__species")
@pytest.mark.xfail( def test_flatten_attr():
reason="rgetattr copied from Stack Overflow and subtly broken", strict=True p = Person()
)
# Test nested
flat, name = flatten_attr(p, "pet__name")
assert flat is p.pet
assert name == "name"
# Test 1 level
flat, name = flatten_attr(p, "pet")
assert flat is p
assert name == "pet"
def test_rgetattr_broken(): def test_rgetattr_broken():
""" """
rgetattr(default) fails to short-circuit/return on the first missing attribute. rgetattr(default) fails to short-circuit/return on the first missing attribute.
@ -72,5 +85,5 @@ def test_rgetattr_broken():
- None.foo AKA return 1 to caller - None.foo AKA return 1 to caller
""" """
result = rgetattr(None, "foo__bar__imag", 1) result = rgetattr(object(), "nothing__imag", 1)
assert result == 1, result assert result == 1, result