# Empty spectrum and image attributes

Warning

This is the most significant API change. It could cause errors in existing Tango clients and devices.

Both server-side writing, and client-side reading could be affected. There are differences depending on an attribute’s data type and its access type (read/read-write). We go into detail below. The goal of the changes was to make usage more intuitive, and to provide a more consistent API.

## Writing - server side

When an empty sequence is written to a spectrum or image attribute of type `DevString` the write function used to receive a `None` value, but now it will receive an empty `list`. For other types, the behaviour is unchanged - they were already receiving an empty `numpy.ndarray`.

Example device:

    from tango import AttrWriteType
    from tango.server import Device, attribute

    class Test(Device):
        str1d = attribute(dtype=(str,), max_dim_x=4, access=AttrWriteType.WRITE)
        int1d = attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.WRITE)
        str2d = attribute(dtype=((str,),), max_dim_x=4, max_dim_y=4, access=AttrWriteType.WRITE)
        int2d = attribute(dtype=((int,),), max_dim_x=4, max_dim_y=4, access=AttrWriteType.WRITE)

        def write_str1d(self, values):
            print(f"Writing str1d: values={values!r}, type={type(values)}")

        def write_int1d(self, values):
            print(f"Writing int1d: values={values!r}, type={type(values)}")

        def write_str2d(self, values):
            print(f"Writing str2d: values={values!r}, type={type(values)}")

        def write_int2d(self, values):
            print(f"Writing int2d: values={values!r}, type={type(values)}")

If a client writes empty data to the device:

    >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")
    >>> dp.str1d = []
    >>> dp.int1d = []
    >>> dp.str2d = [[]]
    >>> dp.int2d = [[]]

Old: The output from a v9.3.x PyTango device would be:

    Writing str1d: values=None, type=<class 'NoneType'>
    Writing int1d: values=array([], dtype=int64), type=<class 'numpy.ndarray'>
    Writing str2d: values=None, type=<class 'NoneType'>
    Writing int2d: values=array([], shape=(1, 0), dtype=int64), type=<class 'numpy.ndarray'>

New: The output from a v9.4.x PyTango device would be:

    Writing str1d: values=[], type=<class 'list'>
    Writing int1d: values=array([], dtype=int64), type=<class 'numpy.ndarray'>
    Writing str2d: values=[], type=<class 'list'>
    Writing int2d: values=array([], shape=(1, 0), dtype=int64), type=<class 'numpy.ndarray'>

Notice the change in values received for the `str1d` and `str2d` attributes. If your existing devices have special handling for `None`, then they may need to change.

## Reading - client side

When clients read from spectrum and image attributes with empty values, clients will now receive an empty sequence instead of a `None` value. For `DevString` and `DevEnum` types, the sequence is a `tuple`, while other types get a `numpy.ndarray` by default.

There is a subtle inconsistency in PyTango 9.3.x - empty read-only spectrum and image attributes always returned `None` values, but read-write spectrum attributes can return empty sequences instead of `None` values. It depends on the set point (written value) stored for the attribute - if it is non-empty, then the client gets an empty sequence. This is shown in the examples below. From PyTango 9.4.x, the behaviour is more consistent.

Warning

Reading attributes of any type can still produce a `None` value if the quality is `ATTR_INVALID`. Client-side code should always be prepared for this. This behaviour is unchanged in PyTango 9.4.x (an exception being the fix for enumerated types using the high-level API, so that they also return `None`).

Note

It is possible to get the data returned in other container types using the `extract_as` argument with `tango.DeviceProxy.read_attribute()`.

This change affects values received via both the high-level API, and the low-level method it uses, `tango.DeviceProxy.read_attribute()`. It also affects all related read methods: `tango.DeviceProxy.read_attributes()`, `tango.DeviceProxy.read_attribute_asynch()`, `tango.DeviceProxy.read_attribute_reply()`, `tango.DeviceProxy.read_attributes_asynch()`, `tango.DeviceProxy.read_attributes_reply()`.

The read attribute methods return data via `tango.DeviceAttribute` objects. These include a `value` field for the read value, and a `w_value` field for the last set point (i.e., last value written). Both of these fields are affected by this change.

Example device:

    from enum import IntEnum
    from tango import AttrWriteType
    from tango.server import Device, attribute

    class AnEnum(IntEnum):
       A = 0
       B = 1

    class Test(Device):
        @attribute(dtype=(str,), max_dim_x=4)
        def str1d(self):
            return []

        @attribute(dtype=(AnEnum,), max_dim_x=4)
        def enum1d(self):
            return []

        @attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.READ)
        def int1d(self):
            return []

        @attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.READ_WRITE)
        def int1d_rw(self):
            return []

        @int1d_rw.write
        def int1d_rw(self, values):
            print(f"Writing int1d_rw: values={values!r}, type={type(values)}")

        @attribute(dtype=((str,),), max_dim_x=4, max_dim_y=4)
        def str2d(self):
            return [[]]

        @attribute(dtype=((int,),), max_dim_x=4, max_dim_y=4)
        def int2d(self):
            return [[]]

