Commit 64300b42 authored by Anton Joubert's avatar Anton Joubert
Browse files

Merge branch 'add-class-prop-to-test-context' into 'develop'

Support class properties in (Multi)DeviceTestContext

See merge request !1012
parents caef839d c5aba3d4
Loading
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -13,6 +13,14 @@ revision
migration/index
```

## Unreleased

### Added

- {class}`~tango.test_context.DeviceTestContext` and
  {class}`~tango.test_context.MultiDeviceTestContext` now support setting class
  properties before the device server starts.

## What's new in PyTango 10.3.0?

Date: 2026-06-03
+54 −15
Original line number Diff line number Diff line
@@ -261,36 +261,42 @@ class MultiDeviceTestContext:
    Example usage::

        from tango import DeviceProxy
        from tango.server import Device, attribute
        from tango.server import Device, attribute, class_property, device_property
        from tango.test_context import MultiDeviceTestContext


        class Device1(Device):
            cls_prop1 = class_property(dtype=str)
            dev_prop1 = device_property(dtype=str)

            @attribute(dtype=int)
            def attr1(self):
                return 1


        class Device2(Device):
            @attribute(dtype=int)
            @attribute(dtype=int, memorized=True, hw_memorized=True)
            def attr2(self):
                dev1 = DeviceProxy("test/device/1")
                return dev1.attr1 * 2

            @attr2.write
            def attr2(self, value):
                print(f"Got new value {value}")


        devices_info = (
            {
                "class": Device1,
                "class_properties": {"cls_prop1": "cls_val"},
                "devices": [
                    {"name": "test/device/1"},
                    {"name": "test/device/1", "properties": {"dev_prop1": "dev_val"}},
                ],
            },
            {
                "class": Device2,
                "devices": [
                    {
                        "name": "test/device/2",
                    },
                    {"name": "test/device/2", "memorized": {"attr2": 123}},
                ],
            },
        )
@@ -316,6 +322,9 @@ class MultiDeviceTestContext:
          the second element being a :class:`~tango.DeviceImpl` or the
          name of some such class

      * "class_properties" (dict), with class property names as keys and
        class property values as the dict values.

      * "devices" which value is a sequence of dicts with the following keys:

        * "name" (str)
@@ -401,6 +410,9 @@ class MultiDeviceTestContext:
        This can be disabled by setting the `enable_test_context_tango_host_override`
        class/instance attribute to `False` before starting the test context.
        Added support for `root_atts` key to "devices" field in `devices_info`.

    .. versionadded:: 10.3.1
        added support for *class_properties* as a dict inside the *devices_info* parameter.
    """

    command = "{0} {1} -ORBendPoint giop:tcp:{2}:{3} -file={4}"
@@ -470,7 +482,8 @@ class MultiDeviceTestContext:
                raise ValueError("multiple entries in devices_info pointing to the same Tango class")
            tangoclass_list.append(tangoclass)
            # File
            self.append_db_file(server_name, instance_name, tangoclass, device_info["devices"])
            class_properties = device_info.get("class_properties")
            self.append_db_file(server_name, instance_name, tangoclass, device_info["devices"], class_properties)
            if device_cls:
                class_list.append((device_cls, device, tangoclass))
            else:
@@ -553,7 +566,7 @@ class MultiDeviceTestContext:
        # now we can connect to the device - success!
        self._startup_exception_queue.put(None)

    def append_db_file(self, server, instance, tangoclass, device_prop_info):
    def append_db_file(self, server, instance, tangoclass, device_prop_info, class_properties=None):
        """Generate a database file corresponding to the given arguments."""
        device_names = [info["name"] for info in device_prop_info]
        # Open the file
@@ -564,16 +577,15 @@ class MultiDeviceTestContext:
            f.write("\n")
        # Create database
        db = Database(self.db)
        # Write properties
        # Write class properties
        class_properties = dict(class_properties or {})
        self._patch_empty_string_properties(class_properties)
        db.put_class_property(tangoclass, class_properties)
        # Write device properties
        for info in device_prop_info:
            device_name = info["name"]
            properties = dict(info.get("properties", {}))
            # Patch the property dict to avoid a PyTango bug
            for key, value in properties.items():
                if is_non_str_seq(value):
                    properties[key] = [v if v != "" else " " for v in value]
                else:
                    properties[key] = value if value != "" else " "
            self._patch_empty_string_properties(properties)
            db.put_device_property(device_name, properties)

            root_atts = info.get("root_atts", {})
@@ -602,6 +614,18 @@ class MultiDeviceTestContext:
            ) from exc
        return validated_db

    def _patch_empty_string_properties(self, properties: dict):

        def ensure_not_empty_string(prop):
            return prop if prop != "" else " "

        # Patch the property dict to avoid a PyTango bug
        for key, value in properties.items():
            if is_non_str_seq(value):
                properties[key] = [ensure_not_empty_string(v) for v in value]
            else:
                properties[key] = ensure_not_empty_string(value)

    def delete_db(self):
        """delete temporary database file only if it was created by this class"""
        if self.handle is not None:
@@ -810,6 +834,16 @@ class DeviceTestContext(MultiDeviceTestContext):
      :class:`~tango.server.Device`.
    :type device_cls:
      :class:`~tango.DeviceClass`
    :param properties:
      The device properties as a dict, with names as the keys, and the corresponding values.
      These will be set in the database prior to the device server starting.
    :type properties:
      :py:obj:`dict`\\[:py:obj:`str`, :py:obj:`object`]
    :param class_properties:
      The class properties as a dict, with names as the keys, and the corresponding values.
      These will be set in the database prior to the device server starting.
    :type class_properties:
      :py:obj:`dict`\\[:py:obj:`str`, :py:obj:`object`]

    The rest of the parameters are described in
    :class:`~tango.test_context.MultiDeviceTestContext`.
@@ -821,6 +855,9 @@ class DeviceTestContext(MultiDeviceTestContext):

    .. versionadded:: 9.3.6
        added *green_mode* parameter.

    .. versionadded:: 10.3.1
        added *class_properties* parameter.
    """

    def __init__(
@@ -841,6 +878,7 @@ class DeviceTestContext(MultiDeviceTestContext):
        memorized=None,
        root_atts=None,
        green_mode=None,
        class_properties=None,
    ):
        # Argument
        if not server_name:
@@ -859,6 +897,7 @@ class DeviceTestContext(MultiDeviceTestContext):
        devices_info = (
            {
                "class": cls,
                "class_properties": class_properties,
                "devices": (
                    {
                        "name": device_name,
+65 −18
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ from tango.asyncio_executor import AsyncioExecutor
from tango.device_server import get_worker
from tango.gevent_executor import GeventExecutor
from tango.green import SynchronousExecutor
from tango.server import Device, attribute, command, device_property
from tango.server import Device, attribute, class_property, command, device_property
from tango.test_utils import (
    ClassicAPISimpleDeviceClass,
    ClassicAPISimpleDeviceImpl,
@@ -576,60 +576,107 @@ def test_multi_with_two_devices_with_properties(server_green_mode):
        class TestDevice1(Device):
            green_mode = server_green_mode

            prop1 = device_property(dtype=str)
            cls_prop1 = class_property(dtype=str)
            dev_prop1 = device_property(dtype=str)

            @command(dtype_out=str)
            async def get_prop1(self):
                return self.prop1
            async def get_cls_prop1(self):
                return self.cls_prop1

            @command(dtype_out=str)
            async def get_dev_prop1(self):
                return self.dev_prop1

        class TestDevice2(Device):
            green_mode = server_green_mode

            prop2 = device_property(dtype=int)
            cls_prop2 = class_property(dtype=int)
            dev_prop2 = device_property(dtype=int)

            @command(dtype_out=int)
            async def get_prop2(self):
                return self.prop2
            async def get_cls_prop2(self):
                return self.cls_prop2

            @command(dtype_out=int)
            async def get_dev_prop2(self):
                return self.dev_prop2

    else:

        class TestDevice1(Device):
            green_mode = server_green_mode

            prop1 = device_property(dtype=str)
            cls_prop1 = class_property(dtype=str)
            dev_prop1 = device_property(dtype=str)

            @command(dtype_out=str)
            def get_prop1(self):
                return self.prop1
            def get_cls_prop1(self):
                return self.cls_prop1

            @command(dtype_out=str)
            def get_dev_prop1(self):
                return self.dev_prop1

        class TestDevice2(Device):
            green_mode = server_green_mode

            prop2 = device_property(dtype=int)
            cls_prop2 = class_property(dtype=int)
            dev_prop2 = device_property(dtype=int)

            @command(dtype_out=int)
            def get_cls_prop2(self):
                return self.cls_prop2

            @command(dtype_out=int)
            def get_prop2(self):
                return self.prop2
            def get_dev_prop2(self):
                return self.dev_prop2

    devices_info = (
        {
            "class": TestDevice1,
            "devices": [{"name": "test/device1/1", "properties": {"prop1": "abcd"}}],
            "class_properties": {"cls_prop1": "cls_val"},
            "devices": [{"name": "test/device1/1", "properties": {"dev_prop1": "dev_val"}}],
        },
        {
            "class": TestDevice2,
            "devices": [{"name": "test/device2/2", "properties": {"prop2": 5555}}],
            "class_properties": {"cls_prop2": 4444},
            "devices": [{"name": "test/device2/2", "properties": {"dev_prop2": 5555}}],
        },
    )

    with MultiDeviceTestContext(devices_info) as context:
        proxy1 = context.get_device("test/device1/1")
        proxy2 = context.get_device("test/device2/2")
        assert proxy1.get_prop1() == "abcd"
        assert proxy2.get_prop2() == 5555
        assert proxy1.get_cls_prop1() == "cls_val"
        assert proxy2.get_cls_prop2() == 4444
        assert proxy1.get_dev_prop1() == "dev_val"
        assert proxy2.get_dev_prop2() == 5555


def test_single_with_properties():

    class TestDevice(Device):
        cls_prop1 = class_property(dtype=str)
        dev_prop1 = device_property(dtype=str)

        @command(dtype_out=str)
        def get_cls_prop1(self):
            return self.cls_prop1

        @command(dtype_out=str)
        def get_dev_prop1(self):
            return self.dev_prop1

    with DeviceTestContext(
        TestDevice,
        class_properties={"cls_prop1": "cls_val"},
        properties={"dev_prop1": "dev_val"},
    ) as proxy:
        assert proxy.get_cls_prop1() == "cls_val"
        assert proxy.get_dev_prop1() == "dev_val"


def test_multi_raises_on_invalid_file_database_properties():
def test_single_raises_on_invalid_file_database_properties():
    class TestDevice(Device):
        empty = device_property(dtype=(str,))