Skip to content

API

binapy

Top-level package for BinaPy.

BinaPy

Bases: bytes

A helper class for binary data manipulation.

This subclass of bytes exposes various binary data manipulation methods. Since this is a bytes subclass, you can use instances of BinaPy anywhere you can use bytes. BinaPy allows (re)encoding of data using encode_to(<format>), decoding using decode_from(<format>), parsing using parse_from(<format>), and serialisation using serialize_to(<format>).

Actual transformations into formats such as Base64, JSON, etc. are implemented using Extensions. Those extensions are registered using the decorators binapy_encoder, binapy_decoder, binapy_checker, binapy_serializer, and binapy_parser.

Source code in binapy/binapy.py
 24
 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
 62
 63
 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
 92
 93
 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
173
174
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
class BinaPy(bytes):
    """A helper class for binary data manipulation.

    This subclass of `bytes` exposes various binary data manipulation methods. Since this is
    a `bytes` subclass, you can use instances of `BinaPy` anywhere you can use `bytes`. BinaPy
    allows (re)encoding of data using `encode_to(<format>)`, decoding using
    `decode_from(<format>)`, parsing using `parse_from(<format>)`, and serialisation using
    `serialize_to(<format>)`.

    Actual transformations into formats such as Base64, JSON, etc. are implemented using
    Extensions. Those extensions are registered using the decorators `binapy_encoder`,
    `binapy_decoder`, `binapy_checker`, `binapy_serializer`, and `binapy_parser`.

    """

    def __new__(
        cls,
        value: bytes | str | int | SupportsBytes = b"",
        encoding: str = "utf-8",
        errors: str = "strict",
    ) -> BinaPy:
        """Override base method to accept a string with a default encoding of "utf-8".

        See Also:
            [`bytes` constructor](https://docs.python.org/3/library/stdtypes.html#bytes) and
            [`str.encode()`](https://docs.python.org/3/library/stdtypes.html#str.encode)

        Args:
            value: a `bytes` or a `str`
            encoding: if value is a `str`, specify the encoding to use to encode this str to bytes
            errors: 'strict', 'ignore', 'replace', 'xmlcharrefreplace', or 'backslashreplace'

        """
        if isinstance(value, str):
            obj = bytes.__new__(cls, value, encoding=encoding, errors=errors)
        else:
            obj = bytes.__new__(cls, value)
        return obj

    @classmethod
    def from_int(
        cls,
        i: int,
        *,
        length: int | None = None,
        byteorder: Literal["little", "big"] = "big",
        signed: bool = False,
    ) -> BinaPy:
        """Convert an `int` to a `BinaPy`.

        This is a wrapper around [int.to_bytes()](https://docs.python.org/3/library/stdtypes.html#int.to_bytes) and
        takes the same parameters.

        Args:
            i: the integer to convert to BinaPy
            length: the length of the integer, in bytes.
              If omitted,default to the minimal length that fits the given integer.
            byteorder: "little" or "big" (defaults to "big")
            signed: determines whether two's complement is used to represent the integer.

        Returns:
            a BinaPy with the binary representation of the given integer

        """
        if length is None:
            length = (i.bit_length() + 7) // 8

        data = i.to_bytes(length, byteorder, signed=signed)
        return cls(data)

    def ascii(self) -> str:
        """Decode this BinaPy to a str.

        This makes sure that only ascii characters are part of the result.

        Returns:
            a str with only ASCII chars

        """
        return self.decode("ascii")

    def re_match(self, pattern: str, encoding: str = "ascii") -> str:
        """Decode this binary value to a string, then match it to a regex.

        Decoding is done using the encoding defined in `encoding` (default to "ascii").
        The decoded value is then matched with the regular expression `pattern`.

        If the match is successful, return the decoded string. Raise a `ValueError` otherwise.

        Args:
            pattern: the regular expression pattern to match
            encoding: the encoding to use to decode the binary value to a string

        Returns:
            the decoded, matching `str`

        Raises:
            ValueError: if the decoded str does not match `pattern`

        """
        res = self.decode(encoding)
        if re.match(pattern, res):
            return res
        msg = f"This value does not match pattern {pattern}"
        raise ValueError(msg)

    def text(self, encoding: str = "ascii") -> str:
        r"""Decode this BinaPy to a str, and check that the result is printable.

        Printable characters are characters from the range `[a-zA-Z0-9!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ]`.

        Args:
            encoding: the encoding to use to decode the binary data

        Returns:
            the decoded text

        """
        return self.re_match(r'^[a-zA-Z0-9!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ]*$', encoding)

    def urlsafe(self) -> str:
        r"""Convert this BinaPy to a str, and check that it contains only url-safe characters.

        Url-safe characters are `[A-Za-z0-9_.\\-~]`.

        Returns:
                 a str with only URL-safe chars

        """
        return self.re_match(r"^[A-Za-z0-9_.\-~]$")

    def alphanumeric(self) -> str:
        """Check that this BinaPy contains only alphanumeric characters.

        Returns:
                 a str with only alphanumeric chars

        """
        return self.re_match(r"^[a-zA-Z]$")

    def to_int(self, *, byteorder: Literal["little", "big"] = "big", signed: bool = False) -> int:
        """Convert this BinaPy to an `int`.

        This is a wrapper around
        [int.from_bytes()](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) and takes
        the same parameters.

        Args:
            byteorder: "little" or "big" (defaults to "big")
            signed: determines whether two's complement is used to represent the integer. Default to False.

        Returns:
            an integer based on this BinaPy binary value

        """
        return int.from_bytes(self, byteorder, signed=signed)

    @classmethod
    def from_binary_string(cls, s: str, *, byteorder: Literal["little", "big"] = "big", signed: bool = False) -> BinaPy:
        """Initialize a BinaPy based on a binary string (containing only 0 and 1).

        The string may contain spaces or tabs characters which will be ignored.

        Args:
            s: a binary string
            byteorder: byte order to use
            signed: True if 2 complement is used to represent negative values

        Returns:
            a BinaPy

        """
        s = s.replace(" ", "").replace("\t", "")
        return cls(int(s, 2).to_bytes((len(s) + 7) // 8, byteorder=byteorder, signed=signed))

    def to_binary_string(
        self,
        *,
        byteorder: Literal["little", "big"] = "big",
        signed: bool = False,
        pad: bool = True,
    ) -> str:
        """Return a string containing this BinaPy value in binary representation.

        Args:
            byteorder: byte order to use
            signed: `True` if 2 complement is used to represent negative values
            pad: if `True`, left pad the result with 0 to make length a multiple of 8

        Returns:
            a string with containing only 0 and 1

        """
        binary = format(self.to_int(byteorder=byteorder, signed=signed), "b")
        if pad and len(binary) % 8:
            d = len(binary) // 8
            return binary.rjust(8 * d + 8, "0")
        return binary

    @classmethod
    def random(cls, length: int) -> BinaPy:
        """Return a BinaPy containing `length` random bytes.

        Args:
            length: number of bytes to generate

        Returns:
            a BinaPy with randomly generated data

        """
        return cls(secrets.token_bytes(length))

    @classmethod
    def random_bits(cls, length: int) -> BinaPy:
        """Return a BinaPy containing `length` random bits. Same as random(length//8).

        Length must be a multiple of 8.

        Args:
            length: number of bits to randomly generate

        Returns:
            a BinaPy with randomly generated data

        """
        return cls(secrets.token_bytes(length // 8))

    @overload
    def __getitem__(self, index: SupportsIndex) -> int:
        ...  # pragma: no cover

    @overload
    def __getitem__(self, slice: slice) -> BinaPy:  # noqa: A002
        ...  # pragma: no cover

    def __getitem__(self, slice: slice | SupportsIndex) -> int | BinaPy:  # noqa: A002
        """Override the base method so that slicing returns a BinaPy instead of just bytes.

        Args:
            slice: a slice or index

        Returns:
            A BinaPy

        """
        if isinstance(slice, int):
            return super().__getitem__(slice)
        return self.__class__(super().__getitem__(slice))

    def char_at(self, index: int) -> str:
        """Return the character at the given index.

        Slicing a standard bytes returns an int. Sometimes what you really want is a single char string.

        Args:
            index: the index of the desired character

        Returns:
            the single character at the given index

        """
        return chr(self[index])

    def __add__(
        self,
        other: Any,
    ) -> BinaPy:
        """Override base method so that addition returns a BinaPy instead of bytes.

        Args:
            other: bytes or BinaPy to add

        Returns:
            a BinaPy

        """
        return self.__class__(super().__add__(other))

    def __radd__(self, other: bytes) -> BinaPy:
        """Override base method so that right addition returns a BinaPy instead of bytes.

        Args:
            other: bytes or BinaPy to radd

        Returns:
            a BinaPy

        """
        return self.__class__(other.__add__(self))

    def split(self, sep: bytes | None = None, maxsplit: int = -1) -> list[BinaPy]:  # type: ignore[override]
        """Override base method to return a BinaPy instead of bytes.

        Args:
            sep: a separator
            maxsplit: the maximum number of splits

        Returns:
            a BinaPy

        """
        return [self.__class__(b) for b in super().split(sep, maxsplit)]

    def split_at(self, *pos: int) -> tuple[BinaPy, ...]:
        """Split this BinaPy at one or more integer positions.

        Args:
            *pos: indexes where to cut the BinaPy

        Returns:
            a tuple of `len(pos) + 1` instances of BinaPy

        """
        spos = sorted(pos)
        return tuple(self.__class__(self[start:end]) for start, end in zip([0, *spos], [*spos, len(self)]))

    cut_at = split_at  # for backward compatibility

    extensions: ClassVar[dict[str, dict[str, Callable[..., Any]]]] = {}
    """Extension registry."""

    @classmethod
    def _get_extension_methods(cls, name: str) -> dict[str, Callable[..., Any]]:
        extension = cls.extensions.get(name)
        if extension is None:
            msg = f"Extension {name} not found"
            raise NotImplementedError(msg)
        return extension

    @classmethod
    def _get_checker(cls, extension_name: str) -> Callable[..., bool]:
        extension_methods = cls._get_extension_methods(extension_name)
        method = extension_methods.get("check")
        if method is None:
            msg = f"Extension '{extension_name}' does not have a checker method"
            raise NotImplementedError(msg)
        return method

    @classmethod
    def _get_decoder(cls, extension_name: str) -> Callable[..., BinaPy]:
        extension_methods = cls._get_extension_methods(extension_name)
        method = extension_methods.get("decode")
        if method is None:
            msg = f"Extension '{extension_name}' does not have a decode method"
            raise NotImplementedError(msg)
        return method

    @classmethod
    def _get_encoder(cls, extension_name: str) -> Callable[..., BinaPy]:
        extension_methods = cls._get_extension_methods(extension_name)
        method = extension_methods.get("encode")
        if method is None:
            msg = f"Extension '{extension_name}' does not have an encode method"
            raise NotImplementedError(msg)
        return method

    @classmethod
    def _get_parser(cls, extension_name: str) -> Callable[..., Any]:
        extension_methods = cls._get_extension_methods(extension_name)
        method = extension_methods.get("parse")
        if method is None:
            msg = f"Extension '{extension_name}' does not have a parse method"
            raise NotImplementedError(msg)
        return method

    @classmethod
    def _get_serializer(cls, extension_name: str) -> Callable[..., BinaPy]:
        extension_methods = cls._get_extension_methods(extension_name)
        method = extension_methods.get("serialize")
        if method is None:
            msg = f"Extension '{extension_name}' does not have a serialize method"
            raise NotImplementedError(msg)
        return method

    def encode_to(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
        """Encode data from this BinaPy according to the format `name`.

        Args:
            name: format to use
            *args: additional position parameters for the extension encoder method
            **kwargs: additional keyword parameters for the extension encoder method

        Returns:
            the resulting data

        """
        encoder = self._get_encoder(name)

        return encoder(self, *args, **kwargs)

    def to(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
        """Alias for `encode_to()`.

        Args:
            name: same as `encode_to()`
            *args:  same as `encode_to()`
            **kwargs:  same as `encode_to()`

        Returns:
            same as `encode_to()`

        """
        return self.encode_to(name, *args, **kwargs)

    def decode_from(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
        """Decode data from this BinaPy according to the format `name`.

        Args:
            name: format name to use
            *args: additional position parameters for the extension decoder method
            **kwargs: additional keyword parameters for the extension decoder method

        Returns:
            the resulting data

        """
        decoder = self._get_decoder(name)

        return decoder(self, *args, **kwargs)

    def check(self, name: str, *, decode: bool = False, raise_on_error: bool = False) -> bool:
        """Check that this BinaPy conforms to a given format extension.

        Args:
            name: the name of the extension to check
            decode: if `True`, and the given extension does not have a checker method,
                try to decode this BinaPy using the decoder method to check if that works.
            raise_on_error: if `True`, exceptions from the checker method, if any,
                will be raised instead of returning `False`.

        Returns:
            a boolean, that is True if this BinaPy conforms to the given extension format, False otherwise.

        """
        # raises an exception in case the extension does not exist
        self._get_extension_methods(name)

        try:
            checker = self._get_checker(name)
            try:
                return checker(self)
            except InvalidExtensionMethodError:
                raise
            except Exception as exc:
                if raise_on_error:
                    raise exc from exc
                return False
        except NotImplementedError:
            # if checker is not implemented and decode is True, try to decode instead
            if decode:
                decoder = self._get_decoder(name)
                try:
                    decoder(self)
                except Exception as exc:
                    if raise_on_error:
                        raise exc from exc
                    return False
                else:
                    return True
            else:
                raise
        return False

    def check_all(self, *, decode: bool = False) -> list[str]:
        """Check if this BinaPy conforms to any of the registered format extensions.

        Returns:
             a list of format extensions that this BinaPy can be decoded from.

        Args:
            decode: if `True`, for extensions that don't have a checker method,
                try to decode this BinaPy using the decoder method to check if that works.

        """

        def get_results() -> Iterator[str]:
            for name in self.extensions:
                with suppress(NotImplementedError):
                    success = self.check(name, decode=decode)
                    if success is True:
                        yield name

        return list(get_results())

    def parse_from(self, name: str, *args: Any, **kwargs: Any) -> Any:
        """Parse data from this BinaPy, based on a given format extension.

        Args:
            name: name of the extension to use
            *args: additional position parameters for the extension decoder method
            **kwargs: additional keyword parameters for the extension decoder method

        Returns:
            the result from parsing this BinaPy

        """
        parser = self._get_parser(name)

        return parser(self, *args, **kwargs)

    @classmethod
    def serialize_to(cls, name: str, *args: Any, **kwargs: Any) -> BinaPy:
        """Serialize (dump) data to a BinaPy, based on a given extension format.

        Args:
            name: name of the extension to use
            *args: additional position parameters
                for the extension decoder method (which includes the data to serialize)
            **kwargs: additional keyword parameters for the extension decoder method

        Returns:
            a BinaPy, resulting from serialization of the data

        """
        serializer = cls._get_serializer(name)

        return serializer(*args, **kwargs)

    @classmethod
    def register_extension(cls, name: str, feature: str, func: Callable[..., Any]) -> None:
        """Register a new feature for the given extension name.

        Args:
            name: the extension name
            feature: name of the feature to register ("encode", "decode", etc.)
            func: the method implementing the feature

        """
        ext_dict = cls.extensions.setdefault(name, {})
        ext_dict[feature] = func

extensions: dict[str, dict[str, Callable[..., Any]]] = {} class-attribute

Extension registry.

from_int(i, *, length=None, byteorder='big', signed=False) classmethod

Convert an int to a BinaPy.

This is a wrapper around int.to_bytes() and takes the same parameters.

Parameters:

Name Type Description Default
i int

the integer to convert to BinaPy

required
length int | None

the length of the integer, in bytes. If omitted,default to the minimal length that fits the given integer.

None
byteorder Literal['little', 'big']

"little" or "big" (defaults to "big")

'big'
signed bool

determines whether two's complement is used to represent the integer.

False

Returns:

Type Description
BinaPy

a BinaPy with the binary representation of the given integer

Source code in binapy/binapy.py
63
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
92
@classmethod
def from_int(
    cls,
    i: int,
    *,
    length: int | None = None,
    byteorder: Literal["little", "big"] = "big",
    signed: bool = False,
) -> BinaPy:
    """Convert an `int` to a `BinaPy`.

    This is a wrapper around [int.to_bytes()](https://docs.python.org/3/library/stdtypes.html#int.to_bytes) and
    takes the same parameters.

    Args:
        i: the integer to convert to BinaPy
        length: the length of the integer, in bytes.
          If omitted,default to the minimal length that fits the given integer.
        byteorder: "little" or "big" (defaults to "big")
        signed: determines whether two's complement is used to represent the integer.

    Returns:
        a BinaPy with the binary representation of the given integer

    """
    if length is None:
        length = (i.bit_length() + 7) // 8

    data = i.to_bytes(length, byteorder, signed=signed)
    return cls(data)

ascii()

Decode this BinaPy to a str.

This makes sure that only ascii characters are part of the result.

Returns:

Type Description
str

a str with only ASCII chars

Source code in binapy/binapy.py
 94
 95
 96
 97
 98
 99
100
101
102
103
def ascii(self) -> str:
    """Decode this BinaPy to a str.

    This makes sure that only ascii characters are part of the result.

    Returns:
        a str with only ASCII chars

    """
    return self.decode("ascii")

re_match(pattern, encoding='ascii')

Decode this binary value to a string, then match it to a regex.

Decoding is done using the encoding defined in encoding (default to "ascii"). The decoded value is then matched with the regular expression pattern.

If the match is successful, return the decoded string. Raise a ValueError otherwise.

Parameters:

Name Type Description Default
pattern str

the regular expression pattern to match

required
encoding str

the encoding to use to decode the binary value to a string

'ascii'

Returns:

Type Description
str

the decoded, matching str

Raises:

Type Description
ValueError

if the decoded str does not match pattern

Source code in binapy/binapy.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def re_match(self, pattern: str, encoding: str = "ascii") -> str:
    """Decode this binary value to a string, then match it to a regex.

    Decoding is done using the encoding defined in `encoding` (default to "ascii").
    The decoded value is then matched with the regular expression `pattern`.

    If the match is successful, return the decoded string. Raise a `ValueError` otherwise.

    Args:
        pattern: the regular expression pattern to match
        encoding: the encoding to use to decode the binary value to a string

    Returns:
        the decoded, matching `str`

    Raises:
        ValueError: if the decoded str does not match `pattern`

    """
    res = self.decode(encoding)
    if re.match(pattern, res):
        return res
    msg = f"This value does not match pattern {pattern}"
    raise ValueError(msg)

text(encoding='ascii')

Decode this BinaPy to a str, and check that the result is printable.

Printable characters are characters from the range [a-zA-Z0-9!"#$%&\'()*+,-./:;<=>?@[\\]^_~ ]`.

Parameters:

Name Type Description Default
encoding str

the encoding to use to decode the binary data

'ascii'

Returns:

Type Description
str

the decoded text

Source code in binapy/binapy.py
130
131
132
133
134
135
136
137
138
139
140
141
142
def text(self, encoding: str = "ascii") -> str:
    r"""Decode this BinaPy to a str, and check that the result is printable.

    Printable characters are characters from the range `[a-zA-Z0-9!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ]`.

    Args:
        encoding: the encoding to use to decode the binary data

    Returns:
        the decoded text

    """
    return self.re_match(r'^[a-zA-Z0-9!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ]*$', encoding)

urlsafe()

Convert this BinaPy to a str, and check that it contains only url-safe characters.

Url-safe characters are [A-Za-z0-9_.\\-~].

Returns:

Type Description
str

a str with only URL-safe chars

Source code in binapy/binapy.py
144
145
146
147
148
149
150
151
152
153
def urlsafe(self) -> str:
    r"""Convert this BinaPy to a str, and check that it contains only url-safe characters.

    Url-safe characters are `[A-Za-z0-9_.\\-~]`.

    Returns:
             a str with only URL-safe chars

    """
    return self.re_match(r"^[A-Za-z0-9_.\-~]$")

alphanumeric()

Check that this BinaPy contains only alphanumeric characters.

Returns:

Type Description
str

a str with only alphanumeric chars

Source code in binapy/binapy.py
155
156
157
158
159
160
161
162
def alphanumeric(self) -> str:
    """Check that this BinaPy contains only alphanumeric characters.

    Returns:
             a str with only alphanumeric chars

    """
    return self.re_match(r"^[a-zA-Z]$")

to_int(*, byteorder='big', signed=False)

Convert this BinaPy to an int.

This is a wrapper around int.from_bytes() and takes the same parameters.

Parameters:

Name Type Description Default
byteorder Literal['little', 'big']

"little" or "big" (defaults to "big")

'big'
signed bool

determines whether two's complement is used to represent the integer. Default to False.

False

Returns:

Type Description
int

an integer based on this BinaPy binary value

Source code in binapy/binapy.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def to_int(self, *, byteorder: Literal["little", "big"] = "big", signed: bool = False) -> int:
    """Convert this BinaPy to an `int`.

    This is a wrapper around
    [int.from_bytes()](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) and takes
    the same parameters.

    Args:
        byteorder: "little" or "big" (defaults to "big")
        signed: determines whether two's complement is used to represent the integer. Default to False.

    Returns:
        an integer based on this BinaPy binary value

    """
    return int.from_bytes(self, byteorder, signed=signed)

from_binary_string(s, *, byteorder='big', signed=False) classmethod

Initialize a BinaPy based on a binary string (containing only 0 and 1).

The string may contain spaces or tabs characters which will be ignored.

Parameters:

Name Type Description Default
s str

a binary string

required
byteorder Literal['little', 'big']

byte order to use

'big'
signed bool

True if 2 complement is used to represent negative values

False

Returns:

Type Description
BinaPy

a BinaPy

Source code in binapy/binapy.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@classmethod
def from_binary_string(cls, s: str, *, byteorder: Literal["little", "big"] = "big", signed: bool = False) -> BinaPy:
    """Initialize a BinaPy based on a binary string (containing only 0 and 1).

    The string may contain spaces or tabs characters which will be ignored.

    Args:
        s: a binary string
        byteorder: byte order to use
        signed: True if 2 complement is used to represent negative values

    Returns:
        a BinaPy

    """
    s = s.replace(" ", "").replace("\t", "")
    return cls(int(s, 2).to_bytes((len(s) + 7) // 8, byteorder=byteorder, signed=signed))

to_binary_string(*, byteorder='big', signed=False, pad=True)

Return a string containing this BinaPy value in binary representation.

Parameters:

Name Type Description Default
byteorder Literal['little', 'big']

byte order to use

'big'
signed bool

True if 2 complement is used to represent negative values

False
pad bool

if True, left pad the result with 0 to make length a multiple of 8

True

Returns:

Type Description
str

a string with containing only 0 and 1

Source code in binapy/binapy.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def to_binary_string(
    self,
    *,
    byteorder: Literal["little", "big"] = "big",
    signed: bool = False,
    pad: bool = True,
) -> str:
    """Return a string containing this BinaPy value in binary representation.

    Args:
        byteorder: byte order to use
        signed: `True` if 2 complement is used to represent negative values
        pad: if `True`, left pad the result with 0 to make length a multiple of 8

    Returns:
        a string with containing only 0 and 1

    """
    binary = format(self.to_int(byteorder=byteorder, signed=signed), "b")
    if pad and len(binary) % 8:
        d = len(binary) // 8
        return binary.rjust(8 * d + 8, "0")
    return binary

random(length) classmethod

Return a BinaPy containing length random bytes.

Parameters:

Name Type Description Default
length int

number of bytes to generate

required

Returns:

Type Description
BinaPy

a BinaPy with randomly generated data

Source code in binapy/binapy.py
223
224
225
226
227
228
229
230
231
232
233
234
@classmethod
def random(cls, length: int) -> BinaPy:
    """Return a BinaPy containing `length` random bytes.

    Args:
        length: number of bytes to generate

    Returns:
        a BinaPy with randomly generated data

    """
    return cls(secrets.token_bytes(length))

random_bits(length) classmethod

Return a BinaPy containing length random bits. Same as random(length//8).

Length must be a multiple of 8.

Parameters:

Name Type Description Default
length int

number of bits to randomly generate

required

Returns:

Type Description
BinaPy

a BinaPy with randomly generated data

Source code in binapy/binapy.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
@classmethod
def random_bits(cls, length: int) -> BinaPy:
    """Return a BinaPy containing `length` random bits. Same as random(length//8).

    Length must be a multiple of 8.

    Args:
        length: number of bits to randomly generate

    Returns:
        a BinaPy with randomly generated data

    """
    return cls(secrets.token_bytes(length // 8))

char_at(index)

Return the character at the given index.

Slicing a standard bytes returns an int. Sometimes what you really want is a single char string.

Parameters:

Name Type Description Default
index int

the index of the desired character

required

Returns:

Type Description
str

the single character at the given index

Source code in binapy/binapy.py
273
274
275
276
277
278
279
280
281
282
283
284
285
def char_at(self, index: int) -> str:
    """Return the character at the given index.

    Slicing a standard bytes returns an int. Sometimes what you really want is a single char string.

    Args:
        index: the index of the desired character

    Returns:
        the single character at the given index

    """
    return chr(self[index])

split(sep=None, maxsplit=-1)

Override base method to return a BinaPy instead of bytes.

Parameters:

Name Type Description Default
sep bytes | None

a separator

None
maxsplit int

the maximum number of splits

-1

Returns:

Type Description
list[BinaPy]

a BinaPy

Source code in binapy/binapy.py
314
315
316
317
318
319
320
321
322
323
324
325
def split(self, sep: bytes | None = None, maxsplit: int = -1) -> list[BinaPy]:  # type: ignore[override]
    """Override base method to return a BinaPy instead of bytes.

    Args:
        sep: a separator
        maxsplit: the maximum number of splits

    Returns:
        a BinaPy

    """
    return [self.__class__(b) for b in super().split(sep, maxsplit)]

split_at(*pos)

Split this BinaPy at one or more integer positions.

Parameters:

Name Type Description Default
*pos int

indexes where to cut the BinaPy

()

Returns:

Type Description
tuple[BinaPy, ...]

a tuple of len(pos) + 1 instances of BinaPy

Source code in binapy/binapy.py
327
328
329
330
331
332
333
334
335
336
337
338
def split_at(self, *pos: int) -> tuple[BinaPy, ...]:
    """Split this BinaPy at one or more integer positions.

    Args:
        *pos: indexes where to cut the BinaPy

    Returns:
        a tuple of `len(pos) + 1` instances of BinaPy

    """
    spos = sorted(pos)
    return tuple(self.__class__(self[start:end]) for start, end in zip([0, *spos], [*spos, len(self)]))

encode_to(name, *args, **kwargs)

Encode data from this BinaPy according to the format name.

Parameters:

Name Type Description Default
name str

format to use

required
*args Any

additional position parameters for the extension encoder method

()
**kwargs Any

additional keyword parameters for the extension encoder method

{}

Returns:

Type Description
BinaPy

the resulting data

Source code in binapy/binapy.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def encode_to(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
    """Encode data from this BinaPy according to the format `name`.

    Args:
        name: format to use
        *args: additional position parameters for the extension encoder method
        **kwargs: additional keyword parameters for the extension encoder method

    Returns:
        the resulting data

    """
    encoder = self._get_encoder(name)

    return encoder(self, *args, **kwargs)

to(name, *args, **kwargs)

Alias for encode_to().

Parameters:

Name Type Description Default
name str

same as encode_to()

required
*args Any

same as encode_to()

()
**kwargs Any

same as encode_to()

{}

Returns:

Type Description
BinaPy

same as encode_to()

Source code in binapy/binapy.py
414
415
416
417
418
419
420
421
422
423
424
425
426
def to(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
    """Alias for `encode_to()`.

    Args:
        name: same as `encode_to()`
        *args:  same as `encode_to()`
        **kwargs:  same as `encode_to()`

    Returns:
        same as `encode_to()`

    """
    return self.encode_to(name, *args, **kwargs)

decode_from(name, *args, **kwargs)

Decode data from this BinaPy according to the format name.

Parameters:

Name Type Description Default
name str

format name to use

required
*args Any

additional position parameters for the extension decoder method

()
**kwargs Any

additional keyword parameters for the extension decoder method

{}

Returns:

Type Description
BinaPy

the resulting data

Source code in binapy/binapy.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
def decode_from(self, name: str, *args: Any, **kwargs: Any) -> BinaPy:
    """Decode data from this BinaPy according to the format `name`.

    Args:
        name: format name to use
        *args: additional position parameters for the extension decoder method
        **kwargs: additional keyword parameters for the extension decoder method

    Returns:
        the resulting data

    """
    decoder = self._get_decoder(name)

    return decoder(self, *args, **kwargs)

check(name, *, decode=False, raise_on_error=False)

Check that this BinaPy conforms to a given format extension.

Parameters:

Name Type Description Default
name str

the name of the extension to check

required
decode bool

if True, and the given extension does not have a checker method, try to decode this BinaPy using the decoder method to check if that works.

False
raise_on_error bool

if True, exceptions from the checker method, if any, will be raised instead of returning False.

False

Returns:

Type Description
bool

a boolean, that is True if this BinaPy conforms to the given extension format, False otherwise.

Source code in binapy/binapy.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def check(self, name: str, *, decode: bool = False, raise_on_error: bool = False) -> bool:
    """Check that this BinaPy conforms to a given format extension.

    Args:
        name: the name of the extension to check
        decode: if `True`, and the given extension does not have a checker method,
            try to decode this BinaPy using the decoder method to check if that works.
        raise_on_error: if `True`, exceptions from the checker method, if any,
            will be raised instead of returning `False`.

    Returns:
        a boolean, that is True if this BinaPy conforms to the given extension format, False otherwise.

    """
    # raises an exception in case the extension does not exist
    self._get_extension_methods(name)

    try:
        checker = self._get_checker(name)
        try:
            return checker(self)
        except InvalidExtensionMethodError:
            raise
        except Exception as exc:
            if raise_on_error:
                raise exc from exc
            return False
    except NotImplementedError:
        # if checker is not implemented and decode is True, try to decode instead
        if decode:
            decoder = self._get_decoder(name)
            try:
                decoder(self)
            except Exception as exc:
                if raise_on_error:
                    raise exc from exc
                return False
            else:
                return True
        else:
            raise
    return False

check_all(*, decode=False)

Check if this BinaPy conforms to any of the registered format extensions.

Returns:

Type Description
list[str]

a list of format extensions that this BinaPy can be decoded from.

Parameters:

Name Type Description Default
decode bool

if True, for extensions that don't have a checker method, try to decode this BinaPy using the decoder method to check if that works.

False
Source code in binapy/binapy.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def check_all(self, *, decode: bool = False) -> list[str]:
    """Check if this BinaPy conforms to any of the registered format extensions.

    Returns:
         a list of format extensions that this BinaPy can be decoded from.

    Args:
        decode: if `True`, for extensions that don't have a checker method,
            try to decode this BinaPy using the decoder method to check if that works.

    """

    def get_results() -> Iterator[str]:
        for name in self.extensions:
            with suppress(NotImplementedError):
                success = self.check(name, decode=decode)
                if success is True:
                    yield name

    return list(get_results())

parse_from(name, *args, **kwargs)

Parse data from this BinaPy, based on a given format extension.

Parameters:

Name Type Description Default
name str

name of the extension to use

required
*args Any

additional position parameters for the extension decoder method

()
**kwargs Any

additional keyword parameters for the extension decoder method

{}

Returns:

Type Description
Any

the result from parsing this BinaPy

Source code in binapy/binapy.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def parse_from(self, name: str, *args: Any, **kwargs: Any) -> Any:
    """Parse data from this BinaPy, based on a given format extension.

    Args:
        name: name of the extension to use
        *args: additional position parameters for the extension decoder method
        **kwargs: additional keyword parameters for the extension decoder method

    Returns:
        the result from parsing this BinaPy

    """
    parser = self._get_parser(name)

    return parser(self, *args, **kwargs)

serialize_to(name, *args, **kwargs) classmethod

Serialize (dump) data to a BinaPy, based on a given extension format.

Parameters:

Name Type Description Default
name str

name of the extension to use

required
*args Any

additional position parameters for the extension decoder method (which includes the data to serialize)

()
**kwargs Any

additional keyword parameters for the extension decoder method

{}

Returns:

Type Description
BinaPy

a BinaPy, resulting from serialization of the data

Source code in binapy/binapy.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
@classmethod
def serialize_to(cls, name: str, *args: Any, **kwargs: Any) -> BinaPy:
    """Serialize (dump) data to a BinaPy, based on a given extension format.

    Args:
        name: name of the extension to use
        *args: additional position parameters
            for the extension decoder method (which includes the data to serialize)
        **kwargs: additional keyword parameters for the extension decoder method

    Returns:
        a BinaPy, resulting from serialization of the data

    """
    serializer = cls._get_serializer(name)

    return serializer(*args, **kwargs)

register_extension(name, feature, func) classmethod

Register a new feature for the given extension name.

Parameters:

Name Type Description Default
name str

the extension name

required
feature str

name of the feature to register ("encode", "decode", etc.)

required
func Callable[..., Any]

the method implementing the feature

required
Source code in binapy/binapy.py
542
543
544
545
546
547
548
549
550
551
552
553
@classmethod
def register_extension(cls, name: str, feature: str, func: Callable[..., Any]) -> None:
    """Register a new feature for the given extension name.

    Args:
        name: the extension name
        feature: name of the feature to register ("encode", "decode", etc.)
        func: the method implementing the feature

    """
    ext_dict = cls.extensions.setdefault(name, {})
    ext_dict[feature] = func

InvalidExtensionMethodError

Bases: ValueError

Raised when an extension method returns invalid data.

Source code in binapy/binapy.py
559
560
class InvalidExtensionMethodError(ValueError):
    """Raised when an extension method returns invalid data."""

binapy_checker(name)

Declare a new checker for BinaPy.

This is a decorator. Checker checks that some data is valid for a given format/extension.

1
name: name of the extension
1
a method decorator
Usage

The matching checker for the special "double-encoding" we created with binapy_encoder():

1
2
3
4
5
6
7
8
9
from binapy import binapy_checker


@binapy_checker("double")
def double_check(data: bytes) -> bytes:
    return len(data) % 2 == 0 and data[: len(data) // 2] == data[len(data) // 2 :]


assert BinaPy(b"abcabc").check("double")
Source code in binapy/binapy.py
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
def binapy_checker(name: str) -> Callable[[F], F]:
    """Declare a new checker for BinaPy.

    This is a decorator. Checker checks that some data is valid for a given format/extension.

    Args:
    ----
        name: name of the extension

    Returns:
    -------
        a method decorator

    Usage:
        The matching checker for the special "double-encoding" we created with `binapy_encoder()`:

        ```python
        from binapy import binapy_checker


        @binapy_checker("double")
        def double_check(data: bytes) -> bytes:
            return len(data) % 2 == 0 and data[: len(data) // 2] == data[len(data) // 2 :]


        assert BinaPy(b"abcabc").check("double")
        ```

    """

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> bool:
            raw_result = func(*args, **kwargs)

            if not isinstance(raw_result, bool):
                msg = f"extension {name} checker method did not return boolean data"
                raise InvalidExtensionMethodError(msg)
            return raw_result

        BinaPy.register_extension(name, "check", wrapper)
        return cast(F, wrapper)

    return decorator

binapy_decoder(name)

Declare a new decoder for BinaPy.

This is a method decorator. Decoders do convert BinaPy data from a given format into another BinaPy.

1
name: name of the extension
1
a method decorator
Usage

The matching decoder for the special "double-encoding" we created with binapy_encoder():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from binapy import binapy_decoder


@binapy_decoder("double")
def double_decode(data: bytes) -> bytes:
    if len(data) % 2 or data[: len(data) // 2] != data[len(data) // 2 :]:
        raise ValueError("This is not a double-encoded data!")
    return data[: len(data) // 2]


assert BinaPy(b"abcabc").decode_from("double") == b"abc"
Source code in binapy/binapy.py
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
def binapy_decoder(name: str) -> Callable[[F], F]:
    """Declare a new decoder for BinaPy.

    This is a method decorator. Decoders do convert BinaPy data from a given format into another BinaPy.

    Args:
    ----
        name: name of the extension

    Returns:
    -------
        a method decorator

    Usage:
        The matching decoder for the special "double-encoding" we created with `binapy_encoder()`:

        ```python
        from binapy import binapy_decoder


        @binapy_decoder("double")
        def double_decode(data: bytes) -> bytes:
            if len(data) % 2 or data[: len(data) // 2] != data[len(data) // 2 :]:
                raise ValueError("This is not a double-encoded data!")
            return data[: len(data) // 2]


        assert BinaPy(b"abcabc").decode_from("double") == b"abc"
        ```

    """

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> BinaPy:
            raw_result = func(*args, **kwargs)
            if not isinstance(raw_result, (bytes, bytearray)):
                msg = f"extension {name} decoder method did not return binary data"
                raise InvalidExtensionMethodError(msg)
            return BinaPy(raw_result)

        BinaPy.register_extension(name, "decode", wrapper)
        return cast(F, wrapper)

    return decorator

binapy_encoder(name)

Declare new encoders for BinaPy.

This is a method decorator. Encoders do convert a BinaPy into another BinaPy using a given format/extension.

1
name: name of the extension
1
a method decorator
Usage

To declare a special "double-encoding" which just copy the data twice:

1
2
3
4
5
6
7
8
9
from binapy import binapy_encoder


@binapy_encoder("double")
def double_encode(data: bytes) -> bytes:
    return data * 2


assert BinaPy(b"abc").to("double") == b"abcabc"
Source code in binapy/binapy.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
def binapy_encoder(name: str) -> Callable[[F], F]:
    """Declare new encoders for BinaPy.

    This is a method decorator. Encoders do convert a BinaPy into another BinaPy using a given format/extension.

    Args:
    ----
        name: name of the extension

    Returns:
    -------
        a method decorator

    Usage:
        To declare a special "double-encoding" which just copy the data twice:

        ```python
        from binapy import binapy_encoder


        @binapy_encoder("double")
        def double_encode(data: bytes) -> bytes:
            return data * 2


        assert BinaPy(b"abc").to("double") == b"abcabc"
        ```

    """

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> BinaPy:
            raw_result = func(*args, **kwargs)
            if not isinstance(raw_result, (bytes, bytearray, str)):
                msg = f"extension {name} encoder method did not return binary data"
                raise InvalidExtensionMethodError(msg)
            return BinaPy(raw_result)

        BinaPy.register_extension(name, "encode", wrapper)
        return cast(F, wrapper)

    return decorator

binapy_parser(name)

Declare a new parser for BinaPy.

This is a method decorator. Parsers do convert BinaPy into Python objects. For example, a JSON parser will convert a BinaPy into a dict, by parsing the JSON formatted string from a BinaPy.

1
name: the extension name to use
1
a method decorator
Source code in binapy/binapy.py
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def binapy_parser(name: str) -> Callable[[F], F]:
    """Declare a new parser for BinaPy.

    This is a method decorator. Parsers do convert BinaPy into Python objects.
    For example, a JSON parser will convert a BinaPy into a dict, by parsing the JSON formatted string from a BinaPy.

    Args:
    ----
        name: the extension name to use

    Returns:
    -------
        a method decorator

    """

    def decorator(func: Callable[..., Any]) -> Any:
        BinaPy.register_extension(name, "parse", func)
        return func

    return decorator

binapy_serializer(name)

Declare a new serializer for BinaPy.

This is a method decorator. Serializers do convert Python objects into BinaPy. For example, a JSON serializer will convert a Python dict into a serialized JSON value.

1
name: extension name to use
1
a method decorator
Source code in binapy/binapy.py
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
def binapy_serializer(name: str) -> Callable[[F], F]:
    """Declare a new serializer for BinaPy.

    This is a method decorator. Serializers do convert Python objects into BinaPy.
    For example, a JSON serializer will convert a Python `dict` into a serialized JSON value.

    Args:
    ----
        name: extension name to use

    Returns:
    -------
        a method decorator

    """

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> BinaPy:
            raw_result = func(*args, **kwargs)
            if not isinstance(raw_result, (bytes, bytearray)):
                msg = f"extension {name} serializer method did not return binary data"
                raise InvalidExtensionMethodError(msg)
            return BinaPy(raw_result)

        BinaPy.register_extension(name, "serialize", wrapper)
        return cast(F, wrapper)

    return decorator