Repository: ImagingDataCommons/highdicom

Issue:
Coded Concept with Long Code Value
The CodedConcept `__init()` assigns the value to a LongCodeValue tag if it is >16 characters, and the `value()` method also supports retrieving the value from either CodeValue, LongCodeValue, or URNCodeValue.  But the `from_dataset()` classmethod raises an attribute error if the passed in dataset does not have the CodeValue tag.  
[coding.py line 129](https://github.com/ImagingDataCommons/highdicom/blob/master/src/highdicom/sr/coding.py#L129)

Is there a way to handle LongCodeNames in this method?  The exception doesn't occur when we create the actual CodedConcept, but it does when the concept is embedded in the dataset passed into hd.sr.EnhancedSR() which calls the `from_dataset()` method.

Discussion:

Patch:
diff --git a/src/highdicom/sr/coding.py b/src/highdicom/sr/coding.py
index d4305b6..3301e86 100644
--- a/src/highdicom/sr/coding.py
+++ b/src/highdicom/sr/coding.py
@@ -126,7 +126,14 @@ class CodedConcept(Dataset):
             raise TypeError(
                 'Dataset must be a pydicom.dataset.Dataset.'
             )
-        for kw in ['CodeValue', 'CodeMeaning', 'CodingSchemeDesignator']:
+        code_value_kws = ['CodeValue', 'LongCodeValue', 'URNCodeValue']
+        num_code_values = sum(hasattr(dataset, kw) for kw in code_value_kws)
+        if num_code_values != 1:
+            raise AttributeError(
+                'Dataset should have exactly one of the following attributes: '
+                f'{", ".join(code_value_kws)}.'
+             )
+        for kw in ['CodeMeaning', 'CodingSchemeDesignator']:
             if not hasattr(dataset, kw):
                 raise AttributeError(
                     'Dataset does not contain the following attribute '


Test patch:
diff --git a/tests/test_sr.py b/tests/test_sr.py
index e1ccb7c..d4b94aa 100644
--- a/tests/test_sr.py
+++ b/tests/test_sr.py
@@ -444,6 +444,12 @@ class TestCodedConcept(unittest.TestCase):
     def setUp(self):
         super().setUp()
         self._value = '373098007'
+        self._long_code_value = 'some_code_value_longer_than_sixteen_chars'
+        self._urn_code_value = (
+            'https://browser.ihtsdotools.org/?perspective=full&conceptId1='
+            '373098007&edition=MAIN/SNOMEDCT-US/2023-03-01&release=&languages='
+            'en'
+        )
         self._meaning = 'Mean Value of population'
         self._scheme_designator = 'SCT'
 
@@ -506,6 +512,81 @@ class TestCodedConcept(unittest.TestCase):
         assert c.CodeMeaning == self._meaning
         assert c.CodingSchemeVersion == version
 
+    def test_long_code_value(self):
+        version = 'v1.0'
+        c = CodedConcept(
+            self._long_code_value,
+            self._scheme_designator,
+            self._meaning,
+            version,
+        )
+        assert c.value == self._long_code_value
+        assert c.scheme_designator == self._scheme_designator
+        assert c.meaning == self._meaning
+        assert c.scheme_version == version
+        assert c.LongCodeValue == self._long_code_value
+        assert not hasattr(c, 'CodeValue')
+        assert c.CodingSchemeDesignator == self._scheme_designator
+        assert c.CodeMeaning == self._meaning
+        assert c.CodingSchemeVersion == version
+
+    def test_urn_code_value(self):
+        version = 'v1.0'
+        c = CodedConcept(
+            self._urn_code_value,
+            self._scheme_designator,
+            self._meaning,
+            version,
+        )
+        assert c.value == self._urn_code_value
+        assert c.scheme_designator == self._scheme_designator
+        assert c.meaning == self._meaning
+        assert c.scheme_version == version
+        assert c.URNCodeValue == self._urn_code_value
+        assert not hasattr(c, 'CodeValue')
+        assert c.CodingSchemeDesignator == self._scheme_designator
+        assert c.CodeMeaning == self._meaning
+        assert c.CodingSchemeVersion == version
+
+    def test_from_dataset(self):
+        ds = Dataset()
+        ds.CodeValue = self._value
+        ds.CodeMeaning = self._meaning
+        ds.CodingSchemeDesignator = self._scheme_designator
+        c = CodedConcept.from_dataset(ds)
+        assert c.value == self._value
+        assert c.scheme_designator == self._scheme_designator
+        assert c.meaning == self._meaning
+
+    def test_from_dataset_long_value(self):
+        ds = Dataset()
+        ds.LongCodeValue = self._long_code_value
+        ds.CodeMeaning = self._meaning
+        ds.CodingSchemeDesignator = self._scheme_designator
+        c = CodedConcept.from_dataset(ds)
+        assert c.value == self._long_code_value
+        assert c.scheme_designator == self._scheme_designator
+        assert c.meaning == self._meaning
+
+    def test_from_dataset_urn_value(self):
+        ds = Dataset()
+        ds.URNCodeValue = self._urn_code_value
+        ds.CodeMeaning = self._meaning
+        ds.CodingSchemeDesignator = self._scheme_designator
+        c = CodedConcept.from_dataset(ds)
+        assert c.value == self._urn_code_value
+        assert c.scheme_designator == self._scheme_designator
+        assert c.meaning == self._meaning
+
+    def test_from_dataset_multiple_value(self):
+        ds = Dataset()
+        ds.CodeValue = self._value
+        ds.URNCodeValue = self._urn_code_value  # two code values, invalid
+        ds.CodeMeaning = self._meaning
+        ds.CodingSchemeDesignator = self._scheme_designator
+        with pytest.raises(AttributeError):
+            CodedConcept.from_dataset(ds)
+
     def test_equal(self):
         c1 = CodedConcept(self._value, self._scheme_designator, self._meaning)
         c2 = CodedConcept(self._value, self._scheme_designator, self._meaning)