Loading doc/versions/news.md +8 −0 Original line number Diff line number Diff line Loading @@ -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 Loading tango/test_context.py +54 −15 Original line number Diff line number Diff line Loading @@ -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}}, ], }, ) Loading @@ -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) Loading Loading @@ -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}" Loading Loading @@ -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: Loading Loading @@ -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 Loading @@ -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", {}) Loading Loading @@ -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: Loading Loading @@ -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`. Loading @@ -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__( Loading @@ -841,6 +878,7 @@ class DeviceTestContext(MultiDeviceTestContext): memorized=None, root_atts=None, green_mode=None, class_properties=None, ): # Argument if not server_name: Loading @@ -859,6 +897,7 @@ class DeviceTestContext(MultiDeviceTestContext): devices_info = ( { "class": cls, "class_properties": class_properties, "devices": ( { "name": device_name, Loading tests/test_test_context.py +65 −18 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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,)) Loading Loading
doc/versions/news.md +8 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
tango/test_context.py +54 −15 Original line number Diff line number Diff line Loading @@ -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}}, ], }, ) Loading @@ -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) Loading Loading @@ -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}" Loading Loading @@ -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: Loading Loading @@ -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 Loading @@ -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", {}) Loading Loading @@ -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: Loading Loading @@ -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`. Loading @@ -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__( Loading @@ -841,6 +878,7 @@ class DeviceTestContext(MultiDeviceTestContext): memorized=None, root_atts=None, green_mode=None, class_properties=None, ): # Argument if not server_name: Loading @@ -859,6 +897,7 @@ class DeviceTestContext(MultiDeviceTestContext): devices_info = ( { "class": cls, "class_properties": class_properties, "devices": ( { "name": device_name, Loading
tests/test_test_context.py +65 −18 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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,)) Loading