From e3960ef3631ffd9e3f2bf7c5f5e18dfc327c1b52 Mon Sep 17 00:00:00 2001 From: Kai Asaoka Date: Thu, 7 Aug 2025 16:22:29 -0700 Subject: [PATCH 1/5] add support to initialise NTTable like NTScalar --- src/p4p/nt/__init__.py | 85 +++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index e4af0489..1d09b109 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -10,7 +10,7 @@ from collections import OrderedDict from ..wrapper import Type, Value from .common import timeStamp, alarm, NTBase -from .scalar import NTScalar +from .scalar import NTScalar, ntwrappercommon, _metaHelper from .ndarray import NTNDArray from .enum import NTEnum @@ -147,23 +147,26 @@ class NTTable(NTBase): Value = Value @staticmethod - def buildType(columns=[], extra=[]): + def buildType(columns=[], extra=[], display=False, control=False, valueAlarm=False): """Build a table :param list columns: List of column names and types. eg [('colA', 'd')] :param list extra: A list of tuples describing additional non-standard fields :returns: A :py:class:`Type` """ - return Type(id="epics:nt/NTTable:1.0", - spec=[ - ('labels', 'as'), - ('value', ('S', None, columns)), - ('descriptor', 's'), - ('alarm', alarm), - ('timeStamp', timeStamp), - ] + extra) - - def __init__(self, columns=[], extra=[]): + F = [ + ('labels', 'as'), + ('value', ('S', None, columns)), + ('descriptor', 's'), + ('alarm', alarm), + ('timeStamp', timeStamp), + ] + # TODO: different metadata options + _metaHelper(F, 'i', display=display, control=control, valueAlarm=valueAlarm) + F.extend(extra) + return Type(id="epics:nt/NTTable:1.0", spec=F) + + def __init__(self, columns=[], **kws): self.labels = [] C = [] for col, type in columns: @@ -171,7 +174,7 @@ def __init__(self, columns=[], extra=[]): raise ValueError("NTTable column types may not be array") C.append((col, 'a' + type)) self.labels.append(col) - self.type = self.buildType(C, extra=extra) + self.type = self.buildType(C, **kws) def wrap(self, values, **kws): """Pack an iterable of dict into a Value @@ -184,11 +187,15 @@ def wrap(self, values, **kws): """ if isinstance(values, Value): return values - else: - cols = dict([(L, []) for L in self.labels]) + elif isinstance(values, list) or isinstance(values, dict): try: + cols = dict([(L, []) for L in self.labels]) + if isinstance(values, list): + update = values + else: + update = values['value'] # unzip list of dict - for V in values: + for V in update: for L in self.labels: try: cols[L].append(V[L]) @@ -199,40 +206,56 @@ def wrap(self, values, **kws): V = cols[L] if len(V) == 0: del cols[L] - try: - values = self.Value(self.type, { + update = self.Value(self.type, { 'labels': self.labels, 'value': cols, }) except: _log.error("Failed to encode '%s' with %s", cols, self.labels) raise + if isinstance(values, list): + values = update + else: + for key, val in values.items(): + if key != 'value' and key != 'labels': + update[key] = val + try: + values = self.Value(self.type, update) + except: + _log.error("Failed to encode '%s' with %s", self.type, update) + raise except: _log.exception("Failed to wrap: %s", values) raise + else: + # index or string + V = self.type() + values = V return self._annotate(values, **kws) - @staticmethod - def unwrap(value): + def unwrap(self, value): """Iterate an NTTable :returns: An iterator yielding an OrderedDict for each column """ - ret = [] - # build lists of column names, and value - lbl, cols = [], [] - for cname, cval in value.value.items(): - lbl.append(cname) - cols.append(cval) + if not (set(value) - {'labels', 'value'}): + ret = [] - # zip together column arrays to iterate over rows - for rval in izip(*cols): - # zip together column names and row values - ret.append(OrderedDict(zip(lbl, rval))) + # build lists of column names, and value + lbl, cols = [], [] + for cname, cval in value.value.items(): + lbl.append(cname) + cols.append(cval) - return ret + # zip together column arrays to iterate over rows + for rval in izip(*cols): + # zip together column names and row values + ret.append(OrderedDict(zip(lbl, rval))) + return ret + else: + return ntwrappercommon._store(self, value) class NTURI(object): From bc9d42a69c03890f0bbb9b8acaac80403b4c8842 Mon Sep 17 00:00:00 2001 From: Kai Asaoka Date: Thu, 7 Aug 2025 17:25:53 -0700 Subject: [PATCH 2/5] add test cases for dict_wrap and dict_unwrap --- src/p4p/nt/__init__.py | 37 ++++++++++++++++++++++--------------- src/p4p/test/test_nt.py | 38 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index 1d09b109..edeb0524 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -167,6 +167,7 @@ def buildType(columns=[], extra=[], display=False, control=False, valueAlarm=Fal return Type(id="epics:nt/NTTable:1.0", spec=F) def __init__(self, columns=[], **kws): + self.unwrap = self._instance_unwrap self.labels = [] C = [] for col, type in columns: @@ -206,6 +207,7 @@ def wrap(self, values, **kws): V = cols[L] if len(V) == 0: del cols[L] + try: update = self.Value(self.type, { 'labels': self.labels, @@ -234,28 +236,33 @@ def wrap(self, values, **kws): values = V return self._annotate(values, **kws) - def unwrap(self, value): + def _instance_unwrap(self, value): + """Unwrap an NTTable into a Value + + :returns: NTTable Value object + """ + return ntwrappercommon._store(self, value) + + @staticmethod + def unwrap(value): """Iterate an NTTable :returns: An iterator yielding an OrderedDict for each column """ + ret = [] - if not (set(value) - {'labels', 'value'}): - ret = [] + # build lists of column names, and value + lbl, cols = [], [] + for cname, cval in value.value.items(): + lbl.append(cname) + cols.append(cval) - # build lists of column names, and value - lbl, cols = [], [] - for cname, cval in value.value.items(): - lbl.append(cname) - cols.append(cval) + # zip together column arrays to iterate over rows + for rval in izip(*cols): + # zip together column names and row values + ret.append(OrderedDict(zip(lbl, rval))) - # zip together column arrays to iterate over rows - for rval in izip(*cols): - # zip together column names and row values - ret.append(OrderedDict(zip(lbl, rval))) - return ret - else: - return ntwrappercommon._store(self, value) + return ret class NTURI(object): diff --git a/src/p4p/test/test_nt.py b/src/p4p/test/test_nt.py index b5a75a10..cd711d8b 100644 --- a/src/p4p/test/test_nt.py +++ b/src/p4p/test/test_nt.py @@ -132,7 +132,7 @@ def test_string_array_wrap(self): class TestTable(RefTestCase): - def test_wrap(self): + def test_dict_of_values_wrap(self): NT = nt.NTTable(columns=[ ('a', 'i'), ('b', 's'), @@ -145,7 +145,21 @@ def test_wrap(self): assert_aequal(V.value.a, [5, 6]) self.assertEqual(V.value.b, ['one', 'two']) - def test_unwrap(self): + def test_dict_wrap(self): + NT = nt.NTTable(columns=[ + ('a', 'i'), + ('b', 's'), + ]) + V = NT.wrap({'value' : [ + {'a': 5, 'b': 'one'}, + {'a': 6, 'b': 'two'}, + ]}) + + assert_aequal(V.value.a, [5, 6]) + self.assertEqual(V.value.b, ['one', 'two']) + + + def test_dict_of_values_unwrap(self): T = nt.NTTable.buildType(columns=[ ('a', 'ai'), ('b', 'as'), @@ -165,6 +179,26 @@ def test_unwrap(self): OrderedDict([('a', 6), ('b', u'two')]), ]) + def test_dict_unwrap(self): + NT = nt.NTTable(columns=[ + ('a', 'i'), + ('b', 's'), + ]) + T = nt.NTTable.buildType(columns=[ + ('a', 'ai'), + ('b', 'as'), + ]) + V = Value(T, { + 'labels': ['a', 'b'], + 'value': { + 'a': [5, 6], + 'b': ['one', 'two'], + }, + }) + + P = NT.unwrap(V) + + self.assertEqual(P, nt.ntwrappercommon._store(NT, V)) class TestURI(RefTestCase): From 332381af982c3b3d2931fc5e909c21c86fc7ccb9 Mon Sep 17 00:00:00 2001 From: Kai Asaoka Date: Fri, 8 Aug 2025 09:17:35 -0700 Subject: [PATCH 3/5] consolidate nttable unwrap to match ntscalar behaviour --- src/p4p/nt/__init__.py | 27 +++------------------------ src/p4p/test/test_nt.py | 29 ++--------------------------- 2 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index edeb0524..d1928ef5 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -167,7 +167,6 @@ def buildType(columns=[], extra=[], display=False, control=False, valueAlarm=Fal return Type(id="epics:nt/NTTable:1.0", spec=F) def __init__(self, columns=[], **kws): - self.unwrap = self._instance_unwrap self.labels = [] C = [] for col, type in columns: @@ -236,33 +235,13 @@ def wrap(self, values, **kws): values = V return self._annotate(values, **kws) - def _instance_unwrap(self, value): - """Unwrap an NTTable into a Value + def unwrap(self, value): + """Unwrap an NTTable into ntwrappercommon - :returns: NTTable Value object + :returns: ntwrappercommon object """ return ntwrappercommon._store(self, value) - @staticmethod - def unwrap(value): - """Iterate an NTTable - - :returns: An iterator yielding an OrderedDict for each column - """ - ret = [] - - # build lists of column names, and value - lbl, cols = [], [] - for cname, cval in value.value.items(): - lbl.append(cname) - cols.append(cval) - - # zip together column arrays to iterate over rows - for rval in izip(*cols): - # zip together column names and row values - ret.append(OrderedDict(zip(lbl, rval))) - - return ret class NTURI(object): diff --git a/src/p4p/test/test_nt.py b/src/p4p/test/test_nt.py index cd711d8b..54503311 100644 --- a/src/p4p/test/test_nt.py +++ b/src/p4p/test/test_nt.py @@ -158,32 +158,8 @@ def test_dict_wrap(self): assert_aequal(V.value.a, [5, 6]) self.assertEqual(V.value.b, ['one', 'two']) - - def test_dict_of_values_unwrap(self): - T = nt.NTTable.buildType(columns=[ - ('a', 'ai'), - ('b', 'as'), - ]) - V = Value(T, { - 'labels': ['a', 'b'], - 'value': { - 'a': [5, 6], - 'b': ['one', 'two'], - }, - }) - - P = list(nt.NTTable.unwrap(V)) - - self.assertListEqual(P, [ - OrderedDict([('a', 5), ('b', u'one')]), - OrderedDict([('a', 6), ('b', u'two')]), - ]) - - def test_dict_unwrap(self): - NT = nt.NTTable(columns=[ - ('a', 'i'), - ('b', 's'), - ]) + def test_unwrap(self): + NT = nt.NTTable() T = nt.NTTable.buildType(columns=[ ('a', 'ai'), ('b', 'as'), @@ -197,7 +173,6 @@ def test_dict_unwrap(self): }) P = NT.unwrap(V) - self.assertEqual(P, nt.ntwrappercommon._store(NT, V)) class TestURI(RefTestCase): From 10970decb6275e24bdd6fdbe4f236fca3b8dd82f Mon Sep 17 00:00:00 2001 From: Kai Asaoka Date: Fri, 8 Aug 2025 10:35:22 -0700 Subject: [PATCH 4/5] refactor nttable wrap to improve readability --- src/p4p/nt/__init__.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index d1928ef5..a56bb1e9 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -187,7 +187,7 @@ def wrap(self, values, **kws): """ if isinstance(values, Value): return values - elif isinstance(values, list) or isinstance(values, dict): + elif isinstance(values, (list, dict)): try: cols = dict([(L, []) for L in self.labels]) if isinstance(values, list): @@ -206,23 +206,17 @@ def wrap(self, values, **kws): V = cols[L] if len(V) == 0: del cols[L] - - try: - update = self.Value(self.type, { - 'labels': self.labels, - 'value': cols, - }) - except: - _log.error("Failed to encode '%s' with %s", cols, self.labels) - raise + update = {'labels': self.labels, 'value': cols} if isinstance(values, list): - values = update - else: - for key, val in values.items(): - if key != 'value' and key != 'labels': - update[key] = val try: values = self.Value(self.type, update) + except: + _log.error("Failed to encode '%s' with %s", cols, self.labels) + raise + else: + try: + values.update(update) + values = self.Value(self.type, values) except: _log.error("Failed to encode '%s' with %s", self.type, update) raise From a0647b50b3118c56935945d63a9731c9507df128 Mon Sep 17 00:00:00 2001 From: Kai Asaoka Date: Thu, 14 Aug 2025 12:15:38 -0700 Subject: [PATCH 5/5] remove exception catching in nttable wrap --- src/p4p/nt/__init__.py | 54 ++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index a56bb1e9..dcc68cf6 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -188,41 +188,29 @@ def wrap(self, values, **kws): if isinstance(values, Value): return values elif isinstance(values, (list, dict)): - try: - cols = dict([(L, []) for L in self.labels]) - if isinstance(values, list): - update = values - else: - update = values['value'] - # unzip list of dict - for V in update: - for L in self.labels: - try: - cols[L].append(V[L]) - except (IndexError, KeyError): - pass - # allow omit empty columns + cols = dict([(L, []) for L in self.labels]) + if isinstance(values, list): + update = values + else: + update = values['value'] + # unzip list of dict + for V in update: for L in self.labels: - V = cols[L] - if len(V) == 0: - del cols[L] - update = {'labels': self.labels, 'value': cols} - if isinstance(values, list): try: - values = self.Value(self.type, update) - except: - _log.error("Failed to encode '%s' with %s", cols, self.labels) - raise - else: - try: - values.update(update) - values = self.Value(self.type, values) - except: - _log.error("Failed to encode '%s' with %s", self.type, update) - raise - except: - _log.exception("Failed to wrap: %s", values) - raise + cols[L].append(V[L]) + except (IndexError, KeyError): + pass + # allow omit empty columns + for L in self.labels: + V = cols[L] + if len(V) == 0: + del cols[L] + update = {'labels': self.labels, 'value': cols} + if isinstance(values, list): + values = self.Value(self.type, update) + else: + values.update(update) + values = self.Value(self.type, values) else: # index or string V = self.type()