1# Copyright 2008-2018 pydicom authors. See LICENSE file for details.
2"""Define the Dataset and FileDataset classes.
3
4The Dataset class represents the DICOM Dataset while the FileDataset class
5adds extra functionality to Dataset when data is read from or written to file.
6
7Overview of DICOM object model
8------------------------------
9Dataset (dict subclass)
10  Contains DataElement instances, each of which has a tag, VR, VM and value.
11    The DataElement value can be:
12        * A single value, such as a number, string, etc. (i.e. VM = 1)
13        * A list of numbers, strings, etc. (i.e. VM > 1)
14        * A Sequence (list subclass), where each item is a Dataset which
15            contains its own DataElements, and so on in a recursive manner.
16"""
17import copy
18from bisect import bisect_left
19import io
20from importlib.util import find_spec as have_package
21import inspect  # for __dir__
22from itertools import takewhile
23import json
24import os
25import os.path
26import re
27from types import TracebackType
28from typing import (
29    Optional, Tuple, Union, List, Any, cast, Dict, ValuesView,
30    Iterator, BinaryIO, AnyStr, Callable, TypeVar, Type, overload,
31    MutableSequence, MutableMapping, AbstractSet
32)
33import warnings
34import weakref
35
36try:
37    import numpy
38except ImportError:
39    pass
40
41import pydicom  # for dcmwrite
42import pydicom.charset
43import pydicom.config
44from pydicom import jsonrep, config
45from pydicom._version import __version_info__
46from pydicom.charset import default_encoding, convert_encodings
47from pydicom.config import logger
48from pydicom.datadict import (
49    dictionary_VR, tag_for_keyword, keyword_for_tag, repeater_has_keyword
50)
51from pydicom.dataelem import DataElement, DataElement_from_raw, RawDataElement
52from pydicom.encaps import encapsulate, encapsulate_extended
53from pydicom.fileutil import path_from_pathlike
54from pydicom.pixel_data_handlers.util import (
55    convert_color_space, reshape_pixel_array, get_image_pixel_ids
56)
57from pydicom.tag import Tag, BaseTag, tag_in_exception, TagType
58from pydicom.uid import (
59    ExplicitVRLittleEndian, ImplicitVRLittleEndian, ExplicitVRBigEndian,
60    RLELossless, PYDICOM_IMPLEMENTATION_UID, UID
61)
62from pydicom.waveforms import numpy_handler as wave_handler
63
64
65class PrivateBlock:
66    """Helper class for a private block in the :class:`Dataset`.
67
68    .. versionadded:: 1.3
69
70    See the DICOM Standard, Part 5,
71    :dcm:`Section 7.8.1<part05/sect_7.8.html#sect_7.8.1>` - Private Data
72    Element Tags
73
74    Attributes
75    ----------
76    group : int
77        The private group where the private block is located as a 32-bit
78        :class:`int`.
79    private_creator : str
80        The private creator string related to the block.
81    dataset : Dataset
82        The parent dataset.
83    block_start : int
84        The start element of the private block as a 32-bit :class:`int`. Note
85        that the 2 low order hex digits of the element are always 0.
86    """
87
88    def __init__(
89        self,
90        key: Tuple[int, str],
91        dataset: "Dataset",
92        private_creator_element: int
93    ) -> None:
94        """Initializes an object corresponding to a private tag block.
95
96        Parameters
97        ----------
98        key : tuple
99            The private (tag group, creator) as ``(int, str)``. The group
100            must be an odd number.
101        dataset : Dataset
102            The parent :class:`Dataset`.
103        private_creator_element : int
104            The element of the private creator tag as a 32-bit :class:`int`.
105        """
106        self.group = key[0]
107        self.private_creator = key[1]
108        self.dataset = dataset
109        self.block_start = private_creator_element << 8
110
111    def get_tag(self, element_offset: int) -> BaseTag:
112        """Return the private tag ID for the given `element_offset`.
113
114        Parameters
115        ----------
116        element_offset : int
117            The lower 16 bits (e.g. 2 hex numbers) of the element tag.
118
119        Returns
120        -------
121            The tag ID defined by the private block location and the
122            given element offset.
123
124        Raises
125        ------
126        ValueError
127            If `element_offset` is too large.
128        """
129        if element_offset > 0xff:
130            raise ValueError('Element offset must be less than 256')
131        return Tag(self.group, self.block_start + element_offset)
132
133    def __contains__(self, element_offset: int) -> bool:
134        """Return ``True`` if the tag with given `element_offset` is in
135        the parent :class:`Dataset`.
136        """
137        return self.get_tag(element_offset) in self.dataset
138
139    def __getitem__(self, element_offset: int) -> DataElement:
140        """Return the data element in the parent dataset for the given element
141        offset.
142
143        Parameters
144        ----------
145        element_offset : int
146            The lower 16 bits (e.g. 2 hex numbers) of the element tag.
147
148        Returns
149        -------
150            The data element of the tag in the parent dataset defined by the
151            private block location and the given element offset.
152
153        Raises
154        ------
155        ValueError
156            If `element_offset` is too large.
157        KeyError
158            If no data element exists at that offset.
159        """
160        return self.dataset.__getitem__(self.get_tag(element_offset))
161
162    def __delitem__(self, element_offset: int) -> None:
163        """Delete the tag with the given `element_offset` from the dataset.
164
165        Parameters
166        ----------
167        element_offset : int
168            The lower 16 bits (e.g. 2 hex numbers) of the element tag
169            to be deleted.
170
171        Raises
172        ------
173        ValueError
174            If `element_offset` is too large.
175        KeyError
176            If no data element exists at that offset.
177        """
178        del self.dataset[self.get_tag(element_offset)]
179
180    def add_new(self, element_offset: int, VR: str, value: object) -> None:
181        """Add a private element to the parent :class:`Dataset`.
182
183        Adds the private tag with the given `VR` and `value` to the parent
184        :class:`Dataset` at the tag ID defined by the private block and the
185        given `element_offset`.
186
187        Parameters
188        ----------
189        element_offset : int
190            The lower 16 bits (e.g. 2 hex numbers) of the element tag
191            to be added.
192        VR : str
193            The 2 character DICOM value representation.
194        value
195            The value of the data element. See :meth:`Dataset.add_new()`
196            for a description.
197        """
198        tag = self.get_tag(element_offset)
199        self.dataset.add_new(tag, VR, value)
200        self.dataset[tag].private_creator = self.private_creator
201
202
203def _dict_equal(
204    a: "Dataset", b: Any, exclude: Optional[List[str]] = None
205) -> bool:
206    """Common method for Dataset.__eq__ and FileDataset.__eq__
207
208    Uses .keys() as needed because Dataset iter return items not keys
209    `exclude` is used in FileDataset__eq__ ds.__dict__ compare, which
210    would also compare the wrapped _dict member (entire dataset) again.
211    """
212    return (len(a) == len(b) and
213            all(key in b for key in a.keys()) and
214            all(a[key] == b[key] for key in a.keys()
215                if exclude is None or key not in exclude)
216            )
217
218
219_DatasetValue = Union[DataElement, RawDataElement]
220_DatasetType = Union["Dataset", MutableMapping[BaseTag, _DatasetValue]]
221
222
223class Dataset:
224    """A DICOM dataset as a mutable mapping of DICOM Data Elements.
225
226    Examples
227    --------
228    Add an element to the :class:`Dataset` (for elements in the DICOM
229    dictionary):
230
231    >>> ds = Dataset()
232    >>> ds.PatientName = "CITIZEN^Joan"
233    >>> ds.add_new(0x00100020, 'LO', '12345')
234    >>> ds[0x0010, 0x0030] = DataElement(0x00100030, 'DA', '20010101')
235
236    Add a sequence element to the :class:`Dataset`
237
238    >>> ds.BeamSequence = [Dataset(), Dataset(), Dataset()]
239    >>> ds.BeamSequence[0].Manufacturer = "Linac, co."
240    >>> ds.BeamSequence[1].Manufacturer = "Linac and Sons, co."
241    >>> ds.BeamSequence[2].Manufacturer = "Linac and Daughters, co."
242
243    Add private elements to the :class:`Dataset`
244
245    >>> block = ds.private_block(0x0041, 'My Creator', create=True)
246    >>> block.add_new(0x01, 'LO', '12345')
247
248    Updating and retrieving element values:
249
250    >>> ds.PatientName = "CITIZEN^Joan"
251    >>> ds.PatientName
252    'CITIZEN^Joan'
253    >>> ds.PatientName = "CITIZEN^John"
254    >>> ds.PatientName
255    'CITIZEN^John'
256
257    Retrieving an element's value from a Sequence:
258
259    >>> ds.BeamSequence[0].Manufacturer
260    'Linac, co.'
261    >>> ds.BeamSequence[1].Manufacturer
262    'Linac and Sons, co.'
263
264    Accessing the :class:`~pydicom.dataelem.DataElement` items:
265
266    >>> elem = ds['PatientName']
267    >>> elem
268    (0010, 0010) Patient's Name                      PN: 'CITIZEN^John'
269    >>> elem = ds[0x00100010]
270    >>> elem
271    (0010, 0010) Patient's Name                      PN: 'CITIZEN^John'
272    >>> elem = ds.data_element('PatientName')
273    >>> elem
274    (0010, 0010) Patient's Name                      PN: 'CITIZEN^John'
275
276    Accessing a private :class:`~pydicom.dataelem.DataElement`
277    item:
278
279    >>> block = ds.private_block(0x0041, 'My Creator')
280    >>> elem = block[0x01]
281    >>> elem
282    (0041, 1001) Private tag data                    LO: '12345'
283    >>> elem.value
284    '12345'
285
286    Alternatively:
287
288    >>> ds.get_private_item(0x0041, 0x01, 'My Creator').value
289    '12345'
290
291    Deleting an element from the :class:`Dataset`
292
293    >>> del ds.PatientID
294    >>> del ds.BeamSequence[1].Manufacturer
295    >>> del ds.BeamSequence[2]
296
297    Deleting a private element from the :class:`Dataset`
298
299    >>> block = ds.private_block(0x0041, 'My Creator')
300    >>> if 0x01 in block:
301    ...     del block[0x01]
302
303    Determining if an element is present in the :class:`Dataset`
304
305    >>> 'PatientName' in ds
306    True
307    >>> 'PatientID' in ds
308    False
309    >>> (0x0010, 0x0030) in ds
310    True
311    >>> 'Manufacturer' in ds.BeamSequence[0]
312    True
313
314    Iterating through the top level of a :class:`Dataset` only (excluding
315    Sequences):
316
317    >>> for elem in ds:
318    ...    print(elem)
319    (0010, 0010) Patient's Name                      PN: 'CITIZEN^John'
320
321    Iterating through the entire :class:`Dataset` (including Sequences):
322
323    >>> for elem in ds.iterall():
324    ...     print(elem)
325    (0010, 0010) Patient's Name                      PN: 'CITIZEN^John'
326
327    Recursively iterate through a :class:`Dataset` (including Sequences):
328
329    >>> def recurse(ds):
330    ...     for elem in ds:
331    ...         if elem.VR == 'SQ':
332    ...             [recurse(item) for item in elem.value]
333    ...         else:
334    ...             # Do something useful with each DataElement
335
336    Converting the :class:`Dataset` to and from JSON:
337
338    >>> ds = Dataset()
339    >>> ds.PatientName = "Some^Name"
340    >>> jsonmodel = ds.to_json()
341    >>> ds2 = Dataset()
342    >>> ds2.from_json(jsonmodel)
343    (0010, 0010) Patient's Name                      PN: 'Some^Name'
344
345    Attributes
346    ----------
347    default_element_format : str
348        The default formatting for string display.
349    default_sequence_element_format : str
350        The default formatting for string display of sequences.
351    indent_chars : str
352        For string display, the characters used to indent nested Sequences.
353        Default is ``"   "``.
354    is_little_endian : bool
355        Shall be set before writing with ``write_like_original=False``.
356        The :class:`Dataset` (excluding the pixel data) will be written using
357        the given endianness.
358    is_implicit_VR : bool
359        Shall be set before writing with ``write_like_original=False``.
360        The :class:`Dataset` will be written using the transfer syntax with
361        the given VR handling, e.g *Little Endian Implicit VR* if ``True``,
362        and *Little Endian Explicit VR* or *Big Endian Explicit VR* (depending
363        on ``Dataset.is_little_endian``) if ``False``.
364    """
365    indent_chars = "   "
366
367    def __init__(self, *args: _DatasetType, **kwargs: Any) -> None:
368        """Create a new :class:`Dataset` instance."""
369        self._parent_encoding: List[str] = kwargs.get(
370            'parent_encoding', default_encoding
371        )
372
373        self._dict: MutableMapping[BaseTag, _DatasetValue]
374        if not args:
375            self._dict = {}
376        elif isinstance(args[0], Dataset):
377            self._dict = args[0]._dict
378        else:
379            self._dict = args[0]
380
381        self.is_decompressed = False
382
383        # the following read_XXX attributes are used internally to store
384        # the properties of the dataset after read from a file
385        # set depending on the endianness of the read dataset
386        self.read_little_endian: Optional[bool] = None
387        # set depending on the VR handling of the read dataset
388        self.read_implicit_vr: Optional[bool] = None
389        # The dataset's original character set encoding
390        self.read_encoding: Union[None, str, MutableSequence[str]] = None
391
392        self.is_little_endian: Optional[bool] = None
393        self.is_implicit_VR: Optional[bool] = None
394
395        # the parent data set, if this dataset is a sequence item
396        self.parent: "Optional[weakref.ReferenceType[Dataset]]" = None
397
398        # known private creator blocks
399        self._private_blocks: Dict[Tuple[int, str], PrivateBlock] = {}
400
401        self._pixel_array: Optional["numpy.ndarray"] = None
402        self._pixel_id: Dict[str, int] = {}
403
404        self.file_meta: FileMetaDataset
405
406    def __enter__(self) -> "Dataset":
407        """Method invoked on entry to a with statement."""
408        return self
409
410    def __exit__(
411        self,
412        exc_type: Optional[Type[BaseException]],
413        exc_val: Optional[BaseException],
414        exc_tb: Optional[TracebackType]
415    ) -> Optional[bool]:
416        """Method invoked on exit from a with statement."""
417        # Returning anything other than True will re-raise any exceptions
418        return None
419
420    def add(self, data_element: DataElement) -> None:
421        """Add an element to the :class:`Dataset`.
422
423        Equivalent to ``ds[data_element.tag] = data_element``
424
425        Parameters
426        ----------
427        data_element : dataelem.DataElement
428            The :class:`~pydicom.dataelem.DataElement` to add.
429        """
430        self[data_element.tag] = data_element
431
432    def add_new(self, tag: TagType, VR: str, value: Any) -> None:
433        """Create a new element and add it to the :class:`Dataset`.
434
435        Parameters
436        ----------
437        tag
438            The DICOM (group, element) tag in any form accepted by
439            :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``,
440            ``(0x10, 0x10)``, ``0x00100010``, etc.
441        VR : str
442            The 2 character DICOM value representation (see DICOM Standard,
443            Part 5, :dcm:`Section 6.2<part05/sect_6.2.html>`).
444        value
445            The value of the data element. One of the following:
446
447            * a single string or number
448            * a :class:`list` or :class:`tuple` with all strings or all numbers
449            * a multi-value string with backslash separator
450            * for a sequence element, an empty :class:`list` or ``list`` of
451              :class:`Dataset`
452        """
453
454        data_element = DataElement(tag, VR, value)
455        # use data_element.tag since DataElement verified it
456        self._dict[data_element.tag] = data_element
457
458    def __array__(self) -> "numpy.ndarray":
459        """Support accessing the dataset from a numpy array."""
460        return numpy.asarray(self._dict)
461
462    def data_element(self, name: str) -> Optional[DataElement]:
463        """Return the element corresponding to the element keyword `name`.
464
465        Parameters
466        ----------
467        name : str
468            A DICOM element keyword.
469
470        Returns
471        -------
472        dataelem.DataElement or None
473            For the given DICOM element `keyword`, return the corresponding
474            :class:`~pydicom.dataelem.DataElement` if present, ``None``
475            otherwise.
476        """
477        tag = tag_for_keyword(name)
478        # Test against None as (0000,0000) is a possible tag
479        if tag is not None:
480            return self[tag]
481        return None
482
483    def __contains__(self, name: TagType) -> bool:
484        """Simulate dict.__contains__() to handle DICOM keywords.
485
486        Examples
487        --------
488
489        >>> ds = Dataset()
490        >>> ds.SliceLocation = '2'
491        >>> 'SliceLocation' in ds
492        True
493
494        Parameters
495        ----------
496        name : str or int or 2-tuple
497            The element keyword or tag to search for.
498
499        Returns
500        -------
501        bool
502            ``True`` if the corresponding element is in the :class:`Dataset`,
503            ``False`` otherwise.
504        """
505        try:
506            return Tag(name) in self._dict
507        except Exception as exc:
508            msg = (
509                f"Invalid value '{name}' used with the 'in' operator: must be "
510                "an element tag as a 2-tuple or int, or an element keyword"
511            )
512            if isinstance(exc, OverflowError):
513                msg = (
514                    "Invalid element tag value used with the 'in' operator: "
515                    "tags have a maximum value of (0xFFFF, 0xFFFF)"
516                )
517
518            if config.INVALID_KEY_BEHAVIOR == "WARN":
519                warnings.warn(msg)
520            elif config.INVALID_KEY_BEHAVIOR == "RAISE":
521                raise ValueError(msg) from exc
522
523        return False
524
525    def decode(self) -> None:
526        """Apply character set decoding to the elements in the
527        :class:`Dataset`.
528
529        See DICOM Standard, Part 5,
530        :dcm:`Section 6.1.1<part05/chapter_6.html#sect_6.1.1>`.
531        """
532        # Find specific character set. 'ISO_IR 6' is default
533        # May be multi-valued, but let pydicom.charset handle all logic on that
534        dicom_character_set = self._character_set
535
536        # Shortcut to the decode function in pydicom.charset
537        decode_data_element = pydicom.charset.decode_element
538
539        # Callback for walk(), to decode the chr strings if necessary
540        # This simply calls the pydicom.charset.decode_element function
541        def decode_callback(ds: "Dataset", data_element: DataElement) -> None:
542            """Callback to decode `data_element`."""
543            if data_element.VR == 'SQ':
544                for dset in data_element.value:
545                    dset._parent_encoding = dicom_character_set
546                    dset.decode()
547            else:
548                decode_data_element(data_element, dicom_character_set)
549
550        self.walk(decode_callback, recursive=False)
551
552    def copy(self) -> "Dataset":
553        """Return a shallow copy of the dataset."""
554        return copy.copy(self)
555
556    def __delattr__(self, name: str) -> None:
557        """Intercept requests to delete an attribute by `name`.
558
559        Examples
560        --------
561
562        >>> ds = Dataset()
563        >>> ds.PatientName = 'foo'
564        >>> ds.some_attribute = True
565
566        If `name` is a DICOM keyword - delete the corresponding
567        :class:`~pydicom.dataelem.DataElement`
568
569        >>> del ds.PatientName
570        >>> 'PatientName' in ds
571        False
572
573        If `name` is another attribute - delete it
574
575        >>> del ds.some_attribute
576        >>> hasattr(ds, 'some_attribute')
577        False
578
579        Parameters
580        ----------
581        name : str
582            The keyword for the DICOM element or the class attribute to delete.
583        """
584        # First check if a valid DICOM keyword and if we have that data element
585        tag = cast(BaseTag, tag_for_keyword(name))
586        if tag is not None and tag in self._dict:
587            del self._dict[tag]
588        # If not a DICOM name in this dataset, check for regular instance name
589        #   can't do delete directly, that will call __delattr__ again
590        elif name in self.__dict__:
591            del self.__dict__[name]
592        # Not found, raise an error in same style as python does
593        else:
594            raise AttributeError(name)
595
596    def __delitem__(self, key: Union[slice, BaseTag, TagType]) -> None:
597        """Intercept requests to delete an attribute by key.
598
599        Examples
600        --------
601        Indexing using :class:`~pydicom.dataelem.DataElement` tag
602
603        >>> ds = Dataset()
604        >>> ds.CommandGroupLength = 100
605        >>> ds.PatientName = 'CITIZEN^Jan'
606        >>> del ds[0x00000000]
607        >>> ds
608        (0010, 0010) Patient's Name                      PN: 'CITIZEN^Jan'
609
610        Slicing using :class:`~pydicom.dataelem.DataElement` tag
611
612        >>> ds = Dataset()
613        >>> ds.CommandGroupLength = 100
614        >>> ds.SOPInstanceUID = '1.2.3'
615        >>> ds.PatientName = 'CITIZEN^Jan'
616        >>> del ds[:0x00100000]
617        >>> ds
618        (0010, 0010) Patient's Name                      PN: 'CITIZEN^Jan'
619
620        Parameters
621        ----------
622        key
623            The key for the attribute to be deleted. If a ``slice`` is used
624            then the tags matching the slice conditions will be deleted.
625        """
626        # If passed a slice, delete the corresponding DataElements
627        if isinstance(key, slice):
628            for tag in self._slice_dataset(key.start, key.stop, key.step):
629                del self._dict[tag]
630                # invalidate private blocks in case a private creator is
631                # deleted - will be re-created on next access
632                if self._private_blocks and BaseTag(tag).is_private_creator:
633                    self._private_blocks = {}
634        elif isinstance(key, BaseTag):
635            del self._dict[key]
636            if self._private_blocks and key.is_private_creator:
637                self._private_blocks = {}
638        else:
639            # If not a standard tag, than convert to Tag and try again
640            tag = Tag(key)
641            del self._dict[tag]
642            if self._private_blocks and tag.is_private_creator:
643                self._private_blocks = {}
644
645    def __dir__(self) -> List[str]:
646        """Give a list of attributes available in the :class:`Dataset`.
647
648        List of attributes is used, for example, in auto-completion in editors
649        or command-line environments.
650        """
651        # Force zip object into a list
652        meths = set(list(zip(
653            *inspect.getmembers(self.__class__, inspect.isroutine)))[0])
654        props = set(list(zip(
655            *inspect.getmembers(self.__class__, inspect.isdatadescriptor)))[0])
656        dicom_names = set(self.dir())
657        alldir = sorted(props | meths | dicom_names)
658        return alldir
659
660    def dir(self, *filters: str) -> List[str]:
661        """Return an alphabetical list of element keywords in the
662        :class:`Dataset`.
663
664        Intended mainly for use in interactive Python sessions. Only lists the
665        element keywords in the current level of the :class:`Dataset` (i.e.
666        the contents of any sequence elements are ignored).
667
668        Parameters
669        ----------
670        filters : str
671            Zero or more string arguments to the function. Used for
672            case-insensitive match to any part of the DICOM keyword.
673
674        Returns
675        -------
676        list of str
677            The matching element keywords in the dataset. If no
678            filters are used then all element keywords are returned.
679        """
680        allnames = [keyword_for_tag(tag) for tag in self._dict.keys()]
681        # remove blanks - tags without valid names (e.g. private tags)
682        allnames = [x for x in allnames if x]
683        # Store found names in a dict, so duplicate names appear only once
684        matches = {}
685        for filter_ in filters:
686            filter_ = filter_.lower()
687            match = [x for x in allnames if x.lower().find(filter_) != -1]
688            matches.update({x: 1 for x in match})
689
690        if filters:
691            return sorted(matches.keys())
692
693        return sorted(allnames)
694
695    def __eq__(self, other: Any) -> bool:
696        """Compare `self` and `other` for equality.
697
698        Returns
699        -------
700        bool
701            The result if `self` and `other` are the same class
702        NotImplemented
703            If `other` is not the same class as `self` then returning
704            :class:`NotImplemented` delegates the result to
705            ``superclass.__eq__(subclass)``.
706        """
707        # When comparing against self this will be faster
708        if other is self:
709            return True
710
711        if isinstance(other, self.__class__):
712            return _dict_equal(self, other)
713
714        return NotImplemented
715
716    @overload
717    def get(self, key: str, default: Optional[Any] = None) -> Any:
718        pass  # pragma: no cover
719
720    @overload
721    def get(
722        self,
723        key: Union[int, Tuple[int, int], BaseTag],
724        default: Optional[Any] = None
725    ) -> DataElement:
726        pass  # pragma: no cover
727
728    def get(
729        self,
730        key: Union[str, Union[int, Tuple[int, int], BaseTag]],
731        default: Optional[Any] = None
732    ) -> Union[Any, DataElement]:
733        """Simulate ``dict.get()`` to handle element tags and keywords.
734
735        Parameters
736        ----------
737        key : str or int or Tuple[int, int] or BaseTag
738            The element keyword or tag or the class attribute name to get.
739        default : obj or None, optional
740            If the element or class attribute is not present, return
741            `default` (default ``None``).
742
743        Returns
744        -------
745        value
746            If `key` is the keyword for an element in the :class:`Dataset`
747            then return the element's value.
748        dataelem.DataElement
749            If `key` is a tag for a element in the :class:`Dataset` then
750            return the :class:`~pydicom.dataelem.DataElement`
751            instance.
752        value
753            If `key` is a class attribute then return its value.
754        """
755        if isinstance(key, str):
756            try:
757                return getattr(self, key)
758            except AttributeError:
759                return default
760
761        # is not a string, try to make it into a tag and then hand it
762        # off to the underlying dict
763        try:
764            key = Tag(key)
765        except Exception as exc:
766            raise TypeError("Dataset.get key must be a string or tag") from exc
767
768        try:
769            return self.__getitem__(key)
770        except KeyError:
771            return default
772
773    def items(self) -> AbstractSet[Tuple[BaseTag, _DatasetValue]]:
774        """Return the :class:`Dataset` items to simulate :meth:`dict.items`.
775
776        Returns
777        -------
778        dict_items
779            The top-level (:class:`~pydicom.tag.BaseTag`,
780            :class:`~pydicom.dataelem.DataElement`) items for the
781            :class:`Dataset`.
782        """
783        return self._dict.items()
784
785    def keys(self) -> AbstractSet[BaseTag]:
786        """Return the :class:`Dataset` keys to simulate :meth:`dict.keys`.
787
788        Returns
789        -------
790        dict_keys
791            The :class:`~pydicom.tag.BaseTag` of all the elements in
792            the :class:`Dataset`.
793        """
794        return self._dict.keys()
795
796    def values(self) -> ValuesView[_DatasetValue]:
797        """Return the :class:`Dataset` values to simulate :meth:`dict.values`.
798
799        Returns
800        -------
801        dict_keys
802            The :class:`DataElements<pydicom.dataelem.DataElement>` that make
803            up the values of the :class:`Dataset`.
804        """
805        return self._dict.values()
806
807    def __getattr__(self, name: str) -> Any:
808        """Intercept requests for :class:`Dataset` attribute names.
809
810        If `name` matches a DICOM keyword, return the value for the
811        element with the corresponding tag.
812
813        Parameters
814        ----------
815        name : str
816            An element keyword or a class attribute name.
817
818        Returns
819        -------
820        value
821              If `name` matches a DICOM keyword, returns the corresponding
822              element's value. Otherwise returns the class attribute's
823              value (if present).
824        """
825        tag = tag_for_keyword(name)
826        if tag is not None:  # `name` isn't a DICOM element keyword
827            tag = Tag(tag)
828            if tag in self._dict:  # DICOM DataElement not in the Dataset
829                return self[tag].value
830
831        # no tag or tag not contained in the dataset
832        if name == '_dict':
833            # special handling for contained dict, needed for pickle
834            return {}
835        # Try the base class attribute getter (fix for issue 332)
836        return object.__getattribute__(self, name)
837
838    @property
839    def _character_set(self) -> List[str]:
840        """The character set used to encode text values."""
841        char_set = self.get(BaseTag(0x00080005), None)
842        if not char_set:
843            return self._parent_encoding
844
845        return convert_encodings(char_set.value)
846
847    @overload
848    def __getitem__(self, key: slice) -> "Dataset":
849        pass  # pragma: no cover
850
851    @overload
852    def __getitem__(self, key: TagType) -> DataElement:
853        pass  # pragma: no cover
854
855    def __getitem__(
856        self, key: Union[slice, TagType]
857    ) -> Union["Dataset", DataElement]:
858        """Operator for ``Dataset[key]`` request.
859
860        Any deferred data elements will be read in and an attempt will be made
861        to correct any elements with ambiguous VRs.
862
863        Examples
864        --------
865        Indexing using :class:`~pydicom.dataelem.DataElement` tag
866
867        >>> ds = Dataset()
868        >>> ds.SOPInstanceUID = '1.2.3'
869        >>> ds.PatientName = 'CITIZEN^Jan'
870        >>> ds.PatientID = '12345'
871        >>> ds[0x00100010].value
872        'CITIZEN^Jan'
873
874        Slicing using element tags; all group ``0x0010`` elements in
875        the  dataset
876
877        >>> ds[0x00100000:0x00110000]
878        (0010, 0010) Patient's Name                      PN: 'CITIZEN^Jan'
879        (0010, 0020) Patient ID                          LO: '12345'
880
881        All group ``0x0002`` elements in the dataset
882
883        >>> ds[(0x0002, 0x0000):(0x0003, 0x0000)]
884        <BLANKLINE>
885
886        Parameters
887        ----------
888        key
889            The DICOM (group, element) tag in any form accepted by
890            :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``,
891            ``(0x10, 0x10)``, ``0x00100010``, etc. May also be a :class:`slice`
892            made up of DICOM tags.
893
894        Returns
895        -------
896        dataelem.DataElement or Dataset
897            If a single DICOM element tag is used then returns the
898            corresponding :class:`~pydicom.dataelem.DataElement`.
899            If a :class:`slice` is used then returns a :class:`Dataset` object
900            containing the corresponding
901            :class:`DataElements<pydicom.dataelem.DataElement>`.
902        """
903        # If passed a slice, return a Dataset containing the corresponding
904        #   DataElements
905        if isinstance(key, slice):
906            return self._dataset_slice(key)
907
908        if isinstance(key, BaseTag):
909            tag = key
910        else:
911            try:
912                tag = Tag(key)
913            except Exception as exc:
914                raise KeyError(f"'{key}'") from exc
915
916        elem = self._dict[tag]
917        if isinstance(elem, DataElement):
918            if elem.VR == 'SQ' and elem.value:
919                # let a sequence know its parent dataset, as sequence items
920                # may need parent dataset tags to resolve ambiguous tags
921                elem.value.parent = self
922            return elem
923
924        if isinstance(elem, RawDataElement):
925            # If a deferred read, then go get the value now
926            if elem.value is None and elem.length != 0:
927                from pydicom.filereader import read_deferred_data_element
928
929                elem = read_deferred_data_element(
930                    self.fileobj_type,
931                    self.filename,
932                    self.timestamp,
933                    elem
934                )
935
936            if tag != BaseTag(0x00080005):
937                character_set = self.read_encoding or self._character_set
938            else:
939                character_set = default_encoding
940            # Not converted from raw form read from file yet; do so now
941            self[tag] = DataElement_from_raw(elem, character_set, self)
942
943            # If the Element has an ambiguous VR, try to correct it
944            if 'or' in self[tag].VR:
945                from pydicom.filewriter import correct_ambiguous_vr_element
946                self[tag] = correct_ambiguous_vr_element(
947                    self[tag], self, elem[6]
948                )
949
950        return cast(DataElement, self._dict.get(tag))
951
952    def private_block(
953        self, group: int, private_creator: str, create: bool = False
954    ) -> PrivateBlock:
955        """Return the block for the given tag `group` and `private_creator`.
956
957        .. versionadded:: 1.3
958
959        If `create` is ``True`` and the `private_creator` does not exist,
960        the private creator tag is added.
961
962        Notes
963        -----
964        We ignore the unrealistic case that no free block is available.
965
966        Parameters
967        ----------
968        group : int
969            The group of the private tag to be found as a 32-bit :class:`int`.
970            Must be an odd number (e.g. a private group).
971        private_creator : str
972            The private creator string associated with the tag.
973        create : bool, optional
974            If ``True`` and `private_creator` does not exist, a new private
975            creator tag is added at the next free block. If ``False``
976            (the default) and `private_creator` does not exist,
977            :class:`KeyError` is raised instead.
978
979        Returns
980        -------
981        PrivateBlock
982            The existing or newly created private block.
983
984        Raises
985        ------
986        ValueError
987            If `group` doesn't belong to a private tag or `private_creator`
988            is empty.
989        KeyError
990            If the private creator tag is not found in the given group and
991            the `create` parameter is ``False``.
992        """
993        def new_block(element: int) -> PrivateBlock:
994            block = PrivateBlock(key, self, element)
995            self._private_blocks[key] = block
996            return block
997
998        key = (group, private_creator)
999        if key in self._private_blocks:
1000            return self._private_blocks[key]
1001
1002        if not private_creator:
1003            raise ValueError('Private creator must have a value')
1004
1005        if group % 2 == 0:
1006            raise ValueError(
1007                'Tag must be private if private creator is given')
1008
1009        # find block with matching private creator
1010        block = self[(group, 0x10):(group, 0x100)]  # type: ignore[misc]
1011        data_el = next(
1012            (
1013                elem for elem in block if elem.value == private_creator
1014            ),
1015            None
1016        )
1017        if data_el is not None:
1018            return new_block(data_el.tag.element)
1019
1020        if not create:
1021            # not found and shall not be created - raise
1022            raise KeyError(
1023                "Private creator '{}' not found".format(private_creator))
1024
1025        # private creator not existing - find first unused private block
1026        # and add the private creator
1027        first_free_el = next(
1028            el for el in range(0x10, 0x100)
1029            if Tag(group, el) not in self._dict
1030        )
1031        self.add_new(Tag(group, first_free_el), 'LO', private_creator)
1032        return new_block(first_free_el)
1033
1034    def private_creators(self, group: int) -> List[str]:
1035        """Return a list of private creator names in the given group.
1036
1037        .. versionadded:: 1.3
1038
1039        Examples
1040        --------
1041        This can be used to check if a given private creator exists in
1042        the group of the dataset:
1043
1044        >>> ds = Dataset()
1045        >>> if 'My Creator' in ds.private_creators(0x0041):
1046        ...     block = ds.private_block(0x0041, 'My Creator')
1047
1048        Parameters
1049        ----------
1050        group : int
1051            The private group as a 32-bit :class:`int`. Must be an odd number.
1052
1053        Returns
1054        -------
1055        list of str
1056            All private creator names for private blocks in the group.
1057
1058        Raises
1059        ------
1060        ValueError
1061            If `group` is not a private group.
1062        """
1063        if group % 2 == 0:
1064            raise ValueError('Group must be an odd number')
1065
1066        block = self[(group, 0x10):(group, 0x100)]  # type: ignore[misc]
1067        return [x.value for x in block]
1068
1069    def get_private_item(
1070        self, group: int, element_offset: int, private_creator: str
1071    ) -> DataElement:
1072        """Return the data element for the given private tag `group`.
1073
1074        .. versionadded:: 1.3
1075
1076        This is analogous to ``Dataset.__getitem__()``, but only for private
1077        tags. This allows to find the private tag for the correct private
1078        creator without the need to add the tag to the private dictionary
1079        first.
1080
1081        Parameters
1082        ----------
1083        group : int
1084            The private tag group where the item is located as a 32-bit int.
1085        element_offset : int
1086            The lower 16 bits (e.g. 2 hex numbers) of the element tag.
1087        private_creator : str
1088            The private creator for the tag. Must match the private creator
1089            for the tag to be returned.
1090
1091        Returns
1092        -------
1093        dataelem.DataElement
1094            The corresponding element.
1095
1096        Raises
1097        ------
1098        ValueError
1099            If `group` is not part of a private tag or `private_creator` is
1100            empty.
1101        KeyError
1102            If the private creator tag is not found in the given group.
1103            If the private tag is not found.
1104        """
1105        block = self.private_block(group, private_creator)
1106        return self.__getitem__(block.get_tag(element_offset))
1107
1108    @overload
1109    def get_item(self, key: slice) -> "Dataset":
1110        pass  # pragma: no cover
1111
1112    @overload
1113    def get_item(self, key: TagType) -> DataElement:
1114        pass  # pragma: no cover
1115
1116    def get_item(
1117        self, key: Union[slice, TagType]
1118    ) -> Union["Dataset", DataElement, RawDataElement, None]:
1119        """Return the raw data element if possible.
1120
1121        It will be raw if the user has never accessed the value, or set their
1122        own value. Note if the data element is a deferred-read element,
1123        then it is read and converted before being returned.
1124
1125        Parameters
1126        ----------
1127        key
1128            The DICOM (group, element) tag in any form accepted by
1129            :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``,
1130            ``(0x10, 0x10)``, ``0x00100010``, etc. May also be a :class:`slice`
1131            made up of DICOM tags.
1132
1133        Returns
1134        -------
1135        dataelem.DataElement
1136            The corresponding element.
1137        """
1138        if isinstance(key, slice):
1139            return self._dataset_slice(key)
1140
1141        elem = self._dict.get(Tag(key))
1142        # If a deferred read, return using __getitem__ to read and convert it
1143        if isinstance(elem, RawDataElement) and elem.value is None:
1144            return self[key]
1145
1146        return elem
1147
1148    def _dataset_slice(self, slce: slice) -> "Dataset":
1149        """Return a slice that has the same properties as the original dataset.
1150
1151        That includes properties related to endianness and VR handling,
1152        and the specific character set. No element conversion is done, e.g.
1153        elements of type ``RawDataElement`` are kept.
1154        """
1155        tags = self._slice_dataset(slce.start, slce.stop, slce.step)
1156        ds = Dataset({tag: self.get_item(tag) for tag in tags})
1157        ds.is_little_endian = self.is_little_endian
1158        ds.is_implicit_VR = self.is_implicit_VR
1159        ds.set_original_encoding(
1160            self.read_implicit_vr, self.read_little_endian, self.read_encoding
1161        )
1162        return ds
1163
1164    @property
1165    def is_original_encoding(self) -> bool:
1166        """Return ``True`` if the encoding to be used for writing is set and
1167        is the same as that used to originally encode the  :class:`Dataset`.
1168
1169        .. versionadded:: 1.1
1170
1171        This includes properties related to endianness, VR handling and the
1172        (0008,0005) *Specific Character Set*.
1173        """
1174        return (
1175            self.is_implicit_VR is not None
1176            and self.is_little_endian is not None
1177            and self.read_implicit_vr == self.is_implicit_VR
1178            and self.read_little_endian == self.is_little_endian
1179            and self.read_encoding == self._character_set
1180        )
1181
1182    def set_original_encoding(
1183        self,
1184        is_implicit_vr: Optional[bool],
1185        is_little_endian: Optional[bool],
1186        character_encoding: Union[None, str, MutableSequence[str]]
1187    ) -> None:
1188        """Set the values for the original transfer syntax and encoding.
1189
1190        .. versionadded:: 1.2
1191
1192        Can be used for a :class:`Dataset` with raw data elements to enable
1193        optimized writing (e.g. without decoding the data elements).
1194        """
1195        self.read_implicit_vr = is_implicit_vr
1196        self.read_little_endian = is_little_endian
1197        self.read_encoding = character_encoding
1198
1199    def group_dataset(self, group: int) -> "Dataset":
1200        """Return a :class:`Dataset` containing only elements of a certain
1201        group.
1202
1203        Parameters
1204        ----------
1205        group : int
1206            The group part of a DICOM (group, element) tag.
1207
1208        Returns
1209        -------
1210        Dataset
1211            A :class:`Dataset` containing elements of the group specified.
1212        """
1213        return self[(group, 0x0000):(group + 1, 0x0000)]  # type: ignore[misc]
1214
1215    def __iter__(self) -> Iterator[DataElement]:
1216        """Iterate through the top-level of the Dataset, yielding DataElements.
1217
1218        Examples
1219        --------
1220
1221        >>> ds = Dataset()
1222        >>> for elem in ds:
1223        ...     print(elem)
1224
1225        The :class:`DataElements<pydicom.dataelem.DataElement>` are returned in
1226        increasing tag value order. Sequence items are returned as a single
1227        :class:`~pydicom.dataelem.DataElement`, so it is up
1228        to the calling code to recurse into the Sequence items if desired.
1229
1230        Yields
1231        ------
1232        dataelem.DataElement
1233            The :class:`Dataset`'s
1234            :class:`DataElements<pydicom.dataelem.DataElement>`, sorted by
1235            increasing tag order.
1236        """
1237        # Note this is different than the underlying dict class,
1238        #        which returns the key of the key:value mapping.
1239        #   Here the value is returned (but data_element.tag has the key)
1240        taglist = sorted(self._dict.keys())
1241        for tag in taglist:
1242            yield self[tag]
1243
1244    def elements(self) -> Iterator[DataElement]:
1245        """Yield the top-level elements of the :class:`Dataset`.
1246
1247        .. versionadded:: 1.1
1248
1249        Examples
1250        --------
1251
1252        >>> ds = Dataset()
1253        >>> for elem in ds.elements():
1254        ...     print(elem)
1255
1256        The elements are returned in the same way as in
1257        ``Dataset.__getitem__()``.
1258
1259        Yields
1260        ------
1261        dataelem.DataElement or dataelem.RawDataElement
1262            The unconverted elements sorted by increasing tag order.
1263        """
1264        taglist = sorted(self._dict.keys())
1265        for tag in taglist:
1266            yield self.get_item(tag)
1267
1268    def __len__(self) -> int:
1269        """Return the number of elements in the top level of the dataset."""
1270        return len(self._dict)
1271
1272    def __ne__(self, other: Any) -> bool:
1273        """Compare `self` and `other` for inequality."""
1274        return not self == other
1275
1276    def clear(self) -> None:
1277        """Delete all the elements from the :class:`Dataset`."""
1278        self._dict.clear()
1279
1280    def pop(self, key: Union[BaseTag, TagType], *args: Any) -> _DatasetValue:
1281        """Emulate :meth:`dict.pop` with support for tags and keywords.
1282
1283        Removes the element for `key` if it exists and returns it,
1284        otherwise returns a default value if given or raises :class:`KeyError`.
1285
1286        Parameters
1287        ----------
1288        key : int or str or 2-tuple
1289
1290            * If :class:`tuple` - the group and element number of the DICOM tag
1291            * If :class:`int` - the combined group/element number
1292            * If :class:`str` - the DICOM keyword of the tag
1293
1294        *args : zero or one argument
1295            Defines the behavior if no tag exists for `key`: if given,
1296            it defines the return value, if not given, :class:`KeyError` is
1297            raised
1298
1299        Returns
1300        -------
1301        RawDataElement or DataElement
1302            The element for `key` if it exists, or the default value if given.
1303
1304        Raises
1305        ------
1306        KeyError
1307            If the `key` is not a valid tag or keyword.
1308            If the tag does not exist and no default is given.
1309        """
1310        try:
1311            key = Tag(key)
1312        except Exception:
1313            pass
1314
1315        return self._dict.pop(cast(BaseTag, key), *args)
1316
1317    def popitem(self) -> Tuple[BaseTag, _DatasetValue]:
1318        """Emulate :meth:`dict.popitem`.
1319
1320        Returns
1321        -------
1322        tuple of (BaseTag, DataElement)
1323        """
1324        return self._dict.popitem()
1325
1326    def setdefault(
1327        self, key: TagType, default: Optional[Any] = None
1328    ) -> DataElement:
1329        """Emulate :meth:`dict.setdefault` with support for tags and keywords.
1330
1331        Examples
1332        --------
1333
1334        >>> ds = Dataset()
1335        >>> elem = ds.setdefault((0x0010, 0x0010), "Test")
1336        >>> elem
1337        (0010, 0010) Patient's Name                      PN: 'Test'
1338        >>> elem.value
1339        'Test'
1340        >>> elem = ds.setdefault('PatientSex',
1341        ...     DataElement(0x00100040, 'CS', 'F'))
1342        >>> elem.value
1343        'F'
1344
1345        Parameters
1346        ----------
1347        key : int, str or 2-tuple of int
1348
1349            * If :class:`tuple` - the group and element number of the DICOM tag
1350            * If :class:`int` - the combined group/element number
1351            * If :class:`str` - the DICOM keyword of the tag
1352        default : pydicom.dataelem.DataElement or object, optional
1353            The :class:`~pydicom.dataelem.DataElement` to use with `key`, or
1354            the value of the :class:`~pydicom.dataelem.DataElement` to use with
1355            `key` (default ``None``).
1356
1357        Returns
1358        -------
1359        pydicom.dataelem.DataElement or object
1360            The :class:`~pydicom.dataelem.DataElement` for `key`.
1361
1362        Raises
1363        ------
1364        ValueError
1365            If `key` is not convertible to a valid tag or a known element
1366            keyword.
1367        KeyError
1368            If :attr:`~pydicom.config.enforce_valid_values` is ``True`` and
1369            `key` is an unknown non-private tag.
1370        """
1371        tag = Tag(key)
1372        if tag in self:
1373            return self[tag]
1374
1375        if not isinstance(default, DataElement):
1376            if tag.is_private:
1377                vr = 'UN'
1378            else:
1379                try:
1380                    vr = dictionary_VR(tag)
1381                except KeyError:
1382                    if config.enforce_valid_values:
1383                        raise KeyError(f"Unknown DICOM tag {tag}")
1384                    else:
1385                        vr = 'UN'
1386                        warnings.warn(
1387                            f"Unknown DICOM tag {tag} - setting VR to 'UN'"
1388                        )
1389
1390            default = DataElement(tag, vr, default)
1391
1392        self[key] = default
1393
1394        return default
1395
1396    def convert_pixel_data(self, handler_name: str = '') -> None:
1397        """Convert pixel data to a :class:`numpy.ndarray` internally.
1398
1399        Parameters
1400        ----------
1401        handler_name : str, optional
1402            The name of the pixel handler that shall be used to
1403            decode the data. Supported names are: ``'gdcm'``,
1404            ``'pillow'``, ``'jpeg_ls'``, ``'rle'``, ``'numpy'`` and
1405            ``'pylibjpeg'``. If not used (the default), a matching handler is
1406            used from the handlers configured in
1407            :attr:`~pydicom.config.pixel_data_handlers`.
1408
1409        Returns
1410        -------
1411        None
1412            Converted pixel data is stored internally in the dataset.
1413
1414        Raises
1415        ------
1416        ValueError
1417            If `handler_name` is not a valid handler name.
1418        NotImplementedError
1419            If the given handler or any handler, if none given, is unable to
1420            decompress pixel data with the current transfer syntax
1421        RuntimeError
1422            If the given handler, or the handler that has been selected if
1423            none given, is not available.
1424
1425        Notes
1426        -----
1427        If the pixel data is in a compressed image format, the data is
1428        decompressed and any related data elements are changed accordingly.
1429        """
1430        # Check if already have converted to a NumPy array
1431        # Also check if pixel data has changed. If so, get new NumPy array
1432        already_have = True
1433        if not hasattr(self, "_pixel_array"):
1434            already_have = False
1435        elif self._pixel_id != get_image_pixel_ids(self):
1436            already_have = False
1437
1438        if already_have:
1439            return
1440
1441        if handler_name:
1442            self._convert_pixel_data_using_handler(handler_name)
1443        else:
1444            self._convert_pixel_data_without_handler()
1445
1446    def _convert_pixel_data_using_handler(self, name: str) -> None:
1447        """Convert the pixel data using handler with the given name.
1448        See :meth:`~Dataset.convert_pixel_data` for more information.
1449        """
1450        # handle some variations in name
1451        handler_name = name.lower()
1452        if not handler_name.endswith('_handler'):
1453            handler_name += '_handler'
1454        if handler_name == 'numpy_handler':
1455            handler_name = 'np_handler'
1456        if handler_name == 'jpeg_ls_handler':
1457            # the name in config differs from the actual handler name
1458            # we allow both
1459            handler_name = 'jpegls_handler'
1460        if not hasattr(pydicom.config, handler_name):
1461            raise ValueError(f"'{name}' is not a known handler name")
1462
1463        handler = getattr(pydicom.config, handler_name)
1464
1465        tsyntax = self.file_meta.TransferSyntaxUID
1466        if not handler.supports_transfer_syntax(tsyntax):
1467            raise NotImplementedError(
1468                "Unable to decode pixel data with a transfer syntax UID"
1469                f" of '{tsyntax}' ({tsyntax.name}) using the pixel data "
1470                f"handler '{name}'. Please see the pydicom documentation for "
1471                "information on supported transfer syntaxes."
1472            )
1473        if not handler.is_available():
1474            raise RuntimeError(
1475                f"The pixel data handler '{name}' is not available on your "
1476                "system. Please refer to the pydicom documentation for "
1477                "information on installing needed packages."
1478            )
1479        # if the conversion fails, the exception is propagated up
1480        self._do_pixel_data_conversion(handler)
1481
1482    def _convert_pixel_data_without_handler(self) -> None:
1483        """Convert the pixel data using the first matching handler.
1484        See :meth:`~Dataset.convert_pixel_data` for more information.
1485        """
1486        # Find all possible handlers that support the transfer syntax
1487        ts = self.file_meta.TransferSyntaxUID
1488        possible_handlers = [
1489            hh for hh in pydicom.config.pixel_data_handlers
1490            if hh is not None
1491            and hh.supports_transfer_syntax(ts)  # type: ignore[attr-defined]
1492        ]
1493
1494        # No handlers support the transfer syntax
1495        if not possible_handlers:
1496            raise NotImplementedError(
1497                "Unable to decode pixel data with a transfer syntax UID of "
1498                f"'{ts}' ({ts.name}) as there are no pixel data "
1499                "handlers available that support it. Please see the pydicom "
1500                "documentation for information on supported transfer syntaxes "
1501            )
1502
1503        # Handlers that both support the transfer syntax and have their
1504        #   dependencies met
1505        available_handlers = [
1506            hh for hh in possible_handlers
1507            if hh.is_available()  # type: ignore[attr-defined]
1508        ]
1509
1510        # There are handlers that support the transfer syntax but none of them
1511        #   can be used as missing dependencies
1512        if not available_handlers:
1513            # For each of the possible handlers we want to find which
1514            #   dependencies are missing
1515            msg = (
1516                "The following handlers are available to decode the pixel "
1517                "data however they are missing required dependencies: "
1518            )
1519            pkg_msg = []
1520            for hh in possible_handlers:
1521                hh_deps = hh.DEPENDENCIES  # type: ignore[attr-defined]
1522                # Missing packages
1523                missing = [dd for dd in hh_deps if have_package(dd) is None]
1524                # Package names
1525                names = [hh_deps[name][1] for name in missing]
1526                pkg_msg.append(
1527                    f"{hh.HANDLER_NAME} "  # type: ignore[attr-defined]
1528                    f"(req. {', '.join(names)})"
1529                )
1530
1531            raise RuntimeError(msg + ', '.join(pkg_msg))
1532
1533        last_exception = None
1534        for handler in available_handlers:
1535            try:
1536                self._do_pixel_data_conversion(handler)
1537                return
1538            except Exception as exc:
1539                logger.debug(
1540                    "Exception raised by pixel data handler", exc_info=exc
1541                )
1542                last_exception = exc
1543
1544        # The only way to get to this point is if we failed to get the pixel
1545        #   array because all suitable handlers raised exceptions
1546        self._pixel_array = None
1547        self._pixel_id = {}
1548
1549        logger.info(
1550            "Unable to decode the pixel data using the following handlers: {}."
1551            "Please see the list of supported Transfer Syntaxes in the "
1552            "pydicom documentation for alternative packages that might "
1553            "be able to decode the data"
1554            .format(", ".join([str(hh) for hh in available_handlers]))
1555        )
1556        raise last_exception  # type: ignore[misc]
1557
1558    def _do_pixel_data_conversion(self, handler: Any) -> None:
1559        """Do the actual data conversion using the given handler."""
1560
1561        # Use the handler to get a 1D numpy array of the pixel data
1562        # Will raise an exception if no pixel data element
1563        arr = handler.get_pixeldata(self)
1564        self._pixel_array = reshape_pixel_array(self, arr)
1565
1566        # Some handler/transfer syntax combinations may need to
1567        #   convert the color space from YCbCr to RGB
1568        if handler.needs_to_convert_to_RGB(self):
1569            self._pixel_array = convert_color_space(
1570                self._pixel_array, 'YBR_FULL', 'RGB'
1571            )
1572
1573        self._pixel_id = get_image_pixel_ids(self)
1574
1575    def compress(
1576        self,
1577        transfer_syntax_uid: str,
1578        arr: Optional["numpy.ndarray"] = None,
1579        encoding_plugin: str = '',
1580        decoding_plugin: str = '',
1581        encapsulate_ext: bool = False,
1582        **kwargs: Any,
1583    ) -> None:
1584        """Compress and update an uncompressed dataset in-place with the
1585        resulting :dcm:`encapsulated<part05/sect_A.4.html>` pixel data.
1586
1587        .. versionadded:: 2.2
1588
1589        The dataset must already have the following
1590        :dcm:`Image Pixel<part03/sect_C.7.6.3.html>` module elements present
1591        with correct values that correspond to the resulting compressed
1592        pixel data:
1593
1594        * (0028,0002) *Samples per Pixel*
1595        * (0028,0004) *Photometric Interpretation*
1596        * (0028,0008) *Number of Frames* (if more than 1 frame will be present)
1597        * (0028,0010) *Rows*
1598        * (0028,0011) *Columns*
1599        * (0028,0100) *Bits Allocated*
1600        * (0028,0101) *Bits Stored*
1601        * (0028,0103) *Pixel Representation*
1602
1603        This method will add the file meta dataset if none is present and add
1604        or modify the following elements:
1605
1606        * (0002,0010) *Transfer Syntax UID*
1607        * (7FE0,0010) *Pixel Data*
1608
1609        If *Samples per Pixel* is greater than 1 then the following element
1610        will also be added:
1611
1612        * (0028,0006) *Planar Configuration*
1613
1614        If the compressed pixel data is too large for encapsulation using a
1615        basic offset table then an :dcm:`extended offset table
1616        <part03/sect_C.7.6.3.html>` will also be used, in which case the
1617        following elements will also be added:
1618
1619        * (7FE0,0001) *Extended Offset Table*
1620        * (7FE0,0002) *Extended Offset Table Lengths*
1621
1622        **Supported Transfer Syntax UIDs**
1623
1624        +----------------------+----------+----------------------------------+
1625        | UID                  | Plugins  | Encoding Guide                   |
1626        +======================+==========+==================================+
1627        | *RLE Lossless* -     |pydicom,  | :doc:`RLE Lossless               |
1628        | 1.2.840.10008.1.2.5  |pylibjpeg,| </guides/encoding/rle_lossless>` |
1629        |                      |gdcm      |                                  |
1630        +----------------------+----------+----------------------------------+
1631
1632        Examples
1633        --------
1634
1635        Compress the existing uncompressed *Pixel Data* in place:
1636
1637        >>> from pydicom.data import get_testdata_file
1638        >>> from pydicom.uid import RLELossless
1639        >>> ds = get_testdata_file("CT_small.dcm", read=True)
1640        >>> ds.compress(RLELossless)
1641        >>> ds.save_as("CT_small_rle.dcm")
1642
1643        Parameters
1644        ----------
1645        transfer_syntax_uid : pydicom.uid.UID
1646            The UID of the :dcm:`transfer syntax<part05/chapter_10.html>` to
1647            use when compressing the pixel data.
1648        arr : numpy.ndarray, optional
1649            Compress the uncompressed pixel data in `arr` and use it
1650            to set the *Pixel Data*. If `arr` is not used then the
1651            existing *Pixel Data* in the dataset will be compressed instead.
1652            The :attr:`~numpy.ndarray.shape`, :class:`~numpy.dtype` and
1653            contents of the array should match the dataset.
1654        encoding_plugin : str, optional
1655            Use the `encoding_plugin` to compress the pixel data. See the
1656            :doc:`user guide </old/image_data_compression>` for a list of
1657            plugins available for each UID and their dependencies. If not
1658            specified then all available plugins will be tried (default).
1659        decoding_plugin : str, optional
1660            Placeholder for future functionality.
1661        encapsulate_ext : bool, optional
1662            If ``True`` then force the addition of an extended offset table.
1663            If ``False`` (default) then an extended offset table
1664            will be added if needed for large amounts of compressed *Pixel
1665            Data*, otherwise just the basic offset table will be used.
1666        **kwargs
1667            Optional keyword parameters for the encoding plugin may also be
1668            present. See the :doc:`encoding plugins options
1669            </guides/encoding/encoder_plugin_options>` for more information.
1670        """
1671        from pydicom.encoders import get_encoder
1672
1673        uid = UID(transfer_syntax_uid)
1674
1675        # Raises NotImplementedError if `uid` is not supported
1676        encoder = get_encoder(uid)
1677        if not encoder.is_available:
1678            missing = "\n".join(
1679                [f"    {s}" for s in encoder.missing_dependencies]
1680            )
1681            raise RuntimeError(
1682                f"The '{uid.name}' encoder is unavailable because its "
1683                f"encoding plugins are missing dependencies:\n"
1684                f"{missing}"
1685            )
1686
1687        if arr is None:
1688            # Encode the current *Pixel Data*
1689            frame_iterator = encoder.iter_encode(
1690                self,
1691                encoding_plugin=encoding_plugin,
1692                decoding_plugin=decoding_plugin,
1693                **kwargs
1694            )
1695        else:
1696            # Encode from an uncompressed pixel data array
1697            kwargs.update(encoder.kwargs_from_ds(self))
1698            frame_iterator = encoder.iter_encode(
1699                arr,
1700                encoding_plugin=encoding_plugin,
1701                **kwargs
1702            )
1703
1704        # Encode!
1705        encoded = [f for f in frame_iterator]
1706
1707        # Encapsulate the encoded *Pixel Data*
1708        nr_frames = getattr(self, "NumberOfFrames", 1) or 1
1709        total = (nr_frames - 1) * 8 + sum([len(f) for f in encoded[:-1]])
1710        if encapsulate_ext or total > 2**32 - 1:
1711            (self.PixelData,
1712             self.ExtendedOffsetTable,
1713             self.ExtendedOffsetTableLengths) = encapsulate_extended(encoded)
1714        else:
1715            self.PixelData = encapsulate(encoded)
1716
1717        self['PixelData'].is_undefined_length = True
1718
1719        # Set the correct *Transfer Syntax UID*
1720        if not hasattr(self, 'file_meta'):
1721            self.file_meta = FileMetaDataset()
1722
1723        self.file_meta.TransferSyntaxUID = uid
1724
1725        # Add or update any other required elements
1726        if self.SamplesPerPixel > 1:
1727            self.PlanarConfiguration: int = 1 if uid == RLELossless else 0
1728
1729    def decompress(self, handler_name: str = '') -> None:
1730        """Decompresses *Pixel Data* and modifies the :class:`Dataset`
1731        in-place.
1732
1733        .. versionadded:: 1.4
1734
1735            The `handler_name` keyword argument was added
1736
1737        If not a compressed transfer syntax, then pixel data is converted
1738        to a :class:`numpy.ndarray` internally, but not returned.
1739
1740        If compressed pixel data, then is decompressed using an image handler,
1741        and internal state is updated appropriately:
1742
1743        - ``Dataset.file_meta.TransferSyntaxUID`` is updated to non-compressed
1744          form
1745        - :attr:`~pydicom.dataelem.DataElement.is_undefined_length`
1746          is ``False`` for the (7FE0,0010) *Pixel Data* element.
1747
1748        .. versionchanged:: 1.4
1749
1750            The `handler_name` keyword argument was added
1751
1752        Parameters
1753        ----------
1754        handler_name : str, optional
1755            The name of the pixel handler that shall be used to
1756            decode the data. Supported names are: ``'gdcm'``,
1757            ``'pillow'``, ``'jpeg_ls'``, ``'rle'``, ``'numpy'`` and
1758            ``'pylibjpeg'``.
1759            If not used (the default), a matching handler is used from the
1760            handlers configured in :attr:`~pydicom.config.pixel_data_handlers`.
1761
1762        Returns
1763        -------
1764        None
1765
1766        Raises
1767        ------
1768        NotImplementedError
1769            If the pixel data was originally compressed but file is not
1770            *Explicit VR Little Endian* as required by the DICOM Standard.
1771        """
1772        self.convert_pixel_data(handler_name)
1773        self.is_decompressed = True
1774        # May have been undefined length pixel data, but won't be now
1775        if 'PixelData' in self:
1776            self[0x7fe00010].is_undefined_length = False
1777
1778        # Make sure correct Transfer Syntax is set
1779        # According to the dicom standard PS3.5 section A.4,
1780        # all compressed files must have been explicit VR, little endian
1781        # First check if was a compressed file
1782        if (
1783            hasattr(self, 'file_meta')
1784            and self.file_meta.TransferSyntaxUID.is_compressed
1785        ):
1786            # Check that current file as read does match expected
1787            if not self.is_little_endian or self.is_implicit_VR:
1788                msg = ("Current dataset does not match expected ExplicitVR "
1789                       "LittleEndian transfer syntax from a compressed "
1790                       "transfer syntax")
1791                raise NotImplementedError(msg)
1792
1793            # All is as expected, updated the Transfer Syntax
1794            self.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
1795
1796    def overlay_array(self, group: int) -> "numpy.ndarray":
1797        """Return the *Overlay Data* in `group` as a :class:`numpy.ndarray`.
1798
1799        .. versionadded:: 1.4
1800
1801        Parameters
1802        ----------
1803        group : int
1804            The group number of the overlay data.
1805
1806        Returns
1807        -------
1808        numpy.ndarray
1809            The (`group`,3000) *Overlay Data* converted to a
1810            :class:`numpy.ndarray`.
1811        """
1812        if group < 0x6000 or group > 0x60FF:
1813            raise ValueError(
1814                "The group part of the 'Overlay Data' element tag must be "
1815                "between 0x6000 and 0x60FF (inclusive)"
1816            )
1817
1818        from pydicom.config import overlay_data_handlers
1819
1820        available_handlers = [
1821            hh for hh in overlay_data_handlers
1822            if hh.is_available()  # type: ignore[attr-defined]
1823        ]
1824        if not available_handlers:
1825            # For each of the handlers we want to find which
1826            #   dependencies are missing
1827            msg = (
1828                "The following handlers are available to decode the overlay "
1829                "data however they are missing required dependencies: "
1830            )
1831            pkg_msg = []
1832            for hh in overlay_data_handlers:
1833                hh_deps = hh.DEPENDENCIES  # type: ignore[attr-defined]
1834                # Missing packages
1835                missing = [dd for dd in hh_deps if have_package(dd) is None]
1836                # Package names
1837                names = [hh_deps[name][1] for name in missing]
1838                pkg_msg.append(
1839                    f"{hh.HANDLER_NAME} "  # type: ignore[attr-defined]
1840                    f"(req. {', '.join(names)})"
1841                )
1842
1843            raise RuntimeError(msg + ', '.join(pkg_msg))
1844
1845        last_exception = None
1846        for handler in available_handlers:
1847            try:
1848                # Use the handler to get an ndarray of the pixel data
1849                func = handler.get_overlay_array  # type: ignore[attr-defined]
1850                return cast("numpy.ndarray", func(self, group))
1851            except Exception as exc:
1852                logger.debug(
1853                    "Exception raised by overlay data handler", exc_info=exc
1854                )
1855                last_exception = exc
1856
1857        logger.info(
1858            "Unable to decode the overlay data using the following handlers: "
1859            "{}. Please see the list of supported Transfer Syntaxes in the "
1860            "pydicom documentation for alternative packages that might "
1861            "be able to decode the data"
1862            .format(", ".join([str(hh) for hh in available_handlers]))
1863        )
1864
1865        raise last_exception  # type: ignore[misc]
1866
1867    @property
1868    def pixel_array(self) -> "numpy.ndarray":
1869        """Return the pixel data as a :class:`numpy.ndarray`.
1870
1871        .. versionchanged:: 1.4
1872
1873            Added support for *Float Pixel Data* and *Double Float Pixel Data*
1874
1875        Returns
1876        -------
1877        numpy.ndarray
1878            The (7FE0,0008) *Float Pixel Data*, (7FE0,0009) *Double Float
1879            Pixel Data* or (7FE0,0010) *Pixel Data* converted to a
1880            :class:`numpy.ndarray`.
1881        """
1882        self.convert_pixel_data()
1883        return cast("numpy.ndarray", self._pixel_array)
1884
1885    def waveform_array(self, index: int) -> "numpy.ndarray":
1886        """Return an :class:`~numpy.ndarray` for the multiplex group at
1887        `index` in the (5400,0100) *Waveform Sequence*.
1888
1889        .. versionadded:: 2.1
1890
1891        Parameters
1892        ----------
1893        index : int
1894            The index of the multiplex group to return the array for.
1895
1896        Returns
1897        ------
1898        numpy.ndarray
1899            The *Waveform Data* for the multiplex group as an
1900            :class:`~numpy.ndarray` with shape (samples, channels). If
1901            (003A,0210) *Channel Sensitivity* is present
1902            then the values will be in the units specified by the (003A,0211)
1903            *Channel Sensitivity Units Sequence*.
1904
1905        See Also
1906        --------
1907        :func:`~pydicom.waveforms.numpy_handler.generate_multiplex`
1908        :func:`~pydicom.waveforms.numpy_handler.multiplex_array`
1909        """
1910        if not wave_handler.is_available():
1911            raise RuntimeError("The waveform data handler requires numpy")
1912
1913        return wave_handler.multiplex_array(self, index, as_raw=False)
1914
1915    # Format strings spec'd according to python string formatting options
1916    #    See http://docs.python.org/library/stdtypes.html#string-formatting-operations # noqa
1917    default_element_format = "%(tag)s %(name)-35.35s %(VR)s: %(repval)s"
1918    default_sequence_element_format = "%(tag)s %(name)-35.35s %(VR)s: %(repval)s"  # noqa
1919
1920    def formatted_lines(
1921        self,
1922        element_format: str = default_element_format,
1923        sequence_element_format: str = default_sequence_element_format,
1924        indent_format: Optional[str] = None
1925    ) -> Iterator[str]:
1926        """Iterate through the :class:`Dataset` yielding formatted :class:`str`
1927        for each element.
1928
1929        Parameters
1930        ----------
1931        element_format : str
1932            The string format to use for non-sequence elements. Formatting uses
1933            the attributes of
1934            :class:`~pydicom.dataelem.DataElement`. Default is
1935            ``"%(tag)s %(name)-35.35s %(VR)s: %(repval)s"``.
1936        sequence_element_format : str
1937            The string format to use for sequence elements. Formatting uses
1938            the attributes of
1939            :class:`~pydicom.dataelem.DataElement`. Default is
1940            ``"%(tag)s %(name)-35.35s %(VR)s: %(repval)s"``
1941        indent_format : str or None
1942            Placeholder for future functionality.
1943
1944        Yields
1945        ------
1946        str
1947            A string representation of an element.
1948        """
1949        exclusion = ('from_json', 'to_json', 'to_json_dict', 'clear')
1950        for elem in self.iterall():
1951            # Get all the attributes possible for this data element (e.g.
1952            #   gets descriptive text name too)
1953            # This is the dictionary of names that can be used in the format
1954            #   string
1955            elem_dict = {
1956                attr: (
1957                    getattr(elem, attr)() if callable(getattr(elem, attr))
1958                    else getattr(elem, attr)
1959                )
1960                for attr in dir(elem) if not attr.startswith("_")
1961                and attr not in exclusion
1962            }
1963            if elem.VR == "SQ":
1964                yield sequence_element_format % elem_dict
1965            else:
1966                yield element_format % elem_dict
1967
1968    def _pretty_str(
1969        self, indent: int = 0, top_level_only: bool = False
1970    ) -> str:
1971        """Return a string of the DataElements in the Dataset, with indented
1972        levels.
1973
1974        This private method is called by the ``__str__()`` method for handling
1975        print statements or ``str(dataset)``, and the ``__repr__()`` method.
1976        It is also used by ``top()``, therefore the `top_level_only` flag.
1977        This function recurses, with increasing indentation levels.
1978
1979        ..versionchanged:: 2.0
1980
1981            The file meta information is returned in its own section,
1982            if :data:`~pydicom.config.show_file_meta` is ``True`` (default)
1983
1984        Parameters
1985        ----------
1986        indent : int, optional
1987            The indent level offset (default ``0``).
1988        top_level_only : bool, optional
1989            When True, only create a string for the top level elements, i.e.
1990            exclude elements within any Sequences (default ``False``).
1991
1992        Returns
1993        -------
1994        str
1995            A string representation of the Dataset.
1996        """
1997        strings = []
1998        indent_str = self.indent_chars * indent
1999        nextindent_str = self.indent_chars * (indent + 1)
2000
2001        # Display file meta, if configured to do so, and have a non-empty one
2002        if (
2003            hasattr(self, "file_meta") and self.file_meta
2004            and pydicom.config.show_file_meta
2005        ):
2006            strings.append(f"{'Dataset.file_meta ':-<49}")
2007            for elem in self.file_meta:
2008                with tag_in_exception(elem.tag):
2009                    strings.append(indent_str + repr(elem))
2010            strings.append(f"{'':-<49}")
2011
2012        for elem in self:
2013            with tag_in_exception(elem.tag):
2014                if elem.VR == "SQ":  # a sequence
2015                    strings.append(
2016                        f"{indent_str}{str(elem.tag)}  {elem.description()}  "
2017                        f"{len(elem.value)} item(s) ---- "
2018                    )
2019                    if not top_level_only:
2020                        for dataset in elem.value:
2021                            strings.append(dataset._pretty_str(indent + 1))
2022                            strings.append(nextindent_str + "---------")
2023                else:
2024                    strings.append(indent_str + repr(elem))
2025        return "\n".join(strings)
2026
2027    def remove_private_tags(self) -> None:
2028        """Remove all private elements from the :class:`Dataset`."""
2029
2030        def remove_callback(dataset: "Dataset", elem: DataElement) -> None:
2031            """Internal method to use as callback to walk() method."""
2032            if elem.tag.is_private:
2033                # can't del self[tag] - won't be right dataset on recursion
2034                del dataset[elem.tag]
2035
2036        self.walk(remove_callback)
2037
2038    def save_as(
2039        self,
2040        filename: Union[str, "os.PathLike[AnyStr]", BinaryIO],
2041        write_like_original: bool = True
2042    ) -> None:
2043        """Write the :class:`Dataset` to `filename`.
2044
2045        Wrapper for pydicom.filewriter.dcmwrite, passing this dataset to it.
2046        See documentation for that function for details.
2047
2048        See Also
2049        --------
2050        pydicom.filewriter.dcmwrite
2051            Write a DICOM file from a :class:`FileDataset` instance.
2052        """
2053        pydicom.dcmwrite(filename, self, write_like_original)
2054
2055    def ensure_file_meta(self) -> None:
2056        """Create an empty ``Dataset.file_meta`` if none exists.
2057
2058        .. versionadded:: 1.2
2059        """
2060        # Changed in v2.0 so does not re-assign self.file_meta with getattr()
2061        if not hasattr(self, "file_meta"):
2062            self.file_meta = FileMetaDataset()
2063
2064    def fix_meta_info(self, enforce_standard: bool = True) -> None:
2065        """Ensure the file meta info exists and has the correct values
2066        for transfer syntax and media storage UIDs.
2067
2068        .. versionadded:: 1.2
2069
2070        .. warning::
2071
2072            The transfer syntax for ``is_implicit_VR = False`` and
2073            ``is_little_endian = True`` is ambiguous and will therefore not
2074            be set.
2075
2076        Parameters
2077        ----------
2078        enforce_standard : bool, optional
2079            If ``True``, a check for incorrect and missing elements is
2080            performed (see :func:`~validate_file_meta`).
2081        """
2082        self.ensure_file_meta()
2083
2084        if self.is_little_endian and self.is_implicit_VR:
2085            self.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
2086        elif not self.is_little_endian and not self.is_implicit_VR:
2087            self.file_meta.TransferSyntaxUID = ExplicitVRBigEndian
2088        elif not self.is_little_endian and self.is_implicit_VR:
2089            raise NotImplementedError("Implicit VR Big Endian is not a "
2090                                      "supported Transfer Syntax.")
2091
2092        if 'SOPClassUID' in self:
2093            self.file_meta.MediaStorageSOPClassUID = self.SOPClassUID
2094        if 'SOPInstanceUID' in self:
2095            self.file_meta.MediaStorageSOPInstanceUID = self.SOPInstanceUID
2096        if enforce_standard:
2097            validate_file_meta(self.file_meta, enforce_standard=True)
2098
2099    def __setattr__(self, name: str, value: Any) -> None:
2100        """Intercept any attempts to set a value for an instance attribute.
2101
2102        If name is a DICOM keyword, set the corresponding tag and DataElement.
2103        Else, set an instance (python) attribute as any other class would do.
2104
2105        Parameters
2106        ----------
2107        name : str
2108            The keyword for the element you wish to add/change. If
2109            `name` is not a DICOM element keyword then this will be the
2110            name of the attribute to be added/changed.
2111        value
2112            The value for the attribute to be added/changed.
2113        """
2114        tag = tag_for_keyword(name)
2115        if tag is not None:  # successfully mapped name to a tag
2116            if tag not in self:
2117                # don't have this tag yet->create the data_element instance
2118                VR = dictionary_VR(tag)
2119                data_element = DataElement(tag, VR, value)
2120                if VR == 'SQ':
2121                    # let a sequence know its parent dataset to pass it
2122                    # to its items, who may need parent dataset tags
2123                    # to resolve ambiguous tags
2124                    data_element.parent = self
2125            else:
2126                # already have this data_element, just changing its value
2127                data_element = self[tag]
2128                data_element.value = value
2129            # Now have data_element - store it in this dict
2130            self[tag] = data_element
2131        elif repeater_has_keyword(name):
2132            # Check if `name` is repeaters element
2133            raise ValueError(
2134                f"'{name}' is a DICOM repeating group element and must be "
2135                "added using the add() or add_new() methods."
2136            )
2137        elif name == "file_meta":
2138            self._set_file_meta(value)
2139        else:
2140            # Warn if `name` is camel case but not a keyword
2141            if _RE_CAMEL_CASE.match(name):
2142                msg = (
2143                    f"Camel case attribute '{name}' used which is not in the "
2144                    "element keyword data dictionary"
2145                )
2146                if config.INVALID_KEYWORD_BEHAVIOR == "WARN":
2147                    warnings.warn(msg)
2148                elif config.INVALID_KEYWORD_BEHAVIOR == "RAISE":
2149                    raise ValueError(msg)
2150
2151            # name not in dicom dictionary - setting a non-dicom instance
2152            # attribute
2153            # XXX note if user mis-spells a dicom data_element - no error!!!
2154            object.__setattr__(self, name, value)
2155
2156    def _set_file_meta(self, value: Optional["Dataset"]) -> None:
2157        if value is not None and not isinstance(value, FileMetaDataset):
2158            if config._use_future:
2159                raise TypeError(
2160                    "Pydicom Future: Dataset.file_meta must be an instance "
2161                    "of FileMetaDataset"
2162                )
2163
2164            FileMetaDataset.validate(value)
2165            warnings.warn(
2166                "Starting in pydicom 3.0, Dataset.file_meta must be a "
2167                "FileMetaDataset class instance",
2168                DeprecationWarning
2169            )
2170
2171        self.__dict__["file_meta"] = value
2172
2173    def __setitem__(
2174        self, key: Union[slice, TagType], elem: _DatasetValue
2175    ) -> None:
2176        """Operator for ``Dataset[key] = elem``.
2177
2178        Parameters
2179        ----------
2180        key : int or Tuple[int, int] or str
2181            The tag for the element to be added to the :class:`Dataset`.
2182        elem : dataelem.DataElement or dataelem.RawDataElement
2183            The element to add to the :class:`Dataset`.
2184
2185        Raises
2186        ------
2187        NotImplementedError
2188            If `key` is a :class:`slice`.
2189        ValueError
2190            If the `key` value doesn't match the corresponding
2191            :attr:`DataElement.tag<pydicom.dataelem.tag>`.
2192        """
2193        if isinstance(key, slice):
2194            raise NotImplementedError(
2195                'Slicing is not supported when setting Dataset items'
2196            )
2197
2198        try:
2199            key = Tag(key)
2200        except Exception as exc:
2201            raise ValueError(
2202                f"Unable to convert the key '{key}' to an element tag"
2203            ) from exc
2204
2205        if not isinstance(elem, (DataElement, RawDataElement)):
2206            raise TypeError("Dataset items must be 'DataElement' instances")
2207
2208        if isinstance(elem.tag, BaseTag):
2209            elem_tag = elem.tag
2210        else:
2211            elem_tag = Tag(elem.tag)
2212
2213        if key != elem_tag:
2214            raise ValueError(
2215                f"The key '{key}' doesn't match the 'DataElement' tag "
2216                f"'{elem_tag}'"
2217            )
2218
2219        if elem_tag.is_private:
2220            # See PS 3.5-2008 section 7.8.1 (p. 44) for how blocks are reserved
2221            logger.debug(f"Setting private tag {elem_tag}")
2222            private_block = elem_tag.element >> 8
2223            private_creator_tag = Tag(elem_tag.group, private_block)
2224            if private_creator_tag in self and elem_tag != private_creator_tag:
2225                if isinstance(elem, RawDataElement):
2226                    elem = DataElement_from_raw(
2227                        elem, self._character_set, self
2228                    )
2229                elem.private_creator = self[private_creator_tag].value
2230
2231        self._dict[elem_tag] = elem
2232
2233    def _slice_dataset(
2234        self,
2235        start: Optional[TagType],
2236        stop: Optional[TagType],
2237        step: Optional[int]
2238    ) -> List[BaseTag]:
2239        """Return the element tags in the Dataset that match the slice.
2240
2241        Parameters
2242        ----------
2243        start : int or 2-tuple of int or None
2244            The slice's starting element tag value, in any format accepted by
2245            :func:`~pydicom.tag.Tag`.
2246        stop : int or 2-tuple of int or None
2247            The slice's stopping element tag value, in any format accepted by
2248            :func:`~pydicom.tag.Tag`.
2249        step : int or None
2250            The slice's step size.
2251
2252        Returns
2253        ------
2254        list of BaseTag
2255            The tags in the :class:`Dataset` that meet the conditions of the
2256            slice.
2257        """
2258        # Check the starting/stopping Tags are valid when used
2259        if start is not None:
2260            start = Tag(start)
2261        if stop is not None:
2262            stop = Tag(stop)
2263
2264        all_tags = sorted(self._dict.keys())
2265        # If the Dataset is empty, return an empty list
2266        if not all_tags:
2267            return []
2268
2269        # Special case the common situations:
2270        #   - start and/or stop are None
2271        #   - step is 1
2272
2273        if start is None:
2274            if stop is None:
2275                # For step=1 avoid copying the list
2276                return all_tags if step == 1 else all_tags[::step]
2277            else:  # Have a stop value, get values until that point
2278                step1_list = list(takewhile(lambda x: x < stop, all_tags))
2279                return step1_list if step == 1 else step1_list[::step]
2280
2281        # Have a non-None start value.  Find its index
2282        i_start = bisect_left(all_tags, start)
2283        if stop is None:
2284            return all_tags[i_start::step]
2285
2286        i_stop = bisect_left(all_tags, stop)
2287        return all_tags[i_start:i_stop:step]
2288
2289    def __str__(self) -> str:
2290        """Handle str(dataset).
2291
2292        ..versionchanged:: 2.0
2293
2294            The file meta information was added in its own section,
2295            if :data:`pydicom.config.show_file_meta` is ``True``
2296
2297        """
2298        return self._pretty_str()
2299
2300    def top(self) -> str:
2301        """Return a :class:`str` representation of the top level elements. """
2302        return self._pretty_str(top_level_only=True)
2303
2304    def trait_names(self) -> List[str]:
2305        """Return a :class:`list` of valid names for auto-completion code.
2306
2307        Used in IPython, so that data element names can be found and offered
2308        for autocompletion on the IPython command line.
2309        """
2310        return dir(self)
2311
2312    def update(self, d: _DatasetType) -> None:
2313        """Extend :meth:`dict.update` to handle DICOM tags and keywords.
2314
2315        Parameters
2316        ----------
2317        dictionary : dict or Dataset
2318            The :class:`dict` or :class:`Dataset` to use when updating the
2319            current object.
2320        """
2321        for key, value in list(d.items()):
2322            if isinstance(key, str):
2323                setattr(self, key, value)
2324            else:
2325                self[Tag(cast(int, key))] = value
2326
2327    def iterall(self) -> Iterator[DataElement]:
2328        """Iterate through the :class:`Dataset`, yielding all the elements.
2329
2330        Unlike ``iter(Dataset)``, this *does* recurse into sequences,
2331        and so yields all elements as if dataset were "flattened".
2332
2333        Yields
2334        ------
2335        dataelem.DataElement
2336        """
2337        for elem in self:
2338            yield elem
2339            if elem.VR == "SQ":
2340                for ds in elem.value:
2341                    yield from ds.iterall()
2342
2343    def walk(
2344        self,
2345        callback: Callable[["Dataset", DataElement], None],
2346        recursive: bool = True
2347    ) -> None:
2348        """Iterate through the :class:`Dataset's<Dataset>` elements and run
2349        `callback` on each.
2350
2351        Visit all elements in the :class:`Dataset`, possibly recursing into
2352        sequences and their items. The `callback` function is called for each
2353        :class:`~pydicom.dataelem.DataElement` (including elements
2354        with a VR of 'SQ'). Can be used to perform an operation on certain
2355        types of elements.
2356
2357        For example,
2358        :meth:`~Dataset.remove_private_tags` finds all elements with private
2359        tags and deletes them.
2360
2361        The elements will be returned in order of increasing tag number within
2362        their current :class:`Dataset`.
2363
2364        Parameters
2365        ----------
2366        callback
2367            A callable function that takes two arguments:
2368
2369            * a :class:`Dataset`
2370            * a :class:`~pydicom.dataelem.DataElement` belonging
2371              to that :class:`Dataset`
2372
2373        recursive : bool, optional
2374            Flag to indicate whether to recurse into sequences (default
2375            ``True``).
2376        """
2377        taglist = sorted(self._dict.keys())
2378        for tag in taglist:
2379
2380            with tag_in_exception(tag):
2381                data_element = self[tag]
2382                callback(self, data_element)  # self = this Dataset
2383                # 'tag in self' below needed in case callback deleted
2384                # data_element
2385                if recursive and tag in self and data_element.VR == "SQ":
2386                    sequence = data_element.value
2387                    for dataset in sequence:
2388                        dataset.walk(callback)
2389
2390    @classmethod
2391    def from_json(
2392        cls: Type["Dataset"],
2393        json_dataset: Union[Dict[str, Any], str, bytes, bytearray],
2394        bulk_data_uri_handler: Optional[
2395            Union[
2396                Callable[[str, str, str], Union[None, str, int, float, bytes]],
2397                Callable[[str], Union[None, str, int, float, bytes]]
2398            ]
2399        ] = None
2400    ) -> "Dataset":
2401        """Return a :class:`Dataset` from a DICOM JSON Model object.
2402
2403        .. versionadded:: 1.3
2404
2405        See the DICOM Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`.
2406
2407        Parameters
2408        ----------
2409        json_dataset : dict, str, bytes or bytearray
2410            :class:`dict`, :class:`str`, :class:`bytes` or :class:`bytearray`
2411            representing a DICOM Data Set formatted based on the :dcm:`DICOM
2412            JSON Model<part18/chapter_F.html>`.
2413        bulk_data_uri_handler : callable, optional
2414            Callable function that accepts either the tag, vr and
2415            "BulkDataURI" value or just the "BulkDataURI" value of the JSON
2416            representation of a data element and returns the actual value of
2417            that data element (retrieved via DICOMweb WADO-RS). If no
2418            `bulk_data_uri_handler` is specified (default) then the
2419            corresponding element will have an "empty" value such as
2420            ``""``, ``b""`` or ``None`` depending on the `vr` (i.e. the
2421            Value Multiplicity will be 0).
2422
2423        Returns
2424        -------
2425        Dataset
2426        """
2427        if isinstance(json_dataset, (str, bytes, bytearray)):
2428            json_dataset = cast(Dict[str, Any], json.loads(json_dataset))
2429
2430        dataset = cls()
2431        for tag, mapping in json_dataset.items():
2432            # `tag` is an element tag in uppercase hex format as a str
2433            # `mapping` is Dict[str, Any] and should have keys 'vr' and at most
2434            #   one of ('Value', 'BulkDataURI', 'InlineBinary') but may have
2435            #   none of those if the element's VM is 0
2436            vr = mapping['vr']
2437            unique_value_keys = tuple(
2438                set(mapping.keys()) & set(jsonrep.JSON_VALUE_KEYS)
2439            )
2440            if len(unique_value_keys) == 0:
2441                value_key = None
2442                value = ['']
2443            else:
2444                value_key = unique_value_keys[0]
2445                value = mapping[value_key]
2446            data_element = DataElement.from_json(
2447                cls, tag, vr, value, value_key, bulk_data_uri_handler
2448            )
2449            dataset.add(data_element)
2450        return dataset
2451
2452    def to_json_dict(
2453        self,
2454        bulk_data_threshold: int = 1024,
2455        bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None,  # noqa
2456        suppress_invalid_tags: bool = False,
2457    ) -> Dict[str, Any]:
2458        """Return a dictionary representation of the :class:`Dataset`
2459        conforming to the DICOM JSON Model as described in the DICOM
2460        Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`.
2461
2462        .. versionadded:: 1.4
2463
2464        Parameters
2465        ----------
2466        bulk_data_threshold : int, optional
2467            Threshold for the length of a base64-encoded binary data element
2468            above which the element should be considered bulk data and the
2469            value provided as a URI rather than included inline (default:
2470            ``1024``). Ignored if no bulk data handler is given.
2471        bulk_data_element_handler : callable, optional
2472            Callable function that accepts a bulk data element and returns a
2473            JSON representation of the data element (dictionary including the
2474            "vr" key and either the "InlineBinary" or the "BulkDataURI" key).
2475        suppress_invalid_tags : bool, optional
2476            Flag to specify if errors while serializing tags should be logged
2477            and the tag dropped or if the error should be bubbled up.
2478
2479        Returns
2480        -------
2481        dict
2482            :class:`Dataset` representation based on the DICOM JSON Model.
2483        """
2484        json_dataset = {}
2485        for key in self.keys():
2486            json_key = '{:08X}'.format(key)
2487            data_element = self[key]
2488            try:
2489                json_dataset[json_key] = data_element.to_json_dict(
2490                    bulk_data_element_handler=bulk_data_element_handler,
2491                    bulk_data_threshold=bulk_data_threshold
2492                )
2493            except Exception as exc:
2494                logger.error(f"Error while processing tag {json_key}")
2495                if not suppress_invalid_tags:
2496                    raise exc
2497
2498        return json_dataset
2499
2500    def to_json(
2501        self,
2502        bulk_data_threshold: int = 1024,
2503        bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None,  # noqa
2504        dump_handler: Optional[Callable[[Dict[str, Any]], str]] = None,
2505        suppress_invalid_tags: bool = False,
2506    ) -> str:
2507        """Return a JSON representation of the :class:`Dataset`.
2508
2509        .. versionadded:: 1.3
2510
2511        See the DICOM Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`.
2512
2513        Parameters
2514        ----------
2515        bulk_data_threshold : int, optional
2516            Threshold for the length of a base64-encoded binary data element
2517            above which the element should be considered bulk data and the
2518            value provided as a URI rather than included inline (default:
2519            ``1024``). Ignored if no bulk data handler is given.
2520        bulk_data_element_handler : callable, optional
2521            Callable function that accepts a bulk data element and returns a
2522            JSON representation of the data element (dictionary including the
2523            "vr" key and either the "InlineBinary" or the "BulkDataURI" key).
2524        dump_handler : callable, optional
2525            Callable function that accepts a :class:`dict` and returns the
2526            serialized (dumped) JSON string (by default uses
2527            :func:`json.dumps`).
2528
2529            .. note:
2530
2531                Make sure to use a dump handler that sorts the keys (see
2532                example below) to create DICOM-conformant JSON.
2533        suppress_invalid_tags : bool, optional
2534            Flag to specify if errors while serializing tags should be logged
2535            and the tag dropped or if the error should be bubbled up.
2536
2537        Returns
2538        -------
2539        str
2540            :class:`Dataset` serialized into a string based on the DICOM JSON
2541            Model.
2542
2543        Examples
2544        --------
2545        >>> def my_json_dumps(data):
2546        ...     return json.dumps(data, indent=4, sort_keys=True)
2547        >>> ds.to_json(dump_handler=my_json_dumps)
2548        """
2549        if dump_handler is None:
2550            def json_dump(d: Any) -> str:
2551                return json.dumps(d, sort_keys=True)
2552
2553            dump_handler = json_dump
2554
2555        return dump_handler(
2556            self.to_json_dict(
2557                bulk_data_threshold,
2558                bulk_data_element_handler,
2559                suppress_invalid_tags=suppress_invalid_tags
2560            )
2561        )
2562
2563    def __getstate__(self) -> Dict[str, Any]:
2564        # pickle cannot handle weakref - remove parent
2565        d = self.__dict__.copy()
2566        del d['parent']
2567        return d
2568
2569    def __setstate__(self, state: Dict[str, Any]) -> None:
2570        self.__dict__.update(state)
2571        # re-add parent - it will be set to the parent dataset on demand
2572        # if the dataset is in a sequence
2573        self.__dict__['parent'] = None
2574
2575    __repr__ = __str__
2576
2577
2578_FileDataset = TypeVar("_FileDataset", bound="FileDataset")
2579
2580
2581class FileDataset(Dataset):
2582    """An extension of :class:`Dataset` to make reading and writing to
2583    file-like easier.
2584
2585    Attributes
2586    ----------
2587    preamble : str or bytes or None
2588        The optional DICOM preamble prepended to the :class:`FileDataset`, if
2589        available.
2590    file_meta : FileMetaDataset or None
2591        The Dataset's file meta information as a :class:`FileMetaDataset`,
2592        if available (``None`` if not present).
2593        Consists of group ``0x0002`` elements.
2594    filename : str or None
2595        The filename that the :class:`FileDataset` was read from (if read from
2596        file) or ``None`` if the filename is not available (if read from a
2597        :class:`io.BytesIO` or  similar).
2598    fileobj_type
2599        The object type of the file-like the :class:`FileDataset` was read
2600        from.
2601    is_implicit_VR : bool
2602        ``True`` if the dataset encoding is implicit VR, ``False`` otherwise.
2603    is_little_endian : bool
2604        ``True`` if the dataset encoding is little endian byte ordering,
2605        ``False`` otherwise.
2606    timestamp : float or None
2607        The modification time of the file the :class:`FileDataset` was read
2608        from, ``None`` if the modification time is not available.
2609    """
2610
2611    def __init__(
2612        self,
2613        filename_or_obj: Union[str, "os.PathLike[AnyStr]", BinaryIO],
2614        dataset: _DatasetType,
2615        preamble: Optional[bytes] = None,
2616        file_meta: Optional["FileMetaDataset"] = None,
2617        is_implicit_VR: bool = True,
2618        is_little_endian: bool = True
2619    ) -> None:
2620        """Initialize a :class:`FileDataset` read from a DICOM file.
2621
2622        Parameters
2623        ----------
2624        filename_or_obj : str or PathLike or BytesIO or None
2625            Full path and filename to the file, memory buffer object, or
2626            ``None`` if is a :class:`io.BytesIO`.
2627        dataset : Dataset or dict
2628            Some form of dictionary, usually a :class:`Dataset` returned from
2629            :func:`~pydicom.filereader.dcmread`.
2630        preamble : bytes or str, optional
2631            The 128-byte DICOM preamble.
2632        file_meta : FileMetaDataset, optional
2633            The file meta :class:`FileMetaDataset`, such as the one returned by
2634            :func:`~pydicom.filereader.read_file_meta_info`, or an empty
2635            :class:`FileMetaDataset` if no file meta information is in the
2636            file.
2637        is_implicit_VR : bool, optional
2638            ``True`` (default) if implicit VR transfer syntax used; ``False``
2639            if explicit VR.
2640        is_little_endian : bool
2641            ``True`` (default) if little-endian transfer syntax used; ``False``
2642            if big-endian.
2643        """
2644        Dataset.__init__(self, dataset)
2645        self.preamble = preamble
2646        self.file_meta: "FileMetaDataset" = (
2647            file_meta if file_meta is not None else FileMetaDataset()
2648        )
2649        self.is_implicit_VR: bool = is_implicit_VR
2650        self.is_little_endian: bool = is_little_endian
2651
2652        filename: Optional[str] = None
2653        filename_or_obj = path_from_pathlike(filename_or_obj)
2654        self.fileobj_type: Any
2655        self.filename: Union[str, BinaryIO]
2656
2657        if isinstance(filename_or_obj, str):
2658            filename = filename_or_obj
2659            self.fileobj_type = open
2660        elif isinstance(filename_or_obj, io.BufferedReader):
2661            filename = filename_or_obj.name
2662            # This is the appropriate constructor for io.BufferedReader
2663            self.fileobj_type = open
2664        else:
2665            # use __class__ python <2.7?;
2666            # http://docs.python.org/reference/datamodel.html
2667            self.fileobj_type = filename_or_obj.__class__
2668            if hasattr(filename_or_obj, "name"):
2669                filename = filename_or_obj.name
2670            elif hasattr(filename_or_obj, "filename"):
2671                filename = (
2672                    filename_or_obj.filename  # type: ignore[attr-defined]
2673                )
2674            else:
2675                # e.g. came from BytesIO or something file-like
2676                self.filename = filename_or_obj
2677
2678        self.timestamp = None
2679        if filename:
2680            self.filename = filename
2681            if os.path.exists(filename):
2682                statinfo = os.stat(filename)
2683                self.timestamp = statinfo.st_mtime
2684
2685    def _copy_implementation(self, copy_function: Callable) -> "FileDataset":
2686        """Implementation of ``__copy__`` and ``__deepcopy__``.
2687        Sets the filename to ``None`` if it isn't a string,
2688        and copies all other attributes using `copy_function`.
2689        """
2690        copied = self.__class__(
2691            self.filename, self, self.preamble, self.file_meta,
2692            self.is_implicit_VR, self.is_little_endian
2693        )
2694        filename = self.filename
2695        if filename is not None and not isinstance(filename, str):
2696            warnings.warn("The 'filename' attribute of the dataset is a "
2697                          "file-like object and will be set to None "
2698                          "in the copied object")
2699            self.filename = None  # type: ignore[assignment]
2700        for (k, v) in self.__dict__.items():
2701            copied.__dict__[k] = copy_function(v)
2702
2703        self.filename = filename
2704
2705        return copied
2706
2707    def __copy__(self) -> "FileDataset":
2708        """Return a shallow copy of the file dataset.
2709        Make sure that the filename is not copied in case it is a file-like
2710        object.
2711
2712        Returns
2713        -------
2714        FileDataset
2715            A shallow copy of the file data set.
2716        """
2717        return self._copy_implementation(copy.copy)
2718
2719    def __deepcopy__(self, _: Optional[Dict[int, Any]]) -> "FileDataset":
2720        """Return a deep copy of the file dataset.
2721        Make sure that the filename is not copied in case it is a file-like
2722        object.
2723
2724        Returns
2725        -------
2726        FileDataset
2727            A deep copy of the file data set.
2728        """
2729        return self._copy_implementation(copy.deepcopy)
2730
2731
2732def validate_file_meta(
2733    file_meta: "FileMetaDataset", enforce_standard: bool = True
2734) -> None:
2735    """Validate the *File Meta Information* elements in `file_meta`.
2736
2737    .. versionchanged:: 1.2
2738
2739        Moved from :mod:`pydicom.filewriter`.
2740
2741    Parameters
2742    ----------
2743    file_meta : Dataset
2744        The *File Meta Information* data elements.
2745    enforce_standard : bool, optional
2746        If ``False``, then only a check for invalid elements is performed.
2747        If ``True`` (default), the following elements will be added if not
2748        already present:
2749
2750        * (0002,0001) *File Meta Information Version*
2751        * (0002,0012) *Implementation Class UID*
2752        * (0002,0013) *Implementation Version Name*
2753
2754        and the following elements will be checked:
2755
2756        * (0002,0002) *Media Storage SOP Class UID*
2757        * (0002,0003) *Media Storage SOP Instance UID*
2758        * (0002,0010) *Transfer Syntax UID*
2759
2760    Raises
2761    ------
2762    ValueError
2763        If `enforce_standard` is ``True`` and any of the checked *File Meta
2764        Information* elements are missing from `file_meta`.
2765    ValueError
2766        If any non-Group 2 Elements are present in `file_meta`.
2767    """
2768    # Check that no non-Group 2 Elements are present
2769    for elem in file_meta.elements():
2770        if elem.tag.group != 0x0002:
2771            raise ValueError("Only File Meta Information Group (0002,eeee) "
2772                             "elements must be present in 'file_meta'.")
2773
2774    if enforce_standard:
2775        if 'FileMetaInformationVersion' not in file_meta:
2776            file_meta.FileMetaInformationVersion = b'\x00\x01'
2777
2778        if 'ImplementationClassUID' not in file_meta:
2779            file_meta.ImplementationClassUID = UID(PYDICOM_IMPLEMENTATION_UID)
2780
2781        if 'ImplementationVersionName' not in file_meta:
2782            file_meta.ImplementationVersionName = (
2783                'PYDICOM ' + ".".join(str(x) for x in __version_info__))
2784
2785        # Check that required File Meta Information elements are present
2786        missing = []
2787        for element in [0x0002, 0x0003, 0x0010]:
2788            if Tag(0x0002, element) not in file_meta:
2789                missing.append(Tag(0x0002, element))
2790        if missing:
2791            msg = ("Missing required File Meta Information elements from "
2792                   "'file_meta':\n")
2793            for tag in missing:
2794                msg += '\t{0} {1}\n'.format(tag, keyword_for_tag(tag))
2795            raise ValueError(msg[:-1])  # Remove final newline
2796
2797
2798class FileMetaDataset(Dataset):
2799    """Contains a collection (dictionary) of group 2 DICOM Data Elements.
2800
2801    .. versionadded:: 2.0
2802
2803    Derived from :class:`~pydicom.dataset.Dataset`, but only allows
2804    Group 2 (File Meta Information) data elements
2805    """
2806
2807    def __init__(self, *args: _DatasetType, **kwargs: Any) -> None:
2808        """Initialize a FileMetaDataset
2809
2810        Parameters are as per :class:`Dataset`; this overrides the super class
2811        only to check that all are group 2 data elements
2812
2813        Raises
2814        ------
2815        ValueError
2816            If any data elements are not group 2.
2817        TypeError
2818            If the passed argument is not a :class:`dict` or :class:`Dataset`
2819        """
2820        super().__init__(*args, **kwargs)
2821        FileMetaDataset.validate(self._dict)
2822
2823        # Set type hints for the possible contents - VR, Type (1|1C|3)
2824        self.FileMetaInformationGroupLength: int  # UL, 1
2825        self.FileMetaInformationVersion: bytes  # OB, 1
2826        self.MediaStorageSOPClassUID: UID  # UI, 1
2827        self.MediaStorageSOPInstanceUID: UID  # UI, 1
2828        self.TransferSyntaxUID: UID  # UI, 1
2829        self.ImplementationClassUID: UID  # UI, 1
2830        self.ImplementationVersionName: Optional[str]  # SH, 3
2831        self.SourceApplicationEntityTitle: Optional[str]  # AE, 3
2832        self.SendingApplicationEntityTitle: Optional[str]  # AE, 3
2833        self.ReceivingApplicationEntityTitle: Optional[str]  # AE, 3
2834        self.SourcePresentationAddress: Optional[str]  # UR, 3
2835        self.ReceivingPresentationAddress: Optional[str]  # UR, 3
2836        self.PrivateInformationCreatorUID: Optional[UID]  # UI, 3
2837        self.PrivateInformation: bytes  # OB, 1C
2838
2839    @staticmethod
2840    def validate(init_value: _DatasetType) -> None:
2841        """Raise errors if initialization value is not acceptable for file_meta
2842
2843        Parameters
2844        ----------
2845        init_value: dict or Dataset
2846            The tag:data element pairs to initialize a file meta dataset
2847
2848        Raises
2849        ------
2850        TypeError
2851            If the passed argument is not a :class:`dict` or :class:`Dataset`
2852        ValueError
2853            If any data elements passed are not group 2.
2854        """
2855        if init_value is None:
2856            return
2857
2858        if not isinstance(init_value, (Dataset, dict)):
2859            raise TypeError(
2860                "Argument must be a dict or Dataset, not {}".format(
2861                    type(init_value)
2862                )
2863            )
2864
2865        non_group2 = [
2866            Tag(tag) for tag in init_value.keys() if Tag(tag).group != 2
2867        ]
2868        if non_group2:
2869            msg = "Attempted to set non-group 2 elements: {}"
2870            raise ValueError(msg.format(non_group2))
2871
2872    def __setitem__(
2873        self, key: Union[slice, TagType], value: _DatasetValue
2874    ) -> None:
2875        """Override parent class to only allow setting of group 2 elements.
2876
2877        Parameters
2878        ----------
2879        key : int or Tuple[int, int] or str
2880            The tag for the element to be added to the Dataset.
2881        value : dataelem.DataElement or dataelem.RawDataElement
2882            The element to add to the :class:`FileMetaDataset`.
2883
2884        Raises
2885        ------
2886        ValueError
2887            If `key` is not a DICOM Group 2 tag.
2888        """
2889
2890        if isinstance(value.tag, BaseTag):
2891            tag = value.tag
2892        else:
2893            tag = Tag(value.tag)
2894
2895        if tag.group != 2:
2896            raise ValueError(
2897                "Only group 2 data elements are allowed in a FileMetaDataset"
2898            )
2899
2900        super().__setitem__(key, value)
2901
2902
2903_RE_CAMEL_CASE = re.compile(
2904    # Ensure mix of upper and lowercase and digits, no underscores
2905    # If first character is lowercase ensure at least one uppercase char
2906    "(?P<start>(^[A-Za-z])((?=.+?[A-Z])[A-Za-z0-9]+)|(^[A-Z])([A-Za-z0-9]+))"
2907    "(?P<last>[A-Za-z0-9][^_]$)"  # Last character is alphanumeric
2908)
2909