From 15f065b8b78e521a7dca7bbb15d73e8c5baf5ef9 Mon Sep 17 00:00:00 2001
From: Bernd Deichmann <68051229+deichmab-draeger@users.noreply.github.com>
Date: Tue, 5 Sep 2023 11:33:40 +0200
Subject: [PATCH] Fix/v1 ext extension (#250)
## Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
## Checklist:
- [x] I have updated the [changelog](../CHANGELOG.md) accordingly.
- [x] I have added tests to cover my changes.
---
CHANGELOG.md | 1 +
src/sdc11073/mdib/containerproperties.py | 97 +++++++--
src/sdc11073/mdib/descriptorcontainers.py | 144 +++++++------
tests/test_descriptorcontainers.py | 12 +-
tests/test_pmtypes.py | 243 +++++++++++++++++++++-
5 files changed, 393 insertions(+), 104 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 224f6993..930b1e9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- fixed a bug where coping a xml node would not work if namespace have been defined multiple time [#232](https://github.com/Draegerwerk/sdc11073/issues/232)
- getContextState sets correct BindingVersion [#168](https://github.com/Draegerwerk/sdc11073/issues/168).
+- comparison of extensions would fail, ExtensionLocalValue type added
### Changed
diff --git a/src/sdc11073/mdib/containerproperties.py b/src/sdc11073/mdib/containerproperties.py
index 981198ae..f8ccc290 100644
--- a/src/sdc11073/mdib/containerproperties.py
+++ b/src/sdc11073/mdib/containerproperties.py
@@ -13,6 +13,7 @@
from sdc11073 import isoduration
from sdc11073.dataconverters import TimestampConverter, DecimalConverter, IntegerConverter, BooleanConverter, \
DurationConverter, NullConverter
+from sdc11073.xmlparsing import copy_node_wo_parent
class ElementNotFoundException(Exception):
@@ -456,8 +457,48 @@ def updateXMLValue(self, instance, node):
subNode.text = value
+def _compare_extension(left: etree_.ElementBase, right: etree_.ElementBase) -> bool:
+ # xml comparison
+ try:
+ if left.tag != right.tag: # compare expanded names
+ return False
+ if dict(left.attrib) != dict(right.attrib): # unclear how lxml _Attrib compares
+ return False
+ except AttributeError: # right side is not an Element type because expected attributes are missing
+ return False
+
+ # ignore comments
+ left_children = [child for child in left if not isinstance(child, etree_._Comment)]
+ right_children = [child for child in right if not isinstance(child, etree_._Comment)]
+
+ if len(left_children) != len(right_children): # compare children count
+ return False
+ if len(left_children) == 0 and len(right_children) == 0:
+ if left.text != right.text: # mixed content is not allowed. only compare text if there are no children
+ return False
+
+ return all(map(_compare_extension, left_children, right_children)) # compare children but keep order
+
+
+class ExtensionLocalValue(list):
+
+ compare_method = _compare_extension
+ """may be overwritten by user if a custom comparison behaviour is required"""
+
+ def __eq__(self, other):
+ try:
+ if len(self) != len(other):
+ return False
+ except TypeError: # len of other cannot be determined
+ return False
+ return all(self.__class__.compare_method(left, right) for left, right in zip(self, other))
+
+
class ExtensionNodeProperty(_PropertyBase):
- """ Represents an ext:Extension Element that contains xml tree of any kind."""
+ """Represents an ext:Extension Element that contains 0...n child elements of any kind.
+
+ The python representation is an ExtensionLocalValue with list of elements.
+ """
def __init__(self, subElementNames=None, defaultPyValue=None):
if subElementNames is None:
@@ -465,36 +506,48 @@ def __init__(self, subElementNames=None, defaultPyValue=None):
else:
subElementNames.append(namespaces.extTag('Extension'))
attrname = '_ext_ext'
- super(ExtensionNodeProperty, self).__init__(attrname, subElementNames, defaultPyValue)
+ super().__init__(attrname, subElementNames, defaultPyValue)
self._converter = None
+ def __set__(self, instance, pyValue):
+ if not isinstance(pyValue, ExtensionLocalValue):
+ pyValue = ExtensionLocalValue(pyValue)
+ super().__set__(instance, pyValue)
+
+ def __get__(self, instance, owner):
+ """Return a python value, uses the locally stored value."""
+ if instance is None: # if called via class
+ return self
+ try:
+ value = getattr(instance, self._localVarName)
+ except AttributeError:
+ value = None
+ if value is None:
+ value = _PropertyValue(None, ExtensionLocalValue())
+ setattr(instance, self._localVarName, value)
+ return value.py_value
+
def getPyValueFromNode(self, node):
+ """Read value from node."""
try:
- subNode = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=False)
- return _PropertyValue(None, subNode) # subNode is the ext:Extension node
+ extension_node = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=False)
except ElementNotFoundException:
- return None
+ return _PropertyValue(None, ExtensionLocalValue())
+ return _PropertyValue(extension_node, ExtensionLocalValue(extension_node[:]))
def updateXMLValue(self, instance, node):
+ """Write value to node.
+
+ The Extension Element is only added if there is at least one element available in local list.
+ """
try:
property_value = getattr(instance, self._localVarName)
- except AttributeError: # set to None (it is in the responsibility of the called method to do the right thing)
- property_value = None
- if property_value is None:
- try:
- parentNode = self._getElementbyChildNamesList(node, self._subElementNames[:-1],
- createMissingNodes=False)
- except ElementNotFoundException:
- return
- subNode = parentNode.find(self._subElementNames[-1])
- if subNode is not None:
- parentNode.remove(subNode)
- else:
- subNode = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=True)
-
- del subNode[:] # delete all children first
- if property_value.py_value is not None:
- subNode.extend([copy.copy(n) for n in property_value.py_value])
+ except AttributeError:
+ return # nothing to add
+ if property_value is None or len(property_value.py_value) == 0:
+ return # nothing to add
+ sub_node = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=True)
+ sub_node.extend(copy_node_wo_parent(x) for x in property_value.py_value)
class SubElementProperty(_PropertyBase):
diff --git a/src/sdc11073/mdib/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py
index 5500cd8c..8210a3a9 100644
--- a/src/sdc11073/mdib/descriptorcontainers.py
+++ b/src/sdc11073/mdib/descriptorcontainers.py
@@ -1,14 +1,15 @@
import inspect
from collections import defaultdict
+
from lxml import etree as etree_
+
+from . import containerproperties as cp
from .containerbase import ContainerBase
-from .. import observableproperties as properties
-from ..namespaces import domTag, extTag, siTag, msgTag
-from ..namespaces import Prefix_Namespace as Prefix
from .. import msgtypes
-
+from .. import observableproperties as properties
from .. import pmtypes
-from . import containerproperties as cp
+from ..namespaces import Prefix_Namespace as Prefix
+from ..namespaces import domTag, extTag, siTag, msgTag
class AbstractDescriptorContainer(ContainerBase):
@@ -34,7 +35,6 @@ class AbstractDescriptorContainer(ContainerBase):
isAlertConditionDescriptor = False
isContextDescriptor = False
-
node = properties.ObservableProperty() # the elementtree node
Handle = cp.NodeAttributeProperty('Handle')
@@ -74,31 +74,17 @@ def codeId(self):
def codingSystem(self):
return self.Type.coding.codingSystem if self.Type is not None else None # pylint:disable=no-member
- @property
- def retrievability(self) -> [msgtypes.Retrievability, None]:
- """look for msgTag('Retrievability') in ext:Extension node"""
- if self.ext_Extension is None:
- return None
- retrievability_tag = msgTag('Retrievability')
- for elem in self.ext_Extension:
- if elem.tag == retrievability_tag:
- return msgtypes.Retrievability.fromNode(elem)
- return None
-
- @retrievability.setter
- def retrievability(self, retrievability_instance: msgtypes.Retrievability):
- """sets msgTag('Retrievability') child node of ext:Extension"""
- retrievability_tag = msgTag('Retrievability')
- if self.ext_Extension is None:
- self.ext_Extension = etree_.Element(extTag('Extension'))
- else:
- # delete current retrievability info if it exists
- for elem in self.ext_Extension:
- if elem.tag == retrievability_tag:
- self.ext_Extension.remove(elem)
- break
- node = retrievability_instance.asEtreeNode(retrievability_tag, nsmap=None)
- self.ext_Extension.append(node)
+ def get_retrievability(self):
+ """Return all retrievability data from Extension."""
+ return [msgtypes.Retrievability.fromNode(x) for x in self.ext_Extension if x.tag == msgTag('Retrievability')]
+
+ def set_retrievability(self, retrievabilities):
+ """Replace all retrievability elements with provided ones in Extension.
+
+ :param retrievabilities: Iterable of msgtypes.Retrievability."""
+ for x in [x for x in self.ext_Extension if x.tag == msgTag('Retrievability')]:
+ self.ext_Extension.remove(x)
+ self.ext_Extension.extend([r.asEtreeNode(msgTag('Retrievability'), {}) for r in retrievabilities])
def incrementDescriptorVersion(self):
if self.DescriptorVersion is None:
@@ -125,8 +111,6 @@ def mkNode(self, tag=None, setXsiType=False):
def updateNode(self, setXsiType=False):
return
-# """ deprecated, remove!"""
-# self.node = self.mkNode(setXsiType=setXsiType)
def connectChildContainers(self, node, containers):
ret = self._connectChildNodes(node, containers)
@@ -206,7 +190,7 @@ def mkDescriptorNode(self, setXsiType=True, tag=None):
"""
myTag = tag or self.nodeName
node = etree_.Element(myTag, attrib={'Handle': self.handle},
- nsmap = self.nsmapper.partialMap(Prefix.PM, Prefix.XSI))
+ nsmap=self.nsmapper.partialMap(Prefix.PM, Prefix.XSI))
self._updateNode(node, setXsiType)
order = self._sortedChildNames()
self._sortChildNodes(node, order)
@@ -243,7 +227,7 @@ def __repr__(self):
@classmethod
def fromNode(cls, nsmapper, node, parentHandle):
obj = cls(nsmapper,
- nodeName=None, # will be determined in constructor from node value
+ nodeName=None, # will be determined in constructor from node value
handle=None, # will be determined in constructor from node value
parentHandle=parentHandle,
node=node)
@@ -258,7 +242,6 @@ class AbstractDeviceComponentDescriptorContainer(AbstractDescriptorContainer):
_childNodeNames = (domTag('ProductionSpecification'),)
-
class AbstractComplexDeviceComponentDescriptorContainer(AbstractDeviceComponentDescriptorContainer):
_childNodeNames = (domTag('AlertSystem'),
domTag('Sco'),)
@@ -354,7 +337,7 @@ class AbstractMetricDescriptorContainer(AbstractDescriptorContainer):
isMetricDescriptor = True
Unit = cp.SubElementProperty([domTag('Unit')], valueClass=pmtypes.CodedValue)
BodySite = cp.SubElementListProperty([domTag('BodySite')], cls=pmtypes.CodedValue)
- Relation = cp.SubElementListProperty([domTag('Relation')], cls=pmtypes.Relation) # o...n
+ Relation = cp.SubElementListProperty([domTag('Relation')], cls=pmtypes.Relation) # o...n
MetricCategory = cp.NodeAttributeProperty('MetricCategory', defaultPyValue='Unspec') # required
DerivationMethod = cp.NodeAttributeProperty('DerivationMethod') # optional
# There is an implied value defined, but it is complicated, therefore here not implemented:
@@ -367,8 +350,9 @@ class AbstractMetricDescriptorContainer(AbstractDescriptorContainer):
DeterminationPeriod = cp.DurationAttributeProperty('DeterminationPeriod') # optional, xsd:duration
LifeTimePeriod = cp.DurationAttributeProperty('LifeTimePeriod') # optional, xsd:duration
ActivationDuration = cp.DurationAttributeProperty('ActivationDuration') # optional, xsd:duration
- _props = ('Unit', 'BodySite', 'Relation', 'MetricCategory', 'DerivationMethod', 'MetricAvailability', 'MaxMeasurementTime',
- 'MaxDelayTime', 'DeterminationPeriod', 'LifeTimePeriod', 'ActivationDuration')
+ _props = (
+ 'Unit', 'BodySite', 'Relation', 'MetricCategory', 'DerivationMethod', 'MetricAvailability',
+ 'MaxMeasurementTime', 'MaxDelayTime', 'DeterminationPeriod', 'LifeTimePeriod', 'ActivationDuration')
_childNodeNames = (domTag('Unit'),
domTag('BodySite'),
domTag('Relation'),
@@ -433,12 +417,14 @@ class AbstractOperationDescriptorContainer(AbstractDescriptorContainer):
isOperationalDescriptor = True
OperationTarget = cp.NodeAttributeProperty('OperationTarget')
SafetyReq = cp.SubElementProperty([extTag('Extension'), siTag('SafetyReq')], valueClass=pmtypes.T_SafetyReq)
- InvocationEffectiveTimeout = cp.DurationAttributeProperty('InvocationEffectiveTimeout') # optional xsd:duration
- MaxTimeToFinish = cp.DurationAttributeProperty('MaxTimeToFinish') # optional xsd:duration
- Retriggerable = cp.BooleanAttributeProperty('Retriggerable', impliedPyValue=True) # optional
- # AccessLevel can be: Usr (User), CSUsr (Clinical Super User), RO (Responsible Organization), SP (Service Personnel), Oth (Other)
+ InvocationEffectiveTimeout = cp.DurationAttributeProperty('InvocationEffectiveTimeout') # optional xsd:duration
+ MaxTimeToFinish = cp.DurationAttributeProperty('MaxTimeToFinish') # optional xsd:duration
+ Retriggerable = cp.BooleanAttributeProperty('Retriggerable', impliedPyValue=True) # optional
+ # AccessLevel can be: Usr (User), CSUsr (Clinical Super User), RO (Responsible Organization),
+ # SP (Service Personnel), Oth (Other)
AccessLevel = cp.NodeAttributeProperty('AccessLevel', impliedPyValue='Usr')
- _props = ('OperationTarget', 'SafetyReq', 'InvocationEffectiveTimeout', 'MaxTimeToFinish', 'Retriggerable', 'AccessLevel')
+ _props = (
+ 'OperationTarget', 'SafetyReq', 'InvocationEffectiveTimeout', 'MaxTimeToFinish', 'Retriggerable', 'AccessLevel')
class SetValueOperationDescriptorContainer(AbstractOperationDescriptorContainer):
@@ -446,7 +432,6 @@ class SetValueOperationDescriptorContainer(AbstractOperationDescriptorContainer)
STATE_QNAME = domTag('SetValueOperationState')
-
class SetStringOperationDescriptorContainer(AbstractOperationDescriptorContainer):
NODETYPE = domTag('SetStringOperationDescriptor')
STATE_QNAME = domTag('SetStringOperationState')
@@ -455,7 +440,7 @@ class SetStringOperationDescriptorContainer(AbstractOperationDescriptorContainer
class AbstractSetStateOperationDescriptor(AbstractOperationDescriptorContainer):
- ModifiableData = cp.SubElementListProperty([domTag('ModifiableData')], cls = pmtypes.ElementWithTextOnly)
+ ModifiableData = cp.SubElementListProperty([domTag('ModifiableData')], cls=pmtypes.ElementWithTextOnly)
_props = ('ModifiableData',)
_childNodeNames = (domTag('ModifiableData'),)
@@ -483,14 +468,14 @@ class SetAlertStateOperationDescriptorContainer(AbstractSetStateOperationDescrip
class ActivateOperationDescriptorContainer(AbstractSetStateOperationDescriptor):
NODETYPE = domTag('ActivateOperationDescriptor')
STATE_QNAME = domTag('ActivateOperationState')
- Argument = cp.SubElementListProperty([domTag('Argument')], cls = pmtypes.Argument)
- _props = ('Argument', )
- _childNodeNames = (domTag('Argument'), )
+ Argument = cp.SubElementListProperty([domTag('Argument')], cls=pmtypes.Argument)
+ _props = ('Argument',)
+ _childNodeNames = (domTag('Argument'),)
class AbstractAlertDescriptorContainer(AbstractDescriptorContainer):
"""AbstractAlertDescriptor acts as a base class for all alert descriptors that contain static alert meta information.
- This class has nor specific data."""
+ This class has nor specific data."""
isAlertDescriptor = True
@@ -516,14 +501,20 @@ class AlertConditionDescriptorContainer(AbstractAlertDescriptorContainer):
isAlertConditionDescriptor = True
NODETYPE = domTag('AlertConditionDescriptor')
STATE_QNAME = domTag('AlertConditionState')
- Source = cp.SubElementListProperty([domTag('Source')], cls = pmtypes.ElementWithTextOnly) # a list of 0...n pm:HandleRef elements
- CauseInfo = cp.SubElementListProperty([domTag('CauseInfo')], cls = pmtypes.CauseInfo) # a list of 0...n pm:CauseInfo elements
- Kind = cp.NodeAttributeProperty('Kind', defaultPyValue=pmtypes.AlertConditionKind.OTHER) # required, type=AlertConditionKind
- Priority = cp.NodeAttributeProperty('Priority', defaultPyValue=pmtypes.AlertConditionPriority.NONE) # required, type= AlertConditionPriority
- DefaultConditionGenerationDelay = cp.DurationAttributeProperty('DefaultConditionGenerationDelay', impliedPyValue=0) # optional
- CanEscalate = cp.NodeAttributeProperty('CanEscalate') # AlertConditionPriority, without 'None'
- CanDeescalate = cp.NodeAttributeProperty('CanDeescalate') # AlertConditionPriority, without 'Hi'
- _props = ('Source', 'CauseInfo', 'Kind', 'Priority', 'DefaultConditionGenerationDelay', 'CanEscalate', 'CanDeescalate')
+ Source = cp.SubElementListProperty([domTag('Source')], # a list of 0...n pm:HandleRef elements
+ cls=pmtypes.ElementWithTextOnly)
+ CauseInfo = cp.SubElementListProperty([domTag('CauseInfo')], # a list of 0...n pm:CauseInfo elements
+ cls=pmtypes.CauseInfo)
+ Kind = cp.NodeAttributeProperty('Kind', # required, type=AlertConditionKind
+ defaultPyValue=pmtypes.AlertConditionKind.OTHER)
+ Priority = cp.NodeAttributeProperty('Priority', # required, type= AlertConditionPriority
+ defaultPyValue=pmtypes.AlertConditionPriority.NONE)
+ DefaultConditionGenerationDelay = cp.DurationAttributeProperty('DefaultConditionGenerationDelay',
+ impliedPyValue=0) # optional
+ CanEscalate = cp.NodeAttributeProperty('CanEscalate') # AlertConditionPriority, without 'None'
+ CanDeescalate = cp.NodeAttributeProperty('CanDeescalate') # AlertConditionPriority, without 'Hi'
+ _props = (
+ 'Source', 'CauseInfo', 'Kind', 'Priority', 'DefaultConditionGenerationDelay', 'CanEscalate', 'CanDeescalate')
_childNodeNames = (domTag('Source'),
domTag('CauseInfo'))
@@ -534,31 +525,33 @@ class LimitAlertConditionDescriptorContainer(AlertConditionDescriptorContainer):
MaxLimits = cp.SubElementProperty([domTag('MaxLimits')], valueClass=pmtypes.Range)
AutoLimitSupported = cp.BooleanAttributeProperty('AutoLimitSupported', impliedPyValue=False)
_props = ('MaxLimits', 'AutoLimitSupported',)
- _childNodeNames = (domTag('MaxLimits'),
- )
+ _childNodeNames = (domTag('MaxLimits'),)
class AlertSignalDescriptorContainer(AbstractAlertDescriptorContainer):
isAlertSignalDescriptor = True
NODETYPE = domTag('AlertSignalDescriptor')
STATE_QNAME = domTag('AlertSignalState')
- ConditionSignaled = cp.NodeAttributeProperty('ConditionSignaled') # required, a HandleRef
- Manifestation = cp.NodeAttributeProperty('Manifestation') # required, an AlertSignalManifestation ('Aud', 'Vis', 'Tan', 'Oth')
- Latching = cp.BooleanAttributeProperty('Latching') # required
- DefaultSignalGenerationDelay = cp.DurationAttributeProperty('DefaultSignalGenerationDelay', impliedPyValue=0)# optional, defaults to PT0s ( 0 seconds)
- SignalDelegationSupported = cp.BooleanAttributeProperty('SignalDelegationSupported', impliedPyValue=False) # optional, defaults to false
- AcknowledgementSupported = cp.BooleanAttributeProperty('AcknowledgementSupported', impliedPyValue=False) # optional, defaults to false
- AcknowledgeTimeout = cp.DurationAttributeProperty('AcknowledgeTimeout') # optional
- _props = ('ConditionSignaled', 'Manifestation', 'Latching', 'DefaultSignalGenerationDelay', 'SignalDelegationSupported',
- 'AcknowledgementSupported', 'AcknowledgeTimeout')
-
-
-
+ ConditionSignaled = cp.NodeAttributeProperty('ConditionSignaled') # required, a HandleRef
+ Manifestation = cp.NodeAttributeProperty(
+ 'Manifestation') # required, an AlertSignalManifestation ('Aud', 'Vis', 'Tan', 'Oth')
+ Latching = cp.BooleanAttributeProperty('Latching') # required
+ DefaultSignalGenerationDelay = cp.DurationAttributeProperty('DefaultSignalGenerationDelay',
+ impliedPyValue=0) # optional, defaults to 0 seconds
+ SignalDelegationSupported = cp.BooleanAttributeProperty('SignalDelegationSupported',
+ impliedPyValue=False) # optional, defaults to false
+ AcknowledgementSupported = cp.BooleanAttributeProperty('AcknowledgementSupported',
+ impliedPyValue=False) # optional, defaults to false
+ AcknowledgeTimeout = cp.DurationAttributeProperty('AcknowledgeTimeout') # optional
+ _props = ('ConditionSignaled', 'Manifestation', 'Latching', 'DefaultSignalGenerationDelay',
+ 'SignalDelegationSupported', 'AcknowledgementSupported', 'AcknowledgeTimeout')
+
+
class SystemContextDescriptorContainer(AbstractDeviceComponentDescriptorContainer):
isSystemContextDescriptor = True
NODETYPE = domTag('SystemContextDescriptor')
STATE_QNAME = domTag('SystemContextState')
- #Child elements are not modeled here
+ # Child elements are not modeled here
_childNodeNames = (domTag('PatientContext'),
domTag('LocationContext'),
domTag('EnsembleContext'),
@@ -591,10 +584,12 @@ class OperatorContextDescriptorContainer(AbstractContextDescriptorContainer):
NODETYPE = domTag('OperatorContextDescriptor')
STATE_QNAME = domTag('OperatorContextState')
+
class MeansContextDescriptorContainer(AbstractContextDescriptorContainer):
NODETYPE = domTag('MeansContextDescriptor')
STATE_QNAME = domTag('MeansContextState')
+
class EnsembleContextDescriptorContainer(AbstractContextDescriptorContainer):
NODETYPE = domTag('EnsembleContextDescriptor')
STATE_QNAME = domTag('EnsembleContextState')
@@ -646,7 +641,8 @@ class EnsembleContextDescriptorContainer(AbstractContextDescriptorContainer):
domTag('SetMetricStateOperationDescriptor'): SetMetricStateOperationDescriptorContainer,
domTag('SetComponentStateOperationDescriptor'): SetComponentStateOperationDescriptorContainer,
domTag('SetAlertStateOperationDescriptor'): SetAlertStateOperationDescriptorContainer,
- }
+}
+
def getContainerClass(qNameType):
"""
diff --git a/tests/test_descriptorcontainers.py b/tests/test_descriptorcontainers.py
index 1dc2c96c..02f23194 100644
--- a/tests/test_descriptorcontainers.py
+++ b/tests/test_descriptorcontainers.py
@@ -32,23 +32,22 @@ def test_AbstractDescriptorContainer(self):
self.assertEqual(dc2.DescriptorVersion, 0)
self.assertEqual(dc2.SafetyClassification, 'Inf')
self.assertEqual(dc.Type, None)
- self.assertEqual(dc.ext_Extension, None)
+ self.assertEqual(len(dc.ext_Extension), 0)
#test update from node
dc.DescriptorVersion = 42
dc.SafetyClassification = 'MedA'
dc.Type = pmtypes.CodedValue('abc', 'def')
- dc.ext_Extension = etree_.Element(namespaces.extTag('Extension'))
- etree_.SubElement(dc.ext_Extension, 'foo', attrib={'someattr':'somevalue'})
- etree_.SubElement(dc.ext_Extension, 'bar', attrib={'anotherattr':'differentvalue'})
+ dc.ext_Extension = [etree_.Element('foo', attrib={'someattr':'somevalue'})]
+ dc.ext_Extension.append(etree_.Element('bar', attrib={'anotherattr':'differentvalue'}))
retrievability = msgtypes.Retrievability([msgtypes.RetrievabilityInfo(msgtypes.RetrievabilityMethod.GET),
msgtypes.RetrievabilityInfo(msgtypes.RetrievabilityMethod.PERIODIC,
update_period=42.0)
]
)
- dc.retrievability = retrievability
+ dc.set_retrievability([retrievability])
node = dc.mkNode()
dc2.updateDescrFromNode(node)
@@ -59,9 +58,8 @@ def test_AbstractDescriptorContainer(self):
self.assertEqual(dc.codeId, 'abc')
self.assertEqual(dc.codingSystem, 'def')
- self.assertEqual(dc2.ext_Extension.tag, namespaces.extTag('Extension'))
self.assertEqual(len(dc2.ext_Extension), 3)
- self.assertEqual(dc2.retrievability, retrievability)
+ self.assertEqual(dc2.get_retrievability(), [retrievability])
def test_AbstractMetricDescriptorContainer(self):
diff --git a/tests/test_pmtypes.py b/tests/test_pmtypes.py
index 0375f5bd..d78f526b 100644
--- a/tests/test_pmtypes.py
+++ b/tests/test_pmtypes.py
@@ -1,6 +1,8 @@
import unittest
-
+from unittest import mock
+from lxml.etree import fromstring, tostring, QName
from sdc11073 import pmtypes
+from sdc11073.mdib.containerproperties import ExtensionLocalValue
class TestPmtypes(unittest.TestCase):
@@ -31,3 +33,242 @@ def test_base_demographics(self):
self.assertEqual(bd.Middlename, ['foo'])
bd = pmtypes.BaseDemographics(middlenames=['foo', 'bar'])
self.assertEqual(bd.Middlename, ['foo', 'bar'])
+
+
+class TestExtensions(unittest.TestCase):
+
+ def test_compare_extensions(self):
+ xml = b"""
+
+
+
+
+
+
+
+ """
+ self.assertNotEqual(fromstring(xml), fromstring(xml))
+ inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml))
+ inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml))
+ self.assertEqual(inst1.ext_Extension, inst2.ext_Extension)
+ self.assertEqual(inst1, inst2)
+
+ another_xml = b"""
+
+
+
+
+
+
+ """
+ inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(another_xml))
+ self.assertNotEqual(inst1.ext_Extension, inst2.ext_Extension)
+ self.assertNotEqual(inst1, inst2)
+
+ def test_compare_extension_with_other_types(self):
+ xml1 = b"""
+
+
+
+ """
+ xml1 = fromstring(xml1) # noqa: S320
+
+ inst1 = ExtensionLocalValue([xml1])
+ self.assertFalse(inst1 == 42)
+ self.assertFalse(inst1 == [41])
+
+ def test_element_order(self):
+ xml1 = b"""
+
+
+
+
+
+
+ """
+ xml2 = b"""
+
+
+
+
+
+
+ """
+ inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml1)) # noqa: S320
+ inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml2)) # noqa: S320
+ self.assertNotEqual(inst1.ext_Extension, inst2.ext_Extension)
+ self.assertNotEqual(inst1, inst2)
+
+ def test_attribute_order(self):
+ xml1 = b"""
+
+
+
+
+
+ """
+ xml2 = b"""
+
+
+
+
+
+ """
+ inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml1)) # noqa: S320
+ inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml2)) # noqa: S320
+ self.assertEqual(inst1.ext_Extension, inst2.ext_Extension)
+ self.assertEqual(inst1, inst2)
+
+ def test_fails_with_qname(self):
+ xml1 = fromstring(b"""
+
+
+ what:lorem
+
+""") # noqa: S320
+ xml2 = fromstring(b"""
+
+
+ who:lorem
+
+""") # noqa: S320
+ self.assertNotEqual(tostring(xml1), tostring(xml2))
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertNotEqual(inst1, inst2)
+
+ def test_ignore_not_needed_namespaces(self):
+ xml1 = fromstring(b"""
+
+What does this mean?
+""") # noqa: S320
+ xml2 = fromstring(b"""
+
+What does this mean?
+""") # noqa: S320
+ self.assertNotEqual(tostring(xml1), tostring(xml2))
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertEqual(inst1, inst2)
+
+ def test_different_length(self):
+ inst1 = ExtensionLocalValue([mock.MagicMock(), mock.MagicMock()])
+ inst2 = ExtensionLocalValue([mock.MagicMock()])
+ self.assertNotEqual(inst1, inst2)
+
+ def test_ignore_comments(self):
+ xml1 = fromstring(b"""
+
+What does this mean?
+
+""") # noqa: S320
+ xml2 = fromstring(b"""
+
+What does this mean?
+""") # noqa: S320
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertEqual(inst1, inst2)
+
+ def test_custom_compare_method(self):
+ xml1 = b"""
+
+
+
+ """
+ xml2 = b"""
+
+
+
+ """
+ xml1 = fromstring(xml1) # noqa: S320
+ xml2 = fromstring(xml2) # noqa: S320
+
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertNotEqual(inst1, inst2)
+
+ def _my_comparer(_, __): # noqa: ANN001 ANN202
+ return True
+
+ orig_method = ExtensionLocalValue.compare_method
+ ExtensionLocalValue.compare_method = _my_comparer
+ self.assertEqual(inst1, inst2)
+ ExtensionLocalValue.compare_method = orig_method
+
+ def test_cdata(self):
+ xml1 = b"""
+
+
+ ]]>
+ What does this mean?
+ """
+ xml2 = b"""
+
+
+ ]]>
+ What does this mean?
+ """
+ xml1 = fromstring(xml1) # noqa: S320
+ xml2 = fromstring(xml2) # noqa: S320
+
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertEqual(inst1, inst2)
+
+ def test_comparison_subelements(self):
+ xml1 = b"""
+
+
+
+
+
+
+ """
+ xml2 = b"""
+
+
+
+
+
+
+ """
+ xml1 = fromstring(xml1) # noqa: S320
+ xml2 = fromstring(xml2) # noqa: S320
+
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertNotEqual(inst1, inst2)
+
+ def test_mixed_content_is_ignored(self):
+ xml1 = fromstring(b"""
+
+ what:lorem
+""") # noqa: S320
+ xml2 = fromstring(b"""
+
+
+ dsafasdf
+ what:lorem
+
+""") # noqa: S320
+ inst1 = ExtensionLocalValue([xml1])
+ inst2 = ExtensionLocalValue([xml2])
+ self.assertEqual(inst1, inst2)