1# -*- coding: utf-8 -*-
2"""General utility functions.
3
4This file is part of PyVISA.
5
6:copyright: 2014-2020 by PyVISA Authors, see AUTHORS for more details.
7:license: MIT, see LICENSE for more details.
8
9"""
10import functools
11import inspect
12import io
13import math
14import os
15import platform
16import struct
17import subprocess
18import sys
19import warnings
20from collections import OrderedDict
21from typing import (
22    Any,
23    Callable,
24    Dict,
25    Iterable,
26    List,
27    Optional,
28    Sequence,
29    Tuple,
30    Type,
31    Union,
32    overload,
33)
34
35from typing_extensions import Literal
36
37from . import constants, logger
38
39try:
40    import numpy as np  # type: ignore
41except ImportError:
42    np = None
43
44
45#: Length of the header found before a binary block (ieee or hp) that will
46#: trigger a warning to alert the user of a possibly incorrect answer from the
47#: instrument. In general binary block are not prefixed by a header and finding
48#: a long one may mean that we picked up a # in the bulk of the message.
49DEFAULT_LENGTH_BEFORE_BLOCK = 25
50
51
52def _use_numpy_routines(container: Union[type, Callable]) -> bool:
53    """Should optimized numpy routines be used to extract data."""
54    if np is None or container in (tuple, list):
55        return False
56
57    if container is np.array or (
58        inspect.isclass(container) and issubclass(container, np.ndarray)  # type: ignore
59    ):
60        return True
61
62    return False
63
64
65def read_user_library_path() -> Optional[str]:
66    """Return the library path stored in one of the following configuration files:
67
68        <sys prefix>/share/pyvisa/.pyvisarc
69        ~/.pyvisarc
70
71    <sys prefix> is the site-specific directory prefix where the platform
72    independent Python files are installed.
73
74    Example configuration file:
75
76        [Paths]
77        visa library=/my/path/visa.so
78        dll_extra_paths=/my/otherpath/;/my/otherpath2
79
80    Return `None` if  configuration files or keys are not present.
81
82    """
83    from configparser import ConfigParser, NoOptionError, NoSectionError
84
85    config_parser = ConfigParser()
86    files = config_parser.read(
87        [
88            os.path.join(sys.prefix, "share", "pyvisa", ".pyvisarc"),
89            os.path.join(os.path.expanduser("~"), ".pyvisarc"),
90        ]
91    )
92
93    if not files:
94        logger.debug("No user defined library files")
95        return None
96
97    logger.debug("User defined library files: %s" % files)
98    try:
99        return config_parser.get("Paths", "visa library")
100    except (NoOptionError, NoSectionError):
101        logger.debug("NoOptionError or NoSectionError while reading config file")
102        return None
103
104
105def add_user_dll_extra_paths() -> Optional[List[str]]:
106    """Add paths to search for .dll dependencies on Windows.
107
108    The configuration files are expected to be stored in one of the following location:
109
110        <sys prefix>/share/pyvisa/.pyvisarc
111        ~/.pyvisarc
112
113    <sys prefix> is the site-specific directory prefix where the platform
114    independent Python files are installed.
115
116    Example configuration file:
117
118        [Paths]
119        visa library=/my/path/visa.so
120        dll_extra_paths=/my/otherpath/;/my/otherpath2
121
122    """
123    from configparser import ConfigParser, NoOptionError, NoSectionError
124
125    # os.add_dll_library_path has been added in Python 3.8
126    if sys.version_info >= (3, 8) and sys.platform == "win32":
127        config_parser = ConfigParser()
128        files = config_parser.read(
129            [
130                os.path.join(sys.prefix, "share", "pyvisa", ".pyvisarc"),
131                os.path.join(os.path.expanduser("~"), ".pyvisarc"),
132            ]
133        )
134
135        if not files:
136            logger.debug("No user defined configuration")
137            return None
138
139        logger.debug("User defined configuration files: %s" % files)
140
141        try:
142            dll_extra_paths = config_parser.get("Paths", "dll_extra_paths").split(";")
143            for path in dll_extra_paths:
144                os.add_dll_directory(path)
145            return dll_extra_paths
146        except (NoOptionError, NoSectionError):
147            logger.debug(
148                "NoOptionError or NoSectionError while reading config file for"
149                " dll_extra_paths."
150            )
151            return None
152    else:
153        logger.debug(
154            "Not loading dll_extra_paths because we are not on Windows "
155            "or Python < 3.8"
156        )
157        return None
158
159
160class LibraryPath(str):
161    """Object encapsulating information about a VISA dynamic library."""
162
163    #: Path with which the object was created
164    path: str
165
166    #: Detection method employed to locate the library
167    found_by: str
168
169    #: Architectural information (32, ) or (64, ) or (32, 64)
170    _arch: Optional[Tuple[int, ...]] = None
171
172    def __new__(
173        cls: Type["LibraryPath"], path: str, found_by: str = "auto"
174    ) -> "LibraryPath":
175        obj = super(LibraryPath, cls).__new__(cls, path)  # type: ignore
176        obj.path = path
177        obj.found_by = found_by
178
179        return obj
180
181    @property
182    def arch(self) -> Tuple[int, ...]:
183        """Architecture of the library."""
184        if self._arch is None:
185            try:
186                self._arch = get_arch(self.path)
187            except Exception:
188                self._arch = tuple()
189
190        return self._arch
191
192    @property
193    def is_32bit(self) -> Union[bool, Literal["n/a"]]:
194        """Is the library 32 bits."""
195        if not self.arch:
196            return "n/a"
197        return 32 in self.arch
198
199    @property
200    def is_64bit(self) -> Union[bool, Literal["n/a"]]:
201        """Is the library 64 bits."""
202        if not self.arch:
203            return "n/a"
204        return 64 in self.arch
205
206    @property
207    def bitness(self) -> str:
208        """Bitness of the library."""
209        if not self.arch:
210            return "n/a"
211        return ", ".join(str(a) for a in self.arch)
212
213
214def cleanup_timeout(timeout: Optional[Union[int, float]]) -> int:
215    """Turn a timeout expressed as a float into in interger or the proper constant."""
216    if timeout is None or math.isinf(timeout):
217        timeout = constants.VI_TMO_INFINITE
218
219    elif timeout < 1:
220        timeout = constants.VI_TMO_IMMEDIATE
221
222    elif not (1 <= timeout <= 4294967294):
223        raise ValueError("timeout value is invalid")
224
225    else:
226        timeout = int(timeout)
227
228    return timeout
229
230
231def warn_for_invalid_kwargs(keyw, allowed_keys):  # pragma: no cover
232    warnings.warn("warn_for_invalid_kwargs will be removed in 1.12", FutureWarning)
233    for key in keyw.keys():
234        if key not in allowed_keys:
235            warnings.warn('Keyword argument "%s" unknown' % key, stacklevel=3)
236
237
238def filter_kwargs(keyw, selected_keys):  # pragma: no cover
239    warnings.warn("warn_for_invalid_kwargs will be removed in 1.12", FutureWarning)
240    result = {}
241    for key, value in keyw.items():
242        if key in selected_keys:
243            result[key] = value
244    return result
245
246
247def split_kwargs(keyw, self_keys, parent_keys, warn=True):  # pragma: no cover
248    warnings.warn("warn_for_invalid_kwargs will be removed in 1.12", FutureWarning)
249    self_kwargs = dict()
250    parent_kwargs = dict()
251    self_keys = set(self_keys)
252    parent_keys = set(parent_keys)
253    all_keys = self_keys | parent_keys
254    for key, value in keyw.items():
255        if warn and key not in all_keys:
256            warnings.warn('Keyword argument "%s" unknown' % key, stacklevel=3)
257        if key in self_keys:
258            self_kwargs[key] = value
259        if key in parent_keys:
260            parent_kwargs[key] = value
261
262    return self_kwargs, parent_kwargs
263
264
265_converters: Dict[str, Callable[[str], Any]] = {
266    "s": str,
267    "b": functools.partial(int, base=2),
268    "c": ord,
269    "d": int,
270    "o": functools.partial(int, base=8),
271    "x": functools.partial(int, base=16),
272    "X": functools.partial(int, base=16),
273    "h": functools.partial(int, base=16),
274    "H": functools.partial(int, base=16),
275    "e": float,
276    "E": float,
277    "f": float,
278    "F": float,
279    "g": float,
280    "G": float,
281}
282
283_np_converters = {
284    "d": "i",
285    "e": "d",
286    "E": "d",
287    "f": "d",
288    "F": "d",
289    "g": "d",
290    "G": "d",
291}
292
293
294ASCII_CONVERTER = Union[
295    Literal["s", "b", "c", "d", "o", "x", "X", "e", "E", "f", "F", "g", "G"],
296    Callable[[str], Any],
297]
298
299
300def from_ascii_block(
301    ascii_data: str,
302    converter: ASCII_CONVERTER = "f",
303    separator: Union[str, Callable[[str], Iterable[str]]] = ",",
304    container: Callable[
305        [Iterable[Union[int, float]]], Sequence[Union[int, float]]
306    ] = list,
307) -> Sequence:
308    """Parse ascii data and return an iterable of numbers.
309
310    Parameters
311    ----------
312    ascii_data : str
313        Data to be parsed.
314    converter : ASCII_CONVERTER, optional
315        Str format of function to convert each value. Default to "f".
316    separator : Union[str, Callable[[str], Iterable[str]]]
317        str or callable used to split the data into individual elements.
318        If a str is given, data.split(separator) is used. Default to ",".
319    container : Union[Type, Callable[[Iterable], Sequence]], optional
320        Container type to use for the output data. Possible values are: list,
321        tuple, np.ndarray, etc, Default to list.
322
323    Returns
324    -------
325    Sequence
326        Parsed data.
327
328    """
329    if (
330        _use_numpy_routines(container)
331        and isinstance(converter, str)
332        and isinstance(separator, str)
333        and converter in _np_converters
334    ):
335        return np.fromstring(ascii_data, _np_converters[converter], sep=separator)
336
337    if isinstance(converter, str):
338        try:
339            converter = _converters[converter]
340        except KeyError:
341            raise ValueError(
342                "Invalid code for converter: %s not in %s"
343                % (converter, str(tuple(_converters.keys())))
344            )
345
346    data: Iterable[str]
347    if isinstance(separator, str):
348        data = ascii_data.split(separator)
349    else:
350        data = separator(ascii_data)
351
352    return container([converter(raw_value) for raw_value in data])
353
354
355def to_ascii_block(
356    iterable: Iterable[Any],
357    converter: Union[str, Callable[[Any], str]] = "f",
358    separator: Union[str, Callable[[Iterable[str]], str]] = ",",
359) -> str:
360    """Turn an iterable of numbers in an ascii block of data.
361
362    Parameters
363    ----------
364    iterable : Iterable[Any]
365        Data to be formatted.
366    converter : Union[str, Callable[[Any], str]]
367        String formatting code or function used to convert each value.
368        Default to "f".
369    separator : Union[str, Callable[[Iterable[str]], str]]
370        str or callable that join individual elements into a str.
371        If a str is given, separator.join(data) is used.
372
373    """
374    if isinstance(separator, str):
375        separator = separator.join
376
377    if isinstance(converter, str):
378        converter = "%" + converter
379        block = separator(converter % val for val in iterable)
380    else:
381        block = separator(converter(val) for val in iterable)
382    return block
383
384
385#: Valid binary header when reading/writing binary block of data from an instrument
386BINARY_HEADERS = Literal["ieee", "hp", "empty"]
387
388#: Valid datatype for binary block. See Python standard library struct module for more
389#: details.
390BINARY_DATATYPES = Literal[
391    "s", "b", "B", "h", "H", "i", "I", "l", "L", "q", "Q", "f", "d"
392]
393
394#: Valid output containers for storing the parsed binary data
395BINARY_CONTAINERS = Union[type, Callable]
396
397
398def parse_ieee_block_header(
399    block: Union[bytes, bytearray],
400    length_before_block: Optional[int] = None,
401    raise_on_late_block: bool = False,
402) -> Tuple[int, int]:
403    """Parse the header of a IEEE block.
404
405    Definite Length Arbitrary Block:
406    #<header_length><data_length><data>
407
408    The header_length specifies the size of the data_length field.
409    And the data_length field specifies the size of the data.
410
411    Indefinite Length Arbitrary Block:
412    #0<data>
413
414    In this case the data length returned will be 0. The actual length can be
415    deduced from the block and the offset.
416
417    Parameters
418    ----------
419    block : Union[bytes, bytearray]
420        IEEE formatted block of data.
421    length_before_block : Optional[int], optional
422        Number of bytes before the actual start of the block. Default to None,
423        which means that number will be inferred.
424    raise_on_late_block : bool, optional
425        Raise an error in the beginning of the block is not found before
426        DEFAULT_LENGTH_BEFORE_BLOCK, if False use a warning. Default to False.
427
428    Returns
429    -------
430    int
431        Offset at which the actual data starts
432    int
433        Length of the data in bytes.
434
435    """
436    begin = block.find(b"#")
437    if begin < 0:
438        raise ValueError(
439            "Could not find hash sign (#) indicating the start of the block. "
440            "The block begin by %r" % block[:25]
441        )
442
443    length_before_block = length_before_block or DEFAULT_LENGTH_BEFORE_BLOCK
444    if begin > length_before_block:
445        msg = (
446            "The beginning of the block has been found at %d which "
447            "is an unexpectedly large value. The actual block may "
448            "have been missing a beginning marker but the block "
449            "contained one:\n%s"
450        ) % (begin, repr(block))
451        if raise_on_late_block:
452            raise RuntimeError(msg)
453        else:
454            warnings.warn(msg, UserWarning)
455
456    try:
457        # int(block[begin+1]) != int(block[begin+1:begin+2]) in Python 3
458        header_length = int(block[begin + 1 : begin + 2])
459    except ValueError:
460        header_length = 0
461    offset = begin + 2 + header_length
462
463    if header_length > 0:
464        # #3100DATA
465        # 012345
466        data_length = int(block[begin + 2 : offset])
467    else:
468        # #0DATA
469        # 012
470        data_length = 0
471
472    return offset, data_length
473
474
475def parse_hp_block_header(
476    block: Union[bytes, bytearray],
477    is_big_endian: bool,
478    length_before_block: int = None,
479    raise_on_late_block: bool = False,
480) -> Tuple[int, int]:
481    """Parse the header of a HP block.
482
483    Definite Length Arbitrary Block:
484    #A<data_length><data>
485
486    The header ia always 4 bytes long.
487    The data_length field specifies the size of the data.
488
489    Parameters
490    ----------
491    block : Union[bytes, bytearray]
492        HP formatted block of data.
493    is_big_endian : bool
494        Is the header in big or little endian order.
495    length_before_block : Optional[int], optional
496        Number of bytes before the actual start of the block. Default to None,
497        which means that number will be inferred.
498    raise_on_late_block : bool, optional
499        Raise an error in the beginning of the block is not found before
500        DEFAULT_LENGTH_BEFORE_BLOCK, if False use a warning. Default to False.
501
502    Returns
503    -------
504    int
505        Offset at which the actual data starts
506    int
507        Length of the data in bytes.
508
509    """
510    begin = block.find(b"#A")
511    if begin < 0:
512        raise ValueError(
513            "Could not find the standard block header (#A) indicating the start "
514            "of the block. The block begin by %r" % block[:25]
515        )
516
517    length_before_block = length_before_block or DEFAULT_LENGTH_BEFORE_BLOCK
518    if begin > length_before_block:
519        msg = (
520            "The beginning of the block has been found at %d which "
521            "is an unexpectedly large value. The actual block may "
522            "have been missing a beginning marker but the block "
523            "contained one:\n%s"
524        ) % (begin, repr(block))
525        if raise_on_late_block:
526            raise RuntimeError(msg)
527        else:
528            warnings.warn(msg, UserWarning)
529
530    offset = begin + 4
531
532    data_length = int.from_bytes(
533        block[begin + 2 : offset], byteorder="big" if is_big_endian else "little"
534    )
535
536    return offset, data_length
537
538
539def from_ieee_block(
540    block: Union[bytes, bytearray],
541    datatype: BINARY_DATATYPES = "f",
542    is_big_endian: bool = False,
543    container: Callable[
544        [Iterable[Union[int, float]]], Sequence[Union[int, float]]
545    ] = list,
546) -> Sequence[Union[int, float]]:
547    """Convert a block in the IEEE format into an iterable of numbers.
548
549    Definite Length Arbitrary Block:
550    #<header_length><data_length><data>
551
552    The header_length specifies the size of the data_length field.
553    And the data_length field specifies the size of the data.
554
555    Indefinite Length Arbitrary Block:
556    #0<data>
557
558    Parameters
559    ----------
560    block : Union[bytes, bytearray]
561        IEEE formatted block of data.
562    datatype : BINARY_DATATYPES, optional
563        Format string for a single element. See struct module. 'f' by default.
564    is_big_endian : bool, optional
565        Are the data in big or little endian order.
566    container : Union[Type, Callable[[Iterable], Sequence]], optional
567        Container type to use for the output data. Possible values are: list,
568        tuple, np.ndarray, etc, Default to list.
569
570    Returns
571    -------
572    Sequence[Union[int, float]]
573        Parsed data.
574
575    """
576    offset, data_length = parse_ieee_block_header(block)
577
578    # If the data length is not reported takes all the data and do not make
579    # any assumption about the termination character
580    if data_length == 0:
581        data_length = len(block) - offset
582
583    if len(block) < offset + data_length:
584        raise ValueError(
585            "Binary data is incomplete. The header states %d data"
586            " bytes, but %d where received." % (data_length, len(block) - offset)
587        )
588
589    return from_binary_block(
590        block, offset, data_length, datatype, is_big_endian, container
591    )
592
593
594def from_hp_block(
595    block: Union[bytes, bytearray],
596    datatype: BINARY_DATATYPES = "f",
597    is_big_endian: bool = False,
598    container: Callable[
599        [Iterable[Union[int, float]]], Sequence[Union[int, float]]
600    ] = list,
601) -> Sequence[Union[int, float]]:
602    """Convert a block in the HP format into an iterable of numbers.
603
604    Definite Length Arbitrary Block:
605    #A<data_length><data>
606
607    The header ia always 4 bytes long.
608    The data_length field specifies the size of the data.
609
610    Parameters
611    ----------
612    block : Union[bytes, bytearray]
613        HP formatted block of data.
614    datatype : BINARY_DATATYPES, optional
615        Format string for a single element. See struct module. 'f' by default.
616    is_big_endian : bool, optional
617        Are the data in big or little endian order.
618    container : Union[Type, Callable[[Iterable], Sequence]], optional
619        Container type to use for the output data. Possible values are: list,
620        tuple, np.ndarray, etc, Default to list.
621
622    Returns
623    -------
624    Sequence[Union[int, float]]
625        Parsed data.
626
627    """
628    offset, data_length = parse_hp_block_header(block, is_big_endian)
629
630    # If the data length is not reported takes all the data and do not make
631    # any assumption about the termination character
632    if data_length == 0:
633        data_length = len(block) - offset
634
635    if len(block) < offset + data_length:
636        raise ValueError(
637            "Binary data is incomplete. The header states %d data"
638            " bytes, but %d where received." % (data_length, len(block) - offset)
639        )
640
641    return from_binary_block(
642        block, offset, data_length, datatype, is_big_endian, container
643    )
644
645
646def from_binary_block(
647    block: Union[bytes, bytearray],
648    offset: int = 0,
649    data_length: Optional[int] = None,
650    datatype: BINARY_DATATYPES = "f",
651    is_big_endian: bool = False,
652    container: Callable[
653        [Iterable[Union[int, float]]], Sequence[Union[int, float]]
654    ] = list,
655) -> Sequence[Union[int, float]]:
656    """Convert a binary block into an iterable of numbers.
657
658
659    Parameters
660    ----------
661    block : Union[bytes, bytearray]
662        HP formatted block of data.
663    offset : int
664        Offset at which the actual data starts
665    data_length : int
666        Length of the data in bytes.
667    datatype : BINARY_DATATYPES, optional
668        Format string for a single element. See struct module. 'f' by default.
669    is_big_endian : bool, optional
670        Are the data in big or little endian order.
671    container : Union[Type, Callable[[Iterable], Sequence]], optional
672        Container type to use for the output data. Possible values are: list,
673        tuple, np.ndarray, etc, Default to list.
674
675    Returns
676    -------
677    Sequence[Union[int, float]]
678        Parsed data.
679
680    """
681    if data_length is None:
682        data_length = len(block) - offset
683
684    element_length = struct.calcsize(datatype)
685    array_length = int(data_length / element_length)
686
687    endianess = ">" if is_big_endian else "<"
688
689    if _use_numpy_routines(container):
690        return np.frombuffer(block, endianess + datatype, array_length, offset)
691
692    fullfmt = "%s%d%s" % (endianess, array_length, datatype)
693
694    try:
695        raw_data = struct.unpack_from(fullfmt, block, offset)
696    except struct.error:
697        raise ValueError("Binary data was malformed")
698
699    if datatype in "sp":
700        raw_data = raw_data[0]
701
702    return container(raw_data)
703
704
705def to_binary_block(
706    iterable: Sequence[Union[int, float]],
707    header: Union[str, bytes],
708    datatype: BINARY_DATATYPES,
709    is_big_endian: bool,
710) -> bytes:
711    """Convert an iterable of numbers into a block of data with a given header.
712
713    Parameters
714    ----------
715    iterable : Sequence[Union[int, float]]
716        Sequence of numbers to pack into a block.
717    header : Union[str, bytes]
718        Header which should prefix the binary block
719    datatype : BINARY_DATATYPES
720        Format string for a single element. See struct module.
721    is_big_endian : bool
722        Are the data in big or little endian order.
723
724    Returns
725    -------
726    bytes
727        Binary block of data preceded by the specified header
728
729    """
730    array_length = len(iterable)
731    endianess = ">" if is_big_endian else "<"
732    fullfmt = "%s%d%s" % (endianess, array_length, datatype)
733
734    if isinstance(header, str):
735        header = bytes(header, "ascii")
736
737    if datatype in ("s", "p"):
738        block = struct.pack(fullfmt, iterable)
739
740    else:
741        block = struct.pack(fullfmt, *iterable)
742
743    return header + block
744
745
746def to_ieee_block(
747    iterable: Sequence[Union[int, float]],
748    datatype: BINARY_DATATYPES = "f",
749    is_big_endian: bool = False,
750) -> bytes:
751    """Convert an iterable of numbers into a block of data in the IEEE format.
752
753    Parameters
754    ----------
755    iterable : Sequence[Union[int, float]]
756        Sequence of numbers to pack into a block.
757    datatype : BINARY_DATATYPES, optional
758        Format string for a single element. See struct module. Default to 'f'.
759    is_big_endian : bool, optional
760        Are the data in big or little endian order. Default to False.
761
762    Returns
763    -------
764    bytes
765        Binary block of data preceded by the specified header
766
767    """
768    array_length = len(iterable)
769    element_length = struct.calcsize(datatype)
770    data_length = array_length * element_length
771
772    header = "%d" % data_length
773    header = "#%d%s" % (len(header), header)
774
775    return to_binary_block(iterable, header, datatype, is_big_endian)
776
777
778def to_hp_block(
779    iterable: Sequence[Union[int, float]],
780    datatype: BINARY_DATATYPES = "f",
781    is_big_endian: bool = False,
782) -> bytes:
783    """Convert an iterable of numbers into a block of data in the HP format.
784
785    Parameters
786    ----------
787    iterable : Sequence[Union[int, float]]
788        Sequence of numbers to pack into a block.
789    datatype : BINARY_DATATYPES, optional
790        Format string for a single element. See struct module. Default to 'f'.
791    is_big_endian : bool, optional
792        Are the data in big or little endian order. Default to False.
793
794    Returns
795    -------
796    bytes
797        Binary block of data preceded by the specified header
798
799    """
800    array_length = len(iterable)
801    element_length = struct.calcsize(datatype)
802    data_length = array_length * element_length
803
804    header = b"#A" + (
805        int.to_bytes(data_length, 2, "big" if is_big_endian else "little")
806    )
807
808    return to_binary_block(iterable, header, datatype, is_big_endian)
809
810
811def get_system_details(backends: bool = True) -> Dict[str, str]:
812    """Return a dictionary with information about the system."""
813    buildno, builddate = platform.python_build()
814    if sys.maxunicode == 65535:
815        # UCS2 build (standard)
816        unitype = "UCS2"
817    else:
818        # UCS4 build (most recent Linux distros)
819        unitype = "UCS4"
820    bits, linkage = platform.architecture()
821
822    from . import __version__
823
824    d = {
825        "platform": platform.platform(),
826        "processor": platform.processor(),
827        "executable": sys.executable,
828        "implementation": getattr(platform, "python_implementation", lambda: "n/a")(),
829        "python": platform.python_version(),
830        "compiler": platform.python_compiler(),
831        "buildno": buildno,
832        "builddate": builddate,
833        "unicode": unitype,
834        "bits": bits,
835        "pyvisa": __version__,
836        "backends": OrderedDict(),
837    }
838
839    if backends:
840        from . import highlevel
841
842        for backend in highlevel.list_backends():
843            if backend.startswith("pyvisa-"):
844                backend = backend[7:]
845
846            try:
847                cls = highlevel.get_wrapper_class(backend)
848            except Exception as e:
849                d["backends"][backend] = [
850                    "Could not instantiate backend",
851                    "-> %s" % str(e),
852                ]
853                continue
854
855            try:
856                d["backends"][backend] = cls.get_debug_info()
857            except Exception as e:
858                d["backends"][backend] = [
859                    "Could not obtain debug info",
860                    "-> %s" % str(e),
861                ]
862
863    return d
864
865
866def system_details_to_str(d: Dict[str, str], indent: str = "") -> str:
867    """Convert the system details to a str.
868
869    System details can be obtained by `get_system_details`.
870
871    """
872
873    details = [
874        "Machine Details:",
875        "   Platform ID:    %s" % d.get("platform", "n/a"),
876        "   Processor:      %s" % d.get("processor", "n/a"),
877        "",
878        "Python:",
879        "   Implementation: %s" % d.get("implementation", "n/a"),
880        "   Executable:     %s" % d.get("executable", "n/a"),
881        "   Version:        %s" % d.get("python", "n/a"),
882        "   Compiler:       %s" % d.get("compiler", "n/a"),
883        "   Bits:           %s" % d.get("bits", "n/a"),
884        "   Build:          %s (#%s)"
885        % (d.get("builddate", "n/a"), d.get("buildno", "n/a")),
886        "   Unicode:        %s" % d.get("unicode", "n/a"),
887        "",
888        "PyVISA Version: %s" % d.get("pyvisa", "n/a"),
889        "",
890    ]
891
892    def _to_list(key, value, indent_level=0):
893        sp = " " * indent_level * 3
894
895        if isinstance(value, str):
896            if key:
897                return ["%s%s: %s" % (sp, key, value)]
898            else:
899                return ["%s%s" % (sp, value)]
900
901        elif isinstance(value, dict):
902            if key:
903                al = ["%s%s:" % (sp, key)]
904            else:
905                al = []
906
907            for k, v in value.items():
908                al.extend(_to_list(k, v, indent_level + 1))
909            return al
910
911        elif isinstance(value, (tuple, list)):
912            if key:
913                al = ["%s%s:" % (sp, key)]
914            else:
915                al = []
916
917            for v in value:
918                al.extend(_to_list(None, v, indent_level + 1))
919
920            return al
921
922        else:
923            return ["%s" % value]
924
925    details.extend(_to_list("Backends", d["backends"]))
926
927    joiner = "\n" + indent
928    return indent + joiner.join(details) + "\n"
929
930
931@overload
932def get_debug_info(to_screen: Literal[True] = True) -> None:
933    pass
934
935
936@overload
937def get_debug_info(to_screen: Literal[False]) -> str:
938    pass
939
940
941def get_debug_info(to_screen=True):
942    """Get the PyVISA debug information."""
943    out = system_details_to_str(get_system_details())
944    if not to_screen:
945        return out
946    print(out)
947
948
949def pip_install(package):  # pragma: no cover
950    warnings.warn("warn_for_invalid_kwargs will be removed in 1.12", FutureWarning)
951    try:
952        import pip  # type: ignore
953
954        return pip.main(["install", package])
955    except ImportError:
956        print(system_details_to_str(get_system_details()))
957        raise RuntimeError("Please install pip to continue.")
958
959
960machine_types = {
961    0: "UNKNOWN",
962    0x014C: "I386",
963    0x0162: "R3000",
964    0x0166: "R4000",
965    0x0168: "R10000",
966    0x0169: "WCEMIPSV2",
967    0x0184: "ALPHA",
968    0x01A2: "SH3",
969    0x01A3: "SH3DSP",
970    0x01A4: "SH3E",
971    0x01A6: "SH4",
972    0x01A8: "SH5",
973    0x01C0: "ARM",
974    0x01C2: "THUMB",
975    0x01C4: "ARMNT",
976    0x01D3: "AM33",
977    0x01F0: "POWERPC",
978    0x01F1: "POWERPCFP",
979    0x0200: "IA64",
980    0x0266: "MIPS16",
981    0x0284: "ALPHA64",
982    # 0x0284: 'AXP64', # same
983    0x0366: "MIPSFPU",
984    0x0466: "MIPSFPU16",
985    0x0520: "TRICORE",
986    0x0CEF: "CEF",
987    0x0EBC: "EBC",
988    0x8664: "AMD64",
989    0x9041: "M32R",
990    0xC0EE: "CEE",
991}
992
993
994def get_shared_library_arch(filename: str) -> str:
995    """Get the architecture of shared library."""
996    with io.open(filename, "rb") as fp:
997        dos_headers = fp.read(64)
998        _ = fp.read(4)
999
1000        magic, skip, offset = struct.unpack("=2s58sl", dos_headers)
1001
1002        if magic != b"MZ":
1003            raise Exception("Not an executable")
1004
1005        fp.seek(offset, io.SEEK_SET)
1006        pe_header = fp.read(6)
1007
1008        sig, skip, machine = struct.unpack(str("=2s2sH"), pe_header)
1009
1010        if sig != b"PE":
1011            raise Exception("Not a PE executable")
1012
1013        return machine_types.get(machine, "UNKNOWN")
1014
1015
1016def get_arch(filename: str) -> Tuple[int, ...]:
1017    """Get the architecture of the platform."""
1018    this_platform = sys.platform
1019    if this_platform.startswith("win"):
1020        machine_type = get_shared_library_arch(filename)
1021        if machine_type == "I386":
1022            return (32,)
1023        elif machine_type in ("IA64", "AMD64"):
1024            return (64,)
1025        else:
1026            return ()
1027    elif this_platform not in ("linux2", "linux3", "linux", "darwin"):
1028        raise OSError("Unsupported platform: %s" % this_platform)
1029
1030    res = subprocess.run(["file", filename], capture_output=True)
1031    out = res.stdout.decode("ascii")
1032    ret = []
1033    if this_platform.startswith("linux"):
1034        if "32-bit" in out:
1035            ret.append(32)
1036        if "64-bit" in out:
1037            ret.append(64)
1038    else:  # darwin
1039        if "(for architecture i386)" in out:
1040            ret.append(32)
1041        if "(for architecture x86_64)" in out:
1042            ret.append(64)
1043
1044    return tuple(ret)
1045