Skip to content

utils¤

Common utility functions.

from_bytes ¤

from_bytes(
    buffer: bytes | bytearray | str,
    fmt: MessageFormat | None = "ieee",
    dtype: MessageDataType = "<f",
) -> NumpyArray1D

Convert bytes into an array.

Parameters:

Name Type Description Default
buffer bytes | bytearray | str

A byte buffer. Can be an already-decoded buffer of type str, but only if fmt is "ascii".

required
fmt MessageFormat | None

The format that buffer is in. See to_bytes for more details.

'ieee'
dtype MessageDataType

The data type of each element in buffer. Can be any object that numpy.dtype supports. See to_bytes for more details.

'<f'

Returns:

Type Description
NumpyArray1D

The input buffer as a numpy array.

Source code in src/msl/equipment/utils.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def from_bytes(  # noqa: C901, PLR0912, PLR0915
    buffer: bytes | bytearray | str, fmt: MessageFormat | None = "ieee", dtype: MessageDataType = "<f"
) -> NumpyArray1D:
    """Convert bytes into an array.

    Args:
        buffer: A byte buffer. Can be an already-decoded buffer of type [str][], but only if `fmt` is `"ascii"`.
        fmt: The format that `buffer` is in. See [to_bytes][msl.equipment.utils.to_bytes] for more details.
        dtype: The data type of each element in `buffer`. Can be any object that [numpy.dtype][] supports.
            See [to_bytes][msl.equipment.utils.to_bytes] for more details.

    Returns:
        The input buffer as a numpy array.
    """
    if fmt == "ascii":
        if isinstance(buffer, (bytes, bytearray)):
            buffer = buffer.decode("ascii")
        return np.fromstring(buffer, dtype=dtype, sep=",")

    if not isinstance(buffer, (bytes, bytearray)):
        msg = f"buffer must be of type bytes | bytearray, got {type(buffer)}"
        raise TypeError(msg)

    _dtype: np.dtype
    if fmt == "ieee":
        offset = buffer.find(b"#")
        if offset == -1:
            msg = "Invalid IEEE-488.2 format, cannot find # character"
            raise ValueError(msg)

        try:
            len_nbytes = int(buffer[offset + 1 : offset + 2])
        except ValueError:
            len_nbytes = None

        if len_nbytes is None:
            msg = "Invalid IEEE-488.2 format, character after # is not an integer"
            raise ValueError(msg)

        if len_nbytes == 0:
            # <INDEFINITE LENGTH ARBITRARY BLOCK RESPONSE DATA>
            # Section 8.7.10, IEEE 488.2-1992
            nbytes = len(buffer) - offset

            # The standard states that the buffer must end in a NL (\n) character,
            # but it may have already been stripped from the buffer
            if buffer.endswith(b"\n"):
                nbytes -= 1
        else:
            # <DEFINITE LENGTH ARBITRARY BLOCK RESPONSE DATA>
            # Section 8.7.9, IEEE 488.2-1992
            try:
                nbytes = int(buffer[offset + 2 : offset + 2 + len_nbytes])
            except ValueError:
                nbytes = None

        if nbytes is None:
            msg = f"Invalid IEEE-488.2 format, characters after #{len_nbytes} are not integers"
            raise ValueError(msg)

        _dtype = np.dtype(dtype)
        offset += 2 + len_nbytes
        count = nbytes // _dtype.itemsize
        return np.frombuffer(buffer, dtype=_dtype, count=count, offset=offset)

    if fmt == "hp":
        offset = buffer.find(b"#A")
        if offset == -1:
            msg = "Invalid HP format, cannot find #A character"
            raise ValueError(msg)

        _dtype = np.dtype(dtype)
        i, j = offset + 2, offset + 4

        byteorder = _dtype.byteorder
        if byteorder == "|":
            # | means not applicable for the dtype specified, assign little endian
            # this redefinition is also declared in to_bytes()
            byteorder = "<"

        try:
            (nbytes,) = struct.unpack(byteorder + "H", buffer[i:j])
        except struct.error:
            nbytes = None

        if nbytes is None:
            msg = "Invalid HP format, characters after #A are not an unsigned short integer"
            raise ValueError(msg)

        count = nbytes // _dtype.itemsize
        return np.frombuffer(buffer, dtype=_dtype, count=count, offset=j)

    return np.frombuffer(buffer, dtype=dtype)

ipv4_addresses ¤

ipv4_addresses() -> set[str]

Get all IPv4 addresses on all network interfaces.

