pak.packets.packet

Base code for Packets.

exception ReservedFieldError(packet_cls, field)

Bases: Exception

An error indicating a field was defined with a reserved name.

See Packet.RESERVED_FIELDS for more information.

Parameters:
  • packet_cls (subclass of Packet) – The Packet which used a reserved field name.

  • field (str) – The name of the offending field.

exception DuplicateFieldError(packet_cls, field)

Bases: Exception

An error indicating a field was defined twice.

Raised when declaring a field already declared in a parent Packet.

Parameters:
  • packet_cls (subclass of Packet) – The Packet which duplicated a field.

  • field (str) – The name of the field which was duplicated.

class Packet(*, ctx=None, **fields)

Bases: object

A collection of values that can be marshaled to and from raw data using Types.

The difference between a Packet and a Type is that Types only define how to marshal values to and from raw data, while Packets actually contain values themselves.

To unpack a Packet from raw data, you should use the unpack() method instead of the constructor.

Parameters:
  • ctx (Packet.Context) – The context for the Packet.

  • **fields – The names and corresponding values of the fields of the Packet.

Raises:

TypeError – If there are any superfluous keyword arguments.

Examples

Basic functionality:

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> p = MyPacket()
>>> p
MyPacket(attr1=0, attr2=0)
>>> p.pack()
b'\x00\x00\x00'
>>> p = MyPacket(attr1=1, attr2=2)
>>> p.pack()
b'\x01\x02\x00'
>>> MyPacket.unpack(b"\xff\x00\x80")
MyPacket(attr1=-1, attr2=-32768)

Additionally your attributes can be properties:

>>> class MyPacket(pak.Packet):
...     prop: pak.Int8
...     @property
...     def prop(self):
...         return self._prop
...     @prop.setter
...     def prop(self, value):
...         self._prop = value + 1
...
>>> p = MyPacket()
>>> p # Int8's default is 0, plus 1 is 1
MyPacket(prop=1)
>>> p.prop = 2
>>> p
MyPacket(prop=3)
>>> p.pack()
b'\x03'

If an attribute is read only, it will only raise an error if you explicitly try to set it, i.e. you specify the value for it in the constructor.

class Context

Bases: object

The context for a Packet.

Packet.Contexts are used to pass arbitrary data to Packet operations, typically just being wrapped in a Type.Context and sent off to Type operations, such as unpacking and packing.

You should customize this class to suit your own purposes, like so:

>>> import pak
>>> class MyPacket(pak.Packet):
...     class Context(pak.Packet.Context):
...         def __init__(self):
...             self.info = ...
...             super().__init__()
...
...         def __hash__(self):
...             return hash(self.info)
...
...         def __eq__(self, other):
...             return self.info == other.info
...

When no Packet.Context is provided to Packet operations that may accept one, then your subclass is attempted to be default constructed and used instead.

Warning

Subclasses must be properly hashable. Accordingly, this means that subclasses should also be immutable. Therefore, the constructor of Packet.Context sets the constructed object to be immutable.

If a subclass of Packet.Context does not provide its own __hash__ implementation, then a TypeError is raised. Since all hashable objects should also be equality comparable, if a subclass of Packet.Context does not provide its own __eq__ implementation, then a TypeError is raised as well.

classmethod EmptyWithID(id, /)

Generates an empty subclass of the Packet class with the specified ID.

Note

This method is decorated with util.cache().

This means that when called twice with the same ID, then the exact same class will be returned.

Parameters:

id – The ID of the generated Packet.

Returns:

The generated Packet.

Return type:

subclass of Packet

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     class Header(pak.Packet.Header):
...         id: pak.Int8
...
>>> empty = MyPacket.EmptyWithID(1)
>>> empty is MyPacket.EmptyWithID(1)
True
>>> issubclass(empty, MyPacket)
True
>>> packet = empty.unpack(b"any data")
>>> packet
MyPacket.EmptyWithID(1)()
>>> packet.pack()
b'\x01'
classmethod GenericWithID(id, /)

Generates a subclass of the Packet class and GenericPacket with the specified ID.

Note

This method is decorated with util.cache().

This means that when called twice with the same ID, then the exact same class will be returned.

GenericPacket will be inherited from such that its data field will be after all the fields of the parent Packet.

Parameters:

id – The ID of the generated Packet.

Returns:

The generated Packet.

Return type:

subclass of Packet

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     class Header(pak.Packet.Header):
...         id: pak.Int8
...
...     field: pak.Int16
...
>>> generic = MyPacket.GenericWithID(1)
>>> generic is MyPacket.GenericWithID(1)
True
>>> issubclass(generic, MyPacket)
True
>>> issubclass(generic, pak.GenericPacket)
True
>>> packet = generic.unpack(b"\x02\x00generic data")
>>> packet
MyPacket.GenericWithID(1)(field=2, data=bytearray(b'generic data'))
>>> packet.pack()
b'\x01\x02\x00generic data'
class Header(packet=None, /, *, ctx=None, **fields)

Bases: Packet

The header for a Packet.

Must be accessible from the Header attribute of your Packet class, such as MyPacket.Header.

For example:

>>> import pak
>>> class MyPacket(pak.Packet):
...     byte: pak.Int8
...     short: pak.Int16
...     class Header(pak.Packet.Header):
...         size: pak.UInt8
...         byte: pak.Int8
...
>>> MyPacket.Header(MyPacket(byte=1))
MyPacket.Header(size=3, byte=1)
>>> # Alternatively, you may use the 'Packet.header' method
>>> MyPacket(byte=1).header()
MyPacket.Header(size=3, byte=1)

To unpack the header, simply call Packet.unpack() on the header class, like so:

>>> import pak
>>> class MyPacket(pak.Packet):
...     byte: pak.Int8
...     short: pak.Int16
...     class Header(pak.Packet.Header):
...         size: pak.UInt8
...         byte: pak.Int8
...
>>> MyPacket.Header.unpack(b"\x03\x01")
MyPacket.Header(size=3, byte=1)

Note

Subclasses of Packet.Header (i.e. all Packet headers) may not have headers or contexts of their own.

Parameters:
  • packet (Packet or None) –

    The Packet for which the header is for.

    Each field of the header will be acquired from packet. If the corresponding attribute in packet is a method, then it will be called with the ctx keyword argument.

    If not None, then no **fields must be passed.

  • ctx (Packet.Context) – The context for the Packet.

  • **fields

    The names and corresponding values of the fields of the Packet.

    If packet is not None, then no fields may be passed.

Raises:

TypeError – If packet is not None and any **fields are passed.

pack(*, ctx=None)

Overrides Packet.pack() to call Packet.pack_without_header() to avoid infinite recursion when packing.

RESERVED_FIELDS = ['ctx']
copy(**new_attrs)

Makes a mutable copy of the Packet.

See also

immutable_copy()

Parameters:

**new_attrs – The new attributes to set on the copy.

Returns:

The copied Packet.

Return type:

Packet

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     field: pak.Int8
...
>>> orig = MyPacket(field=1)
>>> copy = orig.copy()
>>> copy == orig
True
>>> copy is not orig
True
>>>
>>> # Attributes that aren't fields will also be copied:
>>> orig = MyPacket(field=2)
>>> orig.custom_attr = "custom"
>>> copy = orig.copy()
>>> copy == orig
True
>>> copy.custom_attr
'custom'
>>>
>>> # The copy can be mutated:
>>> copy.field = 3
>>> copy
MyPacket(field=3)
>>>
>>> # You can set new fields on the copy by passing keyword arguments:
>>> orig.copy(field=4)
MyPacket(field=4)
classmethod enumerate_field_types()

Enumerates the Types of the fields of the Packet.

Returns:

Each element of the iterable is a (field_name, field_type) pair.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> for attr, attr_type in MyPacket.enumerate_field_types():
...     print(f"{attr}: {attr_type.__qualname__}")
...
attr1: Int8
attr2: Int16
enumerate_field_types_and_values()

Enumerates the Types and values of the fields of the Packet.

Returns:

Each element of the iterable is a (field_name, field_type, field_value) triplet.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> p = MyPacket(attr1=1, attr2=2)
>>> for attr, attr_type, value in p.enumerate_field_types_and_values():
...     print(f"{attr}: {attr_type.__qualname__}; {value}")
...
attr1: Int8; 1
attr2: Int16; 2
enumerate_field_values()

Enumerates the values of the fields of the Packet.

Returns:

Each element of the iterable is a (field_name, field_value) pair.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> p = MyPacket(attr1=1, attr2=2)
>>> for attr, value in p.enumerate_field_values():
...     print(f"{attr}: {value}")
...
attr1: 1
attr2: 2
classmethod field_names()

Gets the names of the fields of the Packet.

Returns:

Each element is the name of a field.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> for name in MyPacket.field_names():
...     print(name)
...
attr1
attr2
classmethod field_types()

Gets the Types of each field of the Packet.

Returns:

Each element is the Type of a field.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> for type in MyPacket.field_types():
...     print(type.__qualname__)
...
Int8
Int16
field_types_and_values()

Gets the Types and values of each field of the Packet.

Returns:

Each element is a (field_type, field_value) pair.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> p = MyPacket(attr1=1, attr2=2)
>>> for type, value in p.field_types_and_values():
...     print(f"{type.__qualname__}; {value}")
...
Int8; 1
Int16; 2
field_values()

Gets the values of each field of the Packet.

Returns:

Each element is the value of a field.

Return type:

iterable

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     attr1: pak.Int8
...     attr2: pak.Int16
...
>>> p = MyPacket(attr1=1, attr2=2)
>>> for value in p.field_values():
...     print(value)
...
1
2
classmethod has_field(name)

Gets whether the Packet has a certain field.

Parameters:

name (str) – The name of the field to check for.

Returns:

Whether the Packet has a field with the specified name.

Return type:

bool

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     defined_field: pak.UInt8
...
>>> MyPacket.has_field("defined_field")
True
>>> MyPacket.has_field("undefined_field")
False
header(*, ctx=None)

Gets the Packet.Header for the Packet.

Parameters:

ctx (Packet.Context) – The context for the Packet.

Returns:

The header for the Packet.

Return type:

Packet.Header

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     id = 1
...     class Header(pak.Packet.Header):
...         id: pak.UInt8
...
>>> MyPacket().header()
MyPacket.Header(id=1)
classmethod id(*, ctx=None)

Gets the ID of the Packet.

Lots of packet protocols specify Packets with an ID to determine what type of Packet should be read. Unless overridden, Packet has no meaningful ID.

When overriding this method with a classmethod, you do not need to default the ctx parameter, you can just have it like so:

class MyPacket(pak.Packet):
    @classmethod
    def id(cls, *, ctx):
        ...

The case where no ctx is specified when calling this method will be handled for you.

If the id attribute of a subclass is enrolled in the DynamicValue machinery, then its dynamic value is returned from this function. Otherwise the value of the id attribute is returned.

Note

The ID of a Packet is not checked for equality between Packets.

Warning

The ID of a Packet must be both hashable and equality comparable for various facilities involving IDs to work correctly.

Many protocols have a Packet.Header prefixing each Packet with just an ID. To model this common protocol, one can do something like this:

>>> import pak
>>> class MyPacket(pak.Packet):
...     id = 3
...     array: pak.Int16[2]
...     class Header(pak.Packet.Header):
...         id: pak.Int8
...
>>> MyPacket(array=[1, 2]).pack()
b'\x03\x01\x00\x02\x00'
Parameters:

ctx (Packet.Context) – The context for the Packet.

Returns:

By default returns None, and if the ID is None then the Packet shouldn’t be considered when looking up Packets from their ID.

Otherwise the ID of the Packet.

Return type:

any

immutable_copy(**new_attrs)

Makes an immutable copy of the Packet.

See also

copy()

Parameters:

**new_attrs – The new attributes to set on the copy.

Returns:

The copied, immutable Packet.

Return type:

Packet

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     field: pak.Int8
...
>>> orig = MyPacket(field=1)
>>> copy = orig.immutable_copy()
>>> copy == orig
True
>>> copy is not orig
True
>>>
>>> # Attributes that aren't fields will also be copied:
>>> orig = MyPacket(field=2)
>>> orig.custom_attr = "custom"
>>> copy = orig.immutable_copy()
>>> copy == orig
True
>>> copy is not orig
True
>>> copy.custom_attr
'custom'
>>>
>>> # You cannot modify the copy:
>>> copy.field = 3
Traceback (most recent call last):
...
AttributeError: This 'MyPacket' instance has been made immutable
>>>
>>> # You can however set fields that the immutable copy will have:
>>> copy = orig.immutable_copy(field=3)
>>> copy
MyPacket(field=3)
>>> # This copy is still immutable:
>>> copy.field = 4
Traceback (most recent call last):
...
AttributeError: This 'MyPacket' instance has been made immutable
make_immutable()

Makes the Packet immutable.

This may be useful for situations where e.g. you pass the same Packet instance to several functions and want to make sure that those functions don’t change the fields of the Packet for the other functions being passed the instance. For example if you had:

import pak

class MyPacket(pak.Packet):
    field: pak.Int8

def foo(packet):
    packet.field = 2

def bar(packet):
    assert packet.field == 1

p = MyPacket(field=1)

foo(p)
bar(p)

Then the assert within bar would fail, since foo changed p.field to 2. If p is made immutable however, an error will be raised in foo, preventing any hard-to-track-down errors.

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     field: pak.Int8
...
>>> p = MyPacket(field=1)
>>> p.field = 2
>>> p
MyPacket(field=2)
>>> p.make_immutable()
>>> p.field = 3
Traceback (most recent call last):
...
AttributeError: This 'MyPacket' instance has been made immutable
pack(*, ctx=None)

