From 144f159f414d6a830f82e879a8d4b461c2a95ad5 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Tue, 5 Feb 2019 00:54:35 -0800 Subject: [PATCH 1/5] Fix rgetattr() with default --- corrscope/gui/data_bind.py | 10 ++++++---- tests/test_gui_binding.py | 5 +---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/corrscope/gui/data_bind.py b/corrscope/gui/data_bind.py index f4f0b5c..c1cadff 100644 --- a/corrscope/gui/data_bind.py +++ b/corrscope/gui/data_bind.py @@ -495,11 +495,13 @@ def rgetattr(obj: DumpableAttrs, dunder_delim_path: str, *default) -> Any: :return: obj.attr1.attr2.etc """ - def _getattr(obj, attr): - return getattr(obj, attr, *default) - 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): diff --git a/tests/test_gui_binding.py b/tests/test_gui_binding.py index 1698d33..f520a4c 100644 --- a/tests/test_gui_binding.py +++ b/tests/test_gui_binding.py @@ -55,9 +55,6 @@ def test_rgetattr(): assert not rhasattr(p, "ghost__species") -@pytest.mark.xfail( - reason="rgetattr copied from Stack Overflow and subtly broken", strict=True -) def test_rgetattr_broken(): """ rgetattr(default) fails to short-circuit/return on the first missing attribute. @@ -72,5 +69,5 @@ def test_rgetattr_broken(): - None.foo AKA return 1 to caller """ - result = rgetattr(None, "foo__bar__imag", 1) + result = rgetattr(object(), "nothing__imag", 1) assert result == 1, result From 072a725747b3d43acf9aee291cb1e3651201c7eb Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Sat, 9 Feb 2019 21:26:33 -0800 Subject: [PATCH 2/5] [ui] Remove units from GUI labels --- corrscope/gui/mainwindow.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/corrscope/gui/mainwindow.ui b/corrscope/gui/mainwindow.ui index 4c63120..4224966 100644 --- a/corrscope/gui/mainwindow.ui +++ b/corrscope/gui/mainwindow.ui @@ -54,7 +54,7 @@ - Trigger Width (ms) + Trigger Width @@ -71,7 +71,7 @@ - Render Width (ms) + Render Width @@ -102,7 +102,7 @@ - Begin Time (s) + Begin Time From 73912f74fd0f9ad82fa1a5c1cc91234438d3a2f3 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Sat, 9 Feb 2019 21:40:53 -0800 Subject: [PATCH 3/5] Factor out flatten_attr() --- corrscope/gui/data_bind.py | 18 +++++-- ...{test_gui_binding.py => test_data_bind.py} | 50 ++++++++++++------- 2 files changed, 48 insertions(+), 20 deletions(-) rename tests/{test_gui_binding.py => test_data_bind.py} (72%) diff --git a/corrscope/gui/data_bind.py b/corrscope/gui/data_bind.py index c1cadff..87ddb79 100644 --- a/corrscope/gui/data_bind.py +++ b/corrscope/gui/data_bind.py @@ -512,6 +512,20 @@ def rhasattr(obj, dunder_delim_path: str): 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 def rsetattr(obj, dunder_delim_path: str, val): """ @@ -519,7 +533,5 @@ def rsetattr(obj, dunder_delim_path: str, val): :param dunder_delim_path: 'attr1__attr2__etc' :param val: obj.attr1.attr2.etc = val """ - parent, _, name = dunder_delim_path.rpartition(DUNDER) - parent_obj = rgetattr(obj, parent) if parent else obj - + parent_obj, name = flatten_attr(obj, dunder_delim_path) return setattr(parent_obj, name, val) diff --git a/tests/test_gui_binding.py b/tests/test_data_bind.py similarity index 72% rename from tests/test_gui_binding.py rename to tests/test_data_bind.py index f520a4c..7d26fc5 100644 --- a/tests/test_gui_binding.py +++ b/tests/test_data_bind.py @@ -1,6 +1,24 @@ 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(): @@ -9,22 +27,6 @@ def test_rgetattr(): 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() # Test rgetattr(present) @@ -55,6 +57,20 @@ def test_rgetattr(): assert not rhasattr(p, "ghost__species") +def test_flatten_attr(): + 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(): """ rgetattr(default) fails to short-circuit/return on the first missing attribute. From 3835421a09fa0bccd22568c5089768353a6ba446 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Sat, 9 Feb 2019 21:41:35 -0800 Subject: [PATCH 4/5] Add unit suffix metadata to config classes --- corrscope/config.py | 14 ++++++++++++++ corrscope/corrscope.py | 8 ++++---- corrscope/renderer.py | 4 ++-- tests/test_config.py | 24 +++++++++++++++++++++++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/corrscope/config.py b/corrscope/config.py index 052e6ae..8d4a301 100644 --- a/corrscope/config.py +++ b/corrscope/config.py @@ -24,6 +24,8 @@ __all__ = [ "copy_config", "DumpableAttrs", "KeywordAttrs", + "with_units", + "get_units", "Alias", "Ignored", "DumpEnumAsStr", @@ -222,6 +224,18 @@ class KeywordAttrs(DumpableAttrs): 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 class Alias: """ diff --git a/corrscope/corrscope.py b/corrscope/corrscope.py index d221a13..a9ceffb 100644 --- a/corrscope/corrscope.py +++ b/corrscope/corrscope.py @@ -12,7 +12,7 @@ import attr from corrscope import outputs as outputs_ 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.renderer import MatplotlibRenderer, RendererConfig, Renderer from corrscope.triggers import ( @@ -51,13 +51,13 @@ class Config( """ Default values indicate optional attributes. """ master_audio: Optional[str] - begin_time: float = 0 + begin_time: float = with_units("s", default=0) end_time: Optional[float] = None fps: int - trigger_ms: int - render_ms: int + trigger_ms: int = with_units("ms") + render_ms: int = with_units("ms") # Performance trigger_subsampling: int = 1 diff --git a/corrscope/renderer.py b/corrscope/renderer.py index bf88684..e917e12 100644 --- a/corrscope/renderer.py +++ b/corrscope/renderer.py @@ -6,7 +6,7 @@ import attr import matplotlib 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.outputs import RGB_DEPTH, ByteBuffer from corrscope.util import coalesce @@ -54,7 +54,7 @@ def default_color() -> str: class RendererConfig(DumpableAttrs, always_dump="*"): width: int height: int - line_width: float = 1.5 + line_width: float = with_units("px", default=1.5) bg_color: str = "#000000" init_line_color: str = default_color() diff --git a/tests/test_config.py b/tests/test_config.py index 0b094f1..6979f8e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,4 @@ +import attr import pytest from ruamel.yaml import yaml_object @@ -9,12 +10,13 @@ from corrscope.config import ( Ignored, CorrError, CorrWarning, + with_units, + get_units, ) # YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html # Load/dump infrastructure testing -import attr def test_dumpable_attrs(): @@ -52,6 +54,26 @@ def test_yaml_object(): 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 From e70ce23a475aa9e84268c2082cba7e7839351972 Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Sat, 9 Feb 2019 21:41:52 -0800 Subject: [PATCH 5/5] Show unit suffixes in GUI --- corrscope/gui/data_bind.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/corrscope/gui/data_bind.py b/corrscope/gui/data_bind.py index 87ddb79..80c08db 100644 --- a/corrscope/gui/data_bind.py +++ b/corrscope/gui/data_bind.py @@ -10,14 +10,16 @@ from typing import ( TYPE_CHECKING, Union, Sequence, + Tuple, ) +import attr from PyQt5 import QtWidgets as qw, QtCore as qc from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QPalette, QColor 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.triggers import lerp from corrscope.util import obj_name, perr @@ -229,12 +231,25 @@ class BoundLineEdit(qw.QLineEdit, 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") gui_changed = alias("valueChanged") set_model = model_setter(int) class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget): + bind_widget = BoundSpinBox.bind_widget + set_gui = alias("setValue") gui_changed = alias("valueChanged") set_model = model_setter(float)