### High-level API reads

Old: A PyTango 9.3.x client reads the empty data from the device using the high-level API:

    >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

    >>> value = dp.str1d
    >>> value, type(value)
    (None, <class 'NoneType'>)

    >>> value = dp.enum1d  # broken in PyTango 9.3.x
    Traceback: ... ValueError: None is not a valid enum1d

    >>> value = dp.int1d  # read-only attribute
    >>> value, type(value)
    (None, <class 'NoneType'>)

    >>> value = dp.int1d_rw  # read-write attribute (default set point is [0])
    >>> value, type(value)
    (array([], dtype=int64), <class 'numpy.ndarray'>)
    >>> dp.int1d_rw = []  # write empty value (set point changed to empty)
    >>> value = dp.int1d_rw  # read again, after set point changed
    >>> value, type(value)
    (None, <class 'NoneType'>)

    >>> value = dp.str2d
    >>> value, type(value)
    (None, <class 'NoneType'>)

    >>> value = dp.int2d
    >>> value, type(value)
    (None, <class 'NoneType'>)

In the above example, notice that high-level API reads of enumerated spectrum types fail under PyTango 9.3.x. Also see the difference in behaviour between read-only attributes like `int1d` and read-write attributes like `int1d_rw`. Read-write spectrum attributes behave inconsistently with empty data prior to PyTango 9.4.x.

New: A PyTango 9.4.x client reads the empty data from the device using the high-level API (device server has been restarted after previous example):

    >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

    >>> value = dp.str1d
    >>> value, type(value)
    ((), <class 'tuple'>)

    >>> value = dp.enum1d
    >>> value, type(value)
    ((), <class 'tuple'>)

    >>> value = dp.int1d  # read-only attribute
    >>> value, type(value)
    (array([], dtype=int64), <class 'numpy.ndarray'>)

    >>> value = dp.int1d_rw  # read-write attribute (default set point is [0])
    >>> value, type(value)
    (array([], dtype=int64), <class 'numpy.ndarray'>)
    >>> dp.int1d_rw = []  # write empty value (set point changed to empty)
    >>> value = dp.int1d_rw  # read again, after set point changed
    >>> value, type(value)
    (array([], dtype=int64), <class 'numpy.ndarray'>)

    >>> value = dp.str2d
    >>> value, type(value)
    ((), <class 'tuple'>)

    >>> value = dp.int2d
    >>> value, type(value)
    (array([], shape=(1, 0), dtype=int64), <class 'numpy.ndarray'>)

### Low-level API reads

In these examples, focus on the `value` field (the value read back), which changes as above, and the `type` field. Using PyTango 9.3.x, the `type` is number 100, which indicates an unknown type, while with PyTango 9.4.0, the type stays correct even when the value is empty. The change in `type` is something updated in cppTango 9.4.1.

> Old: A PyTango 9.3.x client reads the empty data from the device using the low-level API:

>     >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>     >>> print(dp.read_attribute("str1d"))
>     DeviceAttribute[
>     data_format = tango._tango.AttrDataFormat.SPECTRUM
>           dim_x = 0
>           dim_y = 0
>      has_failed = False
>        is_empty = True
>            name = 'str1d'
>         nb_read = 0
>      nb_written = 0
>         quality = tango._tango.AttrQuality.ATTR_VALID
>     r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>            time = TimeVal(tv_nsec = 0, tv_sec = 1676068470, tv_usec = 650091)
>            type = tango._tango.CmdArgType(100)
>           value = None
>         w_dim_x = 0
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>         w_value = None]

>     >>> print(dp.read_attribute("int1d"))
>     DeviceAttribute[
>     data_format = tango._tango.AttrDataFormat.SPECTRUM
>           dim_x = 0
>           dim_y = 0
>      has_failed = False
>        is_empty = False
>            name = 'int1d'
>         nb_read = 0
>      nb_written = 1
>         quality = tango._tango.AttrQuality.ATTR_VALID
>     r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>            time = TimeVal(tv_nsec = 0, tv_sec = 1676068478, tv_usec = 597279)
>            type = tango._tango.CmdArgType.DevLong64
>           value = array([], dtype=int64)
>         w_dim_x = 1
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 1, dim_y = 0)
>         w_value = array([0])]

>     >>> print(dp.read_attribute("str2d"))
>     DeviceAttribute[
>     data_format = tango._tango.AttrDataFormat.IMAGE
>           dim_x = 0
>           dim_y = 1
>      has_failed = False
>        is_empty = True
>            name = 'str2d'
>         nb_read = 0
>      nb_written = 0
>         quality = tango._tango.AttrQuality.ATTR_VALID
>     r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
>            time = TimeVal(tv_nsec = 0, tv_sec = 1676068484, tv_usec = 896408)
>            type = tango._tango.CmdArgType(100)
>           value = None
>         w_dim_x = 0
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>         w_value = None]

