1# Copyright 2008-2018 pydicom authors. See LICENSE file for details.
2"""Pydicom configuration options."""
3
4# doc strings following items are picked up by sphinx for documentation
5
6import logging
7import os
8from typing import Optional, Dict, Any, TYPE_CHECKING
9
10have_numpy = True
11try:
12    import numpy
13except ImportError:
14    have_numpy = False
15
16if TYPE_CHECKING:  # pragma: no cover
17    from pydicom.dataelem import RawDataElement
18    from typing import Protocol
19
20    class ElementCallback(Protocol):
21        def __call__(
22            self,
23            raw_elem: "RawDataElement",
24            **kwargs: Any,
25        ) -> "RawDataElement": ...
26
27
28# Set the type used to hold DS values
29#    default False; was decimal-based in pydicom 0.9.7
30use_DS_decimal = False
31"""Set using :func:`~pydicom.config.DS_decimal` to control if elements with a
32VR of **DS** are represented as :class:`~decimal.Decimal`.
33
34Default ``False``.
35"""
36
37
38data_element_callback: Optional["ElementCallback"] = None
39"""Set to a callable function to be called from
40:func:`~pydicom.filereader.dcmread` every time a
41:class:`~pydicom.dataelem.RawDataElement` has been returned,
42before it is added to the :class:`~pydicom.dataset.Dataset`.
43
44Default ``None``.
45"""
46
47data_element_callback_kwargs: Dict[str, Any] = {}
48"""Set the keyword arguments passed to :func:`data_element_callback`.
49
50Default ``{}``.
51"""
52
53
54def reset_data_element_callback() -> None:
55    """Reset the :func:`data_element_callback` function to the default."""
56    global data_element_callback
57    global data_element_callback_kwargs
58    data_element_callback = None
59    data_element_callback_kwargs = {}
60
61
62def DS_numpy(use_numpy: bool = True) -> None:
63    """Set whether multi-valued elements with VR of **DS** will be numpy arrays
64
65    .. versionadded:: 2.0
66
67    Parameters
68    ----------
69    use_numpy : bool, optional
70        ``True`` (default) to read multi-value **DS** elements
71        as :class:`~numpy.ndarray`, ``False`` to read multi-valued **DS**
72        data elements as type :class:`~python.mulitval.MultiValue`
73
74        Note: once a value has been accessed, changing this setting will
75        no longer change its type
76
77    Raises
78    ------
79    ValueError
80        If :data:`use_DS_decimal` and `use_numpy` are both True.
81
82    """
83
84    global use_DS_numpy
85
86    if use_DS_decimal and use_numpy:
87        raise ValueError(
88            "Cannot use numpy arrays to read DS elements"
89            "if `use_DS_decimal` is True"
90        )
91    use_DS_numpy = use_numpy
92
93
94def DS_decimal(use_Decimal_boolean: bool = True) -> None:
95    """Set DS class to be derived from :class:`decimal.Decimal` or
96    :class:`float`.
97
98    If this function is never called, the default in *pydicom* >= 0.9.8
99    is for DS to be based on :class:`float`.
100
101    Parameters
102    ----------
103    use_Decimal_boolean : bool, optional
104        ``True`` (default) to derive :class:`~pydicom.valuerep.DS` from
105        :class:`decimal.Decimal`, ``False`` to derive it from :class:`float`.
106
107    Raises
108    ------
109    ValueError
110        If `use_Decimal_boolean` and :data:`use_DS_numpy` are
111        both ``True``.
112    """
113    global use_DS_decimal
114
115    use_DS_decimal = use_Decimal_boolean
116
117    if use_DS_decimal and use_DS_numpy:
118        raise ValueError(
119            "Cannot set use_DS_decimal True " "if use_DS_numpy is True"
120        )
121
122    import pydicom.valuerep
123
124    if use_DS_decimal:
125        pydicom.valuerep.DSclass = pydicom.valuerep.DSdecimal
126    else:
127        pydicom.valuerep.DSclass = pydicom.valuerep.DSfloat
128
129
130# Configuration flags
131use_DS_numpy = False
132"""Set using the function :func:`~pydicom.config.DS_numpy` to control
133whether arrays of VR **DS** are returned as numpy arrays.
134Default: ``False``.
135
136.. versionadded:: 2.0
137"""
138
139use_IS_numpy = False
140"""Set to False to avoid IS values being returned as numpy ndarray objects.
141Default: ``False``.
142
143.. versionadded:: 2.0
144"""
145
146allow_DS_float = False
147"""Set to ``True`` to allow :class:`~pydicom.valuerep.DSdecimal`
148instances to be created using :class:`floats<float>`; otherwise, they must be
149explicitly converted to :class:`str`, with the user explicitly setting the
150precision of digits and rounding.
151
152Default ``False``.
153"""
154
155enforce_valid_values = False
156"""Raise exceptions if any value is not allowed by DICOM Standard.
157
158e.g. DS strings that are longer than 16 characters; IS strings outside
159the allowed range.
160
161Default ``False``.
162"""
163
164convert_wrong_length_to_UN = False
165"""Convert a field VR to "UN" and return bytes if bytes length is invalid.
166Default ``False``.
167"""
168
169datetime_conversion = False
170"""Set to ``True`` to convert the value(s) of elements with a VR of DA, DT and
171TM to :class:`datetime.date`, :class:`datetime.datetime` and
172:class:`datetime.time` respectively.
173Note that when datetime conversion is enabled then range matching in
174C-GET/C-FIND/C-MOVE queries is not possible anymore. So if you need range
175matching we recommend to do the conversion manually.
176
177Default ``False``
178
179References
180----------
181* :dcm:`Range Matching<part04/sect_C.2.2.2.5.html>`
182"""
183
184use_none_as_empty_text_VR_value = False
185""" If ``True``, the value of a decoded empty data element with
186a text VR is ``None``, otherwise (the default), it is is an empty string.
187For all other VRs the behavior does not change - the value is en empty
188list for VR **SQ** and ``None`` for all other VRs.
189Note that the default of this value may change to ``True`` in a later version.
190
191.. versionadded:: 1.4
192"""
193
194replace_un_with_known_vr = True
195""" If ``True``, and the VR of a known data element is encoded as **UN** in
196an explicit encoding, the VR is changed to the known value.
197Can be set to ``False`` where the content of the tag shown as **UN** is
198not DICOM conformant and would lead to a failure if accessing it.
199
200.. versionadded:: 2.0
201"""
202
203show_file_meta = True
204"""
205.. versionadded:: 2.0
206
207If ``True`` (default), the 'str' and 'repr' methods
208of :class:`~pydicom.dataset.Dataset` begin with a separate section
209displaying the file meta information data elements
210"""
211
212# Logging system and debug function to change logging level
213logger = logging.getLogger("pydicom")
214logger.addHandler(logging.NullHandler())
215
216import pydicom.overlays.numpy_handler as overlay_np  # noqa
217
218overlay_data_handlers = [overlay_np]
219"""Handlers for converting (60xx,3000) *Overlay Data*
220
221.. versionadded:: 1.4
222
223.. deprecated:: 2.1
224
225.. currentmodule:: pydicom.dataset
226
227This is an ordered list of *Overlay Data* handlers that the
228:meth:`~Dataset.overlay_array` method will use to try to extract a correctly
229sized numpy array from an *Overlay Data* element.
230
231Handlers have two required methods:
232
233def is_available():
234    Return ``True`` if the handler's dependencies are installed, ``False``
235    otherwise.
236
237def get_overlay_array(ds, group):
238    Return a correctly shaped :class:`numpy.ndarray` derived from the
239    *Overlay Data* with element tag `group`, in :class:`Dataset` `ds` or raise
240    an exception.
241
242And two required attributes:
243
244DEPENDENCIES : dict
245    A dict containing the dependencies of the handler as
246    {'package_import_name': ('http://package.com/url', 'Package Name')}
247HANDLER_NAME : str
248    The name of the handler, e.g. 'Numpy Overlay'
249
250The first handler that both announces that it supports the transfer syntax
251and does not raise an exception is the handler that will provide the
252data.
253
254If all handlers fail to convert the data only the last exception is raised.
255"""
256
257import pydicom.pixel_data_handlers.numpy_handler as np_handler  # noqa
258import pydicom.pixel_data_handlers.rle_handler as rle_handler  # noqa
259import pydicom.pixel_data_handlers.pillow_handler as pillow_handler  # noqa
260import pydicom.pixel_data_handlers.jpeg_ls_handler as jpegls_handler  # noqa
261import pydicom.pixel_data_handlers.gdcm_handler as gdcm_handler  # noqa
262import pydicom.pixel_data_handlers.pylibjpeg_handler as pylibjpeg_handler  # noqa
263
264pixel_data_handlers = [
265    np_handler,
266    rle_handler,
267    gdcm_handler,
268    pillow_handler,
269    jpegls_handler,
270    pylibjpeg_handler,
271]
272"""Handlers for converting (7FE0,0010) *Pixel Data*.
273
274.. versionadded:: 1.2
275
276.. currentmodule:: pydicom.dataset
277
278This is an ordered list of *Pixel Data* handlers that the
279:meth:`~Dataset.convert_pixel_data` method will use to try to extract a
280correctly sized numpy array from the *Pixel Data* element.
281
282Handlers shall have four methods:
283
284def supports_transfer_syntax(transfer_syntax: UID)
285    Return ``True`` if the handler supports the transfer syntax indicated in
286    :class:`Dataset` `ds`, ``False`` otherwise.
287
288def is_available():
289    Return ``True`` if the handler's dependencies are installed, ``False``
290    otherwise.
291
292def get_pixeldata(ds):
293    Return a correctly sized 1D :class:`numpy.ndarray` derived from the
294    *Pixel Data* in :class:`Dataset` `ds` or raise an exception. Reshaping the
295    returned array to the correct dimensions is handled automatically.
296
297def needs_to_convert_to_RGB(ds):
298    Return ``True`` if the *Pixel Data* in the :class:`Dataset` `ds` needs to
299    be converted to the RGB colourspace, ``False`` otherwise.
300
301The first handler that both announces that it supports the transfer syntax
302and does not raise an exception, either in getting the data or when the data
303is reshaped to the correct dimensions, is the handler that will provide the
304data.
305
306If they all fail only the last exception is raised.
307
308If none raise an exception, but they all refuse to support the transfer
309syntax, then this fact is announced in a :class:`NotImplementedError`
310exception.
311"""
312
313APPLY_J2K_CORRECTIONS = True
314"""Use the information within JPEG 2000 data to correct the returned pixel data
315
316.. versionadded:: 2.1
317
318If ``True`` (default), then for handlers that support JPEG 2000 pixel data,
319use the component precision and sign to correct the returned ndarray when
320using the pixel data handlers. If ``False`` then only rely on the element
321values within the dataset when applying corrections.
322"""
323
324assume_implicit_vr_switch = True
325"""If invalid VR encountered, assume file switched to implicit VR
326
327.. versionadded:: 2.2
328
329If ``True`` (default), when reading an explicit VR file,
330if a VR is encountered that is not a valid two bytes within A-Z,
331then assume the original writer switched to implicit VR.  This has been
332seen in particular in some sequences.  This does not test that
333the VR is a valid DICOM VR, just that it has valid characters.
334"""
335
336
337INVALID_KEYWORD_BEHAVIOR = "WARN"
338"""Control the behavior when setting a :class:`~pydicom.dataset.Dataset`
339attribute that's not a known element keyword.
340
341.. versionadded:: 2.1
342
343If ``"WARN"`` (default), then warn when an element value is set using
344``Dataset.__setattr__()`` and the keyword is camel case but doesn't match a
345known DICOM element keyword. If ``"RAISE"`` then raise a :class:`ValueError`
346exception. If ``"IGNORE"`` then neither warn nor raise.
347
348Examples
349--------
350
351>>> from pydicom import config
352>>> config.INVALID_KEYWORD_BEHAVIOR = "WARN"
353>>> ds = Dataset()
354>>> ds.PatientName = "Citizen^Jan"  # OK
355>>> ds.PatientsName = "Citizen^Jan"
356../pydicom/dataset.py:1895: UserWarning: Camel case attribute 'PatientsName'
357used which is not in the element keyword data dictionary
358"""
359
360INVALID_KEY_BEHAVIOR = "WARN"
361"""Control the behavior when invalid keys are used with
362:meth:`~pydicom.dataset.Dataset.__contains__` (e.g. ``'invalid' in ds``).
363
364.. versionadded:: 2.1
365
366Invalid keys are objects that cannot be converted to a
367:class:`~pydicom.tag.BaseTag`, such as unknown element keywords or invalid
368element tags like ``0x100100010``.
369
370If ``"WARN"`` (default), then warn when an invalid key is used, if ``"RAISE"``
371then raise a :class:`ValueError` exception. If ``"IGNORE"`` then neither warn
372nor raise.
373
374Examples
375--------
376
377>>> from pydicom import config
378>>> config.INVALID_KEY_BEHAVIOR = "RAISE"
379>>> ds = Dataset()
380>>> 'PatientName' in ds  # OK
381False
382>>> 'PatientsName' in ds
383Traceback (most recent call last):
384  File "<stdin>", line 1, in <module>
385  File ".../pydicom/dataset.py", line 494, in __contains__
386    raise ValueError(msg) from exc
387ValueError: Invalid value used with the 'in' operator: must be an
388element tag as a 2-tuple or int, or an element keyword
389"""
390
391debugging: bool
392
393
394def debug(debug_on: bool = True, default_handler: bool = True) -> None:
395    """Turn on/off debugging of DICOM file reading and writing.
396
397    When debugging is on, file location and details about the elements read at
398    that location are logged to the 'pydicom' logger using Python's
399    :mod:`logging`
400    module.
401
402    .. versionchanged:1.4
403
404        Added `default_handler` keyword parameter.
405
406    Parameters
407    ----------
408    debug_on : bool, optional
409        If ``True`` (default) then turn on debugging, ``False`` to turn off.
410    default_handler : bool, optional
411        If ``True`` (default) then use :class:`logging.StreamHandler` as the
412        handler for log messages.
413    """
414    global logger, debugging
415
416    if default_handler:
417        handler = logging.StreamHandler()
418        formatter = logging.Formatter("%(message)s")
419        handler.setFormatter(formatter)
420        logger.addHandler(handler)
421
422    if debug_on:
423        logger.setLevel(logging.DEBUG)
424        debugging = True
425    else:
426        logger.setLevel(logging.WARNING)
427        debugging = False
428
429
430# force level=WARNING, in case logging default is set differently (issue 103)
431debug(False, False)
432
433_use_future = False
434_use_future_env = os.getenv("PYDICOM_FUTURE")
435
436if _use_future_env:
437    if _use_future_env.lower() in ["true", "yes", "on", "1"]:
438        _use_future = True
439    elif _use_future_env.lower() in ["false", "no", "off", "0"]:
440        _use_future = False
441    else:
442        raise ValueError(
443            "Unknown setting for environment variable "
444            "PYDICOM_FUTURE. Use True or False."
445        )
446
447
448def future_behavior(enable_future: bool = True) -> None:
449    """Imitate the behavior for the next major version of *pydicom*.
450
451    .. versionadded:: 2.1
452
453    This can be used to ensure your code is "future-proof" for known
454    upcoming changes in the next major version of *pydicom*. Typically,
455    deprecations become errors, and default values of config flags may change.
456
457    Parameters
458    ----------
459    enable_future: bool
460        Set ``True`` (default) to emulate future pydicom behavior,
461        ``False`` to reset to current pydicom behavior.
462
463    See also
464    --------
465    :attr:`~pydicom.config.INVALID_KEYWORD_BEHAVIOR`
466    :attr:`~pydicom.config.INVALID_KEY_BEHAVIOR`
467
468    """
469    global _use_future, INVALID_KEYWORD_BEHAVIOR
470
471    if enable_future:
472        _use_future = True
473        INVALID_KEYWORD_BEHAVIOR = "RAISE"
474    else:
475        _use_future = False
476        INVALID_KEYWORD_BEHAVIOR = "WARN"
477
478
479if _use_future:
480    future_behavior()
481