Formatting#

When you just want to see the data, Tibs, Mutibs, View and MutableView all support Python’s format mini-language, so they work directly in f-strings:

>>> packet = Tibs('0xac804f4b')
>>> f"{packet:#x}"
'0xac804f4b'
>>> f"{packet:_.8b}"
'10101100_10000000_01001111_01001011'
>>> f"{packet:u}"
'2894090059'

With no format spec you get the same thing as str(), which is what you’d see if you just printed it:

>>> f"{packet}"
'0xac804f4b'

Type codes#

There are two families of type code. The first are representation codes, which show you the bits themselves and are exactly equivalent to the bin, oct and hex properties:

  • b — binary. Always available.

  • o — octal. Length must be a multiple of 3.

  • x — hexadecimal. Length must be a multiple of 4.

  • X — upper case hexadecimal. Length must be a multiple of 4.

The second are interpretation codes, which decode the bits as a number. These borrow the letters used by Dtype rather than Python’s d, because a Tibs has both a signed and an unsigned reading and there’s no sensible way to guess which you meant:

  • u — the unsigned integer value, as given by Tibs.u.

  • i — the two’s complement signed value, as given by Tibs.i.

Both families work at any length, though the interpretation codes need at least one bit to interpret.

So the same 32 bits can be shown four different ways:

>>> f"{packet:x}, {packet:b}"
'ac804f4b, 10101100100000000100111101001011'
>>> f"{packet:u}, {packet:i}"
'2894090059, -1400877237'
>>> f"{Tibs('0o7531'):o}"
'7531'

Note

The representation codes are not integer formats, so leading zeros are kept and the length of the output always tells you the length of the data. f"{Tibs('0x0f'):b}" is '00001111', whereas f"{15:b}" is '1111'. If you want the number, ask for the number with u or i.

Prefixes and grouping#

The # flag adds the usual 0x, 0X, 0b or 0o prefix, and _ inserts separators between groups of digits. Both are useful for reading long values, and because the string constructors ignore underscores and understand the prefixes, a value formatted with #, with or without grouping, can be fed straight back in:

>>> f"{packet:#_x}"
'0xac80_4f4b'
>>> Tibs(f"{packet:#_x}") == packet
True

Warning

If you combine # with an alignment, the # must come after it. A # immediately followed by an alignment character is read as a fill character instead, which is easy to miss:

>>> f"{packet:>#12x}"
'  0xac804f4b'
>>> f"{packet:#>12x}"
'####ac804f4b'

This is how the mini-language works for any type, but it bites more often here because a prefix is so often what you want.

Python fixes the group size at four digits. That’s often the wrong size for binary data, so tibs lets you set it using the precision field, which the standard mini-language leaves unused for these types:

>>> f"{packet:_b}"
'1010_1100_1000_0000_0100_1111_0100_1011'
>>> f"{packet:_.8b}"
'10101100_10000000_01001111_01001011'
>>> f"{packet:_.2x}"
'ac_80_4f_4b'

Grouping a u or i value works the way it does for any other Python integer, so you get three-digit groups and , is available as well:

>>> f"{packet:_u}"
'2_894_090_059'
>>> f"{packet:,i}"
'-1,400,877,237'

Width and alignment#

Fill, alignment and width all behave as they do elsewhere in Python, which is handy for lining up columns of registers or packet fields:

>>> for name, value in [('ctrl', Tibs('0x0f')), ('status', Tibs('0xbeef'))]:
...     print(f"{name:>8}  {value:>12x}")
    ctrl            0f
  status          beef

Note

Two details differ from integer formatting, both because a Tibs is a sequence rather than a number.

Groups are counted from bit zero, so it’s the last group that comes up short rather than the first. f"{Tibs('0b101010101'):_b}" gives '1010_1010_1', where the equivalent integer format gives '1_0101_0101'.

Padding is added after grouping and is never itself grouped, so the separators stay lined up with real bit positions.

The fill character has to be something that can’t be mistaken for the data, so zero padding is not available for b, o, x and X:

>>> f"{Tibs('0xf'):#06x}"
Traceback (most recent call last):
...
ValueError: Zero padding is not allowed with the 'x' format type, because the padding could not be told apart from the data and would change its apparent length. Align with '<', '>' or '^' to pad with spaces instead, or use the 'u' or 'i' type code for a numeric interpretation.

Zero padding an integer is harmless, because leading zeros don’t change what an integer is. Here they would: a 4-bit value padded to '0x000f' reads as a 16-bit one, and comes back as a 16-bit one if you feed it in again. The same goes for any other fill that is a valid digit for the type, such as f for hex or 1 for binary. Digits that can’t appear in that base are fine, as is anything else:

>>> f"{Tibs('0xf'):*>6x}"
'*****f'
>>> f"{Tibs('0b1111'):8>6b}"
'881111'

If you want a fixed number of bits rather than a fixed number of characters, change the data rather than its presentation, for example with Tibs.from_u().

A few other things that are meaningful for numbers are also rejected for the representation codes: the sign characters +, - and space, and the , separator. All of them are accepted by u and i, which really are numbers.

Long values#

str() gives up on very long values and shows a truncated version with the length appended, and a format spec with no type code does the same. An explicit type code never truncates:

>>> t = Tibs.from_zeros(50_000)
>>> f"{t}".endswith('... # length=50000')
True
>>> len(f"{t:b}")
50000