>     >>> print(dp.read_attribute("int2d"))
>     DeviceAttribute[
>     data_format = tango._tango.AttrDataFormat.IMAGE
>           dim_x = 0
>           dim_y = 1
>      has_failed = False
>        is_empty = True
>            name = 'int2d'
>         nb_read = 0
>      nb_written = 0
>         quality = tango._tango.AttrQuality.ATTR_VALID
>     r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
>            time = TimeVal(tv_nsec = 0, tv_sec = 1676068489, tv_usec = 330193)
>            type = tango._tango.CmdArgType(100)
>           value = None
>         w_dim_x = 0
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>         w_value = None]

New: A PyTango 9.4.x client reads the empty data from the device using the low-level API:

    >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

    >>> print(dp.read_attribute("str1d"))
    DeviceAttribute[
    data_format = tango._tango.AttrDataFormat.SPECTRUM
          dim_x = 0
          dim_y = 0
     has_failed = False
       is_empty = True
           name = 'str1d'
        nb_read = 0
     nb_written = 0
        quality = tango._tango.AttrQuality.ATTR_VALID
    r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
           time = TimeVal(tv_nsec = 0, tv_sec = 1676068550, tv_usec = 333749)
           type = tango._tango.CmdArgType.DevString
          value = ()
        w_dim_x = 0
        w_dim_y = 0
    w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
        w_value = ()]

    >>> print(dp.read_attribute("int1d"))
    DeviceAttribute[
    data_format = tango._tango.AttrDataFormat.SPECTRUM
          dim_x = 0
          dim_y = 0
     has_failed = False
       is_empty = False
           name = 'int1d'
        nb_read = 0
     nb_written = 1
        quality = tango._tango.AttrQuality.ATTR_VALID
    r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
           time = TimeVal(tv_nsec = 0, tv_sec = 1676068554, tv_usec = 243413)
           type = tango._tango.CmdArgType.DevLong64
          value = array([], dtype=int64)
        w_dim_x = 1
        w_dim_y = 0
    w_dimension = AttributeDimension(dim_x = 1, dim_y = 0)
        w_value = array([0])]

    >>> print(dp.read_attribute("str2d"))
    DeviceAttribute[
    data_format = tango._tango.AttrDataFormat.IMAGE
          dim_x = 0
          dim_y = 1
     has_failed = False
       is_empty = True
           name = 'str2d'
        nb_read = 0
     nb_written = 0
        quality = tango._tango.AttrQuality.ATTR_VALID
    r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
           time = TimeVal(tv_nsec = 0, tv_sec = 1676068558, tv_usec = 191433)
           type = tango._tango.CmdArgType.DevString
          value = ()
        w_dim_x = 0
        w_dim_y = 0
    w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
        w_value = ()]

    >>> print(dp.read_attribute("int2d"))
    DeviceAttribute[
    data_format = tango._tango.AttrDataFormat.IMAGE
          dim_x = 0
          dim_y = 1
     has_failed = False
       is_empty = True
           name = 'int2d'
        nb_read = 0
     nb_written = 0
        quality = tango._tango.AttrQuality.ATTR_VALID
    r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
           time = TimeVal(tv_nsec = 0, tv_sec = 1676068562, tv_usec = 50107)
           type = tango._tango.CmdArgType.DevLong64
          value = array([], shape=(1, 0), dtype=int64)
        w_dim_x = 0
        w_dim_y = 0
    w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
        w_value = array([], shape=(0, 0), dtype=int64)]

### Low-level API reads of set point (write value)

In these examples, focus on the `w_value` field which is the set point, or last written value.

> Old: A PyTango 9.3.x client changes the set point and reads using the low-level API:

>     >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>     >>> dp.int1d_rw = [1, 2]
>     >>> print(dp.read_attribute("int1d_rw"))
>     DeviceAttribute[
>         ...
>            type = tango._tango.CmdArgType.DevLong64
>         w_dim_x = 2
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 2, dim_y = 0)
>         w_value = array([1, 2])]

>     >>> dp.int1d_rw = []
>     >>> print(dp.read_attribute("int1d_rw"))
>     DeviceAttribute[
>         ...
>            type = tango._tango.CmdArgType(100)
>         w_dim_x = 0
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>         w_value = None]

> New: A PyTango 9.4.x client changes the set point and reads using the low-level API:

>     >>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>     >>> dp.int1d_rw = [1, 2]
>     >>> print(dp.read_attribute("int1d_rw"))
>     DeviceAttribute[
>         ...
>            type = tango._tango.CmdArgType.DevLong64
>         w_dim_x = 2
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 2, dim_y = 0)
>         w_value = array([1, 2])]

>     >>> dp.int1d_rw = []
>     >>> print(dp.read_attribute("int1d_rw"))
>     DeviceAttribute[
>         ...
>            type = tango._tango.CmdArgType.DevLong64
>         w_dim_x = 0
>         w_dim_y = 0
>     w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
>         w_value = array([], dtype=int64)]