Packs a Packet to raw data.

Note

This does pack the Packet.Header, unlike unpack().

First the Packet.Header is gotten from header(), then it is packed, and then pack_without_header() is called.

Parameters:

ctx (Packet.Context) – The context for the Packet.

Returns:

The raw data marshaled from the Packet.

Return type:

bytes

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     id = 0xFF
...     array: pak.UInt8[pak.UInt8]
...     class Header(pak.Packet.Header):
...         id: pak.UInt8
...
>>> p = MyPacket(array=[0, 1, 2, 3])
>>> p.pack()
b'\xff\x04\x00\x01\x02\x03'
pack_without_header(*, ctx=None)

Packs a Packet to raw data, excluding the Packet.Header.

Parameters:

ctx (Packet.Context) – The context for the Packet.

Returns:

The raw data marshaled from the Packet, excluding the header.

Return type:

bytes

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     id = 0xFF
...     array: pak.UInt8[pak.UInt8]
...     class Header(pak.Packet.Header):
...         id: pak.UInt8
...
>>> p = MyPacket(array=[0, 1, 2, 3])
>>> p.pack_without_header()
b'\x04\x00\x01\x02\x03'
size(*, ctx=None)

Gets the cumulative size of the fields of the Packet.

This may be called either as a classmethod or as an instance method. When called as a classmethod, the static size of the Packet is attempted to be calculated, irrespective of any values. When called as an instance method, then the values of the fields of the Packet are used to calculate the size.

Note

The header is not included in the size.

Parameters:

ctx (Packet.Context) – The context for the Packet.

Returns:

The cumulative size of the fields of the Packet.

Return type:

int

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     array:   pak.Int16[2]
...     float64: pak.Float64
...
>>> MyPacket().size()
12
>>> MyPacket.size()
12
classmethod subclass_with_id(id, /, *, ctx=None)

Gets the subclass with the equivalent ID.

Parameters:
Returns:

If None, then there is no Packet whose ID is id. Otherwise returns the appropriate subclass.

Return type:

subclass of Packet or None

classmethod subclasses()

Gets the recursive subclasses of the Packet.

Useful for when you have categories of Packets, such as serverbound and clientbound, and so you can have an empty class like

class ServerboundPacket(Packet):
    pass

which all serverbound Packets would inherit from, and then use subclasses() to automatically get all the serverbound Packets.

Note

This method is decorated with util.cache(). In particular this means you can’t generate a new subclass after calling this and have it be returned from subclasses() the next time you call it.

Returns:

The recursive subclasses of the Packet.

Return type:

frozenset

type_ctx(ctx)

Converts a Packet.Context to a Type.Context.

Parameters:

ctx (Packet.Context or None) – The context for the Packet.

Returns:

The context for a Type.

Return type:

Type.Context

classmethod unpack(buf, *, ctx=None)

Unpacks a Packet from raw data.

Note

This doesn’t unpack the header, as you often need to unpack the Packet.Header to determine the correct Packet to unpack in the first place, let alone other aspects of the data.

This method will call the Type.unpack() method on the fields of the Packet.

See also

unpack_async()

Parameters:
  • buf (file object or bytes or bytearray) – The buffer containing the raw data.

  • ctx (Packet.Context) – The context for the Packet.

Returns:

The Packet marshaled from the raw data.

Return type:

Packet

Examples

>>> import pak
>>> class MyPacket(pak.Packet):
...     hello: pak.RawByte[5]
...     world: pak.RawByte[5]
...
>>> MyPacket.unpack(b"HelloWorld")
MyPacket(hello=bytearray(b'Hello'), world=bytearray(b'World'))
async classmethod unpack_async(reader, *, ctx=None)

Asynchronously unpacks a Packet from raw data.

Note

This doesn’t unpack the header, as you often need to unpack the Packet.Header to determine the correct Packet to unpack in the first place, let alone other aspects of the data.

This method will call the Type.unpack_async() method on the fields of the Packet.

See also

unpack()

Parameters:
  • reader (asyncio.StreamReader or bytes or bytearray) –

    The stream of data to unpack from.

    Warning

    Nothing else should read from reader until this coroutine finishes executing.

    If bytes or bytearray, then reader is turned into an io.ByteStreamReader.

  • ctx (Packet.Context) – The context for the Packet.

Returns:

The Packet marshaled from the raw data.

Return type:

Packet

class GenericPacket(*, ctx=None, **fields)

Bases: Packet

A generic collection of data.

Reads all of the data in the buffer passed to it.

data: RawByte[None]