# Copyright 2008-2020 pydicom authors. See LICENSE file for details. """Unit tests for the pydicom.dataset module.""" import copy import io import math import pickle import sys import weakref import pytest try: import numpy HAVE_NP = True except ImportError: HAVE_NP = False import pydicom from pydicom import config from pydicom import dcmread from pydicom.data import get_testdata_file from pydicom.dataelem import DataElement, RawDataElement from pydicom.dataset import ( Dataset, FileDataset, validate_file_meta, FileMetaDataset ) from pydicom.encaps import encapsulate from pydicom.filebase import DicomBytesIO from pydicom.overlay_data_handlers import numpy_handler as NP_HANDLER from pydicom.pixel_data_handlers.util import get_image_pixel_ids from pydicom.sequence import Sequence from pydicom.tag import Tag from pydicom.uid import ( ImplicitVRLittleEndian, ExplicitVRBigEndian, JPEGBaseline8Bit, PYDICOM_IMPLEMENTATION_UID ) from pydicom.valuerep import DS class BadRepr: def __repr__(self): raise ValueError("bad repr") class TestDataset: """Tests for dataset.Dataset.""" def setup(self): self.ds = Dataset() self.ds.TreatmentMachineName = "unit001" def test_attribute_error_in_property(self): """Dataset: AttributeError in property raises actual error message.""" # This comes from bug fix for issue 42 # First, fake enough to try the pixel_array property ds = Dataset() ds.file_meta = FileMetaDataset() ds.PixelData = 'xyzlmnop' msg_from_gdcm = r"'Dataset' object has no attribute 'filename'" msg_from_numpy = (r"'FileMetaDataset' object has no attribute " "'TransferSyntaxUID'") msg_from_pillow = (r"'Dataset' object has no attribute " "'PixelRepresentation'") msg = "(" + "|".join( [msg_from_gdcm, msg_from_numpy, msg_from_pillow]) + ")" with pytest.raises(AttributeError, match=msg): ds.pixel_array def test_for_stray_raw_data_element(self): dataset = Dataset() dataset.PatientName = 'MacDonald^George' sub_ds = Dataset() sub_ds.BeamNumber = '1' dataset.BeamSequence = Sequence([sub_ds]) fp = DicomBytesIO() dataset.is_little_endian = True dataset.is_implicit_VR = True pydicom.write_file(fp, dataset) def _reset(): fp.seek(0) ds1 = pydicom.dcmread(fp, force=True) fp.seek(0) ds2 = pydicom.dcmread(fp, force=True) return ds1, ds2 ds1, ds2 = _reset() assert ds1 == ds2 ds1, ds2 = _reset() ds1.PatientName # convert from raw assert ds1 == ds2 ds1, ds2 = _reset() ds2.PatientName assert ds1 == ds2 ds1, ds2 = _reset() ds2.PatientName assert ds2 == ds1 # compare in other order ds1, ds2 = _reset() ds2.BeamSequence[0].BeamNumber assert ds1 == ds2 # add a new element to one ds sequence item ds1, ds2 = _reset() ds2.BeamSequence[0].BeamName = '1' assert ds1 != ds2 # change a value in a sequence item ds1, ds2 = _reset() ds2.BeamSequence[0].BeamNumber = '2' assert ds2 != ds1 fp.close() def test_attribute_error_in_property_correct_debug(self): """Test AttributeError in property raises correctly.""" class Foo(Dataset): @property def bar(self): return self._barr() def _bar(self): return 'OK' def test(): ds = Foo() ds.bar msg = r"'Foo' object has no attribute '_barr'" with pytest.raises(AttributeError, match=msg): test() def test_tag_exception_print(self): """Test that tag appears in exception messages.""" ds = Dataset() ds.PatientID = "123456" # Valid value ds.SmallestImagePixelValue = BadRepr() # Invalid value msg = r"With tag \(0028, 0106\) got exception: bad repr" with pytest.raises(ValueError, match=msg): str(ds) def test_tag_exception_walk(self): """Test that tag appears in exceptions raised during recursion.""" ds = Dataset() ds.PatientID = "123456" # Valid value ds.SmallestImagePixelValue = BadRepr() # Invalid value def callback(dataset, data_element): return str(data_element) def func(dataset=ds): return dataset.walk(callback) msg = r"With tag \(0028, 0106\) got exception: bad repr" with pytest.raises(ValueError, match=msg): func() def test_set_new_data_element_by_name(self): """Dataset: set new data_element by name.""" ds = Dataset() ds.TreatmentMachineName = "unit #1" data_element = ds[0x300a, 0x00b2] assert "unit #1" == data_element.value assert "SH" == data_element.VR def test_set_existing_data_element_by_name(self): """Dataset: set existing data_element by name.""" self.ds.TreatmentMachineName = "unit999" # change existing value assert "unit999" == self.ds[0x300a, 0x00b2].value def test_set_non_dicom(self, setattr_warn): """Dataset: can set class instance property (non-dicom).""" ds = Dataset() msg = ( r"Camel case attribute 'SomeVariableName' used which is not in " r"the element keyword data dictionary" ) with pytest.warns(UserWarning, match=msg): ds.SomeVariableName = 42 assert hasattr(ds, 'SomeVariableName') assert 42 == ds.SomeVariableName def test_membership(self, contains_warn): """Dataset: can test if item present by 'if in dataset'.""" assert 'TreatmentMachineName' in self.ds msg = ( r"Invalid value 'Dummyname' used with the 'in' operator: must be " r"an element tag as a 2-tuple or int, or an element keyword" ) with pytest.warns(UserWarning, match=msg): assert 'Dummyname' not in self.ds def test_contains(self, contains_warn): """Dataset: can test if item present by 'if in dataset'.""" self.ds.CommandGroupLength = 100 # (0000,0000) assert (0x300a, 0xb2) in self.ds assert [0x300a, 0xb2] in self.ds assert 0x300a00b2 in self.ds assert (0x10, 0x5f) not in self.ds assert 'CommandGroupLength' in self.ds # Use a negative tag to cause an exception msg = ( r"Invalid value '\(-16, 16\)' used with the 'in' operator: must " r"be an element tag as a 2-tuple or int, or an element keyword" ) with pytest.warns(UserWarning, match=msg): assert (-0x0010, 0x0010) not in self.ds def foo(): pass # Try a function msg = r"Invalid value '= (3, 8): expected_diff.add('__reversed__') if sys.version_info[:2] >= (3, 9): expected_diff.update([ '__ror__', '__ior__', '__or__', '__class_getitem__' ]) assert expected_diff == set(dir(di)) - set(dir(ds)) def test_copy_filedataset(self): with open(get_testdata_file("CT_small.dcm"), "rb") as fb: data = fb.read() buff = io.BytesIO(data) ds = pydicom.dcmread(buff) buff.close() msg = ("The 'filename' attribute of the dataset is a " "file-like object and will be set to None") with pytest.warns(UserWarning, match=msg): ds_copy = copy.copy(ds) assert ds.filename is not None assert ds_copy.filename is None assert ds_copy == ds def test_deepcopy_filedataset(self): # regression test for #1147 with open(get_testdata_file("CT_small.dcm"), "rb") as fb: data = fb.read() buff = io.BytesIO(data) ds = pydicom.dcmread(buff) buff.close() msg = ("The 'filename' attribute of the dataset is a " "file-like object and will be set to None") with pytest.warns(UserWarning, match=msg): ds_copy = copy.deepcopy(ds) assert ds.filename is not None assert ds_copy.filename is None assert ds_copy == ds def test_equality_with_different_metadata(self): ds = dcmread(get_testdata_file("CT_small.dcm")) ds2 = copy.deepcopy(ds) assert ds == ds2 ds.filename = "foo.dcm" ds.is_implicit_VR = not ds.is_implicit_VR ds.is_little_endian = not ds.is_little_endian ds.file_meta = None ds.preamble = None assert ds == ds2 def test_deepcopy_without_filename(self): """Regression test for #1571.""" file_meta = FileMetaDataset() ds = FileDataset(filename_or_obj="", dataset={}, file_meta=file_meta, preamble=b"\0" * 128) ds2 = copy.deepcopy(ds) assert ds2.filename == "" class TestDatasetOverlayArray: """Tests for Dataset.overlay_array().""" def setup(self): """Setup the test datasets and the environment.""" self.original_handlers = pydicom.config.overlay_data_handlers pydicom.config.overlay_data_handlers = [NP_HANDLER] self.ds = dcmread( get_testdata_file("MR-SIEMENS-DICOM-WithOverlays.dcm") ) class DummyHandler: def __init__(self): self.raise_exc = False self.has_dependencies = True self.DEPENDENCIES = { 'numpy': ('http://www.numpy.org/', 'NumPy'), } self.HANDLER_NAME = 'Dummy' def supports_transfer_syntax(self, syntax): return True def is_available(self): return self.has_dependencies def get_overlay_array(self, ds, group): if self.raise_exc: raise ValueError("Dummy error message") return 'Success' self.dummy = DummyHandler() def teardown(self): """Restore the environment.""" pydicom.config.overlay_data_handlers = self.original_handlers def test_no_possible(self): """Test with no possible handlers available.""" pydicom.config.overlay_data_handlers = [] with pytest.raises((NotImplementedError, RuntimeError)): self.ds.overlay_array(0x6000) def test_possible_not_available(self): """Test with possible but not available handlers.""" self.dummy.has_dependencies = False pydicom.config.overlay_data_handlers = [self.dummy] msg = ( r"The following handlers are available to decode the overlay " r"data however they are missing required dependencies: " ) with pytest.raises(RuntimeError, match=msg): self.ds.overlay_array(0x6000) def test_possible_available(self): """Test with possible and available handlers.""" pydicom.config.overlay_data_handlers = [self.dummy] assert 'Success' == self.ds.overlay_array(0x6000) def test_handler_raises(self): """Test the handler raising an exception.""" self.dummy.raise_exc = True pydicom.config.overlay_data_handlers = [self.dummy] with pytest.raises(ValueError, match=r"Dummy error message"): self.ds.overlay_array(0x6000) class TestFileMeta: def test_deprecation_warning(self): """Assigning ds.file_meta warns if not FileMetaDataset instance""" ds = Dataset() with pytest.warns(DeprecationWarning): ds.file_meta = Dataset() # not FileMetaDataset def test_assign_file_meta(self): """Test can only set group 2 elements in File Meta""" # FileMetaDataset accepts only group 2 file_meta = FileMetaDataset() with pytest.raises(ValueError): file_meta.PatientID = "123" # No error if assign empty file meta ds = Dataset() ds.file_meta = FileMetaDataset() # Can assign non-empty file_meta ds_meta = Dataset() # not FileMetaDataset ds_meta.TransferSyntaxUID = "1.2" with pytest.warns(DeprecationWarning): ds.file_meta = ds_meta # Error on assigning file meta if any non-group 2 ds_meta.PatientName = "x" with pytest.raises(ValueError): ds.file_meta = ds_meta def test_init_file_meta(self): """Check instantiation of FileMetaDataset""" ds_meta = Dataset() ds_meta.TransferSyntaxUID = "1.2" # Accepts with group 2 file_meta = FileMetaDataset(ds_meta) assert "1.2" == file_meta.TransferSyntaxUID # Accepts dict dict_meta = {0x20010: DataElement(0x20010, "UI", "2.3")} file_meta = FileMetaDataset(dict_meta) assert "2.3" == file_meta.TransferSyntaxUID # Fails if not dict or Dataset with pytest.raises(TypeError): FileMetaDataset(["1", "2"]) # Raises error if init with non-group-2 ds_meta.PatientName = "x" with pytest.raises(ValueError): FileMetaDataset(ds_meta) # None can be passed, to match Dataset behavior FileMetaDataset(None) def test_set_file_meta(self): """Check adding items to existing FileMetaDataset""" file_meta = FileMetaDataset() # Raise error if set non-group 2 with pytest.raises(ValueError): file_meta.PatientID = "1" # Check assigning via non-Tag file_meta[0x20010] = DataElement(0x20010, "UI", "2.3") # Check RawDataElement file_meta[0x20010] = RawDataElement( 0x20010, "UI", 4, "1.23", 0, True, True ) def test_del_file_meta(self): """Can delete the file_meta attribute""" ds = Dataset() ds.file_meta = FileMetaDataset() del ds.file_meta assert not hasattr(ds, "file_meta") def test_show_file_meta(self): orig_show = pydicom.config.show_file_meta pydicom.config.show_file_meta = True ds = Dataset() ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = "1.2" ds.PatientName = "test" shown = str(ds) assert shown.startswith("Dataset.file_meta ---") assert shown.splitlines()[1].startswith( "(0002, 0010) Transfer Syntax UID" ) # Turn off file_meta display pydicom.config.show_file_meta = False shown = str(ds) assert shown.startswith("(0010, 0010) Patient's Name") pydicom.config.show_file_meta = orig_show @pytest.mark.parametrize('copy_method', [Dataset.copy, copy.copy, copy.deepcopy]) def test_copy(self, copy_method): ds = Dataset() ds.PatientName = "John^Doe" ds.BeamSequence = [Dataset(), Dataset(), Dataset()] ds.BeamSequence[0].Manufacturer = "Linac, co." ds.BeamSequence[1].Manufacturer = "Linac and Sons, co." ds.is_implicit_VR = True ds.is_little_endian = True ds.read_encoding = "utf-8" ds_copy = copy_method(ds) assert isinstance(ds_copy, Dataset) assert len(ds_copy) == 2 assert ds_copy.PatientName == "John^Doe" assert ds_copy.BeamSequence[0].Manufacturer == "Linac, co." assert ds_copy.BeamSequence[1].Manufacturer == "Linac and Sons, co." if copy_method == copy.deepcopy: assert id(ds_copy.BeamSequence[0]) != id(ds.BeamSequence[0]) else: # shallow copy assert id(ds_copy.BeamSequence[0]) == id(ds.BeamSequence[0]) assert ds_copy.is_implicit_VR assert ds_copy.is_little_endian assert ds_copy.read_encoding == "utf-8" @pytest.fixture def contains_raise(): """Raise on invalid keys with Dataset.__contains__()""" original = config.INVALID_KEY_BEHAVIOR config.INVALID_KEY_BEHAVIOR = "RAISE" yield config.INVALID_KEY_BEHAVIOR = original @pytest.fixture def contains_ignore(): """Ignore invalid keys with Dataset.__contains__()""" original = config.INVALID_KEY_BEHAVIOR config.INVALID_KEY_BEHAVIOR = "IGNORE" yield config.INVALID_KEY_BEHAVIOR = original @pytest.fixture def contains_warn(): """Warn on invalid keys with Dataset.__contains__()""" original = config.INVALID_KEY_BEHAVIOR config.INVALID_KEY_BEHAVIOR = "WARN" yield config.INVALID_KEY_BEHAVIOR = original CAMEL_CASE = ( [ # Shouldn't warn "Rows", "_Rows", "Rows_", "rows", "_rows", "__rows", "rows_", "ro_ws", "rowds", "BitsStored", "bits_Stored", "Bits_Stored", "bits_stored", "_BitsStored", "BitsStored_", "B_itsStored", "BitsS_tored", "12LeadECG", "file_meta", "filename", "is_implicit_VR", "is_little_endian", "preamble", "timestamp", "fileobj_type", "patient_records", "_parent_encoding", "_dict", "is_decompressed", "read_little_endian", "read_implicit_vr", "read_encoding", "parent", "_private_blocks", "default_element_format", "indent_chars", "default_sequence_element_format", "PatientName" ], [ # Should warn "bitsStored", "BitSStored", "TwelveLeadECG", "SOPInstanceUId", "PatientsName", "Rowds" ] ) @pytest.fixture def setattr_raise(): """Raise on Dataset.__setattr__() close keyword matches.""" original = config.INVALID_KEYWORD_BEHAVIOR config.INVALID_KEYWORD_BEHAVIOR = "RAISE" yield config.INVALID_KEYWORD_BEHAVIOR = original @pytest.fixture def setattr_ignore(): """Ignore Dataset.__setattr__() close keyword matches.""" original = config.INVALID_KEYWORD_BEHAVIOR config.INVALID_KEYWORD_BEHAVIOR = "IGNORE" yield config.INVALID_KEYWORD_BEHAVIOR = original @pytest.fixture def setattr_warn(): """Warn on Dataset.__setattr__() close keyword matches.""" original = config.INVALID_KEYWORD_BEHAVIOR config.INVALID_KEYWORD_BEHAVIOR = "WARN" yield config.INVALID_KEYWORD_BEHAVIOR = original def test_setattr_warns(setattr_warn): """"Test warnings for Dataset.__setattr__() for close matches.""" with pytest.warns(None) as record: ds = Dataset() assert len(record) == 0 for s in CAMEL_CASE[0]: with pytest.warns(None) as record: val = getattr(ds, s, None) setattr(ds, s, val) assert len(record) == 0 for s in CAMEL_CASE[1]: msg = ( r"Camel case attribute '" + s + r"' used which is not in the " r"element keyword data dictionary" ) with pytest.warns(UserWarning, match=msg): val = getattr(ds, s, None) setattr(ds, s, None) def test_setattr_raises(setattr_raise): """"Test exceptions for Dataset.__setattr__() for close matches.""" with pytest.warns(None) as record: ds = Dataset() assert len(record) == 0 for s in CAMEL_CASE[0]: with pytest.warns(None) as record: val = getattr(ds, s, None) setattr(ds, s, val) assert len(record) == 0 for s in CAMEL_CASE[1]: msg = ( r"Camel case attribute '" + s + r"' used which is not in the " r"element keyword data dictionary" ) with pytest.raises(ValueError, match=msg): val = getattr(ds, s, None) setattr(ds, s, None) def test_setattr_ignore(setattr_ignore): """Test config.INVALID_KEYWORD_BEHAVIOR = 'IGNORE'""" with pytest.warns(None) as record: ds = Dataset() assert len(record) == 0 for s in CAMEL_CASE[0]: with pytest.warns(None) as record: val = getattr(ds, s, None) setattr(ds, s, val) assert len(record) == 0 ds = Dataset() for s in CAMEL_CASE[1]: with pytest.warns(None) as record: val = getattr(ds, s, None) setattr(ds, s, None) assert len(record) == 0