Source code in src/msl/equipment/utils.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def ipv4_addresses() -> set[str]:
    """Get all IPv4 addresses on all network interfaces."""
    if sys.platform == "win32":
        interfaces = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET)
        addresses = {str(ip[-1][0]) for ip in interfaces}
    elif sys.platform == "linux":
        out = subprocess.check_output(["hostname", "--all-ip-addresses"])  # pyright: ignore[reportUnreachable] # noqa: S607
        # --all-ip-addresses can return IPv6 addresses, which contain :
        addresses = {a for a in out.decode().split() if a[4] != ":"}
    else:
        ps = subprocess.Popen("ifconfig", stdout=subprocess.PIPE)  # pyright: ignore[reportUnreachable]  # noqa: S607
        output = subprocess.check_output(("grep", "inet "), stdin=ps.stdout)
        _ = ps.wait()
        addresses = {line.split()[1] for line in output.decode().splitlines()}
    addresses.discard("127.0.0.1")
    return addresses

to_bytes ¤

to_bytes(
    seq: Sequence1D,
    *,
    fmt: MessageFormat | None = None,
    dtype: MessageDataType = "<f"
) -> bytes

Convert a sequence of numbers into bytes.

Parameters:

Name Type Description Default
seq Sequence1D

A 1-dimensional sequence of numbers (not a multidimensional array).

required
fmt MessageFormat | None

The format to use to convert the sequence. Possible values are:

  • None — convert seq to bytes without a header.

!!! example <byte><byte><byte>...

  • ascii — comma-separated ASCII characters, see the <PROGRAM DATA SEPARATOR> standard that is defined in Section 7.4.2.2 of IEEE 488.2-1992.

!!! example <string>,<string>,<string>,...

  • ieee — arbitrary block data for SCPI messages, see the <DEFINITE LENGTH ARBITRARY BLOCK RESPONSE DATA> standard that is defined in Section 8.7.9 of IEEE 488.2-1992.

!!! example #<length of num bytes value><num bytes><byte><byte><byte>...

  • hp — the HP-IB data transfer standard, i.e., the FORM# command option. See the programming guide for an HP 8530A for more details.

!!! example #A<num bytes as uint16><byte><byte><byte>...

None
dtype MessageDataType

The data type to use to convert each element in seq to. If fmt is ascii then dtype must be of type str and it is used as the format_spec argument in format to first convert each element in seq to a string, and then it is encoded (e.g., '.2e' converts each element to scientific notation with two digits after the decimal point). If dtype includes a byte-order character, it is ignored. For all other values of fmt, the dtype can be any object that numpy.dtype supports (e.g., 'H', 'uint16' and numpy.ushort are equivalent values to convert each element to an unsigned short). If a byte-order character is specified then it is used, otherwise the native byte order of the CPU architecture is used. See struct-format-strings for more details.

'<f'

Returns:

Type Description
bytes

The seq converted to bytes.

Source code in src/msl/equipment/utils.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def to_bytes(seq: Sequence1D, *, fmt: MessageFormat | None = None, dtype: MessageDataType = "<f") -> bytes:
    """Convert a sequence of numbers into bytes.

    Args:
        seq: A 1-dimensional sequence of numbers (not a multidimensional array).
        fmt: The format to use to convert the sequence. Possible values are:

            * `None` &mdash; convert `seq` to bytes without a header.

              !!! example
                 `<byte><byte><byte>...`

            * `ascii` &mdash; comma-separated ASCII characters, see the
                  `<PROGRAM DATA SEPARATOR>` standard that is defined in Section 7.4.2.2 of
                  [IEEE 488.2-1992](https://standards.ieee.org/ieee/488.2/718/).

               !!! example
                   `<string>,<string>,<string>,...`

            * `ieee` &mdash; arbitrary block data for `SCPI` messages, see the
                  `<DEFINITE LENGTH ARBITRARY BLOCK RESPONSE DATA>` standard that is defined in
                  Section 8.7.9 of [IEEE 488.2-1992](https://standards.ieee.org/ieee/488.2/718/).

               !!! example
                   `#<length of num bytes value><num bytes><byte><byte><byte>...`

            * `hp` &mdash; the HP-IB data transfer standard, i.e., the `FORM#` command
                  option. See the programming guide for an
                  [HP 8530A](https://www.keysight.com/us/en/product/8530A/microwave-receiver.html#resources)
                  for more details.

               !!! example
                   `#A<num bytes as uint16><byte><byte><byte>...`

        dtype: The data type to use to convert each element in `seq` to. If `fmt` is
            `ascii` then `dtype` must be of type [str][] and it is used as the `format_spec`
            argument in [format][] to first convert each element in `seq` to a string,
            and then it is encoded (e.g., ``'.2e'`` converts each element to scientific
            notation with two digits after the decimal point). If `dtype` includes a
            byte-order character, it is ignored. For all other values of `fmt`, the
            `dtype` can be any object that [numpy.dtype][] supports (e.g., `'H'`,
            `'uint16'` and [numpy.ushort][] are equivalent values to convert each element
            to an `unsigned short`). If a byte-order character is specified then it
            is used, otherwise the native byte order of the CPU architecture is used.
            See [struct-format-strings][] for more details.

    Returns:
        The `seq` converted to bytes.
    """
    if fmt == "ascii":
        if not isinstance(dtype, str):
            msg = f"dtype must be of type str, got {type(dtype)}"
            raise TypeError(msg)

        format_spec = dtype.lstrip("@=<>!")
        return ",".join(format(item, format_spec) for item in seq).encode("ascii")

    array = seq.astype(dtype=dtype) if isinstance(seq, np.ndarray) else np.fromiter(seq, dtype=dtype, count=len(seq))

    if fmt == "ieee":
        nbytes = str(array.nbytes)
        len_nbytes = len(nbytes)
        if len_nbytes > 9:  # noqa: PLR2004
            # The IEEE-488.2 format allows for 1 digit to specify the number
            # of bytes in the array. This is extremely unlikely to occur in
            # practice since the instrument would require > 1GB of memory.
            msg = "length too big for IEEE-488.2 specification"
            raise OverflowError(msg)
        return f"#{len_nbytes}{nbytes}".encode() + array.tobytes()

    if fmt == "hp":
        byteorder = array.dtype.byteorder
        if byteorder == "|":
            # | means not applicable for the dtype specified, assign little endian
            # this redefinition is also declared in from_bytes()
            byteorder = "<"
        return b"#A" + struct.pack(byteorder + "H", array.nbytes) + array.tobytes()

    return array.tobytes()

to_enum ¤

to_enum(
    obj: object,
    enum: type[EnumType],
    *,
    prefix: str | None = None,
    to_upper: bool = False
) -> EnumType

Convert an object into the specified enum.Enum member.

Parameters:

Name Type Description Default
obj object

An object that can be converted to the specified enum. Can be a value or a member name of the specified enum.

required
enum type[EnumType]

The enum.Enum subclass that obj should be converted to.

required
prefix str | None

If obj is a string, ensures that prefix is included at the beginning of obj before converting obj to the enum.

None
to_upper bool

If obj is a string, then whether to change obj to upper case before converting obj to the enum.

False

Returns:

Type Description
EnumType

The obj converted to enum.

Raises:

Type Description
ValueError

If obj is not in enum.

Source code in src/msl/equipment/utils.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def to_enum(obj: object, enum: type[EnumType], *, prefix: str | None = None, to_upper: bool = False) -> EnumType:
    """Convert an object into the specified [enum.Enum][] member.

    Args:
        obj: An object that can be converted to the specified `enum`.
            Can be a _value_ or a _member_ name of the specified `enum`.
        enum: The [enum.Enum][] subclass that `obj` should be converted to.
        prefix: If `obj` is a string, ensures that `prefix` is included at the beginning
            of `obj` before converting `obj` to the `enum`.
        to_upper: If `obj` is a string, then whether to change `obj` to upper case before
            converting `obj` to the `enum`.

    Returns:
        The `obj` converted to `enum`.

    Raises:
        ValueError: If `obj` is not in `enum`.
    """
    try:
        return enum(obj)
    except ValueError:
        pass

    # then `obj` must the Enum member name
    name = f"{obj}".replace(" ", "_")
    if prefix and not name.startswith(prefix):
        name = prefix + name
    if to_upper:
        name = name.upper()

    try:
        return enum[name]
    except KeyError:
        pass

    msg = f"Cannot create {enum} from {obj!r}"
    raise ValueError(msg)

to_primitive ¤

to_primitive(text: str | bytes) -> bool | float | str

Convert text into a bool, int or float.

Parameters:

Name Type Description Default
text str | bytes

The text to convert.

required

Returns:

Type Description
bool | float | str

The text as a Python primitive type: bool, int or float. Returns the original text (decoded if bytes) if it cannot be converted to any of these types. The text "0" and "1" are converted to an integer not a boolean.

Source code in src/msl/equipment/utils.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def to_primitive(text: str | bytes) -> bool | float | str:
    """Convert text into a [bool][], [int][] or [float][].

    Args:
        text: The text to convert.

    Returns:
        The `text` as a Python primitive type: [bool][], [int][] or [float][].
            Returns the original `text` (decoded if `bytes`) if it cannot be
            converted to any of these types. The text `"0"` and `"1"` are
            converted to an integer not a boolean.
    """
    if isinstance(text, bytes):
        text = text.decode()

    for t in (int, float):  # order matters
        try:
            return t(text)
        except (ValueError, TypeError):  # noqa: PERF203
            pass

    upper = text.upper().strip()
    if upper == "TRUE":
        return True
    if upper == "FALSE":
        return False

    return text